1445 Stimmen

Generische Methode erstellen, die T auf ein Enum einschränkt

Ich baue eine Funktion zur Erweiterung der Enum.Parse Konzept, das

  • Ermöglicht das Parsen eines Standardwerts für den Fall, dass ein Enum-Wert nicht gefunden wird
  • Groß- und Kleinschreibung wird nicht berücksichtigt

Also habe ich das Folgende 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 einen Fehler Constraint kann nicht spezielle Klasse sein System.Enum .

Fair genug, aber gibt es einen Workaround, um eine Generic Enum zu ermöglichen, oder muss ich die Parse Funktion und übergeben Sie einen Typ als Attribut, was die hässliche Boxing-Anforderung in Ihrem Code erzwingt.

EDITAR Für alle nachstehenden Vorschläge sind wir sehr dankbar, danke.

Haben sich auf (Ich habe die Schleife verlassen, um Groß-und Kleinschreibung zu erhalten - ich bin mit diesem 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;
    }
}

EDITAR: (16. Februar 2015) Christopher Currens hat veröffentlicht eine vom Compiler erzwungene typsichere generische Lösung in MSIL oder F# unten, die einen Blick und eine positive Bewertung wert ist. Ich werde diese Bearbeitung entfernen, wenn die Lösung weiter oben auf der Seite auftaucht.

EDIT 2: (13. April 2021) Da dies nun seit C# 7.3 angesprochen und unterstützt wird, habe ich die akzeptierte Antwort geändert, obwohl eine vollständige Durchsicht der wichtigsten Antworten aus akademischem und historischem Interesse lohnenswert ist :)

12 Stimmen

Vielleicht sind Sie verwenden sollten ToUpperInvariant() anstelle von ToLower()...

0 Stimmen

Warum gibt es Erweiterungsmethoden nur für Referenztypen?

34 Stimmen

@Shimmy: Sobald Sie einen Werttyp an die Erweiterungsmethode übergeben, arbeiten Sie an einer Kopie davon, so dass Sie seinen Zustand nicht ändern können.

1104voto

Vivek Punkte 16076

Seit Enum Typ implementiert 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 Werttypen, die IConvertible . Die Chancen sind jedoch selten.

0 Stimmen

Dies scheint nur vs2008 und neuer zu sein, richtig? oder vielleicht ist es einfach nicht in vb2005?

2 Stimmen

Generics sind seit .NET 2.0 verfügbar. Daher sind sie auch in vb 2005 verfügbar.

51 Stimmen

Wenn Sie sich für diesen Weg entscheiden, dann machen Sie es noch eingeschränkter... verwenden Sie "class TestClass<T> where T : struct, IComparable, IFormattable, IConvertible"

939voto

Christopher Currens Punkte 28587

Diese Funktion wird endlich in C# 7.3 unterstützt!

Der folgende Ausschnitt (aus die Dotnet-Beispiele ) zeigt, wie:

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 die Sprachversion in Ihrem C#-Projekt auf Version 7.3 eingestellt ist.


Originalantwort unten:

Ich bin zwar spät dran, aber ich habe es als Herausforderung angenommen, um zu sehen, wie es gemacht werden kann. 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 dieses kleine .... 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
  }
} 

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

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 Sie diesen Teil Ihres Codes in MSIL statt in C# schreiben müssen, wobei der einzige zusätzliche Vorteil darin besteht, dass Sie diese Methode durch System.Enum . Es ist auch eine Art von Schade, weil es in eine separate Baugruppe kompiliert wird. Das bedeutet jedoch nicht, dass Sie es auf diese Weise bereitstellen müssen.

Durch Entfernen der Zeile .assembly MyThing{} und ruft ilasm wie folgt auf:

ilasm.exe /DLL /OUTPUT=MyThing.netmodule

erhalten Sie ein Netzmodul anstelle einer Baugruppe.

Leider unterstützt VS2010 (und früher, offensichtlich) nicht das Hinzufügen von Netmodule Referenzen, was bedeutet, dass Sie es in 2 separaten Assemblys verlassen müssen, wenn Sie debuggen. Der einzige Weg, wie Sie sie als Teil Ihrer Assembly hinzufügen können, wäre, csc.exe selbst auszuführen, indem Sie die /addmodule:{files} Befehlszeilenargument. Es wäre nicht zu schmerzhaft in einem MSBuild-Skript. Wenn Sie mutig oder dumm sind, können Sie csc natürlich auch jedes Mal manuell ausführen. Und es wird sicherlich komplizierter, wenn mehrere Assemblies darauf zugreifen müssen.

Es KANN also in .Net gemacht werden. Ist es den zusätzlichen Aufwand wert? Ähm, nun, ich denke, das müssen Sie selbst entscheiden.


F# Lösung als Alternative

Extra Credit: Es hat sich herausgestellt, dass eine generische Beschränkung auf enum ist neben MSIL auch in mindestens einer anderen .NET-Sprache 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

Diese ist einfacher zu pflegen, da es sich um eine bekannte Sprache mit vollständiger Unterstützung der Visual Studio IDE handelt, aber Sie benötigen trotzdem ein separates Projekt in Ihrer Lösung dafür. Allerdings erzeugt sie natürlich deutlich anderes IL (der Code es sehr unterschiedlich) und stützt sich auf die FSharp.Core Bibliothek, die, 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 zu zeigen, dass es korrekt auf sonst synonyme Strukturen fehlschlägt:

// 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);

79 Stimmen

Ja, sehr Hardcore. Ich habe den allergrößten Respekt vor jemandem, der in IL programmieren kann, y wissen, wie die Funktionen auf der höheren Sprachebene unterstützt werden - einer Ebene, die viele von uns immer noch als niedrige Ebene unter Anwendungen, Geschäftsregeln, Benutzeroberflächen, Komponentenbibliotheken usw. betrachten.

2 Stimmen

@ruslan - es sagt, dass Sie dies nicht in c# im ersten Absatz der Antwort erreichen können. Das ist eigentlich, was diese Antwort zeigt: sehr möglich in cil (da der Code oben erfolgreich funktioniert, wenn in anderen .net-Sprachen verwendet), jedoch nicht möglich in C# selbst.

13 Stimmen

Was mich wirklich interessieren würde, ist, warum das C#-Team dies noch nicht erlaubt hat, da es bereits von MSIL unterstützt wird.

271voto

Julien Lebosquain Punkte 39679

C# 7.3

Ab C# 7.3 (verfügbar mit 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 vom Compiler erzwungene Enum-Beschränkung haben, indem Sie die Beschränkungsvererbung missbrauchen. Der folgende Code spezifiziert sowohl eine class und eine struct Zwänge zur gleichen Zeit:

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 ist ausdrücklich in der C# 5.0 Sprachspezifikation festgelegt:

Wenn der Typparameter S vom Typparameter T abhängt, dann: [...] Es ist gültig, dass S die Werttyp-Beschränkung und T die Referenztyp Einschränkung. Dadurch wird T auf die Typen System.Object, System.ValueType, System.Enum, und jeden Schnittstellentyp.

0 Stimmen

Könnten Sie außerdem Ihre Antwort etwas genauer erläutern? Der von Ihnen eingefügte Kommentar aus der Sprachspezifikation besagt, dass der Enum-Typ System.Object, System.FalueType, System.Enum oder eine beliebige Schnittstelle sein muss. Inwiefern ist er darüber hinaus auf den Typ System.Enum beschränkt? Müssten Sie nicht auch public class EnumUtils : EnumClassUtils<Enum> where Enum : struct, IConvertible ? danke.

9 Stimmen

@DavidI.McIntosh EnumClassUtils<System.Enum> reicht aus, um T auf beliebige System.Enum und alle abgeleiteten Typen. struct en Parse schränkt es dann weiter auf einen echten Aufzählungstyp ein. Sie müssen einschränken auf Enum zu einem bestimmten Zeitpunkt. Dazu muss Ihre Klasse verschachtelt sein. Siehe gist.github.com/MrJul/7da12f5f2d6c69f03d79

0 Stimmen

Ah, jetzt verstehe ich, wie es funktioniert. Dann muss die Verwendung der Klasse immer über einen Verweis auf sie als verschachtelte Klasse erfolgen - nicht angenehm, aber ich denke, das ist das Beste, was man sich erhoffen kann. Danke übrigens für die schnelle Antwort.

36voto

DiskJunky Punkte 4538

Die vorhandenen Antworten sind ab C# <=7.2 richtig. Allerdings gibt es eine C#-Sprache Feature-Anfrage (verbunden mit einer corefx Feature Request), um Folgendes zu ermöglichen;

public class MyGeneric<TEnum> where TEnum : System.Enum
{ }

Zum Zeitpunkt der Abfassung dieses Artikels wird das Feature bei den Treffen zur Sprachentwicklung "diskutiert".

EDITAR

Gemäß nawfal Info, dies wird in C# eingeführt 7.3 .

EDITAR 2

Dies ist jetzt in C# 7.3 vorwärts ( Versionshinweise )

Beispiel;

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;
}

2 Stimmen

Interessante Diskussion, danke. Allerdings ist noch nichts in Stein gemeißelt (bis jetzt)

1 Stimmen

@johnc, sehr wahr, aber eine Anmerkung wert und es es eine häufig nachgefragte Funktion. Die Chancen stehen gut, dass es kommt.

2 Stimmen

Dies wird in C# 7.3 eingeführt: docs.microsoft.com/de-us/visualstudio/releasenotes/ . :)

35voto

Yahoo Serious Punkte 3360

Editar

Diese Frage wurde nun in hervorragender Weise beantwortet durch Julien Lebosquain . Ich möchte seine Antwort noch erweitern um ignoreCase , defaultValue und optionale Argumente, während das Hinzufügen von TryParse y 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
}

Beispiele für die Verwendung:

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);

Alte

Meine alten Verbesserungen an Vivek's Antwort durch die Verwendung der Kommentare und "neuen" Entwicklungen:

  • utiliser TEnum zur Klarheit für die Nutzer
  • Hinzufügen weiterer Schnittstellenbeschränkungen für zusätzliche Beschränkungsprüfungen
  • lassen Sie TryParse Griff ignoreCase mit dem vorhandenen Parameter (eingeführt in VS2010/.Net 4)
  • optional die generische default Wert (eingeführt in VS2005/.Net 2)
  • utiliser optionale Argumente (eingeführt in VS2010/.Net 4) mit Standardwerten, für defaultValue y ignoreCase

was dazu führt:

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;
    }
}

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X