CS4L17——协变逆变
CS4L17——协变逆变
本章代码关键字
1 | out // 协变,它修饰泛型委托和泛型接口的泛型参数,使得此泛型参数只能作为返回值 |
协变和逆变
-
协变:和谐的变化,自然的变化
因为 里氏替换原则 父类可以装子类,所以 子类变父类,比如
string
变成object
,感受是和谐的 -
逆变:逆常规的变化,不正常的变化
因为 里氏替换原则 父类可以装子类 但是子类不能装父类,所以 父类变子类,比如
object
变成string
,感受是不和谐的
协变和逆变是用来修饰泛型的:
- 协变:
out
- 逆变:
in
用于在泛型中 修饰 泛型字母的。只有 泛型接口 和 泛型委托 能使用
它的作用主要是:
-
out
修饰的泛型类型,只能作为返回值类型,in
修饰的泛型类型 只能作为 参数类型 -
遵循里氏替换原则的 ,用
out
和in
修饰的 泛型委托 可以相互装载(有父子关系的泛型)- 协变:父类泛型委托装子类泛型委托
- 逆变:子类泛型委托装父类泛型委托
协变逆变的作用
最直观的理解其作用,就是从 返回值 和 参数 来看:
-
用
out
修饰的泛型,只能作为返回值1
2
3
4
5
6
7
8delegate T TestOut<out T>();
delegate T TestOut<out T>(T arg); // error: 变型无效: 类型参数“T”必须是在“TestOut<T>.Invoke(T)”上有效的 逆变式。“T”为 协变。
delegate T TestOut<T>(T arg);
interface ITestOut<out T>
{
void Testin(T t); // error: 变型无效: 类型参数“T”必须是在“ITestOut<T>.Testin(T)”上有效的 逆变式。“T”为 协变。
T Testout();
} -
用
in
修饰的泛型,只能作为参数1
2
3
4
5
6
7
8delegate void TestIn<in T>(T t);
delegate T TestIn<in T>(T t); // error: 变型无效: 类型参数“T”必须是在“TestIn<T>.Invoke(T)”上有效的 协变式。“T”为 逆变。
delegate T TestIn<T>(T t);
interface ITestIn<in T>
{
void Testin(T t);
T Testout(); // 变型无效: 类型参数“T”必须是在“ITestIn<T>.Testout()”上有效的 协变式。“T”为 逆变。
}
in
和 out
只有 泛型接口 和 泛型委托 能使用
1 | interface ITest<in T> {} |
但如果需要深度理解其作用,就需要结合里氏替换原则原则去理解:
-
协变:让 父类泛型委托/接口 可以装载 子类泛型委托/接口
先声明如下内容:
1
2
3delegate T TestOut<T>(); // 这里没有使用协变
class Father {}
class Son : Father {}如果要让一个 父类泛型委托 直接装载 子类泛型委托变量,让父类泛型委托可以返回一个子类变量,这是做不到的:
1
2
3TestOut<Son> sonFunc = () => new Son();
TestOut<Father> fatherFunc = sonFunc; // error: 无法将类型“TestOut<Son>”隐式转换为“TestOut<Father>”
Father f1 = fatherFunc();这是因为,委托类型变量只能存储另一个类型完全相同的委托变量,因此,即使同一泛型委托类型的泛型参数存在父子关系,也不能装载
接口也是同理:
1
2
3interface ITestOut<T>{} // 这里没有使用协变
class Father {}
class Son : Father {}如果要让一个 父类泛型接口 直接装载 子类泛型接口变量,让父类泛型接口的方法可以返回一个子类变量,也是做不到的:
1
2
3ITestOut<Son> son = new TestClass<Son>();
ITestOut<Father> father = son; // error: 无法将类型“ITestOut<Son>”隐式转换为“ITestOut<Father>”
Father f2 = father.TestOut();但是,如果给泛型接口或者委托的泛型参数加上
out
,就可以让父类泛型接口/委托变量可以装载子类泛型接口/委托变量了1
2
3
4
5
6
7
8
9
10
11
12
13
14delegate T TestOut<out T>();
interface ITestOut<out T>
{
T TestOut();
}
class TestClass<T> : ITestOut<T>
{
public T? TestOut() => default;
}
class Father {}
class Son : Father {}使用
out
修饰的泛型参数T
,就可以让 父类泛型接口/委托变量 装载 子类泛型接口/委托变量1
2
3
4
5
6
7TestOut<Son> sonFunc = () => new Son();
TestOut<Father> fatherFunc = sonFunc;
Father f1 = fatherFunc();
ITestOut<Son> son = new TestClass<Son>();
ITestOut<Father> father = son;
Father f2 = father.TestOut();由于是父类泛型容器装子类泛型容器,符合里氏替换原则,所以是和谐的
-
逆变:让 子类泛型委托/接口 可以装载 父类泛型委托/接口
先声明如下内容:
1
2
3
4
5
6
7
8delegate void TestIn<T>(T arg); // 这里没有使用逆变
class Father
{
public void DoSomething() {}
}
class Son : Father {}如果要让一个 子类泛型委托变量 直接装载 父类泛型委托变量,然后将子类对象传入到泛型委托参数内,这是做不到的:
1
2
3TestIn<Father> iF = f => f.DoSomething();
TestIn<Son> iS = iF; // error: 无法将类型“TestIn<Father>”隐式转换为“TestIn<Son>”
iS(new Son());接口同理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17interface ITestIn<T>
{
public void TestIn(T arg);
}
class TestClass<T> : ITestIn<T>
{
public T? value;
public void TestIn(T arg)
{
value = arg;
}
}
class Father {}
class Son : Father {}如果要让一个 子类泛型接口变量 直接装载 父类泛型接口变量,然后让子类变量作为参数传入到泛型接口的方法内,也是做不到的:
1
2
3ITestIn<Father> father = new TestClass<Father>();
ITestIn<Son> son = father; // error: 无法将类型“ITestIn<Father>”隐式转换为“ITestIn<Son>”
son.TestIn(new Son());使用
in
修饰的泛型参数T
,就可以自动根据里氏替换原则,让 子类泛型接口/委托变量 装载 父类泛型接口/委托变量1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23delegate void TestIn<in T>(T arg);
interface ITestIn<in T>
{
public void TestIn(T arg);
}
class TestClass<T> : ITestIn<T>
{
public T? value;
public void TestIn(T arg)
{
value = arg;
}
}
class Father
{
public void DoSomething() {}
}
class Son : Father {}使用
in
修饰的泛型参数T
,就可以让 子类泛型接口/委托变量 装载 父类泛型接口/委托变量1
2
3
4
5
6
7TestIn<Father> fatherAction = f => f.DoSomething();
TestIn<Son> sonAction = fatherAction;
sonAction(new Son());
ITestIn<Father> father = new TestClass<Father>();
ITestIn<Son> son = father;
son.TestIn(new Son());由于是 子类泛型容器 装 父类泛型容器,看起来不符合里氏替换原则,所以是逆常规的