Có một lý do cho việc tái sử dụng biến của C # trong một foreach không?


1685

Khi sử dụng biểu thức lambda hoặc phương thức ẩn danh trong C #, chúng ta phải cảnh giác với quyền truy cập vào cạm bẫy đóng cửa được sửa đổi . Ví dụ:

foreach (var s in strings)
{
   query = query.Where(i => i.Prop == s); // access to modified closure
   ...
}

Do đóng cửa được sửa đổi, đoạn mã trên sẽ khiến tất cả các Wheremệnh đề trên truy vấn được dựa trên giá trị cuối cùng của s.

Như đã giải thích ở đây , điều này xảy ra vì sbiến được khai báo trong foreachvòng lặp ở trên được dịch như thế này trong trình biên dịch:

string s;
while (enumerator.MoveNext())
{
   s = enumerator.Current;
   ...
}

thay vì như thế này:

while (enumerator.MoveNext())
{
   string s;
   s = enumerator.Current;
   ...
}

Như đã chỉ ra ở đây , không có lợi thế về hiệu suất khi khai báo một biến ngoài vòng lặp và trong các trường hợp thông thường, lý do duy nhất tôi có thể nghĩ đến để làm điều này là nếu bạn dự định sử dụng biến ngoài phạm vi của vòng lặp:

string s;
while (enumerator.MoveNext())
{
   s = enumerator.Current;
   ...
}
var finalString = s;

Tuy nhiên, các biến được xác định trong một foreachvòng lặp không thể được sử dụng bên ngoài vòng lặp:

foreach(string s in strings)
{
}
var finalString = s; // won't work: you're outside the scope.

Vì vậy, trình biên dịch khai báo biến theo cách khiến nó rất dễ bị lỗi thường khó tìm và gỡ lỗi, trong khi không tạo ra lợi ích rõ ràng.

Có điều gì bạn có thể làm với foreachcác vòng lặp theo cách này mà bạn không thể làm được nếu chúng được biên dịch với biến có phạm vi bên trong không, hay đây chỉ là một lựa chọn tùy ý được thực hiện trước khi các phương thức ẩn danh và biểu thức lambda có sẵn hoặc phổ biến Kể từ đó đã được sửa đổi?


4
Có chuyện gì với bạn String s; foreach (s in strings) { ... }vậy?
Brad Christie

5
@BradChristie OP không thực sự nói về foreachnhưng các biểu thức lamda dẫn đến mã tương tự như được hiển thị bởi OP ...
Yahia

22
@BradChristie: Điều đó có biên dịch không? ( Lỗi: Cả loại và mã định danh đều được yêu cầu trong một tuyên bố foreach cho tôi)
Austin Salonen

32
@JakobBotschNielsen: Đó là một địa phương bên ngoài của lambda; Tại sao bạn lại cho rằng nó sẽ ở trên stack? Tuổi thọ của nó dài hơn khung stack !
Eric Lippert

3
@EricLippert: Tôi bối rối. Tôi hiểu rằng lambda thu được một tham chiếu đến biến foreach (được khai báo bên trong vòng lặp) và do đó cuối cùng bạn so sánh với giá trị cuối cùng của nó; mà tôi nhận được. Điều tôi không hiểu là cách khai báo biến trong vòng lặp sẽ tạo ra sự khác biệt nào cả. Từ quan điểm của trình biên dịch-biên dịch, tôi chỉ phân bổ một tham chiếu chuỗi (var 's') trên ngăn xếp bất kể khai báo nằm trong hay ngoài vòng lặp; Tôi chắc chắn sẽ không muốn đẩy một tham chiếu mới lên stack mỗi lần lặp!
Anthony

Câu trả lời:


1407

Trình biên dịch khai báo biến theo cách khiến nó rất dễ bị lỗi thường khó tìm và gỡ lỗi, trong khi không tạo ra lợi ích rõ ràng.

Những lời chỉ trích của bạn là hoàn toàn hợp lý.

Tôi thảo luận chi tiết vấn đề này ở đây:

Đóng trên biến vòng lặp được coi là có hại

Có điều gì bạn có thể làm với các vòng lặp foreach theo cách này mà bạn không thể làm được nếu chúng được biên dịch với một biến có phạm vi bên trong không? hoặc đây chỉ là một lựa chọn tùy ý được thực hiện trước khi các phương thức ẩn danh và các biểu thức lambda có sẵn hoặc phổ biến, và đã không được sửa đổi kể từ đó?

Cái sau Đặc tả C # 1.0 thực sự không cho biết biến vòng lặp nằm bên trong hay bên ngoài thân vòng lặp, vì nó không tạo ra sự khác biệt có thể quan sát được. Khi ngữ nghĩa đóng được giới thiệu trong C # 2.0, lựa chọn được đưa ra để đặt biến vòng lặp bên ngoài vòng lặp, phù hợp với vòng lặp "for".

Tôi nghĩ thật công bằng khi nói rằng tất cả đều hối tiếc về quyết định đó. Đây là một trong những "vấn đề" tồi tệ nhất trong C # và chúng tôi sẽ thực hiện thay đổi đột phá để khắc phục nó. Trong C # 5, biến vòng lặp foreach sẽ được logic bên trong phần thân của vòng lặp, và do đó các lần đóng sẽ nhận được một bản sao mới mỗi lần.

Các forvòng lặp sẽ không được thay đổi, và sự thay đổi sẽ không được "lại chuyển" với các phiên bản trước của C #. Do đó, bạn nên tiếp tục cẩn thận khi sử dụng thành ngữ này.


177
Trên thực tế, chúng tôi đã đẩy lùi sự thay đổi này trong C # 3 và C # 4. Khi chúng tôi thiết kế C # 3, chúng tôi đã nhận ra rằng vấn đề (đã tồn tại trong C # 2) sẽ trở nên tồi tệ hơn vì sẽ có rất nhiều lambdas (và truy vấn hiểu, đó là lambdas ngụy trang) trong các vòng lặp foreach nhờ LINQ. Tôi rất tiếc rằng chúng tôi đã chờ đợi sự cố đủ nghiêm trọng để đảm bảo sửa lỗi quá muộn, thay vì sửa nó trong C # 3.
Eric Lippert

75
Và bây giờ chúng ta sẽ phải nhớ foreachlà "an toàn" nhưng forkhông phải.
leppie

22
@michielvoo: Sự thay đổi đang phá vỡ theo nghĩa là nó không tương thích ngược. Mã mới sẽ không chạy chính xác khi sử dụng trình biên dịch cũ.
leppie

41
@Benjol: Không, đó là lý do tại sao chúng tôi sẵn sàng nhận nó. Jon Skeet đã chỉ ra cho tôi một kịch bản thay đổi quan trọng, đó là ai đó viết mã trong C # 5, kiểm tra nó và sau đó chia sẻ nó với những người vẫn đang sử dụng C # 4, người sau đó ngây thơ tin rằng nó là chính xác. Hy vọng số lượng người bị ảnh hưởng bởi một kịch bản như vậy là nhỏ.
Eric Lippert

29
Bên cạnh đó, ReSharper luôn nắm bắt được điều này và báo cáo nó là "quyền truy cập vào đóng cửa được sửa đổi". Sau đó, bằng cách nhấn Alt + Enter, nó thậm chí sẽ tự động sửa mã cho bạn. jetbrains.com/resharper
Mike Chamberlain

191

Những gì bạn đang hỏi được Eric Lippert trình bày kỹ lưỡng trong bài đăng trên blog của ông Đóng trên biến vòng lặp được coi là có hại và phần tiếp theo của nó.

Đối với tôi, lập luận thuyết phục nhất là việc có biến mới trong mỗi lần lặp sẽ không phù hợp với for(;;)vòng lặp kiểu. Bạn có muốn có một cái mới int itrong mỗi lần lặp for (int i = 0; i < 10; i++)không?

Vấn đề phổ biến nhất với hành vi này là đóng cửa biến lặp và nó có một cách giải quyết dễ dàng:

foreach (var s in strings)
{
    var s_for_closure = s;
    query = query.Where(i => i.Prop == s_for_closure); // access to modified closure

Bài đăng trên blog của tôi về vấn đề này: Đóng trên biến foreach trong C # .


18
Cuối cùng, những gì mọi người thực sự muốn khi họ viết điều này không có nhiều biến số, đó là để vượt qua giá trị . Và thật khó để nghĩ ra một cú pháp có thể sử dụng cho trường hợp đó.
Random832

1
Có, không thể đóng theo giá trị, tuy nhiên có một cách giải quyết rất dễ dàng tôi vừa chỉnh sửa câu trả lời của mình để đưa vào.
Krizz

6
Đó là đóng quá xấu trong C # đóng trên các tài liệu tham khảo. Nếu chúng đóng trên các giá trị theo mặc định, chúng ta có thể dễ dàng chỉ định đóng trên các biến thay vì ref.
Sean U

2
@Krizz, đây là trường hợp tính nhất quán bắt buộc có hại hơn là không nhất quán. Nó nên "hoạt động" như mọi người mong đợi, và rõ ràng mọi người mong đợi điều gì đó khác biệt khi sử dụng foreach thay vì vòng lặp for, với số lượng người gặp vấn đề trước khi chúng tôi biết về quyền truy cập vào vấn đề đóng cửa được sửa đổi (chẳng hạn như bản thân tôi) .
Andy

2
@ Random832 không biết về C # nhưng trong Common LISP có một cú pháp cho điều đó và nó chỉ ra rằng bất kỳ ngôn ngữ nào có biến và đóng có thể thay đổi cũng sẽ ( phải ) cũng có nó. Chúng tôi hoặc gần tham chiếu đến nơi thay đổi, hoặc một giá trị tại thời điểm nhất định (tạo ra một bao đóng). Điều này thảo luận về những thứ tương tự trong Python và Scheme ( cutđối với ref / vars và cuteđể giữ các giá trị được đánh giá trong các bao đóng được đánh giá một phần).
Will Ness

103

Bị cắn bởi điều này, tôi có thói quen bao gồm các biến được xác định cục bộ trong phạm vi trong cùng mà tôi sử dụng để chuyển sang bất kỳ bao đóng nào. Trong ví dụ của bạn:

foreach (var s in strings)
    query = query.Where(i => i.Prop == s); // access to modified closure

Tôi làm:

foreach (var s in strings)
{
    string search = s;
    query = query.Where(i => i.Prop == search); // New definition ensures unique per iteration.
}        

Một khi bạn có thói quen đó, bạn có thể tránh nó trong trường hợp rất hiếm bạn thực sự có ý định liên kết với các phạm vi bên ngoài. Thành thật mà nói, tôi không nghĩ rằng tôi đã từng làm như vậy.


24
Đó là cách giải quyết điển hình Cảm ơn sự đóng góp. Resharper đủ thông minh để nhận ra mô hình này và cũng khiến bạn chú ý, điều này thật tuyệt. Tôi đã không bị mô hình này cắn trong một thời gian, nhưng vì theo lời của Eric Lippert, "báo cáo lỗi không chính xác phổ biến nhất mà chúng tôi nhận được", tôi tò mò muốn biết lý do tại sao hơn là cách tránh nó .
StriplingWar Warrior

62

Trong C # 5.0, sự cố này đã được khắc phục và bạn có thể đóng các biến vòng lặp và nhận được kết quả mà bạn mong đợi.

Đặc tả ngôn ngữ nói:

8.8.4 Tuyên bố foreach

(...)

Một tuyên bố foreach của mẫu

foreach (V v in x) embedded-statement

sau đó được mở rộng thành:

{
  E e = ((C)(x)).GetEnumerator();
  try {
      while (e.MoveNext()) {
          V v = (V)(T)e.Current;
          embedded-statement
      }
  }
  finally {
       // Dispose e
  }
}

(...)

Vị trí của vvòng lặp while rất quan trọng đối với cách nó bị bắt bởi bất kỳ hàm ẩn danh nào xảy ra trong câu lệnh nhúng. Ví dụ:

int[] values = { 7, 9, 13 };
Action f = null;
foreach (var value in values)
{
    if (f == null) f = () => Console.WriteLine("First value: " + value);
}
f();

Nếu vđược khai báo bên ngoài vòng lặp while, nó sẽ được chia sẻ giữa tất cả các lần lặp và giá trị của nó sau vòng lặp for sẽ là giá trị cuối cùng 13, đó là những gì mà lệnh gọi fsẽ in ra. Thay vào đó, bởi vì mỗi lần lặp có một biến riêng v, fnên lần lặp được ghi lại trong lần lặp đầu tiên sẽ tiếp tục giữ giá trị 7, đó là những gì sẽ được in. ( Lưu ý: các phiên bản trước của C # được khai báo vbên ngoài vòng lặp while. )


1
Tại sao phiên bản đầu tiên này của C # tuyên bố v bên trong vòng lặp while? msdn.microsoft.com/en-GB/l
Library / aa664754.aspx

4
@colinfang Hãy chắc chắn đọc câu trả lời của Eric : Thông số kỹ thuật C # 1.0 ( trong liên kết của bạn, chúng tôi đang nói về VS 2003, tức là C # 1.2 ) thực sự không nói liệu biến số vòng lặp nằm trong hay bên ngoài thân vòng lặp, vì nó không có sự khác biệt có thể quan sát được . Khi ngữ nghĩa đóng được giới thiệu trong C # 2.0, lựa chọn được đưa ra để đặt biến vòng lặp bên ngoài vòng lặp, phù hợp với vòng lặp "for".
Paolo Moretti

1
Vì vậy, bạn đang nói rằng các ví dụ trong liên kết không phải là thông số kỹ thuật dứt khoát tại thời điểm đó?
colinfang

4
@colinfang Họ là thông số kỹ thuật dứt khoát. Vấn đề là chúng ta đang nói về một tính năng (tức là đóng chức năng) đã được giới thiệu sau (với C # 2.0). Khi C # 2.0 xuất hiện, họ quyết định đặt biến vòng lặp bên ngoài vòng lặp. Và hơn cả họ đã thay đổi ý định một lần nữa với C # 5.0 :)
Paolo Moretti
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.