委托

前言: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. C语言 设一个函数process,调用它时,实现不同功能。

    //凯鲁嘎吉 - 博客园 http://www.cnblogs.com/kailugaji/ 输入a, b,第一次调用process找最大值,第二次调用process找最小值,第三次调用求和. 方法1 ...

  2. 自定义Chrome浏览器

    一.全局 自用备份,窗体透明化.要添加对应网站的窗体class到对应的位置 /*主页背景*/ /*https://images.cnblogs.com/cnblogs_com/AardWolf/135 ...

  3. 《软工实践》第零次作业 - 一些QA

    <软工实践>第零次作业 - 一些QA Q&A (1)回想一下你初入大学时对计算机专业的畅想 当初你是如何做出选择计算机专业的决定的? 你认为过去两年中接触到的课程是否符合你对计算机 ...

  4. 一文读懂SpringCloud与Eureka,Feign,Ribbon,Hystrix,Zuul核心组件间的关系

    概述 毫无疑问,Spring Cloud是目前微服务架构领域的翘楚,无数的书籍博客都在讲解这个技术.不过大多数讲解还停留在对Spring Cloud功能使用的层面,其底层的很多原理,很多人可能并不知晓 ...

  5. nginx: [emerg] unknown directive "stub_status" in /usr/local/openresty/nginx/conf/conf.d/ngx_metric.conf:19

    问题分析 Nginx没有添加modules/ngx_http_stub_status_module.o模块. 问题解决 没有安装的话,可以在tar包安装编译的时候添加如下参数: # ./configu ...

  6. Java面试知识点之线程篇(一)

    前言:在Java面试中,一定会遇到线程相关问题,因此笔者在这里总结Java中有关线程方面知识点,多数从网上得来(文中会贴出主要参考链接),有些也是笔者在面试中所遇到的问题,如有错误,请不吝指正.主要参 ...

  7. Python中的单例模式——装饰器实现剖析

    Python中单例模式的实现方法有多种,但在这些方法中属装饰器版本用的广,因为装饰器是基于面向切面编程思想来实现的,具有很高的解耦性和灵活性. 单例模式定义:具有该模式的类只能生成一个实例对象. 先将 ...

  8. 吴恩达课后作业学习2-week3-tensorflow learning-1-例子学习

    参考:https://blog.csdn.net/u013733326/article/details/79971488 使用TensorFlow构建你的第一个神经网络 我们将会使用TensorFlo ...

  9. Python 包内的导入问题(绝对导入和相对导入)

    基本概念 Python 中的包,即包含 __init__.py 文件的文件夹. 对于 Python 的包内导入,即包内模块导入包内模块,存在绝对导入和相对导入问题. 普通 Python 模块的搜索路径 ...

  10. 多线程爬虫爬取详情页HTML

    注意:如果想爬取详情页的信息请按须添加方法 import requests import os import re import threading from lxml import etree #爬 ...