C#:泛型的协变和逆变

协变

泛型委托间的协变

当泛型委托中类型参数作为委托方法的返回值时:

//该委托接收一个的方法是:无参数、返回T类型的
delegate T CreateFactory<T>();

class Program
{
    static void Main(string[] args)
    {
        CreateFactory<Dog> createDog = MakDog;
        //Cannot convert source type 'GenericDemo.CreateFactory<GenericDemo.Dog>' to target type 'GenericDemo.CreateFactory<GenericDemo.Animal>'
        CreateFactory<Animal> createAnimal = createDog;
        Console.ReadLine();
    }

    static Dog MakDog()
    {
        return new Dog();
    }
}
class Animal
{
    public int Legs { get; set; } = 4;
}
class Dog : Animal
{
}

上面代码会产生编译错误,是因为CreateFactory< Dog>和CreateFactory< Animal>是两种不兼容的数据类型;从下面代码可以看出:

static void Main(string[] args)
{
    CreateFactory<Dog> createDog = MakDog;
    //若is表达式结果为True,那么表明createDog兼容CreateFactory<Animal>类型
    Console.WriteLine(createDog is CreateFactory<Animal>);
    Console.ReadLine();
}
/*输出:False*/

在类型参数前加上out关键字修饰 就可以使赋值操作正常进行

delegate T CreateFactory<out T>();

out关键字修饰的类型参数,告诉编译器我们期望上面的赋值能够成功;而由out修饰了的泛型委托的构造类型之间便存在了一种协变关系

泛型接口间的协变

当泛型接口的类型参数作为函数成员的返回值时:

interface IResultAble<T>
{
    T GetModel();
}
class MyClass<T> : IResultAble<T>
{
    public T[] Items { get; set; }
    public T GetModel()
    {
        return Items.First();
    }
}
class Program
{
    static void Main(string[] args)
    {
        IResultAble<Dog> dog = new MyClass<Dog>();
        IResultAble<Animal> animal = dog;
        Console.ReadLine();
    }
}
class Animal
{
    public int Legs { get; set; } = 4;
}
class Dog : Animal
{
}

使用out关键字修饰泛型接口的类型参数,使赋值操作正常进行

interface IResultAble<out T>
{
    T GetModel();
}

使用了out关键字,函数成员执行后总能返回一个期望的基类型引用。而赋值操作能够成功进行,靠的是out关键字;使用out关键字的泛型接口的构造类型间存在协变关系

逆变

泛型委托间的逆变

当泛型委托的类型参数作为方法的形参时:

public delegate void HandlerSomething<T>(T param);
class Program
{
    static void Main(string[] args)
    {
        HandlerSomething<Animal> showAnimalLegs = new HandlerSomething<Animal>(ShowLegs);
        //Cannot convert source type 'GenericDemo.HandlerSomething<GenericDemo.Animal>' to target type 'GenericDemo.HandlerSomething<GenericDemo.Dog>'
        HandlerSomething<Dog> showDogLegs = showAnimalLegs;
        Console.ReadLine();
    }
    static void ShowLegs(Animal animal)
    {
        Console.WriteLine(animal.Legs);
    }
}
class Animal
{
    public int Legs { get; set; } = 4;
}
class Dog : Animal
{
}

使用in关键字修饰类型参数,可以使上面的赋值正常进行

public delegate void HandlerSomething<in T>(T param);

通过in关键字"貌似实现了基类型转向子类型的逆向转换",所以我们称in关键字所在的泛型委托的构造类型之间存在逆变关系

泛型接口间的逆变

当泛型接口的类型参数作为函数成员的形参时:

interface IHandlerAble<T>
{
    void PrinName(T value);
}
class MyClass<T> : IHandlerAble<T>
{
    public void PrinName(T value)
    {
        Console.WriteLine(value.GetType().FullName);
    }
}
class Program
{
    static void Main(string[] args)
    {
        IHandlerAble<Animal> animal = new MyClass<Animal>();
        //Cannot convert source type 'GenericDemo.IHandlerAble<GenericDemo.Animal>' to target type 'GenericDemo.IHandlerAble<GenericDemo.Dog>'
        IHandlerAble<Dog> dog = animal;
        Console.ReadLine();
    }
}
class Animal
{
    public int Legs { get; set; } = 4;
}
class Dog : Animal
{
}

使用in关键字修饰类型参数,可使上面的赋值操作正常进行

interface IHandlerAble<in T>
{
    void PrinName(T value);
}

从泛型接口的赋值操作来看,似乎这是一种从父类型到子类型的一种逆向转换:所以in关键字修饰的泛型接口的构造类型间存在逆变关系

协变、逆变也可隐式进行

当赋值号右边还未产生委托对象时,编译器可智能推断出委托类型间的协变、逆变关系:

delegate void MyAction<T>(T param);
delegate T MyFunc<T>();
class Program
{
    static void Main(string[] args)
    {
        MyFunc<Animal> animal = CreateDog;
        var result= animal.Invoke();
        Console.WriteLine(result.GetType().FullName);
        
        MyAction<Dog> getLegs = ShowLegs;
        getLegs.Invoke(new Dog());
        
        Console.ReadLine();
    }
    static Dog CreateDog()
    {
        return  new Dog();
    }
    
    static void ShowLegs(Animal animal)
    {
        Console.WriteLine(animal.Legs);
    }
}
/*
输出:
GenericDemo.Dog
4
*/

当赋值号右边产生了泛型委托对象时,就必须使用out、in关键字了

delegate void MyAction<in T>(T param);
delegate T MyFunc<out T>();
class Program
{
    static void Main(string[] args)
    {
        MyFunc<Animal> animal = CreateDog;
        var result = animal.Invoke();
        Console.WriteLine(result.GetType().FullName);

        MyFunc<Animal> animal1 = new MyFunc<Dog>(CreateDog);
        animal1.Invoke();

        MyAction<Dog> getLegs = ShowLegs;
        getLegs.Invoke(new Dog());

        MyAction<Dog> getLegs1 = new MyAction<Animal>(ShowLegs);
        getLegs1.Invoke(new Dog());

        Console.ReadLine();
    }
    static Dog CreateDog()
    {
        return new Dog();
    }
    static void ShowLegs(Animal animal)
    {
        Console.WriteLine(animal.Legs);
    }
}

学习了逆变、协变后,我们就能明白.NET API 提供的泛型委托、泛型接口为什么都带着in、out关键字了

为了执行带有一个形参且无返回值的方法,而声明的Action< T>委托:

为了执行带有零个形参且有返回值的方法,而声明的Func< TResult>委托:

为了执行带有一个形参且有返回值的方法,而声名的Func<T,TResult>委托:

以上便是对协变、逆变的知识点的总结,记录下来以便以后查阅。

原文地址:https://www.cnblogs.com/bigbosscyb/p/14045646.html