新版C#编译器关于函数闭包的一处更改

 

在Visual Basic.NET中,如果你写下类似下面的代码:

Public Sub Test()

For i = 0 To 100

Dim func = Function(x) x * i

Next

End Sub

Visual Studio会给出一个警告,说在lambda表达式(即匿名函数)中直接使用循环变量可能导致意料之外的结果,建议程序员先将循环变量复制一份,然后再使用。

直接使用循环变量究竟会产生什么意外结果呢?本人并没有用VB.NET尝试过,但是在多年的C#开发中屡次碰到类似问题,以至于向下属定下规矩:循环变量用于匿名函数必须复制一份。在C#中,在匿名函数中直接使用循环变量并不会像VB.NET那样给出警告,所以你往往根本不会意识到程序的运行可能与预想不一致。

看下面的例子。创建一个WPF应用程序,在窗口中摆放10个Button,并且写上1-10的数字。我们程序的逻辑很简单,就是当用户单击按钮时,弹出一个消息框,显示所单击按钮上的数字。熟悉WPF和C#函数式语法的童鞋很快就能写出下面的代码。

//MainWindow.xaml

<Window x:Class="CSharpClosureTest.MainWindow"

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

Title="MainWindow" Height="300" Width="300" Loaded="Window_Loaded">

<StackPanel Name="LayoutRoot">

</StackPanel>

</Window>

//MainWindow.xaml.cs

public partial class MainWindow : Window

{

public MainWindow()

{

InitializeComponent();

}

private void Window_Loaded(object sender, RoutedEventArgs e)

{

AddButtons();

}

private void AddButtons()

{

var list = Enumerable.Range(1, 10).ToList();

foreach (var i in list)

{

Button button = new Button { Content = i };

button.Click += (sender, e) => MessageBox.Show(i.ToString());

LayoutRoot.Children.Add(button);

}

}

}

在这个代码中,很明显,我们在匿名函数中直接使用了循环变量。然而若离开本文的环境,您恐怕很难留意到这个细节。运行程序,将会得到什么结果呢?

我们在VS2012中生成、运行程序。单击一些按钮,似乎程序运行完全正确,没有什么异常情况。

然而,如果你用VS2010打开代码,重新生成并运行,就会发现出问题了。无论你单击哪个按钮,消息框弹出的数字永远是10。

这样的结果令人惊异。相同的代码、相同的.NET Framework版本,仅仅因为在不同的VS版本中编译,程序的运行结果截然不同。

我们知道,.NET框架本身是不理解函数式编程结构的,C#编译器把匿名函数编译成一些名字很怪的嵌套类型,并且把匿名函数上下文中的变量捕获下来,作为嵌套类型的私有成员变量,这就是闭包。闭包变量的捕获发生在编译时。显然,两个C#编译器对闭包变量捕获的处理不同。

为了一探究竟,验证我们的猜测,我们使用Reflector对两个VS生成的exe进行反编译。以下是得到的C#代码,注意我们已经把Reflector优化模式改为.NET1.1版,以便查看匿名函数的真实情况。

VS2012版:

private void AddButtons()
{
List<int> list = Enumerable.Range(1, 10).ToList<int>();
using (List<int>.Enumerator CS$5$0000 = list.GetEnumerator())
{
while (CS$5$0000.MoveNext())
{
RoutedEventHandler CS$<>9__CachedAnonymousMethodDelegate2 = null;
<>c__DisplayClass3 CS$<>8__locals4 = new <>c__DisplayClass3();
CS$<>8__locals4.i = CS$5$0000.Current;
Button <>g__initLocal0 = new Button();
<>g__initLocal0.Content = CS$<>8__locals4.i;
Button button = <>g__initLocal0;
if (CS$<>9__CachedAnonymousMethodDelegate2 == null)
{
CS$<>9__CachedAnonymousMethodDelegate2 = new RoutedEventHandler(CS$<>8__locals4.<AddButtons>b__1);
}
button.Click += CS$<>9__CachedAnonymousMethodDelegate2;
this.LayoutRoot.Children.Add(button);
}
}
}

VS2010版:

private void AddButtons()
{
List<int> list = Enumerable.Range(1, 10).ToList<int>();
using (List<int>.Enumerator enumerator = list.GetEnumerator())
{
RoutedEventHandler handler = null;
<>c__DisplayClass3 class2 = new <>c__DisplayClass3();
while (enumerator.MoveNext())
{
class2.i = enumerator.Current;
Button button2 = new Button();
button2.Content = class2.i;
Button element = button2;
if (handler == null)
{
handler = new RoutedEventHandler(class2.<AddButtons>b__1);
}
element.Click += handler;
this.LayoutRoot.Children.Add(element);
}
}
}

果不其然,二者存在重大差异。在VS2010的结果中,闭包对应的嵌套类型只被实例化了一次,于是在匿名函数执行时,循环变量也就是嵌套类型的私有成员保持了循环最后一次执行时被赋予的值。而在VS2012的结果中,嵌套类型被循环实例化,多个匿名函数各自对应独立的私有成员。

在大多数情况下,你我期望的都会是VS2012给出的直观的结果。我实在想象不出VS2010及之前版本给出的结果有什么应用场景。从这个意义上讲,VS2012的这个改动可以算作一个bug修复。

这个差异是我无意中发现的。当时有一段代码出现了循环变量用于匿名函数的情况,然而我自己忽略了自己定下的规矩,没有复制一份循环变量。由于是VS2012,程序一切正常。当我改用VS2010时,发现程序死活不对。排查了半天,才发现是由于这个坑爹的问题,进而发现VS2012与VS2010表现不同。我认为这个修复具有重大意义,毕竟,留心复制变量是比较别扭的,也容易遗忘。

不过,本人仍有一些疑惑,特在此向广大园友请教。

C#编译器csc.exe是随.NET Framework一同安装的,也就是说,当项目的.NET版本一致时,所使用的编译器应当是同一个。既然如此,又为何会出现不同VS版本编译出的程序不同的情况呢?

 
 
 

新版C#编译器关于函数闭包的更多相关文章

  1. 新版C#编译器关于函数闭包的一处更改

    感谢@DiryBoy的补充,他提到这个问题在MSDN上是有说明的: http://msdn.microsoft.com/en-us/library/vstudio/hh678682.aspx 在Vis ...

  2. 速战速决 (3) - PHP: 函数基础, 函数参数, 函数返回值, 可变函数, 匿名函数, 闭包函数, 回调函数

    [源码下载] 速战速决 (3) - PHP: 函数基础, 函数参数, 函数返回值, 可变函数, 匿名函数, 闭包函数, 回调函数 作者:webabcd 介绍速战速决 之 PHP 函数基础 函数参数 函 ...

  3. C++编译器的函数名修饰规则

    我们知道在C++中有函数重载这样一个东西,当我们定义了几个功能类似且函数名是一样的函数的时候,只要它的参数列表不同,编译是可以通过的,但是在C中是不可以的. double add(double a, ...

  4. Swift语法基础入门三(函数, 闭包)

    Swift语法基础入门三(函数, 闭包) 函数: 函数是用来完成特定任务的独立的代码块.你给一个函数起一个合适的名字,用来标识函数做什么,并且当函数需要执行的时候,这个名字会被用于“调用”函数 格式: ...

  5. 《JS权威指南学习总结--8.6 函数闭包》

    内容要点: 和其他大多数现代编程一样,JS也采用词法作用域,也就是说,函数的执行依赖于变量作用域,这个作用域是在函数定义时决定的,而不是函数调用时决定的. 为了实现这种词法作用域,JS函数对象的内部状 ...

  6. 三个JS函数闭包(closure)例子

    闭包是JS较难分辨的一个概念,我只是按自己的理解写下来,如有不对还请指出. 函数闭包是指当一个函数被定义在另一个函数内部时,这个内部函数使用到的变量会被封闭起来形成一个闭包,这些变量会保持形成闭包时设 ...

  7. Python基础_函数闭包、调用、递归

    这节的主要内容是函数的几个用法闭包,调用.递归. 一.函数闭包 对闭包更好的理解请看:https://www.cnblogs.com/Lin-Yi/p/7305364.html 我们来看一个简单的例子 ...

  8. JavaScript的函数闭包详细解释

    闭包是指有权访问另一个函数作用域中的变量的函数 一.创建闭包的常见的方式: 就是在一个函数内部创建另一个函数,通过另一个函数访问这个函数的局部变量. //通过闭包可以返回局部变量 function b ...

  9. JavaScript碎片—函数闭包(模拟面向对象)

    经过这几天的博客浏览,让我见识大涨,其中有一篇让我感触犹深,JavaScript语言本身是没有面向对象的,但是那些大神们却深深的模拟出来了面向对象,让我震撼不已.本篇博客就是在此基础上加上自己的认知, ...

随机推荐

  1. iOS程序 # 启动过程

    [ App 应用 ] 中文名:缺省 外文名:default 拼音:quē shěng 释义:系统默认状态 全称:缺省状态 -------------- 1.程序启动顺序 1> main.m程序入 ...

  2. 如何从 0 开始学 Ruby on Rails

    如何从 0 开始学 Ruby on Rails (漫步版)Ruby 是一门编程语言,Ruby on Rails 是 Ruby 的一个 web 框架,简称 Rails. 有很多人对 Rails 感兴趣, ...

  3. 拾人牙慧,浅记一些C++的类

    这两天没事又翻了翻Primer,发现自己上岁数了,记单词能力开始下降,索引把一些简单的例子记下来,把一些肥肉剔除,剩一下骨头,方便今后Ctrl+F.  在此感谢:   http://ticktick. ...

  4. iOS MapKit导航及地理转码辅助类

    头文件: #import <Foundation/Foundation.h> #import <MapKit/MapKit.h> @interface DirectionRou ...

  5. SQL编程之生日问题

    在学习MySQL的时候,一个较为经典的SQL编程题目就是生日问题,已知某个用户的出生日期和当前日期,计算他近期的生日. 一般须要考虑两个问题 闰年2月是29天 今年的生日是否过完 比如:某人的生日是1 ...

  6. C语言双向链表

    原文:C语言双向链表 今天写了点双向链表的各种操作,写插入的时候费了点时间,不过,现在看来还是值得耗费那点时间去写的,这种小东西应该能信手拈来才行啊. /*双向链表*/ #include <st ...

  7. Hibernate实体映射配置(XML)简单三步完美配置

    我们在使用Hibernate框架的时候,非常纠结的地方就是实体和表之间的映射,今天借助汤老师的思路写了小教程,以后配置不用纠结了! 第一步:写注释 格式为:?属性,表达的是本对象与?的?关系. 例:“ ...

  8. C#抓取AJAX页面的内容

    原文 C#抓取AJAX页面的内容 现在的网页有相当一部分是采用了AJAX技术,所谓的AJAX技术简单一点讲就是事件驱动吧(当然这种说法可能很不全面),在你提交了URL后,服务器发给你的并不是所有是页面 ...

  9. 第4章2节《MonkeyRunner源码剖析》ADB协议及服务: ADB服务SERVICES.TXT翻译参考(原创)

    天地会珠海分舵注:本来这一系列是准备出一本书的,详情请见早前博文“寻求合作伙伴编写<深入理解 MonkeyRunner>书籍“.但因为诸多原因,没有如愿.所以这里把草稿分享出来,所以错误在 ...

  10. JS判断字符串是否全为中文

    //第一种代码(全为中文则返回"true",不全为中文则返回"false"): <script language="javascript&quo ...