C#中的协变、逆变

一、概述

1、在 C# 中,协变和逆变能够实现数组类型委托类型泛型类型参数的隐式引用转换。

2、按字面,可以这样理解:协变,感觉协和隐式引用转换;逆变,逆反直觉隐式引用转换

二、泛型中的协变和逆变

1、定义

(1)协变,使你能够使用比原始指定的类型派生程度更大的类型。你可以将 IEnumerable<Derived> 的实例分配给 IEnumerable<Base> 类型的变量。

(2)逆变,使你能够使用比原始指定的类型更泛型的类型(派生程度更小的类型)。你可以将 Action<Base> 的实例分配给 Action<Derived> 类型的变量。

(3)不变性,表示只能使用最初指定的类型。 固定泛型类型参数既不是协变,也不是逆变。你无法将 List<Base> 的实例分配给 List<Derived> 类型的变量,反之亦然。

2、具有协变类型参数的泛型接口

(1)关键字 out

(2)在接口方法中,只能作为方法的返回类型

public interface IEnumerator<out T> : IEnumerator, IDisposable
{
    T Current { get; }
}

3、具有逆变类型参数的泛型接口

(1)关键字 in

(2)在接口方法中,只能作为方法的参数类型

public interface IComparer<in T>
{
    int Compare(T? x, T? y);
}

4、代码示例

(1)协变

internal partial class Program
{
    private static void ToTest5()
    {
        ITest1<string> implementStr = new ImplementStr();
        object result1 = implementStr.Foo();
        Console.WriteLine(result1);

        ITest1<object> implementObj = implementStr;
        object result2 = implementObj.Foo();
        Console.WriteLine(result2);
    }
}

public interface ITest1<out T>
{
    T Foo();
}

public partial class ImplementStr : ITest1<string>
{
    public string Foo()
    {
        return " foo";
    }
}

public partial class ImplementObj : ITest1<object>
{
    public object Foo()
    {
        return 1;
    }
}
View Code

运行结果:

(2)逆变

internal partial class Program
{
    private static void ToTest6()
    {
        ITest2<object> implementObj = new ImplementObj();
        implementObj.Bar("bar1");

        ITest2<string> implementStr = implementObj;
        implementStr.Bar("bar2");
    }
}

public interface ITest2<in T>
{
    void Bar(T parameter);
}

public partial class ImplementStr : ITest2<string>
{
    public void Bar(string parameter)
    {
        Console.WriteLine($" string parameter:{parameter}");
    }
}

public partial class ImplementObj : ITest2<object>
{
    public void Bar(object parameter)
    {
        Console.WriteLine($" object parameter:{parameter}");
    }
}
View Code

运行结果:

 (3)示例说明

A、隐式引用转换

(A)协变中,隐式引用转换发生在 object result2 = implementObj.Foo(); 这个语句中。

  • 它实际执行的方法是:public string Foo(),所以输出的是字符串 foo,而不是数字 1;
  • 编译器对这个语句理解是,要求它返回 object 类型,通过鼠标停留,我们可以看到:

 (B)逆变中,隐式引用转换发生在 implementStr.Bar("bar2"); 这个语句中。

  • 参数是字符串 bar2,
  • 调用的方法是 public void Bar(object parameter),所以输入的是  object parameter:bar2

B、协变、逆变名称来源

  • 感觉协和的语句是:ITest1<object> implementObj = implementStr;
  • 逆反直觉的语句是:ITest2<string> implementStr = implementObj;

前者将一个 ITest1<string> 实例赋值给了一个 ITest1<object> 变量,直觉上是子类对象赋值给父类变量,感觉协和的、融洽的;

后者将一个 ITest2<object> 实例赋值给了一个 ITest2<string> 变量,直觉上是父类对象赋值给子类变量,感觉逆反的、别扭的。

实际上,并没有发生“子类对象赋值给父类变量”和“父类对象赋值给子类变量”,发生的是“ ITest1 实例赋值给 ITest1 变量” 以及 “ITest2 实例赋值给 ITest2 变量”。

因为代码携带了实际的类型参数 object 或 string,才让开发者了产生了协和的或者逆反的感觉。

 5、本质

本质都是遵循“里氏替换原则”,示例代码中,无论协变,还是逆变,都是 String 类型向 Object 类型的转换。

关键字 in 和 out 的使用,使编译器能够知道应该如何对类型参数进行“里氏替换原则”的检查,即类型安全检查。因此编译器拒绝这样的代码:

 6、关键点

抓住“谁是变量,谁是实例”这个关键。“实例”的类型决定实际调用哪个方法的实现,“变量”决定返回什么类型(协变),或输入什么类型的参数(逆变)。

 

posted @ 2023-12-08 15:44  误会馋  阅读(289)  评论(0编辑  收藏  举报