Mit Reflection kann ein Programm Typen untersuchen, während es läuft: welche Properties eine Klasse hat, welche Werte sie haben, welche Methoden existieren, welche Attribute angehängt sind. Attribute sind die andere Hälfte: deklarative Markierungen in eckigen Klammern wie [Obsolete] oder [JsonPropertyName("id")], die für sich nichts bedeuten, bis der Compiler oder irgendein Code sie per Reflection liest. Serializer, ORMs, Validierungsbibliotheken, Test-Frameworks und das Routing von ASP.NET funktionieren alle so.
Der meiste Anwendungscode braucht Reflection selten direkt, aber wer weiß, wie sie funktioniert, versteht viel vom Verhalten der Frameworks.
Type-Objekte: typeof und GetType
Alles beginnt bei einem System.Type. Es gibt zwei Wege, einen zu bekommen:
Ausgabe:
Employee
Manager
Employee
False
True
True
System.Int32
False
Name
Der Unterschied, den du dir merken solltest: typeof(X) nennt einen Typ, den du beim Schreiben des Codes kennst, und obj.GetType() fragt ein Objekt, was es wirklich ist. Der Vergleich GetType() == typeof(Employee) ist eine exakte Übereinstimmung, die bei abgeleiteten Klassen scheitert, was meist nicht gewollt ist; is und IsAssignableFrom berücksichtigen Vererbung. nameof sieht ähnlich aus, ist aber überhaupt keine Reflection: Der Compiler ersetzt es durch eine String-Konstante.
GetType() auf einer null-Referenz wirft NullReferenceException, weil es kein Objekt gibt, das man fragen könnte. Bei einem geboxten Nullable-Werttyp gibt es den zugrunde liegenden Typ zurück: ((int?)5).GetType() ist System.Int32.
Properties lesen und setzen
GetProperties() listet die öffentlichen Properties eines Typs als PropertyInfo-Objekte auf, von denen jedes den Wert auf einer bestimmten Instanz lesen und schreiben kann:
Ausgabe:
Name String = Mug
Price Decimal = 8.50
Stock Int32 = 12
7.90
True
SUP-77
Drei Dinge zeigt das:
GetValuegibtobjectzurück, Werttypen kommen also geboxt zurück, und du castest sie, um sie zu verwenden.GetPropertymit einem nicht existierenden Namen gibtnullzurück, und der nächste Aufruf darauf wirftNullReferenceException. Prüfe vor der Verwendung.BindingFlags.NonPublic | BindingFlags.Instanceerreicht private Member. In Tools und Tests ist das legitim, umgeht aber die Kapselung und bricht stillschweigend, wenn die Klasse umgebaut wird.
Diese Schleife ist im Wesentlichen, wie ein CSV-Exporter oder ein Serializer von Objekt zu JSON funktioniert: die Properties durchlaufen, jeden Wert lesen, ihn formatieren.
Methoden aufrufen und Objekte per Namen erzeugen
GetMethod findet eine Methode, und Invoke ruft sie mit einem Array von Argumenten auf. Activator.CreateInstance erzeugt ein Objekt aus einem Type, so bauen Plugin-Systeme und DI-Container Typen, die zur Laufzeit gewählt werden:
Ausgabe:
60.00
Decimal WithTax(1 parameters)
String Describe(0 parameters)
True
DeclaredOnly beschränkt die Liste auf Member, die in der Klasse selbst deklariert sind; ohne es gibt GetMethods auch ToString, Equals, GetHashCode und GetType von object zurück. Type.GetType("Name") braucht den mit Namespace qualifizierten Namen und bei Typen in anderen Assemblies auch den Assembly-Namen ("MyApp.Plugins.Csv, MyApp.Plugins").
Wirft die aufgerufene Methode, verpackt Invoke die Exception in eine TargetInvocationException; das Original steckt in ihrer InnerException.
Attribute: Markierungen, die Compiler und Frameworks lesen
Ein Attribut steht in eckigen Klammern vor dem, was es beschreibt. Das Framework definiert viele; ein paar, die ändern, was der Compiler tut:
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
Andere werden zur Laufzeit von Bibliotheken gelesen: [JsonPropertyName] und [JsonIgnore] von System.Text.Json, [Required] und [MaxLength] von der Modellvalidierung in ASP.NET Core und von Entity Framework, [HttpGet("orders/{id}")] vom Routing in ASP.NET, [Fact] und [Test] von Test-Runnern. Das Attribut selbst tut nichts; der Code, der danach sucht, schon.
Der Name ObsoleteAttribute wird beim Anwenden zu [Obsolete] verkürzt: Per Konvention endet jeder Name einer Attributklasse auf Attribute, und C# erlaubt, das Suffix wegzulassen.
Ein eigenes Attribut deklarieren und lesen
Ein eigenes Attribut ist eine Klasse, die von Attribute ableitet. [AttributeUsage] legt fest, worauf es angewendet werden darf. Konstruktorparameter werden zu positionellen Argumenten, und öffentliche, setzbare Properties zu benannten Argumenten:
Ausgabe:
Username must be at most 20 characters
Email is required
Keep the city code short
0
Das ist eine Miniaturversion dessen, was die Modellvalidierung von ASP.NET Core mit System.ComponentModel.DataAnnotations tut. Attributargumente müssen Konstanten zur Kompilierzeit sein (Zahlen, Strings, typeof(...), Enum-Werte oder Arrays davon), weil sie in den Metadaten der Assembly gespeichert werden. GetCustomAttribute<T>() ist eine Erweiterungsmethode in System.Reflection; es gibt auch IsDefined(typeof(T)), wenn du nur wissen musst, ob ein Attribut vorhanden ist.
Die Kosten von Reflection
Reflection tauscht Geschwindigkeit und Sicherheit gegen Flexibilität:
- Geschwindigkeit. Einen Member per Namen nachzuschlagen und ihn über
InvokeoderGetValueaufzurufen ist weit langsamer als ein direkter Aufruf und boxt Werttypen. Für wiederholte Verwendung schlage diePropertyInfooderMethodInfoeinmal nach und behalte sie, oder mach mitDelegate.CreateDelegateoderMethodInfo.CreateDelegateeinen Delegate daraus und rufe diesen auf. - Sicherheit. Ein falsch geschriebener Name oder eine geänderte Signatur kompiliert problemlos und scheitert zur Laufzeit. Nimm wo immer möglich
nameof(Product.Price)statt des Strings"Price", damit Umbenennungen erkannt werden. - Trimming und AOT. Getrimmte und mit Native AOT kompilierte Apps entfernen Member, die scheinbar nichts verwendet, und Reflection verbirgt die Verwendung vor dieser Analyse. Moderne Bibliotheken (
System.Text.Json,GeneratedRegex, Logging) wechseln zu Source Generators, die dieselbe Arbeit zur Kompilierzeit erledigen.
Nimm Reflection für die Teile eines Programms, die ihre Typen wirklich nicht im Voraus kennen: Plugins, generische Tools, Serializer, Testhilfen. Wenn die Typen bekannt sind, sind gewöhnlicher Code, Generics oder Interfaces schneller und werden vom Compiler geprüft.
Häufige Fehler
GetType() == typeof(Base), um auf eine Basisklasse zu prüfen. Das scheitert bei abgeleiteten Typen. NimmisoderIsAssignableFrom.- Nicht auf
nullprüfen.GetProperty,GetMethodundType.GetTypegebennullzurück, wenn nichts passt. - Reflection in einer heißen Schleife ohne Zwischenspeichern. Speichere die
MemberInfozwischen oder kompiliere einen Delegate. - Die falsche Exception von
Invokeabfangen. Die echte Exception ist dieInnerExceptionderTargetInvocationException. - Magische Strings für Member-Namen. Nimm
nameof.
Häufig gestellte Fragen
Was ist Reflection in C#?
Reflection ist die Fähigkeit eines Programms, Typen zur Laufzeit zu untersuchen: die Properties und Methoden einer Klasse auflisten, Werte per Namen lesen und setzen, Methoden aufrufen, Instanzen erzeugen und Attribute lesen. Sie liegt in System.Reflection und beginnt bei einem Type-Objekt. Serializer, ORMs, Dependency-Injection-Container und Test-Frameworks bauen darauf auf.
Was ist der Unterschied zwischen typeof und GetType in C#?
typeof(Customer) wird zur Kompilierzeit aus einem Typnamen aufgelöst und braucht kein Objekt. obj.GetType() wird zur Laufzeit auf einer Instanz aufgerufen und gibt den tatsächlichen Typ des Objekts zurück, der spezieller sein kann als der deklarierte Typ der Variable: Bei Animal a = new Dog(); ist a.GetType() gleich Dog. GetType() auf einer null-Referenz wirft NullReferenceException.
Wie bekomme ich in C# den Wert einer Property per Namen?
obj.GetType().GetProperty("Price") gibt eine PropertyInfo zurück (oder null, wenn es keine solche öffentliche Property gibt), und .GetValue(obj) liest sie als object. .SetValue(obj, value) schreibt sie. Speichere die PropertyInfo zwischen, wenn du das in einer Schleife tust, denn das Nachschlagen ist der teure Teil.
Wie erstelle ich in C# ein eigenes Attribut?
Deklariere eine Klasse, die von System.Attribute ableitet, gib ihr das Suffix Attribute und lege mit [AttributeUsage] fest, wo sie verwendet werden darf: [AttributeUsage(AttributeTargets.Property)] class MaxLengthAttribute : Attribute { public int Length { get; } public MaxLengthAttribute(int length) { Length = length; } }. Wende es als [MaxLength(50)] an und lies es mit property.GetCustomAttribute<MaxLengthAttribute>().
Was macht das Attribut Obsolete in C#?
[Obsolete("Use PlaceOrderAsync instead")] an einem Member lässt den Compiler an jeder Aufrufstelle die Warnung CS0618 mit deiner Meldung ausgeben. [Obsolete("...", true)] macht aus der Warnung den Fehler CS0619. So ziehen Bibliotheken eine API zurück, ohne Aufrufer über Nacht zu brechen.
Ist Reflection in C# langsam?
Im Vergleich zu einem direkten Aufruf ja: Einen Member per Namen zu finden und ihn über MethodInfo.Invoke oder PropertyInfo.GetValue aufzurufen ist typischerweise zehn- bis hundertmal langsamer und boxt Werttypen. Für Start, Konfiguration und gelegentliche Verwendung ist das in Ordnung. Für heiße Pfade speichere die MemberInfo zwischen, baue einmal einen Delegate oder nimm stattdessen Generics oder einen Source Generator.