新版C#編譯器關於函數閉包的一處更改

感謝@DiryBoy的補充,他提到這個問題在MSDN上是有說明的:程序員

http://msdn.microsoft.com/en-us/library/vstudio/hh678682.aspx編程

 

Visual Basic.NET中,若是你寫下相似下面的代碼:閉包

 

    Public Sub Test()框架

        For i = 0 To 100函數式編程

            Dim func = Function(x) x * i函數

        Next優化

End Subthis

 

Visual Studio會給出一個警告,說在lambda表達式(即匿名函數)中直接使用循環變量可能致使意料以外的結果,建議程序員先將循環變量複製一份,而後再使用。spa

直接使用循環變量究竟會產生什麼意外結果呢?本人並無用VB.NET嘗試過,可是在多年的C#開發中多次碰到相似問題,以致於向下屬定下規矩:循環變量用於匿名函數必須複製一份。在C#中,在匿名函數中直接使用循環變量並不會像VB.NET那樣給出警告,因此你每每根本不會意識到程序的運行可能與預想不一致。code

看下面的例子。建立一個WPF應用程序,在窗口中擺放10Button,而且寫上1-10的數字。咱們程序的邏輯很簡單,就是當用戶單擊按鈕時,彈出一個消息框,顯示所單擊按鈕上的數字。熟悉WPFC#函數式語法的童鞋很快就能寫出下面的代碼。

 

//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時,發現程序死活不對。排查了半天,才發現是因爲這個坑爹的問題,進而發現VS2012VS2010表現不一樣。我認爲這個修復具備重大意義,畢竟,留心複製變量是比較彆扭的,也容易遺忘。

 

不過,本人仍有一些疑惑,特在此向廣大園友請教。

C#編譯器csc.exe是隨.NET Framework一同安裝的,也就是說,當項目的.NET版本一致時,所使用的編譯器應當是同一個。既然如此,又爲什麼會出現不一樣VS版本編譯出的程序不一樣的狀況呢?

相關文章
相關標籤/搜索