Tại sao List <T> .ForEach cho phép danh sách của nó được sửa đổi?


90

Nếu tôi sử dụng:

var strings = new List<string> { "sample" };
foreach (string s in strings)
{
  Console.WriteLine(s);
  strings.Add(s + "!");
}

các Addtrong foreachném một InvalidOperationException (Bộ sưu tập đã được sửa đổi; hoạt động liệt kê không thể thực hiện), mà tôi cân nhắc hợp lý, vì chúng ta đang kéo tấm thảm từ dưới chân chúng ta.

Tuy nhiên, nếu tôi sử dụng:

var strings = new List<string> { "sample" };
strings.ForEach(s =>
  {
    Console.WriteLine(s);
    strings.Add(s + "!");
  });

nó nhanh chóng tự bắn vào chân bằng cách lặp lại cho đến khi ném ra OutOfMemoryException.

Điều này làm tôi ngạc nhiên, vì tôi luôn nghĩ rằng List.ForEach chỉ là một trình bao bọc cho foreachhoặc cho for.
Có ai có lời giải thích cho cách thức và lý do tại sao của hành vi này?

(Được thúc đẩy bởi vòng lặp ForEach cho một Danh sách chung được lặp lại liên tục )


7
Tôi đồng ý. Đây là - không rõ ràng. Tôi rất vui khi bạn đăng điều đó trên microsoft connect và yêu cầu làm rõ.
TomTom

4
"Điều này khiến tôi ngạc nhiên, vì tôi luôn nghĩ rằng List.ForEach chỉ là một cái bao bọc cho foreachhoặc cho for." Nó vẫn có thể sử dụng for. Bạn có thể thực hiện cùng một hành động trong một forvòng lặp và kết quả là tạo ra cùng một OutOfMemoryException.
Anthony Pegram

Điều này dựa trên câu hỏi của tôi: stackoverflow.com/q/9311272/132239 , cảm ơn SWeko đã tìm hiểu chi tiết về nó
Kasrak 17/02/12

Câu trả lời:


68

Đó là bởi vì ForEachphương thức này không sử dụng enumerator, nó lặp qua các mục bằng một forvòng lặp:

public void ForEach(Action<T> action)
{
    if (action == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
    }
    for (int i = 0; i < this._size; i++)
    {
        action(this._items[i]);
    }
}

(mã lấy được bằng JustDecompile)

Vì trình điều tra không được sử dụng, nó không bao giờ kiểm tra xem danh sách đã thay đổi hay chưa và điều kiện kết thúc của forvòng lặp không bao giờ đạt được vì _sizenó được tăng lên ở mỗi lần lặp.


Vâng, nhưng làm thế nào được _sizetính toán? Nếu nó chỉ được tính toán trước thì if chỉ nên chạy một lần cho ví dụ của tôi. Rõ ràng là nó đã được làm mới bằng cách nào đó.
SWeko

7
Nó được làm mới tại Add method -> this._items [this._size ++] = item;
Fabio

1
@SWeko, nó không được tính toán, nó được cập nhật mỗi khi một mục được thêm vào hoặc loại bỏ.
Thomas Levesque

1
Có một _versionbiến riêng trong List<T>đó có thể phát hiện các loại tình huống này, vì nó được cập nhật trên các hoạt động thay đổi chính danh sách.
SWeko

Bạn có thể tránh các trường hợp ngoại lệ bằng cách lấy kích thước đầu tiên (int theSize = this._size), sau đó sử dụng nó trong vòng lặp for?
Lazlow

14

List<T>.ForEachđược thực hiện thông qua forbên trong, vì vậy nó không sử dụng trình điều tra và nó cho phép sửa đổi tập hợp.


6

Vì ForEach được gắn vào lớp List bên trong sử dụng vòng lặp for được gắn trực tiếp với các thành viên bên trong của nó - bạn có thể thấy điều này bằng cách tải xuống mã nguồn cho .NET framework.

http://referencesource.microsoft.com/netframework.aspx

Trong đó vòng lặp foreach trước hết là tối ưu hóa trình biên dịch nhưng cũng phải hoạt động chống lại bộ sưu tập với tư cách là người quan sát - vì vậy nếu bộ sưu tập được sửa đổi, nó sẽ ném ra một ngoại lệ.


Và để trả lời nhận xét trên @Thomas, hãy đăng bài về cách nó làm mới - các thành viên nội bộ được làm mới khi add được gọi, đó là lý do tại sao nó có thể theo kịp các thay đổi. Nếu bạn thực hiện một chèn, ở một chỉ mục nhỏ hơn chỉ mục hiện tại, bạn sẽ không bao giờ thao tác trên mục đó vì nó đã được lặp lại trước mục đó. Nhưng vì bạn đang thêm vào cuối nên nó hoạt động.
Mike Perrenoud

1
Có, thay đổi Adddòng strings.Insert(0, s + "!")chỉ với 'mẫu' in ra. Thật kỳ lạ là điều này hoàn toàn không được đề cập trong tài liệu.
SWeko

Chà, tôi nghĩ Microsoft đã nhận ra rằng không thể cung cấp mọi thông tin báo trước tồn tại trong tài liệu của họ - vì vậy họ cung cấp mã nguồn ngay bây giờ. Thành thật mà nói, tôi thấy rằng một giải pháp tốt hơn nhưng vấn đề duy nhất mà tôi tìm thấy là các sản phẩm như WF không được cập nhật nhanh chóng - mã nguồn 4.x WF vẫn không khả dụng.
Mike Perrenoud

4

Chúng tôi biết về vấn đề này, đó là một sự giám sát khi nó được viết ban đầu. Rất tiếc, chúng tôi không thể thay đổi nó vì nó sẽ ngăn mã hoạt động trước đây chạy:

        var list = new List<string>();
        list.Add("Foo");
        list.Add("Bar");

        list.ForEach((item) => 
        { 
            if(item=="Foo") 
                list.Remove(item); 
        });

Bản thân tính hữu ích của phương pháp này còn bị nghi ngờ như Eric Lippert đã chỉ ra, vì vậy chúng tôi đã không đưa nó vào cho các ứng dụng kiểu .NET for Metro (tức là các ứng dụng Windows 8).

David Kean (BCL Team)


1
Tôi thấy rằng đây sẽ là một thay đổi lớn mang tính đột phá xấu, nhưng dù sao nó có thể thất bại theo những cách không rõ ràng, và đó không bao giờ là một điều tốt. Tôi không thể nhìn thấy một kịch bản mà sử dụng phương pháp ForEach là vượt trội so với một đơn giản cho (hoặc foreach nếu mangling của danh sách ban đầu là không bắt buộc)
SWeko
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.