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


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

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

Do việc đóng cửa đã sửa đổi, mã trên sẽ gây ra tất cả Where các mệnh đề trên truy vấn 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ì s biến được khai báo trong foreach vò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 năng nào để khai báo một biến ngoài vòng lặp, và trong các trường hợp bình thường, lý do duy nhất tôi có thể nghĩ đến để làm điều này là nếu bạn đị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 foreach vò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 làm cho nó dễ bị lỗi thường khó tìm và gỡ lỗi, trong khi không tạo ra lợi ích đáng kể.

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


1484
2018-01-17 17:21


gốc


Có chuyện gì với String s; foreach (s in strings) { ... }? - Brad Christie
@ Bradradie OP không thực sự nói về foreachnhưng về các biểu thức lamda dẫn đến mã tương tự như được hiển thị bởi OP ... - Yahia
@ Bradradie: Liệu biên dịch đó? (Lỗi: Nhập và nhận dạng được yêu cầu trong một tuyên bố foreach cho tôi) - Austin Salonen
@ JakobBotschNielsen: Đó là một địa phương bên ngoài đóng cửa của một lambda; tại sao bạn giả định rằng nó sẽ được trên stack ở tất cả? Cuộc đời của nó là dài hơn khung ngăn xếp! - Eric Lippert
@EricLippert: Tôi đang bối rối. Tôi hiểu rằng lambda nắm bắt một tham chiếu đến biến foreach (được khai báo nội bộ ở ngoài vòng lặp) và do đó bạn kết thúc so sánh với giá trị cuối cùng của nó; mà tôi nhận được. Những gì tôi không hiểu là cách khai báo biến phía trong vòng lặp sẽ tạo ra bất kỳ 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 stack bất chấp việc 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 lại! - Anthony


Các câu trả lời:


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

Lời chỉ trích của bạn 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ó cái gì bạn có thể làm với foreach vòng theo cách này mà bạn không thể nếu họ đã được biên soạn với một biến bên trong phạm vi? hoặc đây chỉ là một sự lựa chọn tùy ý được thực hiện trước khi các phương thức nặc danh và các biểu thức lambda có sẵn hoặc phổ biến, và nó chưa được sửa đổi kể từ đó?

Cái sau. Đặc điểm kỹ thuật C # 1.0 thực sự không nói biến vòng lặp nằm trong hay ngoài vòng lặp, vì nó không tạo ra sự khác biệt quan sát được. Khi ngữ nghĩa đóng đã được giới thiệu trong C # 2.0, sự lựa chọn đã được thực hiện để đặ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 hận vì quyết định đó. Đây là một trong những "gotchas" tồi tệ nhất trong C # và chúng ta sẽ thay đổi đột phá để sửa nó. Trong C # 5 biến vòng lặp foreach sẽ được logic phía trong cơ thể của vòng lặp, và do đó đóng cửa sẽ nhận được một bản sao mới mỗi lần.

Các for vòng lặp sẽ không được thay đổi và thay đổi sẽ không được "chuyển về" thành 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.


1280
2018-01-17 17:56



Chúng tôi đã thực sự đẩ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ẽ tồi tệ hơn vì sẽ có rất nhiều lambdas (và truy vấn comprehensions, đó là lambdas trong ngụy trang) trong vòng foreach nhờ LINQ. Tôi rất tiếc là chúng tôi đã chờ đợi vấn đề này đủ tồi tệ để đảm bảo sửa chữa nó quá muộn, thay vì sửa nó trong C # 3. - Eric Lippert
Và bây giờ chúng ta sẽ phải nhớ foreach là 'an toàn' nhưng for không phải là. - leppie
@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 đúng khi sử dụng trình biên dịch cũ hơn. - leppie
@ Benjol: Không, đó là lý do tại sao chúng tôi sẵn sàng chấp nhận nó. Jon Skeet đã chỉ ra cho tôi một kịch bản thay đổi đột phá 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 rằ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
Như một sang một bên, ReSharper đã luôn luôn bắt gặp điều này và báo cáo nó là "truy cập vào đóng cửa sửa đổi". Sau đó, bằng cách nhấn Alt + Enter, nó sẽ tự động sửa mã của bạn cho bạn hay không. jetbrains.com/resharper - Mike Chamberlain


Những gì bạn đang yêu cầu được Eric Lippert trình bày kỹ lưỡng trong bài đăng trên blog của anh ấy Đó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à có biến mới trong mỗi lần lặp lại sẽ không phù hợp với for(;;)kiểu vòng lặp. Bạn có mong đợi có một cái mới không int i trong mỗi lần lặp for (int i = 0; i < 10; i++)?

Vấn đề phổ biến nhất với hành vi này là làm cho một đóng cửa trên 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 #.


174
2018-01-17 17:39



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 phải là có nhiều biến, nó sẽ đóng lại giá trị. Và thật khó để nghĩ về một cú pháp có thể sử dụng cho điều đó trong trường hợp chung. - Random832
Có, nó không thể đóng bằng giá trị, tuy nhiên có một giải pháp rất dễ dàng tôi vừa chỉnh sửa câu trả lời của tôi để bao gồm. - Krizz
Đó là đóng cửa quá xấu trong C # đóng trên tài liệu tham khảo. Nếu họ đã đóng trên các giá trị theo mặc định, chúng tôi có thể dễ dàng chỉ định đóng trên các biến thay vì ref. - Sean U
@ Krizz, đây là trường hợp sự nhất quán bắt buộc có hại hơn là không nhất quán. Nó sẽ "chỉ 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 khi sử dụng foreach trái ngược với vòng lặp, cho số 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 đã sửa đổi (chẳng hạn như bản thân tôi) . - Andy
@ Random832 không biết về C # nhưng trong Common LISP có một cú pháp cho điều đó, và nó cho rằng bất kỳ ngôn ngữ nào có các biến và đóng được có thể thay đổi được (nay, phải) cũng vậy. Chúng tôi hoặc là đóng tham chiếu đến nơi thay đổi, hoặc một giá trị nó có tại một thời điểm nhất định trong thời gian (tạo ra một đóng cửa). Điều này thảo luận về những thứ tương tự trong Python và Scheme (cut cho refs / vars và cute để giữ đánh giá giá trị trong đóng cửa được đánh giá một phần). - Will Ness


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 bên trong nhất mà tôi sử dụng để chuyển sang bất kỳ đóng cửa 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 rất trường hợp hiếm hoi 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.


95
2018-01-17 17:47



Đó là cách giải quyết điển hình Cảm ơn sự đóng góp. Resharper là đủ thông minh để nhận ra mô hình này và mang lại cho bạn sự chú ý của bạn quá, đó là tốt đẹp. Tôi đã không được bit bởi mô hình này trong một thời gian, nhưng vì nó là, trong lời nói của Eric Lippert, "các báo cáo lỗi không chính xác phổ biến nhất chúng tôi nhận được," Tôi đã tò mò muốn biết tại sao nhiều hơn làm thế nào để tránh nó. - StriplingWarrior


Trong C # 5.0, vấn đề này là cố định 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

(...)

Tuyên bố foreach của biểu mẫu

foreach (V v in x) embedded-statement

sau đó được mở rộng tới:

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

(...)

Vị trí của v bên trong vòng lặp while quan trọng đối với nó   bị bắt bởi bất kỳ chức năng ẩn danh nào xảy ra trong   nhúng tuyên bố. 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ẻ   trong số tất cả các lần lặp lại 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à f sẽ in.   Thay vào đó, vì mỗi lần lặp lại có biến riêng của nó v, cái   bị bắt bởi f 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 #   khai báo v bên ngoài vòng lặp while.)


52
2017-09-03 13:58



Tại sao phiên bản đầu tiên này của C # được khai báo v bên trong vòng lặp while?msdn.microsoft.com/en-GB/library/aa664754.aspx - colinfang
@colinfang Hãy nhớ đọc Câu trả lời của Eric: Đặc 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 ra đã không nói cho dù biến vòng lặp là bên trong hay bên ngoài cơ thể vòng lặp, vì nó không tạo ra sự khác biệt quan sát được. Khi ngữ nghĩa đóng đã được giới thiệu trong C # 2.0, sự lựa chọn đã được thực hiện để đặ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
Vì vậy, bạn đang nói rằng các ví dụ trong liên kết không phải là spec cụ thể tại thời điểm đó? - colinfang
@colinfang Họ là những đặc điểm chính xác. Vấn đề là chúng ta đang nói về một tính năng (tức là đóng cửa 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 là họ đổi ý lần nữa với C # 5.0 :) - Paolo Moretti


Theo tôi, đó là câu hỏi lạ. Nó là tốt để biết làm thế nào trình biên dịch hoạt động nhưng đó chỉ là "tốt để biết".

Nếu bạn viết mã mà phụ thuộc vào thuật toán của trình biên dịch đó là thực hành xấu. Và nó tốt hơn để viết lại mã để loại trừ sự phụ thuộc này.

Đây là câu hỏi hay cho phỏng vấn xin việc. Nhưng trong cuộc sống thực, tôi không phải đối mặt với bất kỳ vấn đề nào mà tôi đã giải quyết khi phỏng vấn xin việc.

90% sử dụng foreach để xử lý từng phần tử của bộ sưu tập (không phải để chọn hoặc tính toán một số giá trị). đôi khi bạn cần tính toán một số giá trị bên trong vòng lặp nhưng thực hành không tốt để tạo vòng lặp BIG.

Tốt hơn là sử dụng các biểu thức LINQ để tính toán các giá trị. Bởi vì khi bạn tính toán nhiều thứ bên trong vòng lặp, sau 2-3 tháng khi bạn (hoặc bất kỳ ai khác) sẽ đọc người viết mã này sẽ không hiểu điều này là gì và nó hoạt động như thế nào.


0
2018-06-15 07:31



"Nếu bạn viết mã phụ thuộc vào thuật toán của trình biên dịch thì đó là thực hành không tốt". Đây không phải là câu hỏi về tối ưu hóa trình biên dịch. Đó là về hành vi thực tế của ngôn ngữ, đó là một cái gì đó bạn không thể thoát khỏi tùy thuộc vào. Tôi thừa nhận rằng biết tại sao một hành vi ngôn ngữ cụ thể đã được thực hiện là nhiều hơn một bài tập học tập, nhưng nó rõ ràng là một câu hỏi hợp pháp, xem xét đội C # quyết định thực hiện một thay đổi đột phá ngôn ngữ để sửa nó. - StriplingWarrior
"Tốt hơn là sử dụng các biểu thức LINQ để tính toán các giá trị." Bạn sẽ lưu ý rằng tôi đang sử dụng vòng lặp để xây dựng một biểu thức LINQ. Có, sử dụng Aggregate sẽ tránh được vấn đề này ngay từ đầu, nhưng nó rất phổ biến để mọi người sử dụng vòng lặp như thế này. Tôi thấy các câu hỏi phát sinh từ loại sự cố này trong JavaScript mọi lúc trên StackOverflow và chúng tương tự phổ biến trong C # trước khi nhóm quyết định thay đổi hành vi ngôn ngữ này. - StriplingWarrior