Create Generic method constraining T to an Enum
我正在构建一个函数来扩展
- 如果找不到枚举值,则允许分析默认值
- 不区分大小写
所以我写了以下内容:
1 2 3 4 5 6 7 8 9 | 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; } |
我得到的错误约束不能是特殊类
很公平,但是是否有一个允许泛型枚举的变通方法,或者我必须模拟
编辑下面的所有建议都非常感谢,谢谢。
已经解决了(我离开循环是为了保持大小写不敏感-我在分析XML时使用这个方法)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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_中发布了一个编译器强制类型安全通用解决方案,值得一看,并进行投票。如果解决方案在页面上进一步冒泡,我将删除此编辑。
由于
1 2 3 4 5 6 7 8 9 |
这仍然允许传递实现
C 7.3!最终支持此功能。
以下代码段(来自dotnet示例)演示了它的用法:
1 2 3 4 5 6 7 8 9 |
一定要将C项目中的语言版本设置为7.3版。
原始答案如下:
我参加比赛迟到了,但我认为这是一个挑战,看如何才能做到。这在C(或vb.net)中是不可能的,但向下滚动F(或vb.net),但在MSIL中是可能的。我写了这个……小东西
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | // 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:
1 | T GetEnumFromString<T>(string valueString, T defaultValue) where T : Enum |
然后使用以下C代码:
1 2 3 4 5 6 7 8 9 | 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_编写代码的这部分,唯一的好处是您能够通过
通过删除行
1 | ilasm.exe /DLL /OUTPUT=MyThing.netmodule |
您将得到一个netmodule而不是一个程序集。
不幸的是,vs2010(以及更早的版本,显然)不支持添加netmodule引用,这意味着您在调试时必须将其保留在两个单独的程序集中。唯一可以将它们作为程序集的一部分添加的方法是使用
所以,它可以在.NET中完成。值得付出额外的努力吗?嗯,我想我会让你决定的。
作为替代方案额外学分:事实证明,除了msil:f_之外,至少还有一种.NET语言可以对
1 2 3 4 5 6 7 8 9 | 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(代码非常不同),并且它依赖于
下面介绍如何使用它(基本上与MSIL解决方案相同),并说明它在其他同义结构上正确失败:
1 2 3 4 | // 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); |
C=7.3
从C 7.3开始(Visual Studio 2017≥v15.7提供),此代码现在完全有效:
1 2 | public static TEnum Parse<TEnum>(string value) where TEnum : struct, Enum { ... } |
C=7.2
您可以通过滥用约束继承来拥有真正的编译器强制的枚举约束。以下代码同时指定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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> { } |
用途:
1 | EnumUtils.Parse<SomeEnum>("value"); |
注:这在C 5.0语言规范中特别说明:
If type parameter S depends on type parameter T then:
[...] It is valid for
S to have the value type constraint and T to have the reference type
constraint. Effectively this limits T to the types System.Object,
System.ValueType, System.Enum, and any interface type.
编辑
这个问题现在得到了朱利安·莱博斯金的完美回答。我还想在增加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | 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 } |
用法示例:
1 2 3 4 | 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); |
旧的
我通过评论和"新"发展对Vivek答案的旧改进:
- 使用
TEnum 为用户提供清晰的信息 - 为附加约束检查添加更多接口约束
- 让
TryParse 用现有参数处理ignoreCase 。(在VS2010/NET 4中引入) - 可选使用通用
default 值(在vs2005/net 2中引入) - 对于
defaultValue 和ignoreCase ,使用带有默认值的可选参数(在vs2010/.net 4中引入)。
导致:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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; } } |
可以为类定义一个静态构造函数,该类将检查类型t是否为枚举,如果不是,则引发异常。这是杰弗里·里克特在他的书《通过C_的CLR》中提到的方法。
1 2 3 4 5 6 7 |
然后在parse方法中,只需使用enum.parse(typeof(t),input,true)将字符串转换为枚举。最后一个真参数用于忽略输入的大小写。
我修改了Dimarzionist的样本。此版本只对枚举有效,不允许结构通过。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
还应该考虑到,因为使用枚举约束的C 7.3版本是开箱即用的,不需要做额外的检查和处理。
因此,接下来,考虑到您已经将项目的语言版本更改为C 7.3,下面的代码将非常好地工作:
1 2 3 4 | private static T GetEnumFromString<T>(string value, T defaultValue) where T : Enum { // Your code goes here... } |
如果您不知道如何将语言版本更改为C 7.3,请参见以下屏幕截图:
编辑1-所需的Visual Studio版本并考虑重新分析
要使Visual Studio识别新的语法,您至少需要15.7版。您可以在Microsoft的发行说明中找到这一点,请参见Visual Studio 2017 15.7发行说明。感谢@mohamedelshaff指出这个有效的问题。
请注意,在我的案例中,Resharper 2018.1自编写之日起,此编辑尚不支持C 7.3。激活Resharper后,它将枚举约束突出显示为一个错误,告诉我不能使用"System.Array"、"System.Delegate"、"System.Enum"、"System.ValueType"、"Object"作为类型参数约束。resharper建议快速修复删除方法的paramter t类型的"enum"约束
但是,如果在"工具"->"选项"->"Resharper Ultimate"-"常规"下暂时关闭Resharper,那么在使用vs 15.7或更高版本以及c 7.3或更高版本的情况下,您会发现语法非常好。
我试图改进代码:
1 2 3 4 5 6 7 8 |
我确实有特定的要求,在这里我需要使用与枚举值相关联的文本枚举。例如,当我使用枚举指定错误类型时,它需要描述错误详细信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | 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()); } |
希望这有帮助:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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; } } |
从C<=7.2开始,现有答案是正确的。但是,有一个C语言功能请求(绑定到一个corefx功能请求)允许以下内容:
1 2 | public class MyGeneric<TEnum> where TEnum : System.Enum { } |
写作时,语言发展会议上的特点是"讨论中"。
编辑
根据Nawfal的信息,这将在C 7.3中介绍。
有趣的是,显然这在其他语言中是可能的(管理C++,IL直接)。
引述:
... Both constraints actually produce valid IL and can also be consumed by C# if written in another language (you can declare those constraints in managed C++ or in IL).
谁知道
这是我的拿手好戏。结合答案和msdn
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public static TEnum ParseToEnum<TEnum>(this string text) where TEnum : struct, IConvertible, IComparable, IFormattable { if (string.IsNullOrEmpty(text) || !typeof(TEnum).IsEnum) throw new ArgumentException("TEnum must be an Enum type"); try { var enumValue = (TEnum)Enum.Parse(typeof(TEnum), text.Trim(), true); return enumValue; } catch (Exception) { throw new ArgumentException(string.Format("{0} is not a member of the {1} enumeration.", text, typeof(TEnum).Name)); } } |
MSDN源
正如前面其他答案所述;虽然这不能用源代码表示,但实际上可以在IL级别上实现。@克里斯托弗·科伦斯的回答显示了IL是如何做到这一点的。
有了FODY,就有了额外的培训。FODY有一个非常简单的方法,包括构建工具,来实现这一点。只需将他们的nuget包(
1 2 3 | public void MethodWithEnumConstraint<[EnumConstraint] T>() {...} public void MethodWithTypeEnumConstraint<[EnumConstraint(typeof(ConsoleColor))] T>() {...} |
FODY将添加必要的IL,以使约束存在。还要注意约束委托的附加功能:
1 2 3 4 5 | public void MethodWithDelegateConstraint<[DelegateConstraint] T> () {...} public void MethodWithTypeDelegateConstraint<[DelegateConstraint(typeof(Func<int>))] T> () {...} |
关于Enums,您可能还需要注意非常有趣的Enums.net。
我喜欢使用IL的Christopher Currens的解决方案,但对于那些不想处理复杂业务(将MSIL包含到构建过程中)的人,我在C中编写了类似的函数。
请注意,尽管不能使用像
我的功能是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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; } |
我已经将vivek的解决方案封装到一个实用程序类中,您可以重用它。请注意,您仍然应该在类型上定义类型约束"where t:struct,iconvertible"。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | using System; internal static class EnumEnforcer { /// <summary> /// Makes sure that generic input parameter is of an enumerated type. /// </summary> /// <typeparam name="T">Type that should be checked.</typeparam> /// <param name="typeParameterName">Name of the type parameter.</param> /// <param name="methodName">Name of the method which accepted the parameter.</param> public static void EnforceIsEnum<T>(string typeParameterName, string methodName) where T : struct, IConvertible { if (!typeof(T).IsEnum) { string message = string.Format( "Generic parameter {0} in {1} method forces an enumerated type. Make sure your type parameter {0} is an enum.", typeParameterName, methodName); throw new ArgumentException(message); } } /// <summary> /// Makes sure that generic input parameter is of an enumerated type. /// </summary> /// <typeparam name="T">Type that should be checked.</typeparam> /// <param name="typeParameterName">Name of the type parameter.</param> /// <param name="methodName">Name of the method which accepted the parameter.</param> /// <param name="inputParameterName">Name of the input parameter of this page.</param> public static void EnforceIsEnum<T>(string typeParameterName, string methodName, string inputParameterName) where T : struct, IConvertible { if (!typeof(T).IsEnum) { string message = string.Format( "Generic parameter {0} in {1} method forces an enumerated type. Make sure your input parameter {2} is of correct type.", typeParameterName, methodName, inputParameterName); throw new ArgumentException(message); } } /// <summary> /// Makes sure that generic input parameter is of an enumerated type. /// </summary> /// <typeparam name="T">Type that should be checked.</typeparam> /// <param name="exceptionMessage">Message to show in case T is not an enum.</param> public static void EnforceIsEnum<T>(string exceptionMessage) where T : struct, IConvertible { if (!typeof(T).IsEnum) { throw new ArgumentException(exceptionMessage); } } } |
我创建了一个扩展方法
1 2 3 4 5 6 7 8 9 10 | 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); } |
这就是用法
1 2 | MemberStatusEnum.Activated.ToInt()// using extension Method (int) MemberStatusEnum.Activated //the ordinary way |
我一直喜欢这个(你可以适当修改):
1 2 3 4 5 6 7 8 9 10 |
如果以后可以使用直接强制转换,我想您可以在任何必要的地方在您的方法中使用
1 2 3 4 5 6 7 8 9 10 11 12 | public static class EnumUtils { public static Enum GetEnumFromString(string value, Enum defaultValue) { if (string.IsNullOrEmpty(value)) return defaultValue; foreach (Enum item in Enum.GetValues(defaultValue.GetType())) { if (item.ToString().ToLower().Equals(value.Trim().ToLower())) return item; } return defaultValue; } } |
然后你可以像这样使用它:
1 | var parsedOutput = (YourEnum)EnumUtils.GetEnumFromString(someString, YourEnum.DefaultValue); |
为了完整性,下面是一个Java解决方案。我确信同样的事情也可以在C中完成。它避免了在代码中的任何地方指定类型,而是在试图解析的字符串中指定类型。
问题是,没有任何方法可以知道字符串可能匹配哪一个枚举,所以答案就是解决这个问题。
不要只接受字符串值,而是接受同时具有枚举和"enumeration.value"形式的值的字符串。工作代码在下面——需要Java 1.8或更高版本。这也会使XML更加精确,因为您会看到类似于color="color.red"的内容,而不仅仅是color="red"。
您将使用包含枚举名称点值名称的字符串调用AcceptEnumeratedValue()方法。
方法返回形式枚举值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | import java.util.HashMap; import java.util.Map; import java.util.function.Function; public class EnumFromString { enum NumberEnum {One, Two, Three}; enum LetterEnum {A, B, C}; Map<String, Function<String, ? extends Enum>> enumsByName = new HashMap<>(); public static void main(String[] args) { EnumFromString efs = new EnumFromString(); System.out.print(" First string is NumberEnum.Two - enum is" + efs.acceptEnumeratedValue("NumberEnum.Two").name()); System.out.print(" Second string is LetterEnum.B - enum is" + efs.acceptEnumeratedValue("LetterEnum.B").name()); } public EnumFromString() { enumsByName.put("NumberEnum", s -> {return NumberEnum.valueOf(s);}); enumsByName.put("LetterEnum", s -> {return LetterEnum.valueOf(s);}); } public Enum acceptEnumeratedValue(String enumDotValue) { int pos = enumDotValue.indexOf("."); String enumName = enumDotValue.substring(0, pos); String value = enumDotValue.substring(pos + 1); Enum enumeratedValue = enumsByName.get(enumName).apply(value); return enumeratedValue; } } |