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

委托

前言: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 = 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语句也适用。
  • 必要时创建额外的类型来保存捕获变量。 要小心! 简单几乎总是比耍小聪明好。

原文地址:https://www.cnblogs.com/pangjianxin/p/8654065.html

时间: 2024-10-12 07:39:14

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

安卓开发复习笔记——WebView组件

我们专业方向本是JAVA Web,这学期突然来了个手机App开发的课设,对于安卓这块,之前自学过一段时间,有些东西太久没用已经淡忘了 准备随笔记录些复习笔记,也当做温故知新吧~ 1.什么是WebView? WebView(网络视图)能加载显示网页,可以将其视为一个浏览器,它使用了WebKit渲染引擎加载显示网页. 废话不多说,直接上代码 1.需要在xml布局文件中声明WebView组件 1 <WebView 2 android:id="@+id/webview" 3 androi

数据库复习笔记(一)

为了解决冗余 这个地方一直有问题,不知道是什么原因?敲on primary老是出错 把数据库保存到某个磁盘的某个目录下的某个文件,下次可以用记事本打开这个.sql的文件,要运行则需要将这些内容拷贝到新建的 "新建查询"中 切换数据库using School 在特定的数据库中建表,注意有时需要人为的选择 都没有数据的脚本 生成数据库的脚本 生成表的脚本 在每一段的后面加一个go,是批处理的意思 这是什么意思? 若有重复的行则会自动去除,所以只会增加3行 修改数据类型的两种方法 先删除表,

2014年软考程序员-常考知识点复习笔记【第一章】

51CTO学院,在软考备考季特别整理了"2014年软考程序员-常考知识点复习笔记[汇总篇]",帮助各位学院顺利过关!更多软件水平考试辅导及试题,请关注51CTO学院-软考分类吧! 查看汇总:2014年软考程序员-常考知识点复习笔记[汇总篇]  常考基础知识必会 A. 排序:排序有几种,各种排序的比较,哪些排序是稳定的,快排的算法; B. 查找:哈希查找.二叉树查找.折半查找的对比,哈希映射和哈希表的区别? C. 链表和数组的区别,在什么情况下用链表什么情况下用数组? D. 栈和队列的区

2014年软考程序员-常考知识点复习笔记【第四章】

51CTO学院,在软考备考季特别整理了"2014年软考程序员-常考知识点复习笔记[汇总篇]",帮助各位学院顺利过关!更多软件水平考试辅导及试题,请关注51CTO学院-软考分类吧! 查看汇总:2014年软考程序员-常考知识点复习笔记[汇总篇]  4.串 串一章需要攻破的主要堡垒有: 1. 串的基本概念,串与线性表的关系(串是其元素均为字符型数据的特殊线性表),空串与空格串的区别,串相等的条件; 2. 串的基本操作,以及这些基本函数的使用,包括:取子串,串连接,串替换,求串长等等.运用串的

Windows 程序设计 复习笔记(共 77 问)

Windows 程序设计 复习笔记(共 77 问) (个人整理,仅做复习用 :D,转载注明出处:http://blog.csdn.net/hcbbt/article/details/42706501) 知识点 双字节字符集和Unicode字符集有何区别?采用双字节字符集有何问题 双字节字符集(DBCS)编码是0-255,DBCS含有1字节代码与2字节代码,而Unicode是统一的16位系统,这样就允许表示 65536个字符.Unicode中的每个字符都是16位宽而不是8位宽.在Unicode中,

安卓开发复习笔记——Fragment+FragmentTabHost组件(实现新浪微博底部菜单)

记得之前写过2篇关于底部菜单的实现,由于使用的是过时的TabHost类,虽然一样可以实现我们想要的效果,但作为学习,还是需要来了解下这个新引入类FragmentTabHost 之前2篇文章的链接: 安卓开发复习笔记——TabHost组件(一)(实现底部菜单导航) 安卓开发复习笔记——TabHost组件(二)(实现底部菜单导航) 关于Fragment类在之前的安卓开发复习笔记——Fragment+ViewPager组件(高仿微信界面)也介绍过,这里就不再重复阐述了. 国际惯例,先来张效果图: 下面

计算机图形学 复习笔记

计算机图形学 复习笔记 (个人整理,仅做复习用 :D,转载注明出处:http://blog.csdn.net/hcbbt/article/details/42779341) 第一章 计算机图形学综述 研究内容 图形的概念:计算机图形学的研究对象 能在人的视觉系统中产生视觉印象的客观对象 包括自然景物.拍摄到的图片.用数学方法描述的图形等等 图形的要素 几何要素:刻画对象的轮廓.形状等 非几何要素:刻画对象的颜色.材质等 图形表示法 点阵表示 枚举出图形中所有的点,简称为图像. 参数表示 由图形的

安卓开发复习笔记——Fragment+ViewPager组件(高仿微信界面)

什么是ViewPager? 关于ViewPager的介绍和使用,在之前我写过一篇相关的文章<安卓开发复习笔记——ViewPager组件(仿微信引导界面)>,不清楚的朋友可以看看,这里就不再重复. 什么是Fragment? Fragment是Android3.0后新增的概念,Fragment名为碎片,不过却和Activity十分相似,具有自己的生命周期,它是用来描述一些行为或一部分用户界面在一个Activity中,我们可以合并多个Fragment在一个单独的activity中建立多个UI面板,或

[Java基础] Java线程复习笔记

先说说线程和进程,现代操作系统几乎无一例外地采用进程的概念,进程之间基本上可以认为是相互独立的,共享的资源非常少.线程可以认为是轻量级的进 程,充分地利用线程可以使得同一个进程中执行多种任务.Java是第一个在语言层面就支持线程操作的主流编程语言.和进程类似,线程也是各自独立的,有自 己的栈,自己的局部变量,自己的程序执行并行路径,但线程的独立性又没有进程那么强,它们共享内存,文件资源,以及其他进程层面的状态等.同一个进程内的 多个线程共享同样的内存空间,这也就意味着这些线程可以访问同样的变量和