C#委托、事件剖析(上)
程序员文章站
2022-04-28 09:47:37
本节对委托、事件做以总结。 一、委托: 1、概念:先来说明变量和函数的概念,变量,以某个地址为起点的一段内存中所存储的值,函数,以某个地址为起点的一段内存中存储的机器语言指令。有了这2个概念以后,我们来看c++中的函数指针,函数指针就是指向这个函数的地址,函数指针所指向的类型就是函数在内存中的大小, ......
本节对委托、事件做以总结。
一、委托:
1、概念:先来说明变量和函数的概念,变量,以某个地址为起点的一段内存中所存储的值,函数,以某个地址为起点的一段内存中存储的机器语言指令。有了这2个概念以后,我们来看c++中的函数指针,函数指针就是指向这个函数的地址,函数指针所指向的类型就是函数在内存中的大小,有了这个起点和大小,函数指针就可以代替函数完成对函数的调用。在c#中,委托delegate就是对c++中函数指针做了一个升级,同样它没有直接调用方法采用的是间接调用,是一种类,所以也是一种数据类型。下面举一个简单的例子,说明它是类。
using system; using system.collections.generic; using system.linq; using system.text; using system.threading.tasks; namespace consoleapp1 { class program { static void main(string[] args) { action action = new action(method); console.writeline(action.gettype().isclass); } static void method() { } } }
这里我们用了c#类库中自带的action委托,先不需要管它是什么样的委托,后面会介绍,然后调用type类的isclass属性,返回true,则他就是一个类,所以它也是一种数据类型。可以看出,委托形成了一种动态调用代码(方法)的结构,功能十分强大。
2、委托的一般使用:在声明一个委托时,这个委托的参数就是一个方法名,这样就可以把这个具体的委托当做参数传入另一个方法,也就相当于把
这个委托中的方法当做参数传入另一个方法,这个被传入的方法分为2种:
(1)回调方法:无返回值,没有返回值就说明他只是做了一些处理,至于被不被调用完全要看主调方法是否选择调用它,这就和找工作一个道理,你发一份简历
出去,至于公司给不给你offer取决于公司。
(2)模(mu)板方法:有返回值,说明你所返回的东西会对调用者起一定的影响作用,有返回值一般也有参数,根据参数的不同返回不同的返回值,所以
它的作用对于调用者是一个模板。
example1:
using system; using system.collections.generic; using system.linq; using system.text; using system.threading.tasks; namespace consoleapp1 { class program { static void main(string[] args) { action action = new action(method); action.invoke(); action(); } static void method() { console.writeline("hello delegate"); } } }
这里用的是c#类库中最常用的返回值为空并且无参的action委托,method方法无参无返回值,2种调用方式,第一种调用委托的invoke()方法,
第二种采用的是函数指针式的调用,都可以使用。
example2:
using system; using system.collections.generic; using system.linq; using system.text; using system.threading.tasks; namespace consoleapp2 { class program { static void main(string[] args) { action<string, int> action = new action<string, int>(method); action.invoke("张三",18); action("李四",19); } static void method(string name, int age) { console.writeline($"我叫{name}今年{age}岁"); } } }
这里用到了c#自带的常见泛型委托action<t>无返回值有参数,泛型这里就当做一个类型就好,会在别的章节做详细说明
example3:
using system; using system.collections.generic; using system.linq; using system.text; using system.threading.tasks; namespace consoleapp2 { class program { static void main(string[] args) { func<double, double, double> func = new func<double, double, double>(add); double result1 = func.invoke(1.5,3.5); console.writeline(result1); double result2 = func(2.5,4.5); console.writeline(result2); } static double add(double x, double y) { double result = x + y; return result; } } }
这里用了c#类库中常用的func<t>委托,也是一个泛型委托,<>中最后一个是返回值结果,可以在vs的提示重载中看到。
example4:
using system; using system.collections.generic; using system.linq; using system.text; using system.threading.tasks; namespace consoleapp2 { class program { static void main(string[] args) { func<string> func = new func<string>(findname); sayhello(func); } static void sayhello(func<string> finddelegate) { console.writeline($"hello {finddelegate()}"); } static string findname() { return "小明"; } } }
这里用了func<t>只有返回值的情况,并将这个委托当做参数传进了另一个方法,也就间接的把findname这个方法当做参数传入了sayhello这个方法。
3、下面举两个比较贴近生活、委托和别的结合使用的典型事例。
example1:
using system; using system.collections.generic; using system.linq; using system.text; using system.threading.tasks; namespace consoleapp3 { public delegate product productdelegate(); public delegate void logdelegate(product product); class program { static void main(string[] args) { wrapproduct product = new wrapproduct(); logger logger = new logger(); wrapfactory wrapfactory = new wrapfactory(); productdelegate productdelegate1 = new productdelegate(product.gettoy); productdelegate productdelegate2 = new productdelegate(product.getstationery); logdelegate logdelegate = new logdelegate(logger.log); box box1 = wrapfactory.getbox(productdelegate1,logdelegate); box box2 = wrapfactory.getbox(productdelegate2,logdelegate); console.writeline($"product1 {box1.product.name} the price is {box1.product.price}"); console.writeline($"product2 {box2.product.name} the price is {box2.product.price}"); } } public class product { public string name { get; set; } public int price { get; set; } } public class box { public product product { get; set; } } public class wrapfactory { public box getbox(productdelegate productdelegate,logdelegate logdelegate) { box box = new box(); product product = productdelegate.invoke(); if (product.price>50) { logdelegate.invoke(product); } box.product = product; return box; } } public class wrapproduct { public product gettoy() { product product = new product(); product.name = "toy"; product.price = 100; return product; } public product getstationery() { product product = new product(); product.name = "stationery"; product.price = 30; return product; } } public class logger { public void log(product product) { console.writeline($"product {product.name} created at {datetime.now.tostring()}"); } } }
delegate既然是类,那么应该和类平级,放于类的外部。这里用了自定义的委托,有返回值的委托productdelegate封装的方法是wrapproduct制造产品类里的制造玩具和制造文具方法,无返回值的委托logdelegate封装的是logger记录日志类里的log日志方法。首先做2个实体类,product产品类,box盒子类,盒子中放的就是产品,然后做一个包装类,返回一个盒子,写一个将产品包装在盒子中的方法,这个方法的2个参数,是2个委托,一个用于创作产品一个当产品价格大于50的时候,就调用log方法记录日志,最后在main方法里开始实例化类并调用,自定义委托和c#类库自带的委托都可以使用,看个人喜好,c#自带的就不用声明委托了。
example2:
using system; using system.collections.generic; using system.linq; using system.text; using system.threading.tasks; namespace consoleapp4 { class program { static void main(string[] args) { func<int, int, int> func1 = new func<int, int, int>((int a, int b) => { return a + b; }); var func2 = new func<int,int,int>((a, b) => { return a * b; }); calculate(func1,2,3); calculate(func2,3,4); calculate((x,y)=> { return x - y; },7,1); } static void calculate<t>(func<t,t,t> func,t x,t y) { t z = func.invoke(x,y); console.writeline(z); } } }
这里用到了蛮多的小知识点,首先泛型函数和泛型委托,然后用到了lambda表达式,精简的说一下lambda表达式:(t t)=>{expression; }小括号里是参数,大括号中是要写的算法,也就是方法体,当然不会写太多,不然还不如写一个方法就用不到lambda表达式了。已经知道,委托声明是封装一个方法,那么就可以用lambda表达式代替方法,这就是把一个lambda表达式赋值给一个委托,c#中很多委托都会用到,所以第三次调用calculate方法时,直接将lambda表达式当成参数传进去,是不会报错的。最后还有一个重要的点,就是泛型委托的类型参数推断,在第二个委托func2中,c#根据传入的参数推断出泛型的具体类型是int,从而将代码简写。
4、委托的抗变和协变
1、概念: .net 4.0中抗变和协变已经成熟了,主要分为2类,委托的和泛型的,此处只讲委托的,泛型的后面会说明。委托所封装的方法和声明委托是所定义的类型不一定相同,这就产生了抗变和协变。
namespace consoleapp5 { class father { } class son : father { } class program { static void main(string[] args) { func<father> func = new func<father>(method); } static son method() { son son = new son(); return son; } } }
上面的是抗变:2个实体类father父类,son子类,继承father,委托声明时,返回值为父类,调用的时候却调用的是返回值为son的方法,
也就是说抗变指的是委托所封装的方法的返回值是声明委托的返回值类型的子类。
namespace consoleapp5 { class father { } class son : father { } class program { static void main(string[] args) { action<son> action = new action<son>(method); } static void method(father father) { } } }
现在这个自然是协变,仍然一个父类一个子类,很明显,协变指的是委托所封装的方法的参数是声明委托时参数的父类。
5、委托的高级使用:
主要讲2个方面:多播委托以及委托的隐式异步调用。
(1)多播委托:
通常的委托,一个委托封装一个方法,多播委托中可以一个委托封装多个方法,这些方法通常都是void的,但是不为空也可以,不会报错,实例如下:
namespace consoleapp6 { class program { static void main(string[] args) { var func = new func<int, int, int>(m1); func += m2; func += m3; func(3,3); console.writeline(func(3,3)); } static int m1(int x, int y) { console.writeline(x+y); return x + y; } static int m2(int x, int y) { console.writeline(x - y); return x - y; } static int m3(int x, int y) { console.writeline(x*y); return x * y; } } }
可以看到确实没有报错,但是它最后的返回值是9,也就是说调用多播委托以后他最后的返回值是最后一个方法的返回值,所以有返回值的方法一般不用于多播委托,来看一个正常的例子。
namespace consoleapp7 { class program { static void main(string[] args) { var action = new action(m1); action += m2; action += m3; action.invoke(); action -= m2; action.invoke(); } static void m1() { console.writeline("m1 is invoked"); } static void m2() { console.writeline("m2 is invoked"); } static void m3() { console.writeline("m3 is invoked"); } } }
这里用+=和-=将方法逐一封装在同一个委托里,实现了只需要调用一次委托就调用了所有方法的功能。
那这里的底层实现是什么呢,先举实例:
namespace consoleapp8 { class program { static void main(string[] args) { var action1 = new action(m1); var action2 = new action(m2); var action3 = new action(m3); action action = null; action = (action)delegate.combine(action,action1); action = (action)delegate.combine(action,action2); action = (action)delegate.combine(action,action3); action(); console.writeline(); action = (action)delegate.remove(action,action2); action(); } static void m1() { console.writeline("m1 is invoked"); } static void m2() { console.writeline("m2 is invoked"); } static void m3() { console.writeline("m3 is invoked"); } } }
从上面的例子中可以看出,+=和-=的具体实现使用delegate的combine和remove方法,来增加或删除委托中的方法。
(2)先来看一下显示异步调用:
namespace consoleapp9 { class program { static void main(string[] args) { thread thread1 = new thread(new threadstart(m1)); thread thread2 = new thread(new threadstart(m2)); thread1.start(); thread2.start(); } static void m1() {} static void m2() {} } }
这里可以用到了线程,可以看到threadstart是一个委托。
namespace consoleapp10 { class program { static void main(string[] args) { task task = new task(new action(m1)); task.start(); } static void m1() { } } }
这里用task也可以,也是线程中的东西关于task会在以后详细说明,可以看到参数也是一个委托。
下面是隐式异步调用的例子:
namespace consoleapp11 { class program { static void main(string[] args) { calculator calculator = new calculator(); func<int, int, int> func = new func<int, int, int>(calculator.add); iasyncresult asyncresult = func.begininvoke(2,3,null,null); console.writeline($"结果是{func.endinvoke(asyncresult)}"); console.writeline("计算完成"); } } public class calculator { public int add(int a, int b) { return a + b; } } }
隐式异步调用的底层机制就是多线程,而委托中的begininvoke方法恰好会生成分支线程,所产生的信息可以通过iasyncresult接受,产生的返回值调用endinvoke方法即可,注:begininvoke方法的参数是一个asynccallback委托,主要用来作为回调函数也就是你调完方法以后还需要做什么,如果不需要传入null就可以了。
到此委托部分结束,事件会在下一节总结。 2018-08-17 10:31:56