c# - type - where t struct




創建通用方法約束T到一個枚舉 (12)

C#<= 7.2的現有答案是正確的。 但是,有一個C#語言功能請求 (綁定到corefx功能請求)允許以下操作;

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

在撰寫本文時,該功能是在語言開發會議上“討論”。

編輯

根據的信息,這是在C# 7.3引入的。

我正在構建一個擴展Enum.Parse概念的函數

  • 允許在未找到枚舉值的情況下解析默認值
  • 不區分大小寫

所以我寫了以下內容:

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

我得到一個錯誤約束不能是特殊類'System.Enum'。

公平的,但是有一個解決方法,以允許通用枚舉,或者我將不得不模仿Parse函數,並將一個類型作為屬性傳遞,這將迫使醜陋的拳擊要求到您的代碼。

編輯下面的所有建議都非常感謝,謝謝。

已經解決了(我已經離開了循環來保持大小寫不敏感 - 我解析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;
    }
}

編輯: (2015年2月16日)Julien Lebosquain最近在MSIL或F#中發布了一個編譯器強制類型安全的通用解決方案 ,這非常值得一看,並且是upvote。 如果解決方案在頁面上進一步起泡,我將刪除此編輯。


編輯

Julien Lebosquain現在回答了這個問題。 我還想用ignoreCasedefaultValue和可選參數來擴展他的答案,同時添加TryParseParseOrDefault

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
}

用法示例:

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

我通過使用評論和“新”發展對share進行了一些改進:

  • 為了使用戶清晰,請使用TEnum
  • 為額外的約束檢查添加更多的接口約束
  • TryParse處理ignoreCase與現有參數(在VS2010 / .Net 4中引入)
  • 可選地使用通用default (在VS2005 / .Net 2中引入)
  • 使用默認值的可選參數 (在VS2010 / .Net 4中引入),用於defaultValueignoreCase

導致:

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

希望這有幫助:

public static TValue ParseEnum<TValue>(string value, TValue defaultValue)
                  where TValue : struct // enum 
{
      try
      {
            if (String.IsNullOrEmpty(value))
                  return defaultValue;
            return (TValue)Enum.Parse(typeof (TValue), value);
      }
      catch(Exception ex)
      {
            return defaultValue;
      }
}

您可以為該類定義一個靜態構造函數,該構造函數將檢查類型T是否為枚舉,如果不是,則拋出異常。 這是Jeffery Richter在他的書CLR via C#中提到的方法。

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

然後在解析方法中,您可以使用Enum.Parse(typeof(T),input,true)將字符串轉換為枚舉。 最後一個真實的參數是忽略輸入的情況。


我創建了一個擴展方法to get integer value from enum看看方法的實現

public static int ToInt<T>(this T soure) where T : IConvertible//enum
{
    if (typeof(T).IsEnum)
    {
        return (int) (IConvertible)soure;// the tricky part
    }
    //else
    //    throw new ArgumentException("T must be an enumerated type");
    return soure.ToInt32(CultureInfo.CurrentCulture);
}

這是用法

MemberStatusEnum.Activated.ToInt()// using extension Method
(int) MemberStatusEnum.Activated //the ordinary way

我喜歡使用IL的Christopher Currens的解決方案,但對於那些不想處理將MSIL納入其構建過程的棘手業務的人,我在C#中編寫了類似的功能。

請注意,雖然你不能像where T : Enum那樣使用泛型限制where T : Enum因為Enum是特殊類型。 因此我必須檢查給定的泛型類型是否真的枚舉。

我的功能是:

public static T GetEnumFromString<T>(string strValue, T defaultValue)
{
    // Check if it realy enum at runtime 
    if (!typeof(T).IsEnum)
        throw new ArgumentException("Method GetEnumFromString can be used with enums only");

    if (!string.IsNullOrEmpty(strValue))
    {
        IEnumerator enumerator = Enum.GetValues(typeof(T)).GetEnumerator();
        while (enumerator.MoveNext())
        {
            T temp = (T)enumerator.Current;
            if (temp.ToString().ToLower().Equals(strValue.Trim().ToLower()))
                return temp;
        }
    }

    return defaultValue;
}

我確實有特定的要求,我需要使用enum與enum值相關聯的文本。 例如,當我使用枚舉指定錯誤類型時,需要描述錯誤細節。

public static class XmlEnumExtension
{
    public static string ReadXmlEnumAttribute(this Enum value)
    {
        if (value == null) throw new ArgumentNullException("value");
        var attribs = (XmlEnumAttribute[]) value.GetType().GetField(value.ToString()).GetCustomAttributes(typeof (XmlEnumAttribute), true);
        return attribs.Length > 0 ? attribs[0].Name : value.ToString();
    }

    public static T ParseXmlEnumAttribute<T>(this string str)
    {
        foreach (T item in Enum.GetValues(typeof(T)))
        {
            var attribs = (XmlEnumAttribute[])item.GetType().GetField(item.ToString()).GetCustomAttributes(typeof(XmlEnumAttribute), true);
            if(attribs.Length > 0 && attribs[0].Name.Equals(str)) return item;
        }
        return (T)Enum.Parse(typeof(T), str, true);
    }
}

public enum MyEnum
{
    [XmlEnum("First Value")]
    One,
    [XmlEnum("Second Value")]
    Two,
    Three
}

 static void Main()
 {
    // Parsing from XmlEnum attribute
    var str = "Second Value";
    var me = str.ParseXmlEnumAttribute<MyEnum>();
    System.Console.WriteLine(me.ReadXmlEnumAttribute());
    // Parsing without XmlEnum
    str = "Three";
    me = str.ParseXmlEnumAttribute<MyEnum>();
    System.Console.WriteLine(me.ReadXmlEnumAttribute());
    me = MyEnum.One;
    System.Console.WriteLine(me.ReadXmlEnumAttribute());
}

我總是喜歡這個(你可以適當修改):

public static IEnumerable<TEnum> GetEnumValues()
{
  Type enumType = typeof(TEnum);

  if(!enumType.IsEnum)
    throw new ArgumentException("Type argument must be Enum type");

  Array enumValues = Enum.GetValues(enumType);
  return enumValues.Cast<TEnum>();
}

我通過dimarzionist修改了樣本。 這個版本只適用於Enums,不能讓結構通過。

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

我遲到了,但我把它看作是一個挑戰,看看它是如何完成的。 在C#(或VB.NET,但向下滾動F#)中是不可能的 ,但在MSIL中可能 。 我寫了這個小東西

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

它生成一個看起來像這樣的函數,如果它是有效的C#:

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

然後用下面的C#代碼:

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
}

不幸的是,這意味著將這部分代碼寫入MSIL而不是C#,唯一的附加好處是可以通過System.Enum約束此方法。 這也是一種無賴,因為它被編譯成單獨的程序集。 但是,這並不意味著您必須以這種方式進行部署。

通過刪除行.assembly MyThing{}並調用ilasm,如下所示:

ilasm.exe /DLL /OUTPUT=MyThing.netmodule

你會得到一個netmodule而不是一個程序集。

不幸的是,VS2010(以及更早的版本,顯然)不支持添加netmodule引用,這意味著當您調試時,您必須將其保留在2個獨立的程序集中。 您可以將它們添加為程序集的一部分的唯一方法是使用/addmodule:{files}命令行參數自己運行csc.exe。 在MSBuild腳本中這不會痛苦。 當然,如果你勇敢或愚蠢,你可以每次手動運行csc。 由於多個程序集需要訪問它,它肯定會變得更加複雜。

所以,它可以在.Net中完成。 這值得額外的努力嗎? 恩,好吧,我想我會讓你決定那個。

F#解決方案作為替代

額外的信用:事實證明,除了MSIL以外,至少還有一種.NET語言可以對enum進行通用限制: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

由於這是一種具有完整Visual Studio IDE支持的著名語言,因此它更容易維護,但您仍然需要在解決方案中使用單獨的項目。 但是,它自然會產生相當不同的IL(代碼非常不同),並且它依賴於FSharp.Core庫,該庫與任何其他外部庫一樣,都需要成為發行版的一部分。

以下是您如何使用它(基本上與MSIL解決方案相同),並顯示它在其他同義詞結構上正確失敗:

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

正如其他答案中所述; 雖然這不能用源代碼來表達,但它實際上可以在IL Level上完成。 @Christopher Currens的answer顯示了IL如何做到這一點。

使用Fody的Add-In ExtraConstraints.Fody有一個非常簡單的方法,通過構建工具來完成這一任務。 只需將他們的nuget軟件包(Fody, ExtraConstraints.Fody )添加到您的項目中,並按如下方式添加約束(摘自ExtraConstraints的自述文件):

public void MethodWithEnumConstraint<[EnumConstraint] T>() {...}

public void MethodWithTypeEnumConstraint<[EnumConstraint(typeof(ConsoleColor))] T>() {...}

並且Fody將添加必要的IL以使約束存在。 另請注意約束代表的附加功能:

public void MethodWithDelegateConstraint<[DelegateConstraint] T> ()
{...}

public void MethodWithTypeDelegateConstraint<[DelegateConstraint(typeof(Func<int>))] T> ()
{...}

關於枚舉,你可能也想記下非常有趣的Enums.NET


由於Enum Type實現了IConvertible接口,更好的實現應該是這樣的:

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

   //...
}

這仍然允許傳遞實現IConvertible的值類型。 雖然這種可能性很少見。







enums