Làm cách nào để các mảng trong C # triển khai một phần IList <T>?


99

Vì vậy, như bạn có thể biết, các mảng trong C # triển khai IList<T> , trong số các giao diện khác. Tuy nhiên, bằng cách nào đó, họ làm điều này mà không thực hiện công khai thuộc tính Count của IList<T>! Mảng chỉ có thuộc tính Độ dài.

Đây có phải là một ví dụ trắng trợn về việc C # /. NET vi phạm các quy tắc của riêng nó về việc triển khai giao diện hay tôi đang thiếu thứ gì đó?


2
Không ai nói rằng Arraylớp học phải được viết bằng C #!
dùng541686

Arraylà một lớp "ma thuật", không thể được triển khai trong C # hoặc bất kỳ ngôn ngữ nào khác nhắm mục tiêu .net. Nhưng tính năng cụ thể này có sẵn trong C #.
CodesInChaos

Câu trả lời:


81

Câu trả lời mới dựa trên câu trả lời của Hans

Nhờ câu trả lời mà Hans đưa ra, chúng ta có thể thấy việc thực hiện có phần phức tạp hơn chúng ta tưởng. Cả trình biên dịch và CLR đều rất cố gắng để tạo ấn tượng rằng kiểu mảng thực thi IList<T>- nhưng phương sai mảng làm cho điều này phức tạp hơn. Trái ngược với câu trả lời từ Hans, các kiểu mảng (đơn chiều, dựa trên không) thực hiện trực tiếp các tập hợp chung, bởi vì kiểu của bất kỳ mảng cụ thể nào không phải System.Array - đó chỉ là kiểu cơ sở của mảng. Nếu bạn hỏi một kiểu mảng mà nó hỗ trợ giao diện nào, thì nó sẽ bao gồm các kiểu chung:

foreach (var type in typeof(int[]).GetInterfaces())
{
    Console.WriteLine(type);
}

Đầu ra:

System.ICloneable
System.Collections.IList
System.Collections.ICollection
System.Collections.IEnumerable
System.Collections.IStructuralComparable
System.Collections.IStructuralEquatable
System.Collections.Generic.IList`1[System.Int32]
System.Collections.Generic.ICollection`1[System.Int32]
System.Collections.Generic.IEnumerable`1[System.Int32]

Đối với mảng đơn chiều, mảng không dựa trên ngôn ngữ , mảng thực sự cũng thực hiện IList<T>. Phần 12.1.2 của đặc tả C # nói như vậy. Vì vậy, bất cứ điều gì triển khai bên dưới làm, ngôn ngữ phải hoạt động như thể loại T[]triển khai IList<T>như với bất kỳ giao diện nào khác. Từ quan điểm này, giao diện được thực hiện với một số thành viên được thực hiện rõ ràng (chẳng hạn như Count). Đó là lời giải thích tốt nhất ở cấp độ ngôn ngữ cho những gì đang xảy ra.

Lưu ý rằng điều này chỉ đúng đối với mảng đơn chiều (và mảng dựa trên 0, không phải ngôn ngữ C # nói bất cứ điều gì về mảng khác 0). T[,] không thực hiện IList<T>.

Từ góc độ CLR, một cái gì đó thú vị hơn đang diễn ra. Bạn không thể lấy ánh xạ giao diện cho các loại giao diện chung. Ví dụ:

typeof(int[]).GetInterfaceMap(typeof(ICollection<int>))

Đưa ra một ngoại lệ của:

Unhandled Exception: System.ArgumentException: Interface maps for generic
interfaces on arrays cannot be retrived.

Vì vậy, tại sao sự kỳ lạ? Chà, tôi tin rằng đó thực sự là do hiệp phương sai mảng, là một điểm yếu trong hệ thống loại, IMO. Mặc dù không phảiIList<T> là hiệp phương sai (và không thể an toàn), hiệp phương sai của mảng cho phép điều này hoạt động:

string[] strings = { "a", "b", "c" };
IList<object> objects = strings;

... khiến nó trông giống như nông typeof(string[])cụ IList<object>, trong khi nó không thực sự.

Phân vùng 1 CLI spec (ECMA-335), mục 8.7.1, có điều này:

Kiểu chữ ký T tương thích với kiểu chữ ký U nếu và chỉ khi có ít nhất một trong các điều sau

...

T là một số không dựa trên cấp bậc-1 mảng V[], và UIList<W>, và V là mảng phần tử tương thích với các W.

(Nó không thực sự đề cập đến ICollection<W>hoặc IEnumerable<W>mà tôi tin rằng đó là một lỗi trong thông số kỹ thuật.)

Đối với không có phương sai, thông số CLI trực tiếp đi cùng với thông số ngôn ngữ. Từ phần 8.9.1 của phân vùng 1:

Ngoài ra, một vectơ được tạo với kiểu phần tử T, triển khai giao diện System.Collections.Generic.IList<U>, trong đó U: = T. (§8.7)

( Vectơ là một mảng đơn chiều với cơ số 0.)

Bây giờ trong điều khoản của chi tiết thực hiện , rõ CLR được làm một số bản đồ sôi nổi để giữ khả năng tương thích gán ở đây: khi một string[]được yêu cầu để thực hiện ICollection<object>.Count, nó không thể xử lý rằng trong khá theo cách thông thường. Điều này có được coi là triển khai giao diện rõ ràng không? Tôi nghĩ là hợp lý để xử lý nó theo cách đó, vì trừ khi bạn yêu cầu ánh xạ giao diện trực tiếp, nó luôn hoạt động theo cách đó từ góc độ ngôn ngữ.

Về ICollection.Countthì sao?

Cho đến nay tôi đã nói về các giao diện chung, nhưng sau đó có những giao diện không chung ICollectionvới thuộc tính của nó Count. Lần này chúng ta có thể nhận được bản đồ giao diện và trên thực tế, giao diện được thực hiện trực tiếp bởi System.Array. Tài liệu cho việc ICollection.Counttriển khai thuộc tính ở Arraycác trạng thái rằng nó được triển khai với triển khai giao diện rõ ràng.

Nếu ai đó có thể nghĩ ra cách triển khai giao diện rõ ràng này khác với triển khai giao diện rõ ràng "bình thường", tôi rất vui được xem xét kỹ hơn.

Câu trả lời cũ xung quanh việc triển khai giao diện rõ ràng

Mặc dù ở trên, phức tạp hơn vì kiến ​​thức về mảng, bạn vẫn có thể làm điều gì đó với các hiệu ứng hiển thị tương tự thông qua triển khai giao diện rõ ràng .

Đây là một ví dụ độc lập đơn giản:

public interface IFoo
{
    void M1();
    void M2();
}

public class Foo : IFoo
{
    // Explicit interface implementation
    void IFoo.M1() {}

    // Implicit interface implementation
    public void M2() {}
}

class Test    
{
    static void Main()
    {
        Foo foo = new Foo();

        foo.M1(); // Compile-time failure
        foo.M2(); // Fine

        IFoo ifoo = foo;
        ifoo.M1(); // Fine
        ifoo.M2(); // Fine
    }
}

5
Tôi nghĩ rằng bạn sẽ gặp lỗi thời gian biên dịch trên foo.M1 (); không foo.M2 ();
Kevin Aenmey

Thách thức ở đây là có một lớp không chung chung, như mảng, triển khai kiểu giao diện chung, như IList <>. Đoạn mã của bạn không làm điều đó.
Hans Passant

@HansPassant: Rất dễ dàng để tạo một lớp không chung chung triển khai kiểu giao diện chung. Không đáng kể. Tôi không thấy bất kỳ dấu hiệu nào cho thấy đó là những gì OP đã yêu cầu.
Jon Skeet

4
@JohnSaunders: Trên thực tế, tôi không tin rằng bất kỳ điều nào trong số đó là không chính xác trước đây. Tôi đã mở rộng nó rất nhiều và giải thích tại sao CLR xử lý các mảng một cách kỳ lạ - nhưng tôi tin rằng câu trả lời của tôi về việc triển khai giao diện rõ ràng là khá đúng trước đây. Bạn không đồng ý theo cách nào? Một lần nữa, chi tiết sẽ hữu ích (có thể trong câu trả lời của riêng bạn, nếu thích hợp).
Jon Skeet

1
@RBT: Có, mặc dù có sự khác biệt ở chỗ việc sử dụng Countlà ổn - nhưng Addsẽ luôn luôn bị ném, vì các mảng có kích thước cố định.
Jon Skeet

86

Vì vậy, như bạn có thể biết, các mảng trong C # triển khai IList<T>, trong số các giao diện khác

Vâng, vâng, ừm không, không hẳn. Đây là khai báo cho lớp Mảng trong khuôn khổ .NET 4:

[Serializable, ComVisible(true)]
public abstract class Array : ICloneable, IList, ICollection, IEnumerable, 
                              IStructuralComparable, IStructuralEquatable
{
    // etc..
}

Nó thực hiện System.Collections.IList, không phải System.Collections.Generic.IList <>. Nó không thể, Mảng không phải là chung chung. Tương tự đối với giao diện IEnumerable <> và ICollection <> chung.

Nhưng CLR tạo ra các kiểu mảng cụ thể ngay lập tức, vì vậy về mặt kỹ thuật, nó có thể tạo ra một kiểu thực thi các giao diện này. Tuy nhiên, đây không phải là tình huống. Hãy thử mã này chẳng hạn:

using System;
using System.Collections.Generic;

class Program {
    static void Main(string[] args) {
        var goodmap = typeof(Derived).GetInterfaceMap(typeof(IEnumerable<int>));
        var badmap = typeof(int[]).GetInterfaceMap(typeof(IEnumerable<int>));  // Kaboom
    }
}
abstract class Base { }
class Derived : Base, IEnumerable<int> {
    public IEnumerator<int> GetEnumerator() { return null; }
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); }
}

Lệnh gọi GetInterfaceMap () không thành công đối với kiểu mảng cụ thể có "Không tìm thấy giao diện". Tuy nhiên, một truyền đến IEnumerable <> hoạt động mà không có vấn đề gì.

Đây là cách gõ bàn phím như vịt. Cũng chính kiểu gõ này tạo ra ảo tưởng rằng mọi kiểu giá trị đều bắt nguồn từ ValueType mà xuất phát từ Object. Cả trình biên dịch và CLR đều có kiến ​​thức đặc biệt về các kiểu mảng, cũng giống như chúng về các kiểu giá trị. Trình biên dịch nhận thấy nỗ lực của bạn trong việc truyền tới IList <> và nói "được rồi, tôi biết cách làm điều đó!". Và phát ra lệnh castclass IL. CLR không gặp khó khăn với nó, nó biết cách cung cấp một triển khai IList <> hoạt động trên đối tượng mảng bên dưới. Nó có kiến ​​thức tích hợp về lớp System.SZArrayHelper ẩn, một trình bao bọc thực sự triển khai các giao diện này.

Điều mà nó không hoạt động rõ ràng như mọi người tuyên bố, thuộc tính Count mà bạn đã hỏi về trông giống như sau:

    internal int get_Count<T>() {
        //! Warning: "this" is an array, not an SZArrayHelper. See comments above
        //! or you may introduce a security hole!
        T[] _this = JitHelpers.UnsafeCast<T[]>(this);
        return _this.Length;
    }

Vâng, bạn chắc chắn có thể gọi nhận xét đó là "phá vỡ các quy tắc" :) Nếu không thì nó rất tiện dụng. Và ẩn rất tốt, bạn có thể kiểm tra điều này trong SSCLI20, bản phân phối nguồn được chia sẻ cho CLR. Tìm kiếm "IList" để xem quá trình thay thế loại diễn ra ở đâu. Nơi tốt nhất để xem nó hoạt động là phương thức clr / src / vm / array.cpp, GetActualImplementationForArrayGenericIListMethod ().

Loại thay thế này trong CLR khá nhẹ so với những gì xảy ra trong phép chiếu ngôn ngữ trong CLR cho phép viết mã được quản lý cho WinRT (hay còn gọi là Metro). Chỉ về bất kỳ loại .NET cốt lõi nào được thay thế ở đó. Ví dụ: IList <> ánh xạ tới IVector <>, một loại hoàn toàn không được quản lý. Bản thân nó là một sự thay thế, COM không hỗ trợ các loại chung chung.

Chà, đó là một cái nhìn về những gì xảy ra sau bức màn. Đó có thể là những vùng biển rất khó chịu, xa lạ với những con rồng sống ở cuối bản đồ. Nó có thể rất hữu ích để làm cho Trái đất phẳng và mô hình hóa một hình ảnh khác về những gì đang thực sự diễn ra trong mã được quản lý. Ánh xạ nó với câu trả lời yêu thích của mọi người là thoải mái theo cách đó. Cái nào không hoạt động tốt cho các loại giá trị (đừng làm thay đổi cấu trúc!) Nhưng cái này được ẩn rất tốt. Lỗi phương thức GetInterfaceMap () là lỗi duy nhất trong phần trừu tượng mà tôi có thể nghĩ ra.


1
Đó là khai báo cho Arraylớp, không phải là kiểu của một mảng. Đây là kiểu cơ sở cho một mảng. Một mảng đơn chiều trong C # không thực hiện IList<T>. Và một loại phi generic chắc chắn có thể thực hiện một giao diện chung anyway ... mà làm việc vì có rất nhiều khác nhau loại - typeof(int[])! = Typeof (string []) , so typeof (int []) `dụng cụ IList<int>typeof(string[])dụng cụ IList<string>.
Jon Skeet

2
@HansPassant: Xin đừng cho rằng tôi không tán thành thứ gì đó chỉ vì nó không đáng lo ngại . Thực tế là cả lý luận của bạn thông qua Array(như bạn hiển thị, là một lớp trừu tượng, vì vậy không thể là kiểu thực tế của một đối tượng mảng) và kết luận (rằng nó không triển khai IList<T>) là IMO không chính xác. Các cách trong đó nó thực hiện IList<T>là không bình thường và thú vị, tôi sẽ đồng ý - nhưng đó hoàn toàn là một thực hiện chi tiết. Tuyên bố rằng T[]không triển khai IList<T>là IMO gây hiểu lầm. Nó đi ngược lại thông số kỹ thuật và tất cả các hành vi được quan sát.
Jon Skeet

6
Vâng, chắc chắn bạn nghĩ rằng nó là không chính xác. Bạn không thể làm cho nó vui vẻ với những gì bạn đọc trong thông số kỹ thuật. Vui lòng xem nó theo cách của bạn, nhưng bạn sẽ không bao giờ đưa ra được lời giải thích hợp lý tại sao GetInterfaceMap () không thành công. "Một cái gì đó sôi nổi" không phải là một cái nhìn sâu sắc. Tôi đang đeo kính triển khai: tất nhiên là nó không thành công, đó là kiểu gõ cục mịch, kiểu mảng cụ thể không thực sự triển khai ICollection <>. Không có gì thú vị về nó. Hãy giữ nó ở đây, chúng tôi sẽ không bao giờ đồng ý.
Hans Passant

4
Điều gì về ít nhất là loại bỏ logic giả mà tuyên bố mảng không thể thực hiện IList<T> Array không? Logic đó là một phần lớn của những gì tôi không đồng ý. Ngoài ra, tôi nghĩ chúng ta phải đồng ý về định nghĩa ý nghĩa của một kiểu để triển khai một giao diện: theo suy nghĩ của tôi, các kiểu mảng hiển thị tất cả các tính năng có thể quan sát được của các kiểu triển khai IList<T>, ngoại trừ GetInterfaceMapping. Một lần nữa, làm thế nào để đạt được điều đó ít quan trọng hơn đối với tôi, giống như tôi ổn khi nói rằng điều đó System.Stringlà bất biến, mặc dù các chi tiết triển khai là khác nhau.
Jon Skeet

1
Điều gì về trình biên dịch C ++ CLI? Điều đó rõ ràng nói rằng "Tôi không biết làm thế nào để làm điều đó!" và đưa ra một lỗi. Nó cần một diễn viên rõ ràng IList<T>để hoạt động.
Tobias Knauss

21

IList<T>.Countđược triển khai rõ ràng :

int[] intArray = new int[10];
IList<int> intArrayAsList = (IList<int>)intArray;
Debug.Assert(intArrayAsList.Count == 10);

Điều này được thực hiện để khi bạn có một biến mảng đơn giản, bạn không có sẵn cả hai CountLengthtrực tiếp.

Nói chung, triển khai giao diện rõ ràng được sử dụng khi bạn muốn đảm bảo rằng một loại có thể được sử dụng theo một cách cụ thể, mà không buộc tất cả người tiêu dùng của loại đó phải nghĩ về nó theo cách đó.

Chỉnh sửa : Rất tiếc, nhớ lại tệ ở đó. ICollection.Countđược thực hiện một cách rõ ràng. Phần chung IList<T>được xử lý như Hans descibes bên dưới .


4
Tuy nhiên, khiến tôi tự hỏi, tại sao họ không chỉ gọi thuộc tính là Count thay vì dài? Mảng là tập hợp chung duy nhất có thuộc tính như vậy (trừ khi bạn đếm string).
Tim S.

5
@TimS Một câu hỏi hay (và câu trả lời mà tôi không biết.) Tôi sẽ suy đoán rằng lý do là vì "count" ngụ ý một số mục, trong khi một mảng có "độ dài" không thể thay đổi ngay khi nó được cấp phát ( bất kể là yếu tố có giá trị).
dlev

1
@TimS Tôi nghĩ điều đó được thực hiện bởi vì ICollectionkhai báo Count, và sẽ còn khó hiểu hơn nếu một kiểu có từ "bộ sưu tập" trong đó không sử dụng Count:). Luôn có sự đánh đổi trong việc đưa ra những quyết định này.
dlev

4
@JohnSaunders: Và một lần nữa ... chỉ là một phản đối không có thông tin hữu ích.
Jon Skeet

5
@JohnSaunders: Tôi vẫn chưa bị thuyết phục. Hans đã đề cập đến việc triển khai SSCLI, nhưng cũng tuyên bố rằng các kiểu mảng thậm chí không triển khai IList<T>, mặc dù cả đặc tả ngôn ngữ và CLI đều có vẻ trái ngược. Tôi dám nói rằng cách triển khai giao diện hoạt động dưới vỏ bọc có thể phức tạp, nhưng đó là trường hợp trong nhiều tình huống. Bạn cũng sẽ phản đối ai đó nói rằng điều đó System.Stringlà bất biến, chỉ vì hoạt động bên trong có thể thay đổi? Đối với tất cả các mục đích thực tế - và chắc chắn là đối với ngôn ngữ C # - nó là hàm ý rõ ràng.
Jon Skeet


2

Nó không khác gì một triển khai giao diện rõ ràng của IList. Chỉ vì bạn triển khai giao diện không có nghĩa là các thành viên của nó cần phải xuất hiện như thành viên lớp. Nó thực hiện tước tài sản, nó chỉ không phơi bày nó trên X [].


1

Với các nguồn tham khảo có sẵn:

//----------------------------------------------------------------------------------------
// ! READ THIS BEFORE YOU WORK ON THIS CLASS.
// 
// The methods on this class must be written VERY carefully to avoid introducing security holes.
// That's because they are invoked with special "this"! The "this" object
// for all of these methods are not SZArrayHelper objects. Rather, they are of type U[]
// where U[] is castable to T[]. No actual SZArrayHelper object is ever instantiated. Thus, you will
// see a lot of expressions that cast "this" "T[]". 
//
// This class is needed to allow an SZ array of type T[] to expose IList<T>,
// IList<T.BaseType>, etc., etc. all the way up to IList<Object>. When the following call is
// made:
//
//   ((IList<T>) (new U[n])).SomeIListMethod()
//
// the interface stub dispatcher treats this as a special case, loads up SZArrayHelper,
// finds the corresponding generic method (matched simply by method name), instantiates
// it for type <T> and executes it. 
//
// The "T" will reflect the interface used to invoke the method. The actual runtime "this" will be
// array that is castable to "T[]" (i.e. for primitivs and valuetypes, it will be exactly
// "T[]" - for orefs, it may be a "U[]" where U derives from T.)
//----------------------------------------------------------------------------------------
sealed class SZArrayHelper {
    // It is never legal to instantiate this class.
    private SZArrayHelper() {
        Contract.Assert(false, "Hey! How'd I get here?");
    }

    /* ... snip ... */
}

Cụ thể phần này:

trình điều phối sơ khai giao diện coi đây là một trường hợp đặc biệt , tải lên SZArrayHelper, tìm phương thức chung tương ứng (đối sánh đơn giản bằng tên phương thức) , khởi tạo nó cho kiểu và thực thi nó.

(Tôi nhấn mạnh)

Nguồn (cuộn lê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.