关于回调函数

Q: 帮忙回答下面几个问题。谢谢,我对C++不熟,所以回答的问题请尽量避免C++程序。

1)在编程过程中哪些程序是必须用回调函数才能解决的或者说用回调函数解决更好?
 请尽量说细些使用回调函数的原因。

2)我看网上一些例子.NET中一般用回调函数都会涉及到委托,那么委托和回调函数是什么关系?
  为什么要用委托。

3)如果可以不用委托的话,那么用了委托有什么好处?



A: 关于委托、事件以及一点点异步处理的内容,可以看看我这里的回帖

(上一篇)

---------

现在就你的问题将回调函数和异步处理详细说说。

如果你OOP编程经验足够的话,你就会有这么一个印象:对象一般来说都是被动的,外界可以通过调用它的方法,或者访问它的属性来告诉它发生了什么事情、需要怎么处理,但一般情况下对象不会主动去观察不属于它管辖范围的外界的事情。

但是有时候的确需要有一个机制,让它可以了解到不属于它管辖范围的外界的事情。虽然可以通过无限循环观察达到这个目的,但是当然这将耗费很多资源。按照上一段所说的原则,对象最好就保持被动,由能管外界的其他对象主动告诉它发生了什么事情。

一个生活化的例子:主管(一个对象)叫一个下属(另一个对象)打一份文件。主管需要知道下属什么时候打完,但是如果守在下属旁边等待工作完成,当然是很愚蠢很浪费自己时间的事情。所以,主管可以下达一个命令:“你打完了就告诉我。”这样,主管就可以做自己要做的工作,并在下属打文件这个过程中保持被动,由下属负责主动通知主管他打完文件。

于是就出现“回调函数”的概念,用生活化的话来说,就是“当发生什么什么事情的时候,告诉我”这样的概念(回调=callback=反向的调用,不是自己调用其他对象的方法,而是让其他对象反过来调用我的方法)。使用的场合很多,但是由于名字的限制,一般都只狭义地指异步处理方法完成之后用来通知调用者的那个方法。

来看一个异步处理的例子:向网络流写一个字节数组。

NetworkStream.BeginWrite(byte[] data, int offset, int count,
 --> AsyncCallback callback <-- , object state);

与一般的写(Write(byte[], int, int))不同的地方是多了个回调函数参数和回调函数里可以使用的自己指定的任意对象。人性化之后,这个方法的意思就是“麻烦将这个数组里面从offset开始的count个字节发出去。做完之后告诉我,并且记得告诉我你在做哪个东西免得让我混淆”。

网络流发送完要发送的内容之后,它就会调用这个作为参数传入的回调函数,并且将你提供的state对象原封不动交回给你。然后你调用 NetworkStream.EndWrite(...) 来正式结束这个异步处理过程。

除了异步处理,也有一些例外的情况会使用到“回调函数”,但是其实可以简单的理解为利用回调函数的方便性来达到不算回调的目的。例如在线程池中添加一个工作:

ThreadPool.QueueUserWorkItem( --> WaitCallback callback <-- , object state);

它的意思是“有线程空闲的时候,麻烦运行这个函数。”严格来说这不算“回调”,因为指定的函数可能是在别的对象上的方法,但是因为回调函数的概念“好用”,所以这里“滥用”了一下。

更加滥用的情况其实逻辑上也是可以的,因为回调函数其实就是委托。委托其实就是一个方法(函数)的“包装”,它可以将一个特征符合的方法包装成一个变量,变量传到哪里,哪里就能通过它运行所包装的方法。(解释“特征”:英文叫signature,中文翻译用了signature的另一个翻译——“签名”,但是意思是“特征”,指方法的参数列表和返回值。如果两个方法有相同的参数列表(参数名可以不同)和返回值,它们就有共同的特征)

委托的使用场合我在本回复开头的连接那里说过了,就是需要调用一个方法的时候,但是不知道具体该调用哪个对象的哪个方法的时候。用委托包装方法之后,“调用方法”就可以代替为“调用委托”,无须知道方法来自哪里。

如果完全不用委托,理论上勉强能解决现在所有用了委托的情况,但是程序将变得非常复杂。就用网络流异步写数据的例子,如果不用委托的话,我猜大概可以这样:

---<虚构>---
调用者实现 IStreamWriteAsyncCallbackHandler 接口(流的写操作的异步回调处理者),该接口有一个 StreamWriteAsyncCallback 方法,当流完成写操作之后,找到它的调用者,类型转换为这个接口,然后调用这个回调方法:
((IStreamWriteAsyncCallbackHandler)caller).StreamWriteAsyncCallback(asyncResult);
---</虚构>---

复杂度可见一斑。

有什么问题再提吧,我说得够多了 

Q: “这就推断出委托的使用场合:当我们要调用某个方法,但是我们只知道这个方法该长什么样子而不知道具体从哪里获得的时候,就是使用委托的时候。”

为什么这么说?我们自己写的程序,哪个类里包含哪个程序难道我们不知道吗?
要想调用某个类下的函数实例化这个类调用就可以了,为什么还定义委托呢?



A: 如果整个工程都是你自己写的,而且你设计的结构足够“好”,委托当然就变得不重要了

但是——看看我说的网络流的例子。微软它们在做网络流这个类的时候,有办法知道你将会用什么对象来调用它的异步方法么?就算知道了,也有办法知道你这个对象公开的一堆方法里,哪个才是它做完工作之后需要调用的?

我之前做过一个桌面应用程序工程,里面也用到了委托,原因是——我知道我要调用某类A的某方法B,但是问题是,类A的实例化不是我控制的,到我要调用方法B的时候,我不知道去哪里找类A的实例。这时候,委托也能派上用场——我叫类A在实例化的时候告诉我它的方法B在哪里。

上例可能显得有点不能理解——为什么不直接叫类A实例化的时候告诉我对象在哪里找?原因有2:我不仅仅要调用类A的方法B,我还要调用类B的方法B,类C的方法B...而且随着工程的扩充,还可能有我预料不到的类的出现——如果用保存对象的方法,我岂不是每次都要返回主工程修改代码?其次,这些方法B的特征都一模一样,也就是我定义个委托,就可以把现有的、未来的方法B都概括了。这时候,使用委托省掉我一大堆功夫。

(当然,这种情况下用抽象类或者接口也能满足要求,而且从资源上看,大家都不相伯仲)

最后还有个例子,比较抽象,我尽量解释。

有一段代码,你的工程里将会非常频繁的使用。说到这里,程序员的第一反应当然就是把这段代码写成函数了。问题是——这段代码正中大概有5%的部分要因应不同情况而有所变动,而且这些不同情况暂时无法预料,不能用 switch-case 语句处理。这时候,委托也能派上用场——将这段未知的代码抽象成一个有输入有输出的函数,也就是委托(输入+输出=特征),在调用这大段代码的时候提供委托。这样,这段代码就可以完成95%的工作,中间插入5%使用委托完成。

可以看看 Windows Forms 中控件的描绘事件是怎么做的。事件其实就是委托的应用。

(需要重绘的时候;Control.OnPaint(PaintEventArgs)方法里)
- 绘制基本的图形,例如填充底色、画控件的基本图案;
- 触发 Paint 事件,让已订阅的事件处理器(委托)进一步描绘;
- 描绘完成,关闭 Graphics 对象,释放资源。

其中,第1行和第3行就是所谓的“95%的相同代码”,第2行就是“未知的5%的代码”,解决方法:委托(事件)。



Q: 楼上的谢谢了。
我对下面的话有一些疑问。 如果 我写的公用函数 可以用 switch-case 语句处理一些不同点就是你说的那 5%,但是我又不想用switch-case 处理了,因为每当我发现一种新的情况产生我就得去改代码。如果我用委托是不是可以避免呢? 但是 “也就是委托(输入+输出=特征),”这句让我很困扰,我写的是一个公用函数,我怎么能让调用我这个函数的人所“输入” 特征呢?
我还是不理解 这个“(输入+输出=特征)”能否给个易理解的例子呢?


A: 如果你看了我另一篇回复(另一个帖子),就可以知道什么叫“特征”,其实我是比较反对中文的“官方翻译”——签名,其实就是signature

所有方法都有一个参数列表(0到多个参数,有顺序)和一个返回值(void也算一个)。如果用数学的“函数”的概念去理解,参数列表就是这个函数期待的“输入”,返回值就是使用函数的人期待的“输出”

委托定义的,恰好就是输入和输出。一个方法,只要输入和输出符合,就可以被这个委托包装成委托变量。

一个大函数,从外界看来,就是一个接受一些输入,处理,然后输出结果的机器,但是如果深入函数内部去看的话,很可能可以将这个函数拆分成很多个小函数,这些函数呈线性排列,每个函数接收前一个函数的输出作为输入,然后处理,然后将结果输出给下一个函数作为输入。注意有时候输出不一定表示返回值,例如修改引用类型的输入,其实就是输出的一种了。其次,输入还不局限于相邻的前面一个函数的输出,任何位于前面的输出都可以作为当前函数的输入。

一个5%代码未知的大函数,也可以用同样的方法拆成小函数的线性排列,那5%很可能就刚好被拆分成一个小函数整体(或者多个整体)。这样就知道该怎么定义这个委托了。

又用回控件重绘的例子吧。从外界看,控件重绘的其中一种情形(完全重绘)不需要任何输入和输出。它的流程大概是:

创建画布
V
在画布上绘制控件的基本样貌
V
在画布上绘制自定义的细节(未知)
V
关闭画布

这么一分,这个流程就很明显被分成4个小函数了:

(输入:无)创建画布(输出:画布)
           v
(输入:画布)在画布上绘制控件的基本样貌(输出:画布)
           v
(输入:画布)在画布上绘制控件的自定义细节(输出:画布)
           v
(输入:画布)关闭画布(输出:无)

于是,未知的第3步——画细节的那步,就可以定义为:

public delegate void/*无返回值*/ PaintDetailsDelegate(Graphics g)/*输入:画布*/

为什么无返回值?不是要输出画布吗?因为画布是引用类型,在画布上修改就可以达到输出的目的了。

现在知道代替5%的函数的样子,就要找个方法将它获取进来。很简单,就在大函数的入口处(参数列表)加一个委托参数就行了。或者还可以预先提供放置该参数的地方(例如某个静态可写公共字段),到需要的时候直接引用。

---------------

A: 想了一下,想出个更容易理解的例子。

例如有一个方法,它的作用是从来源A获取信息然后发布到目标B,但是它不会处理信息。它的工作是这个样子的:

while (还没下班) {
 // 获取信息
 // 获取信息
 // 获取信息
 // 获取信息 。。。

 // 处理信息——我不知该怎么处理

 // 发布信息
 // 发布信息
 // 发布信息
 // 发布信息 。。。
}

它就可以将处理信息那里抽象成一个委托,这个委托的输入是获取回来的信息,输出是经过处理可以发布的信息:
public delegate string/*输出*/ ProcessInfoDelegate(string rawMessage)/*输入*/

这样,方法就可以写成

public void TransferInfo(X source, ProcessInfoDelegate how, Y destination)
{
 string msg;
 // ...
 // 获取信息中
 // ...

 msg = how(msg); // 委托别人做信息处理工作

 // ...
 // 发布信息中
 // ...
}

那么,如果有一个类需要用这个方便的方法,它就先定义它期待的信息处理方法,例如
class User
{
 private string MyProcessor(string input)
 {
    return input.Substring(2); // 不要最前面两个字符
 }

然后调用方法:
 public void Do()
 {
    ....
    ClassName.TransferInfo(src,
      new ProcessInfoDelegate(MyProcessor), // 包装委托变量
      dest);
 }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值