As ref structs in C# have become more popular, their interaction with generics has become a more common pain point. In particular, the restriction that ref structs cannot be substituted for generic parameters is rough. The basic problem with generics is pretty simple: ref structs have a variety of special restrictions, one of them being that they can't be stored on the heap. This is both a safety restriction (they are not safe to share across threads) and a GC limitation (they carry interior pointers). Generics, however, don't have those restrictions. In particular, the following code is perfectly legal:
object M<T>(T t)
{
return (object)t;
}
If T
were a ref struct, you now have a ref struct on the heap. Oops. No good. We need to repair the safety hole.
One of the first proposals was a new constraint, like where T : ref
, which would require ref structs and therefore disallow boxing of T
. This certainly seems to work at first, but it's actually a bit backwards. In most cases we don't want to require that T
be a ref struct, we merely want to allow it to be a ref struct.
When you see the word "constraint" you may think that it represents a limitation. But in C#, that's not completely true. To the caller of methods with constraints—sure, it's constraining. If the T
is unconstrained you can pass in anything, but if someone marks T where T : IDisposable
now you can only pass in things which implement IDisposable
.
But to the callee, this is anything but constraining. Consider what you, a method author, can do with an unconstrained generic. You can copy it around, you can use default(T)
, you can box it to object
, and... well that's pretty much it.1 But if you say where T : IDisposable
, now you can call Dispose
!
void M<T>(T t) where T : IDisposable
{
t.Dispose(); // now legal
}
The insight here is that generic constraints are only constraints to their caller—to the callee, they're capabilities. Knowing this, we can look at "unconstrained" generics and see that they actually have a variety of implicit capabilities as well, boxing in particular. Looking at it through this lens, let's reimagine C# a little. Now, every "unconstrained" generic actually comes with a secret implicit "boxing" constraint. Let's call it box
, as in where T : box
.
Now our ref struct problem becomes much clearer. The reason why we can't pass ref structs through generics is because all generics implicitly carry the box
constraint. The solution to our problem is some sort of syntax to remove or cancel-out the implicit constraints on generic parameters. An anti-constraint. We can easily make up some syntax for that. We could use, ~
, the unary complement operator, to indicate anti-constraints. Now lets take a look at our example again.
object M<T>(T t)
// Add the anti-constraint for boxing
where T : ~box
{
return (object)t; // Error! Can't box T
}
Now we get an error where we should—inside M
. This method can't remove the boxing constraint because it's used inside the method, the same as if you tried to remove an IDisposable
constraint from something which called Dispose
.
But, if you were to apply the anti-boxing constraint to something which didn't box, like our Dispose example, things should be fine:
void M<T>(T t)
where T : IDisposable
where T : ~box
{
t.Dispose();
}
This method would now accept ref structs. Now, if you did this you would immediately run into a second problem: ref structs also can't implement interfaces. But that one is much simpler and mostly comes down to the fact that, because ref structs can't be boxed, if you can't pass them through generics there's really no way to actually invoke the interface method. If the generics restrictions are lifted, the interface restrictions could be lifted as well.
And by the way, you'll notice that there were a few other things that I listed that you can do with "unconstrained" generics—default(T)
and copying. You could imagine making anti-constraints for those as well... but that's for another post.
-
Note that reflection does not count here. The C# language has no idea things like "reflection" exist. The reflection methods (
GetMethod
,GetProperties
, et al.) just look like regular methods and C# assumes they have the same restrictions. ↩