一、委托概述
1、什么是委托
委托和类一样,是一种用户定义类型(即是一种类,所以也是一个引用类型)。在它们组成的结构方面区别是,类表示的是数据和方法的集合,而委托则持有一个或多个方法。
可以把 delegate 看作是一个包含有序方法列表的对象,这些方法具有相同的签名和返回类型。
-
(1)方法的列表称为调用列表。
-
(2)委托持有的方法可以来自任何类或结构,只要它们在下面两方法匹配:
- 委托的返回类型;
- 委托的签名(包括 ref 和 out 修饰符)
-
(3)调用列表中的方法可以是实例方法也可以是静态方法。
-
(4)在调用委托的时候,会执行其调用列表中的所有方法。
2、声明委托类型
声明委托代码:
delegate void MyDel(int x);
//delegate:关键字
//void:返回类型
//MyDel:委托类型名称
//MyDel(int x):签名
委托与方法在声明时有两点不同:
- 以 delegate 关键字开头;
- 没有方法主体。
说明: 虽然委托类型声明看上去和方法的声明一样,但它不需要在类内部声明,因为它是类型声明。
3、创建委托对象
委托类型的变量声明:
MyDel delVar;
//MyDel:委托类型
//delVar:变量
有两种创建委托对象的方法:
(1)使用带 new 运算符的对象创建表达式。
new 运算符的操作数的组成:
- 委托类型名。
- 一组圆括号,其中包含作为调用列表中第一个成员的方法的名称,该方法可以是实例方法或静态方法。
假设 myInstObj 是类对象,MyM1 是 myInstObj 的一个实例方法。 SCLass 是类,OtherM2 是 SCLass 的静态方法。
//创建委托并保存引用
delVar = new MyDel(myInstObj.MyM1);
delVar = new MyDel(SCLass.OtherM2);
(2)快捷语法
仅由方法说明符构造。这种快捷语法能够工作是因为在方法名称和其相应的委托类型之间存在隐式转换。
//创建委托并保存引用
delVar = myInstObj.MyM1;
delVar = SClass.OhterM2;
以下是创建委托对象的完整代码:
delegate void MyDel(int x);
//使用 new 运算符方式
MyDel delVar,dVar;
delVar = new MyDel(myInstObj.MyM1);
dVar = new MyDel(SClass.OtherM2);
//快捷语法方式
MyDel delVar = myInstObj.MyM1;
MyDel dVar = SClass.OhterM2;
除了为委托分配内存,创建委托对象还会把第一个方法放入委托的调用列表。
4、给委托赋值
由于委托是引用类型,我们可以通过给它赋值来改变包含在委托变量中的引用。旧的委托对象被垃圾回收器回收。
MyDel delVar;
delVar = myInstObj.MyM1;
delVar = SClass.OhterM2;
二、组合委托
1、什么是组合委托
委托也可以包含多个方法。
也称为多播委托。
比如,创建了3个委托。其中第3个委托由前两个委托组合而成。
MyDel delA = myInstObj.MyM1;
MyDel delB = SClass.OhterM2;
MyDel delC= delA + delB;//组合调用列表
委托是恒定的,不会因为组合而改变原委托。委托对象被创建后不能再被改变。
2、为委托添加方法
使用 += 运算符来添加方法。
MyDel delVar = inst.MyM1;
//增加两个方法
delVar += SCl.m3;
delVar += X.Act;
在使用 += 运算符时,实际发生的是创建了一个新的委托,其调用列表是左边的委托加上右边方法的组合。然后将这个新的委托赋值给 delVar。
(我有疑问: 委托是可以存放多个方法的方法列表,那为什么还要先创建一个新的委托,再赋值给 delVar 委托对象呢?这里的赋值是不是指在原有的基础上再增加一个元素(方法)的意思?比如类似于string str = “a”; str += “bc”; 【temp = str+“bc”; 把 temp 赋值到 str 当中】 )
2、从委托移除方法
使用 -= 运算符从委托移除方法。(在这里我似乎理解了事件为什么还能跟委托有一定的关联,此处写法很像是事件的订阅和取消订阅的工作方式)
//从委托移除方法
delVar -= SCl.m3;
与为委托添加方法一样,其实是创建了一个新的委托。新的委托是旧委托的副本——只是没有了已经被移除方法的引用。(加强理解: 比如 string str = “abc”; 去除了"bc"后得出一个新值 temp = “a”,然后再把 temp 赋值给 str。)
移除委托时需要记住的一些事项
- 如果在调用列表中的方法有多个实例, -= 运算符将从列表最后开始搜索,并且移除第一个与方法匹配的实例。
- 试图删除委托中不存在的方法将无效。
- 试图调用空委托会抛出异常。可以通过将委托和 null 进行比较来判断委托的调用列表是否为空。如果调用列表为空,则委托是 null。
3、调用委托
调用委托需要知道的重要事项:
- 可以通过两种方法调用委托。一种是像调用方法一样调用委托,另一种是使用委托的 Invoke 方法。
- 在圆括号里传参。即 ==委托对象 (参数) ==或者 Invke (参数)。
- 如果一个方法在调用列表中多次出现,则在调用委托时,每次在列表中遇到该方法时都会调用它。
代码例子:
class TestClass
{
static public void Fun(string str)
{
Console.WriteLine(str);
}
}
class Program
{
delegate void MyDel(string str);
static void Main(string[] args)
{
MyDel delVar = TestClass.Fun;
//又增加了该方法,于是调用列表出现两次该方法
delVar += TestClass.Fun;
delVar.Invoke("调用Fun");
delVar-= TestClass.Fun;
//移除一次后,还剩一个 TestClass.Fun
delVar.Invoke("移除一次该方法后,调用Fun");
Console.ReadKey();
}
}
输出结果:
调用Fun
调用Fun
移除一次该方法后,调用Fun
- 调用时委托不能为空(null),否则将引发异常。(不过可以使用 if 语句进行检查 也可以使用 delVal.?Invoke(参数) ,通过?空条件运算符来检查)
4、委托的示例
本书的示例:
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(string[] args)
{
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.");
Console.ReadKey();
}
}
输出结果:
Print1 – instance
Print2 – static
Print1 – instance
Print2 – static
5、调用带返回值的委托
如果委托有返回值并且在调用列表中有一个以上的方法,会发生如下的情况:
- 调用列表中最后一个方法返回的值就是委托调用返回的值。
- 调用列表中所有其他方法的返回值都会被忽略。
class MyClass
{
int IntValue = 5;
public int Add2() { IntValue += 2; return IntValue; }
public int Add3() { IntValue += 3; return IntValue; }
//为了加强理解书中的内容,自己添加的方法Add4
public int Add4() { return 1234; }
}
class Program
{
static void Main(string[] args)
{
MyClass mc = new MyClass();
MyDel mDel = mc.Add2;//返回值为7被忽略
//为了验证输出结果是否为最后一个方法返回的结果
//mDel += mc.Add4;
mDel += mc.Add3;//返回值为10被忽略
mDel += mc.Add2;
//mDel += mc.Add4;//若执行该语句,则输出内容为1234
Console.WriteLine($"Value:{ mDel() }");
Console.ReadKey();
}
}
输出结果:
Value:12
6、调用带引用参数的委托
如果委托有引用参数,参数值会根据调用列表中的一个或多个方法的返回值而改变。
class MyClass
{
public void Add2(ref int x) { x += 2; }
public void Add3(ref int x) { x += 3; }
}
class Program
{
static void Main(string[] args)
{
MyClass mc = new MyClass();
MyDel mDel = mc.Add2;
mDel += mc.Add3;
mDel += mc.Add2;
int x = 5;
mDel(ref x);
Console.WriteLine($"Value:{ x }");
Console.ReadKey();
}
}
输出结果:
Value:12
三、匿名方法
1、什么是匿名方法
匿名方法是在实例化委托时内联声明的方法。
使用条件: 如果方法只会被使用一次——用来实例化委托,则使用匿名方法来处理。
在如下地方使用匿名方法:
- 声明委托变量时作为初始化表达式。
- 组合委托时在赋值语句的右边。
- 为委托增加事件时在赋值语句的右边。
匿名方法的语法
匿名方法表达式的语法包含如下组成部分:
- delegate 类型关键字。
- 参数列表,如果语句块没有使用任何参数则可以省略。
- 语句块,它包含了匿名方法的代码。
delegate (Parameters){ ImplementationCode }
//delegate:关键字
//Parameters:参数列表
//ImplementationCode:语句块
1)返回类型
匿名方法不会显式声明返回值。
delegate int OhterDel(int InParam);
static void Main()
{
OtherDel del = delegate(int x)
{
retrun x + 20;
};
}
2)参数
除了数组参数,匿名方法的参数列表必须在如下3方面与委托匹配:
- 参数数量;
- 参数类型及位置;
- 修饰符。
可以通过使圆括号为空或省略圆括号来简化匿名方法的参数列表,但必须满足以下两个条件:
- 委托的参数列表不包含任何 out 参数;
- 匿名方法不使用任何参数。
delegate void SomeDel(int x);
SomeDel SDel = delegate
{
PrintMessage();
Cleanup();
};
不懂:如果不使用参数的话,怎么对参数进行操作呢?这不就是相当于无参数的委托么?
3)params 参数
如果委托声明的参数列表包含了 params 参数,那么匿名方法的参数列表将忽略 params 关键字。
- 委托类型声明指定最后一个参数为 params 类型的参数;
- 匿名方法参数列表必须省略 params 关键字。
delegate void SomeDel(int X,params int[] Y);
SomeDel mDel = delegate(int X,int Y)
{
};
2、变量和参数的作用域
delegate void MyDel(int x);
class Program
{
static void Main(string[] args)
{
MyDel mDel = delegate (int y)
{
int z = 10;
Console.WriteLine("{0},(1)", y, z);
//y,z 的作用域在花括号内
};
Console.WriteLine("{0},(1)", y, z);//编译错误
Console.ReadKey();
}
}
外部变量
- 外围作用域的变量叫作外部变量。(在 delegate 花括号之外的变量)
- 用在匿名方法实现代码中的外部变量称为被方法捕获。(外部变量在 delegate 花括号内使用。其实外部方法也作用在匿名方法内?书上没提到这点,不知道这算不算被方法捕获。)
四、Lambda 表达式
Lambda 表达式简化了匿名方法的语句。
把匿名方法转换为 Lambda 表达式:
- 删除 delegate 关键字
- 在参数列表和匿名方法主体之间放置 Lambda 运算符 =>。Lambda 运算符读作“goes to”。
MyDel del = delegate(int x){ return x + 1; }; //匿名方法
MyDel le1 = (int x) => { return x + 1; }; //Lambda 表达式
进一步简化 Lambda 表达式:
-
委托参数
- 1)带有类型的参数列表称为显式类型。
- 2)省略类型的参数列表称为隐式类型。
-
如果只有一个隐式类型参数,可以省略两端的圆括号。
-
Lambda 表达式的主体是语句块或表达式,如果语句块包含了一个返回值语句,可以将语句块替换为 return 关键字后的表达式。(即 { return x + 1 ; }; 替换为 x + 1 ; )
//匿名方法
MyDel del = delegate(int x){ return x + 1; } ;
// Lambda 表达式
MyDel le1 = (int x) => { return x + 1; } ;
MyDel le2 = (x) => { return x + 1; } ;
MyDel le3 = x => { return x + 1; } ;
MyDel le4 = x => x + 1 ;
有关 Lambda 表达式的参数列表的要点:
- Lambda 表达式参数列表中的参数必须在参数数量、类型和位置上与委托相匹配。
- 表达式的参数列表中的参数不一定需要包含类型(隐式类型),除非委托有 ref 或 out 参数——此时必须注明类型(显式类型)。
- 如果只有一个参数,并且是隐式类型的,则两端的圆括号可以省略,否则必须有括号。
- 如果没有参数,必须使用一组空的圆括号。