Tại sao mã này ném 'Bộ sưu tập đã được sửa đổi', nhưng khi tôi lặp lại một cái gì đó trước nó, nó lại không?


102
var ints = new List< int >( new[ ] {
    1,
    2,
    3,
    4,
    5
} );
var first = true;
foreach( var v in ints ) {
    if ( first ) {
        for ( long i = 0 ; i < int.MaxValue ; ++i ) { //<-- The thing I iterate
            ints.Add( 1 );
            ints.RemoveAt( ints.Count - 1 );
        }
        ints.Add( 6 );
        ints.Add( 7 );
    }
    Console.WriteLine( v );
    first = false;
}

Nếu bạn nhận xét forvòng lặp bên trong , nó ném ra, rõ ràng là do chúng tôi đã thay đổi bộ sưu tập.

Bây giờ nếu bạn bỏ ghi chú nó, tại sao vòng lặp này cho phép chúng tôi thêm hai mục đó? Phải mất một lúc để chạy nó như nửa phút (Trên CPU Pentium), nhưng nó không hoạt động và điều buồn cười là nó xuất ra:

Hình ảnh

Đó là một chút mong đợi, nhưng nó chỉ ra rằng chúng tôi có thể thay đổi và nó thực sự thay đổi bộ sưu tập. Bất kỳ ý tưởng tại sao hành vi này xảy ra?


2
Nó thật thú vị. Tôi có thể tái tạo hành vi, nhưng không thể nếu tôi thay đổi vòng lặp nội bộ từ Int.MaxValue thành giá trị như 100
Steve

bạn đã chờ đợi bao lâu? Phải mất nhiều thời gian để kết thúc int.MaxValuelặp ...
Jon Skeet

1
Tôi tin rằng foreach sẽ kiểm tra xem liệu bộ sưu tập có được sửa đổi ở đầu mỗi vòng lặp hay không .... vì vậy việc thêm và sau đó xóa mục trong mỗi vòng lặp sẽ không gây ra bất kỳ lỗi nào.
Kaz

6
Bạn có thể tự trả lời câu hỏi này bằng cách xem nguồn tham khảo và xem cách phát hiện thay đổi hoạt động. Không phải ai cũng biết nguồn tham khảo thậm chí còn tồn tại, chỉ cần truyền bá :)
Christopher Currens

2
Chỉ vì tò mò: bạn đã gặp vấn đề này trong một đoạn mã trong thế giới thực?
ken2k

Câu trả lời:


119

Vấn đề là cách List<T>phát hiện các sửa đổi là giữ một trường phiên bản, loại int, tăng nó lên mỗi lần sửa đổi. Do đó, nếu bạn đã thực hiện chính xác một số sửa đổi trong số 2 32 sửa đổi đối với danh sách giữa các lần lặp lại, nó sẽ làm cho những sửa đổi đó vô hình khi có liên quan đến việc phát hiện. (Nó sẽ tràn từ int.MaxValueđến int.MinValuevà cuối cùng trở về giá trị ban đầu.)

Nếu bạn thay đổi khá nhiều thứ về mã của mình - thêm 1 hoặc 3 giá trị thay vì 2 hoặc giảm số lần lặp của vòng lặp bên trong của bạn xuống 1, thì nó sẽ đưa ra một ngoại lệ như mong đợi.

(Đây là một chi tiết triển khai chứ không phải là hành vi được chỉ định - và đó là một chi tiết triển khai có thể được coi là một lỗi trong một trường hợp rất hiếm. Tuy nhiên, sẽ rất bất thường nếu nó gây ra sự cố trong một chương trình thực.)


5
Chỉ để tham khảo: mã nguồn có liên quan , lưu ý rằng _versiontrường là một int.
Lucas Trzesniewski

1
Đúng vậy, nó được thiết lập vừa phải để sau khi vòng lặp for kết thúc, _version có giá trị là -2 .... sau đó thêm 6 và 7 sẽ đặt nó thành 0, làm cho danh sách giống như chưa được sửa đổi.
Kaz

4
Tôi không chắc đây nên được gọi là "chi tiết triển khai", bởi vì có một tác dụng phụ của quyết định triển khai đó, mà ngay cả khi không thể xảy ra, là có thật. Thông số kỹ thuật (hoặc ít nhất là tài liệu) nói rằng nó nên ném một InvalidOperationException, điều này thực sự không phải lúc nào cũng đúng. Tất nhiên điều này phụ thuộc vào định nghĩa của "chi tiết triển khai".
ken2k

3
Jon Skeet, bạn là nhà thiết kế ngôn ngữ lập trình? (Không tìm thấy bất cứ điều gì liên quan trên Google) Một chút tò mò tại sao bạn cũng có kiến ​​thức này. Câu hỏi này hơi mang tính trêu chọc để thấy được "sức mạnh" của Stack Overflow.
LyingOnTheSky

6
@LyingOnTheSky: Không, mặc dù tôi thích đóng vai trò là nhà thiết kế ngôn ngữ về việc theo dõi và phê bình ngôn ngữ C #. Tôi cũng vào nhóm kỹ thuật ECMA-334 cho tiêu chuẩn hóa C # 5 ... vì vậy tôi nhận được để chọn lỗ nhưng không làm công việc thiết kế ngôn ngữ thật :)
Jon Skeet
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.