C#进阶-协变与逆变

简介: 我们知道子类转换到父类,在C#中是能够隐式转换的。这种子类到父类的转换就是协变。而另外一种类似于父类转向子类的变换,可以简单的理解为逆变。逆变协变可以用于泛型委托和泛型接口,本篇文章我们将讲解C#里逆变和协变的使用。逆变和协变的语法第一次接触难免感到陌生,最好的学习方式就是在项目中多去使用,相信会有很多感悟。

一、协变与逆变的概念

1、协变(共变)

协变是一种类型关系,通过它,我们可以在泛型委托或泛型接口中进行父类到子类的隐式转换。具体而言,如果类型 A 是类型 B 的子类型(A ≤ B),则支持协变的情况下,泛型类型参数 T<A> 可以被隐式转换为 T<B>。这使得我们可以在一些上下文中更通用地使用较为抽象的类型。


示例公式:若 A ≤ B,则 T<A> 可协变为 T<B>。

// 协变示例publicinterfaceICoVariant<outT>{
TGetItem();
}
publicclassBaseClass { }
publicclassDerivedClass : BaseClass { }
// 使用协变的接口ICoVariant<BaseClass>baseInstance=newCoVariant<DerivedClass>();
BaseClassitem=baseInstance.GetItem(); // 隐式转换,获取 DerivedClass 实例

2、逆变(反变)

逆变则允许在泛型委托或泛型接口中进行子类到父类的隐式转换。如果类型 A 是类型 B 的子类型(A ≤ B),则支持逆变的情况下,泛型类型参数 T<B> 可以被隐式转换为 T<A>。逆变为我们提供了更多的灵活性,允许我们传递更具体类型的对象给接受更抽象类型的泛型接口或委托。


示例公式:若 A ≤ B,则 T<B> 可逆变为 T<A>。

// 逆变示例publicinterfaceIContraVariant<inT>{
voidPrintItem(Titem);
}
publicclassBaseClass { }
publicclassDerivedClass : BaseClass { }
// 使用逆变的接口IContraVariant<DerivedClass>derivedInstance=newContraVariant<BaseClass>();
derivedInstance.PrintItem(newDerivedClass()); // 隐式转换,接受 BaseClass 实例

3、继承关系与类型转换

继承关系与类型转换是协变和逆变的基础。在协变的情况下,随着类型的派生程度增加,泛型类型参数的继承关系也得到了保持。而在逆变的情况下,派生程度更大的类型可以被隐式地转换为派生程度更小的类型。这些关系可以用公式表示如下:


当 A ≤ B 时,若 f(x) 是逆变的,则 f(B) ≤ f(A) 成立。

当 A ≤ B 时,若 f(x) 是协变的,则 f(A) ≤ f(B) 成立。

这些公式强调了在协变和逆变中,类型转换的方向是根据类型的继承关系而变化的。


二、逆变与协变的运用

1、泛型委托

a、协变

协变允许我们在泛型委托中进行父类到子类的隐式转换。请参考下面的示例:

publicdelegateTDelegateFuncA<outT>(); //支持协变DelegateFuncA<object>funcObject=null;
DelegateFuncA<string>funcString=null;
funcObject=funcString; //协变

在这个例子中,DelegateFuncA 声明了一个支持协变的泛型委托。通过将 funcString 赋值给 funcObject,我们实现了从 string 到 object 的协变转换。这种灵活性可以在一些场景中提高代码的可读性和可维护性。


b、逆变

逆变允许我们在泛型委托中进行子类到父类的隐式转换。请参考下面的示例:

publicdelegatevoidDelegateFuncB<inT>(Tparam); //支持逆变DelegateFuncB<object>actionObject=null;
DelegateFuncB<string>actionString=null;
actionString=actionObject; //逆变

在这个示例中,DelegateFuncB 是一个支持逆变的泛型委托。通过将 actionObject 赋值给 actionString,我们实现了从 object 到 string 的逆变转换。逆变为我们提供了更多的灵活性,使得我们能够更容易地传递具有更具体类型的方法给接受更抽象类型的委托。


2、泛型接口

a、协变

协变在泛型接口中的应用同样是非常有意义的。请参考下面的示例:

publicdelegateTInterfaceFuncA<outT>(); //支持协变InterfaceFuncA<object>interfaceFuncObject=null;
InterfaceFuncA<string>interfaceFuncString=null;
interfaceFuncObject=interfaceFuncString; //协变

在这个例子中,我们使用支持协变的泛型接口 InterfaceFuncA。通过将 interfaceFuncString 赋值给 interfaceFuncObject,我们实现了从 string 到 object 的协变转换。


b、逆变

逆变同样可以在泛型接口中发挥作用。请参考下面的示例:

publicdelegatevoidInterfaceFuncB<inT>(Tparam); //支持逆变InterfaceFuncB<object>interfaceFuncObjectB=null;
InterfaceFuncB<string>interfaceFuncStringB=null;
interfaceFuncStringB=interfaceFuncObjectB; //逆变

在这个例子中,我们使用支持逆变的泛型接口 InterfaceFuncB。通过将 interfaceFuncObjectB 赋值给 interfaceFuncStringB,我们实现了从 object 到 string 的逆变转换。


3、数组逆变

逆变同样可以在数组中发挥作用,使得派生程度更大的类型的数组能够隐式转换为派生程度更小的类型的数组。请参考下面的示例:

// 数组的逆变使派生程度更大的类型的数组能够隐式转换为派生程度更小的类型的数组。object[] array=newstring[10];
array[0] =10;

在这个示例中,我们创建了一个 object 类型的数组,但实际上它指向的是一个 string 类型的数组。这是数组逆变的一个实际应用,使得我们能够更灵活地处理不同派生程度的数组类型。


三、逆变与协变的总结

C#中的逆变和协变是一组强大的泛型特性,它们为我们在处理泛型委托和泛型接口时提供了更灵活、更安全的类型转换机制。在面向对象的编程中,我们经常需要在不同层次的类之间进行转换,而逆变和协变正是为了在这些转换过程中提供便利。


协变,即子类到父类的转换,使得我们能够以一种更通用的方式使用类型。这对于泛型委托和泛型接口的设计非常有用,允许我们在一些场景中将泛型参数类型更灵活地指定为其基类型。这种灵活性为代码的可扩展性和可维护性带来了巨大的优势。


与协变相反,逆变提供了一种父类到子类的转换方式。这在某些场景下同样非常有用,允许我们将具有更具体类型的方法传递给接受更抽象类型的泛型委托或泛型接口。逆变为代码的复用性和可读性提供了更高的提升。


尽管逆变和协变的语法可能在初次接触时显得陌生,但通过实际项目的使用,你将逐渐体会到它们的价值。在日常编码中,多次应用这些特性,你会发现它们在提高代码的表达能力和逻辑清晰性方面发挥了关键作用。因此,鼓励大家在实践中多次运用逆变和协变,相信你会在这个过程中获得丰富的经验和感悟。逐渐地,你将更加熟练地运用这些强大的语言特性,提升你的C#编程技能。

目录
相关文章
|
C# 开发者
C#——协变逆变
C#——协变逆变
95 0
|
编译器 C#
C#中的协变和逆变
C#中的协变和逆变
116 0
|
算法
一起谈.NET技术,C#4.0新特性-&quot;协变&quot;与&quot;逆变&quot;以及背后的编程思想
  在《上篇》中我们揭示了“缺省参数”的本质,现在我们接着来谈谈C#4.0中另一个重要的新特性:协变(Covariance)与逆变(Contravariance)。对于协变与逆变,大家肯定不会感到陌生,但是我相信有很多人不能很清晰地说出他们之间的区别。
904 0
|
算法
C#4.0新特性-&quot;协变&quot;“.NET研究”与&quot;逆变&quot;以及背后的编程思想
  在《上篇》中我们揭示了“缺省参数”的本质,现在我们接着来谈谈C#4.0中另一个重要的新特性:协变(Covariance)与逆变(Contravariance)。对于协变与逆变,大家肯定不会感到陌生,但是我相信有很多人不能很清晰地说出他们之间的区别。
1094 0
|
C# 编译器 Java
C# 逆变与协变
原文:C# 逆变与协变 该文章中使用了较多的 委托delegate和Lambda表达式,如果你并不熟悉这些,请查看我的文章《委托与匿名委托》、《匿名委托与Lambda表达式》以便帮你建立完整的知识体系。
1347 0