C#4.0新特性-"协变"“.NET研究”与"逆变"以及背后的编程思想

简介:   在《上篇》中我们揭示了“缺省参数”的本质,现在我们接着来谈谈C#4.0中另一个重要的新特性:协变(Covariance)与逆变(Contravariance)。对于协变与逆变,大家肯定不会感到陌生,但是我相信有很多人不能很清晰地说出他们之间的区别。

  在《上篇》中我们揭示了“缺省参数”的本质,现在我们接着来谈谈C#4.0中另一个重要的新特性:协变(Covariance)与逆变(Contravariance)。对于协变与逆变,大家肯定不会感到陌生,但是我相信有很多人不能很清晰地说出他们之间的区别。我希望通过这篇文章能够让读者更加深刻的认识协变与逆变。但是也不排除另一种可能,那就是读者这篇文章你对这两个概念更加模糊。文章一些内容仅代表个人观点,如有不妥,还望指正。

上海闵行企业网站制作quote>

目录
一、两个概念:强类型与弱类型
二、委托中的协变与逆变的使用
三、接口中的协变与逆变的使用
四、从Func<T,TResult>看协变与逆变的本质
五、逆变实现了“算法”的重用

  一、两个概念:强类型与弱类型

  为了后面叙述方便,我现在这里自定义两个概念:强类型和弱类型。在本篇文章中,强类型和弱类型指的是两个具有直接或者间接继承关系的两个类。如果一个类是另一个类的直接或者间接基类,那么它为弱类型,直接或者间接子类为强类型。后续的介绍中会用到的两个类Foo和Bar先定义在这里。Bar继承自Foo。Foo是弱类型,而Bar则是强类型。

 
 
1 : public class Foo
2 : {
3 : // Others Members...
4 : }
5 : public class Bar:Foo
6 : {
7 : // Others Members...
8 : }

  有了强类型和弱类型的概念,我们就可以这样的定义协变和逆变:如果类型TBar是基于强类型Bar的类型(比如类型参数为Bar的泛型类型,或者是参数/返回值类型为Bar的委托),而类型TFoo是基于弱类型Foo的类型,协变就是将TBar类型的实例赋值给TFoo类型的变量,而逆变则是将TFoo类型的实例赋值给TBar类型的变量。

  二、委托中的协变与逆变的使用

  协变和逆变主要体现在两个地方:接口和委托,先来看看在委托中如何使用协变和逆变。现在我们定义了如下一个表示无参函数的泛型委托Function<T>,类型参数为函数返回值的类型。泛型参数之前添加了一个out关键字表示T是一个协变变体。那么在使用过程中,基于强类型的委托Fucntion<Bar>实例就可以赋值给基于弱类型的委托Fucntion<Foo>变量。

 
 
1 : public delegate T Function < out T > ();
2 : class Program
3 : {
4 : static void Main()
5 : {
6 : Function < Bar > funcBar = new Function < Bar > (GetInstance);
7 : Function < Foo > funcFoo = funcBar;
8 : Foo foo = funcFoo();
9 : }
10 : static Bar GetInstance()
11 : {
12 : return new Bar();
13 : }
14 : }

  接下来介绍逆变委托的用法。下面定义了一个名称为Operate的泛型委托,接受一个具有泛型参数类型的参数。在定义泛型参数前添加了in关键字,表示T是一个基于逆变的变体。由于使用了逆变,我们就可以将基于弱类型的委托Operate<Foo>实例就可以赋值给基于强类型的委托Operate<Bar>变量。

 
 
1 : public delegate void Operate < in T > (T instance);
2 : class Program
3 : {
4 : static void Main()
5 : {
6 : Operate < Foo > opFoo = new Operate < Foo > (DoSth);
7 : Operate < Bar > opBar = opFoo;
8 : opBar( new Bar());
9 : }
10 : static void DoSth(Foo foo)
11 : {
12 : // Others...
13 : }
14 : }

  三、接口中的协变与逆变的使用

  接下来我们同样通过一个简单的例子来说明在接口中如何使用协变和逆变。下面定义了一个继承自 IEnumerable<T>接口的IGroup<out T>集合类型,和上面一样,泛型参数T之前的out关键字表明这是一个协变。既然是协变,我们就可以将一个基于强类型的委托IGroup<Bar>实例就可以赋值给基于弱类型的委托IGroup<Foo>变量。

 
 
1 : public interface IGroup < out T > : IEnumerable < T >
2 : { }
3 : public class Group < T > : List < T > , IGroup < T >
4 : { }
5 : public delegate void Operate < in T > (T instance);
6 : class Program
7 : {
8 : static void Main()
9 : {
10 : IGroup < Bar > groupOfBar = new Group < Bar > ();
11 : IGroup < Foo > groupOfFoo = groupOfBar;
12 : // Others...
13 : }
14 : }

  下面是一个逆变接口的例子。首先定义了一个IPaintable的接口,里面定义了一个可读写的Color属性,便是实现该接口的类型的对象具有自己的颜色,并可以改变颜色。类型Car实现了该接口。接口IBrush<in T>定义了一把刷子,泛型类型需要实现IPaintable接口,in关键字表明这是一个逆变。方法Paint用于将指定的对象粉刷成相应的颜色,表示被粉刷的对象的类型为泛型参数类型。Brush<T>实现了该接口。由于IBrush<in T>定义成逆变,我们就可以将基于弱类型的委托IBrush<IPaintable>实例就可以赋值给基于强类型的委托IBrush<Car>变量。

 
 
1 : public interface IPaintable
2 : {
3 : Color Color { get ; set ; }
4 : }
5 : public class Car : IPaintable
6 : {
7 : public Color Color { get ; set 上海网站建设le="color: #000000;">; }
8 : }
9 :
10 : public interface IBrush < in T > where T : IPaintable
11 : {
12 : void Paint(T objectToPaint, Color color);
13 : }
14 : public class Brush < T > : IBrush < T > where T : IPaintable
15 : {
16 : public void Paint(T objectToPaint, Color color)
17 : {
18 : objectToPaint.Color = color;
19 : }
20 : }
21 :
22 : class Program
23 : {
24 : static void Main()
25 : {
26 : IBrush < IPaintable > brush = new Brush < IPaintable > ();
27 : IBrush < Car >上海企业网站制作pan style="color: #000000;"> carBrush = brush;
28 : Car car = new Car();
29 : carBrush.Paint(car, Color.Red);
30 : Console.WriteLine(car.Color.Name);
31 : }
32 : }

  四、从Func<T,TResult>看协变与逆变的本质

  接下来我们来谈谈协变和逆变的本质区别是什么。在这里我们以我们非常熟悉的一个委托Func<T, TResult>作为例子,下面给出了该委托的定义。我们可以看到Func<T, TResult>定义的两个泛型参数分别属于逆变和协变。具体来说输入参数类型为逆变,返回值类型为协变。

 
 
1 : public delegate TResult Func < in T, out TResult > (T arg);

  再重申以下这句话“输入参数类型为逆变,返回值类型为协变”。然后,你再想想为什么逆变用in关键字,而协变用out关键字。这两个不是偶然,实际上我们可以将协变/逆变与输出/输入匹配起来。

  我们再从另一个角度来理解协变与逆变。我们知道接口代表一种契约,当一个类型实现一个接口的时候就相当于签署了这份契约,所以必须是实现接口中所有的成员。实际上类型继承也属于一种契约关系,基类定义契约,子类“签署”该契约。对于类型系统来说,接口实现和类型继承本质上是一致的。契约是弱类型,签署这份契约的是强类型。

  将契约的观点应用在委托上面,委托实际上定义了一个方法的签名(参数列表和返回值),那么参数和返回值的类型就是契约,现在的关键是谁去履行这份契约。所有参数是外界传入的,所以基于参数的契约履行者来源于外部,也就是被赋值变量的类型,所以被赋值变量类型是强类型。而对于代理本身来说,参数是一种输入,也就是一种采用in关键字表示的逆变。上海徐汇企业网站制作/span>

  而对于委托的返回值,这是给外部服务的,是委托自身对外界的一种承诺,所以它自己是契约的履行着,因此它自己应该是强类型。相应地,对于代理本身来说,返回值是一种输出,也就是一种采用out关键字定义的协变。

  也正式因为这个原因,对于一个委托,你不能将参数类型定义成成协变,也不能将返回类型定义成逆变。下面两中变体定义方式都是不能通过编译的。

   1: delegate TResult Fucntion<out T, TResult>(T arg);
   2: delegate TResult Fucntion<T, in TResult>(T arg);

  说到这里,我想有人要问一个问题,既然输入表示逆变,输出表示协变,委托的输出参数应该定义成协变了?非也,实际上输出参数在这里既输出输出,也输出输入(毕竟调用的时候需要指定一个对应类型的对象)。也正是为此,输出参数的类型及不能定义成协变,也不能定义成逆变。所以下面两种变体的定义也是不能通过编译的。

 
 
1 : delegate void Action < in T > ( out T arg);
2 : delegate void Action < out T > ( out T arg);

  虽然这里指介绍了关于委托的协变与逆变,上面提到的契约和输入/输出的关系也同样适用于基于接口的协变与逆变。你自己可以采用这样的方式去分析上面一部分我们定义的IGroup<Foo>和IBrush<in T>。

  五、逆变实现了“算法”的重用

  实际上关系协变和逆变体现出来的编程思想,还有一种我比较推崇的说法,那就是:协变是继承的体现,而逆变体现的则是多态(可以参考idior的文章《Covariance and Contravariance》)。实际上这与上面分析的契约关系本质上是一致的。

  关于逆变,在这里请容我再啰嗦一句:逆变背后蕴藏的编程思想体现出了对算法的重用——我们为基类定义了一套操作,可以自动应用于所有子类的对象。

目录
相关文章
|
3月前
|
人工智能 开发框架 .NET
.NET技术的强大功能:.NET技术的基础特性、在现代开发中的应用、以及它如何助力未来的软件开发。
.NET技术是软件开发领域的核心支柱,以其强大功能、灵活性及安全性广受认可。本文分三部分解析:基础特性如多语言支持、统一运行时环境;现代应用如企业级与Web开发、移动应用、云服务及游戏开发;以及未来趋势如性能优化、容器化、AI集成等,展望.NET在不断变化的技术环境中持续发展与创新。
117 4
|
9天前
|
编译器 C# 开发者
C# 9.0 新特性解析
C# 9.0 是微软在2020年11月随.NET 5.0发布的重大更新,带来了一系列新特性和改进,如记录类型、初始化器增强、顶级语句、模式匹配增强、目标类型的新表达式、属性模式和空值处理操作符等,旨在提升开发效率和代码可读性。本文将详细介绍这些新特性,并提供代码示例和常见问题解答。
25 7
C# 9.0 新特性解析
|
8天前
|
C# 开发者
C# 10.0 新特性解析
C# 10.0 在性能、可读性和开发效率方面进行了多项增强。本文介绍了文件范围的命名空间、记录结构体、只读结构体、局部函数的递归优化、改进的模式匹配和 lambda 表达式等新特性,并通过代码示例帮助理解这些特性。
20 2
|
2月前
|
编译器 C# Android开发
震惊!Uno Platform 与 C# 最新特性的完美融合,你不可不知的跨平台开发秘籍!
Uno Platform 是一个强大的跨平台应用开发框架,支持 Windows、macOS、iOS、Android 和 WebAssembly,采用 C# 和 XAML 进行编程。C# 作为其核心语言,持续推出新特性,如可空引用类型、异步流、记录类型和顶级语句等,极大地提升了开发效率。要在 Uno Platform 中使用最新 C# 特性,需确保开发环境支持相应版本,并正确配置编译器选项。通过示例展示了如何在 Uno Platform 中应用可空引用类型、异步流、记录类型及顶级语句等功能,帮助开发者更好地构建高效、优质的跨平台应用。
202 59
|
27天前
|
JSON C# 开发者
C#语言新特性深度剖析:提升你的.NET开发效率
【10月更文挑战第15天】C#语言凭借其强大的功能和易用性深受开发者喜爱。随着.NET平台的演进,C#不断引入新特性,如C# 7.0的模式匹配和C# 8.0的异步流,显著提升了开发效率和代码可维护性。本文将深入探讨这些新特性,助力开发者在.NET开发中更高效地利用它们。
33 1
|
1月前
|
存储 编译器
.Net特性Attribute的高级使用
【10月更文挑战第14天】在.NET中,特性(Attribute)是一种强大的机制,用于在代码中添加元数据。本文介绍了特性的高级用法,包括自定义特性、通过反射读取特性、条件编译与特性结合、多个特性应用以及特性继承。通过示例展示了如何创建自定义特性类、应用自定义特性,并通过反射获取特性信息。此外,还介绍了如何利用条件编译符号实现不同版本的代码控制,以及如何在一个代码元素上应用多个特性。最后,探讨了如何通过`AttributeUsage`控制特性的继承行为。
|
3月前
|
开发框架 .NET 编译器
总结一下 C# 如何自定义特性 Attribute 并进行应用
总结一下 C# 如何自定义特性 Attribute 并进行应用
|
3月前
|
C# 索引
C#各大版本特性
C#各大版本特性
76 0
|
13天前
|
C# 开发者
C# 一分钟浅谈:Code Contracts 与契约编程
【10月更文挑战第26天】本文介绍了 C# 中的 Code Contracts,这是一个强大的工具,用于通过契约编程增强代码的健壮性和可维护性。文章从基本概念入手,详细讲解了前置条件、后置条件和对象不变量的使用方法,并通过具体代码示例进行了说明。同时,文章还探讨了常见的问题和易错点,如忘记启用静态检查、过度依赖契约和性能影响,并提供了相应的解决建议。希望读者能通过本文更好地理解和应用 Code Contracts。
29 3
|
1月前
|
安全 C# 数据安全/隐私保护
实现C#编程文件夹加锁保护
【10月更文挑战第16天】本文介绍了两种用 C# 实现文件夹保护的方法:一是通过设置文件系统权限,阻止普通用户访问;二是使用加密技术,对文件夹中的文件进行加密,防止未授权访问。提供了示例代码和使用方法,适用于不同安全需求的场景。
101 0