c#---delegate关键字
在C#中,delegate是⼀个神奇的关键字,值得拿出来单独作为⼀个话题。
⼀.基本内容
调⽤(invoke)委托,相当于调⽤委托所绑定的⽅法,⼀个委托可以绑定多个⽅法,使⽤"+="就可以向委托中添加新的⽅法,使⽤"-="可以从委托中删除⽅法:
public delegate void Print();
class Program{
public static void Main(String[] args){
Print print = PrintMethod1;
print += PrintMethod2;
print();
print -= PrintMethod1;
print();
}
public static void PrintMethod1(){
Console.WriteLine("我是第⼀个委托⽅法");
}
public static void PrintMethod2(){
Console.WriteLine(" 我是第⼆个委托⽅法");
}
}
如果我们从委托中减去⼀个它根本未添加过的⽅法,会怎么样呢?答案是不会报错,但也不会有任何影响。
还有更加现实的问题,就是如果添加的⽅法中有多个⽅法都有返回值,那怎么办?只返回最后⼀个具有返回值的⽅法的返回值。
如果我们想要详细的研究,就得从CLR(Command Language RunTime,公共语⾔运⾏时,相当于java的虚拟机,即运⾏时环境)下⼿。
其实,任何委托类型都是继承⾃System.MulticastDelegate类,但这个类⼜继承⾃System.Delegate类。为什么会有两个委托类呢?FCL(Framework Class Library,即Framework类库)应该只有⼀个委托类才对。这是⼀个设计上的遗憾。虽然所有的委托都是MulticastDelegate的派⽣类,但是个别情况下,我们也要使⽤Delegate,像是Delegate的静态⽅法Combind()和Remove()就是实现委托⽅法的添加和删除,"+="和"-="这两个运算符被重载了,⾥⾯封装的就是这两个⽅法的调⽤。
神奇的是,我们可以忽略参数列表!
先来个例⼦:
class Program
{
public static void Main(String[] args)
{
Action<String> action = PrintMethod;
action += delegate
{ Console.WriteLine("我是没有参数的委托⽅法"); }
;
action("");
}
public static void PrintMethod(String str)
{
Console.WriteLine("我是委托⽅法");
}
}
我们往⽅法组⾥⾯添加⼀个匿名⽅法,但是没有参数String!为什么能这样呢?因为该参数并没有被使⽤,所以可以忽略它。这样的好处就是我们可以不⽤写⼀⼤串参数列表,但既然参数没有⽤到,为什么我们还要传进来呢?想想事件处理程序,我们就知道为什么会有这样的"语法糖"了。事件处理程序需要我们传递⼀个事件,判断该事件是否为null,然后调⽤相应的事件处理动作,标准的事件处理程序的参数列表像是这样:
public event EventHandler(Object sender, EventArgs e);
烦,就是我的第⼀眼感觉,⽽且要是使⽤到这样的⽅法组,光是参数列表就可以让我想死了。所以,快捷的⽅法就是忽略参数列表,参数列表的信息只是为了判断事件是否为空,然后调⽤相应的处理,我们可以⼀开始就认定事件已经发⽣了,然后启动处理。事实上,很多时候都是不需要参数列表的信息。
这就是参数通配(parameter wildcarding)。虽然它好⽤,但也存在问题:因为我们忽略了参数,所以很容易和没有参数的⽅法的委托混在⼀块,像是下⾯这样:
class Program
{
public static void Main(String[] args)
{
PrintMethod(delegate
{ Console.WriteLine("我是第⼀个委托类型"); }
);
}
public static void PrintMethod(Show show)
{
show();
}
public static void PrintMethod(Show2 show)
{
show("");
}
}
我们假设Show2该委托并没有使⽤到参数str。编译器根本不知道该调⽤哪⼀个委托,因为它们似乎都可以。这时我们只能显⽰的指定参数或者将匿名⽅法强制转换为正确的委托类型。
既然在C#中委托是⼀个类型,那我们可否将它作为参数呢?答案是可以的:
public delegate void Print();
class Program{
public static void PrintMethod(){
Console.WriteLine("我是委托⽅法");
}
public static void PrintMethod2(Print print){
print();
}
public static void Main(String[] args){
Print print = PrintMethod;
PrintMethod2(print);
}
}
从这段代码中我们可以看到,委托类型作为参数,就可以回调它所绑定的⽅法,实现代码的复⽤,像是上⾯的例⼦,如果没有传递委托类型,我们的PrintMethod2()可能就要继续写Console.WriteLine()。
我们还能将上⾯的代码进⾏简化:
public delegate void Print();
class Program{
public static void PrintMethod(){
Console.WriteLine("我是委托⽅法");
}
public static void PrintMethod2(Print print){
print();
}
public static void Main(String[] args){
PrintMethod2(PrintMethod);
writeline方法的作用}
}
为什么能这样呢?因为我们的编译器知道PrintMethod2()的参数是⼀个委托,⽽委托实际上是包装⽅法的引⽤,所以,编译器会⾃⼰推断,该⽅法是否是委托所包装的,如果是,就会⾃⼰⽣成⼀个委托来包装该⽅法,但我们是看不到的。效率并没有得到提⾼,只是我们简化了语法⽽已。
在CLR中,有内置的委托类型:
public delegate void Action<T1, T2>(T1 arg1, T2 arg2);
.....
public delegate TResult Func<TResult>();
public delegate TResult Func<T, TResult>(T arg);
public delegate TResult Func<T1, T2, TResult>(T1 arg1, T2 arg2);
.....
CLR⿎励我们使⽤这些内置的委托类型,防⽌系统中出现太多的类型。下⾯如果没有特别的声明,我
都会使⽤这些内置的类型,像是这样修改前⾯的例⼦:class Program
{
public static void Main(String[] args)
{
Action action = PrintMethod;
action();
}
public static void PrintMethod()
{
Console.WriteLine("我是委托⽅法");
}
}
但我们有时候也要⾃定义委托类型,像是上⾯的内置类型参数超过16个的话(这种情况是不会发⽣的,能够写出16以上参数⽅法的⼈,我都不认为他是在编程),像是使⽤ref或out参数以便参数按引⽤的⽅式传递,因为这些内置的委托的名字都⼀样,⽽参数⼀旦按引⽤传递,就意味着该⽅法对该参数的修改都能直接反映在该变量上,这是很不安全的。还有就是委托要通过C#的param关键字获取可变数量的参数,要为委托的任何参数指定默认值,或者要对委托的泛型类型参数进⾏约束,等等。
⼆.委托的⼯作原理
委托是如此神奇,以⾄于我们甚⾄根本不相信有这样的东西。但委托这些神奇的特性并不是凭空产⽣的,我们可以从CLR中到它们的实现机制。
我们先来个委托类型的声明:
public delegate void Show();
就是这样简单的⼀句声明,在编译器那边可是⼀个⼤事件:
public class Show : System.MulticastDelegate{
//构造器
public Show(Object object, IntPtr method);
//与声明⼀样的⽅法
public virtual void Invoke();
....
}
编译器会⽣成⼀个与委托同名的类,该类有四个⽅法,这⾥只关注前两个,剩下两个⽅法涉及到异步调⽤,暂且不谈。
所有的委托都有构造器,该构造器接受两个参数:对象引⽤object和⽅法标识值method(整数)。对象引⽤其实就是this引⽤,⽽⽅法标识值是⼀个特殊的整数,它标识了我们想要回调的⽅法,它是从MethodDef或MemberRef元数据token获得,⾄于这两个东西到底是什么,这⾥不展开,因为我只是初学者,最好不要⼀开始就纠缠于编译器实现的内容。对于静态⽅法,object为null,因为静态⽅法没有this引⽤。
这两个值是理解委托⼯作原理最重要的部分。
我们先看看它们保存在哪⾥。在编译器⽣成的类中,有三分继承⾃MulticastDelegate的私有字段:
_target: 对象引⽤;
_methodPtr:标识要回调的⽅法的整数值;
_invocationList:构造⽅法组时,引⽤⼀个委托数组。
我们先看前两个,_target和_methodPtr。因为是私有,所以我们⽆法访问,但是,Delegate却有两个只读的公共属性:Target和Method,分别对应这两个值。
我们可以利⽤这两个公共属性做很多有趣的事情,⽐如说,我们可以检查委托对象引⽤的是否在⼀个特定类型中定义的⽅法:
Boolean Check(MulticastDelegate delegateMethod, Type type){
return((delegateMethod.Target != null) && delegateMethod.Target.GetType() == type);
}
Boolean是布尔值的包装类型。我们还可以检查回调⽅法是否有⼀个特定的名称:
Boolean Check2(MulticastDelegate delegateMethod, String name){
return(delegateMethod.Method.Name == name);
}
利⽤这两个属性,我们可以实现像是反射这种⾼级特性。
现在该轮到_invocationList。
这个字段⾮常特殊,⼀般都是null,但如果我们为委托添加⼀个⽅法组(method group, 有些地⽅称为委托链),该字段就会引⽤⼀个委托数组。在我们使⽤"+="为委托添加⽅法的时候,其实就是添加⽅法组。什么叫⽅法组呢?就是能够被委托实例包装的⽅法,它们的特点就是匹配委托声明的签名和返回值。当我们添加⽅法组的时候,编译器其实都在为我们添加的每⼀个⽅法⽣成⼀个委托,然后将该委托和前⼀个委托放进⼀个数组中,该数组的⼤⼩刚刚好能够容纳这两个委托。就是因为这样,使得每次添加新的⽅法(委托)时,编译器都必须重新创建⼀个新的数组来容纳新的委托,然后将之前的委托和数组都交给垃圾回收器回收。其实从_invocationList中的List就可以知道它的低层是怎样实现的。学过java的⼈⼀定对容器类List⾮常熟悉,它的⼯作原理也是同样的道理:低层是⼀个数组,当该数组⽆
法容纳新的元素时,就会⾃动将数组⼤⼩扩为两倍。
从⽅法组中删除⽅法也是同样的过程,如果⽅法组中还有超过⼀个⾮null的元素,就会⽣成⼀个新的数组来容纳剩下的元素。但删除⽅法只能⼀次删除⼀个,⽽不是删除所有匹配的⽅法,事实上⽅法组中的所有⽅法都是匹配的。
了解了⽅法组,我们就不难看出,为什么委托会有上⾯那样神奇的特性。但神奇归神奇,这样的实现是存在很⼤的隐患的:如果⽅法组中的⼀个⽅法抛出异常,该怎么办?如果真丝这样,后⾯的委托就全废了!所以,MulticastDelegate提供了⼀个实例⽅法:GetInvocationList⽤于显⽰的调⽤⽅法组中的每⼀个委托:
Delegate[] arrayDelegate = delegateMethod.GetInvocationList();
得到该数组后,我们就能做很多事:可以遍历该数组,调⽤它⾥⾯的每⼀个⽅法,当然得进⾏null的判断,因为⽅法组允许包含null,⽽且我们也能捕获异常并进⾏处理。
⽅法组中的委托不能向上转型为System.Delegate,因为编译器会不知道该具体创建哪个委托(毕竟⽅法组中的声明是匹配的)。
三.匿名⽅法
学过java的⼈⼀定想起来,在java中,闭包的替代物就是内部类,那么,C#有没有类似的机制呢?
C#中类似的就是匿名⽅法。我们先来看⼀个简单的例⼦:
public delegate void Print();
class Program{
public static void Main(String[] args){
Print print = delegate{
Console.WriteLine("我是⼀个匿名⽅法");
};
print();
}
}
匿名⽅法也可以具有参数,在使⽤匿名⽅法的时候,要注意,匿名⽅法可以使⽤⽅法外部定义的变量,但是⽆法使⽤外部定义的ref参数和out参数,⽐如说:
public void PrintMethod(ref int number){
Print print = delegate{
Console.WriteLine(number);
};
}
这种⽅法中的匿名⽅法就不能使⽤⽅法的ref参数。为什么呢?ref参数的作⽤是将值类型当做引⽤类型传递,使得⽅法对参数都能反映到该变量中。如果我们允许匿名⽅法使⽤ref参数,那么我们的变量就会因为匿名⽅法⽽发⽣变化,这样其他⽅法所使⽤的该变量就会发⽣变化,从⽽引起错误。想想java的匿名类,它就要求使⽤到的参数都必须是final,也是同样的道理。
显然,使⽤匿名⽅法必须⼩⼼,如果发⽣错误,我们很难定位到该⽅法,因为它根本就没有名字,所以,在匿名⽅法中不能访问不安全的代码,像是
指针就不能在匿名⽅法中使⽤(它在C#中就认为是不安全的,谢天谢地,我⼀直都对使⽤指针⼼有余悸)。
匿名⽅法也不能使⽤跳转语句(break,continue,goto)跳⾄外部,反之亦然,匿名⽅法外部的跳转语句也不能跳转到匿名⽅法内部,因为它们实际上也是⼀个⽅法,从来没有见过跳转语句能够跨⽅法的。
使⽤匿名⽅法的最⼤好处就是减少系统开销,就像java的内部类⼀样,仅在需要的时候才定义。
匿名⽅法在CLR中并不是没有名字(就像java⼀样),但该名字是CLR⽣成的⼀串难以解读的字符串,该⽅法是private的,禁⽌不在类型内定义的任何代码访问该⽅法。⾄于是静态还是⾮静态的,就得看它⾥⾯给引⽤的成员变量,如果引⽤实例成员变量,就是⾮静态⽅法,其他情况⼀律为静态⽅法。
匿名⽅法更多是作为⽅法参数使⽤:
public delegate void Print();
class Program
{
public static void Main(String[] args)
{
Action<Print> action = PrintMethod;
action(delegate{ Console.WriteLine("我是委托⽅法");});
}
public static void PrintMethod(Print print)
{
print();
}
}
这和java中的匿名内部类的使⽤时⼀样的道理,⽽且特别适合⽤在事件处理中:等到响应的时候才创
建要响应的动作。但我们要注意代码的可读性。也许这样我们的代码会很简单,但正如java的匿名内部类,它使得我们的代码可读性和重构⼯作变得特别具有挑战性。
⽐较好的风格是这样的:
public delegate void Print();
class Program
{
public static void Main(String[] args)
{
Action<Print> action = PrintMethod;
action(delegate
{ Console.WriteLine("我是委托⽅法");}
);
}
public static void PrintMethod(Print print)
{
print();
}
}
这样匿名⽅法的参数是⼀⾏,代码体是⼀⾏,结尾的括号和分号表⽰这是匿名⽅法(事实上,java的匿名内部类就是这样的写法)。
匿名⽅法的出现,使得我们的委托机制⾮常强⼤,不光上⾯提到的地⽅,匿名⽅法还有⼀个更加重要的⽅⾯:捕获变量。
我们先来了解下闭包。
学习编程的⼈⼀定听说过这个词,它在计算机科学领域是⼀个⾮常古⽼的词汇。闭包的基本概念是:
⼀个函数除了能通过提供给它的参数与环境互动之外,还能同环境进⾏更⼤程度的互动。这个概念是⼀个抽象甚⾄可以说是⼀个理想的说法,实际上的应⽤就是我们的匿名⽅法可以使⽤外部变量(看到这⾥,我⾮常淡定,因为java的匿名内部类也可以做到这点,因为它拥有外部类的this引⽤)。
所谓的外部变量,就是指其作⽤域(scope)包括⼀个匿名⽅法的局部变量或参数,this引⽤也是⼀个外部变量。匿名⽅法内部使⽤的外部变量就是被捕获的变量(captured outer variable)。
对应闭包的概念,我们可以这样理解:函数指匿名⽅法,互动环境指由这个匿名⽅法捕捉到的变量集。
匿名⽅法捕捉到的变量就是原变量,⽽不是它的值(即副本),我们所做的修改都可以反映到该变量上。
匿名⽅法对外部变量的捕获并不仅仅是使⽤该变量,甚⾄可以延长该变量的寿命。我们知道,定义在⽅法内部的变量⼀旦离开⽅法体就会被回收,但如果是被⽅法体内的匿名⽅法捕获呢?只要包装匿名⽅法的委托实例不被回收,该变量就会⼀直存在。
这种情况就真的是让⼈感到匪夷所思了。要想明⽩原理,我们还是要回到CLR中(事实上,要想真正明⽩C#,CLR的学习是必须的)。编译器会创建额外的类来容纳变量,⽽之前定义该变量的类的实例拥
有该变量的引⽤,捕获到该变量的匿名⽅法同样也拥有该变量的引⽤。所以,⽅法内的变量并不是我们想象中存储在⽅法对应的栈帧中。我们知道,所有引⽤都是在拥有该引⽤的实例的堆上,除⾮委托被回收,否则该引⽤就不会被销毁。
这种情况埋下了隐患:被延长⽣命周期的变量其实就是明显的内存泄露(leak)。这是我们在编程中要注意的。
对于变量捕获的讨论远⾮如此。我们来想想这样的情况:
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论