委托概述

委托和类一样,是一种用户定义类型。但类表示的是数据和方法的集合,而委托则持有一个或多个方法,以及一系列预定义操作。
委托相当于是一个类型安全的、面向对象的C++函数指针。
可以把delegate看作一个包含有序方法列表的对象,这些方法具有相同的签名和返回类型,如下图所示。

  • 方法的列表称为调用列表。
  • 委托持有的方法可以来自任何类或结构,只要它们在下面两方面匹配:
    • 委托的返回类型;
    • 委托的签名(包括refout修饰符)。
  • 调用列表中的方法可以是实例方法也可以是静态方法。
  • 在调用委托的时候,会执行其调用列表中的所有方法。

    ![[csharp-11-delegate-liebiao.png]]
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
delegate void MyDel(int value);  //声明委托类型

class Program
{
void PrintLow( int value )
{
Console.WriteLine($"{ value } - Low Value");
}

void PrintHigh( int value )
{
Console.WriteLine($"{ value } - High Value");
}

static void Main( )
{
Program program = new Program();

MyDel del; //声明委托变量.

//创建随机整数生成器对象,并得到0到99之间的一个随机数
Random rand = new Random();
int randomValue = rand.Next( 99 );

//创建一个包含PrintLow或PrintHigh的委托对象并将其赋值给del变量
del = randomValue < 50
? new MyDel( program.PrintLow )
: new MyDel( program.PrintHigh );

del( randomValue ); //执行委托
}
}
1
28 - Low Value

声明委托类型

与类一样,委托类型必须在被用来创建变量以及类型的对象之前声明。
委托类型的声明看上去与方法的声明很相似,有返回类型签名。返回类型和签名指定了委托接受的方法的形式。
委托类型声明在两个方面与方法声明不同。委托类型声明:

  • delegate关键字开头;
  • 没有方法主体。
1
2
3
4
5
关键字    委托类型名称
↓ ↓
delegate void MyDel( int x );
↑ ↑
返回类型 签名

该声明指定了MyDel类型的委托只会接受不返回值并且有单个int参数的方法。

创建委托对象

委托是引用类型,因此有引用和对象。在委托类型声明之后,我们可以声明变量并创建类型的对象。

1
2
3
委托类型 变量
↓ ↓
MyDel delVar;

有两种创建委托对象的方式,第一种是使用带new运算符的对象创建表达式。

  • 委托类型名。
  • 一组圆括号,其中包含作为调用列表中第一个成员的方法的名称。该方法可以是实例方法或静态方法。
1
2
3
4
5
6
                实例方法

delVar = new MyDel( myInstObj.MyM1 ); //创建委托并保存引用
dVar = new MyDel( SClass.OtherM2 ); //创建委托并保存引用

静态方法

第二种是快捷语法,它仅由方法说明符构成,这段代码和之前的代码是等价的。这种快捷语法能够工作是因为在方法名称和其相应的委托类型之间存在隐式转换。

1
2
delVar = myInstObj.MyM1;    //创建委托并保存引用
dVar = SClass.OtherM2; //创建委托并保存引用

除了为委托分配内存,创建委托对象还会把第一个方法放入委托的调用列表。
我们还可以使用初始化语法在同一条语句中创建变量和初始化对象。

1
2
MyDel delVar = new MyDel( myInstObj.MyM1 );
MyDel dVar = new MyDel( SClass.OtherM2 );
1
2
MyDel delVar = myInstObj.MyM1;
MyDel dVar = SClass.OtherM2;

给委托赋值

由于委托是引用类型,我们可以通过给它赋值来改变包含在委托变量中的引用。旧的委托对象会被垃圾回收器回收。

1
2
3
4
MyDel delVar;
delVar = myInstObj.MyM1; //创建委托对象并赋值
//...
delVar = SClass.OtherM2; //创建新的委托对象并赋值

组合委托

委托可以使用额外的运算符来+“组合”。这个运算最终会创建一个新的委托,其调用列表连接了作为操作数的两个委托的调用列表副本

1
2
3
4
MyDel delA = myInstObj.MyM1;
MyDel delB = SClass.OtherM2;

MyDel delC = delA + delB; //组合调用列表

尽管术语组合委托(combining delegate)让我们觉得好像操作数委托被修改了,但其实它们并没有被修改。事实上,委托是恒定的。委托对象被创建后不能再被改变。

为委托添加方法

尽管通过上一节的内容我们知道了委托其实是不变的,不过C#提供了看上去可以为委托添加方法的语法,即使用+=运算符。

1
2
3
MyDel delVar = inst.MyM1;   //创建并初始化
delVar += SCl.m3; //增加方法
delVar += X.Act; //增加方法

由于委托是不可变的,所以为委托的调用列表添加3个方法后的结果其实是变量指向的一个全新的委托。在使用+=运算符时,实际发生的是创建了一个新的委托,其调用列表是左边的委托加上右边方法的组合。然后将这个新的委托赋值给delVar。可以为委托添加多个方法。每次添加都会在调用列表中创建一个新的元素。

从委托移除方法

我们还可以使用-=运算符从委托移除方法。

1
delVar -= SCl.m3;        //从委托移除方法

与为委托添加方法一样,其实是创建了一个新的委托。新的委托是旧委托的副本——只是没有了已经被移除方法的引用。
如下是移除委托时需要记住的一些事项。

  • 如果在调用列表中的方法有多个实例,-=运算符将从列表最后开始搜索,并且移除第一个与方法匹配的实例。
  • 试图删除委托中不存在的方法将无效。
  • 试图调用空委托会抛出异常。可以通过将委托和null进行比较来判断委托的调用列表是否为空。如果调用列表为空,则委托是null

调用委托

关于调用委托需要知道的重要事项如下:

  • 可以通过两种方式调用委托。一种是像调用方法一样调用委托,另一种是使用委托的Invoke方法。
  • 可以将参数放在调用的圆括号内。用于调用委托的参数作用于调用列表中的每个方法(除非其中一个参数是输出参数,稍后将介绍)。
  • 如果一个方法在调用列表中多次出现,则在调用委托时,每次在列表中遇到该方法时都会调用它。
  • 调用时委托不能为空(null),否则将引发异常。可以使用if语句进行检查,也可以使用空条件运算符和Invoke方法。
1
2
3
4
5
6
7
8
9
10
MyDel delVar = inst.MyM1;
delVar += SCl.m3;
delVar += X.Act;
//...
if (delVar != null)
{
delVar(55); //调用委托
}
delVar?.Invoke(65); //使用Invoke和空条件运算符
//...

委托的示例

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
//定义一个没有返回值和参数的委托类型
delegate void PrintFunction();

class Test
{
public void Print1()
{
Console.WriteLine("Print1 -- instance");
}

public static void Print2()
{
Console.WriteLine("Print2 -- static");
}
}

class Program
{
static void Main()
{
Test t = new Test(); //创建一个测试类实例
PrintFunction pf; //创建一个空委托

pf = t.Print1; //实例化并初始化该委托

//给委托增加3个另外的方法
pf += Test.Print2;
pf += t.Print1;
pf += Test.Print2;
//现在,委托含有4个方法

if (null != pf) //确认委托有方法
pf(); //调用委托
else
Console.WriteLine("Delegate is empty");
}
}
1
2
3
4
Print1 -- instance
Print2 -- static
Print1 -- instance
Print2 -- static

调用带返回值的委托

如果委托有返回值并且在调用列表中有一个以上的方法,会发生下面的情况。

  • 调用列表中最后一个方法返回的值就是委托调用返回的值。
  • 调用列表中所有其他方法的返回值都会被忽略。
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
delegate int MyDel();          //声明有返回值的委托类型
class MyClass
{
int IntValue = 5;
public int Add2()
{
IntValue += 2;
return IntValue;
}
public int Add3()
{
IntValue += 3;
return IntValue;
}
}

class Program
{
static void Main()
{
MyClass mc = new MyClass();
MyDel mDel = mc.Add2; //创建并初始化委托
mDel += mc.Add3; //追加方法
mDel += mc.Add2; //追加方法
Console.WriteLine($"Value: { mDel() }");
}
}
1
Value: 12

调用带引用参数的委托

如果委托有引用参数,参数值会根据调用列表中的一个或多个方法的返回值而改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
delegate void MyDel( ref int X );

class MyClass {
public void Add2(ref int x) { x += 2; }
public void Add3(ref int x) { x += 3; }
static void Main() {
MyClass mc = new MyClass();

MyDel mDel = mc.Add2;
mDel += mc.Add3;
mDel += mc.Add2;

int x = 5;
mDel(ref x);

Console.WriteLine($"Value: { x }");
}
}
1
Value: 12

匿名方法

匿名方法(anonymous method)是在实例化委托时内联(inline)声明的方法。

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
class Program
{
public static int Add20(int x)
{
return x + 20;
}

delegate int OtherDel(int InParam);
static void Main()
{
OtherDel del = Add20;

Console.WriteLine("{0}", del(5));
Console.WriteLine("{0}", del(6));
}
}
//匿名方法版本
class Program
{
delegate int OtherDel(int InParam);
static void Main()
{
OtherDel del = delegate(int x)
{
return x + 20;
};

Console.WriteLine("{0}", del(5));
Console.WriteLine("{0}", del(6));
}
}
1
2
25
26

使用匿名方法

使用匿名方法的地方:

  • 声明委托变量时作为初始化表达式。
  • 组合委托时在赋值语句的右边。
  • 为委托增加事件时在赋值语句的右边。
1
2
3
4
5
6
7
8
9
// 1.声明委托变量时作为初始化表达式
OtherDel del = delegate(int x) { return x + 20; };
// 2.组合委托(+=)赋值右侧
del += delegate(int x) { return x * 2; };
// 3.绑定事件赋值右侧(事件章节)
button.Click += delegate(object sender, EventArgs e)
{
Console.WriteLine("按钮点击");
};

匿名方法的语法

匿名方法表达式的语法组成部分:

  • delegate 类型关键字。
  • 参数列表,如果语句块没有使用任何参数则可以省略。
  • 语句块,它包含了匿名方法的代码。
1
2
3
关键字      参数列表        语句块
↓ ↓ ↓
delegate ( Parameters ) { ImplementationCode }

返回类型

匿名方法不会显式声明返回值。然而,实现代码本身的行为必须通过返回一个与委托的返回类型相同的值来匹配委托的返回类型。如果委托有 void 类型的返回值,匿名方法就不能返回值。

1
2
3
4
5
6
7
8
9
10
11
// 委托类型的返回类型int
delegate int OtherDel(int InParam);

static void Main()
{
OtherDel del = delegate(int x)
{
return x + 20 ; //返回一个整型值
};

}

参数

除了数组参数,匿名方法的参数列表必须在如下3方面与委托匹配:

  • 参数数量;
  • 参数类型及位置;
  • 修饰符。
    可以通过使圆括号为空或省略圆括号来简化匿名方法的参数列表,但必须满足以下两个条件:
  • 委托的参数列表不包含任何out参数;
  • 匿名方法不使用任何参数。
    例如,如下代码声明了一个没有任何out参数的委托,和一个没有使用任何参数的匿名方法。由于两个条件都满足了,所以可以省略匿名方法的参数列表。
1
2
3
4
5
6
delegate void SomeDel ( int X );        //声明委托类型
SomeDel SDel = delegate //省略参数列表
{
PrintMessage();
Cleanup();
};

params参数

如果委托声明的参数列表包含了params参数,那么匿名方法的参数列表将忽略params关键字。

  • 委托类型声明指定最后一个参数为params类型的参数;
  • 然而,匿名方法参数列表必须省略params关键字。
1
2
3
4
5
6
7
//在委托类型声明中使用 params 关键字
delegate void SomeDel( int X, params int[] Y);
//在匹配的匿名方法中省略关键字
SomeDel mDel = delegate (int X, int[] Y)
{
//…
};

变量和参数的作用域

参数以及声明在匿名方法内部的局部变量的作用域限制在实现代码的主体之内。

1
2
3
4
5
6
7
8
9
delegate void MyDel( int x );
//…
MyDel mDel = delegate (int y)
{
int z = 10;
Console.WriteLine("{0},{1}", y, z);
};

Console.WriteLine("{0}, {1}", y, z); //编译错误,离开作用域

外边变量

与委托的具名方法不同,匿名方法可以访问它们外围作用域的局部变量和环境。

  • 外围作用域的变量叫作外部变量(outer variable)。
  • 用在匿名方法实现代码中的外部变量称为被方法捕获
1
2
3
4
5
6
int x = 5;   //变量x定义在匿名方法作用域的前面
//…
MyDel mDel = delegate
{
Console.WriteLine("{0}", x);
};

捕获变量的生命周期的扩展

只要捕获方法是委托的一部分,即使变量已经离开了作用域,捕获的外部变量也会一直有效。

  • 局部变量x在块中声明和初始化。
  • 然后,委托mDel用匿名方法初始化,该匿名方法捕获了外部变量x
  • 块关闭时,x超出了作用域。
  • 如果取消块关闭之后的WriteLine语句的注释,就会产生编译错误,因为它引用的x现在已经离开了作用域。
  • 然而,mDel委托中的匿名方法在它的环境中保留了x,并在调用mDel时输出了它的值。

    ![[csharp-11-delegate-outer-var.png]]
1
Value of x: 5

Lambda表达式

C#2.0引入了匿名方法。然而它的语法有一点冗长,而且需要一些编译器已经知道的信息。C#3.0引入了Lambda表达式,简化了匿名方法的语法,从而避免包含这些多余的信息。

在匿名方法的语法中,delegate关键字有点多余,因为编译器已经知道我们在将方法赋值给委托。我们可以很容易地通过如下步骤把匿名方法转换为Lambda表达式:

  • 删除delegate关键字;
  • 在参数列表和匿名方法主体之间放置Lambda运算符=>。Lambda运算符读作 “goes to”。
1
2
MyDel del = delegate(int x)  { return x + 1; };   //匿名方法
MyDel le1 = (int x) => { return x + 1; }; //Lambda表达式

编译器可以推断更多信息,所以可以进一步简化Lambda表达式:

  • 编译器还可以从委托的声明中知道委托参数的类型,因此Lambda表达式允许省略类型参数,如le2的赋值代码所示。
    • 带有类型的参数列表称为显式类型
    • 省略类型的参数列表称为隐式类型
  • 如果只有一个隐式类型参数,我们可以省略两端的圆括号,如le3的赋值代码所示。
  • 最后,Lambda表达式允许表达式的主体是语句块或表达式。如果语句块包含了一个返回语句,我们可以将语句块替换为return关键字后的表达式,如le4的赋值代码所示。
1
2
3
4
5
MyDel del = delegate(int x)  { return x + 1; };   //匿名方法
MyDel le1 = (int x) => { return x + 1; }; //Lambda表达式
MyDel le2 = (x) => { return x + 1; }; //Lambda表达式
MyDel le3 = x => { return x + 1; }; //Lambda表达式
MyDel le4 = x => x + 1; //Lambda表达式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
delegate double MyDel(int par);

class Program
{
static void Main()
{
delegate int MyDel(int x);

MyDel del = delegate(int x) { return x + 1; }; //匿名方法

MyDel le1 = (int x) => { return x + 1; }; //Lambda表达式
MyDel le2 = (x) => { return x + 1; };
MyDel le3 = x => { return x + 1; };
MyDel le4 = x => x + 1;

Console.WriteLine($"{ del (12) }");
Console.WriteLine($"{ le1 (12) }");
Console.WriteLine($"{ le2 (12) }");
Console.WriteLine($"{ le3 (12) }");
Console.WriteLine($"{ le4 (12) }");
}
}
1
2
3
4
5
13
13
13
13
13

有关Lambda表达式的参数列表的要点如下。

  • Lambda表达式参数列表中的参数必须在参数数量、类型和位置上与委托相匹配。
  • 表达式的参数列表中的参数不一定需要包含类型(隐式类型),除非委托有 refout 参数——此时必须注明类型(显式类型)。
  • 如果只有一个参数,并且是隐式类型的,则两端的圆括号可以省略,否则必须有括号。
  • 如果没有参数,必须使用一组空的圆括号。


![[csharp-11-delegate-lambda.png]]