Generic method with T constraint on Enum: under the hood of C#

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Generic method with T constraint on Enum: how it works under the hood

\n\n

Imagine: you are writing in C# and want to create a universal method that accepts any enumeration (Enum). Seems simple, right? But if you have ever tried to write public void DoSomething<T>() where T : Enum, you know — the compiler throws an error. Before C# 7.3 this was impossible. After that, it became possible, but with nuances. Let's figure out how it works under the hood, and why this is one of the most popular topics on Stack Overflow.

\n\n

This article is not just a dry retelling of the documentation. We will dig deeper: from IL code to ReSharper recommendations. Let's go!

\n\n

Why couldn't you just write where T : Enum?

\n\n

For a long time, C# prohibited using Enum or Delegate as a constraint for generic parameters. Why? Because the compiler needed to guarantee that type T is a value type (struct) or a reference type (class). Enum, however, is a special structural type inheriting from System.Enum. But in the C# type system, it did not fit into simple rules:

\n\n
    \n
  • Enum is a struct, but not any struct.
  • \n
  • Enum inherits from ValueType, but the compiler did not allow using where T : System.Enum.
  • \n
  • Attempting to write where T : struct, Enum also did not work.
  • \n
\n\n

Developers resorted to workarounds: runtime checking via typeof(T).IsEnum or using reflection. But this killed all performance and type safety.

\n\n

C# 7.3: The long-awaited solution

\n\n

Starting with C# 7.3 and .NET Core 2.0 / .NET Framework 4.8, it became possible to write:

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

Now the compiler understands: T is an enumeration. But how exactly does it do this? Let's look under the hood.

\n\n

What happens in IL code?

\n\n

When you write where T : Enum, the compiler generates a special constrained call for methods like Enum.HasFlag, Enum.GetName, etc. Instead of using boxing or reflection, the JIT compiler can substitute a direct call to the static method, knowing the specific type T.

\n\n

Example IL code for the method above:

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

Notice the attribute ([mscorlib]System.Enum) T. This is the new constraint. The JIT sees it and knows that T is an enum, meaning boxing can be avoided when passing value to GetName.

\n\n

How to use a generic method with Enum in practice?

\n\n

Let's create a useful example: a method for parsing a string into any enumeration with case-insensitive support.

\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

Important: Despite T being an Enum, the cast (T)result still requires an explicit cast. This is because at runtime the object type does not know about the generic parameter. But the compiler guarantees that T is an enum, so the cast is safe.

\n\n

A more elegant option with Enum.TryParse<T>

\n\n

Starting with .NET 5, an overload appeared

Blogs

Book Recommendations