委托

前言: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. bootstrap的datepicker在选择日期后调用某个方法

    bootstrap的datepicker在选择日期后调用某个方法 2016-11-08 15:14 1311人阅读 评论(0) 收藏 举报 首先感谢网易LOFTER博主Ivy的博客,我才顿悟了问题所在 ...

  2. 寒假训练——搜索 K - Cycle

    A tournament is a directed graph without self-loops in which every pair of vertexes is connected by ...

  3. Java基础知识点(三)

    前言:准备将Java基础知识点总结成一个系列,用于平常复习并加深理解.每篇尽量做到短小精悍,便于阅读. 1.Math类中相关函数 Math.floor(x):返回不大于x的最大整数.eg:Math.f ...

  4. 【ZJOI2017】仙人掌

    [ZJOI2017]仙人掌 参考博客:https://www.cnblogs.com/wfj2048/p/6636028.html 我们先求出\(dfs\)树(就是\(dfs\)一遍),然后问题就变成 ...

  5. Linux平台上轻松安装与配置Domino

    Linux平台上轻松安装与配置Domino Domino Server的编译安装过程中需要用到libstdc++-2.9和glibc-2.1.1(或者其更高的版本)两个编译模块,它们是Linux开发编 ...

  6. Linux vi/vim编辑器

    所有的 Unix Like 系统都会内建 vi 文书编辑器,其他的文书编辑器则不一定会存在. 但是目前我们使用比较多的是 vim 编辑器. vim 具有程序编辑的能力,可以主动的以字体颜色辨别语法的正 ...

  7. 5.05-requests_cookies2

    import requests # 请求数据url member_url = 'https://www.yaozh.com/member/' headers = { 'User-Agent': 'Mo ...

  8. 5.03-requests_ssl

    import requests url = 'https://www.12306.cn/mormhweb/' headers = { 'User-Agent': 'Mozilla/5.0 (Macin ...

  9. BZOJ2521:[SHOI2010]最小生成树(最小割)

    Description Secsa最近对最小生成树问题特别感兴趣.他已经知道如果要去求出一个n个点.m条边的无向图的最小生成树有一个Krustal算法和另一个Prim的算法.另外,他还知道,某一个图可 ...

  10. Spring Cloud Config(配置中心)

    每天学习一点点 编程PDF电子书.视频教程免费下载:http://www.shitanlife.com/code 一.简介 Spring Cloud Config为分布式系统中的外部配置提供服务器和客 ...