簡體   English   中英

在 C# 的循環中捕獲的變量

[英]Captured variable in a loop in C#

我遇到了一個關於 C# 的有趣問題。 我有如下代碼。

List<Func<int>> actions = new List<Func<int>>();

int variable = 0;
while (variable < 5)
{
    actions.Add(() => variable * 2);
    ++ variable;
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

我希望它是 output 0、2、4、6、8。但是,它實際上輸出了五個 10。

似乎這是由於所有操作都引用了一個捕獲的變量。 結果,當它們被調用時,它們都具有相同的 output。

有沒有辦法繞過這個限制,讓每個動作實例都有自己的捕獲變量?

是的 - 在循環內獲取變量的副本:

while (variable < 5)
{
    int copy = variable;
    actions.Add(() => copy * 2);
    ++ variable;
}

您可以將其想象為 C# 編譯器每次遇到變量聲明時都會創建一個“新”局部變量。 事實上,它會創建適當的新閉包對象,如果您在多個范圍內引用變量,它會變得復雜(在實現方面),但它可以工作:)

請注意,此問題更常見的情況是使用forforeach

for (int i=0; i < 10; i++) // Just one variable
foreach (string x in foo) // And again, despite how it reads out loud

有關這方面的更多詳細信息,請參閱 C# 3.0 規范的第 7.14.4.2 節,我關於閉包的文章也有更多示例。

請注意,從 C# 5 及更高版本開始(即使指定了 C# 的早期版本), foreach的行為發生了變化,因此您不再需要制作本地副本。 有關更多詳細信息,請參閱此答案

我相信您正在經歷的是被稱為 Closure http://en.wikipedia.org/wiki/Closure_(computer_science)的東西。 您的 lamba 引用了一個變量,該變量的范圍在函數本身之外。 在調用它之前,不會解釋您的 lamba,一旦調用它,它將獲得變量在執行時的值。

在幕后,編譯器正在為您的方法調用生成一個代表閉包的類。 它為循環的每次迭代使用閉包類的單個實例。 代碼看起來像這樣,這樣更容易看出錯誤發生的原因:

void Main()
{
    List<Func<int>> actions = new List<Func<int>>();

    int variable = 0;

    var closure = new CompilerGeneratedClosure();

    Func<int> anonymousMethodAction = null;

    while (closure.variable < 5)
    {
        if(anonymousMethodAction == null)
            anonymousMethodAction = new Func<int>(closure.YourAnonymousMethod);

        //we're re-adding the same function 
        actions.Add(anonymousMethodAction);

        ++closure.variable;
    }

    foreach (var act in actions)
    {
        Console.WriteLine(act.Invoke());
    }
}

class CompilerGeneratedClosure
{
    public int variable;

    public int YourAnonymousMethod()
    {
        return this.variable * 2;
    }
}

這實際上不是您示例中的編譯代碼,但我檢查了我自己的代碼,這看起來與編譯器實際生成的非常相似。

解決此問題的方法是將您需要的值存儲在代理變量中,並捕獲該變量。

IE

while( variable < 5 )
{
    int copy = variable;
    actions.Add( () => copy * 2 );
    ++variable;
}

這與循環無關。

觸發此行為是因為您使用了 lambda 表達式() => variable * 2 ,其中外部作用域variable實際上並未在 lambda 的內部作用域中定義。

Lambda 表達式(在 C#3+ 中,以及在 C#2 中的匿名方法)仍然創建實際方法。 將變量傳遞給這些方法會遇到一些難題(按值傳遞?按引用傳遞?C# 通過引用進行 - 但這會引發另一個問題,即引用可能比實際變量更有效)。 C# 解決所有這些困境的方法是創建一個新的輔助類(“閉包”),其中的字段對應於 lambda 表達式中使用的局部變量,方法對應於實際的 lambda 方法。 代碼中對variable的任何更改實際上都會轉換為該ClosureClass.variable中的更改

因此,您的 while 循環不斷更新ClosureClass.variable直到達到 10,然后您的 for 循環執行所有操作,這些操作都在同一個ClosureClass.variable上運行。

要獲得預期的結果,您需要在循環變量和正在關閉的變量之間創建一個分隔符。 您可以通過引入另一個變量來做到這一點,即:

List<Func<int>> actions = new List<Func<int>>();
int variable = 0;
while (variable < 5)
{
    var t = variable; // now t will be closured (i.e. replaced by a field in the new class)
    actions.Add(() => t * 2);
    ++variable; // changing variable won't affect the closured variable t
}
foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

您還可以將閉包移動到另一種方法來創建這種分離:

List<Func<int>> actions = new List<Func<int>>();

int variable = 0;
while (variable < 5)
{
    actions.Add(Mult(variable));
    ++variable;
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

您可以將 Mult 實現為 lambda 表達式(隱式閉包)

static Func<int> Mult(int i)
{
    return () => i * 2;
}

或使用實際的助手類:

public class Helper
{
    public int _i;
    public Helper(int i)
    {
        _i = i;
    }
    public int Method()
    {
        return _i * 2;
    }
}

static Func<int> Mult(int i)
{
    Helper help = new Helper(i);
    return help.Method;
}

在任何情況下, “閉包”都不是與循環相關的概念,而是與使用局部范圍變量的匿名方法/ lambda 表達式有關——盡管循環的一些不小心使用證明了閉包陷阱。

是的,您需要在循環中限定variable並以這種方式將其傳遞給 lambda:

List<Func<int>> actions = new List<Func<int>>();

int variable = 0;
while (variable < 5)
{
    int variable1 = variable;
    actions.Add(() => variable1 * 2);
    ++variable;
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

Console.ReadLine();

同樣的情況也發生在多線程(C# 、.NET 4.0)中。

請參閱以下代碼:

目的是按順序打印1,2,3,4,5。

for (int counter = 1; counter <= 5; counter++)
{
    new Thread (() => Console.Write (counter)).Start();
}

輸出很有趣! (可能像 21334...)

唯一的解決方案是使用局部變量。

for (int counter = 1; counter <= 5; counter++)
{
    int localVar= counter;
    new Thread (() => Console.Write (localVar)).Start();
}
for (int n=0; n < 10; n++) //forloop syntax
foreach (string item in foo) foreach syntax

它被稱為閉包問題,只需使用一個復制變量,就可以了。

List<Func<int>> actions = new List<Func<int>>();

int variable = 0;
while (variable < 5)
{
    int i = variable;
    actions.Add(() => i * 2);
    ++ variable;
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

由於這里沒有人直接引用ECMA-334

10.4.4.10 對於語句

對形式的 for 語句進行明確的賦值檢查:

for (for-initializer; for-condition; for-iterator) embedded-statement

就像寫了語句一樣完成:

{
    for-initializer;
    while (for-condition) {
        embedded-statement;
    LLoop: for-iterator;
    }
}

進一步在規范中,

12.16.6.3 局部變量的實例化

當執行進入變量的范圍時,局部變量被認為是實例化的。

[示例:例如,當調用以下方法時,局部變量x被實例化並初始化 3 次——循環的每次迭代一次。

static void F() {
  for (int i = 0; i < 3; i++) {
    int x = i * 2 + 1;
    ...
  }
}

但是,將x x單個實例化:

static void F() {
  int x;
  for (int i = 0; i < 3; i++) {
    x = i * 2 + 1;
    ...
  }
}

結束示例]

如果未捕獲,則無法准確觀察局部變量被實例化的頻率——因為實例化的生命周期是不相交的,每個實例化可以簡單地使用相同的存儲位置。 然而,當一個匿名函數捕獲一個局部變量時,實例化的效果就變得很明顯了。

[示例:示例

using System;

delegate void D();

class Test{
  static D[] F() {
    D[] result = new D[3];
    for (int i = 0; i < 3; i++) {
      int x = i * 2 + 1;
      result[i] = () => { Console.WriteLine(x); };
    }
  return result;
  }
  static void Main() {
    foreach (D d in F()) d();
  }
}

產生輸出:

1
3
5

但是,當x的聲明移出循環時:

static D[] F() {
  D[] result = new D[3];
  int x;
  for (int i = 0; i < 3; i++) {
    x = i * 2 + 1;
    result[i] = () => { Console.WriteLine(x); };
  }
  return result;
}

輸出是:

5
5
5

請注意,允許(但不是必需)編譯器將三個實例優化為單個委托實例(第 11.7.2 節)。

如果 for 循環聲明了一個迭代變量,則該變量本身被認為是在循環之外聲明的。 [示例:因此,如果更改示例以捕獲迭代變量本身:

static D[] F() {
  D[] result = new D[3];
  for (int i = 0; i < 3; i++) {
    result[i] = () => { Console.WriteLine(i); };
  }
  return result;
}

僅捕獲迭代變量的一個實例,這會產生輸出:

3
3
3

結束示例]

哦,是的,我想應該提到的是,在 C++ 中不會出現這個問題,因為您可以選擇是通過值還是通過引用來捕獲變量(請參閱: Lambda 捕獲)。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM