2013-03-07 62 views
4

我有以下簡單的代碼:封閉拍攝變量修改原始以及

static void Main(string[] args) 
{ 
    int j = 0; 
    Func<int> f =() => 
    { 
     for (int i = 0; i < 3; i++) 
     { 
      j += i; 
     } 
     return j; 
    }; 

    int myStr = f(); 
    Console.WriteLine(myStr); 
    Console.WriteLine(j); 
    Console.Read(); 
} 

從我讀的時候倒閉都參與其中,一個新的類型由編譯器生成的,因此它可以存儲捕獲的變量,保持對它的引用。但是,當我運行下面的代碼時,兩行都顯示了3.我期待着0和3,因爲匿名方法在編譯器生成的類中有自己的變量。那麼爲什麼它也會修改外部變量?

回答

9

外部變量和閉包中的變量是相同變量。您的程序相當於:

private class Closure 
{ 
    public int j; 
    public int Method() 
    { 
     for (int i = 0; i < 3; i++) 
     { 
      this.j += i; 
     } 
     return this.j; 
    } 
} 
static void Main(string[] args) 
{ 
    Closure closure = new Closure(); 
    closure.j = 0; 
    Func<int> f = closure.Method; 
    int myStr = f(); 
    Console.WriteLine(myStr); 
    Console.WriteLine(closure.j); 
    Console.Read(); 
} 

現在清楚您爲什麼會得到觀察結果?

+0

哇,首先,你一段時間沒有看到你的回答。其次,現在非常清楚,儘管我看着IL,對我來說它有點古怪。我沒有意識到它就是這樣。 – Freeman 2013-03-07 16:04:27

+1

Eric,你說這個行爲可能會在未來的C#版本中發生變化。我知道你和MS說了你的再見。但是,這種改變是否已經達到5.0或者仍然認爲會發生? – 2013-03-07 16:07:55

+5

@ P.Brian.Mackey:lambda捕獲一個外部變量的行爲是通過設計而永遠不會改變的。但是,我們在C#5中做了一些改變:foreach *的*循環變量現在邏輯上*在循環體內,而不是邏輯上*在循環體外部。因此,每當關閉foreach的循環變量時,就會得到一個* fresh *變量來關閉C#5.0中的變量,而不是像在C#4.0中那樣的共享變量。 – 2013-03-07 16:09:48

4

這是關閉如何工作,他們捕獲變量,而不是值。所以j將被改變。

如果你不希望出現這種情況,你可以這樣做:

static void Main(string[] args) 
{ 
    int j = 0; 
    Func<int> f =() => 
    { 
     int k = j; 
     for (int i = 0; i < 3; i++) 
     { 
      k += i; 
     } 
     return k; 
    }; 
    int myStr = f(); 
    Console.WriteLine(myStr); 
    Console.WriteLine(j); 
    Console.Read(); 
} 

j仍然被封閉拍攝,但不能修改。只有副本k被修改。

編輯:

您正確的注意,這不會爲引用類型的工作。在這種情況下,k = j存儲對對象的引用副本。對象仍然有一個副本被引用,因此對該對象的任何修改都會影響這兩個變量。

這裏是你將如何使用封閉的引用類型,而不是更新原始變量的例子:

static void Main(string[] args) 
{ 
    Foo j = new Foo(0); 
    Func<Foo> f =() => 
    { 
     Foo k = new Foo(j.N); // Can't just say k = j; 
     for (int i = 0; i < 3; i++) 
     { 
      k.N += 1; 
     } 
     return k; 
    }; 

    Console.WriteLine(f().N); 
    Console.WriteLine(j.N); 
    Console.Read(); 
} 

public class Foo 
{ 
    public int N { get; set; } 

    public Foo(int n) { N = n; } 
} 

然而,字符串是不變引用類型,你居然可以只是說k = j ,與任意參考類型不同。考慮不變性的一種方法是,每次更新字符串的值時,實際上都是在創建一個新實例。所以k = k + "1"就像是說k = new String(k + "1")。此時,它不再是與j相同的字符串的引用。

+0

起初它看起來很奇怪的行爲,儘管k是一個值類型,因此它可能是j的新副本,所以這就是爲什麼只有k受影響 – Freeman 2013-03-07 15:58:38

+0

拷貝變量是否提供了相同的行爲參考類型?例如,j是一個字符串。 – Freeman 2013-03-07 16:00:05

+0

@Freeman:試試吧! – 2013-03-07 16:01:54

3

語言規範說:

匿名方法是在Lisp程序設計語言類似lambda函數。 C#2.0支持創建「閉包」,其中匿名方法訪問周圍的局部變量和參數。

而'j'在你的情況是周圍的變量。