Menu

C# Reflection und Attribute: typeof, GetType und eigene Attribute

Wie Reflection in C# funktioniert: Type-Objekte aus typeof und GetType, Properties lesen und setzen, Methoden per Namen aufrufen, Instanzen erzeugen, und Attribute: die eingebauten wie Obsolete, eigene deklarieren und sie zur Laufzeit lesen. Dazu, was Reflection kostet.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

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:

  • GetValue gibt object zurück, Werttypen kommen also geboxt zurück, und du castest sie, um sie zu verwenden.
  • GetProperty mit einem nicht existierenden Namen gibt null zurück, und der nächste Aufruf darauf wirft NullReferenceException. Prüfe vor der Verwendung.
  • BindingFlags.NonPublic | BindingFlags.Instance erreicht 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 Invoke oder GetValue aufzurufen ist weit langsamer als ein direkter Aufruf und boxt Werttypen. Für wiederholte Verwendung schlage die PropertyInfo oder MethodInfo einmal nach und behalte sie, oder mach mit Delegate.CreateDelegate oder MethodInfo.CreateDelegate einen 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. Nimm is oder IsAssignableFrom.
  • Nicht auf null prüfen. GetProperty, GetMethod und Type.GetType geben null zurück, wenn nichts passt.
  • Reflection in einer heißen Schleife ohne Zwischenspeichern. Speichere die MemberInfo zwischen oder kompiliere einen Delegate.
  • Die falsche Exception von Invoke abfangen. Die echte Exception ist die InnerException der TargetInvocationException.
  • 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.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S