原文:http://www.cnblogs.com/lsxqw2004/archive/2008/12/17/1356705.html
不像Windows API中使用C语言风格的函数指针这种不安全的方式进行回调。.Net中此功能使用使用更为安全和面向对象的委托(delegate)来完成。委托是一个类型安全的对象,它指向程序中另一个以后会被调用的方法(或多个方法)。
委托类型包含3个重要信息:
-
它所调用的方法的名称
-
该方法的参数(可选)
-
该方法的返回值(可选)
当上述信息被提供后,委托可以在运行时动态调用其指向的方法。很重要的一点:.Net中每个委托都被自动赋予同步或异步访问方法的能力。
定义委托
在C#中使用delegate关键字创建一个委托。我们称这种类为委托类。委托类的实例成为委托对象。从概念上说,委托对象是一种指向一个或多个方法(静态或非静态)的引用。要求是此委托匹配它指向的方法的签名。
如下委托可以指向一任何传入两个整数返回一个整数的方法。
1
|
public
delegate
int
BinaryOp
(
int
x
,
int
y
)
;
|
-
定义委托后,系统生成一个派生自MulticastDelegate类的密封类。此类中有3个方法:
-
Invoke()方法,用来以同步方式调用委托维护的每个方法。(不能在C#中显示调用此方法,Invoke()在后台被调用)
-
BeginInvoke()与EndInvoke()方法在第二个线程上异步调用当前方法。
开发人员创建第二个执行线程的原因调用比较耗时的方法。(相当于委托顺带实现了一些System.Threading命名空间管理的线程问题)
这个委托的密封类大概如下:
1
2
3
4
5
6
7
|
sealed
class
BinaryOp
:
System
.
MulticastDelegate
{
public
BinaryOp
(
object
target
,
uint
functionAddress
)
;
public
int
Invoke
(
int
x
,
int
y
)
;
public
IAsyncResult
BeginInvoke
(
int
x
,
int
y
,
AsyncCallback
cb
,
object
state
)
;
public
int
EndInvoke
(
IAsyncResult
result
)
;
}
|
下面给一个简单的委托示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
// 这个委托指向任何一个传入两个整数并返回一个整数的方法
public
delegate
int
BinaryOp
(
int
x
,
int
y
)
;
#region SimpleMath class
public
class
SimpleMath
{
public
int
Add
(
int
x
,
int
y
)
{
return
x
+
y
;
}
public
int
Subtract
(
int
x
,
int
y
)
{
return
x
-
y
;
}
public
static
int
SquareNumber
(
int
a
)
{
return
a
*
a
;
}
}
#endregion
class
Program
{
static
void
Main
(
string
[
]
args
)
{
Console
.
WriteLine
(
"***** Simple Delegate Example *****n"
)
;
// 创建一个指向SimpleMath.Add()方法的BinaryOp对象
SimpleMath
m
=
new
SimpleMath
(
)
;
BinaryOp
b
=
new
BinaryOp
(
m
.
Add
)
;
// 使用委托调用调用Add()方法
// 此处也是Invoke()被调用的位置
Console
.
WriteLine
(
"n10 + 10 is {0}"
,
b
(
10
,
10
)
)
;
Console
.
ReadLine
(
)
;
}
}
|
委托类型安全的体现
如果传入一个与委托声明不匹配的方法,将在编译时报错。如上例中如果传入int SquareNumber(int),将会导致一个编译时错误。
获取委托中调用函数列表的方法,示例:
假设有名为delObj的委托对象,使用如下方式得到调用函数的信息
1
2
3
|
Delegate
d
in
delObj
.
GetInvocationList
(
)
Console
.
WriteLine
(
"Method Name: {0}"
,
d
.
Method
)
;
Console
.
WriteLine
(
"Target Name: {0}"
,
d
.
Target
)
;
|
Method属性表示调用的函数的签名,Target表示调用的函数所在的对象的类型名,所以如果委托调用的是一个静态方法则Target不会有任何显示,只有当委托调用的是一个实例方法时,Target属性才有值。
更完整的委托应用(示例来自C#与.Net3.0高级程序设计),代码:
汽车类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
|
using
System
;
using
System
.
Collections
.
Generic
;
using
System
.
Text
;
namespace
CarDelegate
{
public
class
Car
{
// 定义委托类型
public
delegate
void
AboutToBlow
(
string
msg
)
;
public
delegate
void
Exploded
(
string
msg
)
;
// 定义各自委托类型的对象
private
AboutToBlow
almostDeadList
;
private
Exploded
explodedList
;
// 将成员添加到调用列表
public
void
OnAboutToBlow
(
AboutToBlow
clientMethod
)
{
almostDeadList
+=
clientMethod
;
}
public
void
OnExploded
(
Exploded
clientMethod
)
{
explodedList
+=
clientMethod
;
}
// 由调用列表移除方法
public
void
RemoveAboutToBlow
(
AboutToBlow
clientMethod
)
{
almostDeadList
-=
clientMethod
;
}
public
void
RemoveExploded
(
Exploded
clientMethod
)
{
explodedList
-=
clientMethod
;
}
// 内部状态成员
private
int
currSpeed
;
private
int
maxSpeed
;
private
string
petName
;
// 汽车坏了吗?
bool
carIsDead
;
public
Car
(
)
{
maxSpeed
=
100
;
}
public
Car
(
string
name
,
int
max
,
int
curr
)
{
currSpeed
=
curr
;
maxSpeed
=
max
;
petName
=
name
;
}
public
void
SpeedUp
(
int
delta
)
{
// 如果汽车坏了,触发Exploded事件
if
(
carIsDead
)
{
if
(
explodedList
!=
null
)
explodedList
(
"Sorry, this car is dead"
)
;
}
else
{
currSpeed
+=
delta
;
// 几乎要坏了?
if
(
10
==
maxSpeed
-
currSpeed
&&
almostDeadList
!=
null
)
{
almostDeadList
(
"Careful buddy! Gonna blow!"
)
;
}
// 还好!
if
(
currSpeed
>=
maxSpeed
)
carIsDead
=
true
;
else
Console
.
WriteLine
(
"->CurrSpeed = {0}"
,
currSpeed
)
;
}
}
}
}
|
主函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
namespace
CarDelegate
{
class
Program
{
static
void
Main
(
string
[
]
args
)
{
// 制造一辆车
Car
c1
=
new
Car
(
"SlugBug"
,
100
,
10
)
;
// 注册事件处理函数
Car
.
Exploded
d
=
new
Car
.
Exploded
(
CarExploded
)
;
c1
.
OnAboutToBlow
(
new
Car
.
AboutToBlow
(
CarIsAlmostDoomed
)
)
;
c1
.
OnAboutToBlow
(
new
Car
.
AboutToBlow
(
CarAboutToBlow
)
)
;
c1
.
OnExploded
(
d
)
;
// 加速 (这将触发事件)
Console
.
WriteLine
(
"n***** 加速 *****"
)
;
for
(
int
i
=
0
;
i
<
6
;
i
++
)
c1
.
SpeedUp
(
20
)
;
// 由调用列表移除CarExploded方法
c1
.
RemoveExploded
(
d
)
;
Console
.
WriteLine
(
"n***** 加速 *****"
)
;
for
(
int
i
=
0
;
i
<
6
;
i
++
)
c1
.
SpeedUp
(
20
)
;
Console
.
ReadLine
(
)
;
}
public
static
void
CarAboutToBlow
(
string
msg
)
{
Console
.
WriteLine
(
msg
)
;
}
public
static
void
CarIsAlmostDoomed
(
string
msg
)
{
Console
.
WriteLine
(
"Critical Message from Car: {0}"
,
msg
)
;
}
public
static
void
CarExploded
(
string
msg
)
{
Console
.
WriteLine
(
msg
)
;
}
}
}
|
对多路广播的支持
.Net委托内置多路,即一个委托可以维护一个可调用方法的列表而不只是单独一个方法,使用重载过的+=运算符可以向一个委托对象添加多个方法。关于对对路广播的支持可以参考上述示例。
在多路广播的支持中有一个需要注意的问题,一个委托调用的多个方法需要无参数且无返回值,因为在调用委托时,即使传入了参数也不知道具体应该传给哪一个方法,即使这些方法有返回值也不知道该接受那个函数的返回值。所以说直接不要调用有参数及返回值的方法,这点与事件关联多个事件处理方法时对处理方法签名的要求相同(可以参见本系列介绍事件的文章)。
注意:我们可以用调用方法的语法”调用”委托对象。这样会调用委托对象所引用的方法。(事件的触发与委托的调用相同,本来事件就是一个委托类型的对象)。这些方法的调用是在调用委托的方法所在的线程中完成的。这种调用称同步调用。
C#2.0编译器的委托类推测功能
C#编译器引入了在创建委托变量时可以推测其类型的能力。这样就可以将一个方法赋给隐式创建的委托对象。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public
class
Program
{
delegate
void
Deleg1
(
)
;
delegate
string
Deleg2
(
string
s
)
;
static
void
f1
(
)
{
System
.
Console
.
WriteLine
(
"f1() called."
)
;
}
static
string
f2
(
string
s
)
{
string
_s
=
string
.
Format
(
"f2() called with the param "
{
0
}
"."
,
s
)
;
System
.
Console
.
WriteLine
(
_s
)
;
return
_s
;
}
public
static
void
Main
(
)
{
Deleg1
d1
=
f1
;
// 代替 Deleg1 d1 = new Deleg1( f1 );
d1
(
)
;
Deleg2
d2
=
f2
;
// 代替 Deleg2 d2 = new Deleg2( f2 );
string
s
=
d2
(
"hello"
)
;
}
}
|
委托协变(covariance)
允许创建一个委托,其返回的对象的类型是继承关系的,示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
// 简单的集成关系的两个类
class
Car
{
public
override
string
ToString
(
)
{
return
"A stateless car"
;
}
}
class
SportsCar
:
Car
{
public
override
string
ToString
(
)
{
return
"A stateless sports car"
;
}
}
class
Program
{
// 定义一个返回Car或SportsCar的委托
public
delegate
Car
ObtainVehicalDelegate
(
)
;
// 委托指向目标
public
static
Car
GetBasicCar
(
)
{
return
new
Car
(
)
;
}
public
static
SportsCar
GetSportsCar
(
)
{
return
new
SportsCar
(
)
;
}
static
void
Main
(
string
[
]
args
)
{
ObtainVehicalDelegate
targetA
=
new
ObtainVehicalDelegate
(
GetBasicCar
)
;
Car
c
=
targetA
(
)
;
Console
.
WriteLine
(
c
)
;
// 协变允许指定这样的目标方法
ObtainVehicalDelegate
targetB
=
new
ObtainVehicalDelegate
(
GetSportsCar
)
;
SportsCar
sc
=
(
SportsCar
)
targetB
(
)
;
Console
.
WriteLine
(
sc
)
;
Console
.
ReadLine
(
)
;
}
}
|
委托逆变,其中参数具有集成关系,委托签名的参数类型(派生类型)比方法具有的参数类型(基类型)更具体。定义一个参数类型是派生类型的委托,这个委托可以接收具有基类型参数的方法,因为派生类型隐式转换成了基类型。注意此方法必须接收与委托签名相同的参数类型(派生类型),虽然方法的签名中参数是基类型。
接下来说一下委托在多线程程序中的应用,主角有两个:ThreadStart和ParameterizedThreadStart,它们都定义与System.Threading命名空间下。
使用这两个委托,你可以以编程方式创建此线程来分担一些任务,步骤如下:
-
创建一个方法作为新线程的入口点。
-
创建一个ParameterizedThreadStart(或ThreadStart)委托,并把之前所定义的方法传给委托的构造函数。
-
创建一个Thread对象,并把ParameterizedThreadStart或ThreadStart委托作为构造函数的参数。
-
建立任意初始化线程的特性(名称、优先级等)。
-
调用Thread.Start()方法。
完成上述步骤,在第2步建立的委托所指向的方法将在线程中尽快开始执行。
ThreadStart委托指向一个没有参数、无返回值的方法,它在调用一个被设计用来仅仅在后台运行、而没有更多的交互时非常有用。它的局限在于无法给这个函数出入参数,所以在.Net2.0中出现了ParameterizedThreadStart了方法,它可以接受一个包含了任意个数的参数(传给它要调用的方法的)的Object类型对象做参数(即允许用户为新线程要执行的方法传入一个对象作为参数)。但注意这两种委托指向的函数的返回值都必须是void。
看看示例代码,首先是ThreadStart委托的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public
class
Printer
{
public
void
PrintNumbers
(
)
{
//具体实现省略
}
}
class
Program
{
static
void
Main
(
string
[
]
args
)
{
Printer
p
=
new
Printer
(
)
;
Thread
bgroundThread
=
new
Thread
(
new
ThreadStart
(
p
.
PrintNumbers
)
)
;
// 控制此线程是否在后台运行?
bgroundThread
.
IsBackground
=
true
;
bgroundThread
.
Start
(
)
;
}
}
|
接下来的代码示例了ParameterizedThreadStart委托的使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
//包装参数的类
class
AddParams
{
public
int
a
;
public
int
b
;
public
AddParams
(
int
numb1
,
int
numb2
)
{
a
=
numb1
;
b
=
numb2
;
}
}
class
Program
{
static
void
Main
(
string
[
]
args
)
{
Console
.
WriteLine
(
"主线程ID:{0}"
,
Thread
.
CurrentThread
.
GetHashCode
(
)
)
;
//生成要传入的参数
AddParams
ap
=
new
AddParams
(
10
,
10
)
;
Thread
t
=
new
Thread
(
new
ParameterizedThreadStart
(
Add
)
)
;
t
.
Start
(
ap
)
;
}
//ParameterizedThreadStart委托调用的方法
//使用AddParams类对象做参数
public
static
void
Add
(
object
data
)
{
if
(
data
is
AddParams
)
{
Console
.
WriteLine
(
"后台线程ID: {0}"
,
Thread
.
CurrentThread
.
GetHashCode
(
)
)
;
AddParams
ap
=
(
AddParams
)
data
;
Console
.
WriteLine
(
"{0} + {1} is {2}"
,
ap
.
a
,
ap
.
b
,
ap
.
a
+
ap
.
b
)
;
}
}
}
|
详细通过上面两段简单的代码示例,你已经对ThreadStart和ParameterizedThreadStart的使用有了全面的了解。
另外有一点需要说的,有些情况下可以省略这个委托对象的构造,即构造Thread对象时,直接向Thread的构造函数传入一个方法的名称,而不用先构造一个委托的对象。传入的方法既可以是静态方法也可以是实例方法。
另外委托在异步编程中的作用见异步编程的文章
参考资料:
C#与.Net3.0高级程序设计
C#与.Net2.0实战
CLR via C# 第二版