为什么必须在后代类型上重新声明泛型类型限制

本文关键字:声明 泛型类型 新声明 后代 类型 为什么 | 更新日期: 2023-09-27 18:11:12

在c#中,给定一个像这样的泛型类型:

interface IGenericType<T> where T : new()

和后代类型,例如:

class GenericTypeImplementation<U> : IGenericType<U>

为什么我们需要用父类型的所有限制显式地限制泛型类型U ?

class GenericTypeImplementation<U> : IGenericType<U> where U : new()

我在推断问题是在编译器计算限制的联合正确吗?

interface IGenericType<T> where T : new()
interface IGenericType2<T> where T : SomeOtherType
class GenericTypeImplementation<U> : IGenericType<U>, IGenericType2<U>
/* Hypothesis: Compiler can't infer U must be "SomeOtherType + new()" */

为什么必须在后代类型上重新声明泛型类型限制

在我看来,编译器可以足够聪明,从理论上推断出这些限制。但是不应该这么聪明,因为过于聪明的编译器有时是危险的。开发人员总是需要对所有内容有一个清晰/明确的定义。请看这个场景:

(1)有一个interface IFoo<T> where T : new()

(2) a class Foo<T> : IFoo<T>new()约束由编译器自动添加(太棒了!)

(3)类Foo<T>在整个项目中是一个非常基本的类,class A<T> : Foo<T>,然后class B<T> : A<T>

(4)现在另一个开发人员很难通过查看类的定义来意识到有这样一个约束,他将得到奇怪的编译错误(这是可以接受的)。但是如果它们是通过反射调用的呢?有时程序是正确的,因为数据偶然地满足了限制。

编译器能够推断出U必须可以转换为SomeOtherType并且必须有一个默认构造函数。它将为每个约束生成一个编译器错误:

Error   1   The type 'U' must have a public parameterless constructor in order to use it as parameter 'T' in the generic type or method '....IGenericType<T>'
Error   2   The type 'U' must be convertible to '....SomeOtherType' in order to use it as parameter 'T' in the generic type or method '....IGenericType2<T>'

如果只实现其中一个接口,也会发生这种情况。类必须成功实现这两个接口才能被编译:

class GenericTypeImplementation<U> : IGenericType<U>, IGenericType2<U>
    where U : SomeOtherType, new()
{...}

或作为非泛型类型:

class GenericTypeImplementation : IGenericType<SomeType>, IGenericType2<SomeOtherType>
{...}

将一个类标记为实现了一个接口,并不是对类的泛型类型参数指定约束的一种方式;这是一种要求这些约束存在于新类型参数上,或者由所提供的类型满足这些约束的方法。

也许你可以这样想:接口是类的约束集,泛型类也是类的约束集。泛型接口是一组受约束的泛型类。当你说一个泛型类实现了一个泛型接口时,你是在问编译器,"这个泛型类是否严格在这个泛型接口指定的集合内?"您不仅仅是将它们作为进一步约束的类集相交。

因为泛型类型限制是在定义类的类型参数上(在您的示例中是U),从CLR的角度来看,它与接口的类型参数是不同的类型。

类的类型参数不必是接口的实际类型参数。它甚至不需要是简单类型,如:

class Implementation<T> : IGenericType<List<T>> { /* ... */ }

在这种情况下,编译器识别List<T>满足约束,因此不需要进一步的规范。但是如果不了解泛型类型参数,编译器会要求您显式声明它。

将此与泛型方法的类似但不相同的行为进行比较是有指导意义的。与实现接口的类一样,类型限制必须与声明一起指定。有一个值得注意的例外:如果实现是显式的。事实上,当您尝试重新施加这些限制时,编译器将生成一个错误。

例如,给定一个接口

interface ISomething {
    void DoIt<T>() where T : new();
}

实现这个接口的两种正确方法是:

class OneThing : ISomething {
    public void DoIt<T>() where T : new() { }
}
class OtherThing : ISomething {
    void ISomething.DoIt<T>() { }
}

OneThing中省略约束或在OtherThing中插入约束会产生编译时错误。为什么我们在第一个实现中需要约束,而在第二个实现中不需要?对于接口上的类型约束,我要说的原因与上面提到的相同:在第一个实现中,类型T与接口方法上的类型参数没有关系,因此必须显式地使方法与接口方法匹配。在第二个实现中,显式声明接口意味着类型参数T与接口中使用的类型参数完全相同。