Show this page in english

Directives du préprocesseur et attributs

Publié le 3 Septembre 2006

1. Les directives du préprocesseur

1.1 Introduction

Les directives du préprocesseur consistent en C# en un certain nombre de commandes qui permettent de conditionner la compilation du projet.

Par exemple, les directives du préprocesseur sont utiles lorsque vous souhaitez réaliser un même programme proposant plus ou moins de fonctions. Ces commandes permettent alors d'indiquer au compilateur si celui-ci doit ou non compiler tel ou tel morceau de code pour cette fois-ci permettant d'ajouter ou de supprimer telle options souhaitée.

Les directives du préprocesseur sont également très utiles lorsqu'il s'agit de créer un programme proposant un mode "debug" et un mode "release". Le mode "debug" affichant des informations de déboggage utiles au programmeur mais indésirables bien souvent pour le client, ces directives permettent au développeur de créer très rapidement une, deux, trois ou plusieurs versions de son logiciel.

Les directives du préprocesseur sont introduites par le symbole #. C# compte en tout 14 de ces commandes dont voici la liste complète :

Nous allons ici étudier chacune de ces commandes : voir comment elles s'utilisent et dans quels cas il est interessant de s'en servir.

1.2 #define et #undef

#define permet de créer une sorte de variable dont le compilateur aura la seule connaissance. Pas de trace de cette déclaration dans votre programme au final donc.

#define DEBUG

Cela ressemble bien à une déclaration de variable sauf que cette variable (ici DEBUG) n'a pas de valeur. Le compilateur sait simplement qu'elle existe, que DEBUG est défini, et il en tient compte.

Vous l'aurez probablement deviné, #undef fait exactement l'inverse de #define, cette déclaration indique au compilateur que la variable n'est plus défini. La syntaxe est la même :

#undef DEBUG

#define et #undef doivent être placé au début du fichier source, avant qu'un quelque objet ne doive être compilé.

Si vous souhaitez définir un même symbol pour tous vos fichiers sources, vous pouvez, plutôt que de répéter de multiples fois #define monSymbol, passer en paramètre ce symbole au compilateur C# en utilisant l'option de compilation /define.

Remarque : #define et #undef et ne servent à rien en l'état. Il faut les associer avec d'autres commandes du préprocesseur pour que leur utilité et leur interêt soit révélé. Et ces commandes, sont notamment celles de la sous-partie suivante...

1.3 #if, #else, #elif, et #endif

Vous reconnaîtrez dans ces quatre commandes le fonctionnement et l'utilisation qui doit en être faite.

En effet, #if correspond tout simplement à if, #else correspond à else, #elif est la contraction de else if et #endif indique la fin du bloc conditionnel, correspondances dans le domaine du préprocesseur bien sûr.

Vous pouvez utiliser les trois opérateurs logiques avec ces commandes, à savoir : le ET logique (&&), le OU logique (||), différent de (!=).

Exemples :

#if DEBUG Console.WriteLine("Mode debug"); #endif #if ENTERPRISE //Ajouter les fonctions spécifiques à la version ENTERPRISE #elif HOME //Ajouter les fonctions spécifiques à la version HOME #else //Version BASIC //Ajouter les fonctions spécifiques à la version BASIC #endif

Ici, les conditions sont évaluées non pas à l'exécution comme pour des if, else et autres mais à la compilation et la compilation devient alors une compilation conditionnelle. Cela veut dire que si, dans le premier exemple de code, DEBUG n'est pas défini, l'instruction Console.WriteLine("Mode debug"); ne sera pas compilée, mais si DEBUG est défini alors cette instruction sera compilée.

1.4 #warning et #error

#warning et #error sont également très utiles. En effet, ils permettent d'indiquer au compilateur si celui-ci doit précisément renvoyer une erreur ou un warning (ou rien du tout s'ils ne sont pas rencontrés). Leur utilisation est triviale :

#if DEBUG && RELEASE #error "Vous avez défini DEBUG et RELEASE dans un même un fichier." #endif #warning "Ce code est pas terrible, faut pas que j'oublie de le refaire une prochaine fois que j'ai le temps !" if (Int32.Parse(Console.ReadLine()) == 1) return;

1.5 #line

Cette commande permet de "fausser" les informations de base que le compilateur reçoit à propos du fichier source. En effet, considérez le code suivant :

using System; class Program { public static void Main() { #warning "Premier warning" /* Permier warning se trouve à la ligne 7 * dans le fichier source et pour le compilateur * aussi */ #line 100 #warning "Second warning" /* Celui-ci se trouve à la ligne 12 dans le fichier * source, mais le compilateur indiquera qu'il * se trouve à la ligne 100 */ } }

#line permet donc de changer le numéro de ligne pour le compilateur. Mais aussi :

using System; class Program { public static void Main() { //Code visible par le désassembleur (déboggeur) Console.WriteLine("1"); #line hidden //Code non visible par le désassembleur Console.WriteLine("2"); #line default //Code visible par le désassembleur Console.WriteLine("3"); Console.ReadLine(); } }

#line hidden masque les lignes qui suivent cette commande au déboggeur jusqu'à ce qu'une autre commande #line soti rencontrée (sauf s'il s'agit d'un nouveau #line hidden).

#line peut s'utiliser également pour modifier le nom du fichier qui apparaîtra dans les paramètres de compilation. Par exemple :

using System; class Program { public static void Main() { #warning "Warning n°1" Console.WriteLine("1"); #line 9 "Fichier.cs" #warning "Warning n°2" Console.WriteLine("2"); #line default #warning "Warning n°3" Console.WriteLine("3"); Console.ReadLine(); } }

#line default rétablit les valeurs par défaut des numéros de lignes, nom de fichier, etc.

1.6 #region et #endregion

#region permet de spécifier un début de bloc de code.

#endregion permet de terminer ce même bloc commencé par #region.

#region Methods //Méthodes #endregion #region Properties //Propriétés #endregion

Ces commandes n'affectent en aucun cas le processus de compilation, mais elles sont utiles car elles permettent, par exemple avec Visual Studio qui les reconnaît, de passer en mode plan ou non.

1.7 #pragma, #pragma warning et #pragma checksum

La directive #pragma est une nouveauté apporté par la version 2.0 de C#.

#pragma est inutilisable en lui-même. Il faut le combiner avec certains noms d'autres pragma que le compilateur C# reconnaît.

Ainsi, avec la version 2.0 de C#, #pragma ne peut s'utiliser qu'avec warning et checksum, mais les prochaînes versions de C# devraient intégrer de nouvelles directives #pragma.

#pragma warning est très facile d'utilisation : il permet soit de désactiver, soit de réactiver certains avertissements selon le modèle suivant :

où list-of-value corresponds aux valeurs des erreurs.

Exemple :

using System; class Program { /* On désactive les erreurs (warning) qui suivent. * Cette commande est valable pour tous le fichier source. */ #pragma warning disable 219, 168 public static void Main() { int i = 1; //Erreur de niveau 3 : CS0219 char c; //Erreur de niveau 3 : CS0168 Console.WriteLine("Hello !"); } //On réactive les deux warning désactivés précédemment #pragma warning restore 219, 168 public void DisplayHello() { int i = 1; //Erreur de niveau 3 : CS0219 char c; //Erreur de niveau 3 : CS0168 Console.WriteLine("Hello !"); } }

#pragma checksum est principalement utilisé dans les pages ASP.NET pour aider au déboggage. Pour davantage d'informations sur cette commande, veuillez consultez l'aide MSDN.

1.8 Remarques

Une directive prépropcesseur est une commande commançant pas la symbole # et se terminant au bout de la ligne. Pas de commentaires, ni rien d'autres que ce qui concerne la commande donc sur cette ligne.

Les directives prépropcesseurs utilisent une syntaxe un peu particulière et différente du C#. Celles-ci se terminant à chaque fin de ligne, les point virgules (;) sont inutiles et interdits.

En fait, C# ne possède pas vraiment un préprocesseur comme en C ou en C++. C'est le compilateur C# qui s'occupe à gérer les différentes commandes. Cependant, C# conserve le terme "directives du préprocesseur" afin de suggérer l'intervention d'un préprocesseur.

Pour les développeurs venant de C ou de C++, vous aurez pu constaté que les directives du préprocesseur en C# sont moins nombreuses et moins interessantes et centrales. Il est, par exemple, impossible de définir des macros avec ces directives en C#. Mais C# offre d'autres mécanismes qui effectuent le même travail que les macros de C/C++.

2. Les attributs

2.1 Qu'est ce qu'un attribut ?

Un attribut est une classe .NET qui permet d'apporter des informations au compilateur, lequel va modifier en conséquence le processus de compilation.

Certains attributs ont un fonctionnement similaire aux directives du préprocesseur de C# car ils ne se traduisent pas par des instructions dans le code compilé.

D'autres apportent des informations aux compilateur sur la nature de l'objet en question ou vont modifier la façon de compiler votre code, etc. Mais d'une manière générale, un attribut n'entraîne aucune action de compilation, mais provoque une émission de données spéciales vers l'assemblage compilé.

La différence vient du fait que les attributs sont des classes .NET et qu'il est donc possible de créer ses propres attributs et que leur nombre peut ainsi donc être, en théorie, illimité alors que les directives du préprocesseur dépendent de C# et sont en quantité limité.

Les attributs peut donc être représenté comme étant une sorte de marqueur appliqué à un élement de votre code. Cet élément peut être une classe, une méthode ou même un argument.

2.2 Quelques attributs assez courant

Certains attributs sont utilisés plus fréquement que d'autres et notamment :

Ci dessous, un exemple utilisant ces attributs :

/* Si vous supprimez le #undef DEBUG, une erreur sera immédiatement * signalée par le compilateur */ #undef DEBUG #define RELEASE #if DEBUG && RELEASE #error "You can't define DEBUG and RELEASE symbol simultaneously !" #endif using System; using System.IO; using System.Diagnostics; using System.Runtime.Serialization.Formatters.Soap; using System.Windows.Forms; class Program { [System.Runtime.InteropServices.DllImport("User32.dll")] public static extern void MessageBox( int hParent, string message, string caption, int type ); static void Main() { DebugInfos(); string path = "customer.xml"; Customer c = new Customer("45C-24E6J", "Microsoft", "mailto@microsoft.com", "No comment" ); Console.WriteLine("Avant"); WriteCustomer(c); Stream stream = File.Create(path); SoapFormatter formatter = new SoapFormatter(); formatter.Serialize(stream, c); stream.Close(); //La ligne suivante provoque un warning //DisplaySuccessMessage(); DisplayMessageSucces(); DisplayFile(path); c = null; #if DEBUG WriteCustomer(c); #endif stream = File.Open(path, FileMode.Open); c = (Customer)formatter.Deserialize(stream); stream.Close(); Console.WriteLine("Après"); WriteCustomer(c); } static void WriteCustomer(Customer c) { if (c != null) { Console.Write("----------------------------------------" + "----------------------------------------"); Console.WriteLine(" ID | " + c.Id); Console.WriteLine(" Name | " + c.Name); Console.WriteLine(" Email | " + c.CustomerEmail); Console.WriteLine(" Commentaire | " + c.Comment); Console.Write("----------------------------------------" + "----------------------------------------"); } else { Console.WriteLine("\nErreur : customer a pour valeur null\n"); } Console.ReadLine(); } [Conditional("DEBUG")] static void DebugInfos() { Console.WriteLine("Machine : {0}\nOS : {1}\nVersion du CLR : {2}", Environment.MachineName, Environment.OSVersion, Environment.Version ); } [Conditional("DEBUG")] static void DisplayFile(string path) { StreamReader sr = new StreamReader(path); RichTextBox rtb = new RichTextBox(); rtb.Text = sr.ReadToEnd(); sr.Close(); rtb.Dock = DockStyle.Fill; Form form = new Form(); form.Text = "Fichier : " + path; form.Controls.Add(rtb); form.ShowDialog(); } [Conditional("RELEASE"), Obsolete("DisplaySuccessMessage est obsolète." + "Préférez lui DisplayMessageSuccess", false)] static void DisplaySuccessMessage() { Console.WriteLine("Fichier écrit avec succès"); } static void DisplayMessageSucces() { MessageBox(0, "Fichier écrit avec succès", "Info", 0); } } [Serializable] public class Customer { private string id; private string name; private string customerEmail; [NonSerialized] private string comment; public Customer(string id, string name, string customerEmail, string comment) { this.id = id; this.name = name; this.customerEmail = customerEmail; this.comment = comment; } public string Id { get { return id; } set { id = value; } } public string Name { get { return name; } set { name = value; } } public string CustomerEmail { get { return customerEmail; } set { customerEmail = value; } } public string Comment { get { return comment; } set { comment = value; } } }

Quelques explications :

La classe Customer est marquée comme Serialized et tout son contenu : id, name, customerEmail (mais pas comment vu qu'il est marqué comme étant NonSerialied) est donc marqué Serialized. Cet attribut permet de transfomer une classe, un objet, en une chaîne de caractères relativement simple : soit au format XML avec SoapFormatter soit au format binaire avec la classe System.Runtime.Serialization.Formatters.Binary.BinaryFormatter. En exécutant le précédant exemple, vous verrez ce que Serialized et NonSerialized ont permis...

Remarque : Vous pouvez également sérialiser une classe, un objet avec la classe System.Xml.Serialization.XmlSerializer qui vous décrira votre objet au format xml mais l'attribut Serialized n'est pas nécessaire dans ce cas là.

Conditional correspond à une sorte commande #if placé devant une méthode. Mais c'est en fait bien plus que cela. En effet, si une méthode marqué comme Conditional ne répond pas à la condition fixée, tous les appels à cette méthode se trouvant dans le projet sont ignorés, non compilés.

ObsoleteAttribute est applicable à tous les éléments de programme, sauf aux assemblys, aux modules, aux paramètres ou aux valeurs de retour. Un élément marqué comme obsolète informe l'utilisateur que cet élément sera supprimé dans les prochaines versions du produit. (MSDN 2006). Cet attribut prend en paramètre un booléen : celui indique si le compilateur doit retourner une erreur (true) ou un simple warning (false).

L'attribut DllImport permet d'importer une fonction provenant d'une DLL externe (appartenant à l'API Win32 par exemple). Vous devez, pour pouvoir vous en servir convenablement, connaître le nom exact de la fonction ainsi que le nombre et le type de ses paramètres, sans oublier bien sûr le nom de DLL dans laquelle se trouve la fonction.

2.3 Syntaxe

La syntaxe est toute simple : votre attribut se place entre crochets ([...]) devant la classe, méthode, champs (quand cela est autorisé)... Si votre attribut prend des paramètres, vous êtes obligé de les préciser. Si par contre, celui-ci n'en prend pas du tout, vous pouvez vous passer des parenthèses.

[Conditional("RELEASE")] private void foo() { } [Serializable] public class MyClass { }

Vous pouvez déclarer plus d'un attribut devant une méthode, classe, etc. Et vous pouvez le faire de deux façons différentes :

- soit en plaçant tous les attributs entre les mêmes crochets séparés par une virgule :

[Conditional("DEBUG"), Obsolete("Pas cool :(")] private void foo() { }

- soit en les séparant chacun d'eux par des crochets :

[Conditional("DEBUG")] [Obsolete("Pas cool :(")] private void foo() { }

2.4 Côté compilateur

Voyons à présent ce qui se passe du côté du compilateur lorsque celui-ci rencontre un attribut Prenons par exemple :

[Conditional("DEBUG")] private void foo() { }

Lorsque le compilateur rencontre la méthode foo(), il s'aperçoit que celle-ci possède un attribut nommé Conditional. Il va alors ajouter au nom de cet attribut le mot Attribute et rechercher la classe nommée ConditionalAttribute ainsi que les constructeurs que celle-ci propose pour voir s'il en existe un prenant un type string en paramètre.

Attribute n'est ajouté que si votre attribut ne se termine pas déjà par Attribute Par conséquent, le code suivant revient au même que le précédant :

[ConditionalAttribute("DEBUG")] private void foo() { }

Le compilateur s'attend donc à trouver une classe nommée ConditionalAttribute et dérivant de la classe abstraite System.Attribute. Celui-ci va ensuite vérifier que cet attribut a bien le droit d'être déclaré devant une méthode (dans cet exemple, il s'agit d'une méthode).

Pour cela, l'attribute CondionalAttribute possède lui même un attribut : AttributeUsage qui spécifie si la classe d'attribut à le droit d'être utilisée devant une méthode, une propriété, une classe, etc. Cela signfie que la classe ConditionalAttribute est de la forme :

[Serializable, AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple=true), ComVisible(true)] public sealed class ConditionalAttribute : Attribute { //Contenu }

Vous l'aurez compris, c'est AttributeTargets, qui est une énumération, qui spécifie l'élément devant lequel l'attribut peut être placé. L'énumération AttributeTargets est définie de la sorte :

[Serializable, Flags, ComVisible(true)] public enum AttributeTargets { //Champs All = 0x7fff, Assembly = 1, Class = 4, Constructor = 0x20, Delegate = 0x1000, Enum = 0x10, Event = 0x200, Field = 0x100, GenericParameter = 0x4000, Interface = 0x400, Method = 0x40, Module = 2, Parameter = 0x800, Property = 0x80, ReturnValue = 0x2000, Struct = 8 }

On voit ainsi sur quels élément un attribut peut être appliqué...

Il n'y a qu'un seul élément qui est à préciser : Assembly qui ne correspond à aucun élément de programme.
Un attribut peut être appliqué à tout un assemblage et ce à n'importe quel endroit dans le code ; il suffit simplement de spécifier devant l'attribute le mot assembly comme cela :

[assembly : SomeAttributeThatDoSomething()]

AttributeUsage prend optionnellement deux paramètres : AllowMultiple et Inherited, des booléens, qui s'utilisent avec un syntaxe particulière :

[AttributeUsage(AttributeTargs.All, AllowMultiple=true, Inherited=false)]

AllowMultiple indique si l'attribut peut être déclaré plusieurs fois devant un même élément (true pour oui, false pour non).

Inherited définit à true indique que l'attribut appliqué à une classe ou interface s'applique également à tous ses dérivés.

2.5 Attributs personnalisés

Maintenant que vous avez vu comment le compilateur utilise les attributs, vous n'aurez guère de mal à créer les vôtres, le minimum étant d'avoir une classe dérivant de System.Attribute.

Créons par exemple un attribut nous permettant d'obtenir des informations sur les classes utilisées dans notre programme.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Property, AllowMultiple=true, Inherited=true)] public class DocumentationAttribute : Attribute { private string doc; private DateTime maj; public DocumentationAttribute(string doc, string maj) { this.doc = doc; this.maj = DateTime.Parse(maj); } public string DocumentationString { get { return doc; } set { doc = value; } } public DateTime DerniereMiseAJour { get { return maj; } set { maj = value; } } }

Le plus difficile lorsque l'on crée ses propres attributs n'est pas d'écrire la classe de l'attribut mais plutôt de s'en servir. En effet, en voyant, cette classe, vous vous demandez probablement comment celle-ci pourrait bien être utilisée car elle ressemble à n'importe quelle classe très simple, si ce n'est qu'elle dérive de la classe Attribute.

En fait, les attributs ne peuvent fonctionner que parce que la réflexion entre en jeu. Eh oui ! Rappelons ce qu'est un attribut : il s'agit d'une sorte de méta-information que le compilateur C# reconnait en tant que telle mais qu'il ne sait pas comment l'utiliser : il faut lui préciser comment en exploiter les informations et c'est en utilisant les classes de l'espace de nom System.Reflection que cela est possible.

Mais trève de bavardage et voyons tout de suite comment il est possible d'utiliser notre précédent attribut. Ci dessous, le code complet exploitant notre classe d'attribut :

using System; using System.Reflection; using System.Runtime.InteropServices; namespace MyProjet { [Documentation("Classe principale", "02/09/2006 23:13")] public class Program { private static DateTime lastUpdate = new DateTime(2006, 09, 2, 22, 45, 0); [Documentation("Point d'entré du programme", "02/09/2006 22:30")] public static void Main() { Program p = new Program(); p.Run(); } [Documentation("Permet de lancer la suite d'instructions à l'origine du programme", "02/09/2006 23:09")] public void Run() { Assembly assemblage = this.GetType().Assembly; Console.WriteLine("Assemblage : " + assemblage.FullName); Type[] types = assemblage.GetTypes(); foreach (Type t in types) DisplayInfos(t); Console.ReadLine(); } [Documentation("Appelle les méthodes chargées d'afficher les informations sur les propriétés, classes, etc.", "02/09/2006 23:08")] public void DisplayInfos(Type type) { //On ne traitera que les classes if (!type.IsClass) return; Console.Write("--------------------------------------------------------------------------------"); Console.WriteLine("Classe : " + type.Name); //On récupère les attributs de la classe Attribute[] attribs = Attribute.GetCustomAttributes(type); if (attribs.Length > 0) foreach (Attribute a in attribs) WriteAttribute(a); else Console.WriteLine("Classe non modifiée"); Console.WriteLine(" ----------------------------------------"); //ainsi que les méthodes MethodInfo[] methods = type.GetMethods(); Console.WriteLine("Méthodes qui ont été modifiées après le : {0}", lastUpdate); foreach (MethodInfo mi in methods) { object[] mi_attribs = mi.GetCustomAttributes(false); if (attribs.Length > 0) { Console.WriteLine("Méthode : {0} {1}()", mi.ReturnType, mi.Name); foreach (Attribute a in mi_attribs) { WriteAttribute(a); Console.WriteLine(); } } else { Console.WriteLine("Les méthodes de cette classes n'ont pas été modifiées depuis cette date."); } } //Idem pour les propriétés... Console.Write("--------------------------------------------------------------------------------"); } [Documentation("Affiche les attributs", "02/09/2006 22:49")] public void WriteAttribute(Attribute attrib) { DocumentationAttribute docAttrib = attrib as DocumentationAttribute; if (docAttrib == null || docAttrib.DerniereMiseAJour == null || docAttrib.DerniereMiseAJour < lastUpdate) return; Console.WriteLine("Dernière modification : {0}", docAttrib.DerniereMiseAJour); if (docAttrib.DocumentationString != null || docAttrib.DocumentationString != String.Empty) Console.WriteLine("Informations : " + docAttrib.DocumentationString); } } [Documentation("Implémente les mises à jours de méthodes, propriétés et classes", "02/09/2006 22:24")] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = true, Inherited = true)] [ComVisible(true)] public class DocumentationAttribute : Attribute { private string doc; private DateTime maj; public DocumentationAttribute(string doc, string maj) { this.doc = doc; this.maj = DateTime.Parse(maj); } public string DocumentationString { get { return doc; } set { doc = value; } } public DateTime DerniereMiseAJour { get { return maj; } set { maj = value; } } } }

Exécutez le code et faite un deboggage si besoin est pour le comprendre.

2.6 Toujours pas compris ?

Vous n'avez pas encore compris à quoi peuvent servir les attributs ou comment les utiliser, pourquoi il faut forcément utiliser la réflexion pour s'en servir ?

Dans ce cas, pensez aux Designers que Visual Studio, SharpDevelop ou autre du même style. Lorsque vous créez votre interface graphique, que vous ajoutez vos contrôles dans votre boîte, l'explorateur de propriétés vous permet de les éditer très facilement et très rapidement et pourtant, lorsque vous retounez dans le code, vous vous apercevez que le contrôle en question possède beaucoup plus de propriétés que ce que l'explorateur vous affiche.

Alors ? Qu'a-t-il fait ? En aurait-il oublié ?

Et bien non ! En fait, chacune des propriétés qui s'affichent dans l'explorateur s'y affichent justement parce que la classe qui les implémente (la classe du contrôle) possède devant certaines propriétés un attribut que l'EDI peut retrouver. S'il le retrouve, il le traite et affiche ou non la propriété dans l'explorateur de propriétés comme l'attribut le spéciifie.

Par exemple, pour une propriété d'un contrôle Windows Form, voici quelques attributs qui entrent en jeu et vont amener à modifier le contenu et le comportement de l'explorateur de propriétés lorsque le contrôle sera sélectionné :

[Category("My Own Category")] [Description("Coucou ;) (retourne une chaine de caractère : type System.String)")] [Browsable(true)] [DefaultValue("coucou")] public string Coucou { get { return coucouString; } set { coucouString = value; } }

Vous comprenez à présent comment les attributs sont utilisés ? Non ? Toujours pas ? Alors détaillons l'usage de ceux-ci (du précédant exemple).

Chacun de ses attributs n'entrent pas en jeu lors de la compilation du code. Et pour cause, ils n'apportent là que des informations qui sont inutiles pour l'assemblage final. En revanche, un environnement de développement tel que Visual Studio, qui est capable de lire ses informations grâce à la réflexion, peut les utiliser pour personnaliser son interface et offrir davantage d'options au développeur/utilisateur.

Ce n'est donc pas le compiltateur C# qui intervient, traite, exploite ces attribut mais bien un logiciel externe : Visual Studio (ou autre EDI possédant également un RAD), qui pour retrouver ces attributs et s'en servir, demande à ce que le contrôle en question soit compilé (avec succès bien entendu), pour enfin pouvoir utiliser les classes de l'espace de nom System.Reflection et pouvoir alors récupérer les attributs susceptibles de l'interesser !