.NET Tools
Essential productivity kit for .NET and game developers
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:
- Declaration expressions in initializers and queries
- Tuple equality
- Unmanaged, delegate and enum type constraints
- Ref local re-assignment
- Fixed pattern and simplified access to fixed size buffer elements
- C# updates for stackalloc
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): unmanaged
, System.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
orUIntPtr
. - Be an enum type.
- Be a pointer type.
- Be a user-defined
struct
that satisfies theunmanaged
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.
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.
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!