C#中委托的基础入门与实现方法
前言
似乎委托对于c#而言是一种高级属性,但是我依旧希望你就算第一次看我的文章,也能有很大的收获。
所以本博客的语言描述尽量简单易懂,知识点也是面向初入门对于委托不了解的学习者的。当然如果有幸有大佬发现文章的错误点,也欢迎留言指出!
关于委托
关于委托的介绍主要来源于c#文档:(本文章优势在于去掉一些不必要的细节,对于初学者而言简单高效)
委托的定义主要是下面几个方面:
- 委托是一种引用类型:表示对具有特定参数列表和返回类型的方法的引用
- 在实例化委托时,你可以将其实例与任何具有兼容签名和返回类型的方法相关联。 你可以通过委托实例调用方法
委托本质上来讲就是将方法作为参数传递给其他方法的一种实现方式,当然开发者也可以直接去调用方法。但是当一个项目扩展到足够大时,这种直接调用的方式就会很复杂,难以维护。而委托不会,可以很方便的进行后期的扩展开发。只需要将自己的方法传入已经写好的对应的委托即可。而不需要再在大量的代码中找到调用处写入自己的方法。
关于委托的一些特点是(暂时不了解没有关系):
- 委托类似于 c++ 函数指针,但委托完全面向对象,不像 c++ 指针会记住函数,委托会同时封装对象实例和方法。
- 委托允许将方法作为参数进行传递。
- 委托可用于定义回调方法。
- 委托可以链接在一起;例如,可以对一个事件调用多个方法。
- 方法不必与委托类型完全匹配。 有关详细信息,请参阅。
- 使用lambda 表达式可以更简练地编写内联代码块。 lambda表达式(在某些上下文中)可编译为委托类型。 若要详细了解lambda 表达式,请参阅 。
如果对于一个初学者,你可以简单的理解,委托就是一个更高级的调用方法的方式,而你要学习的,就是这种方式的实现方法,然后后期再慢慢理解更多的细节。
委托的实现
一、基本实现方式
前面也说,委托是一个引用类型,要使用委托,肯定就需要对其进行定义并创建一个委托对象,下面使用一个带有一个参数的案例来理解委托
public class demodelegate { //t1: delegate void testdel(string s); testdel del; //t4: static void main(string[] args) { demodelegate demo = new demodelegate(); demo.createdelobject(); demo.del?.invoke("你们好"); } //t3: public void createdelobject() { del += testeventone; del += testeventtwo; } //t2: public void testeventone(string str) { console.writeline(str); } public void testeventtwo(string str) { console.writeline(str); } }
如果你是刚刚接触委托这个概念,可能对于这些代码的含义不是特别了解,没关系,你可以根据注释的顺序来看代码并理解委托的实现机制:
- t1:通过delegate关键字定义一个委托类型testdel,并创建一个实例del
- t2: 也很好理解,创建两个测试方法,可以执行输出
- t3: 是委托的关键,为刚刚创建的委托来添加事件方法
- t4: 执行委托实例del,可以简单理解为执行该实例绑定的所有事件方法
当然有一些语法是比较独特的,比如说+=这样的语法,就是一种为委托添加事件方法的方法,而对于执行委托语句demo.del?.invoke("你们好");
中invoke()为执行该委托对象内方法的api,?则可以在del委托对象为空时,系统不报错
关于?的具体含义,可查阅:
其实委托主要是有这简单的四步来实现了,通过这个案例,更加明显的体现出委托将一系列方法作为参数来让其他方法去调用的特点。
二、使用委托时的一些特殊方式
通过上面的案例,可以看出对于委托的实现是很简单的,但是c#还是为我们提供了很多更加间接或者集成的用法,具体有:
1、委托实例对象的创建多元化:
创建委托类型的多种方式:
- 直接使用new来创建一个对象,但是注意,在new时需要绑定直接添加一个事件方法,不然会报错(这也是与其他引用类型不同的地方)
- 使用直接赋值的方式创建
- 定义方法名,后期添加事件方法时自动实例(上面的例子)
关于具体的实现代码:
public class demodelegate { delegate void testdel(string str); //第一种:new 的同时绑定方法 testdel delone = new testdel(testeventone); //第二种 testdel deltwo = testeventone; static void testeventone(string str) { console.writeline(str); } }
注意,关于第三种后期绑定有一种现象值得留意,在使用后期绑定时,如果你是创建一个成员变量(全局变量)委托类型,后期绑定可以直接使用+=来增加委托绑定的方法,而你如果是创建一个局部变量的委托,需要先通过=
来添加一个方法后,才能使用+=来增加方法,不然就会报空,如图:
出现这种情况的原因在于成员变量与局部变量之间的区别,如果想要了解更多,可以执行百度,这边列出两者在本案例中的区别:
成员变量:有默认初始化值局部变量:没有默认初始化值,必须定义,赋值,然后才能使用。
2、事件绑定的多种方式
对于事件的绑定,可以使用的方式有很多,在不同的情况下不同的方式也有不同的优势与局限性,可以根据自己的需求进行自行选择
- 使用方法名通过+=来添加方法,可以通过-=来删除方法
- 使用匿名方法
- 使用lambda表达式
关于第一种方法,已经在上面表示的很清楚。
关于社会的进步与发展,某一方面来讲,是由于人类的懒来驱动的,c#开发人员可能觉得第一种方式太复杂了,于是就出现匿名函数的脚本,先通过代码来看一下其实现方式:
public class demodelegate { delegate void testdel(string str); testdel del; static void main(string[] args) { demodelegate demo = new demodelegate(); //匿名方法使用方式演示 demo.del = delegate (string str) { console.writeline("这是一个匿名方法的测试"); }; } }
通过上面的代码可以看出,通过匿名方式使得我们不需要重新定义方法来进行绑定,只需要通过委托关键字,而省去数据类型修饰符、方法签名等结构
匿名方法定义(菜鸟教程):
匿名方法提供了一种传递代码块作为委托参数的技术。匿名方法是没有名称只有主体的方法。在匿名方法中不需要指定返回类型,它是从方法主体内的 return 语句推断的()
而lambda 表达式更加极致,将能省掉的东西全部省掉,使得最终的表达式极其的简洁:
public class demodelegate { delegate void testdel(string str); testdel del; static void main(string[] args) { demodelegate demo = new demodelegate(); //lambda表达式,括号内为参数变量名,如果没有直接() demo.del += (str)=> { console.writeline(str); }; } }
可以通过脚本看出,lambda 表达式对于语法的节省到达了极致,去掉了所有的修饰符,包括传入的参数的数据类型修饰符,只需要一个变量名即可,而一个委托如果没有参数,直接使用()即可,简单到极致
注意(关于()的应用):
如果lambda 无参数,则必须要有()
有一个参数可以去掉(),直接+= str=>{};
如果有多个参数,也必须要有()
三、委托的几种特殊实现方式
除了使用delegate关键字来实现委托外,c#还提供了几种升级版的集成化的使用方式,比如action和func方法等等。但是注意,高的集成化往往意味着低的适配性。所以对于下面介绍几种方式他们往往有一定的使用限制,不如delegate来的灵活,不过对于特定场景更加的简单快捷
1,使用action方法
在开始介绍使用方式之前,先说明一下其使用特殊
- 对于没有返回值的委托,简单的理解就是不需要return(这种想法是错误的,但是好理解)
其实很简单的对不对,其脚本实现更加简单:
//无参数的action委托对象的定义 action<> actdemoone; //带参数的action委托对象的定义,参数最多十六个 action<int> actdemotwo;
除了定义不同外,创建的委托实例对于方法的绑定与执行与标准的委托相同,其实action本来就是通过delegate来定义的一个委托类型,但是这个定义是c#系统进行定义的。
在代码中我们很容易找到这句定义
而带参数的类型与上面类似
这样我们可以直接使用c#提供定义好的委托类型来创建我们的委托实例,这种方式的优势就是代码结构简洁好用。不过限制就是只能绑定没有返回值的方法
2,使用func方法
其实func的用法是与action相反互补的,其主要的特点是有返回值,为了突出,依旧列出来
- 主要用于没有参数的委托类型,且必须有返回值
- 但是同时是可以有参数的,并不是与action完全相反
通过代码来表述这一特点:
// int为该委托绑定方法的返回值类型,注意必须要有返回值 func<int> fundemo; //绑定与执行与标准委托相同,来复习一下 public class demodelegate { static void main(string[] args) { demodelegate demo = new demodelegate(); func<int> fundemo; //为委托添加方法 fundemo = demo.testeventone; //执行委托 fundemo?.invoke(); //依旧可以使用lambda表达式 fundemo += () => { return 0; }; /*-----------带参数的func用法--------*/ func<string,int> fundemotwo; fundemotwo = demo.testeventtwo; } public int testeventone() { return 0; } public int testeventtwo(string str) { console.writeline(str); return 0; } }
上面的代码将两种情况放在一起解释的,可以分开逻辑进行理解,但是重要的是要理解带参数的func的用法,我们可以看到func<string,int>
有两个数据类型的写入,前面一个就是传入参数的类型,而后面就是返回值类型,一定要区分开来
四、委托的一些特殊小知识
1、委托闭包的产生
在正式开始介绍闭包概念之前,先通过一个案例来发掘关于闭包产生的现象,你可以猜想一下下面的代码的输出结果:
class demodelegate { delegate void testdel(); testdel del; static void main(string[] args) { demodelegate demo = new demodelegate(); for (int i = 0; i < 3; i++) { demo.del+= () => { console.writeline(i); }; } demo.del?.invoke(); } }
如果根据正常的逻辑思路去判断,会很直接的判断得到的输出为:0、1、2,但是经过执行打印却发现最终的输出结果为:3、3、3(为啥不是2、2、2,思考一下++i与i++,或者往后看)
这样的结果确实很奇妙,但是如果认真思考,你可能会有一些小想法,是不是由于存储的是数据i地址呢,而导致最终结果相同呢。其实可以简单的理解成这个样子。
如果想要更加深入的了解闭包,希望下面的一些解释可以帮助你理解
闭包概念:
是一个函数与其他相关引用环境组合的实体,而在引用环境消失后,该函数会依旧保存从函数内引用的变量
根据上面的例子来翻译一下就是委托的代码块使用了代码块外的变量,而这个变量是test()方法的局部变量,当test()方法执行完毕后,这个局部变量本应该被销毁,但是由于闭包原因却保存其状态到内存中,来保证自己后续的使用。
因此,所有的委托方法需要的变量内存地址都指向了本应被销毁的局部变量i,最终的读取就是i最后的状态,也就是全是经过三次i++后的3
闭包需要注意的问题
如果使用必要,需要注意由于闭包而产生的内存泄露的问题,由于闭包是访问另一个函数中的变量,就会影响另一函数中的局部变量的内存回收。而一直占用在内存中。造成内存泄露的后果。
但是也有另外的观点讲闭包不会造成内存泄露。原因在于c#的垃圾回收是有相应的处理的。由于本人对于垃圾回收机制认识比较浅显,目前也做不出判断。还希望了解的人可以留言告知
2,关于事件
在c#中,也提出了事件这个概念,本质上来讲也是委托,但是是一个受限的委托
与灵活的委托不同,事件只能在定义的类内被调用,不过可以在其他类里面进行方法的绑定
事件的用法
在我们定义一个委托类型后,我们可以通过event创建一个事件实例:
通过上面的案例可以看到,使用event修饰委托实例后,只能够在定义类中执行委托的方法,而不能在其他类中去调用执行
总结
关于委托的内容还是挺多的,但是大多都是对于本质的东西进行的一些扩展,在学习初期,只需要掌握关于delegate的核心的用法即可,后续再慢慢的延申扩展
到此这篇关于c#中委托的基础入门与实现的文章就介绍到这了,更多相关c#委托基础入门内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!