Tại sao hiệp phương sai và chống chỉ định không hỗ trợ loại giá trị


149

IEnumerable<T>đồng biến thể nhưng nó không hỗ trợ loại giá trị, chỉ là loại tham chiếu. Mã đơn giản dưới đây được biên dịch thành công:

IEnumerable<string> strList = new List<string>();
IEnumerable<object> objList = strList;

Nhưng thay đổi từ stringthành intsẽ nhận được lỗi biên dịch:

IEnumerable<int> intList = new List<int>();
IEnumerable<object> objList = intList;

Lý do được giải thích trong MSDN :

Phương sai chỉ áp dụng cho các loại tham chiếu; nếu bạn chỉ định một loại giá trị cho tham số loại biến thể, tham số loại đó là bất biến cho loại được xây dựng kết quả.

Tôi đã tìm kiếm và thấy rằng một số câu hỏi đề cập đến lý do là quyền anh giữa loại giá trị và loại tham chiếu . Nhưng nó vẫn không làm sáng tỏ tâm trí của tôi nhiều tại sao đấm bốc là lý do?

Ai đó có thể vui lòng đưa ra một lời giải thích đơn giản và chi tiết tại sao hiệp phương sai và chống chỉ định không hỗ trợ loại giá trị và quyền anh ảnh hưởng đến điều này như thế nào?


3
xem thêm câu trả lời của Eric cho câu hỏi tương tự của tôi: stackoverflow.com/questions
406262 / /

Câu trả lời:


126

Về cơ bản, phương sai áp dụng khi CLR có thể đảm bảo rằng nó không cần thực hiện bất kỳ thay đổi đại diện nào cho các giá trị. Tất cả các tham chiếu trông giống nhau - vì vậy bạn có thể sử dụng IEnumerable<string>như một IEnumerable<object>mà không có bất kỳ thay đổi nào trong biểu diễn; bản thân mã gốc không cần biết bạn đang làm gì với các giá trị, miễn là cơ sở hạ tầng đã đảm bảo rằng nó chắc chắn sẽ hợp lệ.

Đối với các loại giá trị, điều đó không hiệu quả - coi IEnumerable<int>như là mộtIEnumerable<object> , mã sử dụng chuỗi sẽ phải biết có nên thực hiện chuyển đổi quyền anh hay không.

Bạn có thể muốn đọc bài đăng trên blog của Eric Lippert về đại diện và danh tính để biết thêm về chủ đề này nói chung.

EDIT: Đọc lại bài đăng trên blog của Eric, ít nhất là về bản sắc như đại diện, mặc dù cả hai đều được liên kết. Đặc biệt:

Đây là lý do tại sao các chuyển đổi covariant và contravariant của các loại giao diện và đại biểu yêu cầu tất cả các đối số loại khác nhau phải là các loại tham chiếu. Để đảm bảo rằng chuyển đổi tham chiếu biến thể luôn được bảo toàn danh tính, tất cả các chuyển đổi liên quan đến đối số loại cũng phải được bảo toàn danh tính. Cách dễ nhất để đảm bảo rằng tất cả các chuyển đổi không tầm thường trên các đối số loại là bảo toàn danh tính là hạn chế chúng là các chuyển đổi tham chiếu.


5
@CuongLe: Vâng, đó là một chi tiết triển khai trong một số giác quan, nhưng đó là lý do cơ bản cho sự hạn chế, tôi tin.
Jon Skeet

2
@ AndréCaron: Bài đăng trên blog của Eric rất quan trọng ở đây - nó không chỉ là đại diện, mà còn là bảo tồn danh tính. Nhưng bảo toàn đại diện có nghĩa là mã được tạo ra không cần quan tâm đến điều này cả.
Jon Skeet

1
Chính xác, danh tính không thể được bảo tồn vì intkhông phải là một kiểu con của object. Việc một sự thay đổi mang tính đại diện được yêu cầu chỉ là hệ quả của việc này.
André Caron

3
Làm thế nào là int không phải là một kiểu con của đối tượng? Int32 kế thừa từ System.ValueType, kế thừa từ System.Object.
David Klempfner

1
@DavidKlempfner Tôi nghĩ rằng bình luận của @ AndréCaron rất kém. Bất kỳ loại giá trị nào, chẳng hạn như Int32có hai hình thức đại diện, "đóng hộp" và "không được đóng hộp". Trình biên dịch phải chèn mã để chuyển đổi từ dạng này sang dạng khác, mặc dù điều này thường vô hình ở cấp mã nguồn. Trong thực tế, chỉ có dạng "đóng hộp" được hệ thống cơ bản coi là một kiểu con của objecttrình biên dịch, nhưng trình biên dịch sẽ tự động xử lý việc này bất cứ khi nào một loại giá trị được gán cho giao diện tương thích hoặc loại nào đó object.
Steve

10

Có lẽ dễ hiểu hơn nếu bạn nghĩ về biểu diễn cơ bản (mặc dù đây thực sự là một chi tiết triển khai). Đây là một bộ sưu tập các chuỗi:

IEnumerable<string> strings = new[] { "A", "B", "C" };

Bạn có thể nghĩ về việc stringscó đại diện sau:

[0]: tham chiếu chuỗi -> "A"
[1]: tham chiếu chuỗi -> "B"
[2]: tham chiếu chuỗi -> "C"

Nó là một tập hợp gồm ba phần tử, mỗi phần tử là một tham chiếu đến một chuỗi. Bạn có thể truyền nó tới một bộ sưu tập các đối tượng:

IEnumerable<object> objects = (IEnumerable<object>) strings;

Về cơ bản nó là cùng một đại diện ngoại trừ bây giờ các tham chiếu là tham chiếu đối tượng:

[0]: tham chiếu đối tượng -> "A"
[1]: tham chiếu đối tượng -> "B"
[2]: tham chiếu đối tượng -> "C"

Các đại diện là như nhau. Các tài liệu tham khảo chỉ được đối xử khác nhau; bạn không thể truy cập vào string.Lengthtài sản nhưng bạn vẫn có thể gọi object.GetHashCode(). So sánh điều này với một bộ sưu tập ints:

IEnumerable<int> ints = new[] { 1, 2, 3 };
[0]: int = 1
[1]: int = 2
[2]: int = 3

Để chuyển đổi IEnumerable<object>dữ liệu này thành dữ liệu phải được chuyển đổi bằng quyền anh ints:

[0]: tham chiếu đối tượng -> 1
[1]: tham chiếu đối tượng -> 2
[2]: tham chiếu đối tượng -> 3

Chuyển đổi này đòi hỏi nhiều hơn một diễn viên.


2
Quyền anh không chỉ là một "chi tiết thực hiện". Các loại giá trị được đóng hộp được lưu trữ giống như các đối tượng lớp và hành xử, theo như thế giới bên ngoài có thể nói, giống như các đối tượng lớp. Sự khác biệt duy nhất là trong định nghĩa của loại giá trị được đóng hộp, thisđề cập đến một cấu trúc có các trường bao phủ các đối tượng heap lưu trữ nó, thay vì đề cập đến đối tượng chứa chúng. Không có cách rõ ràng nào cho một thể hiện loại giá trị được đóng hộp để có được một tham chiếu đến đối tượng heap kèm theo.
supercat

7

Tôi nghĩ mọi thứ bắt đầu từ definiton của LSP(Nguyên tắc thay thế Liskov), mà climes:

if q (x) là một thuộc tính có thể chứng minh được về các đối tượng x loại T thì q (y) phải đúng với các đối tượng y loại S trong đó S là một kiểu con của T.

Nhưng các loại giá trị, ví dụ intkhông thể thay thế objectbằng C#. Chứng minh rất đơn giản:

int myInt = new int();
object obj1 = myInt ;
object obj2 = myInt ;
return ReferenceEquals(obj1, obj2);

Điều này trả về falsengay cả khi chúng ta gán cùng một "tham chiếu" cho đối tượng.


1
Tôi nghĩ rằng bạn đang sử dụng đúng nguyên tắc nhưng không có bằng chứng nào được đưa ra: intkhông phải là một kiểu con objectnên nguyên tắc này không được áp dụng. "Bằng chứng" của bạn dựa trên một đại diện trung gian Integer, là một kiểu con objectvà ngôn ngữ có chuyển đổi ngầm định ( object obj1=myInt;được mở rộng thực tế sang object obj1=new Integer(myInt);).
André Caron

Ngôn ngữ đảm nhiệm việc truyền chính xác giữa các loại, nhưng hành vi ints không tương ứng với ngôn ngữ mà chúng ta mong đợi từ kiểu con của đối tượng.
Tigran

Toàn bộ quan điểm của tôi là chính xác đó intkhông phải là một kiểu con object. Hơn nữa, LSP không áp dụng vì myInt, obj1obj2đề cập đến ba đối tượng khác nhau: một intvà hai (ẩn) Integers.
André Caron

22
@ André: C # không phải là Java. intTừ khóa của C # là một bí danh cho BCL System.Int32, trên thực tế là một kiểu con của object(một bí danh System.Object). Trong thực tế, intlớp System.ValueTypecơ sở là lớp cơ sở của ai System.Object. Hãy thử đánh giá biểu thức sau đây và xem : typeof(int).BaseType.BaseType. Lý do ReferenceEqualstrả về sai ở đây là inthộp được đóng thành hai hộp riêng biệt và danh tính của mỗi hộp là khác nhau đối với bất kỳ hộp nào khác. Do đó, hai hoạt động đấm bốc luôn mang lại hai đối tượng không bao giờ giống nhau, bất kể giá trị đóng hộp.
Allon Guralnek

@ ALLonGuralnek: Mỗi loại giá trị (ví dụ System.Int32hoặc List<String>.Enumerator) thực sự đại diện cho hai loại: loại vị trí lưu trữ và loại đối tượng heap (đôi khi được gọi là "loại giá trị được đóng hộp"). Các vị trí lưu trữ có loại xuất phát từ System.ValueTypesẽ giữ vị trí cũ; các đối tượng heap có kiểu làm tương tự sẽ giữ cái sau. Trong hầu hết các ngôn ngữ, một dàn diễn viên mở rộng tồn tại từ cái trước và cái đúc hẹp từ cái sau sang cái trước. Lưu ý rằng trong khi các loại giá trị được đóng hộp có mô tả cùng loại với các vị trí lưu trữ loại giá trị, ...
supercat

3

Nó đi xuống một chi tiết triển khai: Các loại giá trị được triển khai khác với các loại tham chiếu.

Nếu bạn buộc các loại giá trị được coi là loại tham chiếu (ví dụ: đóng hộp chúng, ví dụ bằng cách tham chiếu chúng qua giao diện), bạn có thể nhận được phương sai.

Cách dễ nhất để thấy sự khác biệt chỉ đơn giản là xem xét một Array: một mảng các loại Giá trị được đặt liền nhau trong bộ nhớ (trực tiếp), trong đó một mảng các loại Tham chiếu chỉ có tham chiếu (một con trỏ) liền kề trong bộ nhớ; các đối tượng được chỉ ra được phân bổ riêng biệt.

Vấn đề (liên quan) khác (*) là (hầu hết) tất cả các loại Tham chiếu có cùng biểu diễn cho mục đích phương sai và nhiều mã không cần biết về sự khác biệt giữa các loại, vì vậy có thể đồng biến và chống đối thực hiện - thường chỉ bằng cách bỏ qua kiểm tra loại bổ sung).

(*) Nó có thể được coi là cùng một vấn đề ...

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.