Pertanyaan Buat metode Generik yang membatasi T menjadi Enum


Saya sedang membangun fungsi untuk memperpanjang Enum.Parse konsep itu

  • Memungkinkan nilai default diuraikan jika nilai Enum tidak ditemukan
  • Adalah case sensitive

Jadi saya menulis sebagai berikut:

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

Saya mendapatkan Hambatan Kesalahan tidak bisa menjadi kelas khusus System.Enum.

Cukup adil, tetapi apakah ada solusi untuk memungkinkan Generik Enum, atau saya harus meniru Parse berfungsi dan lulus tipe sebagai atribut, yang memaksa persyaratan tinju jelek untuk kode Anda.

EDIT Semua saran di bawah ini sangat kami hargai, terima kasih.

Telah menetap di (saya telah meninggalkan loop untuk menjaga ketidaksensitifan kasus - Saya menggunakan ini ketika mengurai 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;
    }
}

EDIT: (16th Feb 2015) Julien Lebosquain baru-baru ini memposting compiler enforced type-safe generic solution dalam MSIL atau F # di bawah ini, yang layak dilihat, dan upvote. Saya akan menghapus suntingan ini jika solusi menggelembung lebih jauh ke atas halaman.


945
2017-09-17 01:56


asal


Jawaban:


Sejak Enum Ketik alat IConvertible antarmuka, implementasi yang lebih baik harus seperti ini:

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

   //...
}

Ini masih akan mengizinkan melewati jenis nilai yang diimplementasikan IConvertible. Kemungkinannya sangat langka.


853
2017-09-17 04:13



Fitur ini akhirnya didukung dalam C # 7.3!

Potongan berikut (dari sampel dotnet) menunjukkannya menggunakan:

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

Pastikan untuk mengatur versi bahasa Anda dalam proyek C # Anda ke versi 7.3.


Jawaban Asli di bawah ini:

Saya terlambat ke permainan, tetapi saya menganggapnya sebagai tantangan untuk melihat bagaimana hal itu bisa dilakukan. Ini tidak mungkin di C # (atau VB.NET, tetapi gulir ke bawah untuk F #), tetapi adalah mungkin di MSIL. Saya menulis hal kecil .... ini

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

Yang menghasilkan fungsi itu akan terlihat seperti ini, jika itu benar C #:

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

Kemudian dengan kode C # berikut:

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
}

Sayangnya, ini berarti Anda memiliki bagian kode ini yang ditulis dalam MSIL alih-alih C #, dengan satu-satunya manfaat tambahan adalah Anda dapat membatasi metode ini dengan System.Enum. Ini juga agak mengecewakan, karena itu dikompilasi menjadi majelis terpisah. Namun, itu tidak berarti Anda harus menerapkannya seperti itu.

Dengan menghapus garis .assembly MyThing{} dan memohon ilasm sebagai berikut:

ilasm.exe /DLL /OUTPUT=MyThing.netmodule

Anda mendapatkan netmodule bukan perakitan.

Sayangnya, VS2010 (dan sebelumnya, tentu saja) tidak mendukung penambahan referensi netmodule, yang berarti Anda harus meninggalkannya dalam 2 majelis terpisah ketika Anda melakukan debugging. Satu-satunya cara Anda dapat menambahkannya sebagai bagian dari perakitan Anda adalah dengan menjalankan csc.exe sendiri menggunakan /addmodule:{files} argumen baris perintah. Itu tidak akan terjadi terlalu menyakitkan dalam skrip MSBuild. Tentu saja, jika Anda berani atau bodoh, Anda dapat menjalankan csc sendiri secara manual setiap saat. Dan tentu saja menjadi lebih rumit karena banyak majelis membutuhkan akses ke sana.

Jadi, BISA dilakukan di. Bersih. Apakah itu sepadan dengan usaha ekstra? Um, baik, saya kira saya akan membiarkan Anda memutuskan yang satu itu.


F # Solution sebagai alternatif

Extra Credit: Ternyata ada pembatasan generik enum dimungkinkan dalam setidaknya satu bahasa .NET lainnya selain MSIL: 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

Yang ini lebih mudah untuk dipelihara karena ini adalah bahasa yang terkenal dengan dukungan penuh Visual Studio IDE, tetapi Anda masih memerlukan proyek terpisah dalam solusi Anda untuk itu. Namun, secara alami menghasilkan IL (kode yang sangat berbeda) aku s sangat berbeda) dan itu bergantung pada FSharp.Core perpustakaan, yang sama seperti perpustakaan eksternal lainnya, perlu menjadi bagian dari distribusi Anda.

Berikut ini bagaimana Anda dapat menggunakannya (pada dasarnya sama dengan solusi MSIL), dan untuk menunjukkan bahwa itu benar gagal pada struct yang bersinonim:

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

Dimulai dengan C # 7.3 (tersedia dengan Visual Studio 2017 ≥ v15.7), kode ini sekarang benar-benar valid:

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

C # ≤ 7.2

Anda dapat memiliki kendala enum diberlakukan nyata kompilator dengan menyalahgunakan warisan kendala. Kode berikut menetapkan keduanya a class dan a struct kendala pada saat yang bersamaan:

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

Pemakaian:

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

Catatan: ini secara khusus dinyatakan dalam spesifikasi bahasa C # 5.0:

Jika parameter tipe S tergantung pada parameter tipe T maka:   [...] Ini berlaku untuk   S memiliki batasan tipe nilai dan T untuk memiliki tipe referensi   paksaan. Secara efektif ini membatasi T ke tipe System.Object,   System.ValueType, System.Enum, dan jenis antarmuka apa pun.


149
2018-02-15 15:16



Edit

Pertanyaannya sekarang telah dijawab dengan luar biasa Julien Lebosquain. Saya juga ingin menyampaikan jawabannya ignoreCase, defaultValue dan argumen opsional, sambil menambahkan TryParse dan 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
}

Contoh penggunaan:

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

Tua

Perbaikan lama saya Jawaban Vivek dengan menggunakan komentar dan perkembangan 'baru':

  • menggunakan TEnum untuk kejelasan bagi pengguna
  • menambahkan lebih banyak kendala antarmuka untuk pemeriksaan kendala tambahan
  • membiarkan TryParse menangani ignoreCase dengan parameter yang ada (diperkenalkan di VS2010 / .Net 4)
  • opsional menggunakan generik default nilai (diperkenalkan di VS2005 / .Net 2)
  • menggunakan argumen opsional(diperkenalkan di VS2010 / .Net 4) dengan nilai default, untuk defaultValue dan ignoreCase

yang menghasilkan:

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



Anda dapat mendefinisikan konstruktor statis untuk kelas yang akan memeriksa bahwa tipe T adalah enum dan melempar pengecualian jika tidak. Ini adalah metode yang disebutkan oleh Jeffery Richter dalam bukunya CLR via C #.

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

Kemudian dalam metode parse, Anda bisa menggunakan Enum.Parse (typeof (T), input, true) untuk mengkonversi dari string ke enum. Parameter sebenarnya yang terakhir adalah untuk mengabaikan kasus input.


18
2017-09-17 02:32



Saya memodifikasi sampel dengan dimarzionist. Versi ini hanya akan berfungsi dengan Enums dan tidak membiarkan struct selesai.

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



Saya mencoba memperbaiki kode sedikit:

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