1. 程式人生 > 實用技巧 >C#:由泛型產生的協變和逆變

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和CreateFactory是兩種不相容的資料型別;從下面程式碼可以看出:

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委託:

為了執行帶有零個形參且有返回值的方法,而宣告的Func委託:

為了執行帶有一個形參且有返回值的方法,而聲名的Func<T,TResult>委託:

以上便是對,因派生關係而產生的協變、逆變關係的知識點的總結,記錄下來以便以後查閱。