Frage Create Generische Methode, die T auf ein Enum beschränkt


Ich baue eine Funktion, um die Enum.Parse Konzept das

  • Ermöglicht das Analysieren eines Standardwerts, falls ein Enum-Wert nicht gefunden wird
  • Groß- / Kleinschreibung wird nicht berücksichtigt

Also habe ich folgendes geschrieben:

public static T GetEnumFromString<T>(string value, T defaultValue) where T : Enum
{
    if (string.IsNullOrEmpty(value)) return defaultValue;
    foreach (T item in Enum.GetValues(typeof(T)))
    {
        if (item.ToString().ToLower().Equals(value.Trim().ToLower())) return item;
    }
    return defaultValue;
}

Ich erhalte eine Fehlereinschränkung kann keine spezielle Klasse sein System.Enum.

Fair genug, aber gibt es einen Workaround, um eine generische Enum zu erlauben, oder werde ich das nachahmen müssen Parse Funktion und übergeben Sie einen Typ als Attribut, was die hässliche Box-Anforderung zu Ihrem Code zwingt.

BEARBEITEN Alle untenstehenden Vorschläge wurden sehr geschätzt, danke.

Habe mich entschieden (Ich habe die Schleife verlassen, um die Groß- / Kleinschreibung zu beachten - ich benutze das beim Parsen von XML)

public static class EnumUtils
{
    public static T ParseEnum<T>(string value, T defaultValue) where T : struct, IConvertible
    {
        if (!typeof(T).IsEnum) throw new ArgumentException("T must be an enumerated type");
        if (string.IsNullOrEmpty(value)) return defaultValue;

        foreach (T item in Enum.GetValues(typeof(T)))
        {
            if (item.ToString().ToLower().Equals(value.Trim().ToLower())) return item;
        }
        return defaultValue;
    }
}

BEARBEITEN: (16. Feb. 2015) Julien Lebosquain hat kürzlich gepostet Ein Compiler erzwingt typsichere generische Lösung in MSIL oder F # darunter, was einen Blick wert ist, und ein upvote. Ich werde diese Änderung entfernen, wenn die Lösung weiter oben auf der Seite erscheint.


945
2017-09-17 01:56


Ursprung


Antworten:


Schon seit Enum Geben Sie implementiert ein IConvertible Schnittstelle sollte eine bessere Implementierung in etwa so aussehen:

public T GetEnumFromString<T>(string value) where T : struct, IConvertible
{
   if (!typeof(T).IsEnum) 
   {
      throw new ArgumentException("T must be an enumerated type");
   }

   //...
}

Dies ermöglicht weiterhin die Übergabe von implementierenden Werttypen IConvertible. Die Chancen sind jedoch selten.


853
2017-09-17 04:13



Diese Funktion wird abschließend in C # 7.3 unterstützt!

Das folgende Snippet (von Die Dotnet-Samples) demonstriert es verwenden:

public static Dictionary<int, string> EnumNamedValues<T>() where T : System.Enum
{
    var result = new Dictionary<int, string>();
    var values = Enum.GetValues(typeof(T));

    foreach (int item in values)
        result.Add(item, Enum.GetName(typeof(T), item));
    return result;
}

Stellen Sie sicher, dass Ihre Sprachversion in Ihrem C # -Projekt auf Version 7.3 festgelegt ist.


Original Antwort unten:

Ich komme zu spät zum Spiel, aber ich habe es als eine Herausforderung angesehen, um zu sehen, wie es gemacht werden könnte. Es ist nicht möglich in C # (oder VB.NET, aber scrollen Sie nach unten für F #), aber ist möglich in MSIL. Ich habe diese kleine .... Sache geschrieben

// license: http://www.apache.org/licenses/LICENSE-2.0.html
.assembly MyThing{}
.class public abstract sealed MyThing.Thing
       extends [mscorlib]System.Object
{
  .method public static !!T  GetEnumFromString<valuetype .ctor ([mscorlib]System.Enum) T>(string strValue,
                                                                                          !!T defaultValue) cil managed
  {
    .maxstack  2
    .locals init ([0] !!T temp,
                  [1] !!T return_value,
                  [2] class [mscorlib]System.Collections.IEnumerator enumerator,
                  [3] class [mscorlib]System.IDisposable disposer)
    // if(string.IsNullOrEmpty(strValue)) return defaultValue;
    ldarg strValue
    call bool [mscorlib]System.String::IsNullOrEmpty(string)
    brfalse.s HASVALUE
    br RETURNDEF         // return default it empty

    // foreach (T item in Enum.GetValues(typeof(T)))
  HASVALUE:
    // Enum.GetValues.GetEnumerator()
    ldtoken !!T
    call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
    call class [mscorlib]System.Array [mscorlib]System.Enum::GetValues(class [mscorlib]System.Type)
    callvirt instance class [mscorlib]System.Collections.IEnumerator [mscorlib]System.Array::GetEnumerator() 
    stloc enumerator
    .try
    {
      CONDITION:
        ldloc enumerator
        callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
        brfalse.s LEAVE

      STATEMENTS:
        // T item = (T)Enumerator.Current
        ldloc enumerator
        callvirt instance object [mscorlib]System.Collections.IEnumerator::get_Current()
        unbox.any !!T
        stloc temp
        ldloca.s temp
        constrained. !!T

        // if (item.ToString().ToLower().Equals(value.Trim().ToLower())) return item;
        callvirt instance string [mscorlib]System.Object::ToString()
        callvirt instance string [mscorlib]System.String::ToLower()
        ldarg strValue
        callvirt instance string [mscorlib]System.String::Trim()
        callvirt instance string [mscorlib]System.String::ToLower()
        callvirt instance bool [mscorlib]System.String::Equals(string)
        brfalse.s CONDITION
        ldloc temp
        stloc return_value
        leave.s RETURNVAL

      LEAVE:
        leave.s RETURNDEF
    }
    finally
    {
        // ArrayList's Enumerator may or may not inherit from IDisposable
        ldloc enumerator
        isinst [mscorlib]System.IDisposable
        stloc.s disposer
        ldloc.s disposer
        ldnull
        ceq
        brtrue.s LEAVEFINALLY
        ldloc.s disposer
        callvirt instance void [mscorlib]System.IDisposable::Dispose()
      LEAVEFINALLY:
        endfinally
    }

  RETURNDEF:
    ldarg defaultValue
    stloc return_value

  RETURNVAL:
    ldloc return_value
    ret
  }
} 

Was erzeugt eine Funktion, die würde so aussehen, wenn es gültig wäre C #:

T GetEnumFromString<T>(string valueString, T defaultValue) where T : Enum

Dann mit dem folgenden C # -Code:

using MyThing;
// stuff...
private enum MyEnum { Yes, No, Okay }
static void Main(string[] args)
{
    Thing.GetEnumFromString("No", MyEnum.Yes); // returns MyEnum.No
    Thing.GetEnumFromString("Invalid", MyEnum.Okay);  // returns MyEnum.Okay
    Thing.GetEnumFromString("AnotherInvalid", 0); // compiler error, not an Enum
}

Leider bedeutet dies, dass dieser Teil Ihres Codes in MSIL anstatt in C # geschrieben ist, mit dem einzigen zusätzlichen Vorteil, dass Sie diese Methode einschränken können System.Enum. Es ist auch eine Art Mist, weil es in eine separate Assembly kompiliert wird. Es bedeutet jedoch nicht, dass Sie es auf diese Weise bereitstellen müssen.

Durch Entfernen der Linie .assembly MyThing{} und den ilasm wie folgt aufrufen:

ilasm.exe /DLL /OUTPUT=MyThing.netmodule

Sie erhalten ein Netmodul statt einer Baugruppe.

Leider unterstützt VS2010 (und früher, natürlich) das Hinzufügen von Netzmodulverweisen nicht, was bedeutet, dass Sie beim Debuggen in zwei separaten Assemblys bleiben müssen. Die einzige Möglichkeit, sie als Teil Ihrer Assembly hinzuzufügen, besteht darin, csc.exe mit dem Befehl selbst auszuführen /addmodule:{files} Befehlszeilenargument. Das wäre es nicht auch schmerzhaft in einem MSBuild-Skript. Natürlich, wenn Sie mutig oder dumm sind, können Sie csc jedes Mal manuell ausführen. Und es wird sicherlich komplizierter, da mehrere Baugruppen Zugriff darauf benötigen.

Also, es kann in .Net getan werden. Lohnt sich der zusätzliche Aufwand? Ähm, nun, ich denke ich werde dich entscheiden lassen.


F # Lösung als Alternative

Extra Credit: Es stellt sich heraus, dass eine generische Einschränkung auf enum ist in mindestens einer anderen .NET-Sprache neben MSIL möglich: F #.

type MyThing =
    static member GetEnumFromString<'T when 'T :> Enum> str defaultValue: 'T =
        /// protect for null (only required in interop with C#)
        let str = if isNull str then String.Empty else str

        Enum.GetValues(typedefof<'T>)
        |> Seq.cast<_>
        |> Seq.tryFind(fun v -> String.Compare(v.ToString(), str.Trim(), true) = 0)
        |> function Some x -> x | None -> defaultValue

Dieser ist leichter zu pflegen, da es eine bekannte Sprache mit voller Visual Studio-IDE-Unterstützung ist, aber Sie benötigen dafür noch ein separates Projekt in Ihrer Lösung. Es erzeugt jedoch natürlich erheblich andere IL (der Code ist sehr unterschiedlich) und es beruht auf der FSharp.Core Bibliothek, die genau wie jede andere externe Bibliothek Teil Ihrer Distribution werden muss.

Hier ist, wie Sie es verwenden können (im Grunde das gleiche wie die MSIL-Lösung), und um zu zeigen, dass es auch auf anderen Strukturen korrekt ausfällt:

// works, result is inferred to have type StringComparison
var result = MyThing.GetEnumFromString("OrdinalIgnoreCase", StringComparison.Ordinal);
// type restriction is recognized by C#, this fails at compile time
var result = MyThing.GetEnumFromString("OrdinalIgnoreCase", 42);

419
2017-11-10 21:46



C # ≥ 7.3

Ab C # 7.3 (verfügbar ab Visual Studio 2017 ≥ v15.7) ist dieser Code nun vollständig gültig:

public static TEnum Parse<TEnum>(string value)
where TEnum : struct, Enum { ... }

C # ≤ 7.2

Sie können eine echte Enumerationsbeschränkung für den Compiler erzwingen, indem Sie die Constraint-Vererbung missbrauchen. Der folgende Code gibt beide a an class und ein struct Einschränkungen gleichzeitig:

public abstract class EnumClassUtils<TClass>
where TClass : class
{

    public static TEnum Parse<TEnum>(string value)
    where TEnum : struct, TClass
    {
        return (TEnum) Enum.Parse(typeof(TEnum), value);
    }

}

public class EnumUtils : EnumClassUtils<Enum>
{
}

Verwendung:

EnumUtils.Parse<SomeEnum>("value");

Hinweis: Dies wird speziell in der C # 5.0-Sprachspezifikation angegeben:

Wenn der Typparameter S vom Typparameter T abhängt, dann:   [...] Es ist gültig für   S, um den Werttyp constraint und T den Referenztyp zu haben   Zwang. Dies begrenzt T effektiv auf die Typen System.Object,   System.ValueType, System.Enum und ein beliebiger Schnittstellentyp.


149
2018-02-15 15:16



Bearbeiten

Die Frage wurde jetzt hervorragend beantwortet Julien Lebosquain. Ich möchte auch seine Antwort mit erweitern ignoreCase, defaultValue und optionale Argumente beim Hinzufügen TryParse und ParseOrDefault.

public abstract class ConstrainedEnumParser<TClass> where TClass : class
// value type constraint S ("TEnum") depends on reference type T ("TClass") [and on struct]
{
    // internal constructor, to prevent this class from being inherited outside this code
    internal ConstrainedEnumParser() {}
    // Parse using pragmatic/adhoc hard cast:
    //  - struct + class = enum
    //  - 'guaranteed' call from derived <System.Enum>-constrained type EnumUtils
    public static TEnum Parse<TEnum>(string value, bool ignoreCase = false) where TEnum : struct, TClass
    {
        return (TEnum)Enum.Parse(typeof(TEnum), value, ignoreCase);
    }
    public static bool TryParse<TEnum>(string value, out TEnum result, bool ignoreCase = false, TEnum defaultValue = default(TEnum)) where TEnum : struct, TClass // value type constraint S depending on T
    {
        var didParse = Enum.TryParse(value, ignoreCase, out result);
        if (didParse == false)
        {
            result = defaultValue;
        }
        return didParse;
    }
    public static TEnum ParseOrDefault<TEnum>(string value, bool ignoreCase = false, TEnum defaultValue = default(TEnum)) where TEnum : struct, TClass // value type constraint S depending on T
    {
        if (string.IsNullOrEmpty(value)) { return defaultValue; }
        TEnum result;
        if (Enum.TryParse(value, ignoreCase, out result)) { return result; }
        return defaultValue;
    }
}

public class EnumUtils: ConstrainedEnumParser<System.Enum>
// reference type constraint to any <System.Enum>
{
    // call to parse will then contain constraint to specific <System.Enum>-class
}

Anwendungsbeispiele:

WeekDay parsedDayOrArgumentException = EnumUtils.Parse<WeekDay>("monday", ignoreCase:true);
WeekDay parsedDayOrDefault;
bool didParse = EnumUtils.TryParse<WeekDay>("clubs", out parsedDayOrDefault, ignoreCase:true);
parsedDayOrDefault = EnumUtils.ParseOrDefault<WeekDay>("friday", ignoreCase:true, defaultValue:WeekDay.Sunday);

Alt

Meine alten Verbesserungen auf Viveks Antwort mit den Kommentaren und 'neuen' Entwicklungen:

  • benutzen TEnum für die Klarheit der Benutzer
  • Fügen Sie weitere Interface-Constraints für zusätzliche Constraint-Checks hinzu
  • Lassen TryParse Griff ignoreCase mit dem vorhandenen Parameter (eingeführt in VS2010 / .Net 4)
  • Verwenden Sie optional das generische default Wert (eingeführt in VS2005 / .Net 2)
  • benutzen optionale Argumente(eingeführt in VS2010 / .Net 4) mit Standardwerten, z defaultValue und ignoreCase

ergebend:

public static class EnumUtils
{
    public static TEnum ParseEnum<TEnum>(this string value,
                                         bool ignoreCase = true,
                                         TEnum defaultValue = default(TEnum))
        where TEnum : struct,  IComparable, IFormattable, IConvertible
    {
        if ( ! typeof(TEnum).IsEnum) { throw new ArgumentException("TEnum must be an enumerated type"); }
        if (string.IsNullOrEmpty(value)) { return defaultValue; }
        TEnum lResult;
        if (Enum.TryParse(value, ignoreCase, out lResult)) { return lResult; }
        return defaultValue;
    }
}

29
2018-05-24 14:07



Sie können einen statischen Konstruktor für die Klasse definieren, der prüft, ob der Typ T eine Aufzählung ist, und eine Ausnahme auslösen, falls dies nicht der Fall ist. Dies ist die Methode, die Jeffery Richter in seinem Buch CLR via C # erwähnt hat.

internal sealed class GenericTypeThatRequiresAnEnum<T> {
    static GenericTypeThatRequiresAnEnum() {
        if (!typeof(T).IsEnum) {
        throw new ArgumentException("T must be an enumerated type");
        }
    }
}

Dann können Sie in der Parse-Methode einfach Enum.Parse (typeof (T), input, true) verwenden, um von string in enum zu konvertieren. Der letzte wahre Parameter ist, den Fall der Eingabe zu ignorieren.


18
2017-09-17 02:32



Ich modifizierte die Probe durch Dimarzionist. Diese Version funktioniert nur mit Enums und lässt Strukturen nicht durchkommen.

public static T ParseEnum<T>(string enumString)
    where T : struct // enum 
    {
    if (String.IsNullOrEmpty(enumString) || !typeof(T).IsEnum)
       throw new Exception("Type given must be an Enum");
    try
    {

       return (T)Enum.Parse(typeof(T), enumString, true);
    }
    catch (Exception ex)
    {
       return default(T);
    }
}

11
2017-09-17 02:24



Ich habe versucht, den Code ein wenig zu verbessern:

public T LoadEnum<T>(string value, T defaultValue = default(T)) where T : struct, IComparable, IFormattable, IConvertible
{
    if (Enum.IsDefined(typeof(T), value))
    {
        return (T)Enum.Parse(typeof(T), value, true);
    }
    return defaultValue;
}

9
2017-12-16 11:24



Es sollte auch berücksichtigt werden, dass die Veröffentlichung von C # 7.3 unter Verwendung von Enum-Constraints sofort unterstützt wird, ohne dass zusätzliche Prüfungen und ähnliches erforderlich sind.

Wenn Sie also die Sprachversion Ihres Projekts in C # 7.3 ändern, wird der folgende Code einwandfrei funktionieren:

    private static T GetEnumFromString<T>(string value, T defaultValue) where T : Enum
    {
        // Your code goes here...
    }

Falls Sie nicht wissen, wie Sie die Sprachversion in C # 7.3 ändern können, sehen Sie den folgenden Screenshot: enter image description here

EDIT 1 - Benötigte Visual Studio Version und ReSharper

Damit Visual Studio die neue Syntax erkennt, benötigen Sie mindestens Version 15.7. Sie finden das auch in den Microsoft-Versionshinweisen, siehe Visual Studio 2017 15.7 Versionshinweise. Danke @MohamedElshawaf für das Aufzeigen dieser gültigen Frage.

Bitte beachten Sie auch, dass in meinem Fall ReSharper 2018.1 zum Schreiben dieses EDIT noch nicht C # 7.3 unterstützt. Wenn ReSharper aktiviert ist, wird die Enum-Einschränkung als Fehler angezeigt 'System.Array', 'System.Delegate', 'System.Enum', 'System.ValueType', 'Objekt' kann nicht als Typ-Parameter-Einschränkung verwendet werden. ReSharper schlägt als schnelle Lösung vor Entfernen Sie die 'Enum'-Einschränkung des Typparameters T der Methode 

Allerdings, wenn Sie ReSharper vorübergehend unter deaktivieren Extras -> Optionen -> ReSharper Ultimate -> Allgemein Sie werden feststellen, dass die Syntax vollkommen korrekt ist, wenn Sie VS 15.7 oder höher und C # 7.3 oder höher verwenden.


6
2018-05-10 09:55