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