欢迎光临散文网 会员登陆 & 注册

第 73 讲:C# 2 之匿名函数(一):委托实例化的简化

2021-12-18 22:37 作者:SunnieShine  | 我要投稿

还记得前面学习的委托吗?我们仍使用排序来举例。

Part 1 引例

假设,我们需要使用委托传参的方式来表示数组排序 Array.Sort 方法,而我们也学习了如何调用这个方法:

是的,这样的代码就可以完成排序。可是,这样代码仍然很长。因为我们要自己写一个方法,而且还得使用 new Comparison 来实例化委托对象来作为传参过程。因此,C# 2 开始,为了简化语法,发明了一种新的语法格式:匿名函数(Anonymous Function)。

Part 2 匿名函数的语法

我们先来看简化版代码应该怎么写:

当然,也可以换个行:

是的,就两个语句即可。其中第二个语句有些奇怪。第一个参数仍然没有改变,但第二个参数变长了。其实也是猜得到的:因为实际的方法不见了,改成了这样的语法被替换到了参数的位置上。

仔细看这个参数写法 delegate (int a, int b) { return a.CompareTo(b); },它就被称为匿名函数。

匿名函数的语法如下:

匿名函数的作用就是为了简化委托类型的实例化。它的写法是直接将“方法的关键部分”参数化,直接写进参数里。这种写法大大改变了你书写代码(特别是用委托的时候)的风格和习惯。因此,你必须要习惯这种书写格式。

它的语法格式是这样的。参数表列直接对应了我们这里的 Comparison<T> 委托类型的参数表列。因为我们这里对数组排序,因此比较的是两个 int 数据,因此我们直接写 (int a, int b) 即可;与此同时,我们直接把执行代码和前面的 a.CompareTo(b) 抄过来就可以了。因此,这个匿名函数长这样。

之所以叫匿名函数,就是因为它没有方法名称,而不论什么时候,委托的实例化代换为匿名函数语法的时候都是直接退化为了 delegate 关键字了,所以这样的方法是没有名字的。

当然,也有其它的方法也可以使用到这个语法。比如这样的代码:

这个 Array.TrueForAll 方法表示验证整个数组序列是不是全部的元素都满足同样的条件。而这个条件,就从第二个参数进行指定。所以,第二个参数是一个条件的委托实例。我们这里只需要验证元素的数据,所以这个匿名函数需要一个参数,并返回 bool 结果。

是的,你说对了。Array.TrueForAll 方法的第二个参数的类型就是 Predicate<T>

Part 3 委托实例化转匿名函数的套路

其实前面基本上可以搞懂如何简化这样的代码,用上匿名函数。不过,我还是简单说一下,如何从原生代码转换为匿名函数语法。

3-1 第一步:看委托类型的签名,确定参数和返回值类型

如何转换语法?先看委托类型。匿名函数用就用在委托类型实例化之上,所以一定要把眼睛关注到委托类型上。先看委托类型的签名,这样就可以确定返回值和参数的类型。

比如说,我的委托类型定义为这样:

那么它的参数类型是 ref intref int,而返回值类型则是 void

3-2 第二步:脑内推测方法的完整语法

第二步是在脑内构造一个方法,契合这个签名的。按照委托类型的尿性,我们一般不在意方法是不是 static 修饰的,所以我们不必去管方法自身的修饰符,只大概想一想它的参数和返回值放上去后是啥样的。

这个时候,得看方法的实际使用过程来确定具体的代码执行逻辑。因为 Swapper 委托类型的名字是“交换”的类似意思,所以我们实现的时候就得考虑是不是用于交换变量了。显然,参数类型都是带有 ref 修饰符的,它的意图已经很明显了:没有 ref 修饰符就是复制副本,因此无法完成交换过程。正是因为交换需要方法内外都使用同一个数据信息,因此 ref 修饰符才得以存在。正是因为这个修饰符,所以我们才可以推断得到这样的实现逻辑。

那么既然是交换,我们就直接在里面写上交换逻辑即可。

3-3 第三步:去掉返回值类型,把方法名替换为 delegate 关键字

那么,匿名函数的作用是把 匿名函数语法 代替掉原来的 new 委托(方法名),因此我们仅需要替换掉返回值类型和方法名。于是乎,刚才的代码可以改写成这样:

至此,匿名函数就改写完成了。

3-4 特殊情况:无参匿名函数

这里稍微说一下一种特殊情况:无参数的匿名函数。

C# 允许无参匿名函数省略这参数的这对小括号。所谓的省略,就是允许直接不写这对括号。假设一个委托类型是 Action 的话,那么这种情况的方法的签名里,参数是为空的。正是因为这样的情况,匿名函数的原本语法应该长这样:

而这种情况下,小括号可以不写。因此,无参匿名函数的写法可以去掉这对小括号:

Part 4 匿名函数的底层原理

既然都说到这里了,那么还是有必要说明一下,这个 C# 的匿名函数,它的完整版是怎么样的。

4-1 Show you the code

废话不多说直接上代码。不过我们还是得举例说明,才能说明底层原理。假设,我们有这么一个委托类型的实例,赋的是一个匿名函数。请仔细观察此代码:

其中的 delegate (int v) { return v % 2 != 0; } 是匿名函数。它对应了这样一部分的代码。

这,就是 C# 的编译器对匿名函数的实现。换句话说,这段代码才是匿名函数的“真相”。让我们来分析这一段代码。

4-2 Closure 类型本身

首先,我们是把 predicate 实例化的这么一个过程扩展为了一个类类型,就是这个所谓的 Closure 类型。这个设计其实挺奇怪的,对吧。我们最初的办法是在 Main 方法的所在类型里单独创建一个方法,然后把方法名抄到 new 委托 的实例化过程里去。可现在编译器居然生成了一个 Closure 类型,着实让我们初学的时候摸不着头脑。不过,要解释这个问题,我们先不着急。下一讲内容我们会探讨这个问题。

先来看看它自身的修饰符。privatesealed。有趣。private 是防止外部调用。因为这个类型是编译器生成的代码,那么它自然就有一个效果,即不允许任何别的地方乱用和滥用这种编译器创建的类型,否则会导致执行代码的紊乱。所以,private 是有道理的。那么 sealed 呢?不让创建派生类呗。没有人会考虑去对一个编译器生成的类型创建派生类的。但是,也不乏有人会这么去做,所以考虑到这种派生的隐藏的问题,编译器生成的代码自带了 sealed 修饰符不让任何人去从它派生别的类型出来。

而就类型本身来说,它还标记了两个特性,分别是:

  • SerializableAttribute:象征对象是进行二进制序列化和反序列化的操作的;

  • CompilerGeneratedAttribute:这个特性表示这个成员(或者类型)是编译器自主生成的,跟用户写的代码有关。

其中,序列化和反序列化是编程里较为重要的两个概念,这个其实早已说明过,这里再次出现,因此我们稍微回顾一下。

序列化指的是把一个编程里的实现的对象,它包含的数据信息存储到电脑本地里(用一个二进制的文件来存储)的这么一个过程;反序列化就是反过来:把一个本来就是原本序列化得到的这个二进制结果文件,给解析出来,得到对象本身的过程。

不过这个内容已经超出我们本教程的讲解范畴,而且序列化在 .NET 6 框架提供的 API 里有更为方便和好用的新序列化模型:JSON 序列化,因此二进制序列化将会被抛弃掉,因此教程也不打算对二进制序列化作讲解。

那么,看了一眼解释和文字,我们需要了解的就只有这个 CompilerGeneratedAttribute 了。可是这个特性其实也没啥多说的,它是代码在生成后自动添加上去的特性,因此跟我们自己写的代码自身也没有任何关系。毕竟,它是编译器生成的代码,那打个标记好像也没啥不妥,对吧,反正特性也不影响程序执行,只是在反射里会稍微用到一下。而且这个特性会在以后的新语法的底层经常看到。

那么特性就不多说了。接下来我们来说说里面的实现过程和逻辑。

4-3 Closure 的两个字段

那么,既然类型已经创建好了,自然就得说明一下这里都有一些什么了。这个 Closure 类型虽然是一个类类型,但本身也不复杂,它只包含两个字段和一个方法,没别的了。

这两个字段都是 static 修饰的,其中第一个是 readonly 的,即无法修改的 Instance 字段,就是它自己这个类型的字段,另外一个则是 CachedField 字段,是我们这里用到的 Predicate<> 委托类型的字段。

先来说 InstanceInstance 是用于调取里面的方法实例之类的信息用的。而为什么要 readonly 呢?因为 new 类型() 呗。这实例化的是它自己这个类型的情况,那么肯定不会让别人去随意修改它,否则程序也会紊乱。那么为什么是 static 的呢?因为速度和效率。有没有 static 关键字的区别在于,什么时候创建对象。static readonly 的字段的赋值过程会在程序初始化(还没开始运行之前,会有一段时间去初始化程序的数据信息)的时候赋值。这样在运行的时候就不会影响程序的运行速度和效率了;而没有 static 的对象,是在运行时才会创建分配内存空间和赋值,因此会慢一些。考虑到效率的问题,所以用到的是 static

而另外一个字段 CachedField 就是我们这里用到的目标字段了。它用于和记录缓存的委托实例的结果。可以看到它是 static 修饰的,所以它不会受到对象实例化的影响而去单独分配内存之类的。但是它最开始是没有存储任何数值的,并且一直保持 null 这个数值结果。都说了是缓存嘛,缓存缓存那自然是运行的时候用到了才会去看它,对吧。

4-4 Method 方法

仔细看看它的实现代码就可以发现,欸?这不是我前面给的匿名函数里的执行代码吗?是的,它被原封不动地抄写了过来。而方法是实例的方法,且是 internal 修饰符修饰的。奇了怪了,为啥是 internal 而不是 private?这就得说说,它在啥时候被用到了。

可以看到,在 Main 里,我们的那一段匿名函数被改成了 if 语句。如果 Closure 类型的 CachedField 静态字段是为 null 的,那么我们就需要为其实例化和赋值。可以看到,它是在 Main 里使用到了 Method 实例方法。而 Method 这个方法是被放在了 Closure 这个类里了,它并不是在 Main 所在的类 Program 里面。因此,如果我们给 Method 方法标记的是 private 修饰符的话,那么我们无论如何也无法得到这个 Method 了。因此,这里用到的是 internal。那么,protectedprotected internal 行不行呢?当然不行,因为它又不是派生和继承链上的一环。而 protected internal 则是扩大了 internal 修饰符的可访问级别,它允许在项目外部以“从这个类型派生”的方式来得到它里面的内容。哪怕我允许了 Closure 类型不标记 sealed 修饰符,开放继承,也会造成隐藏的不安全性。因此,这里用的是 internal,这也是目前最为合适的级别了。

至此,我们就相当于说明了,这个 Closure 类型的具体的内容,以及匿名函数为什么要被编译器改成这样。当然,至于为什么要用类来封装包裹一层方法,请听下回分解。


第 73 讲:C# 2 之匿名函数(一):委托实例化的简化的评论 (共 条)

分享到微博请遵守国家法律