C#由变量捕获引起对闭包的思考

简介:

前言

偶尔翻翻书籍看看原理性的东西确实有点枯燥,之前有看到园中有位园友说到3-6年工作经验的人应该了解的.NET知识,其中就有一点是关于C#中的闭包,其实早之前在看书时(之前根本不知道C#中还有闭包这一说)看到对于闭包的内容篇幅很少而且介绍的例子一看就懂(最终也就是有个印象而已),反正工作又用不到来让你去实现闭包,于是乎自己心存侥幸心理,这两天心血来潮再次翻了翻书想仔细研究一番(或许是出于内心的惶恐吧,工作几年竟然不知道闭包,就算知道而且仅止于了解,你是在自欺欺人么),紧接着就查了下资料来研究研究这个东西,若有错误之处,请指出。

话题

首先来我们来看看委托的演变进化史,有人问了,本节的主题不是【C#由变量捕获引起对闭包的思考】?哦,看的还仔细,是的,咱能别着急么,又有人问了,你之前不是写过有关委托的详细介绍么,哦,看来还是我的粉丝知道的还挺多,但是这个介绍侧重点不同啦,废话少来,进入主题才是真理。

C#1.0之delegate

我们今天知道过来一个列表可以通过lamda如where或者predicate来实现,而在C#1.0中我们必须来写一个方法来实现predicate的逻辑,紧接着创建委托实例是通过指定的方法名来创建。我们来创建一个帮助类(ListUtil),如下:

复制代码
/// <summary>
    /// 操作list帮助类
    /// </summary>
    static class ListUtil
    {
        /// <summary>
        /// 创建一个predicate
        /// </summary>
        public static IList<T> Filter<T>(IList<T> source, Predicate<T> predicate)
        {
            List<T> ret = new List<T>();
            foreach (T item in source)
            {
                if (predicate(item))
                {
                    ret.Add(item);
                }
            }
            return ret;
        }

        /// <summary>
        ///遍历列表并在控制台上进行打印
        /// </summary>
        public static void Dump<T>(IList<T> list)
        {
            foreach (T item in list)
            {
                Console.WriteLine(item);
            }
            Console.ReadKey();
        }
    }
复制代码

同时给出一个测试数据:

复制代码
    static class SampleData
    {
        public static readonly IList<string> Words =
            new List<string> { "the", "quick", "brown", "fox", "jumped",
             "over", "the", "lazy", "dog" }.AsReadOnly();    
    }
复制代码

现在我们要做的是返回其长度小等于4的字符串并打印,给出长度小于4的方法:

        static bool MatchFourLettersOrFewer(string item)
        {
            return item.Length <= 4;
        }

下面我们在控制台调用上述方法来进行过滤:

  Predicate<string> predicate = new Predicate<string>(MatchFourLettersOrFewer);
            IList<string> shortWords = ListUtil.Filter(SampleData.Words, predicate);
            ListUtil.Dump(shortWords);

结果打印如下:

上述一切都是so easy!当我们利用委托来实现时只是简单的进行一次调用不会多次用到,为了精简代码,此时匿名方法出现在C# 2.0.

C#2.0之delegate 

上述在控制台进行调用方法我们稍作修改即可达到同样效果,如下:

复制代码
            Predicate<string> predicate = 
                delegate(string item) 
                {
                    return item.Length <= 4;
                };
            IList<string> shortWords = ListUtil.Filter(SampleData.Words, predicate);
            ListUtil.Dump(shortWords);
复制代码

好了,到了这里貌似有点浪费篇幅,到这里我们反观上述代码,对于predicate中过滤数据长度都是硬编码,缺少点什么,我们首先要讲的是闭包,那对于闭包需要的可以基本概括为:闭包是函数与其引用环境组合而成的实体(来源于:你必须知道的.NET)。我们可以将其理解为函数与上下文的综合。我们需要来通过手动输入过滤数据的长度来给出一个上下文。我们给出一个过滤长度的类(VariableLengthMather):

复制代码
    public class VariableLengthMatcher
    {
        int maxLength;

        /// <summary>
        /// 将手动输入的数据进行传递
        /// </summary>
        /// <param name="maxLength"></param>
        public VariableLengthMatcher(int maxLength)
        {
            this.maxLength = maxLength;
        }

        /// <summary>
        /// 类似于匿名方法
        /// </summary>
        public bool Match(string item)
        {
            return item.Length <= maxLength;
        }
    }
复制代码

下面我们来进行手动输入调用以此来过滤数据:

复制代码
            Console.Write("Maximum length of string to include? ");
            int maxLength = int.Parse(Console.ReadLine());

            VariableLengthMatcher matcher = new VariableLengthMatcher(maxLength);
            Predicate<string> predicate = matcher.Match;
            IList<string> shortWords = ListUtil.Filter(SampleData.Words, predicate);
            ListUtil.Dump(shortWords);
复制代码

演示如下:

接着我们将上述控制台代码进行如下改造:

复制代码
            Console.Write("Maximum length of string to include? ");
            int maxLength = int.Parse(Console.ReadLine());
            VariableLengthMatcher matcher = new VariableLengthMatcher(maxLength);
            Predicate<string> predicate = matcher.Match;
            IList<string> shortWords = ListUtil.Filter(SampleData.Words, predicate);
            ListUtil.Dump(shortWords);
            Console.WriteLine("Now for words with <= 5 letters:");
            maxLength = 5;
            shortWords = ListUtil.Filter(SampleData.Words, predicate);
            ListUtil.Dump(shortWords);
复制代码

我们只是将maxLength值改变了下,再次进行打印,演示结果如下:

C#3.0之delegate

为了更好的演示代码,我们利用C#3.0中lamda表达式来进行演示,我们继续接着上述来讲,当我们将maxLength修改为5时,此时过滤的数据和4一样,此时我们利用匿名方法或者lamda表达式同样进行如上演示,如下。

复制代码
            Console.Write("Maximum length of string to include? ");
            int maxLength = int.Parse(Console.ReadLine());

            Predicate<string> predicate = item => item.Length <= maxLength;
            IList<string> shortWords = ListUtil.Filter(SampleData.Words, predicate);
            ListUtil.Dump(shortWords);

            Console.WriteLine("Now for words with <= 5 letters:");
            maxLength = 5;
            shortWords = ListUtil.Filter(SampleData.Words, predicate);
            ListUtil.Dump(shortWords);
复制代码

看看演示结果:

从上述演示结果可以看出此时的maxLength为5,当然打印过滤的结果则不一样,这个时候就得说到第一个话题【变量捕获】。在C# 2.0和3.0中的匿名方法和lambda表达式都能捕获到本地变量。

那么问题来了,什么是变量捕获呢?我们怎么去理解呢?

我们接下来利用lambda表达式再来看一个例子:

            var name = "cnblogs";
            Func<String> capture = () => name;
            name = "xpy0928";
            Print(capture);
        static void Print(Func<string> capture)
        {
            Console.WriteLine(capture());
            Console.ReadKey();
        }

那么打印的结果将会是什么呢?cnblogs?xpy0928?

name被捕获,当本地变量发生改变时lambda也同样作出对应的改变(因lambda会延迟执行),所以会输出xpy0928。那么编译器到底做了什么才使得输出xpy0928呢?编译器内部将上述代码进行了大致如下转换。

复制代码
    public class Capture
    {
        public string name;
        public string printName()
        {
            return this.name;
        }
    }
复制代码
    var capture = new Capture();
    capture.name = "cnblogs";
    capture.name = "xpy0928";
    Print(capture.printName);

到了这里想必我们能够理解了捕获变量的意义lambda始终指向当前对象中的name值即对象中的引用始终存在于lamda中。

接下来就要说到闭包,在此之前一直在讨论变量捕获,因为闭包产生的源头就是变量捕获。(个人理解,若有错误请指正)。

变量捕获的结果就是编译器将产生一个对象并将该局部变量提升为实例变量从而达到延长局部变量的生命周期,存储到的这个对象叫做所谓的闭包。

上述这句话又是什么意思?我们再来看一个例子:

复制代码
            List<Func<int>> funcs = new List<Func<int>>();

            for (int j = 0; j < 10; j++)

                funcs.Add(() => j);

            foreach (Func<int> func in funcs)

                Console.WriteLine(func());
            Console.ReadKey();
复制代码

有人说上述例子就是闭包,对,是闭包且结果返回10个10,恩完事!不能就这样吧,我们还得解释清楚。我们一句一句来看。

  funcs.Add(() => j);

()=>j代表什么意思,来我们来看看之前lambda表示式的六部进化曲:

实例化一个匿名委托并返回j值,注意这里说()=>j是返回变量j的当前值而非返回值j。返回的匿名委托为一个匿名类并访问此类中的属性j(为什么说返回的匿名委托为一个匿名类,请看此链接:http://www.cnblogs.com/jujusharp/archive/2011/08/04/C-Sharp-And-Closure.html) 。好说完这里,我们再来解释为什么打印10个10?

此时创建的每个匿名委托都捕获了这个变量j,所以每个匿名委托即匿名类保持了对字段j的引用,当for循环完毕时即10时此时字段值全变为10,直到j不被匿名委托所引用,j才会被垃圾回收器回收。

我们再来看看对上述进行修改:

复制代码
            List<Func<int>> funcs = new List<Func<int>>();

            for (int j = 0; j < 10; j++)
            {

                int tempJ = j;

                funcs.Add(() => tempJ);

            }
            foreach (Func<int> func in funcs)
                Console.WriteLine(func());
            Console.ReadKey();
复制代码

很明显将输出0-9因为此时创建了一个临时变量tempJ,当每次进行迭代时匿名委托即lambda捕获的是不同的tempJ,所以此时能按照我们预期所输出。

我们再来看一种情况:

复制代码
             for (int j = 0; j < 10; j++)
            {
                Func<int> fun = () => j;
                Console.WriteLine(fun());

            }
复制代码

此时还是正常输出0-9,因为此时lambda表达式每次迭代完立即执行,而不像第一个例子直到延迟到循环到10才开始进行所有的lambda执行。

总结 

闭包概念:闭包允许你将一些行为封装,将它像一个对象一样传来递去,而且它依然能够访问到原来第一次声明时的上下文。这样可以使控制结构、逻辑操作等从调用细节中分离出来。

作用:(1)利于代码精简。(2)利于函数编程。(3)代码安全。





本文转自Jeffcky博客园博客,原文链接:http://www.cnblogs.com/CreateMyself/p/5573643.html,如需转载请自行联系原作者

目录
相关文章
|
4月前
|
存储 C# 图形学
【Unity 3D】C#数据类型和变量、命名规范的讲解(附源码)
【Unity 3D】C#数据类型和变量、命名规范的讲解(附源码)
130 1
|
20天前
|
存储 C# 索引
C# 一分钟浅谈:变量与数据类型简介
【9月更文挑战第1天】在 C# 编程中,了解变量与数据类型至关重要。本文详细介绍了 C# 中的值类型(如整数、浮点数、布尔值等)和引用类型(如类、接口、数组、字符串)。通过示例代码展示了变量的声明与使用方法,并针对数据类型转换错误、变量未初始化及数值溢出等常见问题提供了解决方案。正确选择数据类型不仅能提升程序性能,还可避免潜在错误,有助于编写高质量代码。
117 47
|
1月前
|
传感器 开发框架 JSON
聊聊 C# dynamic 类型,并分享一个将 dynamic 类型变量转为其它类型的技巧和实例
聊聊 C# dynamic 类型,并分享一个将 dynamic 类型变量转为其它类型的技巧和实例
|
3月前
|
存储 C# 开发者
C# 编程基础:注释、变量、常量、数据类型和自定义类型
C# 编程基础:注释、变量、常量、数据类型和自定义类型
|
3月前
|
存储 Java 编译器
C# 变量与参数详解
C# 变量与参数详解
|
3月前
|
开发框架 .NET 程序员
掌握C#语言的精髓:基础知识与实用技能详解(数据类型与变量+ 条件与循环+函数与模块+LINQ+异常+OOP)
掌握C#语言的精髓:基础知识与实用技能详解(数据类型与变量+ 条件与循环+函数与模块+LINQ+异常+OOP)
25 0
|
4月前
|
存储 C# 开发工具
22.C# 中使用变量记录玩家创建的角色名:实现与游戏角色的互动
22.C# 中使用变量记录玩家创建的角色名:实现与游戏角色的互动
47 0
|
4月前
|
程序员 编译器 C#
C#变量命名规则
C#变量命名规则
48 0
|
4月前
|
存储 C# 开发者
C#变量类型
C#变量类型
41 0
|
4月前
|
存储 C# 容器
掌握 C# 变量:在代码中声明、初始化和使用不同类型的综合指南
变量是用于存储数据值的容器。 在 C# 中,有不同类型的变量(用不同的关键字定义),例如: int - 存储整数(没有小数点的整数),如 123 或 -123 double - 存储浮点数,有小数点,如 19.99 或 -19.99 char - 存储单个字符,如 'a' 或 'B'。Char 值用单引号括起来 string - 存储文本,如 "Hello World"。String 值用双引号括起来 bool - 存储具有两个状态的值:true 或 false
81 2