CS4L17——协变逆变

本章代码关键字

1
2
out        // 协变,它修饰泛型委托和泛型接口的泛型参数,使得此泛型参数只能作为返回值
in // 逆变,它修饰泛型委托和泛型接口的泛型参数,使得此泛型参数只能作为参数

协变和逆变

  • 协变:和谐的变化,自然的变化

    因为 里氏替换原则 父类可以装子类,所以 子类变父类,比如 string​ 变成 object​,感受是和谐的

  • 逆变:逆常规的变化,不正常的变化

    因为 里氏替换原则 父类可以装子类 但是子类不能装父类,所以 父类变子类,比如 object​ 变成 string​,感受是不和谐的

协变和逆变是用来修饰泛型的:

  • 协变:out
  • 逆变:in

用于在泛型中 修饰 泛型字母的。只有 泛型接口 和 泛型委托 能使用

它的作用主要是:

  1. out​ 修饰的泛型类型,只能作为返回值类型,in​ 修饰的泛型类型 只能作为 参数类型

  2. 遵循里氏替换原则的 ,用 out​ 和 in​ 修饰的 泛型委托 可以相互装载(有父子关系的泛型)

    • 协变:父类泛型委托装子类泛型委托
    • 逆变:子类泛型委托装父类泛型委托

协变逆变的作用

最直观的理解其作用,就是从 返回值 和 参数 来看:

  • out​ 修饰的泛型,只能作为返回值

    1
    2
    3
    4
    5
    6
    7
    8
    delegate 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
    8
    delegate 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
2
3
interface ITest<in T> {}
delegate T TestOut<out T>();
class Test<out T> {} // error: 变型修饰符无效。只有接口和委托类型的参数可以指定为变量。

但如果需要深度理解其作用,就需要结合里氏替换原则原则去理解:

  • 协变:让 父类泛型委托/接口 可以装载 子类泛型委托/接口

    先声明如下内容:

    1
    2
    3
    delegate T TestOut<T>();    // 这里没有使用协变
    class Father {}
    class Son : Father {}

    如果要让一个 父类泛型委托 直接装载 子类泛型委托变量,让父类泛型委托可以返回一个子类变量,这是做不到的:

    1
    2
    3
    TestOut<Son> sonFunc = () => new Son();
    TestOut<Father> fatherFunc = sonFunc; // error: 无法将类型“TestOut<Son>”隐式转换为“TestOut<Father>”
    Father f1 = fatherFunc();

    这是因为,委托类型变量只能存储另一个类型完全相同的委托变量,因此,即使同一泛型委托类型的泛型参数存在父子关系,也不能装载

    接口也是同理:

    1
    2
    3
    interface ITestOut<T>{}        // 这里没有使用协变
    class Father {}
    class Son : Father {}

    如果要让一个 父类泛型接口 直接装载 子类泛型接口变量,让父类泛型接口的方法可以返回一个子类变量,也是做不到的:

    1
    2
    3
    ITestOut<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
    14
    delegate 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
    7
    TestOut<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
    8
    delegate void TestIn<T>(T arg);        // 这里没有使用逆变

    class Father
    {
    public void DoSomething() {}
    }

    class Son : Father {}

    如果要让一个 子类泛型委托变量 直接装载 父类泛型委托变量,然后将子类对象传入到泛型委托参数内,这是做不到的:

    1
    2
    3
    TestIn<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
    17
    interface 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
    3
    ITestIn<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
    23
    delegate 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
    7
    TestIn<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());

    由于是 子类泛型容器 装 父类泛型容器,看起来不符合里氏替换原则,所以是逆常规的