创建泛型方法将 T 约束为枚举类型
技术背景
在开发过程中,我们有时需要创建泛型方法,并将泛型类型参数 T
约束为枚举类型。然而,在早期的 C# 版本中,直接对泛型类型参数进行枚举约束并不容易实现。随着 C# 版本的不断更新,对枚举约束的支持也在不断完善。
实现步骤
C# ≥ 7.3
从 C# 7.3 开始(可在 Visual Studio 2017 ≥ v15.7 中使用),可以直接使用 Enum
约束。
示例代码
1 2 3 4 5 6
| public static TEnum Parse<TEnum>(string value) where TEnum : struct, Enum { return (TEnum)Enum.Parse(typeof(TEnum), value); }
|
使用方法
1 2 3 4 5
| private enum MyEnum { Yes, No, Okay } static void Main(string[] args) { var result = Parse<MyEnum>("Yes"); }
|
C# ≤ 7.2
在 C# 7.2 及更早版本中,可以通过滥用约束继承来实现真正的编译器强制枚举约束。
示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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 2 3 4 5
| private enum SomeEnum { Value1, Value2 } static void Main(string[] args) { var result = EnumUtils.Parse<SomeEnum>("Value1"); }
|
使用 MSIL 实现
可以编写 MSIL 代码来生成一个具有枚举约束的泛型方法。
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# 调用 MSIL 生成的方法
1 2 3 4 5 6
| using MyThing; private enum MyEnum { Yes, No, Okay } static void Main(string[] args) { var result = Thing.GetEnumFromString("Yes", MyEnum.No); }
|
使用 F# 实现
F# 也可以实现泛型枚举约束。
示例代码
1 2 3 4 5 6 7
| type MyThing = static member GetEnumFromString<'T when 'T :> Enum> str defaultValue: 'T = 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
|
C# 调用 F# 方法
1 2 3 4 5
| using System; static void Main(string[] args) { var result = MyThing.GetEnumFromString("OrdinalIgnoreCase", StringComparison.Ordinal); }
|
通过 Fody 的 Add-In ExtraConstraints.Fody 可以很简单地实现枚举约束。
示例代码
1 2 3 4
| public void MethodWithEnumConstraint<[EnumConstraint] T>() { }
|
核心代码
C# 7.3 及以上版本的核心代码
1 2 3 4 5
| public static TEnum Parse<TEnum>(string value) where TEnum : struct, Enum { return (TEnum)Enum.Parse(typeof(TEnum), value); }
|
C# 7.2 及以下版本的核心代码
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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> { }
|
最佳实践
- 使用最新版本的 C#:如果项目允许,尽量使用 C# 7.3 及以上版本,因为它直接支持枚举约束,代码更简洁。
- 添加错误处理:在解析枚举时,应该添加适当的错误处理逻辑,以避免程序崩溃。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13
| public static TEnum Parse<TEnum>(string value) where TEnum : struct, Enum { try { return (TEnum)Enum.Parse(typeof(TEnum), value); } catch (ArgumentException) { return default(TEnum); } }
|
- 使用扩展方法:可以将枚举解析方法封装为扩展方法,方便在项目中使用。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public static class EnumExtensions { public static TEnum ParseEnum<TEnum>(this string value) where TEnum : struct, Enum { try { return (TEnum)Enum.Parse(typeof(TEnum), value); } catch (ArgumentException) { return default(TEnum); } } }
|
使用方法:
1 2 3 4 5
| private enum MyEnum { Yes, No, Okay } static void Main(string[] args) { var result = "Yes".ParseEnum<MyEnum>(); }
|
常见问题
编译错误:“Cannot use ‘System.Array’, ‘System.Delegate’, ‘System.Enum’, ‘System.ValueType’, ‘object’ as type parameter constraint”
这可能是因为使用的 ReSharper 版本不支持 C# 7.3。可以暂时关闭 ReSharper,方法是在“Tools -> Options -> ReSharper Ultimate -> General”中关闭。
运行时错误:“T must be an enumerated type”
这通常是因为泛型类型参数 T
不是枚举类型。在使用泛型方法时,要确保传入的类型是枚举类型。
需要在项目中添加 Fody
和 ExtraConstraints.Fody
的 NuGet 包。可以通过 Visual Studio 的 NuGet 包管理器来添加。