C#复习笔记(3)--C#2:解决C#1的问题(进入快速通道的委托)

委托

前言:C#1中就已经有了委托的概念,可是其繁杂的用法并无引发开发者太多的关注,在C#2中,进行了一些编译器上的优化,能够用匿名方法来建立一个委托。同时,还支持的方法组和委托的转换。顺便的,C#2中增长了委托的协变和逆变。闭包

方法组转换

方法组这个词的含义来自于方法的重载:咱们能够定义一堆方法,这堆方法的名称都同样,可是接受的参数不一样或者返回类型不一样(总之就是签名不一样----除了名字),这就是方法的重载。ide

public static void SomeMethod(object helloworld) { Console.WriteLine(helloworld); } public static void SomeMethod() { Console.WriteLine("hello world"); }

   ThreadStart ts = SomeMethod;
   ParameterizedThreadStart ps = SomeMethod;学习

 

上面显示的两个调用没有问题,编译器可以找到与之匹配的相应方法去实例化相应的委托,可是,问题在于,对于自己已经重载成使用ThreadStart和ParameterizedThreadStart的Thread类来讲(这里是举例,固然适用于全部这样的状况),传入方法组会致使编译器报错:优化

Thread t=new Thread(SomeMethod); //编译器报错:方法调用具备二义性 

一样的状况不能用于将一个方法组直接转换成Delegate,须要显式的去转换:spa

Delegate parameterizedThreadStart = (ParameterizedThreadStart) SomeMethod; Delegate threadStart = (ThreadStart) SomeMethod;

协变性和逆变性

C#1并不支持委托上面的协变性和逆变性,这意味着要为每一个委托定义一个方法去匹配。C#2支持了委托的协变和逆变,这意味着咱们能够写下以下的代码:线程

假定两个类,其中一个继承另外一个:设计

public class BaseClass { } public class DerivedClass : BaseClass { }

C#2支持以下写法:code

class Program { delegate BaseClass FirstMethod(DerivedClass derivedClass); static void Main(string[] args) { FirstMethod firstMethod = SomeMethod; Console.ReadKey(); } static DerivedClass SomeMethod(BaseClass derivedClass) { return new DerivedClass(); } }

而在C#4中,支持了泛型类型和泛型委托的协变和逆变:orm

public class BaseClass{}对象

public class DerivedClass : BaseClass{}

Func<BaseClass, DerivedClass> firstFunc = delegate(BaseClass baseClass)

 { return new DerivedClass(); };
Func
<DerivedClass, BaseClass> secondFunc = firstFunc;

本质上C#4泛型上的协变和逆变只是引用之间的转换,并无在后面建立一个新的对象。

不兼容的风险

C#2支持了委托协变和逆变后会出现下面的问题:

假设如今BaseClass和DerivedClass改成下面这样的:

public class BaseClass { public void CandidateAction(string x) { Console.WriteLine("Baseclass.CandidateAction"); } } public class DerivedClass : BaseClass { public void CandidateAction(object x) { Console.WriteLine("Derived.CandidateAction"); } }

在DerivedClass中重载了BaseClass中的方法,因为C#2的泛型逆变和协变,写下以下代码:

class Program { delegate void FirstMethod(string x); static void Main(string[] args) { DerivedClass derivedClass=new DerivedClass(); FirstMethod firstMethod = derivedClass.CandidateAction; firstMethod("hello world");//DerivedClass.CandidateAction Console.ReadKey(); } }

输出结果是”DerivedClass.CandidateAction!看到的这个结果确定是在C#2以及之后的结果,若是在C#1中,那么该结果应该是输出“BaseClass.CandidateAction"

匿名方法

下面这个出场的匿名方法是咱们以后学习linq和lambda等等一系列重要概念的始做俑者。

首先他要解决的问题是C#1中的委托调用起来太繁琐的问题。在C#1中,要创建一个委托并使用这个委托的话一般要经历四部,关键是无论你要调用一个多么简单的委托都要写一个专门被委托调用的方法放到类里面,若是没有合适的类的话你还要新建一个类。。。

匿名方法是编译器耍的小把戏,编译器会在后台建立一个类,来包含匿名方法所表示的那个方法,而后和普通委托调用同样,通过那四部。CLR根本不知道匿名委托这个东西,就好像它不存在同样。

若是不在意参数,能够省略:delegate{...do something..},但涉及到方法重载时,要根据编译器的提示补充相应的参数。

匿名方法捕获的变量

闭包。

delegate void MethodInvoker(); void EnclosingMethod() { int outerVariable = 5; //❶ 外部变量( 未捕获的变量) 
            string capturedVariable = "captured"; //❷ 被匿名方法捕获的外部变量 
            if (DateTime. Now. Hour == 23) { int normalLocalVariable = DateTime. Now. Minute; //❸ 普通方法的局部变量 
 Console. WriteLine( normalLocalVariable); } MethodInvoker x = delegate() { string anonLocal = "local to anonymous method"; //❹ 匿名方法的局部变量 
                Console. WriteLine( capturedVariable + anonLocal); //❺ 捕获外部变量 
 }; x(); }

被匿名方法捕捉到的确实是变量, 而不是建立委托实例时该变量的值。只有在委托被执行的时候才会去采集这个被捕获变量的值:

int a = 4; MethodInvoker invoker = delegate() { a = 5; Console.WriteLine(a); }; Console.WriteLine(a);//4
            invoker();//5

要点在于,在整个方法中,咱们使用的是同一个被捕获的变量。

捕获变量的好处

 

简单地说, 捕获变量能简化避免专门建立一些类来存储一个委托须要处理的信息(除了做为参数传递的信息以外)。

捕获的变量的生命周期

对于一个捕获变量, 只要还有任何委托实例在引用它, 它就会一直存在。

delegate void MethodInvoker(); static MethodInvoker CreateMethodInvokerInstance() { int a = 4; MethodInvoker invoker = delegate () { Console.WriteLine(a); a++; }; invoker();
            return invoker; }
static void Main(string[] args) { MethodInvoker invoker = CreateMethodInvokerInstance();//4
            invoker();//5
            invoker();//6
 Console.ReadKey(); }

能够看到,CreateDelegateInstance执行完成后,它对应的栈帧已经被销毁,按道理说局部变量a也会随之寿终正寝,可是后面仍是会继续输出5和6,缘由就在于,编译器为匿名方法建立的那个类捕获了这个变量并保存它的值!CreateDelegateInstance拥有对该类的实例的一个引用,因此它能使用变量a,委托也有对该类的实例的一个引用,因此也能使用变量a。这个实例和其余实例同样都在堆上。

局部变量实例化

每当执行到声明一个局部变量的做用域时, 就称该局部变量被实例化 。

局部变量被声明到栈上,因此在for这样的结构中没必要每次循环都实例化。

局部变量屡次被声明和单次被声明产生的效果是不同的。

delegate void MethodInvoker(); static void Main(string[] args) { List<MethodInvoker> methodInvokers=new List<MethodInvoker>(); for (int i = 0; i < 10; i++) { int count = i * 10; methodInvokers.Add(delegate() { Console.WriteLine(count); count++; }); } foreach (var item in methodInvokers) { item(); } methodInvokers[0]();//1 methodInvokers[0]();//2 methodInvokers[0]();//3 methodInvokers[1]();//11 Console.ReadKey(); }

上面的例子中,count在每次循环中都从新建立一次,致使委托捕获到的变量都是新的、不同的变量,因此维护的值也不同。

若是把count去掉,换成这样:

delegate void MethodInvoker(); static void Main(string[] args) { List<MethodInvoker> methodInvokers = new List<MethodInvoker>(); for (int i = 0; i < 10; i++) { methodInvokers.Add(delegate () { Console.WriteLine(i); i++; }); } foreach (var item in methodInvokers) { item(); } methodInvokers[0](); methodInvokers[0](); methodInvokers[0](); methodInvokers[1](); Console.ReadKey(); }

此次委托直接捕获的是i这个变量,for循环中的循环变量被认为是声明在for循环外部的一个变量,相似于下面的代码:

int i=0; for(i;i<10;i++) { ..... }

注意,这个例子能够用局部变量只被实例化一次仍是屡次的道理说服,背后的原理是编译器建立的那个类实例化的地方不同。第一次用count变量来接受i的值时,在for循环的内部每循环一次编译器都会建立一个新的实例来保存count的值并被委托调用,而把count去掉时,编译器建立的这个类会在for循环外部被建立,因此只会建立一次,捕获的时i的最终的那个值因此,我猜测,编译器建立的那个类和被捕获的变量的做用域时有关系的,编译器建立的那个类的实例化的位置应该和被捕获的变量的实例化的位置或者说是做用域相同。

看下面的例子:

 

delegate void MethodInvoker(); static void Main(string[] args) { MethodInvoker[] methods=new MethodInvoker[2]; int outSide = 1; for (int i = 0; i < 2; i++) { int inside = 1; methods[i] = delegate() { Console.WriteLine($"outside:{outSide}inside:{inside}"); outSide++; inside++; }; } MethodInvoker first = methods[0]; MethodInvoker second = methods[1]; first(); first(); first(); second(); second(); Console.ReadKey(); }

这张图说明了上面的问题。

使用捕获变量时, 请参照如下规则。

  • 若是用或不用捕获变量时的代码一样简单, 那就不要用。
  • 捕获由for或foreach语句声明的变量以前, 思考你的委托是否须要在循环迭代结束以后延续, 以及是否想让它看到那个变量的后续值。 若是须要, 就在循环内另建一个变量, 用来复制你想要的值。( 在 C# 5 中, 你 没必要 担忧 foreach 语句, 但 仍需 当心 for 语句。) 若是建立多个委托实例(不论是在循环内, 仍是显式地建立), 并且捕获了变量, 思考一下是否 但愿它们捕捉同一个变量。
  • 若是捕捉的变量不会发生改变( 不论是在匿名方法中, 仍是在包围着匿名方法的外层方法主体中), 就不须要有这么多担忧。
  • 若是你建立的委托实例永远不从方法中“ 逃脱”, 换言之, 它们永远不会存储到别的地方, 不会返回, 也不会用于启动线程—— 那么事情就会简单得多。
  • 从垃圾回收的角度, 思考任 捕获变量被延长的生存期。 这方面的问题通常都不大, 但假如捕获的对象会产生昂贵的内存开销, 问题就会凸现出来。

[英]Jon Skeet. 深刻理解C#(第3版) (图灵程序设计丛书) (Kindle 位置 4363-4375). 人民邮电出版社. Kindle 版本.

 本章划重点

  • 捕获的是变量, 而不是建立委托实例时它的值。
  • 捕获的变量的生存期被延长了, 至少和捕捉它的委托同样 长。
  • 多个委托能够捕获同一个变量……
  • …… 但在循环内部, 同一个变量声明实际上会引用不一样的变量“ 实例”。
  • 在for循环的声明中建立的变量仅在循环持续期间有效—— 不会在每次循环迭代时都实例化。 这一状况对 C# 5以前的foreach语句也适用。
  • 必要时建立额外的类型来保存捕获变量。 要当心! 简单几乎老是比耍小聪明好。
相关文章
相关标签/搜索