CSharp委托与匿名函数

场景

面对事件处理,我们通常会通过定义某一个通用接口,在该接口中定义方法,然后在框架代码中,调用实现该接口的类实例的方法来实现函数的回调。可能这样来说有些抽象,那我们提供一个具体的情形来实现这一情形。

假设目前我在编写某一个服务,这个服务通过Start启动,并在一定的时间内不停地监听某一个事件的发生:

// 伪代码
public class Service
{
public void Start()
{
int i = 0;
Random rand = new Random();
while (i <= 10000)// 一段时间内
{
int eventInt = rand.Next(10);
// 监听事件的发生,这里就是i能够被随机数整除的事件
if (i % eventInt == 0)
{
// TODU
}
i++;
}
}
}

以上这段代码就大致描述了我的服务运行过程。往往我们都是自行的编写服务代码,在上面的TODU处编写处理函数,以应对这些事件发生的时候进行的操作。比如,现在我想要当事件发生的时候,能够打印eventInt,只需要在TODU处编写输出函数就行了。

然而,我们编写这样的代码扩展性受到了严重的限制。因为假如我要修改事件处理的函数,就必须要到这个地方来修改。其次,假设我现在的想法是这段框架代码我编写好了,而你作为客户端代码使用者,想要定义其他的处理函数,当我打包编译好了这段代码,你完全没法修改它,只能够告诉我,然后将你的代码加入TODU中,这样的维护几乎不现实。

但是,接口(或者是抽象类等其他同思想)可以帮助我们改变这一现状。我们使用一个通用的接口比如EventHandler,在其中定义一个名为EventHandle的方法,就像下面这样:

public interface IEventHandler
{
void EventHandle();
}

然后在原先的服务代码,定义一个接口对象域,通过构造函数或者是定义一个注册方法来注册这个处理类,就像下面这样:

public class Service
{
// 定义一个接口对象
private IEventHandler _eventHandler;
// 通过构造函数注册这个处理对象
public Service(IEventHandler eventHandler)
{
this._eventHandler = eventHandler;
}
// 通过顶定义一个方法来注册这个对象
public void RegistHandler(IEventHandler eventHandler)
{
this._eventHandler = eventHandler;
}
public void Start()
{
...// 省略部分代码
if (i % eventInt == 0)
{
// 把操作全部交给 _eventHandler 来进行
_eventHandler.EventHandle();
}
...
}
}

于是,在我们客户端代码,只需要实现这个接口,并定义自己的方法处理内容,然后实例化这个对象并将其注册到Service中就能够,那么当事件发生的时候,就能够通过运行时候的多态,动态根据我们new出来的不同的Handler对象进行定制的操作,并且,Service端是可以与客户端分离出来的。

更好的语法糖——c#委托

使用委托的角度

诚然,在学习的初期,我十分推荐完全利用面向对象的思想来构建和理解接口与事件处理的代码。但是我们可以发现,这样的代码还不足够的简练。还是以上面的例子来说,每次我要去定制属于自己的事件处理代码的时候,都需要我们去实现这个接口,然后实现其中的接口的方法,然后把这个实例化对象,再注册到代码中去。实际上,在c#中,我们可以利用更加舒服的语法糖来实现:委托。委托的声明类似于函数,但是不带函数体,且要用delegate关键字。大致形式如下:

namespace XXX
{
public delegate void EventHandle();
// 不同的返回类型以及参数类型
public delegate bool Check(int param);
}

实际上,委托的语法应该这样理解:第一个是我定义了一个名为EventHandle的委托,它代表了一个函数,这个函数名字我也不知道是什么,只知道他是参数为空,返回为void的函数;第二个是我定义了一个名为Check的委托,它代表了一个只有一个int类型参数的,返回值为bool的函数。

定义规则如下:

namespace XXX
{
public delegate void EventHandle();
// 不同的返回类型以及参数类型
public delegate bool Check(int param);
}

使用的方式如下:

namespace ServiceSimulate
{
namespace XXX
{
public delegate void EventHandle();
public delegate bool Check(int param);
}
class Program
{
public static void MyEventHandle()
{
Console.WriteLine("Do IT");
}
public static bool MyCheck(int param)
{
return param > 0;
}
static void Main(string[] args)
{
EventHandle myEventHandle = new EventHandle(Program.MyEventHandle);
myEventHandle();
Check myCheck = new Check(Program.MyCheck);
Console.WriteLine(myCheck(2));
Console.ReadKey();
}
}
}

在上面的Program类中,我分别定义了两个函数MyEventHandle和MyCheck,这两个函数的签名(只考虑参数和返回类型)与定义的两个委托EventHandle和Check的语义是一样的。在这样的情况下,我在使用这两个委托的时候,可以上面Main方法中的语法一样,首先定义一个委托类型(EventHandle myEventHandle),通过new 委托的方式将方法设置到委托中(= new EventHandle(Program.MyEventHandle))。

于是接下来我可以直接使用委托变量来达到和使用函数一样的作用,输出见下方:

// OUTPUT
DO IT
True

当然,我们还可以通过更加简洁的声明方式,不用new关键字,而是直接将函数赋予委托类型变量:

EventHandle myEventHandle = Program.MyEventHandle;
Check myCheck = Program.MyCheck;

目前位置大致介绍了委托的语法与语义,那么回到一开始的服务代码中,我们可以看到,每次我们去实现接口,都会去实现接口中的固定的方法,然后再注册,被调用。实际上,我们完全可以使用委托的方式来来简化代码:

我们现在可以不用定义统一的接口了,而是定义一个委托,然后想Service注册这个委托,就完全能够达到一开始调用实现接口的类中的方法的目的了(有点拗口)。

namespace ServiceSimulate
{
public delegate void EventHandle();// 定义委托类型
public class Service
{
private EventHandle _eventHandle;
// 通过构造函数注册委托对象
public Service(EventHandle eventHandle)
{
this._eventHandle = eventHandle;
}
// 通过定义的方法注册委托对象
public void RegistHandler(EventHandle eventHandle)
{
this._eventHandle = eventHandle;
}
public void Start()
{
int i = 0;
Random rand = new Random();
while (i <= 10000)
{
int eventInt = rand.Next(10);
if (i % eventInt == 0)
{
// 注意这里直接就是委托对象语法形式来调用方法了
_eventHandle();
}
i++;
}
}
}
}

在客户端代码中,我们就可以定义满足委托类型语法语义的函数,将函数注册到委托对象上:

class Program
{
// 定义处理方法就行了,不用再实现接口,再定义处理内容
public static void MyEventHandle()
{
Console.WriteLine("Do IT");
}
static void Main(string[] args)
{
Service ss = new Service(MyEventHandle);// 通过构造函数注册委托对象
ss.Start();
}
}

如此以来,利用委托,能够更加方便的函数回调。实际上,我个人对委托的理解,再代码的底层编译器处理的过程中,应该还是将委托转化为了接口函数(个人猜测,技艺不深,无法验证)。

定义委托的角度

在前面的介绍中,我谈了关于委托的使用过程及其思想,主要是从客户端的角度,谈了谈如何使用定义好的委托。在这一节中,我将从结合泛型来谈一谈在我们编写框架代码的时候,如何更为高效的定义我们的委托。

回到一开始的例子,当作为服务端(此服务端是指为客户端程序员提供代码)代码编写者,在以后的开发中,我们会发现我们会定义大量的委托,并且,这些委托实际上绝大部分是具有共性的。有点抽象,具体一点讲,上面的例子中Service我们定义了一个名为EventHandle的委托,他代表了一个返回值为void,无参的函数类型。在以后的开发中,我们可能会定义更多的类似结构的委托,返回值或有不同,参数列表或有不同,就像下面这种情形:

namespace XXX
{
public delegate void EventHandle();
public delegate void EventHandle2(int p1);
public delegate bool EventHandle3(int p1);
public delegate bool EventHandle4(int p1, double p2);
public delegate bool EventHandle5(double p1, int p2);
}

可以看到,我们定义如此多的事件处理委托类型,他们由于返回值、参数的差异彼此不同。不难看出,不同的返回值、参数类型的搭配,可以定义成千上万个委托类型,同时他们彼此还很接近。为了解决这一定义爆炸,c#提供了三种基本的泛型委托,我们只需要改变泛型参数,就能够达到定义不同的委托:

Predicate<T>

该泛型委托的原型定义如下:

public delegate bool Predicate<in T>(T obj);

该委托只需要指定一个参数类型,就能够定义一个返回值类型为bool,一个参数的函数语义委托定义。

Action<T1, T2, ......T16> or Action

泛型Action<T>委托表示引用一个void返回类型的方法。Action<T>委托类存在不同的变体,可以传递至多16种不同的参数类型,没有泛型参数的Action类可以调用没有参数的方法。例如:Action<in T1>调用带一个参数的方法,Action<in T1,in T2>调用带两个参数的方法等.。其某两个原型定义如下:

public delegate void Action();
public delegate void Action<in T>(T obj);
// 极限 void Action<T1,..., T16>(T1 arg1, ...., T16 arg16)
Func<[T1, T2, ...T16,] TResult>

Func<T>的用法和Action<T>用法类似,但是Func<T>表示引用一个带返回类型的方法,Func<T>也存在不同的变体,至多可以传递16个参数类型和1个返回类型,例如:Func<in T1,out Resout>表示带一个参数的方法,Func<in T1,in T2,out Resout>表示调用带两个参数的方法。其某两个原型定义如下:

// 没有参数 + 1个返回类型
public delegate TResult Func<out TResult>();
// 2个参数 + 个返回类型
public delegate TResult Func<in T1, int T2, out TResult>(T1 arg1, T2 arg2);
// 极限 TResult Func<T1,..., T16, TResult>(T1 arg1, ...., T16 arg16)

匿名函数

通过前面的介绍,我们已经能够更为简洁通用的定义自己的委托类型了,比如现在我需要一个定义一个返回值为string,参一个int类型与一个double类型的参数形式的委托类型,可以按照如下定义:

namespace Test
{
class Program
{
public static void Main(string[] args)
{
Func<int, double, string> myFunc;
}
}
}

为了使用这个委托,我们定义一个方法并赋予这个委托myFunc:

class Program
{
public static string f(int p1, double p2)
{
return p1 + ", " + p2;
}
public static void Main(string[] args)
{
Func<int, double, string> myFunc = f;
Console.WriteLine(myFunc(2, 3.33));
Console.ReadKey();
}
}

我们会发现,每次要“实例化”一个委托,我们都要在某个地方去编写一个函数,无论是static的还是实例的方法,都需要我们到一个地方去显式指定好方法函数名等等。而且,通常这些函数我们都是某种工具函数,只会在一些特定的地方去调用,为了这些函数,我们还需要给他们创建一个类,这往往是很多余的(尽管贯彻了OO思想,但是匿名方法往往更体现在函数式编程FP而不是面向对象编程OOP,这是一种编程哲学)。于是,为了脱离面向对象,更好的方式是采取匿名的形式,因为既然我们定义好了委托类型,他制定了返回值制定了参数类型,我们还有必要去显示制定一个函数的名称吗?正如委托语义一样,委托类型就是定义了一个返回值是XXX类型,参数列表是XX t1, xx t1...的函数,至于这个函数到底叫什么根本不用关心。而匿名函数就符合这样的要求。而匿名函数在c#中又分为两种:Lambda表达式和匿名方法表达式。在几乎所有的情况下,Lambda表达式都比匿名方法表达式更为简介具有表现力。

Lambda表达式:

(匿名的函数签名) => (匿名的函数体)

其中匿名的函数签名可以包括两种,一种是隐式的匿名函数签名另一种是显式的匿名函数签名:

  1. 隐式的函数签名:(p)、(p1,p1)
  2. 显式的函数签名:(int p)、(int p1,int p2)、(ref int p1, out int p2)

在显式类型化参数列表中,每个参数的类型是显式声明的,在隐式类型化参数列表中,参数的类型是从匿名函数出现的上下文中推断出来的。

匿名的函数体可以是表达式或者代码块。

当Lambda表达式只有一个具有隐式类型化参数的时候,参数列表可以省略圆括号,也就是说:

(参数) => 表达式 可以简写为 参数 => 表达式

匿名方法表达式:

delegate (显式的匿名函数签名) {代码块}

从表达式来看,匿名方法实际上就是单纯的将函数名省去,而其他部分都和一般定义一个方法一样。

下面是是综合了上述两种表达式形式的是实例

// Lambda表达式
x => x + 1 //隐式的类型化,函数体为表达式
x => {return x + 1;} //隐式的类型化,函数体为代码块
(int x) => x + 1 //显式的类型化,函数体为表达式
(int x) => {return x + 1;} //显式的类型化,函数体为代码块
(x , y) => x * y //多参数
() => Console.WriteLine() //无参数
// 匿名函数方法表达式
delegate (int x) {return x + 1;}
delegate {return 1 + 1;} //参数列表省略

那么,匿名方法表达式和Lambda表达式有什么区别呢?从上面的介绍看来有以下的几点:

  1. 在参数列表上,Lambda表达式能够通过上下文推断参数的类型信息,故可以使用隐式类型化参数。而匿名方法表达式必须要显示的参数类型化。
  2. 当没有参数或者是多个参数的时候,Lambda表达式是不能够省略括号的;匿名方法表达式允许完全省略参数列表。
  3. 在函数体上,Lambda表达式的主题可以是表达式,也可以是代码块;而匿名方法表达式只能是代码块。

回到上面的代码,我们利用匿名函数来实现f方法,这一次我们完全不需要在其他类中去定义这个f方法了:

Func<int, double, string> myFunc = (x, y) => x + ", " + y;
Func<int, double, string> myFunc2 = delegate (int x, double y) { return x + ", " + y; };
Console.WriteLine(myFunc(2, 3.33));
Console.WriteLine(myFunc2(2, 3.33));
Console.ReadKey();
// output
2, 2.33
2, 2.33

CSharp委托与匿名函数的更多相关文章

  1. 3 委托、匿名函数、lambda表达式

    委托.匿名函数.lambda表达式 在 2.0 之前的 C# 版本中,声明委托的唯一方法是使用命名方法.C# 2.0 引入了匿名方法,而在 C# 3.0 及更高版本中,Lambda 表达式取代了匿名方 ...

  2. 用委托、匿名函数、Lambda的方式输出符合要求的数

    最近看了一些博客,对委托和匿名函数和Lambda的方式有了一些更深的理解,在前人的基础上.我也写3个例子 using System; using System.Collections.Generic; ...

  3. C#匿名委托,匿名函数,lambda表达式

    一.类型.变量.实例之间的关系. 类型>变量>实例 类型可以创建变量,实体类可以创建实例,实例可以存储在变量里. 二.委托使用过程: 1.定义委托(写好签名): 2.创建委托变量: 3.给 ...

  4. 面向对象的基本特征:封装(接口 、struct、枚举、委托、匿名函数) 继承,多态。

    如何理解面向对象的基本特征:封装 我们通过接口 .struct.枚举.委托.泛型.匿名函数的去理解封装 接口 .struct.枚举.委托.泛型.匿名函数有什么区别?我们通过这些IL探究真相,案例如下: ...

  5. 委托,匿名函数和lambda表达式

    很早之前就接触到了委托,但是一直对他用的不是太多,主要是本人是菜鸟,能写的比较高级的代码确实不多,但是最近在看MSDN微软的类库的时候,发现了微软的类库好多都用到了委托,于是决定好好的研究研究,加深一 ...

  6. 委托、匿名函数与Lambda表达式初步

    (以下内容主要来自<C#本质论第三版>第十二章委托和Lambda表达式) 一.委托续 上上周五看了看委托,初步明白了其是个什么,如何定义并调用.上周五准备看Lambda表达式,结果发现C# ...

  7. 委托、匿名函数、Lambda表达式和事件的学习

    委托: 还记得C++里的函数指针么?大家可以点击这里查看一下以前的笔记.C#的委托和C++中的函数指针效果一致. 当我们需要将函数作为对象进行传递和使用时就需要用到委托. 下面我们看一个例子: usi ...

  8. C#中委托,匿名函数,lamda表达式复习

    一.委托 1.就给类比较,类用class声明,委托用delegate声明. 2.委托要指向一个真正的方法. 3.委托的签名,要和指向的方法一样. //1.声明一个委托 public delegate ...

  9. C#委托(匿名函数)的各种变形写法

      static void TestDelegate() { //类C++11风格:指定初始化容量20,使用初始化列表给部分成员赋值 ) { , , , , -, , }; ; i < lst. ...

随机推荐

  1. MAC下Jetbrains编译器无法打开问题解决

    这段时间不知道怎么回事,每次打开Rider必定闪退,毫无头绪,只好暂时放弃使用Rider,试用了一段时间Visual Studio. 可惜...虽然大学时候觉得VS天下第一,但是用惯了JB的编译器,再 ...

  2. 剑指offer计划9(动态规划中等版)---java

    1.1.题目1 剑指 Offer 42. 连续子数组的最大和 1.2.解法 得到转移方程后,单次遍历. 当前面的连续子数组的和比较是否大于0,是则加起来, 若小于零,则当前的值就可当子数组的开头. 判 ...

  3. 20201219 u,v,w

    开考前刚起床,所以一边考一边吃饭,然后整场都很迷... A. u 考场 半天才搞懂"下三角区域"指哪个区域,手模样例确认后打了 \(O(qn^2)\) 的裸暴力,然后就不会做了. ...

  4. Python - 面向对象编程 - 三大特性之封装

    简单介绍封装 封装是面向对象编程的一大特点 封装可以被认为是一个保护屏障,防止该类的属性.方法和数据结构被外部随意访问 要访问该类的属性.私有方法.数据结构,必须由指定的方法控制访问 深入理解封装 在 ...

  5. freeswitch编译安装依赖

    ncurses:提供字符界面 zlib:数据压缩 libjpeg:JPEG图片格式数据的解码/编码/其他. lua:lua解释器 libedit:一种编辑操作的库,对一些可以交互操作的场景,或转为了自 ...

  6. CodeForces - 706B Interesting drink(二分查找)

    Interesting drink Problem Vasiliy likes to rest after a hard work, so you may often meet him in some ...

  7. 使用Java操作Excel表格

    目录 一.配置第三方库 二.使用Apache POI API 1. 打开Excel文件 2. 选择对应的sheet 3. Sheet接口的基本使用 3.1 获取开头行和结束行 3.2 获取Row对象 ...

  8. 使用metaweblog API实现通用博客发布 之 版本控制

    使用metaweblog API实现通用博客发布 之 版本控制 接上一篇本地图片自动上传以及替换路径,继续解决使用API发布博客的版本控制问题. 当本地文档修订更新以后,如何发现版本更新,并自动发布到 ...

  9. PHP操作用户提交内容时需要注意的危险函数

    对于我们的程序开发来说,用户的输入是解决安全性问题的第一大入口.为什么这么说呢?不管是SQL注入.XSS还是文件上传漏洞,全部都和用户提交的输入参数有关.今天我们不讲这些问题,我们主要探讨下面对用户的 ...

  10. ECSHOP任意页面显示指定分类、数量、排序的任意类型文章,包括只显示置顶or普通的文章

    1.在需要使用此功能的PHP页面里最后的?>前面添加以下代码,现在以article.php为例子 /** jinmozhe 专业ECSHOP二次开发 * 获得指定分类ID.文章类型.指定数量.排 ...