可以在接口中将泛型类型参数声明为协变或逆变。 协变允许接口方法具有与泛型类型参数定义的返回类型相比,派生程度更大的返回类型。 逆变允许接口方法具有与泛型形参指定的实参类型相比,派生程度更小的实参类型。 具有协变或逆变泛型类型参数的泛型接口称为 变体。
注释
.NET Framework 4 引入了对多个现有泛型接口的方差支持。 有关 .NET 中变体接口的列表,请参阅泛型接口中的变体(C#)。
声明变体泛型接口
可以使用关键字in
和out
为泛型类型参数声明变体泛型接口。
重要
ref
和 in
out
C# 中的参数不能是变体。 值类型也不支持差异。
可以使用关键字声明泛型类型参数协变 out
。 协变类型必须满足以下条件:
该类型仅用作接口方法的返回类型,而不用作方法参数的类型。 以下示例对此进行了说明,在该示例中,类型
R
声明为协变。interface ICovariant<out R> { R GetSomething(); // The following statement generates a compiler error. // void SetSomething(R sampleArg); }
此规则有一个例外。 如果有逆变泛型委托作为方法参数,则可以将该类型用作该委托的泛型类型参数。 下面的示例中的类型
R
对此进行了说明。 有关详细信息,请参阅委托中的协变和逆变(C#)以及在 Func 和 Action 泛型委托中使用协变和逆变(C#)。interface ICovariant<out R> { void DoSomething(Action<R> callback); }
该类型不用作接口方法的泛型约束。 以下代码对此进行了说明。
interface ICovariant<out R> { // The following statement generates a compiler error // because you can use only contravariant or invariant types // in generic constraints. // void DoSomething<T>() where T : R; }
可以使用关键字声明泛型类型参数逆变 in
。 逆变量类型只能用作方法参数的类型,不能用作接口方法的返回类型。 逆变类型也可用于泛型约束。 以下代码演示如何声明逆变接口,并对其方法之一使用泛型约束。
interface IContravariant<in A>
{
void SetSomething(A sampleArg);
void DoSomething<T>() where T : A;
// The following statement generates a compiler error.
// A GetSomething();
}
也可以在同一接口中同时支持协变和逆变,但对于不同的类型参数,如以下代码示例所示。
interface IVariant<out R, in A>
{
R GetSomething();
void SetSomething(A sampleArg);
R GetSetSomethings(A sampleArg);
}
实现变体泛型接口
通过使用用于固定接口的相同语法,在类中实现变体泛型接口。 下面的代码示例演示如何在泛型类中实现协变接口。
interface ICovariant<out R>
{
R GetSomething();
}
class SampleImplementation<R> : ICovariant<R>
{
public R GetSomething()
{
// Some code.
return default(R);
}
}
实现变体接口的类是固定的。 例如,请考虑以下代码。
// The interface is covariant.
ICovariant<Button> ibutton = new SampleImplementation<Button>();
ICovariant<Object> iobj = ibutton;
// The class is invariant.
SampleImplementation<Button> button = new SampleImplementation<Button>();
// The following statement generates a compiler error
// because classes are invariant.
// SampleImplementation<Object> obj = button;
扩展变体泛型接口
扩展变体泛型接口时,必须使用 in
和 out
关键字显式指定派生接口是否支持变体。 编译器不会根据正在扩展的接口来推断变体。 例如,请考虑以下接口。
interface ICovariant<out T> { }
interface IInvariant<T> : ICovariant<T> { }
interface IExtCovariant<out T> : ICovariant<T> { }
在接口中 IInvariant<T>
,泛型类型参数 T
是固定的,而在类型参数中 IExtCovariant<out T>
是协变的,尽管这两个接口都扩展了相同的接口。 同一规则应用于逆变泛型类型参数。
无论泛型类型参数 T
在接口中是协变还是逆变,都可以创建一个接口来扩展这两类接口,只要在扩展接口中,该 T
泛型类型参数为固定参数。 下面的代码示例对此进行了说明。
interface ICovariant<out T> { }
interface IContravariant<in T> { }
interface IInvariant<T> : ICovariant<T>, IContravariant<T> { }
但是,如果在一个接口中声明泛型类型参数 T
,则不能在扩展接口中声明该参数是逆变的,反之亦然。 下面的代码示例对此进行了说明。
interface ICovariant<out T> { }
// The following statement generates a compiler error.
// interface ICoContraVariant<in T> : ICovariant<T> { }
避免歧义
实现变体泛型接口时,变体有时可能会导致歧义。 应避免这种歧义。
例如,如果在一个类中显式实现具有相同泛型类型参数的同一变体泛型接口,则可以创建歧义。 在这种情况下,编译器不会生成错误,但未指定将在运行时选择哪个接口实现。 这种歧义可能会导致代码中出现细微的 bug。 请考虑以下代码示例。
// Simple class hierarchy.
class Animal { }
class Cat : Animal { }
class Dog : Animal { }
// This class introduces ambiguity
// because IEnumerable<out T> is covariant.
class Pets : IEnumerable<Cat>, IEnumerable<Dog>
{
IEnumerator<Cat> IEnumerable<Cat>.GetEnumerator()
{
Console.WriteLine("Cat");
// Some code.
return null;
}
IEnumerator IEnumerable.GetEnumerator()
{
// Some code.
return null;
}
IEnumerator<Dog> IEnumerable<Dog>.GetEnumerator()
{
Console.WriteLine("Dog");
// Some code.
return null;
}
}
class Program
{
public static void Test()
{
IEnumerable<Animal> pets = new Pets();
pets.GetEnumerator();
}
}
在此示例中,未指定方法如何在 pets.GetEnumerator
和 Cat
之间进行选择Dog
。 这可能会导致代码中出现问题。