C# 协变 和 逆变 Covariance and Contravariance

协变和逆变允许我们在处理有继承结构时更灵活。

在我们了解协变和逆变之前,请考虑以下类层次结构:

public class Small
{ 

}
public class Big: Small
{

}
public class Bigger : Big
{ 
    
}

根据上面的示例类,Small 是 Big 的基类,Big 是 Bigger 的基类。 这里要记住的一点是,派生类总是比基类有更多的东西,所以基类比派生类小。

现在,考虑以下初始化:

static void Main(string[] args)
{
    Small s1 = new Small();
    Small s2 = new Big();
    Small s3 = new Bigger();
    Big b1 = new Bigger();
    Big b2 = new Small();// 无法隐式转换,不合法,逆变不了
    Big b3 = (Big)(Small)(new Big()); //要显示转换, 如果我们只是创建一个Small的类然后想强制转成 Big哪是不行的。 会报错Unable to cast object of type 
}

正如我们在上面看到的,基类可以接受派生类,但派生类不能接受基类。 也就是说,一个实例变量要求小的是可以接受大,但要求大就不能接受小的了。 这个是很和协的变化。 方法参数也是这样的。参数类型为基类的,是可以接收子类的。如下示例

class Program
{
    static void Main(string[] args)
    {
        Print("abc");
    }

    public static void Print(object o)
    {
        Console.WriteLine(o.ToString());
    }
}

现在,让我们了解协变和逆变。

C#中的协变和逆变

协变使我们可以在需要基类型的地方传递派生类型(子类)。 协变就是很和协的变化。 其他派生类被认为是向基类型添加额外功能的同一种类。 因此,协变允许我们在需要基类的地方使用派生类(规则:如果需要小,可以接受大)。

协变可以应用于委托、泛型、数组、接口等。 协变只支持引用类型,不支持值类型

本质上其实没有逆变,只是匿名函数转成Delegate,接口时,或者Delegate转另一个Delegate,Interface转另一个Interface时,表面看起来是 输入参数 由基类变成子类了。

数组协变

数组只支持协变,不支持逆变

object[] myArray = new string[5];
IComparable[] myOtherArray = new string[5];

值类型不支持协变

//object[] myArray = new int[5];
//IComparable[] myOtherArray = new int[5];

IEnumerable<> 支持协变

List 不支持协变. (因为我们可能会往list里面加一些其它的派生类,导致出问题) 有一些场合可以换成 IEnumerable 或者 IReadOnlyList

	var bigList = new List<Big>();
	
	//List<Small> smallList = BigList; //协变不了
	
	IEnumerable<Big> bigList2 = bigList;
	IEnumerable<Small> smallList2 = bigList2;

委托中的变体 Variance in Delegates

下面的例子展示了 委托的参数是支持逆变的,返回值是支持协变的。

把方法赋给委托的时候 参数看起来是逆变,实则协变。(调用的过程相当于把子类传给基类了)。 返回参数,看起来是协变,实际上也是协变。

public class First { }  
public class Second : First { }  
public delegate First SampleDelegate(Second a);  
public delegate R SampleGenericDelegate<A, R>(A a);

创建 SampleDelegate 或 SampleGenericDelegate<A, R> 类型的委托时,可以将以下任一方法分配给这些委托。

// 符合签名  
public static First ASecondRFirst(Second second)  
{ return new First(); }  
  
// 返回值是子类
public static Second ASecondRSecond(Second second)  
{ return new Second(); }  
  
// 参数是基类
public static First AFirstRFirst(First first)  
{ return new First(); }  
  
// 返回类型是子类
// 参数是基类 
public static Second AFirstRSecond(First first)  
{ return new Second(); }  

以下代码示例说明了方法签名与委托类型之间的隐式转换。

// 把一个完全符合签名的赋值给委托
// 没有类型转换  
SampleDelegate dNonGeneric = ASecondRFirst;  

// 方法的 返回值是子类 协变
// 方法的 参数是基类 而delgate是子类 看起来是逆变,但是实际调用的过程是相当于把一个子类转给基类方法,是完全安全的。
// 使用隐式转换
SampleDelegate dNonGenericConversion = AFirstRSecond;  
  
// 把完全符合签名的赋值给泛型委托
// 没有类型转换
SampleGenericDelegate<Second, First> dGeneric = ASecondRFirst;  

// 返回值是子类 协变
// 参数是基类 逆变
// 使用隐式转换 
SampleGenericDelegate<Second, First> dGenericConversion = AFirstRSecond;  

泛型类型参数中的变体 Variance in Generic Type Parameters

在 .NET Framework 4 或更高版本中,可以启用委托之间的隐式转换,以便在具有泛型类型参数所指定的不同类型按变体的要求继承自对方时,可以将这些类型的泛型委托分配给对方。

若要启用隐式转换,必须使用 in 或 out 关键字将委托中的泛型参数显式声明为协变或逆变。

以下代码示例演示了如何创建一个具有协变泛型类型参数的委托。

// 通过给T添加out关键字表示 这个类型是允许协变的  
public delegate T SampleGenericDelegate<out T>();
public class Program
{
    public static void Test()
    {
        SampleGenericDelegate<String> dString = () => " ";

        // 可以把 dString 赋值给 dObject 因为 T被定义成协变了
        SampleGenericDelegate<Object> dObject = dString;

        // 下面的这个不合法
        // SampleGenericDelegate<String> dString2 = dObject;
    }
}

如果仅使用变体支持来匹配方法签名和委托类型,且不使用 in 和 out 关键字,则可能会发现有时可以使用相同的 lambda 表达式或方法实例化委托,但不能将一个委托分配给另一个委托。 在以下代码示例中,SampleGenericDelegate 不能显式转换为 SampleGenericDelegate,尽管 String 继承 Object。 可以使用 out 关键字标记 泛型参数 T 解决此问题。

public delegate T SampleGenericDelegate<T>();  
  
public static void Test()  
{  
    SampleGenericDelegate<String> dString = () => " ";  
  
    // 可以把 string 委托 赋值给 Object 委托
    SampleGenericDelegate<Object> dObject = () => " ";  
  
    // 下面有编译错误 
    // 因为泛型T 没有标记为协变
    // SampleGenericDelegate <Object> dObject = dString;  
  
}  

方法参数允许逆变。

// 通过给T添加in关键字表示 这个参数的类型是允许协变的 ,给方法参数加out是不合法的
public delegate void SampleGenericDelegate<in T>(T a);
public class Program
{
    public static void Test()
    {
        SampleGenericDelegate<Object> dObject = (o) => { };
        SampleGenericDelegate<String> dString = dObject;
    }
}

如下系统自带的委托与支持协变逆变

  • System 命名空间的 Action 委托,例如 Action 和 Action<T1,T2>
  • System 命名空间的 Func 委托,例如 Func 和 Func<T,TResult>
  • Predicate 委托
  • Comparison 委托
  • Converter<TInput,TOutput> 委托

一个完整的例子

using System;

namespace ConsoleApp1
{
    public class Small
    {
    }
    public class Big : Small
    {
    }
    delegate Small covarDel(Big mc);

    class Program
    {
        static Big Method1(Big bg)
        {
            Console.WriteLine("Method1");
            return new Big();
        }
        static Small Method2(Big bg)
        {
            Console.WriteLine("Method2");
            return new Small();
        }

        static Small Method3(Small sml)
        {
            Console.WriteLine("Method3");

            return new Small();
        }

        static Big Method4(Small sml)
        {
            Console.WriteLine("Method3");

            return new Big();
        }

        static void Main(string[] args)
        {
            covarDel del = Method1;
            del += Method2;
            del += Method3;
            del += Method4;

            Small sm = del(new Big()); // 方法的参数对于委托来说是逆变,但是实际调用的时候它基实是协变。
        }
    }
}

输出

Method1
Method2
Method3
Method4

泛型接口的协变逆变

跟委托类似。输入参数在实际使用的过程中,输入的是string. 所以表面看起来是逆变,实则协变。

public interface IMyInterface<in T, out U>
{
    U MyFunc();
    void MyFoo(T obj);
}
public class MyClass<T, U> : IMyInterface<T, U>
{
    public U MyFunc()
    {
        return default;
    }

    public void MyFoo(T obj)
    {
        Console.WriteLine(obj.ToString());
    }
}

class Program
{
    static void Main(string[] args)
    {
        //使用时:
        IMyInterface<string, object> myObj = new MyClass<object, string>();
        IMyInterface<object, string> myObj1 = new MyClass<object, string>();
        myObj = myObj1;
        myObj.MyFoo("abc");
    }
}
上一篇:C# 事件event
下一篇:C# 扩展方法
最近更新的
...