Is it possible to write something that will fail either at compile or runtime (i.e. by any means) on invocation of just one of these methods?
public static void Register<TInterface, TImplementation>()
where TImplementation : class, TInterface
{
}
public static void RegisterRestrictive<TInterface, TImplementation>()
where TInterface : class
where TImplementation : class, TInterface
{
}
The following will pass both for example:
public interface IInterface
{
}
public class Implementation : IInterface
{
}
public void Test()
{
Register<IInterface, Implementation>();
RegisterRestrictive<IInterface, Implementation>();
}
I don't think it is, because you can't extend structs?
Asking because of this https://github.com/aspnet/DependencyInjection/pull/624
The question is, as I understand:
class C<T, U> where T : class, U where U : class { }
class D<T, U> where T : class, U { }
Is there a construction that is legal for D
that is not legal for C
?
Not if U
and T
are closed types. That is, types that have no type parameters in them. As Jon Hanna's answer points out, an open type can cause a problem here:
class N<T, U> where T : class, U { C<T, U> c; D<T, U> d; }
D
's constraints are not met, so this construction is illegal.
For type arguments which are closed types we can reason as follows:
In C
, U
is required by constraint to be a reference type.
In D
, T
can be a class, interface, delegate or array. In every case, U
has to be identical to T
, or a base class of T
, or something T
is convertible to via (possibly variant) implicit reference conversion. No matter what, U
is a reference type.
Note though that neither the C# compiler, nor the CLR verifier, nor the JIT compiler are required to deduce that U
is always going to be a reference type! The C# compiler can and will in this circumstance, for instance, generate unnecessary boxing instructions on usages of U
, even though you and I know that U
is not going to be a value type under construction.
This can lead to situations where a U
is boxed and then immediately unboxed, and the last time I checked -- which was, uh, ten years ago -- the jitter did not generate optimal code for that scenario. Since no doubt the jitter has been rewritten one or more times since I last checked, you should probably not take my word for it.
The good practice here is to put the constraint on there and spell it out.
Some fun related facts:
You can get into similar situations by pulling shenanigans like
class B<T> { public virtual void M<U>(U u) where U : T {} }
class D : B<int> { public override void M<U>(U u) { } }
Note that C# does NOT allow you to re-state the constraint, which is now where U : int
. But you cannot do a generic construction of M
with anything other than int.
This can lead to some truly bizarre scenarios in IL generation, because the CLR has a poorly-documented rule whereby it does not allow the "known-to-be-a-reference-type"-ness of a type parameter to change across a virtual override. I redid the codegen for such methods to try to get something that would compile, pass the verifier, and jit efficiently several times before giving up and going back to whatever C# 2 did.
Is it possible to write something that will fail either at compile or runtime (i.e. by any means) on invocation of just one of these methods?
Yes, this fails at compile time:
public static void CallThem<TA, TB>()
where TB : class, TA
{
Register<TA, TB>(); // Fine
RegisterRestrictive<TA, TB>(); // CS0452
}
There's no pair of concrete types that match TInterface
and TImplementation
for only one of them, but the type-parameter types of a calling method certainly can, and type-parameter types are types we need to consider in designing APIs as well a concrete types.
Constraints do not involve inference of other constraints.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With