带有T枚举约束的泛型方法:C#内部机制

面向初学者的在线Python训练器

轻松学习Python,无需理论负担。在浏览器中直接解决带自动检查的实践任务,无需安装任何东西。

开始课程

带有T枚举约束的泛型方法:内部工作原理

\n\n

想象一下:您正在用C#编写代码,想要创建一个接受任何枚举(Enum)的通用方法。看起来很简单?但如果您曾经尝试编写public void DoSomething<T>() where T : Enum,您就会知道——编译器会报错。在C# 7.3之前,这是不可能的。之后虽然可以了,但存在一些细微差别。让我们深入探讨其内部工作原理,以及为什么这是Stack Overflow上最热门的话题之一。

\n\n

本文不仅仅是枯燥的文档复述。我们将深入挖掘:从IL代码到ReSharper的建议。开始吧!

\n\n

为什么不能直接写where T : Enum?

\n\n

长期以来,C#禁止将Enum或Delegate用作泛型参数的约束。为什么?因为编译器需要保证类型T是值类型(struct)或引用类型(class)。而Enum是一种特殊的结构类型,继承自System.Enum。但在C#的类型系统中,它不符合简单的规则:

\n\n
    \n
  • Enum是struct,但不是任何struct都可以。
  • \n
  • Enum继承自ValueType,但编译器不允许使用where T : System.Enum。
  • \n
  • 尝试编写where T : struct, Enum也不起作用。
  • \n
\n\n

开发者们不得不采用变通方法:通过typeof(T).IsEnum进行运行时检查,或使用反射。但这会严重影响性能和类型安全性。

\n\n

C# 7.3:期待已久的解决方案

\n\n

从C# 7.3和.NET Core 2.0 / .NET Framework 4.8开始,可以编写:

\n\n
public static string GetEnumName<T>(T value) where T : Enum\n{\n    return Enum.GetName(typeof(T), value);\n}\n
\n\n

现在编译器明白了:T是一个枚举。但它具体是如何做到的呢?让我们深入内部一探究竟。

\n\n

IL代码中发生了什么?

\n\n

当您编写where T : Enum时,编译器会为Enum.HasFlag、Enum.GetName等方法生成一个特殊的constrained call。JIT编译器无需使用装箱(boxing)或反射,而是可以在知道具体类型T的情况下,直接调用静态方法。

\n\n

上述方法的IL代码示例:

\n\n
.method public hidebysig static \n    string GetEnumName<([mscorlib]System.Enum) T>(!!0 value) cil managed\n{\n    // 使用constrained前缀调用Enum.GetName\n    call string [mscorlib]System.Enum::GetName(class [mscorlib]System.Type, object)\n}\n
\n\n

请注意属性([mscorlib]System.Enum) T。这就是新的约束。JIT看到它,就知道T是一个枚举,因此在将value传递给GetName时可以避免装箱。

\n\n

如何在实际中使用带有Enum的泛型方法?

\n\n

让我们创建一个实用的例子:一个将字符串解析为任何枚举并支持忽略大小写的方法。

\n\n
public static T ParseEnum<T>(string value, bool ignoreCase = true) where T : Enum\n{\n    if (string.IsNullOrWhiteSpace(value))\n        throw new ArgumentException("Value cannot be null or empty.", nameof(value));\n\n    if (!Enum.TryParse(typeof(T), value, ignoreCase, out object? result))\n        throw new ArgumentException($"'{value}' is not a valid value for enum {typeof(T).Name}.");\n\n    return (T)result;\n}\n
\n\n

重要提示:尽管T是Enum,但强制转换(T)result仍然需要显式转换。这是因为在运行时,object类型不知道泛型参数。但编译器保证T是一个枚举,因此强制转换是安全的。

\n\n

使用Enum.TryParse<T>的更优雅方案

\n\n

从.NET 5开始,出现了重载

博客

书籍推荐