感謝@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 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版本編譯出的程序不同的情況呢?
文章列表