2009-07-24 6 views
42

tra một lỗi, tôi phát hiện ra nó là do sự kì quái này trong C#:Tại sao mảng C# của tôi mất thông tin ký hiệu loại khi truyền tới đối tượng?

sbyte[] foo = new sbyte[10]; 
object bar = foo; 
Console.WriteLine("{0} {1} {2} {3}", 
     foo is sbyte[], foo is byte[], bar is sbyte[], bar is byte[]); 

Đầu ra là "True False Đúng True", trong khi tôi lại có thể ngờ "bar is byte[]" để trở về False. Rõ ràng thanh là cả hai byte[] và một sbyte[]? Điều tương tự cũng xảy ra đối với các loại đã ký/chưa ký khác như Int32[] so với UInt32[], nhưng không phải cho số Int32[]Int64[].

Có ai có thể giải thích hành vi này không? Đây là trong .NET 3.5.

Trả lời

65

UPDATE: Tôi đã sử dụng câu hỏi này làm cơ sở cho một blog entry, ở đây:

http://blogs.msdn.com/ericlippert/archive/2009/09/24/why-is-covariance-of-value-typed-arrays-inconsistent.aspx

Xem blog ý kiến ​​cho một cuộc thảo luận dài vấn đề này. Cảm ơn vì câu hỏi tuyệt vời của bạn!


Bạn đã tình cờ gặp một sự không nhất quán thú vị và không may giữa hệ thống loại CLI và hệ thống kiểu C#.

CLI có khái niệm "khả năng tương thích gán". Nếu giá trị x của kiểu dữ liệu đã biết S là "gán tương thích" với một vị trí lưu trữ cụ thể y của kiểu dữ liệu đã biết T, thì bạn có thể lưu trữ x trong y. Nếu không, thì làm như vậy không phải là mã có thể kiểm chứng và người xác minh sẽ không cho phép.

Hệ thống kiểu CLI cho biết, ví dụ, các kiểu con của kiểu tham chiếu được gán tương ứng với siêu kiểu của kiểu tham chiếu. Nếu bạn có một chuỗi, bạn có thể lưu nó trong một biến kiểu đối tượng, bởi vì cả hai là các kiểu tham chiếu và chuỗi là một kiểu con của đối tượng. Nhưng điều ngược lại không đúng; supertypes không được gán tương thích với các kiểu con. Bạn không thể dính cái gì đó chỉ được biết đến là đối tượng vào một biến kiểu chuỗi mà không đầu tiên đúc nó.

Về cơ bản "gán tương thích" có nghĩa là "có ý nghĩa khi gắn các bit chính xác này vào biến này". Việc gán từ giá trị nguồn tới biến đích phải là "bảo quản đại diện". Xem bài viết của tôi trên đó để biết chi tiết:

http://ericlippert.com/2009/03/03/representation-and-identity/

Một trong những quy tắc của CLI là "nếu X là nhiệm vụ tương thích với Y, sau đó X [] là nhiệm vụ tương thích với Y []".

Tức là, mảng là biến thể liên quan đến khả năng tương thích của bài tập. Đây thực sự là một loại hiệp phương sai; xem bài viết của tôi về điều đó để biết chi tiết.

http://blogs.msdn.com/ericlippert/archive/2007/10/17/covariance-and-contravariance-in-c-part-two-array-covariance.aspx

Đó không phải là quy tắc của C#. Quy tắc hiệp phương sai của mảng C# là "nếu X là kiểu tham chiếu ngầm chuyển đổi thành kiểu tham chiếu Y, thì X [] được chuyển đổi hoàn toàn thành Y []". Đó là một quy tắc khác biệt tinh tế, và do đó tình huống khó hiểu của bạn.

Trong CLI, uint và int tương ứng với nhiệm vụ. Nhưng trong C#, chuyển đổi giữa int và uint là EXPLICIT, chứ không phải IMPLICIT, và đây là các kiểu giá trị, không phải kiểu tham chiếu. Vì vậy, trong C#, nó không hợp pháp để chuyển đổi một int [] thành một uint [].

Nhưng nó là hợp pháp trong CLI. Vì vậy, bây giờ chúng tôi đang phải đối mặt với một sự lựa chọn.

1) Thực hiện "là" sao cho khi trình biên dịch không thể xác định câu trả lời tĩnh, nó thực sự gọi phương thức kiểm tra tất cả các quy tắc C# cho tính chuyển đổi bảo toàn danh tính. Điều này là chậm và 99,9% thời gian phù hợp với quy tắc CLR. Nhưng chúng tôi có hiệu suất đạt đến 100% tuân thủ các quy tắc của C#.

2) Thực hiện "là" sao cho khi trình biên dịch không thể xác định câu trả lời tĩnh, kiểm tra khả năng tương thích CLR cực nhanh, và sống với thực tế là uint [] là int [], mặc dù điều đó sẽ không thực sự hợp pháp trong C#.

Chúng tôi đã chọn cái sau. Thật không may là các thông số C# và CLI không đồng ý về điểm nhỏ này nhưng chúng tôi sẵn sàng sống với sự không thống nhất.

+0

Đọc lớn. Cảm ơn câu trả lời. –

+2

Xin chào Eric, ngoài sự tò mò, các bạn đã quyết định chấp nhận sự mâu thuẫn này hay chưa được dự đoán trước? Chỉ cần tự hỏi. –

+0

Đã xóa bài đăng của tôi trong sự trì hoãn đến một câu trả lời tốt hơn và độc đáo hơn. – LBushkin

0

Chắc chắn đầu ra là chính xác. bar "là" cả sbyte [] và byte [], bởi vì nó tương thích với cả hai, vì thanh chỉ đơn thuần là một đối tượng sau đó nó "có thể" được ký hoặc chưa ký.

"được" được định nghĩa là "biểu thức có thể được truyền để nhập".

+1

Nhưng vì 'bar' thuộc loại' đối tượng', loại cơ sở của bất kỳ loại nào khác, có nghĩa là bạn có thể nói 'bar là ' và nó sẽ trả về true, và đó không phải là trường hợp. Tại sao bạn có thể tạo một 'sbyte []' thành 'byte []' chỉ vì nó xảy ra để truyền theo kiểu tham chiếu? –

9

Ran đoạn qua Reflector:

sbyte[] foo = new sbyte[10]; 
object bar = foo; 
Console.WriteLine("{0} {1} {2} {3}", new object[] { foo != null, false, bar is sbyte[], bar is byte[] }); 

Các biên dịch C# là tối ưu hóa hai so sánh đầu tiên (foo is sbyte[]foo is byte[]). Như bạn có thể thấy chúng đã được tối ưu hóa thành foo != null và đơn giản là luôn luôn false.

5

Cũng thú vị:

sbyte[] foo = new sbyte[] { -1 }; 
    var x = foo as byte[]; // doesn't compile 
    object bar = foo; 
    var f = bar as byte[]; // succeeds 
    var g = f[0];    // g = 255 
+0

Tôi đang thiếu thứ gì đó ở đây. Đó không phải là điều bạn mong đợi. Điều kỳ quặc là gì? – cdm9002

+0

Không phải là 'g = 255', được mong đợi, nhưng' thanh như byte [] 'không trả về giá trị rỗng. –

+2

Phải - vì vậy bây giờ bạn đã đọc câu trả lời của tôi, bạn có thể suy ra rằng cùng một loại điều đang xảy ra ở đây. Với cái đầu tiên, chúng ta biết tại thời gian biên dịch rằng điều này vi phạm các quy tắc của C#. Với cái thứ hai, chúng ta không biết điều đó. Vì vậy, chúng ta phải hoặc (1) phát ra một phương thức thực hiện tất cả các quy tắc của ngôn ngữ C# để làm diễn viên, hoặc (2) sử dụng các quy tắc đúc CLR, khác với các quy tắc của C# cho một tỷ lệ nhỏ các trường hợp kỳ quái. Chúng tôi đã chọn (2). –