Constructing/making 泛型类型并将类型约束转换为基类型约束

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

通常我们不能将类型参数 T 限制为从密封类型(例如 struct 类型)派生。这将毫无意义,因为只有一种类型适合,因此不需要泛型。所以约束如下:

where T : string

或:

where T : DateTime

是非法的,这是有充分理由的。

但是,当约束到另一个类型参数时,当另一个类型参数 "substituted" 变成实际类型(碰巧是密封的)时,有时会发生这种情况。考虑 class:

abstract class ExampleBase<TFromType>
{
  internal abstract void M<TFromMethod>(TFromMethod value) where TFromMethod : TFromType;
}

这是很无辜的。具体化中:

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);
  }
}

我们让 TFromType 等于 string。这可能是有意义的。 M<> 以外的其他成员。但是M<>本身还是可以使用的:代码:

  var e1 = new ExampleOne();
  e1.M("abcdefghijklmnopqrstuvwxyz");

将运行写成:

错误
kl

到控制台。所以约束基本上变成了where TFromMethod : string,但事情仍然很好。

这个问题是关于如果 TFromType 是一个值类型会发生什么。所以这次我们这样做:

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);
  }
}

那么为什么不允许来自 cd 声明的调用? 毕竟 strangeDate 具有编译时类型 TFromMethod 被限制为 DateTime。所以肯定 strangeDate 隐式地是一个 DateTime?毕竟,这适用于 string(上面的 class ExampleOne)。

我希望参考官方 C# 语言规范中相关位置的答案。

请注意,当尝试添加 d 时,键入 strangeDate.Ad... 会使 IntelliSense(Visual Studio 的自动完成器)列出 [的所有可访问实例成员=30=],很明显 IntelliSense 认为 d 中的调用应该是合法的!

当然,在cd被注释掉之后,我们可以使用ExampleTwo(加上ef),而代码:

  var e2 = new ExampleTwo();
  e2.M(new DateTime(2015, 2, 13));

运行s 并写出:

2015 年 2 月 13 日,星期五
'strangeDate' 是一个盒子吗?假

在 C#5 章节 13.4.3 Implementation of generic methods 中说:

interface I<C>
{
    void H<T>(T t) where T: C;
}

class C: I<string>
{
    void H<T>(T t) 
    {
        string s = t;   // Ok
    }
}

他们说:

Note that the assignment from t to s is valid since T inherits a constraint of T: string, even though this constraint is not expressible in source code.

我理解为:"you can write it, declare it, even if you can't use it"

这里String是一个引用类型,当我们写s = t时,我们给引用赋值,赋值是有效的,因为where T: C约束允许。

对于 DateTime,当我们写 s = t 时,我们复制的是值,而不是引用。

引用 C# 5.0 规范:

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.

换句话说,给定 where U : T 约束 T = stringU 的有效基数 class 是 string。给定 T = DateTimewhere U : T 约束,U 的有效基数 class 不是 DateTime,而是 ValueType。并且类型参数唯一相关的隐式转换是从类型参数类型到它的有效基 class.

正如您所发现的,这似乎确实导致了一些相当奇怪的行为,但它一定是一个有意识的决定,因为它已被明确说明以您所见的方式行事。

我猜想使这项工作在编译器中造成困难,在某些情况下编译器假定它在这种情况下处理引用类型,并且使它工作的好处微乎其微。不过,仅此而已:一个猜测。