感谢刘铁猛老师的《C#入门详解》和擅码网Monkey老师的《C#面向对象基础》
本专栏的委托与事件部分已经更新完毕,跳转链接如下:
第一篇:感性认识委托
感性认识委托 - 褚星痕的文章 - 知乎 https://zhuanlan.zhihu.com/p/146341073
第二篇:函数指针:委托的由来
函数指针:委托的由来 - 褚星痕的文章 - 知乎 https://zhuanlan.zhihu.com/p/146637091
第三篇:委托的用法
委托的用法 - 褚星痕的文章 - 知乎 https://zhuanlan.zhihu.com/p/147242231
第四篇:感性认识事件
闹钟响了我起床——感性认识事件 - 褚星痕的文章 - 知乎 https://zhuanlan.zhihu.com/p/147932169
第五篇:事件的调用
事件的调用 - 褚星痕的文章 - 知乎 https://zhuanlan.zhihu.com/p/148561855
第六篇:事件的完整声明,触发和事件的本质
事件的完整声明,触发和事件的本质 - 褚星痕的文章 - 知乎 https://zhuanlan.zhihu.com/p/150967817
第七篇:为什么我们需要事件&补充和总结
为什么我们需要事件&补充和总结 - 褚星痕的文章 - 知乎 https://zhuanlan.zhihu.com/p/162065756
第八篇:用委托事件机制模拟游戏场景
浅谈C#委托事件机制:开阔地机枪兵对射问题 - 褚星痕的文章 - 知乎 https://zhuanlan.zhihu.com/p/166465013
每个程序员都会对自己掌握的第一门语言怀有特殊感情,对我来说,这种语言正是C#;希望我的文字能为大家带来一点帮助,还请多多指教~
作为系列的第三篇文章;我们接着上一篇,说说委托的用法,这将有助于我们理解“事件”。
委托的实例就像一个“大号的方法”,这个大号方法里可以存放真正要使用的方法,而调用方法的动作时可以通过调用这个大口袋来间接进行——当然,委托里只能“塞”符合委托标准的方法。
委托是一种class[类],类是数据类型所以委托也是数据类型(正如同函数指针是数据类型一样),其声明方式与一般的类不同,主要是照顾可读性以及C/C++传统
·注意委托的声明位置,注意自己是否需要嵌套类型的委托(因为作用域会有限)
·委托与可被封装的方法必须类型兼容
委托的声明语法:
delegate method-return-type delegate-name (params-list);
其中,委托的返回值类型必须与method-return-type一致,而参数列表也必须与params-list一致
注意这个声明语法有意保持了与函数指针类似的声明格式
复习:函数指针声明:
//定义一种函数指针数据类型Calc
//Calc类型是一种包含两个int类型传值参数且返回int类型数据的函数的指针,*Calc是函数指针的意思
typedef int(*Calc)(int a, int b);
通过上述声明语法,我们可以自定义一种新的数据类型,这种数据类型描述了“返回值为method-return-type且参数列表为params-list的方法”;
现在我们再来看第一课中阿宅的例子:
我们定义一种委托来描述阿宅吃饭所需要的方法:
//定义一种委托类型,这种委托可以封装参数列表为空,没有返回值的方法
delegate void FindSthToEat();
然后我们实例化一个委托的实例,并且把我们需要的方法塞进这个大口袋;最后我们再通过调用这个大口袋来间接调用被塞进去的那些方法:
static void Main(string[] args)
{
Otaku Rosen = new Otaku();
Rosen.Name = "Rosen";
Rosen.DishName = "小笼包子!";
FindSthToEat xiaoLongBaoZi = new FindSthToEat(Rosen.WalkIn);
xiaoLongBaoZi += Rosen.SitDown;
xiaoLongBaoZi += Rosen.Thinking;
xiaoLongBaoZi += Rosen.OrderDish;
xiaoLongBaoZi += Rosen.Eat;
//调用委托
xiaoLongBaoZi();
Console.ReadLine();
}
在这里,我们把委托实例“xiaoLongBaoZi”直接视为一个方法,使用方法调用操作符"f(x)"进行了调用,但委托实例还有其它调用方式,使用其它调用方式时一般是出于一些特殊的目的,例如多线程或是挂载事件处理器,我们会在稍后讲解
对了,这里顺便提两个C#预定义的委托类型:
Action委托
Action委托可以封装无返回值,有最多16个参数的方法;因为没有返回值,通常用于执行某段代码最后的逻辑——函数是数据的加工厂,如果这个加工厂只负责加工而不负责产出(无返回值)的话,其中进行“加工”的部分就可以使用Action委托封装。
在上面的例子中,其实没有必要自定义委托来描述阿宅吃饭的动作;直接用这个预定义的Action委托就可以
static void Main(string[] args)
{
Otaku Rosen = new Otaku();
Rosen.Name = "Rosen";
Rosen.DishName = "小笼包子";
Action xiaoLongBaoZi = new Action(Rosen.WalkIn);
xiaoLongBaoZi += Rosen.SitDown;
xiaoLongBaoZi += Rosen.Thinking;
xiaoLongBaoZi += Rosen.OrderDish;
xiaoLongBaoZi += Rosen.Eat;
xiaoLongBaoZi();
Console.ReadLine();
}
Func委托
Func委托:Func是系统预定义的另一种委托,可以封装有返回值,最多16个参数的方法,“封装一个数据加工厂使其可以在别处被调用”
为了演示Func委托,我们不妨定义一种新的类型:猪
众所周知,猪有四种状态:
1,活的
2,死的
3,被切片的
4,被做成香肠的
而猪的初始状态是活的,没有死,没有被切片也没有被做成香肠的:
class Pig
{
//定义猪的四种状态:活的,死的,被切碎的,被做成香肠的
public bool isLivelyPig {
get; set; }
public bool isDeathPig {
get; set; }
public bool isSlicedPig {
get; set; }
public bool isSausage {
get; set; }
//使猪的初始状态是活的,没死,没被切片也不是香肠
public Pig()
{
isLivelyPig = true;
isDeathPig = false;
isSlicedPig = false;
isSausage = false;
}
}
考虑到猪不太可能把自己做成香肠,所以加工猪肉的方法都应该都是静态方法:
public static Pig KillPig(Pig pig)
{
pig.isLivelyPig = false;
pig.isDeathPig = true;
return pig;
}
public static Pig CutPig(Pig pig)
{
pig.isSlicedPig = true;
return pig;
}
public static Pig CookPig(Pig pig)
{
pig.isSausage = true;
return pig;
}
一个一个调用这些加工猪肉的方法太麻烦了,所以我们定义一个新的方法来一次性完成从杀猪到做香肠的步骤。
这里的每一步都是有返回值的,一个理想的加工流程应该是输入一头猪,依次对猪进行杀,切片和烹饪,再输出香肠才对,这里的具体步骤就可以用Func委托来封装:
public static Pig MakeSausage(Pig pig, Func killPig, Func CutPig, Func CookPig)
{
pig = KillPig(pig);
pig = CutPig(pig);
pig = CookPig(pig);
return pig;
}
解释一下,在使用Func委托时,尖括号里的最后一个参数是委托实例的返回值类型,其它参数是内部逻辑需要使用的参数的数据类型,在本例中,第一个Pig代表Pig类型参数,第二个Pig代表返回Pig类型的返回值。
下面我们来创造一只猪,并把这只猪做成香肠:
static void Main(string[] args)
{
//准备委托实例以待作为参数
Func killPig = new Func(Pig.KillPig);
Func cutPig = new Func(Pig.CutPig);
Func cookPig = new Func(Pig.CookPig);
//实例化一只大大猪
Pig bigBigPig = new Pig();
//把大大猪做成香肠,将刚才获得的三个委托实例实例作为参数传入回调方法
Pig.MakeSausage(bigBigPig, killPig, cutPig, cookPig);
//查看是否成功
if (bigBigPig.isSausage == true)
{
Console.WriteLine("香肠真好吃");
}
else
{
Console.WriteLine("这猪是活的");
}
Console.ReadLine();
}
输出为:
香肠真好吃
模板方法和回调方法是委托在事件以外最主要的应用场景,现在我们就来说说这两种委托的用法。
所有编程语言的语法机制都是对现实世界逻辑问题的模拟,而模板方法和回调方法也一样:他们并非真的是什么需要死记硬背的设计模式,只是委托机制为了适应不同程序设计需求所自然产生的,能比较好解决问题的灵活用法而已。
在第一课中我们举了一个例子:
当写一个控制阿宅午休行为的程序时,如果直接写方法会失去灵活性,导致阿宅每天中午都只能做同一件事,于是我们借助委托的帮助,把“EnjoyANiceDay”方法中关于午休的部分空出来,等调用的时候再根据具体需要用委托来填补那段逻辑。
阿宅的EnjoyANiceDay和上面Pig类的MakeSausage都是典型的模板方法:
借用指定的外部方法来产生结果——模板方法就如同一个模板,其它部分都确定,只有委托的部分是不确定的
·相当于“填空题”
·常位于代码中部
模板方法的使用:在一个常规方法中有一段逻辑需要借用其他方法实现,在实现了这个逻辑后再继续执行本来的逻辑。
对于有返回值的模板方法来说,中间占位的委托通常需要返回值(把模板方法加工的数据交给外部方法加工一下再继续交给模板方法剩下的逻辑继续加工——永远记住,函数是数据的加工厂)
为什么使用委托完成模板方法而不是直接调用其它方法:这正体现了“模板”二字,模板方法只提供一个模板,具体采用哪个方法是由调用者决定的,而不是直接在方法体里写死,直接写死就没有灵活性了,永远只能调用写定的那个方法
回调方法
当一个方法所需要调用的外部逻辑位于方法体的分支结尾时,这个方法就是回调方法。
·相当于流水线
·常位于代码末尾
·委托通常不需要返回值
回调方法就像是很多人都给了你名片,你在需要的帮助的时候可以通过不同名片去找到不同的人来帮助你,因为回调方法位于方法体的结尾,所以一般不需要返回值(因为方法已经结束了,不需要用返回值来延续后面的逻辑)
回调方法又被称为“好莱坞方法”,这很有助于理解回调方法是怎样工作的:
一些演员去好莱坞试镜,每人都给导演留下了自己的电话号码,导演说:如果你们选上的话我就会给你们打电话——导演可以不打电话,也可以决定具体拨打哪一个人的电话
PS:不必从定义上纠结什么是模板方法,什么是回调方法,这两个人为定义的概念没有什么意义,大家只要借此了解怎样灵活使用委托就好了——我们完全可以让一个有多个委托类型参数的方法既是模板方法又是回调方法啊:
//产品类,定义了“产品”
class Products
{
public string Name {
get; set; }
public int Price {
get; set; }
}
//盒子类,定义了“包装箱”
class Box
{
public Products Product {
get; set; }
}
//定义一个登记器类来记录程序运行状态
class Logger
{
public void log(Products product)
{
Console.WriteLine("产品{0},创建于{1},价格是{2}",product.Name,DateTime.UtcNow,product.Price);
}
}
//包装工厂类,可以产出包有产品的包装箱
class WrapFactory
{
//定义一个返回Box的方法,这个方法的第一个传值参数是返回Product对象的方法(Func委托)
//第二个传值参数是进行登记器登记的方法(action委托)
public Box WrapProduct(Func getProduct,Action log)
{
//创建一个新的空盒子
Box box = new Box();
//创建一个产品,这个产品是调用getProduct方法得到的product
Products product = getProduct.Invoke();
//盒子里的产品就是刚才生产出来的产品
box.Product = product;
//如果产品的价格大于50就进行登记
if (box.Product.Price >= 50)
{
//调用action委托对box.Product进行登记
log.Invoke(box.Product);
}
//返回盒子
return box;
}
}
//产品工厂类,可以制造产品
class ProductFactory
{
public Products MakeCake()
{
Products product = new Products();
product.Name = "cake";
product.Price = 12;
return product;
}
public Products MakeToyCar()
{
Products product = new Products();
product.Name = "ToyCar";
product.Price = 50;
return product;
}
}
}
}
输出为:
产品ToyCar,创建于2020/5/14 5:40:56,价格是50
cake
ToyCar
MulticastDelegate[多播委托]
当一个委托内部封装着不止一个方法时,这个委托就是多播委托,执行多播委托的顺序是多播委托的封装顺序:先执行较早封装的方法,再执行较迟封装的方法。
前面阿宅的“XiaoLongBaoZi”就是一个多播委托(因为封装了多个方法)
FindSthToEat xiaoLongBaoZi = new FindSthToEat(Rosen.WalkIn);
xiaoLongBaoZi += Rosen.SitDown;
xiaoLongBaoZi += Rosen.Thinking;
xiaoLongBaoZi += Rosen.OrderDish;
xiaoLongBaoZi += Rosen.Eat;
显然,当我们执行这个委托时,是先执行SitDown,再Thinking,再OrderDish,最后Eat(按照封装顺序执行)
前面我们说过,委托除了像方法一样调用以外还有其它调用方式,现在我们来介绍里面的两种:
所有委托都隐式继承了C#预定义的Delegate类,就像所有Class都继承了Object类一样。
从这个类中,任何委托的实例的成员中都有Invoke和BeginInvoke方法,通过委托实例名.Invoke()或委托实例名.BeginInvoke()可以调用这两个方法来执行委托。
二者的区别在于:Invoke()和直接用委托名+方法调用操作符“f(x)”一样,都是在主线程中对委托进行同步调用;而BeginInvoke会自动生成一个分线程,并在分线程中执行委托实例。
因为是“自动生成分线程”,这是一种隐式异步调用(如果要显式异步调用的话,就直接声明一个新的Thread或者Task然后把委托放进去再手动启动分线程)
刘铁猛老师在《C#入门详解》中给出了一个非常好的例子来直观的说明这种通过BeginInvoke方法造成的隐式异步调用:(省略了对Student和Teacher类的定义,各自有一个写作业和改作业的方法)
static void Main(string[] args)
{
//主线程启动,执行流从Main方法开始
Student s1 = new Student() {
ID = 1, penColor = ConsoleColor.Red};
Student s2 = new Student() {
ID = 2, penColor = ConsoleColor.Yellow};
Student s3 = new Student() {
ID = 3, penColor = ConsoleColor. Blue };
Action act1 = new Action(s1.DoHomeWork);
Action act2 = new Action(s2.DoHomeWork);
Action act3 = new Action(s3.DoHomeWork);
//隐式启动新的执行流act1(分线程)
act1.BeginInvoke(null,null);
//隐式启动新的执行流act2(分线程)
act2.BeginInvoke(null,null);
//隐式启动新的执行流act3(分线程)
act3.BeginInvoke(null,null);
//为主线程添加新的逻辑
for (int i = 1; i <= 5; i++)
{
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("Teacher has checken homework for {0} hours(s)",i);
Thread.Sleep(1000);
}
Console.ReadKey();
}
输出为:(颜色存在同步是因为不同线程争抢了Console.ForegroundColor这个系统资源,这里没有用进程锁来避免这种情况以免影响主线内容讲解)
众所周知,同样从C/C++语言发展出来的Java语言里根本没有委托这种东西,Java里接口取代了所有C#中委托与事件的作用,这是为什么呢?
因为委托易使用,难精通,功能还很强大,在滥用时很容易造成糟糕后果:
1>.作为方法级别的耦合,耦合性太强,很容易造成意料之外的错误
2>.可读性下降,Debug难度增加
3>.当委托回调,异步调用和多线程纠结在一起时,代码会非常难以阅读和维护
4>.委托使用不当容易造成内存泄漏和程序性能下降:当委托是一个实例方法时,内存中必须存在一个实例,且永远不能被清空,否则委托就无法正常调用了
呼.....其实委托的知识我们还没有讲完——记不记得第一课里我们说过,委托“可以作为一种约束条件来限制方法的调用”?
为了说明这点,我们得先介绍一下“事件”;这个比委托还要复杂的概念。
敬请期待~