Dotnet logo

.NET Tools

Essential productivity kit for .NET and game developers

How-To's

Unmanaged, delegate and enum type constraints – C# 7.3 in Rider and ReSharper

Last time in our series about C# 7.3 language features, we covered tuple equality. Today we will look at unmanaged, delegate and enum type constraints. The latest Early Access Preview (EAP) versions of ReSharper 2018.2 and Rider 2018.2 come with language support for C# 7.3, do give them a try!

This post is part of a series:

Ever since C# 2.0, when generics were introduced, it has been possible to set type constraints on generic parameters. For example, we could specify that the type constraint should implement a specific interface, and perhaps also have a parameterless constructor:

public class Repository<T>
    where T : IEntity, new()
{
    // ...
}

Generic type constraints can be added on class declarations, method declarations, and local functions. They allow us to constrain the types which can be used with the class or method we are creating.

Up until now, these constraints included reference type constraint (class), value type constraint (struct), interfaces or base class constraints, and whether a parameterless constructor should be present (new()). With C# 7.3, three new generic type constraints are introduced (proposal): unmanagedSystem.Delegate and System.Enum.

(Fun fact: the CLR already supported these constraints, however the C# language prohibited using them. Jon Skeet has a blog post on making these constraints work in earlier C# versions.)

The System.Enum constraint

Let’s look at a simple example of the Enum constraint and write a method that returns the string representations of all values in an Enum. With the System.Enum generic type constraint, we can ensure this method only gets called with an Enum, and never with another type.

public static IEnumerable<string> GetValues<T>()
    where T : struct, System.Enum
{
    var enumType = typeof(T);
    var items = Enum.GetValues(enumType);

    foreach (var item in items)
    {
        yield return Enum.GetName(enumType, item);
    }
}

(Note we also added the struct, constraint here, to make sure T can’t be the System.Enum class itself – we want to prevent GetValues<System.Enum>() from being called!)

The System.Delegate constraint

Similarly, we can constrain generic classes and methods to System.Delegate now as well. All types this constraint is defined on must be the same. So combining two delegates of the same type (example 1) will work fine, combining two delegates with different types (example 2) will fail to compile:

public static TDelegate Combine<TDelegate>(TDelegate source, TDelegate target)
    where TDelegate : Delegate
{
    return (TDelegate)Delegate.Combine(source, target);
}

// Example 1
void Hello() => Console.WriteLine("Hello");
Action world = () => Console.WriteLine("World");

var helloWorld = Combine(Hello, world);

// Example 2
Func<bool> test = () => true;
var example = Combine(test, world);

The unmanaged constraint

While the unmanaged constraint will probably be used less, it does come in handy for some developers, typically when authoring low-level libraries and frameworks.

In order to satisfy the unmanaged constraint, a type must be a struct and all the fields of the type must fall into one of the following categories:

  • Have the type sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool, IntPtr or UIntPtr.
  • Be an enum type.
  • Be a pointer type.
  • Be a user-defined struct that satisfies the unmanaged constraint.

For example, when writing a class or method that only works with unmanaged types, we’d normally have to create overloads for every type. We can now use the unmanaged constraint instead, and then work with our value and declare pointers of unmanaged types, use the sizeof operator, allocate arrays on the stack, pin heap-allocated data using the fixed statement, and so on.

private static unsafe void DoSomething<T>(T value) 
    where T : unmanaged
{
    // get size
    int size = sizeof(T);

    // get address and store it in pointer variable
    T* a = &value;

    // allocate array on stack
    T* arr = stackalloc T[42];

    // allocate array on heap and pin it
    fixed (T* p = new T[42])
    {
        // ...
    }
}

When we call this method using a managed type (e.g. DoSomething("test")), the unmanaged constraint is not satisfied and code will not compile.

ReSharper and Rider come with a code inspection and quick fix that suggests adding an unmanaged constraint. For example, when we try to declare a pointer to a managed type, code analysis will spot this and let us correct the issue using Alt+Enter.

Add unmanaged constraint quick-fix

Now let’s add a bit of geekiness and have a look at the Intermediate Language (IL) that is emitted by the C# compiler (ReSharper | Tools | IL Code). The CLR itself has no notion of the unmanaged constraint – it only exists in C#.

To make the constraint work, C# adds an attribute on our type parameter (System.Runtime.CompilerServices.IsUnmanagedAttribute), and emits a modreq (“required modifier”) of type ([mscorlib]System.Runtime.InteropServices.UnmanagedType) as well. By doing so, the compiler indicates that there are special semantics that should not be ignored. As such, compilers that do not emit this modreq will be unable to satisfy this constraint.

IL Viewer - unmanaged constraint

It’s exciting to see some additions to the generic type system in .NET!

Download ReSharper 2018.2 EAP now! Or give Rider 2018.2 EAP a try. We’d love to hear your feedback!

image description