使用接口的委托作为参数类型时逆变无效
Contravariance invalid when using interface's delegate as a parameter type
考虑带有委托的逆变接口定义:
public interface IInterface<in TInput>
{
delegate int Foo(int x);
void Bar(TInput input);
void Baz(TInput input, Foo foo);
}
Baz
的定义失败并出现错误:
CS1961
Invalid variance: The type parameter 'TInput
' must be covariantly valid on 'IInterface<TInput>.Baz(TInput, IInterface<TInput>.Foo)
'. 'TInput
' is contravariant.
我的问题是为什么?乍一看这应该是有效的,因为 Foo
委托与 TInput
无关。不知道是编译器过于保守还是我遗漏了什么。
请注意,通常您不会在接口内声明委托,特别是这不会在早于 C# 8 的版本上编译,因为接口中的委托需要默认接口实现。
如果允许此定义,是否有办法破坏类型系统,或者编译器是否保守?
TL;DR;根据 ECMA-335 规范,这是正确的,令人困惑的是,在 某些 情况下它确实有效
假设我们有两个变量
IInterface<Animal> i1 = anInterfaceAnimalValue;
IInterface<Cat> i2 = anInterfaceCatValue;
我们可以拨打这些电话
i1.Baz(anAnimal, j => 5);
//this is the same as doing
i1.Baz(anAnimal, new IInterface<Animal>.Foo(j => 5));
i1.Baz(aCat, j => 5);
//this is the same as doing
i1.Baz(aCat, new IInterface<Animal>.Foo(j => 5));
i2.Baz(aCat, j => 5);
//this is the same as doing
i2.Baz(aCat, new IInterface<Cat>.Foo(j => 5));
如果我们现在分配 i1 = i2;
那么会发生什么?
i1.Baz(anAnimal, j => 5);
//this is the same as doing
i1.Baz(anAnimal, new IInterface<Animal>.Foo(j => 5));
但是IInterface<Cat>.Baz
(实际对象类型)不接受IInterface<Animal>.Foo
,它只接受IInterface<Cat>.Foo
.这两个事实代表具有相同的签名并不会影响他们成为不同的类型。
让我们再深入一点
先说两点:
首先,记住接口中的协变泛型类型可以出现在output位置(这允许更派生的类型), input 位置的反变体(允许更多的基本类型)。
Covariance and contravariance in generics
In general, a covariant type parameter can be used as the return type of a delegate, and contravariant type parameters can be used as parameter types. For an interface, covariant type parameters can be used as the return types of the interface's methods, and contravariant type parameters can be used as the parameter types of the interface's methods.
对于你传入的参数的类型参数,这有点令人困惑:如果T
是协变的(输出),一个函数可以使用void (Action<T>)
它看起来像是一个 input,并且可以接受一个 more 派生的委托。也可以 return Func<T>
.
如果 T
是逆变的,则相反。
有关这一点的进一步说明,请参阅 and 。
其次,ECMA-335,它定义了 CLI 的规范,如下(我的粗体):
II.9.1 Generic type definitions
The generic parameter is in scope in the declarations of:
- snip...
- all members (instance and static fields, methods, constructors, properties and events) except nested classes. [Note: C# allows generic parameters from an enclosing class to be used in a nested class, but adds any required extra generic parameters to the nested class definition in metadata. end note]
所以嵌套类型,其中 Foo
委托就是一个例子,实际上在作用域中没有泛型 T
类型。 C# 编译器将它们添加进来。
现在,看下面的代码,我已经注意到哪些行没有编译:
public delegate void FooIn<in T>(T input);
public delegate T FooOut<out T>();
public interface IInterfaceIn<in T>
{
void BarIn(FooIn<T> input); //must be covariant
FooIn<T> BazIn();
void BarOut(FooOut<T> input);
FooOut<T> BazOut(); //must be covariant
public delegate void FooNest();
public delegate void FooNestIn(T input);
public delegate T FooNestOut();
void BarNest(FooNest input); //must be covariant
void BarNestIn(FooNestIn input); //must be covariant
void BarNestOut(FooNestOut input); //must be covariant
FooNest BazNest();
FooNestIn BazNestIn();
FooNestOut BazNestOut();
}
public interface IInterfaceOut<out T>
{
void BarIn(FooIn<T> input);
FooIn<T> BazIn(); //must be contravariant
void BarOut(FooOut<T> input); //must be contravariant
FooOut<T> BazOut();
public delegate void FooNest();
public delegate void FooNestIn(T input);
public delegate T FooNestOut();
void BarNest(FooNest input); //must be contravariant
void BarNestIn(FooNestIn input); //must be contravariant
void BarNestOut(FooNestOut input); //must be contravariant
FooNest BazNest();
FooNestIn BazNestIn();
FooNestOut BazNestOut();
}
我们暂时坚持IInterfaceIn
。
取无效BarIn
。它使用FooIn
,其类型参数是协变的。
现在,如果我们有 anAnimalInterfaceValue
,那么我们可以用 FooIn<Animal>
参数调用 BarIn()
。这意味着委托接受一个 Animal
参数。如果我们随后将其转换为 IInterface<Cat>
,那么我们可以使用 FooIn<Cat>
调用它, 要求 类型为 Cat
的参数和底层对象不期待这么严格的委托,它期望能够通过 any Animal
.
因此 BarIn
只能使用与声明的类型相同或更少 派生的类型,因此它无法接收 T
IInterfaceIn
最终可能会 更多 派生。
然而,BarOut
是有效的,因为它使用 FooOut
,它有一个 contra-变体 T
.
现在让我们看看 FooNestIn
和 FooNestOut
。这些实际上重新声明了封闭类型的 T
参数。 FooNestOut
无效,因为它在输出位置使用协变体 in T
。 FooNestIn
有效。
让我们继续 BarNest
、BarNestIn
和 BarNestOut
。这些都是 all 无效的,因为它们使用具有 co-variant 泛型参数的委托。 这里的关键是我们不关心委托是否真的在必要的位置使用了类型参数,我们关心的是委托的泛型参数的变化是否与我们提供的类型相匹配。
啊哈,你说,那为什么 IInterfaceOut
嵌套参数不起作用?
让我们再看看 ECMA-335,它讨论了泛型参数是有效的,并断言泛型类型的每个部分都必须是有效的(我的粗体,S
指的是泛型类型,例如 List<T>
, T
表示类型参数, var
表示相应参数的 in/out
):
II.9.7 Validity of member signatures
Given the annotated generic parameters S = <var_1 T_1, ..., var_n T_n>
, we define what it means for various components of the type definition to be valid with respect to S
. We define a negation operation on annotations, written ¬S
, to mean “flip negatives to positives, and positives to negatives”
Methods. A method signature tmeth(t_1,...,t_n)
is valid with respect to S
if
- its result type signature
t
is valid with respect to S
; and
- each argument type signature
t_i
is valid with respect to ¬S
.
- each method generic parameter constraint type
t_j
is valid with respect to ¬S
. [Note: In other words, the result behaves covariantly and the arguments behave contravariantly...
所以我们翻转方法参数中使用的类型的方差。
所有这一切的结果是 永远不会 有效地在方法中使用嵌套的联合 or contra-variant 类型参数位置,因为所需的 variance 被翻转,因此不会匹配。不管我们怎么做,都行不通。
相反,在 return 位置使用委托总是有效的。
我不确定这是否是协变与逆变问题。
Foo
代表不是 接口的成员。它是嵌套类型声明。
IInterface<A>.Foo
和IInterface<B>.Foo
是两种不同的类型。
- 这使得两个不同
IInterface<T>.Baz
方法(T
= A
和 B
)的 foo
参数不兼容。
- 因此,您不能用
IInterface<A>
替换 IInterface<B>
,反之亦然(无论 A
和 B
之间的继承关系是什么。
- 结论:
IInterface<T>
不能变体(既不是同向也不是对向)。
分辨率:
- 将委托移动到顶层(在命名空间的主体中)。它是类型声明,因此不需要嵌入。
- 或者将它嵌入到一个没有类型参数的类型中。例如,您可以为此创建一个非泛型
IInterface
(并保留您的泛型)。
但@EricLippert 肯定知道得更多。
考虑带有委托的逆变接口定义:
public interface IInterface<in TInput>
{
delegate int Foo(int x);
void Bar(TInput input);
void Baz(TInput input, Foo foo);
}
Baz
的定义失败并出现错误:
CS1961
Invalid variance: The type parameter 'TInput
' must be covariantly valid on 'IInterface<TInput>.Baz(TInput, IInterface<TInput>.Foo)
'. 'TInput
' is contravariant.
我的问题是为什么?乍一看这应该是有效的,因为 Foo
委托与 TInput
无关。不知道是编译器过于保守还是我遗漏了什么。
请注意,通常您不会在接口内声明委托,特别是这不会在早于 C# 8 的版本上编译,因为接口中的委托需要默认接口实现。
如果允许此定义,是否有办法破坏类型系统,或者编译器是否保守?
TL;DR;根据 ECMA-335 规范,这是正确的,令人困惑的是,在 某些 情况下它确实有效
假设我们有两个变量
IInterface<Animal> i1 = anInterfaceAnimalValue;
IInterface<Cat> i2 = anInterfaceCatValue;
我们可以拨打这些电话
i1.Baz(anAnimal, j => 5);
//this is the same as doing
i1.Baz(anAnimal, new IInterface<Animal>.Foo(j => 5));
i1.Baz(aCat, j => 5);
//this is the same as doing
i1.Baz(aCat, new IInterface<Animal>.Foo(j => 5));
i2.Baz(aCat, j => 5);
//this is the same as doing
i2.Baz(aCat, new IInterface<Cat>.Foo(j => 5));
如果我们现在分配 i1 = i2;
那么会发生什么?
i1.Baz(anAnimal, j => 5);
//this is the same as doing
i1.Baz(anAnimal, new IInterface<Animal>.Foo(j => 5));
但是IInterface<Cat>.Baz
(实际对象类型)不接受IInterface<Animal>.Foo
,它只接受IInterface<Cat>.Foo
.这两个事实代表具有相同的签名并不会影响他们成为不同的类型。
让我们再深入一点
先说两点:
首先,记住接口中的协变泛型类型可以出现在output位置(这允许更派生的类型), input 位置的反变体(允许更多的基本类型)。
Covariance and contravariance in generics
In general, a covariant type parameter can be used as the return type of a delegate, and contravariant type parameters can be used as parameter types. For an interface, covariant type parameters can be used as the return types of the interface's methods, and contravariant type parameters can be used as the parameter types of the interface's methods.
对于你传入的参数的类型参数,这有点令人困惑:如果T
是协变的(输出),一个函数可以使用void (Action<T>)
它看起来像是一个 input,并且可以接受一个 more 派生的委托。也可以 return Func<T>
.
如果 T
是逆变的,则相反。
有关这一点的进一步说明,请参阅
其次,ECMA-335,它定义了 CLI 的规范,如下(我的粗体):
II.9.1 Generic type definitions
The generic parameter is in scope in the declarations of:
- snip...
- all members (instance and static fields, methods, constructors, properties and events) except nested classes. [Note: C# allows generic parameters from an enclosing class to be used in a nested class, but adds any required extra generic parameters to the nested class definition in metadata. end note]
所以嵌套类型,其中 Foo
委托就是一个例子,实际上在作用域中没有泛型 T
类型。 C# 编译器将它们添加进来。
现在,看下面的代码,我已经注意到哪些行没有编译:
public delegate void FooIn<in T>(T input);
public delegate T FooOut<out T>();
public interface IInterfaceIn<in T>
{
void BarIn(FooIn<T> input); //must be covariant
FooIn<T> BazIn();
void BarOut(FooOut<T> input);
FooOut<T> BazOut(); //must be covariant
public delegate void FooNest();
public delegate void FooNestIn(T input);
public delegate T FooNestOut();
void BarNest(FooNest input); //must be covariant
void BarNestIn(FooNestIn input); //must be covariant
void BarNestOut(FooNestOut input); //must be covariant
FooNest BazNest();
FooNestIn BazNestIn();
FooNestOut BazNestOut();
}
public interface IInterfaceOut<out T>
{
void BarIn(FooIn<T> input);
FooIn<T> BazIn(); //must be contravariant
void BarOut(FooOut<T> input); //must be contravariant
FooOut<T> BazOut();
public delegate void FooNest();
public delegate void FooNestIn(T input);
public delegate T FooNestOut();
void BarNest(FooNest input); //must be contravariant
void BarNestIn(FooNestIn input); //must be contravariant
void BarNestOut(FooNestOut input); //must be contravariant
FooNest BazNest();
FooNestIn BazNestIn();
FooNestOut BazNestOut();
}
我们暂时坚持IInterfaceIn
。
取无效BarIn
。它使用FooIn
,其类型参数是协变的。
现在,如果我们有 anAnimalInterfaceValue
,那么我们可以用 FooIn<Animal>
参数调用 BarIn()
。这意味着委托接受一个 Animal
参数。如果我们随后将其转换为 IInterface<Cat>
,那么我们可以使用 FooIn<Cat>
调用它, 要求 类型为 Cat
的参数和底层对象不期待这么严格的委托,它期望能够通过 any Animal
.
因此 BarIn
只能使用与声明的类型相同或更少 派生的类型,因此它无法接收 T
IInterfaceIn
最终可能会 更多 派生。
BarOut
是有效的,因为它使用 FooOut
,它有一个 contra-变体 T
.
现在让我们看看 FooNestIn
和 FooNestOut
。这些实际上重新声明了封闭类型的 T
参数。 FooNestOut
无效,因为它在输出位置使用协变体 in T
。 FooNestIn
有效。
让我们继续 BarNest
、BarNestIn
和 BarNestOut
。这些都是 all 无效的,因为它们使用具有 co-variant 泛型参数的委托。 这里的关键是我们不关心委托是否真的在必要的位置使用了类型参数,我们关心的是委托的泛型参数的变化是否与我们提供的类型相匹配。
啊哈,你说,那为什么 IInterfaceOut
嵌套参数不起作用?
让我们再看看 ECMA-335,它讨论了泛型参数是有效的,并断言泛型类型的每个部分都必须是有效的(我的粗体,S
指的是泛型类型,例如 List<T>
, T
表示类型参数, var
表示相应参数的 in/out
):
II.9.7 Validity of member signatures
Given the annotated generic parameters
S = <var_1 T_1, ..., var_n T_n>
, we define what it means for various components of the type definition to be valid with respect toS
. We define a negation operation on annotations, written¬S
, to mean “flip negatives to positives, and positives to negatives”Methods. A method signature
tmeth(t_1,...,t_n)
is valid with respect toS
if
- its result type signature
t
is valid with respect toS
; and- each argument type signature
t_i
is valid with respect to¬S
.- each method generic parameter constraint type
t_j
is valid with respect to¬S
. [Note: In other words, the result behaves covariantly and the arguments behave contravariantly...
所以我们翻转方法参数中使用的类型的方差。
所有这一切的结果是 永远不会 有效地在方法中使用嵌套的联合 or contra-variant 类型参数位置,因为所需的 variance 被翻转,因此不会匹配。不管我们怎么做,都行不通。
相反,在 return 位置使用委托总是有效的。
我不确定这是否是协变与逆变问题。
Foo
代表不是 接口的成员。它是嵌套类型声明。IInterface<A>.Foo
和IInterface<B>.Foo
是两种不同的类型。- 这使得两个不同
IInterface<T>.Baz
方法(T
=A
和B
)的foo
参数不兼容。 - 因此,您不能用
IInterface<A>
替换IInterface<B>
,反之亦然(无论A
和B
之间的继承关系是什么。 - 结论:
IInterface<T>
不能变体(既不是同向也不是对向)。
分辨率:
- 将委托移动到顶层(在命名空间的主体中)。它是类型声明,因此不需要嵌入。
- 或者将它嵌入到一个没有类型参数的类型中。例如,您可以为此创建一个非泛型
IInterface
(并保留您的泛型)。
但@EricLippert 肯定知道得更多。