2010-07-02 17 views
43

Chúng ta đều biết mutable structs are evil nói chung. Tôi cũng khá chắc chắn rằng bởi vì IEnumerable<T>.GetEnumerator() trả về loại IEnumerator<T>, cấu trúc được ngay lập tức đóng hộp thành một loại tham chiếu, chi phí nhiều hơn nếu chúng chỉ đơn giản là các loại tham chiếu để bắt đầu.Tại sao Bộ sưu tập BCL sử dụng các điều tra cấu trúc, không phải các lớp?

Vậy tại sao, trong các bộ sưu tập chung của BCL, tất cả các bảng liệt kê có thể thay đổi được không? Chắc chắn đã có một lý do chính đáng. Điều duy nhất xảy ra với tôi là các cấu trúc có thể được sao chép dễ dàng, do đó bảo toàn trạng thái điều tra tại một điểm tùy ý. Nhưng việc thêm phương thức Copy() vào giao diện IEnumerator sẽ ít phiền toái hơn, vì vậy tôi không thấy điều này như là một sự biện minh hợp lý.

Thậm chí nếu tôi không đồng ý với quyết định thiết kế, tôi muốn có thể hiểu được lý do đằng sau nó.

+0

trang liên quan cho những người khác chạy trên này: http://stackoverflow.com/questions/384511/enumerator-implementation-use-struct-or-class http://www.eggheadcafe.com/software/ aspnet/31702392/c-compiler-challenge - s.aspx –

Trả lời

62

Thật vậy, đó là vì lý do hiệu suất. Nhóm BCL đã thực hiện nghiên cứu về điểm này trước khi quyết định đi theo những gì bạn gọi đúng là thực tiễn đáng ngờ và nguy hiểm: việc sử dụng loại giá trị có thể thay đổi.

Bạn hỏi tại sao điều này không gây ra quyền anh. Đó là bởi vì trình biên dịch C# không tạo mã cho các công cụ hộp để IEnumerable hoặc IEnumerator trong một vòng lặp foreach nếu nó có thể tránh nó!

Khi chúng ta thấy

foreach(X x in c) 

điều đầu tiên chúng tôi làm là kiểm tra xem nếu c có một phương pháp gọi là GetEnumerator. Nếu có, thì chúng ta kiểm tra xem kiểu nó trả về có phương thức MoveNext và thuộc tính hiện tại không. Nếu có, thì vòng lặp foreach được tạo hoàn toàn bằng cách sử dụng các cuộc gọi trực tiếp đến các phương thức và thuộc tính đó. Chỉ khi "mẫu" không thể khớp với nhau thì chúng ta sẽ quay trở lại để tìm kiếm các giao diện.

Điều này có hai hiệu ứng mong muốn.

Đầu tiên, nếu bộ sưu tập là một tập hợp các int, nhưng được viết trước khi loại chung được phát minh, thì nó không lấy hình phạt quyền anh của giá trị Hiện tại thành đối tượng và sau đó unboxing nó thành int. Nếu Current là thuộc tính trả về int, chúng ta chỉ sử dụng nó.

Thứ hai, nếu điều tra viên là loại giá trị thì nó không được liệt kê trong bảng điều tra cho IEnumerator. Như tôi đã nói, nhóm BCL đã làm rất nhiều nghiên cứu về điều này và phát hiện ra rằng phần lớn thời gian, hình phạt phân bổ và deallocating điều tra đủ lớn mà nó đã được giá trị làm cho nó một loại giá trị , mặc dù làm như vậy có thể gây ra một số lỗi điên rồ.

Ví dụ, hãy xem xét điều này:

struct MyHandle : IDisposable { ... } 
... 
using (MyHandle h = whatever) 
{ 
    h = somethingElse; 
} 

Bạn sẽ khá đúng mong đợi những nỗ lực đột biến h thất bại, và thực sự nó làm. Trình biên dịch phát hiện rằng bạn đang cố gắng thay đổi giá trị của một cái gì đó có một xử lý đang chờ xử lý, và làm như vậy có thể gây ra các đối tượng cần phải được xử lý để thực sự không được xử lý.

Bây giờ giả sử bạn có:

struct MyHandle : IDisposable { ... } 
... 
using (MyHandle h = whatever) 
{ 
    h.Mutate(); 
} 

gì xảy ra ở đây? Bạn có thể mong đợi hợp lý rằng trình biên dịch sẽ làm những gì nó làm nếu h là một trường chỉ đọc: make a copy, and mutate the copy để đảm bảo rằng phương thức không vứt bỏ các thứ trong giá trị cần phải được xử lý.

Tuy nhiên, đó mâu thuẫn với trực giác của chúng ta về những gì phải xảy ra ở đây:

using (Enumerator enumtor = whatever) 
{ 
    ... 
    enumtor.MoveNext(); 
    ... 
} 

Chúng tôi hy vọng rằng làm một MoveNext bên trong một sử dụng khối sẽ di chuyển Enumerator kế tiếp bất kể đó là một cấu trúc hoặc một kiểu ref.

Thật không may, trình biên dịch C# ngày nay có lỗi. Nếu bạn đang ở trong tình huống này, chúng tôi chọn chiến lược nào để theo dõi không nhất quán. Các hành vi hôm nay là:

  • nếu con người biến giá trị gia gõ đột biến thông qua một phương pháp là một bình thường địa phương sau đó nó bị đột biến thường

  • nhưng nếu nó là một địa phương kéo lên (vì đó là một closed- trên biến của một hàm ẩn danh hoặc trong khối lặp) thì địa chỉ thực sự được tạo dưới dạng trường chỉ đọc và thiết bị đảm bảo các đột biến xảy ra trên bản sao.

Rất tiếc, thông số cung cấp ít hướng dẫn về vấn đề này. Rõ ràng một cái gì đó bị hỏng bởi vì chúng tôi đang làm nó không nhất quán, nhưng những gì quyền điều cần làm là không rõ ràng.

+1

+1 Điều này có nghĩa là có (tối thiểu) các hình phạt về hiệu suất của việc đi qua một 'IEnumerable 'trái ngược với bộ sưu tập chung gốc - trong một bài kiểm tra chế độ phát hành nhanh liệt kê trên một danh sách' Danh mục ' và khi được đúc thành một 'IEnumerable ', tôi thấy sự khác biệt về thời gian nhất quán là 2: 1 (trong trường hợp này là ~ 100ms so với ~ 50ms). –

+0

Câu trả lời hay, tôi không biết về tối ưu hóa đó - nhưng nó có ý nghĩa hoàn hảo. Tôi thấy nó hơi mỉa mai khi tôi liên kết blog của bạn để sao lưu tuyên bố của tôi rằng cấu trúc có thể biến đổi là điều ác - và bạn trả lời câu hỏi của tôi :) – Eloff

+0

Btw, đó là một trường hợp góc xấu xí, một vấn đề khác với cấu trúc có thể thay đổi. – Eloff

5

Phương thức cấu trúc được gạch chân khi loại cấu trúc được biết tại thời gian biên dịch và phương thức gọi qua giao diện chậm, vì vậy câu trả lời là: vì lý do hiệu suất.

+0

Nhưng đây là những cấu trúc bên trong, vì vậy loại này không bao giờ được biết tại thời gian biên dịch; và tất cả mã người dùng cuối truy cập chúng thông qua giao diện. –

+1

@Stephen: 'Danh sách . Số đếm' được ghi thành công khai trong MSDN ... –

+0

Nếu bạn xem ví dụ Danh sách .GetEnunmerator method (http://msdn.microsoft.com/en-us/library/b0yss765 .aspx) bạn có thể thấy nó trả về Danh sách :: Enumerator struct. vòng lặp foreach trong C# không sử dụng giao diện IEnumerable trực tiếp, nó là đủ cho nó nếu lớp có phương pháp GetEnumerator. Vì vậy, loại điều tra được biết đến tại thời gian biên dịch. – STO

Các vấn đề liên quan