Attributs divers interprétés par le compilateur C#

Il existe plusieurs attributs que vous pouvez appliquer à des éléments de votre code qui y ajoutent une signification sémantique :

  • Conditional : rendre l’exécution d’une méthode dépendante d’un identificateur de prétraitement.
  • Obsolete : marquer un type ou membre pour une suppression possible (possible).
  • AttributeUsage : déclarer les éléments d’un langage où vous pouvez appliquer un attribut.
  • AsyncMethodBuilder : déclarer un type de générateur de méthode asynchrone.
  • InterpolatedStringHandler : définir un générateur de chaîne interpolée pour un scénario connu.
  • ModuleInitializer : déclarer une méthode qui initialise un module.
  • SkipLocalsInit : omettre le code qui initialise un stockage de variable local sur 0.
  • UnscopedRef : déclarer qu’une variable ref normalement interprétée comme scoped doit être traitée comme non délimitée.
  • OverloadResolutionPriority : Ajoutez un attribut de départage pour influencer la résolution de surcharge en cas de surcharges potentiellement ambiguës.
  • Experimental : marquer un type ou membre comme expérimental.

Le compilateur utilise ces significations sémantiques pour modifier sa sortie et signaler les erreurs possibles par les développeurs qui utilisent votre code.

Attribut Conditional

Avec l’attribut Conditional, l’exécution d’une méthode dépend d’un identificateur de prétraitement. L’attribut Conditional est un alias pour ConditionalAttribute, et peut être appliqué à une méthode ou une classe d’attributs.

Dans l’exemple suivant, Conditional est appliqué à une méthode pour activer ou désactiver l’affichage d’informations de diagnostic spécifiques au programme :

#define TRACE_ON
using System.Diagnostics;

namespace AttributeExamples;

public class Trace
{
    [Conditional("TRACE_ON")]
    public static void Msg(string msg)
    {
        Console.WriteLine(msg);
    }
}

public class TraceExample
{
    public static void Main()
    {
        Trace.Msg("Now in Main...");
        Console.WriteLine("Done.");
    }
}

Si l’identificateur TRACE_ON n’est pas défini, la sortie de trace n’est pas affichée. Explorez par vous-même dans la fenêtre interactive.

L’attribut Conditional est souvent utilisé avec l’identificateur DEBUG pour activer des fonctionnalités de traçage et de journalisation pour les builds de débogage, mais pas pour les versions Release, comme le montre l’exemple suivant :

[Conditional("DEBUG")]
static void DebugMethod()
{
}

Quand une méthode marquée comme conditionnelle est appelée, la présence ou l’absence du symbole de prétraitement spécifié détermine si le compilateur inclut ou omet l’appel à la méthode. Si le symbole est défini, l’appel est inclus ; sinon, il est omis. Une méthode conditionnelle doit figurer dans une déclaration de classe ou de struct et doit avoir une type de retour void. Conditional offre une solution à la fois plus efficace, plus élégante et moins sujette aux erreurs que les méthodes englobantes contenues dans les blocs #if…#endif.

Si une méthode a plusieurs attributs Conditional, le compilateur inclut des appels à la méthode si un ou plusieurs symboles conditionnels sont définis (les symboles sont liés logiquement à l’aide de l’opérateur OR). Dans l’exemple suivant, la présence de résultats A ou B entraîne un appel de méthode :

[Conditional("A"), Conditional("B")]
static void DoIfAorB()
{
    // ...
}

Utilisation de Conditional avec des classes d’attributs

L’attribut Conditional peut également être appliqué à une définition de classe d’attributs. Dans l’exemple suivant, l’attribut personnalisé Documentation ajoute des informations aux métadonnées si DEBUG est défini.

[Conditional("DEBUG")]
public class DocumentationAttribute : System.Attribute
{
    string text;

    public DocumentationAttribute(string text)
    {
        this.text = text;
    }
}

class SampleClass
{
    // This attribute will only be included if DEBUG is defined.
    [Documentation("This method displays an integer.")]
    static void DoWork(int i)
    {
        System.Console.WriteLine(i.ToString());
    }
}

Attribut Obsolete

L'attribut Obsolete marque un élément de code comme n’est plus recommandé pour une utilisation. L’utilisation d’une entité marquée comme obsolète génère un avertissement ou une erreur. Obsolete est un attribut à usage unique et peut être appliqué à toute entité qui autorise des attributs. Obsolete est un alias pour ObsoleteAttribute.

Dans l’exemple suivant, l’attribut Obsolete est appliqué à la classe A et à la méthode B.OldMethod. Comme le deuxième argument du constructeur d’attribut appliqué à B.OldMethod a la valeur true, l’utilisation de cette méthode entraîne une erreur du compilateur, alors que l’utilisation de la classe A n’entraîne qu’un avertissement. Toutefois, l’appel de B.NewMethod ne produit aucun avertissement ni aucune erreur. Par exemple, utilisé avec les définitions précédentes, le code suivant génère deux avertissements et une erreur :


namespace AttributeExamples
{
    [Obsolete("use class B")]
    public class A
    {
        public void Method() { }
    }

    public class B
    {
        [Obsolete("use NewMethod", true)]
        public void OldMethod() { }

        public void NewMethod() { }
    }

    public static class ObsoleteProgram
    {
        public static void Main()
        {
            // Generates 2 warnings:
            A a = new A();

            // Generate no errors or warnings:
            B b = new B();
            b.NewMethod();

            // Generates an error, compilation fails.
            // b.OldMethod();
        }
    }
}

La chaîne fournie comme premier argument au constructeur d’attribut est affichée dans l’avertissement ou dans l’erreur. Deux avertissements pour la classe A sont générés : un pour la déclaration de la référence de classe et un pour le constructeur de classe. L’attribut Obsolete peut être utilisé sans arguments, mais il est recommandé d’inclure une explication indiquant quoi utiliser en remplacement.

En C# 10, vous pouvez utiliser l’interpolation de chaîne constante et l’opérateur nameof pour vous assurer que les noms correspondent :

public class B
{
    [Obsolete($"use {nameof(NewMethod)} instead", true)]
    public void OldMethod() { }

    public void NewMethod() { }
}

Attribut Experimental

À compter de C# 12, les types, les méthodes et les assemblys peuvent être marqués avec System.Diagnostics.CodeAnalysis.ExperimentalAttribute pour indiquer une fonctionnalité expérimentale. Le compilateur émet un avertissement si vous accédez à une méthode ou un type annoté de ExperimentalAttribute. Tous les types déclarés dans un assembly ou un module marqués avec l’attribut Experimental sont expérimentaux. Le compilateur émet un avertissement si vous accédez à l’un d’entre eux. Vous pouvez désactiver ces avertissements pour piloter une fonctionnalité expérimentale.

Avertissement

Les fonctionnalités expérimentales sont soumises à des modifications. Les API peuvent changer, ou elles peuvent être supprimées au cours des prochaines mises à jour. L’inclusion de fonctionnalités expérimentales est un moyen pour les auteurs de bibliothèques d’obtenir des commentaires sur les idées et les concepts pour le développement futur. Utilisez une prudence extrême lors de l’utilisation d’une fonctionnalité marquée comme expérimentale.

Vous pouvez en savoir plus sur l’attribut Experimental dans la spécification de fonctionnalité.

Attribut SetsRequiredMembers

L’attribut SetsRequiredMembers informe le compilateur qu’un constructeur définit tous les membres required de cette classe ou de ce struct. Le compilateur suppose que tout constructeur avec l’attribut System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute initialise tous les membres required. Tout code qui appelle un tel constructeur n’a pas besoin d’initialiseurs d’objet pour définir les membres requis. L’ajout de l’attribut SetsRequiredMembers est principalement utile pour les enregistrements positionnels et les constructeurs principaux.

Attribut AttributeUsage

L’attribut AttributeUsage détermine de quelle manière une classe d’attributs personnalisés peut être utilisée. AttributeUsageAttribute est un attribut que vous appliquez à des définitions d’attribut personnalisé. L’attribut AttributeUsage vous permet de contrôler :

  • À quels éléments de programme les attributs peuvent être appliqués. Sauf si vous en limitez l’utilisation, un attribut peut être appliqué aux éléments de programme suivants :
    • Assembly
    • Module
    • Champ
    • Événement
    • Method
    • Paramètre
    • Propriété
    • Renvoie
    • Type
  • Si un attribut peut être appliqué plusieurs fois à un même élément de programme.
  • Si les classes dérivées héritent des attributs.

L’exemple suivant montre des paramètres par défaut quand ils sont appliqués explicitement :

[AttributeUsage(AttributeTargets.All,
                   AllowMultiple = false,
                   Inherited = true)]
class NewAttribute : Attribute { }

Dans cet exemple, la classe NewAttribute peut être appliquée à n’importe quel élément de programme pris en charge. Elle ne peut cependant être appliquée qu’une seule fois à chaque entité. Les classes dérivées héritent de l’attribut quand il est appliqué à une classe de base.

Les arguments AllowMultiple et Inherited étant facultatifs, le code suivant a le même effet :

[AttributeUsage(AttributeTargets.All)]
class NewAttribute : Attribute { }

Le premier argument AttributeUsageAttribute doit correspondre à un ou plusieurs éléments de l’énumération AttributeTargets. Vous pouvez lier ensemble plusieurs types de cibles avec l’opérateur OR, comme dans l’exemple suivant :

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
class NewPropertyOrFieldAttribute : Attribute { }

Les attributs peuvent être appliqués à la propriété ou au champ de stockage d’une propriété implémentée automatiquement. L’attribut s’applique à la propriété, sauf si vous spécifiez le spécificateur field sur l’attribut. Les deux cas sont illustrés dans l’exemple suivant :

class MyClass
{
    // Attribute attached to property:
    [NewPropertyOrField]
    public string Name { get; set; } = string.Empty;

    // Attribute attached to backing field:
    [field: NewPropertyOrField]
    public string Description { get; set; } = string.Empty;
}

Si l’argument AllowMultiple est défini sur true, l’attribut résultant peut être appliqué plusieurs fois à une même entité, comme illustré dans l’exemple suivant :

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
class MultiUse : Attribute { }

[MultiUse]
[MultiUse]
class Class1 { }

[MultiUse, MultiUse]
class Class2 { }

Dans ce cas, MultiUseAttribute peut être appliqué à plusieurs reprises, car AllowMultiple est défini sur true. Les deux formats indiqués pour appliquer plusieurs attributs sont valides.

Si Inherited est false, les classes dérivées n’héritent pas de l’attribut d’une classe avec attributs. Par exemple :

[AttributeUsage(AttributeTargets.Class, Inherited = false)]
class NonInheritedAttribute : Attribute { }

[NonInherited]
class BClass { }

class DClass : BClass { }

Dans ce cas, NonInheritedAttribute n’est pas appliqué à DClass via l’héritage.

Vous pouvez également utiliser ces mots clés pour spécifier où un attribut doit être appliqué. Par exemple, vous pouvez utiliser le field: spécificateur pour ajouter un attribut au champ de stockage d’une propriété implémentée automatiquement. Vous pouvez également utiliser le spécificateur field:, property:ou param: pour appliquer un attribut à l’un des éléments générés à partir d’un enregistrement positionnel. Pour obtenir un exemple, consultez Syntaxe positionnelle pour la définition de propriété.

Attribut AsyncMethodBuilder

Vous ajoutez l’attribut System.Runtime.CompilerServices.AsyncMethodBuilderAttribute à un type qui peut être un type de retour asynchrone. L’attribut spécifie le type qui génère l’implémentation de la méthode asynchrone lorsque le type spécifié est retourné à partir d’une méthode asynchrone. L’attribut AsyncMethodBuilder peut être appliqué à un type qui :

Le constructeur de l’attribut AsyncMethodBuilder spécifie le type du générateur associé. Le générateur doit implémenter les membres accessibles suivants :

  • Méthode statique Create() qui retourne le type du générateur.

  • Propriété lisible Task qui renvoie le type de retour asynchrone.

  • Méthode void SetException(Exception) qui définit l’exception en cas d’erreur d’une tâche.

  • Méthode void SetResult() ou void SetResult(T result) qui marque la tâche comme terminée et définit éventuellement le résultat de la tâche

  • Méthode Start avec la signature d’API suivante :

    void Start<TStateMachine>(ref TStateMachine stateMachine)
              where TStateMachine : System.Runtime.CompilerServices.IAsyncStateMachine
    
  • Méthode AwaitOnCompleted avec la signature suivante :

    public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : System.Runtime.CompilerServices.INotifyCompletion
        where TStateMachine : System.Runtime.CompilerServices.IAsyncStateMachine
    
  • Méthode AwaitUnsafeOnCompleted avec la signature suivante :

          public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
              where TAwaiter : System.Runtime.CompilerServices.ICriticalNotifyCompletion
              where TStateMachine : System.Runtime.CompilerServices.IAsyncStateMachine
    

Vous pouvez en savoir plus sur les générateurs de méthodes asynchrones en lisant les générateurs suivants fournis par .NET :

Dans C# 10 et versions ultérieures, l’attribut AsyncMethodBuilder peut être appliqué à une méthode asynchrone pour remplacer le générateur pour ce type.

Attributs InterpolatedStringHandler et InterpolatedStringHandlerArguments

À compter de C# 10, utilisez ces attributs pour spécifier qu’un type est un gestionnaire de chaîne interpolé. La bibliothèque .NET 6 inclut déjà System.Runtime.CompilerServices.DefaultInterpolatedStringHandler pour les scénarios où vous utilisez une chaîne interpolée comme argument pour un paramètre string. Vous pouvez avoir d’autres instances pour lesquelles vous souhaitez contrôler la façon dont les chaînes interpolées sont traitées. Vous appliquez System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute au type qui implémente votre gestionnaire. Vous appliquez System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute aux paramètres du constructeur de ce type.

Vous pouvez en savoir plus sur la création d’un gestionnaire de chaînes interpolées dans la spécification de fonctionnalité C# 10 pour les améliorations de chaîne interpolée.

Attribut ModuleInitializer

L’attribut ModuleInitializer marque une méthode que le runtime appelle lorsque l’assembly se charge. ModuleInitializer est un alias pour ModuleInitializerAttribute.

L’attribut ModuleInitializer ne peut être appliqué qu’à une méthode qui :

  • Est statique.
  • Est sans paramètre.
  • Retourne void.
  • Est accessible à partir du module conteneur, c’est à dire, internal ou public.
  • N’est pas une méthode générique.
  • N’est pas contenue dans une classe générique.
  • N’est pas une fonction locale.

L’attribut ModuleInitializer peut être appliqué à plusieurs méthodes. Dans ce cas, l’ordre dans lequel le runtime les appelle est déterministe, mais pas spécifié.

L’exemple suivant illustre l’utilisation de plusieurs méthodes d’initialiseur de module. Les méthodes Init1 et Init2 s’exécutent avant Main et chacune ajoute une chaîne à la propriété Text. Par conséquent, lors de l’exécution de Main, la propriété Text a déjà des chaînes provenant des deux méthodes d’initialiseur.

using System;

internal class ModuleInitializerExampleMain
{
    public static void Main()
    {
        Console.WriteLine(ModuleInitializerExampleModule.Text);
        //output: Hello from Init1! Hello from Init2!
    }
}
using System.Runtime.CompilerServices;

internal class ModuleInitializerExampleModule
{
    public static string? Text { get; set; }

    [ModuleInitializer]
    public static void Init1()
    {
        Text += "Hello from Init1! ";
    }

    [ModuleInitializer]
    public static void Init2()
    {
        Text += "Hello from Init2! ";
    }
}

Les générateurs de code source doivent parfois générer du code d’initialisation. Les initialiseurs de module fournissent un emplacement standard pour ce code. Dans la plupart des autres cas, vous devez écrire un constructeur statique au lieu d’un initialiseur de module.

Attribut SkipLocalsInit

L’attribut SkipLocalsInit empêche le compilateur de définir l’indicateur .locals init lors de l’émission en métadonnées. L’attribut SkipLocalsInit est un attribut à usage unique qui peut être appliqué à une méthode, une propriété, une classe, un struct, une interface ou un module, mais pas à un assembly. SkipLocalsInit est un alias pour SkipLocalsInitAttribute.

L’indicateur .locals init fait que le CLR initialise toutes les variables locales déclarées dans une méthode sur leurs valeurs par défaut. Étant donné que le compilateur s’assure également que vous n’utilisez jamais une variable avant de lui attribuer une valeur, .locals init n’est généralement pas nécessaire. Toutefois, l’initialisation zéro supplémentaire peut avoir un impact mesurable sur les performances dans certains scénarios, par exemple, lorsque vous utilisez stackalloc pour allouer un tableau sur la pile. Dans ces cas, vous pouvez ajouter l’attribut SkipLocalsInit. S’il est appliqué directement à une méthode, l’attribut affecte cette méthode et toutes ses fonctions imbriquées, y compris les fonctions lambda et locales. S’il est appliqué à un type ou à un module, il affecte toutes les méthodes imbriquées à l’intérieur. Cet attribut n’affecte pas les méthodes abstraites, mais il affecte le code généré pour l’implémentation.

Cet attribut nécessite l’option de compilateur AllowUnsafeBlocks. Cette exigence signale que dans certains cas, le code peut afficher la mémoire non affectée (par exemple, la lecture à partir de la mémoire allouée à la pile non initialisée).

L’exemple suivant illustre l’effet de l’attribut SkipLocalsInit sur une méthode qui utilise stackalloc. La méthode affiche ce qui était en mémoire lorsque le tableau d’entiers a été alloué.

[SkipLocalsInit]
static void ReadUninitializedMemory()
{
    Span<int> numbers = stackalloc int[120];
    for (int i = 0; i < 120; i++)
    {
        Console.WriteLine(numbers[i]);
    }
}
// output depends on initial contents of memory, for example:
//0
//0
//0
//168
//0
//-1271631451
//32767
//38
//0
//0
//0
//38
// Remaining rows omitted for brevity.

Pour essayer ce code vous-même, définissez l’option du compilateur AllowUnsafeBlocks dans votre fichier .csproj :

<PropertyGroup>
  ...
  <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

Attribut UnscopedRef

L’attribut UnscopedRef marque une déclaration de variable comme non délimitée, ce qui signifie que la référence est autorisée à être placée dans une séquence d’échappement.

Vous ajoutez cet attribut où le compilateur traite un ref comme scoped implicitement :

  • Paramètre this pour les méthodes d’instance struct.
  • Paramètres ref qui font référence aux types ref struct.
  • Paramètres out.

L’application de System.Diagnostics.CodeAnalysis.UnscopedRefAttribute marque l’élément comme non délimité.

Attribut OverloadResolutionPriority

Le OverloadResolutionPriorityAttribute permet aux auteurs de bibliothèques de privilégier une surcharge par rapport à une autre lorsque deux surcharges peuvent être ambiguës. Son principal cas d’utilisation est pour les auteurs de bibliothèques qui souhaitent écrire des surcharges offrant de meilleures performances tout en continuant de prendre en charge le code existant sans rupture.

Par exemple, vous pourriez ajouter une nouvelle surcharge qui utilise ReadOnlySpan<T> pour réduire les allocations de mémoire :

[OverloadResolutionPriority(1)]
public void M(params ReadOnlySpan<int> s) => Console.WriteLine("Span");
// Default overload resolution priority of 0
public void M(params int[] a) => Console.WriteLine("Array");

La résolution de surcharge considère les deux méthodes comme équivalentes pour certains types d’arguments. Pour un argument de int[], elle privilégie la première surcharge. Pour que le compilateur privilégie la version ReadOnlySpan, vous pouvez augmenter la priorité de cette surcharge. L’exemple suivant montre l’effet de l’ajout de l’attribut :

var d = new OverloadExample();
int[] arr = [1, 2, 3];
d.M(1, 2, 3, 4); // Prints "Span"
d.M(arr); // Prints "Span" when PriorityAttribute is applied
d.M([1, 2, 3, 4]); // Prints "Span"
d.M(1, 2, 3, 4); // Prints "Span"

Toutes les surcharges avec une priorité inférieure à la priorité de surcharge la plus élevée sont supprimées de l’ensemble des méthodes applicables. Les méthodes sans cet attribut ont une priorité de surcharge définie par défaut à zéro. Les auteurs de bibliothèques devraient utiliser cet attribut en dernier recours lors de l’ajout d’une nouvelle surcharge plus performante. Les auteurs de bibliothèques devraient avoir une compréhension approfondie de la manière dont la résolution de surcharge impacte le choix de la meilleure méthode. Sinon, des erreurs inattendues peuvent se produire.

Voir aussi