委托Delegate一文弄懂 C# 匿名方法 Lambda表达式 Action

在对于编程语言以及设计模式更深入的学习之中,很容易就会遇到“委托”或者是类似的东西。为了优化代码整体的结构,我们会用到“回调”之类的手法去实现通信的目的。如果是使用弱类型的语言的话,比如js,就可以直接把方法的名字当作参数传过去,简单直接。不过很多情况下我们需要使用的是强类型的语言,对于参数的类型需要一个明确的定义,所以就产生了专门的定义。
当然,不同编程语言对于此事的定义并不相同,我就挑选我比较熟悉的c#举例。(在c++中有方法类型的指针,Java中是使用接口来模拟)
在委托的概念提出来之后,结合了其语言特性,C#也引出了事件event等机制,在后文也会一并进行解释。

委托 Delegate

委托是一种引用类型变量,存有对某种方法的应用。
在委托声明的时候,决定了能被该委托引用的方法的类型。这一步称为委托类型的声明。而为了真的使用这个类型的委托去存储对某个方法的引用,我们需要用new关键字去创建委托对象,并将需要引用的函数的名字当作参数传递给new语句。

public delegate <return type> <delegate name>(parameter list);
<delegate name> <del-obj name> = new <delegate name>(function name);
// example
public delegate int GetNum(string s);
GetNum getAge = new GetNum(GetAvaAge);

像这个代码块里写的一样,我们就已经声明了一个叫做GetNum的委托类型,并且创建了一个它的委托对象。每个委托对象都可以引用一个与它的类型规定一样的方法。在这个实例代码中,就是任意一个需要一个字符串参数,并且返回一个int值的方法。
委托对象也可以引用匿名方法和lambda表达式(其实他们也都是委托)

委托的多播 Multicasting

之前说到了,每一个委托对象都和一个特定的方法相关联,那这样使用起来会不会有些死板呢?这个问题被接下来的这个机制迎刃而解:委托对象可以进行合并。调用合并起来的委托的时候,相当于调用了每一个组件委托,就像一个广播一样把调用的操作发送给各个引用,所以叫做“多播委托”。
用+和-操作符就可以快速的合并或分离委托。当然了,只有类型相同的委托对象才可以合并。

public delegate void PrintNum(int n);
PrintNum pn;
PrintNum pn1 = new PrintNum(PrintNumA);
PrintNum pn2 = new PrintNum(PrintNumB);
pn = pn1;
pn += pn2;
pn1 += pn2;
//调用pn等于调用pn1和pn2
//调用pn1也等于调用pn1和pn2
pn += PrintNumC;
//也可以直接把一个同类型的方法合并进去

在上述代码中,pn作为一个合并委托包含了pn1和pn2两个委托对象,但是或许你会感到疑惑:pn1原本不是一个普通的委托对象吗?为什么最后它自己也可以+=一个pn2,变成了合并委托呢?
原因很简单,单独的委托对象没有什么用,所以当我们创建委托对象的时候,其实它们都是派生自多播委托的类。也就是说创建任意一个委托对象的时候,它都可以和其他同类型委托对象合并。
值得注意的小tips
在日常使用当中,也许会遇到合并委托的调用列表中的方法被-=操作全部删除的情况,这时候就会调用到空委托,为了避免这种情况,我们使用?.进行调用:

<del-obj name>?.Invoke(parameter list);
// example
pn?.Invoke(121);

至于这个Invoke是什么东西,随后就会进行解释。

多播委托的返回值

不妨亲自写一个多播委托,看看返回值是什么?

using System;
namespace DelTest{
    class DelegateTest{
        public delegate int GetNum(int n);
        public int Plus2(int n){
            n += 2;
            return n; 
        }
        public int Mult4(int n){
            n *= 4;
            return n;
        }
        public int Sub3(int n){
            n -= 3;
            return n;
        }
        static void Main(string[] args){
            GetNum gn;
            GetNum gn1 = new GetNum(Plus2);
            GetNum gn2 = new GetNum(Mult4);
            GetNum gn3 = new GetNum(Sub3);
            gn += gn1;
            gn += gn2;
            gn += gn3;
            Console.WriteLine(gn?.Invoke(6));
        }
    }
}

这个多播委托当中包含了三个方法,分别返回对输入的n加2、乘4、减3的结果。那么最后得到的结果是多少呢?

3

最终只得到了gn3的返回值。为什么呢?其实三个方法都被调用过了,只是多播委托会不停的持续调用委托列表之中的方法,直到结束。而新被调用的方法的返回值就会覆盖掉上一个,最终只剩下最后一个方法的返回值被返回。
所以,如果你的多播委托精心安排了许多的非空返回值,那很可能是没有意义的。
其实也不一定喔

多播委托的逐个调用

题接上文,有没有方法可以得到多播委托中的多个返回值?
答案是可以的,c#提供了一个GetInvocationList方法,可以直接获取到调用列表中的每一个方法:

foreach(GetNum g in gn.GetInvocationList())
    Console.WriteLine(g.Invoke(6));

和期望中一样,可以得到每一个返回值:

8
32
5

但是嘛,如果想要仔细利用每一个返回值的话,选择用多播委托然后循环挑选出每个返回值,还是直接用单个委托一个个调用,哪个更简单哪个更复杂我觉得就有待考虑了。像这样组织成多播委托然后再拆开单独调用,算不算一种绕远路呢?

委托实现回调

用委托实现回调就非常简单了,准确来说这本来就是委托这个机制的主要作用。
在文章的一开始就说到了,在一些弱类型的编程语言当中,可以直接把函数当作参数传递给其他的函数,很轻松的实现回调功能。而强类型的编程语言比如c#引入委托就是为了完成这一类需求。在创建完委托对象并且完成引用之后,我们就可以直接把它当作参数传递出去了。

using System
namespace DelTest{
    class DelegateTest{
        public delegate void PrintString();
        public void ShowID(){
            Console.WriteLine("iLoner");
        }
        public void ShowNum(){
            Console.WriteLine("121");
        }

        //主调函数
        public void Printer(PrintString ps){
            Console.WriteLine("My name is:")
            //调用回调函数
            ps();
        }

        static void Main(string[] args){
            PrintString ps;
            ps += ShowID;
            ps += ShowNum;
            Printer(ps);
        }
    }
}

在上述代码中,Printer方法把一个多播委托对象作为参数,在执行完自己的业务之后调用了多播委托,实现了函数回调的功能。多播委托将信号像广播一样传达到它的委托列表里的每一个引用,最终输出了我的ID。

里世界|委托的(异步)调用

还记得之前说的delegate?.Invoke吗?这是个什么东西咧
如果你学习过c#的并行编程相关的知识的话,应该已经懂了。在.NET中让你可以异步调用任何方法的做法就是调用类型相同的一个委托。
而在调用任意委托的时候,编译器都会自动帮我们生成一个Invoke方法,而我们直接调用委托的时候,其作用与调用.Invoke()是完全等价的。而在上文中,我们或许会面对调用空委托的风险,在这个情况下我们可以使用DelegateName?.Invoke()来进行调用。此时,如果该委托为空,那么调用就不会发生;如果委托不为空,就会正常地调用它。
不过,C#不止提供了.Invoke()这一个调用方法,除此之外还有.BeginInvoke()。在.NET framework中委托和多线程具有非常紧密的关系,仔细解释它们涉及了太多其他的知识,所以在此处就不全部解释了。(不过我最近也在系统的去学习c#并行编程,以后一定会推出具体的博客)
简单来说,这俩的区别就是:一个是同步的,一个是异步的。
那再详细说一下,就是用Invoke的话,程序会等待这个委托执行完毕之后再继续执行;而使用BeginInvoke的话,就会再抓取出一个线程来让委托方法和后续的程序异步运行。
Invoke的使用方法和上文中提到的一样,与正常调用没有什么区别。而使用BeginInvoke以及委托的功能,我们就可以让任意一个(多个)方法异步运行。

大致流程

如果要异步调用委托的话,首先使用BeginInvoke方法去调用它。调用时会多出两个参数,分别是回调方法和一个传递进回调方法的对象(一会儿细说)。BeginInvoke会立刻返回,这样程序就可以继续运行。它将返回的是一个IAsyncResult接口,用于检测异步调用的过程。在合适的时候我们可以使用EndInvoke去阻塞线程直到异步调用完成。
必须使用EndInvoke去结束异步调用

常见操作

既然我们必须要使用EndInvoke去结束异步调用,那么为了能尽快的回收,又不至于让整个程序提前等待它的话,就需要在合适的地方使用EndInvoke,下面我就大概总结一下四种使用EndInvoke的方法:

  1. 在调用BeginInvoke之后做一些其他操作,然后在合适的时候调用EndInvoke(废话!!)
  2. 获取调用BeginInvoke后返回的IAsyncResult,使用IAsyncResult.AsyncWaitHandle中的WaitOne方法阻塞线程直至收到WaitHandle信号,调用EndInvoke(还是得等..)
  3. 检查BeginInvoke的返回值IAsyncResult的状态,判断是不是要调用EndInvoke(还是得找合适的地方)
  4. 使用BeginInvoke的附加参数,在它完成的时候用回调函数触发EndInvoke
    接下来直接用代码来演示吧。

代码演示

首先展示一下这两个方法的参数

public IAsyncResult BeginInvoke (Delegate method, params object[] args);
//method: Delegate 一个方法委托,在BeginInvoke完成后调用
//args: Object[] 一个对象数组,内容就是method所需要的参数

还是相当人性化的。如果这两者都不需要使用的话,就填入null就好了。

public object EndInvoke (IAsyncResult asyncResult);
//asyncResult: 调用BeginInvoke时的返回值

在使用BeginInvoke的过程中,调用委托所需的参数直接在附加参数前面输入即可,并且可以使用out参数。但是如果你在BeginInvoke中使用了out参数,那么就必须要在EndInvoke的参数列表中也写出来这个out参数才可以。(具体什么是out参数就不在这里讲了)

在”合适”的地方调用EndInvoke

//...
public delegate int Test1(int n);

static void Main(string[] args){
    Test1 t1 = n => n*2;  //这是就是传说中的Lambda表达式
    //开始异步调用
    IAsyncResult result = t1.BeginInvoke(2,null,null);
    //处理其他业务
    Thread.Sleep(500);  //当务之急就是去睡觉
    //调用EndInvoke方法,届时会阻塞线程直至委托调用结束
    int res = t1.EndInvoke(result);
}
//...

在这个例子里面,委托调用需要的时间极短,所以睡了500ms之后应当已经完成了。但是在实际应用的过程中,如果委托调用的耗时非常长,那么在这里程序的后续可能就都要等待它跑完才能继续运转了。

使用WaitHandle来等待

//...
public delegate int Test2(int n);

static void Main(string[] args){
    Test2 t2 = n => n*2;
    //开始异步调用
    IAsyncResult result = t2.BeginInvoke(2,null,null);
    //处理其他业务
    Thread.Sleep(500);
    //等待异步调用完成
    result.AsyncWaitHandle.WaitOne();
    //异步运行结束,代码从这里开始继续运行
    int res = t2.EndInvoke(result);
    //关闭WaitHandle,释放资源
    result.AsyncWaitHandle.Close();
}
//...

在我们直接用EndInvoke去结束异步调用的时候,WaitHandle并不会立刻被回收,该句柄会在随后被垃圾回收机制处理掉。而如果我们使用WaitHandle中的close方法关闭掉它的话,这部分资源就会被立刻释放。

检查IAsyncResult的状态

//...
public delegate int Test3(int n);

static void Main(string[] args){
    Test3 t3 = n => n*2;
    //开启异步操作
    IAsyncResult result = t3.BeginInvoke(2,null,null);
    //处理其他业务
    Thread.Sleep(500);
    //循环检查完成状态
    while(resule.IsCompleted==false){
        Thread.Sleep(10);  //等的时候也睡一会儿吧!
        Console.WriteLine("还没有运行完喔");
    }
    //异步结束
    int res = t3.EndInvoke(result);
}
//...

其实和前面两个方法在本质上也没有什么分别,都是要在一个地方阻塞线程至异步结束,只是写法不同,在使用时结合具体问题具体分析。

在回调函数中使用EndInvoke

//...
//某个属性
int k=0;
public delegate int Test4(int n);
//回调函数
public void callBack(IAsyncResult r){
    Test4 t4 = r.AsyncState as Test4;
    int res = t4.EndInvoke(r);
    k = res;
    Console.WriteLine("Completed");
}

static void Main(string[] args){
    Test4 t4 = n => n*2;
    //异步调用
    IAsyncResult result = t4.BeginInvoke(2,callBack,t4);
    //等BeginInvoke完成后自动会调用callBack,我们只需要在确认它完成后去取用k值就好了
}
//...

回调方法中使用参数的方式好像有些抽象,不过在这里我尝试做一个大概的解释。IAsyncResult是一个有严格定义的接口,它由包含可异步操作的方法的类实现(比如委托啊)。但是它其中的AsyncState属性只是一个简单的object引用,是专门定义好的用于传递参数的渠道,一个通用的属性(object)。
那么我们就要根据之前传入的东西是什么来决定如何利用这个属性。我传进去的东西就是一个委托对象,那么我就把它转换成委托对象(写成(Test4)r.AsyncState也行),这样就成功传进来了。

泛型委托

上面说的委托是完全自定义 ,每次都需要自己写一些重复的东西,所以微软贴心的给我们准备了一些泛型委托,可以让我们快速自定义出特定类型的委托。

Action委托

Action是没有返回值的泛型委托。利用它可以快速创建一个没有返回值的委托对象。
Action类型至少有0个参数,最多有16个,没有返回值。

Action<string,int,int> action = new Action<string,int,int>(OnAction);
//创建了一个有传入参数string,int,int无返回值的委托

Func委托

Func是有返回值的委托,使用方法和Action基本相同,<>内最后一个类型为返回值的类型。
同样,Func至少有0个参数,最多有16个参数,但是必须有返回值,且返回类型不可为void

Func<int,int,string> func = new Func<int,int,string>(OnFunc);
//传入参数类型为int,int并返回一个string

Predicate委托

返回bool类型的委托。它有且只有一个参数,返回值固定为bool

Predicate<int> predicate = new Predicate<int>(OnPredicate);

匿名方法

由上文,我们基本已经理解了委托的原理。它是一种代理,可以代为执行一个方法的方法主题。那么在实际使用过程中,原方法的名字相当于被委托给代替了。
所以,我们干脆不要给原来的方法起名字了,它不要方法名,只有方法主体。相当于直接把方法主体的代码本身直接托管给了委托——这就是匿名方法。

delegate int Test(int n);
Test test = delegate(int n){
    return n*2;
};

我们首先创建了一个叫做test的委托对象,然后我们让它等于的这个东西就是一个匿名函数。有两个需要格外注意的点:

  1. 在匿名函数的花括号结束之后还有一个分号;
  2. 匿名函数只指定了参数类型,没有指定返回类型(但是它还能正常返回是吧),因为它对自动从方法主体内推断返回类型
    由于匿名方法的实质就是一个委托,所以它当然可以由另一个委托去引用

Lambda表达式

这个东西在前文已经见过了吧?一个莫名其妙的格式,神秘的 =>箭头,它到底是个什么东西呢?其实它就是一个匿名方法!
首先我们知道,交给委托的方法名字是不重要的,所以我们可以直接把它写成一个匿名方法:

Test test = delegate(int n){
    return n*2;
};

既然我们都知道它是个委托了,不妨改造一下,让它好看一点(不用一直提醒我你是个delegate!)

Test test = (int n) => {return n*2;};

参数的类型也去掉好了,编译器会识别出来它的类型的。反正只有一个参数,括号也去掉了吧(如果有两个及以上个参数,那一定不能去掉括号)

Test test = n => {return n*2;};

因为方法体只有一行,所以花括号也可以去掉(两行及以上就不能去掉)

return……能不能去掉?当然可以!一行语句是没有返回值的,但是如果是一个表达式,那么是有返回值的。所以直接写出一个n*2,就等于告诉了编译器我在这里要返回一个n*2。但是它后面一定不能加分号,不然它就会变成一个语句!(这里的分号是整个语句最后的分号,不是Lambda表达式中的分号)

Test test = n => n*2;

那么你就获得了一个最简单的Lambda表达式。在参数更多的时候,就增加括号;再方法体内行数更多的时候,就增加花括号。适当添加 return 等语句便于阅读,就可以完成一个Lambda表达式了。
这一路看下来,也可以发现委托的进化历史,原本的委托还需要单独定义方法,再由委托引用;后来出现了匿名方法,只用写方法体了;最后连匿名方法都进行了进一步简化,变成了Lambda表达式,视觉上也变得更加简单易懂。
只要出现匿名方法的地方就可以把它写成Lambda表达式,同样的,他们也都能转化成相应的泛型委托。一切都是为了便利代码的编写而存在的。

结语

原本以为委托这个话题说不了多久,没有想到直接就写了五千字,而且还没有把我原计划的内容写完(原计划还要写Event)
不过这部分内容以后肯定还是补回来的,只是这玩意比想象中费时费力太多了,我尽我所能去完成更多的总结。
不过最近也许会变得很忙,事实上已经变得很忙了!最近更新博客的速度可谓是越来越慢,以后尽量保持一定的更新速度吧。
在写文章的时候也纠结过一个问题,就是一篇到底该多长。虽然可能根本就没啥人能看到我写的这些东西,但是我还是思考过文章太长会不会很难读完。当然了,最后还是决定把相关的内容写成一篇比较好。不然零零散散的搞出来几十篇博客很简单,但是又有什么意义呢?博客对我来说最重要的是记录我的学习和进步,而不是灌水,对吧

其他文章:Makefile 是什么?GNU下的make详解

文章创作不易,如果感兴趣请持续关注
暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇