Normally we cannot constrain a type parameter T
to deriving from a sealed type (such as a struct
type). This would be meaningless because there is only one type which could fit, and as such there is no need for generics. So constraints like:
where T : string
or:
where T : DateTime
are illegal, for a very good reason.
However, when constraining to another type parameter, this can sometimes happen when that other type parameter is "substituted" into an actual type (which happens to be sealed). Consider the class:
abstract class ExampleBase<TFromType>
{
internal abstract void M<TFromMethod>(TFromMethod value) where TFromMethod : TFromType;
}
which is quite innocent. In the concretization:
class ExampleOne : ExampleBase<string>
{
internal override void M<TFromMethod>(TFromMethod strangeString)
{
var a = string.IsNullOrEmpty(strangeString);
Console.WriteLine(a);
var b = strangeString.Substring(10, 2);
Console.WriteLine(b);
}
}
we make TFromType
equal to string
. This could be meaningful wrt. other members than M<>
. But M<>
itself can still be used: The code:
var e1 = new ExampleOne();
e1.M("abcdefghijklmnopqrstuvwxyz");
will run and write:
False kl
to the console. So the constraint essentially became where TFromMethod : string
, but things were still fine.
This question is about what happens if TFromType
is a value type. So this time we do:
class ExampleTwo : ExampleBase<DateTime>
{
internal override void M<TFromMethod>(TFromMethod strangeDate)
{
// var c = DateTime.SpecifyKind(strangeDate, DateTimeKind.Utc); // will not compile
// var d = strangeDate.AddDays(66.5); // will not compile
var e = string.Format(CultureInfo.InvariantCulture, "{0:D}", strangeDate); // OK, through boxing
Console.WriteLine(e);
var f = object.ReferenceEquals(strangeDate, strangeDate);
Console.WriteLine("Was 'strangeDate' a box? " + f);
}
}
So why are the calls from the c
and d
declarations not allowed? After all strangeDate
has compile-time type TFromMethod
which is constrained to be a DateTime
. So surely strangeDate
is implicitly a DateTime
? After all, this worked with string
(class ExampleOne
above).
I would prefer an answer which referred to the relevant place in the official C# Language Specification.
Note that when trying to add d
, typing strangeDate.Ad
... makes IntelliSense (Visual Studio's autocompleter) come up with a list of all accessible instance members of DateTime
, so clearly IntelliSense thinks the call in d
should be legal!
Of course, after c
and d
have been commented out, we can use ExampleTwo
(with e
and f
), and the code:
var e2 = new ExampleTwo();
e2.M(new DateTime(2015, 2, 13));
runs and writes out:
Friday, 13 February 2015 Was 'strangeDate' a box? False
To quote the C# 5.0 specification:
6.1.10 Implicit conversions involving type parameters
The following implicit conversions exist for a given type parameter
T
:
From
T
to its effective base classC
, fromT
to any base class ofC
, and fromT
to any interface implemented byC
. [...][...]
10.1.5 Type parameter constraints
The effective base class of a type parameter
T
is defined as follows:
- [...]
- If
T
has no class-type constraint but has one or more type-parameter constraints, its effective base class is the most encompassed type (§6.4.2) in the set of effective base classes of its type-parameter constraints. The consistency rules ensure that such a most encompassed type exists.- [...]
For the purpose of these rules, if
T
has a constraintV
that is a value-type, use instead the most specific base type ofV
that is a class-type. This can never happen in an explicitly given constraint, but may occur when the constraints of a generic method are implicitly inherited by an overriding method declaration or an explicit implementation of an interface method.These rules ensure that the effective base class is always a class-type.
In other words, given a where U : T
constraint with T = string
, the effective base class of U
is string
. Given a where U : T
constraint with T = DateTime
, the effective base class of U
is not DateTime
, but ValueType
. And the only relevant implicit conversion of a type parameter is from the type parameter type to its effective base class.
This does seem to lead to some rather odd behaviour, as you've found, but it must nonetheless have been a conscious decision, as it has been explicitly spelled out to behave the way you've seen.
I would guess that making this work caused difficulties in the compiler, that there are cases where the compiler assumes that it's dealing with a reference type in such cases, and that there is only minimal benefit in making it work. That's just that, though: a guess.