Search code examples
c#genericsboxingtype-constraintsgeneric-constraints

Constructing/making a generic type and turning a type constraint into a struct-as-base-type constraint


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

Solution

  • 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 class C, from T to any base class of C, and from T to any interface implemented by C. [...]

    • [...]

    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 constraint V that is a value-type, use instead the most specific base type of V 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.