Dissecting new generic constraints in C# 7.3


During the last Build conference, Microsoft has announced the next version of Visual Studio with C# 7.3 support. This is yet another minor language update with some quite interesting features. The main change was related to generics, starting from C# 7.3 there 3 more constraints: unmanaged, System.Enum and System.Delegate.

The unmanaged constraint

The unmanaged constraint on generic type T enforces the type to be an 'unmanaged' struct that does not recursively contain reference type fields. The concept of 'unmanaged types' existed in the C# language for a long time and you can find this term in the C# language specification and in the official documentation, but now the compiler can actually enforce it.

This feature could be useful for low-level scenarios, like native-managed interoperability, but could be helpful for common enterprise scenarios as well. An instance of 'unmanaged' type T is convertible to T* which is convertible to byte* that opens some interesting possibilities. Let suppose you want to keep your data in a key-value store. To get a key you may consider using SHA1 or some other hashing algorithm, but you don't want to implement the hashing logic for every type in the system.

Now you can implement this logic for all 'unmanaged' types in your application the following way:

public unsafe static byte[] ComputeHash<T>(T data) where T : unmanaged
{
   
byte* bytes = (byte*)(&
data);
   
using (var sha1 = SHA1.
Create())
    {
       
var size = sizeof(T
);
       
using (var ms = new UnmanagedMemoryStream
(bytes, size))
        {
           
return sha1.ComputeHash(ms);
        }
    }
}

This code works perfectly fine because sizeof operator is actually designed for 'unmanaged' types even though there was no special constraint in the language to enforce the correctness of this operator at compile time.

The feature is relatively simple but there are some caveats:

  • Unmanaged types are different from 'blittable types'. Blittable types have the same representation in both managed and unmanaged code. This means that some 'unmanaged' types are not blittable and some blittable types are not 'unmanaged'. For instance, bool and char are 'unmanaged' types but they're not blittable, because the underlying representation for both of these types could be "platform-specific". On the other hand, strings could be blittable in some cases, but they do not belong to a new family of 'unmanaged' types because string is a reference type.
  • Existing generic types are not 'unmanaged'. int?, (int, double), System.KeyValuePair<int, int> are not 'unmanaged' types because these generic types don't have the 'unmanaged' constraint.
  • You still can't safely use Marshal.SizeOf<T> when T is an 'unmanaged' type. Even though the documentation for Marhal.SizeOf<T> states that the method "Returns the size of an unmanaged type in bytes.", it is not quite true today. Any enum is considered to be an unmanaged type but trying to use an enum type in Marshal.SizeOf<MyEnum>() will lead to a runtime error. For all other unmanaged types, this function works similar to sizeof operator.

The Enum constraint

The System.Enum constraint on type T enforces that the type is an enum. Enum types are not as ubiquitous as other primitive types, but this constraint still may be very useful in many scenarios.

For instance, you can solve issues with the existing API provided by System.Enum type:

public static TEnum[] GetValues<TEnum>() where TEnum : System.Enum
{
   
return (TEnum[])Enum.GetValues(typeof(TEnum
));
}


// BCL-based version
MyEnum[] values = (MyEnum[])Enum.GetValues(typeof(MyEnum
));
           

// Type-safe version
MyEnum[] values2 = GetValues<MyEnum>();

But we can make a step forward and introduce a concept of EnumTraits - a special type that represents a metadata for a given enum type. For instance, we can get a min/max value, available values, and some other information:

public static class EnumTraits<TEnum> where TEnum : struct, Enum
{
   
private static HashSet<TEnum
> _valuesSet;
   
static
EnumTraits()
    {
       
var type = typeof(TEnum
);
       
var underlyingType = Enum.
GetUnderlyingType(type);

        EnumValues
= (TEnum[])Enum.GetValues(typeof(TEnum
));
        _valuesSet
= new HashSet<TEnum
>(EnumValues);

       
var longValues =
            EnumValues
           
.Select(v => Convert.
ChangeType(v, underlyingType))
           
.Select(v => Convert.
ToInt64(v))
           
.
ToList();

        IsEmpty
= longValues.Count == 0
;
       
if (!
IsEmpty)
        {
           
var sorted = longValues.OrderBy(v => v).
ToList();
            MinValue
= sorted.
Min();
            MaxValue
= sorted.
Max();
        }
    }

   
public static bool IsEmpty { get
; }
   
public static long MinValue { get
; }
   
public static long MaxValue { get
; }
   
public static TEnum[] EnumValues { get
; }

   
// This version is almost an order of magnitude faster then Enum.IsDefined
    public static bool IsValid(TEnum value) => _valuesSet.
Contains(value);
}

enum EnumLong : long
{
    X
= -1
,
    Y
= 1
,
    Z
= 2
,
}
 

Console.WriteLine(EnumTraits<EnumLong>.MinValue); // -1
Console.WriteLine(EnumTraits<EnumLong>.MaxValue); // 2
Console.WriteLine(EnumTraits<EnumLong>.IsValid(0));
// False

We can go even further: in the static constructor we can look for a specific attribute that user may use to provide enum's friendly name. We can check for duplicates and expose a property like HasDuplicates, we can check FlagsAttribute for an enum type and expose this information via IsFlagsEnum property. Or we can even implement a more efficient version of Enum.HasFlag that causes two boxing allocations per call.

As you probably know .NET Core 2.1 has a lot of performance improvements, including an optimized version of Enum.HasFlag. But unfortunately, not every application can benefit from these optimizations, so it makes sense to have a more efficient version of Enum.HasFlag for the other runtimes.

But this is not as simple as you would think. The Enum constraint does not allow you to perform arithmetic operations on enum's instances.

public static long ToLong<TEnum>(TEnum value) where TEnum : System.Enum
{
   
// Cannot convert 'TEnum' to 'long'
    return (long)value;
}

Every enum type implicitly implements IConvertible and you may try to leverage this:

public static long ToLong<TEnum>(TEnum value) where TEnum : System.Enum, IConvertible
{
   
return value.ToInt64(provider: null);
}

This code works fine but causes boxing allocation for each invocation: the thing that we tried to avoid in the first place.

The only remaining option that I could think of is the code generation. Even though different enum instances could be of a different size (byte, short, int -- the default enum's underlying type, or long) the IL-code that is required to convert an integral value of different types to a specific target type is the same. For instance, the IL-code for converting byte, short or int to a long is the same: Conv_I8:

public static class EnumConverter
{
   
public static Func<T, long> CreateConvertToLong<T>() where T : struct, Enum
    {
       
var method = new DynamicMethod
(
            name:
"ConvertToLong"
,
            returnType:
typeof(long
),
            parameterTypes:
new[] { typeof(T
) },
            m:
typeof(EnumConverter).
Module,
            skipVisibility:
true
);

       
ILGenerator ilGen = method.
GetILGenerator();

        ilGen
.Emit(OpCodes.
Ldarg_0);
        ilGen
.Emit(OpCodes.
Conv_I8);
        ilGen
.Emit(OpCodes.
Ret);
       
return (Func<T, long>)method.CreateDelegate(typeof(Func<T, long>));
    }
}

The implementation of an allocation-free version of HasFlags is fairly straightforward:

public static class EnumExtensions
{
   
/// <summary>
    /// Allocation-free version of <see cref="Enum.HasFlag(Enum)"/>
    /// </summary>
    public static bool HasFlags<TEnum>(this TEnum left, TEnum right) where TEnum: struct, Enum
    {
       
var fn = Converter<TEnum>.
ConverterFn;
       
return (fn(left) & fn(right)) != 0
;
    }

   
private class Converter<TEnum> where TEnum: struct, Enum
    {
       
public static readonly Func<TEnum, long> ConverterFn = EnumConverter.CreateConvertToLong<TEnum>();
    }
}

Let's compare this implementation with Enum.HasFlag and with a simple bitwise comparison.

[Benchmark(OperationsPerInvoke = 10_000, Baseline = true)]
public bool
EnumHasFlag()
{
   
FileAccess value = FileAccess.
ReadWrite;
   
bool result = true
;

   
for (int i = 0; i < 10_000; i++
)
    {
        result
&= value.HasFlag(FileAccess.
Read);
    }

   
return
result;
}

[
Benchmark(OperationsPerInvoke = 10_000)]
public bool
EnumExHasFlags()
{
   
FileAccess value = FileAccess.
ReadWrite;
   
bool result = true
;

   
for (int i = 0; i < 10_000; i++
)
    {
        result
&= value.HasFlags(FileAccess.
Read);
    }

   
return
result;
}

[
Benchmark(OperationsPerInvoke = 10_000)]
public bool
EnumBitwiseComparison()
{
   
FileAccess value = FileAccess.
ReadWrite;
   
bool result = true
;

   
for (int i = 0; i < 10_000; i++
)
    {
        result
&= ((value & FileAccess.Read) != 0
);
    }

   
return result;
}

 

Method | Mean | Error | StdDev | Scaled | Gen 0 | Allocated | ---------------------- |-----------:|----------:|----------:|-------:|---------:|----------:| EnumHasFlag | 24.6219 ns | 0.4849 ns | 0.6798 ns | 1.00 | 152.3438 | 480005 B | EnumExHasFlags | 7.4968 ns | 0.1374 ns | 0.1285 ns | 0.30 | - | 0 B | EnumBitwiseComparison | 0.3752 ns | 0.0074 ns | 0.0134 ns | 0.02 | - | 0 B |

As you can see, the optimized version is 3 times faster than the original Enum.HasFlagimplementation and is allocation free. It is still way slower than a simple bit-wise comparison that consists only of a few assembly instructions.

The oddities of the Enum constraint

There is one very interesting caveat with the Enum constraint: the constraint does not imply but itself that the T is a struct. Moreover, you can actually combine the Enumconstraint with the class constraint:

// The only valid TEnum type is `System.Enum`
public static void Foo<TEnum>() where TEnum : class, Enum
{ }
       
static void Main(string
[] args)
        {

// Error 'EnumLong' must be a class
Foo<EnumLong>();
// Ok, but why would you do that?!?!?
Foo<System.Enum>();

The combination where TEnum: class, Enum makes no sense and the only reasonable way to use the Enum constraint is to use it with the struct constraint: where TEnum: struct, Enum.

The Delegate constraint

The System.Delegate constraint on type T enforces that the type is a delegate. I think this is the least useful constraint from all the new ones, but I can definitely see the value of it in some scenarios.

In the previous section, we've used code generation to work-around some restrictions for the Enum constraint. The solution was relatively simple but it may be improved by creating a facade class with the new Delegate constraint.

/// Non-generic facade for creating dynamic methods for a given delegate type
/// </summary>

public static class DynamicMethodGenerator
{
   
public static DynamicMethodGenerator<TDelegate> Create<TDelegate>(string
name)
       
where TDelegate : Delegate
        => new DynamicMethodGenerator<TDelegate
>(name);
}

/// <summary>
/// Generator class for constructing delegates of type <typeparamref
name="TDelegate"
/>.
/// </summary>

public sealed class DynamicMethodGenerator<TDelegate> where TDelegate : Delegate
{
   
private readonly DynamicMethod
_method;

   
internal DynamicMethodGenerator(string
name)
    {
       
MethodInfo invoke = typeof(TDelegate).GetMethod("Invoke"
);

       
var parameterTypes = invoke.GetParameters().Select(p => p.ParameterType).
ToArray();

        _method
= new DynamicMethod(name, invoke.
ReturnType,
            parameterTypes, restrictedSkipVisibility:
true
);
    }

   
public ILGenerator GetILGenerator() => _method.
GetILGenerator();

   
public TDelegate
Generate()
    {
       
return (TDelegate)_method.CreateDelegate(typeof(TDelegate));
    }
}

This simple class drastically simplifies the code generation and makes it less error-prone:

public static Func<T, long> CreateConvertToLong<T>() where T : struct, Enum
{
   
var generator = DynamicMethodGenerator.Create<Func<T, long>>("ConvertToLong"
);

   
ILGenerator ilGen = generator.
GetILGenerator();

    ilGen
.Emit(OpCodes.
Ldarg_0);
    ilGen
.Emit(OpCodes.
Conv_I8);
    ilGen
.Emit(OpCodes.
Ret);
   
return generator.Generate();
}

Conclusion

None of the new constraints are game changing features but they definitely can be quite useful in real life scenarios.

  • The unmanaged constraint is useful for native-managed interop scenarios as well as for more widely use cases like serialization/deserialization or hash computations.
  • The Enum constraint helps to work-around existing issues with utility methods from System.Enum type, or for creating application-specific "enum traits" types for custom validation, enum-ToString conversion etc.
  • The Delegate constraint is helpful for code generation but may be helpful in other cases as well. For instance, when dealing with expression trees, for creating generic event handlers or for implementing command pattern.

Comments (9)

  1. dotScience says:

    Thanks for a satisfyingly deep, well-written, article !

    On the big-picture level: the absence of a genetic constraint for any numeric Type … one of the most requested features … seems strange.

    re: unmanaged: “Let suppose you want to keep your data in a key-value store. To get a key you may consider using SHA1 or some other hashing algorithm, but you don’t want to implement the hashing logic for every type in the system.” I’m trying to imagine what type of C# BCL key-value store would benefit from this … perhaps you might expand on this ?

    It would assist me, and possibly ? others, if you would include a download of the code for a ready-to paste-in to a VS 7.3 project … and compile/run … for the above examples.

    For those not familiar with the open-source ‘Benchmark project, you may wish to cite, and link-to: https://github.com/dotnet/BenchmarkDotNet

    cheers, Bill

    1. Anonymous says:
      (The content was deleted per user request)
    2. dotScience says:

      Too bad there’s no way to edit a comment, and no way to stop line-breaks being stripped out of a comment ! re unmanaged: consider this limitation: https://github.com/dotnet/roslyn/issues/26854

    3. > I’m trying to imagine what type of C# BCL key-value store would benefit from this … perhaps you might expand on this ?

      The feature is not very useful for dictionary-like types in BCL, but more for in-memory or persistent key-value stores like Redis or RocksDb.

      > It would assist me, and possibly ? others, if you would include a download of the code for a ready-to paste-in to a VS 7.3 project … and compile/run … for the above examples.
      This is a good idea. I’ll publish the code soon.

  2. Nukepayload2 says:

    I personally don’t like the `unmanaged` constraint. At least I’ll not make this constraint public in my class libraries. Because it’s totally incompatible with Visual Basic, just like ref struct and pointer types. Many of my clients are using VB or earlier versions of C#. Compatibility is more important than performance to me.

    1. It is good to know that the `unmanaged` constraint is not compatible with VB.

  3. Paolo Pagano says:

    It would be very useful to have constraits on constructor parameters, an example:

    class MyType where T : class, new( int a, string b ) { … }

    allowing code like:

    var t = new T( 1, “a” );

    letting MyType create “T” instances with mandatory parameters and having instances of “T” in safe state, avoiding this:

    var t = new T();

    t.PropertyA = 1;
    t.PropertyB = “a”;

    forcing “PropertyA” and “PropertyB” be mutable

    1. Indeed, this type of constraint would be very helpful. The only “workaround” today is to take a func, which is far from perfect.

  4. lobster2012 says:

    (duplicate)
    What abount delegate with 2 args?
    https://gist.github.com/lobster2012-user/a5957b439c6085fbbe28d2a227634ab2

    public static Func create_has_flags_delegate1()
    where T : Enum
    {

    var method = DynamicMethodGenerator.Create<Func>(“EnumHasFlags”);
    var ilGen = method.GetILGenerator();
    ilGen.Emit(OpCodes.Ldarg_0);
    ilGen.Emit(OpCodes.Ldarg_1);
    ilGen.Emit(OpCodes.And);
    ilGen.Emit(OpCodes.Ldc_I4_0);
    ilGen.Emit(OpCodes.Cgt_Un);
    ilGen.Emit(OpCodes.Ret);
    return method.Generate();
    }

Skip to main content