C# 知识点 part 2(out 泛型修饰符,协变和逆变)

Posted by moloach on 2017-08-17

out(泛型修饰符)-指定类型参数是协变的

对于泛型类型参数,out 关键字可指定类型参数是协变的。 可以在泛型接口和委托中使用 out 关键字。

协变使你使用的类型可以比泛型参数指定的类型派生程度更大。 这样可以隐式转换实现变体接口的类以及隐式转换委托类型。 引用类型支持协变和逆变,但值类型不支持它们。

具有协变类型参数的接口使其方法返回的类型可以比类型参数指定的类型派生程度更大。 例如,因为在 .NET Framework 4 的 IEnumerable 中,类型 T 是协变的,所以可以将 IEnumerabe(Of String) 类型的对象分配给 IEnumerable(Of Object) 类型的对象,而无需使用任何特殊转换方法。

可以向协变委托分配相同类型的其他委托,不过要使用派生程度更大的泛型类型参数。

有关详细信息,请参阅协变和逆变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Covariant interface.协变接口
interface ICovariant<out R> { }

// Extending covariant interface.扩展协变接口
interface IExtCovariant<out R> : ICovariant<R> { }

// Implementing covariant interface.
class Sample<R> : ICovariant<R> { }

class Program{
static void Test()
{
ICovariant<Object> iobj = new Sample<Object>();
ICovariant<String> istr = new Sample<String>();

// You can assign istr to iobj because
// the ICovariant interface is covariant.
iobj = istr;
}
}

注意事项

类型参数满足一下条件,则可以声明为协变:

  • 类型参数只用作接口方法的返回类型,而不用做方法参数的类型
    • note:
    • 如果在协变接口中将逆变泛型委托用作方法参数,则可以将协变类型用作此委托的泛型类型参数。(例外)
  • 类型参数不用做接口方法的泛型约束。

example

如何声明、实例化和调用协变泛型委托。

如何隐性转换委托类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Covariant delegate.
public delegate R DCovariant<out R>();

// Methods that match the delegate signature.
public static Control SampleControl()
{ return new Control(); }

public static Button SampleButton()
{ return new Button(); }

public void Test()
{
// Instantiate the delegates with the methods.
DCovariant<Control> dControl = SampleControl;
DCovariant<Button> dButton = SampleButton;

// You can assign dButton to dControl
// because the DCovariant delegate is covariant.
dControl = dButton;

// Invoke the delegate.
dControl();
}

协变和逆变(Covariance Contravariance)

可以对数组,委托,泛型类型进行隐性的引用转换。

本质上是一种检查类型的转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Assignment compatibility.   
string str = "test";
// An object of a more derived type is assigned to an object of a less derived type.
object obj = str;

// Covariance.
IEnumerable<string> strings = new List<string>();
// An object that is instantiated with a more derived type argument
// is assigned to an object instantiated with a less derived type argument.
// Assignment compatibility is preserved.
IEnumerable<object> objects = strings;

// Contravariance.
// Assume that the following method is in the class:
// static void SetObject(object o) { }
Action<object> actObject = SetObject;
// An object that is instantiated with a less derived type argument
// is assigned to an object instantiated with a more derived type argument.
// Assignment compatibility is reversed.
Action<string> actString = actObject;

对数组的协变是一种可以产生更多派生类型的数组的隐性转换。

1
2
3
object[] array = new String[10];
// the following statement produces a run-time expection.
//array[0] = 10;

协变和逆变在函数组中的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 static object GetObject() { return null; }  
static void SetObject(object obj) { }

static string GetString() { return ""; }
static void SetString(string str) { }

static void Test()
{
// Covariance. A delegate specifies a return type as object,
// but you can assign a method that returns a string.
Func<object> del = GetString;

// Contravariance. A delegate specifies a parameter type as string,
// but you can assign a method that takes an object.
Action<string> del2 = SetObject;
}

创建变体泛型接口

接口中的泛型类型参数可以声明为协变或逆变。

协变允许接口方法具有与泛型类型参数定义的返回类型相比,派生程度更大的返回类型。

逆变允许接口方法具有与泛型形参指定的实参类型相比,派生程度更小的实参类型。

具有协变或逆变泛型类型参数的泛型接口称为“变体”。

声明变体泛型接口

可通过对泛型类型参数使用 in 和 out 关键字来声明变体泛型接口。

注意事项

ref 和 out 参数不能为变体,值类型也不能为变体。

可以使用 out 关键字将泛型类型参数声明为协变。

协变类型必须满足以下条件:

  • 可以使用 out 关键字将泛型类型参数声明为协变。 协变类型必须满足以下条件

    1
    2
    3
    4
    5
    6
    7
    interface ICovariant<out R>  
    {
    R GetSomething();
    // The following statement generates a compiler error.
    // void SetSometing(R sampleArg);

    }
  • 类型不用作接口方法的泛型约束。

    1
    2
    3
    4
    5
    6
    7
    interface ICovariant<out R>  
    {
    // The following statement generates a compiler error
    // because you can use only contravariant or invariant types
    // in generic contstraints.
    // void DoSomething<T>() where T : R;
    }

可以使用 in 关键字将泛型类型参数声明为逆变。

逆变类型只能用作方法参数的类型,不能用作接口方法的返回类型。 逆变类型还可用于泛型约束。 以下代码演示如何声明逆变接口,以及如何将泛型约束用于其方法之一。

1
2
3
4
5
6
7
interface IContravariant<in A>  
{
void SetSomething(A sampleArg);
void DoSomething<T>() where T : A;
// The following statement generates a compiler error.
// A GetSomething();
}

还可以在同一接口中同时支持协变和逆变,但需应用于不同类型参数。

1
2
3
4
5
interface IVariant<out R, in A>{
R GetSomething();
Void SetSomething(A sampleArg);
R SetSomething(R sampleArg);
}

实现变体泛型接口

在类中实现变体泛型接口时,所用语法和固定接口的语法相同。

1
2
3
4
5
6
7
8
9
10
interface ICovariant<out R>{
R GetSomething();
}

class SampleImplementation<R>:ICovariant<R>{
public R GetSomething(){
//some code.
return default(R);
}
}

实现变体接口的类是固定类。

1
2
3
4
5
6
7
8
9
// 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 关键字来显式指定派生接口是否支持变体。 编译器不会根据正在扩展的接口来推断变体。

1
2
3
interface ICovariant<out T> { }  
interface IInvariant<T> : ICovariant<T> { }
interface IExtCovariant<out T> : ICovariant<T> { }

尽管 IInvariant 接口和 IExtCovariant 接口扩展的是同一个接口,但泛型类型参数 T 在前者中为固定参数,在后者中为协变参数。 此规则也适用于逆变泛型类型参数。

无论泛型类型参数 T 在接口中是协变还是逆变,都可以创建一个接口来扩展这两类接口,只要在扩展接口中,该 T 泛型类型参数为固定参数。

1
2
3
interface ICovariant<out T> { }  
interface IContravariant<in T> { }
interface IInvariant<T> : ICovariant<T>, IContravariant<T> { }

如果泛型类型参数 T 在一个接口中声明为协变,则无法在扩展接口中将其声明为逆变,反之亦然。

1
2
3
interface ICovariant<out T> { }  
// The following statement generates a compiler error.
// interface ICoContraVariant<in T> : ICovariant<T> { }

避免多义性

如果在一个类中使用不同的泛型类型参数来显式实现同一变体泛型接口,便会产生多义性。 在这种情况下,编译器不会产生错误,但未指定将在运行时选择哪个接口实现。 这可能导致代码中出现微妙的 bug。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 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 之间选择。 这可能导致代码中出现问题。