优秀的编程知识分享平台

网站首页 > 技术文章 正文

C#核心-委托和匿名函数揭秘2(c#委托实例)

nanyue 2024-10-25 13:19:03 技术文章 2 ℃

我们接着《C#核心-委托和匿名函数揭秘1》继续讲解。

上篇我们提到C#事件内部其实是通过委托进行实现的。而且也讲解了事件是实现"发布/订阅"模式的实现之一,而且也讲了委托可以作为方法参数或者变量进行传递的,这个和事件完全不一样。正是因为事件内部是委托这个技术实现的,所以大家一提到事件就会和委托一起讨论。但是我在这里问一个问题?事件只能通过委托这个技术手段实现吗?或者说实现"发布/订阅"模式只能通过委托这个技术实现? 请看下图。




请看图1,图2,图3,我通过接口来实现"发布/订阅"模式。具体代码我就不说了。这里我只是想表达的是委托是实现事件的技术手段,也是实现"发布/订阅"模式的技术手段,我想也可以通过别的方式实现。所以面试的时候,我们最好如上述那样解释,而不是说"事件是特殊的委托",或者说"事件是受限制的委托",这些解释感觉都不太准确。

我并不想多聊事件,因为委托的作用不止如此,因为委托可以作为变量或者方法参数随意传递,所以很多地方委托可以展示他的优势。

C#1.0委托主要用于实现事件。因为实例化委托并不方便,需要创建实例化的过程,需要绑定一个对象的方法或者类的静态方法。那么首先实例化过程麻烦,第二还得创建一个类或者静态方法,才能绑定上去,才能使用,这也挺麻烦的。

接下来,C#2.0引入了匿名函数,C#3.0引入了lambda表达式,从此委托广泛应用起来。我分别对应匿名函数和lambda表达式做个例子,如下图。



如图4,图5,图6,是匿名函数。具体用法大家应该都能看明白。我们先看下图。


图7里面这段代码和图5可以看出来,图5省略

new Action<string>这个实例化过程,直接将匿名函数赋值给了委托实例,还有一点就是我们之前提到的,这里不需要创建一个类和方法或者静态方法绑定到委托实例上了。这样又简便了创建过程。

到了C#3.0,简化到了lambda表达式,直接可以简化到下图


没有关键字delegate,甚至方法都不需要加"()"括号,方法体也不需要加"{}"。当然了,如果参数多个的话还是需要加"()",实现体里面包括多行代码,还是得加"{}"。

上述只是基本用法,我们按照分析事件一样的方式,分析一下编译器对匿名函数和lambda表达式做了什么。我们把代码降级。如下图



从上面代码可以看到降级以后的代码,没有了匿名方法或者lambda表达式代码了,只剩下和C#1.0写的代码一样,创建委托实例需要

new Action<string>(xxx),

然后xxx方法是需要创建一个类,然后创建一个方法,然后把方法指向这个委托类,请看图10里面的"<>c"类以及"<.ctor>b__4_0方法"。

所以这里可以得出结论,匿名方法或者lambda表达式,我这里后面把它们统称为匿名函数,匿名函数本质上还是会创建类和方法去绑定委托。只是C#编译器简化了这个过程,这是C#编译器做的一个魔术而已。触发委托本质上还是触发了类的实例方法。

这个编译器生成的类的实例又可以称为闭包!

想必大家都听说过闭包吧,闭包是不是比较难以理解呢。因为看不到,所以难以理解。

看不到什么呢,就是这个编译器生成的类。如果我们知道这一点,我们是不是相当于看到了,闭包不就是C#给我们生成的这个类的实例吗,因为委托的绑定是需要类的实例方法绑定到,所以需要有一个类的实例

这个实例不只是定义了一个需要绑定的方法,还存了这个匿名方法捕获到的变量,以便后续方法被触发的时候能用到这些变量。什么是捕获到的变量,我举个例子大家便明白,请看下图。


我们把代码降级。



我们这里可以看到C#编译器帮我们生成了"<>c_DisplayClass8_0"这个,这个类里面同时生成了a,a1两个字段。正因为我们匿名方法里面用到了这两个变量,所以我们C#编译器帮我们生成的类就包含这两个字段。你们可以看到,为了参考,我在DelegateText1构造函数里面定义了a,a1,a2变量,DelegateText1类里面定义了A,A1字段,因为我们匿名方法只用了a,a1,所以这两个就叫捕获的变量,捕获的变量就会变成我们生成类的两个字段。

看图12,"class_ = new <>c_DisplayClass8_0()",然后依次给a,a1两个字段赋值。然后把实例方法"<.ctor>b_0"绑定到委托实例action上。这里,这个"class_"就是闭包。

至此我们知道了,匿名方法只是C#编译器变的一个魔术,他变出来了一个类,类里面除了需要绑定委托的的方法,还有匿名方法捕获到的变量也都变成这个类的字段。

所有巧妙使用委托和匿名函数的应用,都是利用了C#编译器的这个魔术,它帮助了我们少写了这个类以及类的成员。表面上我们只写了下面的代码。

Action<string> printReverse = s =>

{

Console.WriteLine(s);

Console.WriteLine(a);

Console.WriteLine(a1);

};

实际上,背后生成了多少代码。这不应该疯狂利用吗?接下来,我举几个实战中的例子。


在winform里面,比如做一个报表,点击按钮,查询数据库,然后展示到界面上。为了不阻塞UI界面,我们会调用一个线程,线程里面查询数据,然后绑定数据源,绑定的过程我们不能在线程里面直接调用UI控件,因为直接绑定会报错。

如下图。


图15,BeginInvoke方法参数本身就是一个委托实例,委托实例绑定了lambda表达式,这里其实就是简化了生成一个类以及方法。图15,其实内部就是类似图14的样子,通过拿到上下文,通过上下文Context.Send(new SendOrPostCallback(xx)) 这里可以保证里面调用的方法是在ui线程里面操作的,不会报错。这个我在《C#核心- async await 揭秘》这一篇文章里面有提过,有兴趣的朋友可以去看下。这个例子只是说简化了生成一个类和方法。

我在另外一篇文章《C#核心-迭代器揭秘》里面谈到了链式编程,推荐大家看看。

在这篇文章中,为了实现链式编程,定义了一个扩展方法,请看下图。


最终实现以下链式编程。

大家试想以下,图17,2,3,4这三部,如果我不用lambda表达式,我是不是得每一个步骤先创建一个类,然后写一个方法,然后将实例方法绑定到参数上。类似这样

var enumerable = values.InterationSample4()

.Select(new Func<string,Class1>(创建一个类.new实例.方法))

.Select(new Func<Class1,Class1>(再创建一个类.new实例.方法))

.Select(new Func<Class1,Class2>(再创建一个类.new实例.方法))

看看一句代码,我的先创建3个类,然后实例化对象,然后把方法绑定上去。如果这么麻烦,我干嘛还用链式编程呢。

上面的例子貌似都没有捕获变量的情况,有没有捕获变量的例子呢?答案是肯定有。

请看下图。



这个代码是DI内部实现简化之后的代码,这里不想很仔细的在讲解下去。因为DI有很多内容。我简单讲一下,DI是依赖注入,目的是实例化对象的过程让系统去做,我们应用的时候只需要通过接口就行了,如下图


这个是某个功能的服务层,这里需要使用其他的服务,但是这里我们看不到其他那个服务的具体实现,我们只看到了接口,这就是我们想要的,面向接口编程。面向接口编程不用实例化对象了吗,并不是,而是实例化这个过程让系统层面去做了。具体面向接口的作用我们需要另外开一篇讲解。这里回过头看图19,这个是注册类,也就是实现DI,首先系统DI得知道一个接口要用什么类实现,那样系统才能实例化对象,然后分配给这个接口,用户才能用。这个ServiceRegister就是收集这个接口以及怎么实例化的对象的过程。这里有很多种实例化的方式,可以直接,如下图。

可以通过接口,和实现类,可以通过自定义的实现方式,可以直接返回一个固定对象。虽然有这么多实现方式,但是都可以通过一个委托来统一,只是不同的接口对应委托绑定的方法不一样而已。

public static Cat Register(this Cat cat, Type from, Type to, LifeTime lifeTime)

{

cat.AddServiceRegister(new ServiceRegister(from, (_, arguments) => _.Create(to, arguments), lifeTime) { });

return cat;

}

比如这段代码,to就是被捕捉的变量。(_,arguments)=>_.Create(to,argument)

这个lambda表达式是实例化过程,这里如果要用C#1.0的实现方式,需要创建一个类,类里面有一个方法,方法里面需要调用Create方法,然后类需要创建一个to的字段,最终需要

new Func<Cat, Type[], object>(类实例.方法)。这样代码就多了,多几个这样的方法就需要多几个这样的类。

这里就是通过捕获变量来简化代码。这里可能你会看不懂,但是不要紧,我们知道原理就行,具体这个例子我还会开一篇具体讨论一下这个优点。

Tags:

最近发表
标签列表