委托

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

方法组转换

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

 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,需要显式的去转换:

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

协变性和逆变性

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

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

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

C#2支持如下写法:

 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中,支持了泛型类型和泛型委托的协变和逆变:

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 = ; //❶ 外部变量( 未捕获的变量)
string capturedVariable = "captured"; //❷ 被匿名方法捕获的外部变量
if (DateTime. Now. Hour == )
{
int normalLocalVariable = DateTime. Now. Minute; //❸ 普通方法的局部变量
Console. WriteLine( normalLocalVariable);
}
MethodInvoker x = delegate()
{
string anonLocal = "local to anonymous method"; //❹ 匿名方法的局部变量
Console. WriteLine( capturedVariable + anonLocal); //❺ 捕获外部变量
};
x();
}

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

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

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

捕获变量的好处

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

捕获的变量的生命周期

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

 delegate void MethodInvoker();
static MethodInvoker CreateMethodInvokerInstance()
{
int a = ;
MethodInvoker invoker = delegate ()
{ Console.WriteLine(a);
a++;
};
invoker();
return invoker;
}
 static void Main(string[] args)
{
MethodInvoker invoker = CreateMethodInvokerInstance();//
invoker();//
invoker();//
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 = ; i < ; i++)
{
int count = i * ;
methodInvokers.Add(delegate()
{
Console.WriteLine(count);
count++;
}); }
foreach (var item in methodInvokers)
{
item();
}
methodInvokers[]();//1
methodInvokers[]();//2
methodInvokers[]();//3
methodInvokers[]();//11
Console.ReadKey();
}

上面的例子中,count在每次循环中都重新创建一次,导致委托捕获到的变量都是新的、不一样的变量,所以维护的值也不一样。

如果把count去掉,换成这样:

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

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

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

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

看下面的例子:

        delegate void MethodInvoker();
static void Main(string[] args)
{
MethodInvoker[] methods=new MethodInvoker[];
int outSide = ;
for (int i = ; i < ; i++)
{
int inside = ;
methods[i] = delegate()
{
Console.WriteLine($"outside:{outSide}inside:{inside}");
outSide++;
inside++;
}; }
MethodInvoker first = methods[];
MethodInvoker second = methods[];
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语句也适用。
  • 必要时创建额外的类型来保存捕获变量。 要小心! 简单几乎总是比耍小聪明好。

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

  1. Java基础复习笔记系列 八 多线程编程

    Java基础复习笔记系列之 多线程编程 参考地址: http://blog.csdn.net/xuweilinjijis/article/details/8878649 今天的故事,让我们从上面这个图 ...

  2. Angular复习笔记7-路由(下)

    Angular复习笔记7-路由(下) 这是angular路由的第二篇,也是最后一篇.继续上一章的内容 路由跳转 Web应用中的页面跳转,指的是应用响应某个事件,从一个页面跳转到另一个页面的行为.对于使 ...

  3. Angular复习笔记7-路由(上)

    Angular复习笔记7-路由(上) 关于Angular路由的部分将分为上下两篇来介绍.这是第一篇. 概述 路由所要解决的核心问题是通过建立URL和页面的对应关系,使得不同的页面可以用不同的URL来表 ...

  4. Angular复习笔记6-依赖注入

    Angular复习笔记6-依赖注入 依赖注入(DependencyInjection)是Angular实现重要功能的一种设计模式.一个大型应用的开发通常会涉及很多组件和服务,这些组件和服务之间有着错综 ...

  5. tarjan复习笔记

    tarjan复习笔记 (关于tarjan读法,优雅一点读塔洋,接地气一点读塔尖) 0. 连通分量 有向图: 强连通分量(SCC)是个啥 就是一张图里面两个点能互相达到,那么这两个点在同一个强连通分量里 ...

  6. 树的直径,LCA复习笔记

    前言 复习笔记第6篇. 求直径的两种方法 树形DP: dfs(y); ans=max( ans,d[x]+d[y]+w[i] ); d[x]=max( d[x],d[y]+w[i] ); int di ...

  7. 状压DP复习笔记

    前言 复习笔记第4篇.CSP RP++. 引用部分为总结性内容. 0--P1433 吃奶酪 题目链接 luogu 题意 房间里放着 \(n\) 块奶酪,要把它们都吃掉,问至少要跑多少距离?一开始在 \ ...

  8. 斜率优化DP复习笔记

    前言 复习笔记2nd. Warning:鉴于摆渡车是普及组题目,本文的难度定位在普及+至省选-. 参照洛谷的题目难度评分(不过感觉部分有虚高,提高组建议全部掌握,普及组可以选择性阅读.) 引用部分(如 ...

  9. Java基础复习笔记系列 九 网络编程

    Java基础复习笔记系列之 网络编程 学习资料参考: 1.http://www.icoolxue.com/ 2. 1.网络编程的基础概念. TCP/IP协议:Socket编程:IP地址. 中国和美国之 ...

随机推荐

  1. echarts 设置图例的颜色,不设置color,echarts里面也会有默认的颜色

  2. docker1.13.1的安装与卸载及mysql5.5安装实例

    docker中国官方地址:https://www.docker-cn.com/ 您可以使用以下命令直接从该镜像加速地址进行拉取: $ docker pull registry.docker-cn.co ...

  3. 让vue-cli脚手架搭建的项目可以处理vue文件中postcss语法

    图中&属于postcss的语法,这样书写样式可以清楚的看出选择器之前的层级关系,非常好用. 在利用vue-cli脚手架搭建的项目中如果不配置是不支持这种写法的,这样写不会报错,但是样式不生效. ...

  4. Java学习笔记(四)——好记性不如烂键盘(答答租车)

    根据所学知识,编写一个控制台版的租车系统. 功能: 1. 展示所有可租车辆 2. 选择车型.租车辆 3. 展示租车清单,包含:总金额.总载货量及其车型.总载人量及其车型 代码参考imooc中Java课 ...

  5. linux中启动 java -jar 运行程序

    每天学习一点点 编程PDF电子书.视频教程免费下载:http://www.shitanlife.com/code 直接用java -jar xxx.jar,当退出或关闭shell时,程序就会停止掉.以 ...

  6. 【转】【fiddler】抓取https数据失败,全部显示“Tunnel to......443”

    这个问题是昨天下午就一直存在的,知道今天上午才解决,很感谢“韬光养晦”. 问题描述: 按照网络上的教程,设置fiddler开启解密https的选项,同时fiddler的证书也是安装到系统中,但是抓取h ...

  7. docker 15 dockerfile案例-CMD、ENTRYPOINT案例

    CMD.ENTRYPOINT都是指定一个容器启动时要运行的命令.但是CMD会覆盖前面的参数,而ENTRYP会追加组合原来的参数 未完待续...

  8. springboot配置文件中使用当前配置的变量

    在开发中,有时我们的application.properties某些值需要重复使用,比如配置redis和数据库或者mongodb连接地址,日志,文件上传地址等,且这些地址如果都是相同或者父路径是相同的 ...

  9. Java NIO5:通道和文件通道

    一.通道是什么 通道式(Channel)是java.nio的第二个主要创新.通道既不是一个扩展也不是一项增强,而是全新的.极好的Java I/O示例,提供与I/O服务的直接连接.Channel用于在字 ...

  10. mybatis-plus学习笔记(一)

    一.概述 概述见官网,不再赘述(简称mytatis-plus为MP) 在mybatis的基础之上,重点关注新特性:https://mp.baomidou.com/guide/#%E7%89%B9%E6 ...