La réflexion permet à un programme d'examiner des types pendant qu'il s'exécute : quelles propriétés a une classe, quelles sont leurs valeurs, quelles méthodes existent, quels attributs sont attachés. Les attributs en sont l'autre moitié : des étiquettes déclaratives entre crochets, comme [Obsolete] ou [JsonPropertyName("id")], qui ne signifient rien par elles-mêmes tant que le compilateur ou du code ne les lit pas par réflexion. Les sérialiseurs, les ORM, les bibliothèques de validation, les frameworks de test et le routage d'ASP.NET fonctionnent tous ainsi.
La plupart du code applicatif a rarement besoin de la réflexion directement, mais savoir comment elle fonctionne explique beaucoup du comportement des frameworks.
Les objets Type : typeof et GetType
Tout part d'un System.Type. Il y a deux façons d'en obtenir un :
Sortie :
Employee
Manager
Employee
False
True
True
System.Int32
False
Name
La différence à retenir : typeof(X) nomme un type que vous connaissez en écrivant le code, et obj.GetType() demande à un objet ce qu'il est réellement. Comparer GetType() == typeof(Employee) est une correspondance exacte qui échoue pour les classes dérivées, ce qui n'est en général pas ce que vous voulez ; is et IsAssignableFrom respectent l'héritage. nameof y ressemble mais n'est pas du tout de la réflexion : le compilateur le remplace par une constante chaîne.
GetType() sur une référence null lève NullReferenceException, puisqu'il n'y a aucun objet à interroger. Sur un type valeur nullable boxé, il renvoie le type sous-jacent : ((int?)5).GetType() vaut System.Int32.
Lire et définir des propriétés
GetProperties() liste les propriétés publiques d'un type sous forme d'objets PropertyInfo, dont chacun peut lire et écrire la valeur sur une instance donnée :
Sortie :
Name String = Mug
Price Decimal = 8.50
Stock Int32 = 12
7.90
True
SUP-77
Ce que cela montre :
GetValuerenvoie unobject, donc les types valeur reviennent boxés et vous devez les caster pour les utiliser.GetPropertyavec un nom qui n'existe pas renvoienull, et l'appel suivant sur ce résultat lèveNullReferenceException. Vérifiez avant utilisation.BindingFlags.NonPublic | BindingFlags.Instanceatteint les membres privés. C'est légitime dans l'outillage et les tests, mais cela contourne l'encapsulation et casse silencieusement quand la classe est remaniée.
Cette boucle est en substance le fonctionnement d'un exportateur CSV ou d'un sérialiseur d'objets en JSON : parcourir les propriétés, lire chaque valeur, la formater.
Appeler des méthodes et créer des objets par leur nom
GetMethod trouve une méthode, et Invoke l'appelle avec un tableau d'arguments. Activator.CreateInstance crée un objet à partir d'un Type, et c'est ainsi que les systèmes de plugins et les conteneurs d'injection de dépendances construisent des types choisis à l'exécution :
Sortie :
60.00
Decimal WithTax(1 parameters)
String Describe(0 parameters)
True
DeclaredOnly limite la liste aux membres déclarés dans la classe elle-même ; sans cela, GetMethods renvoie aussi ToString, Equals, GetHashCode et GetType hérités d'object. Type.GetType("Name") demande le nom qualifié par le namespace et, pour les types d'autres assemblies, aussi le nom de l'assembly ("MyApp.Plugins.Csv, MyApp.Plugins").
Si la méthode invoquée lève une exception, Invoke l'enveloppe dans une TargetInvocationException ; l'originale se trouve dans son InnerException.
Attributs : des étiquettes que lisent le compilateur et les frameworks
Un attribut s'écrit entre crochets devant ce qu'il décrit. Le framework en définit beaucoup ; en voici quelques-uns qui changent ce que fait le compilateur :
public class OrderService
{
[Obsolete("Use PlaceOrderAsync instead.")]
public void PlaceOrder(Order order) { }
// Every call site: warning CS0618: 'OrderService.PlaceOrder(Order)' is obsolete: 'Use PlaceOrderAsync instead.'
// [Obsolete("...", true)] makes it error CS0619 instead.
[Conditional("DEBUG")]
public void Trace(string message) => Console.WriteLine(message);
// Calls to Trace are removed entirely from builds without the DEBUG symbol.
}
[Flags] enum Channels { None = 0, Email = 1, Sms = 2 } // changes how ToString formats combinations
[Serializable] class Snapshot { } // marks a type for legacy binary serialization
D'autres sont lus par des bibliothèques à l'exécution : [JsonPropertyName] et [JsonIgnore] par System.Text.Json, [Required] et [MaxLength] par la validation de modèles d'ASP.NET Core et par Entity Framework, [HttpGet("orders/{id}")] par le routage d'ASP.NET, [Fact] et [Test] par les lanceurs de tests. L'attribut lui-même ne fait rien ; c'est le code qui le cherche qui agit.
Le nom ObsoleteAttribute se raccourcit en [Obsolete] à l'application : par convention, chaque nom de classe d'attribut se termine par Attribute, et C# permet d'omettre ce suffixe.
Déclarer et lire un attribut personnalisé
Un attribut personnalisé est une classe qui dérive de Attribute. [AttributeUsage] indique à quoi il peut s'appliquer. Les paramètres du constructeur deviennent des arguments positionnels, et les propriétés publiques modifiables deviennent des arguments nommés :
Sortie :
Username must be at most 20 characters
Email is required
Keep the city code short
0
C'est une version miniature de ce que fait la validation de modèles d'ASP.NET Core avec System.ComponentModel.DataAnnotations. Les arguments d'attribut doivent être des constantes de compilation (nombres, chaînes, typeof(...), valeurs d'enum, ou tableaux de celles-ci), car ils sont stockés dans les métadonnées de l'assembly. GetCustomAttribute<T>() est une méthode d'extension de System.Reflection ; il existe aussi IsDefined(typeof(T)) quand vous avez seulement besoin de savoir si un attribut est présent.
Le coût de la réflexion
La réflexion échange de la vitesse et de la sûreté contre de la souplesse :
- Vitesse. Rechercher un membre par son nom et l'appeler via
InvokeouGetValueest bien plus lent qu'un appel direct, et boxe les types valeur. Pour un usage répété, recherchez une fois lePropertyInfoou leMethodInfoet gardez-le, ou transformez-le en délégué avecDelegate.CreateDelegateouMethodInfo.CreateDelegateet appelez celui-ci. - Sûreté. Un nom mal orthographié ou une signature modifiée compile sans problème et échoue à l'exécution. Préférez
nameof(Product.Price)à la chaîne"Price"partout où c'est possible, pour que les renommages soient détectés. - Trimming et AOT. Les applications réduites par trimming et Native AOT suppriment les membres que rien ne semble utiliser, et la réflexion cache ces usages à cette analyse. Les bibliothèques modernes (
System.Text.Json,GeneratedRegex, la journalisation) passent aux générateurs de source, qui font le même travail à la compilation.
Utilisez la réflexion pour les parties d'un programme qui ne connaissent vraiment pas leurs types à l'avance : plugins, outillage générique, sérialiseurs, fonctions d'aide aux tests. Quand les types sont connus, du code ordinaire, des génériques ou des interfaces sont plus rapides et vérifiés par le compilateur.
Erreurs courantes
GetType() == typeof(Base)pour tester une classe de base. Cela échoue pour les types dérivés. UtilisezisouIsAssignableFrom.- Ne pas vérifier
null.GetProperty,GetMethodetType.GetTyperenvoientnullquand rien ne correspond. - De la réflexion dans une boucle critique sans cache. Mettez le
MemberInfoen cache ou compilez un délégué. - Intercepter la mauvaise exception d'
Invoke. La vraie exception est l'InnerExceptiondeTargetInvocationException. - Des chaînes magiques pour les noms de membres. Utilisez
nameof.
Questions fréquentes
Qu'est-ce que la réflexion en C# ?
La réflexion est la capacité d'un programme à examiner des types à l'exécution : lister les propriétés et méthodes d'une classe, lire et définir des valeurs par leur nom, appeler des méthodes, créer des instances et lire des attributs. Elle se trouve dans System.Reflection et part d'un objet Type. Les sérialiseurs, les ORM, les conteneurs d'injection de dépendances et les frameworks de test reposent dessus.
Quelle est la différence entre typeof et GetType en C# ?
typeof(Customer) est résolu à la compilation à partir d'un nom de type et n'a besoin d'aucun objet. obj.GetType() est appelé sur une instance à l'exécution et renvoie le type réel de l'objet, qui peut être plus dérivé que le type déclaré de la variable : pour Animal a = new Dog();, a.GetType() vaut Dog. GetType() sur une référence null lève NullReferenceException.
Comment obtenir la valeur d'une propriété par son nom en C# ?
obj.GetType().GetProperty("Price") renvoie un PropertyInfo (ou null s'il n'existe pas de propriété publique de ce nom), et .GetValue(obj) la lit sous forme d'object. .SetValue(obj, value) l'écrit. Mettez le PropertyInfo en cache si vous faites cela dans une boucle, car c'est la recherche qui coûte cher.
Comment créer un attribut personnalisé en C# ?
Déclarez une classe qui dérive de System.Attribute, nommez-la avec le suffixe Attribute, et indiquez où elle peut être utilisée avec [AttributeUsage] : [AttributeUsage(AttributeTargets.Property)] class MaxLengthAttribute : Attribute { public int Length { get; } public MaxLengthAttribute(int length) { Length = length; } }. Appliquez-la sous la forme [MaxLength(50)] et lisez-la avec property.GetCustomAttribute<MaxLengthAttribute>().
Que fait l'attribut Obsolete en C# ?
[Obsolete("Use PlaceOrderAsync instead")] sur un membre fait émettre par le compilateur l'avertissement CS0618, avec votre message, à chaque endroit où il est appelé. [Obsolete("...", true)] transforme l'avertissement en erreur CS0619. C'est ainsi que les bibliothèques retirent une API sans casser les appelants du jour au lendemain.
La réflexion est-elle lente en C# ?
Par rapport à un appel direct, oui : trouver un membre par son nom et l'invoquer via MethodInfo.Invoke ou PropertyInfo.GetValue est en général des dizaines à des centaines de fois plus lent, et cela boxe les types valeur. Cela convient au démarrage, à la configuration et à un usage occasionnel. Pour les chemins critiques, mettez le MemberInfo en cache, construisez un délégué une fois, ou utilisez plutôt des génériques ou un générateur de source.