Biến được chụp trong một vòng lặp trong C #


216

Tôi đã gặp một vấn đề thú vị về C #. Tôi có mã như dưới đây.

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());
}

Tôi hy vọng nó sẽ xuất 0, 2, 4, 6, 8. Tuy nhiên, nó thực sự xuất ra năm 10 giây.

Có vẻ như đó là do tất cả các hành động đề cập đến một biến bị bắt. Kết quả là, khi chúng được gọi, tất cả chúng đều có cùng một đầu ra.

Có cách nào để vượt qua giới hạn này để mỗi trường hợp hành động có biến được bắt riêng không?


15
Xem thêm loạt Blog của Eric Lippert về chủ đề: Đóng trên vòng lặp biến được coi là có hại
Brian

10
Ngoài ra, họ đang thay đổi C # 5 để hoạt động như bạn mong đợi trong một cuộc thảo luận. (phá vỡ sự thay đổi)
Neal Tibrewala


3
@Neal: mặc dù ví dụ này vẫn không hoạt động chính xác trong C # 5, vì nó vẫn cho ra năm 10 giây
Ian Oakes

6
Nó đã xác minh rằng nó xuất ra năm 10 giây cho đến ngày hôm nay trên C # 6.0 (VS 2015). Tôi nghi ngờ rằng hành vi này của các biến đóng là một ứng cử viên cho sự thay đổi. Captured variables are always evaluated when the delegate is actually invoked, not when the variables were captured.
RBT

Câu trả lời:


196

Có - lấy một bản sao của biến trong vòng lặp:

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

Bạn có thể nghĩ về nó như thể trình biên dịch C # tạo ra một biến cục bộ "mới" mỗi khi nó chạm vào khai báo biến. Trong thực tế, nó sẽ tạo ra các đối tượng đóng mới phù hợp và nó trở nên phức tạp (về mặt triển khai) nếu bạn tham chiếu đến các biến trong nhiều phạm vi, nhưng nó hoạt động :)

Lưu ý rằng sự cố phổ biến hơn của sự cố này là sử dụng forhoặc foreach:

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

Xem phần 7.14.4.2 của thông số C # 3.0 để biết thêm chi tiết về điều này và bài viết của tôi về việc đóng cửa cũng có nhiều ví dụ hơn.

Lưu ý rằng kể từ trình biên dịch C # 5 và hơn thế nữa (ngay cả khi chỉ định phiên bản C # trước đó), hành vi foreachđã thay đổi để bạn không còn cần phải sao chép cục bộ. Xem câu trả lời này để biết thêm chi tiết.


32
Cuốn sách của Jon cũng có một chương rất hay về điều này (đừng khiêm tốn nữa, Jon!)
Marc Gravell

35
Sẽ tốt hơn nếu tôi để người khác cắm nó;) (Tôi thú nhận rằng tôi có xu hướng bỏ phiếu trả lời đề xuất mặc dù vậy.)
Jon Skeet

2
Như mọi khi, thông tin phản hồi đến skeet@pobox.com sẽ được đánh giá cao :)
Jon Skeet

7
Đối với hành vi C # 5.0 là khác nhau (hợp lý hơn), hãy xem câu trả lời mới hơn của Jon Skeet - stackoverflow.com/questions/16264289/
mẹo

1
@Florimond: Đó không phải là cách đóng cửa hoạt động trong C #. Họ nắm bắt các biến , không phải giá trị . (Điều đó đúng bất kể vòng lặp, và dễ dàng được chứng minh bằng lambda bắt được một biến và chỉ in giá trị hiện tại bất cứ khi nào nó được thực thi.)
Jon Skeet

23

Tôi tin rằng những gì bạn đang trải nghiệm là một cái gì đó được gọi là Đóng http://en.wikipedia.org/wiki/Clences_(computer_science) . Lamba của bạn có một tham chiếu đến một biến nằm ngoài phạm vi của chính nó. Lamba của bạn không được giải thích cho đến khi bạn gọi nó và một khi nó sẽ nhận được giá trị mà biến có tại thời điểm thực hiện.


11

Đằng sau hậu trường, trình biên dịch đang tạo một lớp đại diện cho bao đóng cho lệnh gọi phương thức của bạn. Nó sử dụng một thể hiện duy nhất của lớp bao đóng cho mỗi lần lặp của vòng lặp. Mã trông giống như thế này, giúp dễ dàng hơn để xem tại sao lỗi xảy ra:

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;
    }
}

Đây thực sự không phải là mã được biên dịch từ mẫu của bạn, nhưng tôi đã kiểm tra mã của riêng tôi và nó trông rất giống với những gì trình biên dịch sẽ thực sự tạo ra.


8

Cách xung quanh này là lưu trữ giá trị bạn cần trong một biến proxy và để biến đó được bắt giữ.

I E

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

Xem giải thích trong câu trả lời chỉnh sửa của tôi. Bây giờ tôi đang tìm một chút liên quan của thông số kỹ thuật.
Jon Skeet

Haha jon, tôi thực sự chỉ cần đọc bài viết của bạn: csharpindepth.com/Articles/Ch CHƯƠNG5 / Closures.aspx Bạn làm tốt lắm bạn của tôi.
tjlevine

@tjlevine: Cảm ơn rất nhiều. Tôi sẽ thêm một tham chiếu đến câu trả lời của tôi. Tôi đã quên nó rồi!
Jon Skeet

Ngoài ra, Jon, tôi rất muốn đọc về suy nghĩ của bạn về các đề xuất đóng cửa Java 7 khác nhau. Tôi đã thấy bạn đề cập rằng bạn muốn viết một cái, nhưng tôi chưa thấy nó.
tjlevine

1
@tjlevine: Được rồi, tôi hứa sẽ cố gắng viết nó vào cuối năm nay :)
Jon Skeet

6

Điều này không có gì để làm với các vòng lặp.

Hành vi này được kích hoạt bởi vì bạn sử dụng biểu thức lambda () => variable * 2trong đó phạm vi bên ngoài variablekhông thực sự được xác định trong phạm vi bên trong của lambda.

Các biểu thức Lambda (trong C # 3 +, cũng như các phương thức ẩn danh trong C # 2) vẫn tạo ra các phương thức thực tế. Truyền các biến cho các phương thức này liên quan đến một số tình huống khó xử (truyền theo giá trị? Truyền theo tham chiếu? C # đi kèm với tham chiếu - nhưng điều này mở ra một vấn đề khác trong đó tham chiếu có thể tồn tại lâu hơn biến thực tế). Những gì C # làm để giải quyết tất cả các tình huống khó xử này là tạo ra một lớp trình trợ giúp mới ("bao đóng") với các trường tương ứng với các biến cục bộ được sử dụng trong các biểu thức lambda và các phương thức tương ứng với các phương thức lambda thực tế. Mọi thay đổi variabletrong mã của bạn thực sự được dịch để thay đổi trong đóClosureClass.variable

Vì vậy, vòng lặp while của bạn tiếp tục cập nhật ClosureClass.variablecho đến khi nó đạt đến 10, sau đó bạn cho các vòng lặp thực hiện các hành động, tất cả đều hoạt động như nhau ClosureClass.variable.

Để có được kết quả mong đợi của bạn, bạn cần tạo một sự tách biệt giữa biến vòng lặp và biến đang được đóng. Bạn có thể làm điều này bằng cách giới thiệu một biến khác, tức là:

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());
}

Bạn cũng có thể di chuyển bao đóng sang một phương thức khác để tạo sự tách biệt này:

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());
}

Bạn có thể triển khai Mult dưới dạng biểu thức lambda (đóng ẩn)

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

hoặc với một lớp người trợ giúp thực tế:

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;
}

Trong mọi trường hợp, "Đóng" KHÔNG phải là một khái niệm liên quan đến các vòng lặp , mà là các phương thức ẩn danh / biểu thức lambda sử dụng các biến trong phạm vi cục bộ - mặc dù một số cách sử dụng vòng lặp vô thức chứng minh các bẫy đóng.


5

Có, bạn cần phải phạm vi variabletrong vòng lặp và chuyển nó đến lambda theo cách đó:

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();

5

Tình trạng tương tự đang xảy ra trong đa luồng (C #, .NET 4.0].

Xem mã sau đây:

Mục đích là in 1,2,3,4,5 theo thứ tự.

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

Đầu ra thật thú vị! (Nó có thể giống như 21334 ...)

Giải pháp duy nhất là sử dụng các biến cục bộ.

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

Điều này dường như không giúp tôi. Vẫn không xác định.
Mladen Mihajlovic

0

Vì không ai ở đây trích dẫn trực tiếp ECMA-334 :

10.4.4.10 Đối với báo cáo

Kiểm tra chuyển nhượng xác định cho một tuyên bố cho mẫu:

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

được thực hiện như thể tuyên bố đã được viết:

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

Hơn nữa trong thông số kỹ thuật,

12.16.6.3 Khởi tạo các biến cục bộ

Một biến cục bộ được coi là khởi tạo khi thực thi đi vào phạm vi của biến.

[Ví dụ: Ví dụ, khi phương thức sau được gọi, biến cục bộ xđược khởi tạo và khởi tạo ba lần ba lần một lần cho mỗi lần lặp của vòng lặp.

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

Tuy nhiên, việc di chuyển khai báo xbên ngoài vòng lặp dẫn đến một lần khởi tạo x:

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

ví dụ cuối]

Khi không được chụp, không có cách nào để quan sát chính xác tần suất của một biến cục bộ được khởi tạo bởi vì thời gian tồn tại của các tức thời là khác nhau, mỗi lần sử dụng chỉ có thể sử dụng cùng một vị trí lưu trữ. Tuy nhiên, khi một hàm ẩn danh nắm bắt một biến cục bộ, các hiệu ứng của khởi tạo trở nên rõ ràng.

[Ví dụ: Ví dụ

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();
  }
}

tạo ra đầu ra:

1
3
5

Tuy nhiên, khi khai báo xđược di chuyển ra ngoài vòng lặp:

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;
}

đầu ra là:

5
5
5

Lưu ý rằng trình biên dịch được cho phép (nhưng không bắt buộc) để tối ưu hóa ba lần xuất hiện thành một thể hiện đại biểu duy nhất (§11.7.2).

Nếu một vòng lặp for khai báo một biến lặp, thì chính biến đó được coi là được khai báo bên ngoài vòng lặp. [Ví dụ: Do đó, nếu ví dụ được thay đổi để bắt chính biến lặp:

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

chỉ có một phiên bản của biến lặp được bắt giữ, tạo ra đầu ra:

3
3
3

ví dụ cuối]

Ồ phải, tôi đoán rằng nên đề cập rằng trong C ++, vấn đề này không xảy ra bởi vì bạn có thể chọn nếu biến được ghi theo giá trị hoặc bằng tham chiếu (xem: Chụp Lambda ).


-1

Nó được gọi là vấn đề đóng, chỉ cần sử dụng một biến sao chép và thế là xong.

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());
}

4
Câu trả lời của bạn khác với câu trả lời của ai đó ở trên?
Thangadurai
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.