Définir et lire des attributs personnalisés

Les attributs fournissent un moyen permettant d’associer des informations au code de manière déclarative. Ils peuvent également fournir un élément réutilisable qui peut être appliqué à diverses cibles. Prenons l’exemple d’ObsoleteAttribute. Vous pouvez l’appliquer aux classes, structures, méthodes, constructeurs et bien plus encore. Il déclare que l’élément est obsolète. Il revient ensuite au compilateur C# de rechercher cet attribut et d’effectuer des actions en réponse.

Dans ce tutoriel, vous découvrez comment ajouter des attributs à votre code, comment créer et utiliser vos propres attributs et comment utiliser des attributs qui sont intégrés à .NET.

Prérequis

Vous devez configurer votre ordinateur pour exécuter .NET. Vous trouverez les instructions d’installation dans la page Téléchargements .NET. Vous pouvez exécuter cette application sur Windows, Ubuntu Linux, macOS ou dans un conteneur Docker. Vous devez installer l’éditeur de code de votre choix. Les descriptions suivantes utilisent Visual Studio Code, un éditeur multiplateforme open source. Cependant, vous pouvez utiliser les outils avec lesquels vous êtes le plus à l’aise.

Créer l’application

Maintenant que vous avez installé tous les outils, créez une application console .NET. Pour utiliser le générateur de ligne de commande, exécutez la commande suivante dans votre interpréteur de commandes préféré :

dotnet new console

Cette commande crée des fichiers projet .NET simples. Exécutez dotnet restore pour restaurer les dépendances nécessaires à la compilation de ce projet.

Vous n’avez pas besoin d’exécuter dotnet restore, car il est exécuté implicitement par toutes les commandes qui nécessitent une restauration pour se produire, comme dotnet new, dotnet build, dotnet run, dotnet test, dotnet publish et dotnet pack. Pour désactiver la restauration implicite, utilisez l’option --no-restore .

La commande dotnet restore est toujours utile dans certains scénarios où la restauration explicite est logique, comme les builds d’intégration continue dans Azure DevOps Services ou dans les systèmes de génération qui doivent contrôler explicitement le moment où la restauration se produit.

Pour plus d’informations sur la gestion des flux NuGet, consultez la documentation de dotnet restore.

Pour exécuter le programme, utilisez dotnet run. Vous devriez voir le résultat dans la « Hello, World » dans la console.

Ajouter des attributs au code

En C#, les attributs sont des classes qui héritent de la classe de base Attribute. Toute classe qui hérite de Attribute peut être utilisée comme une sorte de « balise » sur les autres éléments de code. Par exemple, il existe un attribut appelé ObsoleteAttribute. Cet attribut signale que le code est obsolète et ne doit plus être utilisé. Vous pouvez placer cet attribut sur une classe, par exemple, en utilisant des crochets.

[Obsolete]
public class MyClass
{
}

Bien que classe soit nommée ObsoleteAttribute, il vous suffit d’utiliser [Obsolete] dans le code. La majorité du code C# suit cette convention. Vous pouvez utiliser le nom complet [ObsoleteAttribute] si vous le souhaitez.

Lorsque vous marquez une classe comme étant obsolète, il est judicieux de fournir des informations indiquant pourquoi elle est obsolète, et/ou quelle classe utiliser à la place. Vous incluez un paramètre de chaîne à l’attribut Obsolete pour fournir cette explication.

[Obsolete("ThisClass is obsolete. Use ThisClass2 instead.")]
public class ThisClass
{
}

La chaîne est passée comme argument à un constructeur ObsoleteAttribute, comme si vous écriviez var attr = new ObsoleteAttribute("some string").

Les paramètres à un constructeur d’attribut sont limités aux types simples/littéraux : bool, int, double, string, Type, enums, etc et les tableaux de ces types. Vous ne pouvez pas utiliser une expression ou une variable. Vous êtes libre d’utiliser des paramètres positionnels ou nommés.

Créer votre propre attribut

Vous créez un attribut en définissant une nouvelle classe qui hérite de la classe de base Attribute.

public class MySpecialAttribute : Attribute
{
}

Avec le code précédent, vous pouvez utiliser [MySpecial] (ou [MySpecialAttribute]) comme attribut ailleurs dans la base de code.

[MySpecial]
public class SomeOtherClass
{
}

Les attributs dans la bibliothèque de classes de base .NET, comme ObsoleteAttribute, déclenchent certains comportements au sein du compilateur. Toutefois, un attribut que vous créez agit uniquement comme métadonnées et ne produit pas de code dans la classe d’attributs en cours d’exécution. Il vous appartient d’agir sur ces métadonnées ailleurs dans votre code.

Il y a ici un « piège » à éviter. Comme mentionné précédemment, seuls certains types peuvent être passés comme arguments lors de l’utilisation d’attributs. Toutefois, quand vous créez un type d’attribut, le compilateur C# ne vous empêche pas de créer ces paramètres. Dans l’exemple suivant, vous avez créé un attribut avec un constructeur qui se compile correctement.

public class GotchaAttribute : Attribute
{
    public GotchaAttribute(Foo myClass, string str)
    {
    }
}

Toutefois, vous ne pouvez pas utiliser ce constructeur avec une syntaxe d’attribut.

[Gotcha(new Foo(), "test")] // does not compile
public class AttributeFail
{
}

Le code précédent provoque une erreur du compilateur du type : Attribute constructor parameter 'myClass' has type 'Foo', which is not a valid attribute parameter type

Comment limiter l’utilisation d’attributs

Les attributs peuvent être utilisés sur les « cibles » suivantes. Les exemples précédents les montrent sur des classes, mais ils peuvent également être utilisés sur :

  • Assembly
  • Classe
  • Constructeur
  • Délégué
  • Énumération
  • Événement
  • Champ
  • GenericParameter
  • Interface
  • Méthode
  • Module
  • Paramètre
  • Propriété
  • ReturnValue
  • Structure

Quand vous créez une classe d’attributs, par défaut, C# vous permet d’utiliser cet attribut sur l’une des cibles d’attribut possibles. Si vous souhaitez limiter votre attribut à certaines cibles, vous pouvez le faire à l’aide de AttributeUsageAttribute sur votre classe d’attributs. C’est exact, un attribut sur un attribut !

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class MyAttributeForClassAndStructOnly : Attribute
{
}

Si vous tentez de placer l’attribut ci-dessus sur quelque chose qui n’est ni une classe ni un struct, vous obtenez une erreur du compilateur du type : Attribute 'MyAttributeForClassAndStructOnly' is not valid on this declaration type. It is only valid on 'class, struct' declarations

public class Foo
{
    // if the below attribute was uncommented, it would cause a compiler error
    // [MyAttributeForClassAndStructOnly]
    public Foo()
    { }
}

Guide d’utilisation des attributs associés à un élément de code

Les attributs agissent comme métadonnées. Sans une certaine force extérieure, vous ne les voyez pas faire grand-chose.

Pour rechercher des attributs et agir dessus, la réflexion est nécessaire. La réflexion vous permet d’écrire du code en C# qui examine un autre code. Par exemple, vous pouvez utiliser la réflexion pour obtenir des informations sur une classe (ajoutez using System.Reflection; au début de votre code) :

TypeInfo typeInfo = typeof(MyClass).GetTypeInfo();
Console.WriteLine("The assembly qualified name of MyClass is " + typeInfo.AssemblyQualifiedName);

Vous obtenez une sortie qui ressemble à ceci : The assembly qualified name of MyClass is ConsoleApplication.MyClass, attributes, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null

Une fois que vous avez un objet TypeInfo (ou un objet MemberInfo, FieldInfo ou autre), vous pouvez utiliser la méthode GetCustomAttributes. Cette méthode retourne une collection d’objets Attribute. Vous pouvez également utiliser GetCustomAttribute et spécifier un type d’attribut.

Voici un exemple d’utilisation de GetCustomAttributes sur une instance de MemberInfo pour MyClass (avec un attribut [Obsolete] associé, comme vu précédemment).

var attrs = typeInfo.GetCustomAttributes();
foreach(var attr in attrs)
    Console.WriteLine("Attribute on MyClass: " + attr.GetType().Name);

Qui imprime sur la console : Attribute on MyClass: ObsoleteAttribute. Essayez d’ajouter d’autres attributs à MyClass.

Il est important de noter que ces objets Attribute sont instanciés de manière différée. Autrement dit, ils ne sont pas instanciés tant que vous n’utilisez pas GetCustomAttribute ou GetCustomAttributes. Ils sont également instanciés chaque fois. Le fait d’appeler GetCustomAttributes deux fois de suite retourne deux instances différentes de ObsoleteAttribute.

Attributs communs dans le runtime

Les attributs sont utilisés par de nombreux outils et frameworks. NUnit utilise des attributs tels que [Test] et [TestFixture], qui sont utilisés par le Test Runner de NUnit. ASP.NET MVC utilise des attributs tels que [Authorize] et fournit un framework de filtres d’action pour traiter les problèmes transversaux sur les actions MVC. PostSharp utilise la syntaxe d’attribut pour permettre la programmation orientée aspect en C#.

Voici quelques attributs importants générés dans les bibliothèques de classe de base de .NET Core :

  • [Obsolete]. Celui-ci a été utilisé dans les exemples ci-dessus, et il se trouve dans l’espace de noms System. Il est utile de fournir des informations déclaratives sur une base de code changeante. Un message peut être fourni sous la forme d’une chaîne et un autre paramètre booléen peut servir à passer d’un avertissement du compilateur à une erreur du compilateur.
  • [Conditional]. Cet attribut se trouve dans l'espace de noms System.Diagnostics. Cet attribut peut être appliqué aux méthodes (ou classes d’attributs). Vous devez transmettre une chaîne au constructeur. Si cette chaîne ne correspond pas à une directive #define, le compilateur C# supprime les appels à cette méthode (mais pas la méthode proprement dite). En général, vous utilisez cette technique pour le débogage (diagnostic).
  • [CallerMemberName]. Cet attribut peut être utilisé sur les paramètres et se trouve dans l’espace de noms System.Runtime.CompilerServices. CallerMemberName est un attribut qui permet d’injecter le nom de la méthode qui appelle une autre méthode. C’est un moyen d’éliminer les « chaînes magiques » lors de l’implémentation d’INotifyPropertyChanged dans divers frameworks d’interface utilisateur. Par exemple :
public class MyUIClass : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    public void RaisePropertyChanged([CallerMemberName] string propertyName = default!)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    private string? _name;
    public string? Name
    {
        get { return _name;}
        set
        {
            if (value != _name)
            {
                _name = value;
                RaisePropertyChanged();   // notice that "Name" is not needed here explicitly
            }
        }
    }
}

Dans le code ci-dessus, il est inutile d’avoir une chaîne littérale "Name". L’utilisation de CallerMemberName permet d’éviter les bogues liés aux fautes de frappe et facilite également la refactorisation/le renommage. Si les attributs offrent à C# la puissance déclarative, ils constituent une forme de code de type métadonnées et n’agissent pas par eux-mêmes.