C#委托、事件、回调函数用法解析
整篇分为三个部分:
- 委托,函数当参数传递的方法
- 事件,比委托安全的方法
- 回调函数
一、委托:函数当参数传递的方法
第一眼看到委托的定义时,是头脑发懵的:委托封装了方法,它定义了方法的类型,使得可以将方法当作另一个方法的参数来进行传递。先稍微用代码解释下:
定义一个委托(假定返回值为void,参数列表为空)
public delegate void TestDelegateNoArg();
与普通函数相比多了一个delegate关键字而已。
同样定义两个函数
static void PrintTest1() { Console.WriteLine("无参数委托测试1"); } static void PrintTest2() { Console.WriteLine("无参数委托测试2"); }
TestDelegatetestNoArg testNoArg = new TestDelegatetestNoArg(PrintTest1); //委托是一个类,因此使用new创建委托实例,将PrintTest1方法
赋给委托实例testNoArg
testNoArg += PrintTest2; //为委托实例再添加一个PrintTest2方法
testNoArg(); //或通过testNoArg.Invoke(),执行委托实例
输出为:
/************************************************************************/
无参数委托测试1
无参数委托测试2
/************************************************************************/
同理,带参数的委托类似,例子如下
public delegate void TestDelHasArg(string str); static void PrintArgTest1(string str) { Console.WriteLine(str); } static void PrintArgTest2(string str) { Console.WriteLine(str); }
main函数中
TestDelHasArg testHasArg = new TestDelHasArg(PrintArgTest1); testHasArg += PrintArgTest2; testHasArg.Invoke("带参数委托测试");
输出为:
/************************************************************************/
带参数委托测试
带参数委托测试
/************************************************************************/
上述的方法可能大家基本上能看懂,但有一个问题可能会产生困恼,以第一个无参数委托的例子为例,我明明可以通过一个函数
static void PrintNoArg() { PrintTest1(); PrintTest2(); }
来实现这样的功能,为何要大费周章用委托呢?
这就涉及到委托的第一个优点:解耦,对修改关闭,对扩展开放。逻辑分离。
假设我们的主函数在类A中, PrintTest1()和PrintTest2()这两个函数在类B中,我们在A中要想调用B的方法,需要用B.PrintNoArg()来调用,这会导致A、B两个类的耦合性较高,B类如果进行了较大的修改,可能会影响调用该方法的结果,使用委托可以进行解耦;并且在大型项目开发中,B这个类可能是另一位程序员写的,PrintTest1()的方法具体的实现是我们不care的,因此提供一个方法让他把对应函数“绑定”到我们的委托中,我们只要在委托中设置正确的方法签名,即参数个数,类型相同,返回值类型相同即可,这就是对修改关闭,以及逻辑分离。并且可以通过“+=”将新方法添加进委托中,不用在PrintNoArg()再添加代码,实现良好的扩展性。
还有一点,委托可以实现动态地赋给参数的做法,避免在程序中大量使用If-Else(Switch)语句。
请允许我借用博主付伤年华的例子来解释下这一优点,对原作者的例子进行了一些改动
原文地址:https://www.cnblogs.com/ruanraun/p/6037075.html
以加减乘除运算为例,运算方法如下:
static void Calculate(string calWay,int a,int b) { if (calWay == "Add") { Console.WriteLine(Add(a, b)); } else if (calWay == "subtract") { Console.WriteLine(subtract(a, b)); } } static int Add(int a, int b) { return a + b; } static int subtract(int a, int b) { return a - b; }
并在main函数中调用
Calculate("subtract", 2, 3);
这样虽然能达到相同的效果,但是在Calculate()这个函数内部的实现中夹杂着大量的If-Else(Switch)语句,比较繁琐。在四则运算中,我们可以发现这些加减乘除的函数实现有一个共性:拥有相同的函数签名,这时候委托就能体现自己的价值了。
一般来说,开发中加减乘除这些具体操作的实现我们会放在一个单独的工具类中,在这里命名为CalTool类,只提供一个接口给外部访问,如下
static int Add(int a, int b) { return a + b; } static int subtract(int a, int b) { return a - b; }
我们在main函数中使用委托的做法
public delegate int Expression(int a, int b); public static void Calculate(Expression ex, int a, int b) { Console.WriteLine(ex(a, b)); }
调用是通过
Calculate(CalTool.Add, 2, 3);
实现,需要不同的方法时将上一行函数的CalTool.Add改为其余方法即可,相当于只改变了Calculate()方法中的一个参数,减少了If-Else(Switch)语句的使用。
总结:
重新看看刚开始对委托的定义:委托封装了方法,它定义了方法的类型,使得可以将方法当作另一个方法的参数来进行传递。是不是有点恍然大悟了?第二个例子中我们定义了委托Expression的函数签名,并在Calculate()中将拥有相同签名的加减乘除的计算方法传递到参数中实现功能。而第一节中的例子更多的是引出委托的定义,两个优点如下:
解耦,对修改关闭,对扩展开放。逻辑分离。
动态地赋给参数的做法,避免在程序中大量使用If-Else(Switch)语句
有经验的朋友可以在第一节的例子中看出一些事件的影子,这也是接下来要讲的重点内容——事件,委托的一个具体应用。
二、事件:比委托安全的方法
这篇的内容主要参照http://www.cnblogs.com/SkySoot/archive/2012/04/05/2433639.html
也是委托事件的启蒙博客,推荐阅读。
事件(Event)是对委托进行了一次更好的封装,在上一篇中的第一节中,我们知道只要调用了委托,就会执行相应的代码。这带来了一个问题,实际开发中,假设我们有服务端A和客户端B,B除了往A中的委托绑定方法以外,还能触发A中的委托事件,这样的操作其实是有风险的,服务端只想在需要的时候执行对应的委托,让客户端执行对应代码,而不是让客户端决定何时触发。事件就是解决这一问题的好办法。
这就是事件的第一个好处,更好的封装性。
以上篇中的加减计算为例。拷贝代码如下
class Program { public delegate int CalculateFunc(int a, int b); static void Main(string[] args) { Calculate(Add, 2, 3); } static int Add(int a, int b) { return a + b; } static int subtract(int a, int b) { return a - b; } public static void Calculate(CalculateFunc cal, int a, int b) { Console.WriteLine(cal(a, b)); } }
在实际开发中,Add(),subtract()等函数细节会单独封装在一个类中(名为 CalculateFunction),具体的计算方法Calculate()也会封装在一个类中(名为CalculateManager),实际调用Calculate()的代码在另外一个类(Main函数中)。在这种协同开发的环境下,我们改装下上述代码,使其满足实际开发需求。
public delegate int CalculateFuncDel(int a, int b); class Program { static void Main(string[] args) { CalculateFuncDel calFuncDelegate = new CalculateFuncDel(CalculateFunction.Add); CalculateManager.Calculate(calFuncDelegate, 2, 3); } } public class CalculateManager { public static void Calculate(CalculateFuncDel cal, int a, int b) { Console.WriteLine(cal(a, b)); } } public class CalculateFunction { public static int Add(int a, int b) { return a + b; } public static int subtract(int a, int b) { return a - b; } }
看起来结构性也不错是吧,但问题是,CalculateFuncDel这一委托暴露给了所有的类,这些类都可以自己创建、绑定自己的函数,并有权限自己触发这些委托,通过
CalculateFuncDel calFuncDelegate = new CalculateFuncDel(CalculateFunction.Add);
即可实现
如果别的类也绑定了这个委托,那么对应的函数也会相应执行,这不是客户端该做的,而是我们发布人员的函数中需要考虑的,在这个例子中,发布人员应该在CalculateManager这个类中对委托进行操作与管理。为了提高封装性,我们将委托CalculateFuncDel放在CalculateManager类中,并在CalculateManager类中加上一个事件。
public class CalculateManager { public delegate int CalculateFuncDel(int a, int b); public event CalculateFuncDel calFuncEvent; //添加CalculateFuncDel对应的事件 public void DoSomeThing() //在Manager中调用 { Calculate(calFuncEvent, 2, 3); } public static void Calculate(CalculateFuncDel cal, int a, int b) { Console.WriteLine(cal(a, b)); } }
这么做的好处如下,
1.使用CalculateManager newCalFunc = new CalculateManager()创建实例后,无法通过这个实例访问到委托变量,只能访问到事件
2.在Main函数中直接使用实例创建一个新事件,即通过
CalculateManager newCalFunc = new CalculateManager(); newCalFunc.calFuncEvent = CalculateFunction.subtract;
进行事件的创建,会报如下错误
只能用 +=和-=进行事件的添加和删除
3.无法通过
newCalFunc.calFuncEvent()
newCalFunc.calFuncEvent.Invoke()
等委托触发方式,只能在Main函数中只能通过newCalFunc.DoSomeThing()进行调用,如下
CalculateManager newCalFunc = new CalculateManager(); newCalFunc.calFuncEvent += CalculateFunction.subtract; newCalFunc.DoSomeThing();
这也是事件相较于委托更有优势的特点:
将委托进行封装,只能通过事件进行委托事件的添加和删除,保障了委托的封装性。
只有在特定的时候由发布者进行事件的触发,限制了客户端的权限,保护代码。
三、回调函数
实际开发代码中,回调一般和委托一起出现,所以专门用一节来讲委托与回调的结合运用。
回调:一个对象将一个方法的引用(方法的引用用委托保存)传入另一个对象,使得只有他能返回信息,这就是一个回调。
接下来通过三部曲讲解回调函数的应用。
第一步:基础的回调函数实现如下
首先定义一个委托:
delegate void DelegateDone(); //定义一个无返回值的委托
再定义一个实现函数,将委托作为参数传递进来:
void DoWork(DelegateDone callBack){ callBack(); }
就实现了一个简单的回调函数了。
来个简单的例子,
class Program { delegate void DelegateDone(); static void Main(string[] args) { Program test = new Program(); test.DoWork(testCallBack); } void DoWork(DelegateDone callBack) { Console.WriteLine("执行某一函数"); callBack(); } static void testCallBack() { Console.WriteLine("输出回调成功"); } }
这个小demo的作用就是:让某个执行功能的函数DoWork()做完某件事之后,调用回调函数testCallBack去做别的事。但在上述案例中,这个回调函数显然缺少了一点灵魂,如果只是输出一行"输出回调成功"的提示,就有点大材小用了。接下来进入三部曲之二,稍微展现下回调的作用。
对上述代码进行些许变动,如下
class Program { delegate void DelegateDone(bool isSuccess); static void Main(string[] args) { Program test = new Program(); test.DoWork(testCallBack); } void DoWork(DelegateDone callBack) { Console.WriteLine("执行某一函数"); bool suc = true; callBack(suc); } static void testCallBack(bool isSuc) { if(isSuc) { Console.WriteLine("输出回调成功"); } } }
改动仅仅在于向委托中加入了一个参数,在执行回调时可以相应的传入一个参数suc,即callBack(suc),这个传递回来的参数可以是DoWork()函数中的变量数据,也可以是执行成功与否的参数信息,一般而言,回调会传回一些有用的数据,便于进一步的处理。
看懂了以上两步,就掌握了回调的用法了。可以进入第三步了,在实际的开发中,testCallBack()中执行的的函数一般会用匿名函数代替,如下
class Program { delegate void DelegateDone(bool isSuccess); static void Main(string[] args) { Program test = new Program(); test.DoWork((isSuc) => { if (isSuc) { Console.WriteLine("输出回调成功"); } } ); } void DoWork(DelegateDone callBack) { Console.WriteLine("执行某一函数"); bool suc = true; callBack(suc); } }
这段代码与含有testCallBack()的代码功能完全一样,唯一不同的地方只是在于它简化了testCallBack(),将它转换成了匿名函数,用lambda表达式进行处理,在开发中的结构会更加清晰。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)