热爱测试,主要研究性能测试和自动化测试方面的技术,希望与同样对测试有热情的你一同进步成长

形象解释回调函数的文章

上一篇 / 下一篇  2007-09-20 09:44:32 / 个人分类:软件开发

#k4{9F M7qU&A0  测试的软件中很多地方用到了回调函数,而对于我这个对开发一知半解的人来说,理解这个概念费了不少劲。今天总算在网上发现一篇文章,对回调函数的解释比较形象,现转贴出来(不过这个网址也是转的):51Testing软件测试网 JR$lgiP

51Testing软件测试网U0Fn ? M4V |~+G

出处:http://blog.csdn.net/nine425/archive/2007/02/10/1506982.aspx 51Testing软件测试网3N!t%Me1iD3Li f

51Testing软件测试网 K,g]&j7R Z2v q

内容:关于回调函数51Testing软件测试网'{NB,C$z5@,I

7?&H(z-rJ o8|i_5d8T0Q: 帮忙回答下面几个问题。谢谢,我对C++不熟,所以回答的问题请尽量避免C++程序。
CyD%mz Im051Testing软件测试网.Sv#\Ek0Iu
1)在编程过程中哪些程序是必须用回调函数才能解决的或者说用回调函数解决更好?51Testing软件测试网-esBy/OX ~8k2X
 请尽量说细些使用回调函数的原因。
3KD2}S&B051Testing软件测试网sE0qpg"[v8r?
2)我看网上一些例子.NET中一般用回调函数都会涉及到委托,那么委托和回调函数是什么关系?
0HMXx wrVn/N9W0  为什么要用委托。51Testing软件测试网t ?*A4d3S
51Testing软件测试网SFQng)q o9w
3)如果可以不用委托的话,那么用了委托有什么好处?
X5^9{EC%EU051Testing软件测试网6Mh2D%M7i Q:X*H
51Testing软件测试网Rx![:F%u"[!I
51Testing软件测试网:eaV1XD D&S1I+eS
A: 关于委托、事件以及一点点异步处理的内容,可以看看我这里的回帖
p%c"EvfE051Testing软件测试网SI P9aK'F4E t
(上一篇)
:?.v4Sk'vP+wGzM*s*T0
@ ]h8X9F I2D9Jy%}0---------51Testing软件测试网H-[$[Y%om

/K&? h(y V*D0现在就你的问题将回调函数和异步处理详细说说。51Testing软件测试网dHJP y'f3}
51Testing软件测试网#Y-C|'K [kR
如果你OOP编程经验足够的话,你就会有这么一个印象:对象一般来说都是被动的,外界可以通过调用它的方法,或者访问它的属性来告诉它发生了什么事情、需要怎么处理,但一般情况下对象不会主动去观察不属于它管辖范围的外界的事情。
&t~$v#p(K+A051Testing软件测试网!M6Nd"ER`+P|h'p
但是有时候的确需要有一个机制,让它可以了解到不属于它管辖范围的外界的事情。虽然可以通过无限循环观察达到这个目的,但是当然这将耗费很多资源。按照上一段所说的原则,对象最好就保持被动,由能管外界的其他对象主动告诉它发生了什么事情。51Testing软件测试网}(EM/Mz
51Testing软件测试网bZ0l~)lQ
一个生活化的例子:主管(一个对象)叫一个下属(另一个对象)打一份文件。主管需要知道下属什么时候打完,但是如果守在下属旁边等待工作完成,当然是很愚蠢很浪费自己时间的事情。所以,主管可以下达一个命令:“你打完了就告诉我。”这样,主管就可以做自己要做的工作,并在下属打文件这个过程中保持被动,由下属负责主动通知主管他打完文件。51Testing软件测试网]xs~9X2S_D

DMbIsu;q T0于是就出现“回调函数”的概念,用生活化的话来说,就是“当发生什么什么事情的时候,告诉我”这样的概念(回调=callback=反向的调用,不是自己调用其他对象的方法,而是让其他对象反过来调用我的方法)。使用的场合很多,但是由于名字的限制,一般都只狭义地指异步处理方法完成之后用来通知调用者的那个方法。
zc.^n*{u4s ?)Vw051Testing软件测试网9iT:I9gi[ w
来看一个异步处理的例子:向网络流写一个字节数组。
e+R0`$aLP n g(q051Testing软件测试网dP0_t t
NetworkStream.BeginWrite(byte[] data, int offset, int count,
$Y+K9k#S@0 --> AsyncCallback callback <-- , object state);
Q4Q6OiX:r051Testing软件测试网jtU&E hBG
与一般的写(Write(byte[], int, int))不同的地方是多了个回调函数参数和回调函数里可以使用的自己指定的任意对象。人性化之后,这个方法的意思就是“麻烦将这个数组里面从offset开始的count个字节发出去。做完之后告诉我,并且记得告诉我你在做哪个东西免得让我混淆”。51Testing软件测试网6@3s { i T}f#E
51Testing软件测试网T`9G7c Rp
网络流发送完要发送的内容之后,它就会调用这个作为参数传入的回调函数,并且将你提供的state对象原封不动交回给你。然后你调用 NetworkStream.EndWrite(...) 来正式结束这个异步处理过程。
0{FE]aP*N}O1Z3_051Testing软件测试网`T9Y5X+t H bE
除了异步处理,也有一些例外的情况会使用到“回调函数”,但是其实可以简单的理解为利用回调函数的方便性来达到不算回调的目的。例如在线程池中添加一个工作:51Testing软件测试网lf le4B
51Testing软件测试网})JJ"^ | r"Lc
ThreadPool.QueueUserWorkItem( --> WaitCallback callback <-- , object state);51Testing软件测试网\%[gwhRJ

P_2d0s/h0它的意思是“有线程空闲的时候,麻烦运行这个函数。”严格来说这不算“回调”,因为指定的函数可能是在别的对象上的方法,但是因为回调函数的概念“好用”,所以这里“滥用”了一下。
vT]/UfLb_ \0
,B'^!wb9?V;d0更加滥用的情况其实逻辑上也是可以的,因为回调函数其实就是委托。委托其实就是一个方法(函数)的“包装”,它可以将一个特征符合的方法包装成一个变量,变量传到哪里,哪里就能通过它运行所包装的方法。(解释“特征”:英文叫signature,中文翻译用了signature的另一个翻译——“签名”,但是意思是“特征”,指方法的参数列表和返回值。如果两个方法有相同的参数列表(参数名可以不同)和返回值,它们就有共同的特征)
Et^ l5B Mj'K~5U0
X M g\]x0委托的使用场合我在本回复开头的连接那里说过了,就是需要调用一个方法的时候,但是不知道具体该调用哪个对象的哪个方法的时候。用委托包装方法之后,“调用方法”就可以代替为“调用委托”,无须知道方法来自哪里。51Testing软件测试网v0W4EfR9K6s
51Testing软件测试网/V%s\ D,A
如果完全不用委托,理论上勉强能解决现在所有用了委托的情况,但是程序将变得非常复杂。就用网络流异步写数据的例子,如果不用委托的话,我猜大概可以这样:51Testing软件测试网V2N+gAu*lx1I
51Testing软件测试网/K\Izs$Bsj!d\
---<虚构>---
Shz @!\2}0调用者实现 IStreamWriteAsyncCallbackHandler 接口(流的写操作的异步回调处理者),该接口有一个 StreamWriteAsyncCallback 方法,当流完成写操作之后,找到它的调用者,类型转换为这个接口,然后调用这个回调方法:51Testing软件测试网/b['SX3q_d7`
((IStreamWriteAsyncCallbackHandler)caller).StreamWriteAsyncCallback(asyncResult);51Testing软件测试网,p'oL2?\:s4G Sh`,GR!I
---</虚构>---51Testing软件测试网 a#dG4tW3}
51Testing软件测试网wmt;|y5OVO8r
复杂度可见一斑。
(O3Xg f\6f051Testing软件测试网K,SO0yP+Xg({ I
有什么问题再提吧,我说得够多了 

r"Fe8W6CLs8B0

)RY,w3N"s+T0Q: “这就推断出委托的使用场合:当我们要调用某个方法,但是我们只知道这个方法该长什么样子而不知道具体从哪里获得的时候,就是使用委托的时候。”
Wl7uHFZz/zT051Testing软件测试网s#vo$o0v$i3R
为什么这么说?我们自己写的程序,哪个类里包含哪个程序难道我们不知道吗?51Testing软件测试网]s&iw7s
要想调用某个类下的函数实例化这个类调用就可以了,为什么还定义委托呢?51Testing软件测试网h!u"TYtBU4N g

$T d+I(q(M p$p-i6T2b7?@051Testing软件测试网H8vr![&B!ZI
51Testing软件测试网(R}`Y*K
A: 如果整个工程都是你自己写的,而且你设计的结构足够“好”,委托当然就变得不重要了
"[ e bM/l051Testing软件测试网fs(In\ [ J
但是——看看我说的网络流的例子。微软它们在做网络流这个类的时候,有办法知道你将会用什么对象来调用它的异步方法么?就算知道了,也有办法知道你这个对象公开的一堆方法里,哪个才是它做完工作之后需要调用的?51Testing软件测试网] { Yu-rU

{ m\\eah!S0我之前做过一个桌面应用程序工程,里面也用到了委托,原因是——我知道我要调用某类A的某方法B,但是问题是,类A的实例化不是我控制的,到我要调用方法B的时候,我不知道去哪里找类A的实例。这时候,委托也能派上用场——我叫类A在实例化的时候告诉我它的方法B在哪里。51Testing软件测试网[U B ALr?u

B|M4GV'V0上例可能显得有点不能理解——为什么不直接叫类A实例化的时候告诉我对象在哪里找?原因有2:我不仅仅要调用类A的方法B,我还要调用类B的方法B,类C的方法B...而且随着工程的扩充,还可能有我预料不到的类的出现——如果用保存对象的方法,我岂不是每次都要返回主工程修改代码?其次,这些方法B的特征都一模一样,也就是我定义个委托,就可以把现有的、未来的方法B都概括了。这时候,使用委托省掉我一大堆功夫。
8?0W E3J*U-U051Testing软件测试网sQ w5t$_8c
(当然,这种情况下用抽象类或者接口也能满足要求,而且从资源上看,大家都不相伯仲)51Testing软件测试网R+xF?0pgVe7` ]
51Testing软件测试网1Y%@ V OFW m!z
最后还有个例子,比较抽象,我尽量解释。
ps ^%w0h$h~/T051Testing软件测试网-{"y(C;EWMR4[L&r
有一段代码,你的工程里将会非常频繁的使用。说到这里,程序员的第一反应当然就是把这段代码写成函数了。问题是——这段代码正中大概有5%的部分要因应不同情况而有所变动,而且这些不同情况暂时无法预料,不能用 switch-case 语句处理。这时候,委托也能派上用场——将这段未知的代码抽象成一个有输入有输出的函数,也就是委托(输入+输出=特征),在调用这大段代码的时候提供委托。这样,这段代码就可以完成95%的工作,中间插入5%使用委托完成。
!yfF3wMzX0
-IE:w,`9j5M,S0可以看看 Windows Forms 中控件的描绘事件是怎么做的。事件其实就是委托的应用。
0J:DZ-m p@Ii9w1v051Testing软件测试网*I"A2I'ZEDy
(需要重绘的时候;Control.OnPaint(PaintEventArgs)方法里)51Testing软件测试网X,u;S#x.N
- 绘制基本的图形,例如填充底色、画控件的基本图案;51Testing软件测试网[c&W bcA
- 触发 Paint 事件,让已订阅的事件处理器(委托)进一步描绘;
G"R'Y.m:ra0- 描绘完成,关闭 Graphics 对象,释放资源。
(bt]6k7J*H1De#j0
7z X ])w7j0其中,第1行和第3行就是所谓的“95%的相同代码”,第2行就是“未知的5%的代码”,解决方法:委托(事件)。
,IP[v+B0u@ T0
E5qa5Rka$n?0
U)Q J3WE%ut#nx4k051Testing软件测试网c\)B$C'W.tb'ys;M
Q: 楼上的谢谢了。
d K4mm[;Z0我对下面的话有一些疑问。 如果 我写的公用函数 可以用 switch-case 语句处理一些不同点就是你说的那 5%,但是我又不想用switch-case 处理了,因为每当我发现一种新的情况产生我就得去改代码。如果我用委托是不是可以避免呢? 但是 “也就是委托(输入+输出=特征),”这句让我很困扰,我写的是一个公用函数,我怎么能让调用我这个函数的人所“输入” 特征呢?
'M7{nhw5e7H e-E0我还是不理解 这个“(输入+输出=特征)”能否给个易理解的例子呢?
8W-B }/J1e+zhH w0
7H `9I/{]4^051Testing软件测试网'^.p,O e?~Ab nH
A: 如果你看了我另一篇回复(另一个帖子),就可以知道什么叫“特征”,其实我是比较反对中文的“官方翻译”——签名,其实就是signature51Testing软件测试网!oDf{:? mm
51Testing软件测试网-o)^1^ A9q
所有方法都有一个参数列表(0到多个参数,有顺序)和一个返回值(void也算一个)。如果用数学的“函数”的概念去理解,参数列表就是这个函数期待的“输入”,返回值就是使用函数的人期待的“输出”51Testing软件测试网-_$] LG4vv

v!|3p4l%q FSl z m7c0委托定义的,恰好就是输入和输出。一个方法,只要输入和输出符合,就可以被这个委托包装成委托变量。
R2OP3B F0
:g2Cs j-L"_ln2H0一个大函数,从外界看来,就是一个接受一些输入,处理,然后输出结果的机器,但是如果深入函数内部去看的话,很可能可以将这个函数拆分成很多个小函数,这些函数呈线性排列,每个函数接收前一个函数的输出作为输入,然后处理,然后将结果输出给下一个函数作为输入。注意有时候输出不一定表示返回值,例如修改引用类型的输入,其实就是输出的一种了。其次,输入还不局限于相邻的前面一个函数的输出,任何位于前面的输出都可以作为当前函数的输入。51Testing软件测试网xn#Ar#s0R1M]G#x
51Testing软件测试网7cBoUEaE
一个5%代码未知的大函数,也可以用同样的方法拆成小函数的线性排列,那5%很可能就刚好被拆分成一个小函数整体(或者多个整体)。这样就知道该怎么定义这个委托了。
D,cA(xiw{0
:d}-IY"b0又用回控件重绘的例子吧。从外界看,控件重绘的其中一种情形(完全重绘)不需要任何输入和输出。它的流程大概是:51Testing软件测试网OCaX#vxNxp
51Testing软件测试网'E7V,\-N J_k&WN
创建画布51Testing软件测试网@)t)YG0vJj2bFn
V
&_Ir#J'm0在画布上绘制控件的基本样貌51Testing软件测试网r4PUTL:c,T Z
V51Testing软件测试网#}%W/c*R`-p;w,T.tg%S
在画布上绘制自定义的细节(未知)
1r g2E3`~kU0V51Testing软件测试网 c+a!RlGe }z
关闭画布51Testing软件测试网.a9pEn0a2`8K&E

,_l8MTM2a e8_Z0这么一分,这个流程就很明显被分成4个小函数了:51Testing软件测试网c6~$@f1q

fw8I"IXX s0(输入:无)创建画布(输出:画布)
2Fr9`3T8NA0           v51Testing软件测试网XI2`G*s b
(输入:画布)在画布上绘制控件的基本样貌(输出:画布)
:b)O-pK$@ ?3{2j0           v
]7r7F+FY'f#ES0KY0(输入:画布)在画布上绘制控件的自定义细节(输出:画布)
/B7hZ O$LU0           v
X7fU+S]0(输入:画布)关闭画布(输出:无)
0D-C@@c.c9W k%o0
NCv*? ] kom0于是,未知的第3步——画细节的那步,就可以定义为:51Testing软件测试网7sgwyD\_W.^
51Testing软件测试网c%@,W-n Q1p
public delegate void/*无返回值*/ PaintDetailsDelegate(Graphics g)/*输入:画布*/
;SZQL0B7o5z S051Testing软件测试网 HR1Y#g;cC-z&]5H L
为什么无返回值?不是要输出画布吗?因为画布是引用类型,在画布上修改就可以达到输出的目的了。51Testing软件测试网 G,q$P!P2uB3b@
51Testing软件测试网]jiQh3t ~w&w9w
现在知道代替5%的函数的样子,就要找个方法将它获取进来。很简单,就在大函数的入口处(参数列表)加一个委托参数就行了。或者还可以预先提供放置该参数的地方(例如某个静态可写公共字段),到需要的时候直接引用。
Q1Lf E,D;L^|0
eRZ2s*i"Qc `iF7P0---------------51Testing软件测试网;P7[cZ'F X\o4P
51Testing软件测试网'al"S"z\$| mV:vr B \
A: 想了一下,想出个更容易理解的例子。
/I%jV9y7MP?0
gw&Q h wlZ0例如有一个方法,它的作用是从来源A获取信息然后发布到目标B,但是它不会处理信息。它的工作是这个样子的:
/J6u!J+ZEU~@051Testing软件测试网fd]0q9SJ@ Ly
while (还没下班) {
c5UB4]rR4n8y0 // 获取信息51Testing软件测试网 `K1\c*o&Bw!F9r _
 // 获取信息51Testing软件测试网} ?Iy5Q;XH^
 // 获取信息51Testing软件测试网$pzsJ9?/X;Ot
 // 获取信息 。。。
'G'S'W5U~n QW1`0
T'`pG-rS0 // 处理信息——我不知该怎么处理51Testing软件测试网 f#Y%j@syk1aT
51Testing软件测试网rx~'Ni9^O
 // 发布信息51Testing软件测试网/^3xo!U^*b?6q
 // 发布信息
\-n'G1Q4P lB0 // 发布信息
u'wh1To|0 // 发布信息 。。。51Testing软件测试网`+n/s8v$pT
}
H:sC[)f6T0
kb!P8B-UO$?0它就可以将处理信息那里抽象成一个委托,这个委托的输入是获取回来的信息,输出是经过处理可以发布的信息:
KrFF'H3rPLk0public delegate string/*输出*/ ProcessInfoDelegate(string rawMessage)/*输入*/
;_0{@o@y051Testing软件测试网D9BH^f tae
这样,方法就可以写成
X G6fIs051Testing软件测试网\t:`"r`_ VGW
public void TransferInfo(X source, ProcessInfoDelegate how, Y destination)51Testing软件测试网G4es&QI C
{
q lGsqIA WNA0 string msg;51Testing软件测试网 }zx T1UeMU
 // ...
kQA7o8X*MN|0 // 获取信息中
h3cN-v8aL#k0 // ...
N v.DX-K6g k+N'g051Testing软件测试网T4G[`&m(I
 msg = how(msg); // 委托别人做信息处理工作51Testing软件测试网'l4S GXs8S*f

$G!mAt\u1V0 // ...
q g~{q,M6R_#d`a0 // 发布信息中51Testing软件测试网b] pZg3{
 // ...51Testing软件测试网 ZN w?-W3e k0_
}
^+ZR0lJ5Q-~051Testing软件测试网 PY8Ai;\
那么,如果有一个类需要用这个方便的方法,它就先定义它期待的信息处理方法,例如51Testing软件测试网3G Rv)^-k oV*gg
class User51Testing软件测试网%eIL kEB'gN
{51Testing软件测试网4`&hO5wC Y#GB m,J
 private string MyProcessor(string input)
`Jlq-p"d$E0 {
B,a V*c4P1k Ed0    return input.Substring(2); // 不要最前面两个字符
8n:q)z`O x#@0 }
I[TUq3w-KS y$P*\U)n@051Testing软件测试网S4Ja"U3~"Z
然后调用方法:
)L0Bm/ozC4e"~.w } U0 public void Do()51Testing软件测试网\dv}N$H3Jd:mg
 {51Testing软件测试网1kqxy$nIW
    ....51Testing软件测试网_5?8FC!t&`#Q
    ClassName.TransferInfo(src,51Testing软件测试网IQtO3Y0~` H
      new ProcessInfoDelegate(MyProcessor), // 包装委托变量
-W_k@M4R*W0      dest);
-T'_q*k0KB6b1G0 }
3|X%au3o3]0}

_4\D$XxK%F0

TAG: 软件开发

 

评分:0

我来说两句

Open Toolbar