Generische Methode mit Einschränkung T auf Enum: Wie funktioniert das unter der Haube
\n\nStellen Sie sich vor: Sie programmieren in C# und möchten eine universelle Methode erstellen, die jede Aufzählung (Enum) akzeptiert. Klingt einfach? Aber wenn Sie jemals versucht haben, public void DoSomething<T>() where T : Enum zu schreiben, wissen Sie – der Compiler gibt einen Fehler aus. Vor C# 7.3 war dies unmöglich. Danach wurde es möglich, aber mit Nuancen. Lassen Sie uns verstehen, wie das unter der Haube funktioniert und warum dies eines der beliebtesten Themen auf Stack Overflow ist.
Dieser Artikel ist nicht nur eine trockene Nacherzählung der Dokumentation. Wir graben tiefer: vom IL-Code bis zu den Empfehlungen von ReSharper. Los geht's!
\n\nWarum konnte man nicht einfach where T : Enum schreiben?
\n\nLange Zeit verbot C# die Verwendung von Enum oder Delegate als Einschränkung für generische Parameter. Warum? Weil der Compiler garantieren musste, dass der Typ T ein Wertetyp (struct) oder ein Referenztyp (class) ist. Enum hingegen ist ein spezieller Strukturtyp, der von System.Enum erbt. Aber im Typsystem von C# passte er nicht in die einfachen Regeln:
- \n
- Enum ist ein struct, aber nicht jeder struct. \n
- Enum erbt von ValueType, aber der Compiler erlaubte nicht die Verwendung von
where T : System.Enum. \n - Der Versuch,
where T : struct, Enumzu schreiben, funktionierte ebenfalls nicht. \n
Entwickler griffen zu Workarounds: Überprüfung zur Laufzeit mittels typeof(T).IsEnum oder Verwendung von Reflection. Aber das zerstörte die gesamte Leistung und Typsicherheit.
C# 7.3: Lang erwartete Lösung
\n\nAb C# 7.3 und .NET Core 2.0 / .NET Framework 4.8 wurde es möglich, Folgendes zu schreiben:
\n\npublic static string GetEnumName<T>(T value) where T : Enum\n{\n return Enum.GetName(typeof(T), value);\n}\n\n\nJetzt versteht der Compiler: T ist eine Aufzählung. Aber wie genau macht er das? Werfen wir einen Blick unter die Haube.
\n\nWas passiert im IL-Code?
\n\nWenn Sie where T : Enum schreiben, generiert der Compiler einen speziellen constrained call für Methoden wie Enum.HasFlag, Enum.GetName usw. Anstatt Boxing oder Reflection zu verwenden, kann der JIT-Compiler einen direkten Aufruf der statischen Methode einfügen, da er den konkreten Typ T kennt.
Beispiel für IL-Code der obigen Methode:
\n\n.method public hidebysig static \n string GetEnumName<([mscorlib]System.Enum) T>(!!0 value) cil managed\n{\n // Aufruf von Enum.GetName mit constrained-Präfix\n call string [mscorlib]System.Enum::GetName(class [mscorlib]System.Type, object)\n}\n\n\nBeachten Sie das Attribut ([mscorlib]System.Enum) T. Dies ist die neue Einschränkung. Der JIT sieht es und weiß, dass T ein Enum ist, sodass Boxing beim Übergeben von value an GetName vermieden werden kann.
Wie verwendet man eine generische Methode mit Enum in der Praxis?
\n\nErstellen wir ein nützliches Beispiel: eine Methode zum Parsen eines Strings in eine beliebige Aufzählung mit Unterstützung für Groß-/Kleinschreibung ignorieren.
\n\npublic static T ParseEnum<T>(string value, bool ignoreCase = true) where T : Enum\n{\n if (string.IsNullOrWhiteSpace(value))\n throw new ArgumentException("Value cannot be null or empty.", nameof(value));\n\n if (!Enum.TryParse(typeof(T), value, ignoreCase, out object? result))\n throw new ArgumentException($"'{value}' is not a valid value for enum {typeof(T).Name}.");\n\n return (T)result;\n}\n\n\nWichtig: Obwohl T ein Enum ist, erfordert die Umwandlung (T)result immer noch eine explizite Umwandlung. Dies liegt daran, dass der Typ object zur Laufzeit nichts über den generischen Parameter weiß. Aber der Compiler garantiert, dass T ein Enum ist, daher ist die Umwandlung sicher.
Elegantere Variante mit Enum.TryParse<T>
\n\nAb .NET 5 gibt es eine Überladung