Trả về IList <T> có tệ hơn trả về T [] hoặc List <T> không?


80

Câu trả lời cho những câu hỏi như sau: List <T> hoặc IList <T> dường như luôn đồng ý rằng trả về một giao diện tốt hơn là trả về một triển khai cụ thể của một tập hợp. Nhưng tôi đang đấu tranh với điều này. Việc khởi tạo một giao diện là không thể, vì vậy nếu phương thức của bạn đang trả về một giao diện, thì nó thực sự vẫn trả về một triển khai cụ thể. Tôi đã thử nghiệm một chút với điều này bằng cách viết 2 phương pháp nhỏ:

public static IList<int> ExposeArrayIList()
{
    return new[] { 1, 2, 3 };
}

public static IList<int> ExposeListIList()
{
    return new List<int> { 1, 2, 3 };
}

Và sử dụng chúng trong chương trình thử nghiệm của tôi:

static void Main(string[] args)
{
    IList<int> arrayIList = ExposeArrayIList();
    IList<int> listIList = ExposeListIList();

    //Will give a runtime error
    arrayIList.Add(10);
    //Runs perfectly
    listIList.Add(10);
}

Trong cả hai trường hợp khi tôi cố gắng thêm một giá trị mới, trình biên dịch của tôi không cho tôi lỗi nào, nhưng rõ ràng là phương thức hiển thị mảng của tôi dưới dạng một IList<T>lỗi thời gian chạy khi tôi cố gắng thêm thứ gì đó vào nó. Vì vậy, những người không biết điều gì đang xảy ra trong phương thức của tôi và phải thêm các giá trị vào nó, trước tiên buộc phải sao chép của tôi IListvào a Listđể có thể thêm các giá trị mà không có nguy cơ mắc lỗi. Tất nhiên, họ có thể đánh máy để xem liệu họ đang xử lý a Listhay an Array, nhưng nếu họ không làm như vậy và họ muốn thêm các mục vào bộ sưu tập, họ không có lựa chọn nào khác là sao chép IListvào a List, ngay cả khi nó đã là mộtList . Một mảng không bao giờ nên được hiển thị như IList?

Một mối quan tâm khác của tôi dựa trên câu trả lời được chấp nhận của câu hỏi được liên kết (tôi nhấn mạnh):

Nếu bạn đang hiển thị lớp của mình thông qua một thư viện mà những người khác sẽ sử dụng, bạn thường muốn hiển thị nó qua các giao diện hơn là triển khai cụ thể. Điều này sẽ hữu ích nếu bạn quyết định thay đổi việc triển khai lớp của mình sau này để sử dụng một lớp cụ thể khác. Trong trường hợp đó, người dùng thư viện của bạn sẽ không cần cập nhật mã của họ vì giao diện không thay đổi.

Nếu bạn chỉ đang sử dụng nó trong nội bộ, bạn có thể không quan tâm lắm, và sử dụng List có thể ok.

Hãy tưởng tượng ai đó thực sự đã sử dụng phương pháp của tôi IList<T>mà họ nhận được từ ExposeListIlist()phương pháp của tôi giống như vậy để thêm / bớt các giá trị. Mọi thứ đều hoạt động tốt. Nhưng bây giờ giống như câu trả lời gợi ý, bởi vì trả về một giao diện linh hoạt hơn, tôi trả về một mảng thay vì Danh sách (không có vấn đề gì với tôi!), Sau đó chúng sẽ được xử lý ...

TLDR:

1) Để lộ một giao diện gây ra các phôi không cần thiết? Điều đó không thành vấn đề?

2) Đôi khi nếu người dùng của thư viện không sử dụng truyền, mã của họ có thể bị hỏng khi bạn thay đổi phương thức của mình, mặc dù phương thức vẫn hoàn toàn tốt.

Tôi có lẽ đã suy nghĩ quá nhiều về điều này, nhưng tôi không nhận được sự đồng thuận chung rằng việc trả lại giao diện được ưu tiên hơn là trả lại một triển khai.


9
Sau đó quay lại IEnumerable<T>và bạn có thời gian biên dịch an toàn. Bạn vẫn có thể sử dụng tất cả các phương thức mở rộng LINQ thường cố gắng tối ưu hóa hiệu suất bằng cách truyền nó đến một loại cụ thể (Thích ICollection<T>sử dụng thuộc Counttính thay vì liệt kê nó).
Tim Schmelter

4
Mảng được thực hiện IList<T>bởi một "hack". Nếu bạn muốn hiển thị một phương thức trả về nó, hãy trả về một kiểu thực sự triển khai nó.
CodeCaster

3
Chỉ cần không hứa bạn không thể giữ. Lập trình viên khách hàng rất có thể sẽ thất vọng khi đọc bản hợp đồng "Tôi sẽ trả lại danh sách" của bạn. Nó không phải là vì vậy chỉ đừng làm điều đó.
Hans Passant

7
Từ MSDN : IList là hậu duệ của giao diện ICollection và là giao diện cơ sở của tất cả các danh sách không chung chung. Việc triển khai IList được chia thành ba loại: chỉ đọc, kích thước cố định và kích thước thay đổi. Không thể sửa đổi IList chỉ đọc. IList có kích thước cố định không cho phép thêm hoặc bớt các phần tử, nhưng nó cho phép sửa đổi các phần tử hiện có. IList có kích thước thay đổi cho phép thêm, bớt và sửa đổi các phần tử.
Hamid Pourjam

3
@AlexanderDerck: nếu bạn muốn người tiêu dùng có thể thêm các mục vào danh sách của bạn, đừng đưa nó IEnumerable<T>vào danh sách ngay từ đầu. Sau đó, nó là tốt để trở lại IList<T>hoặc List<T>.
Tim Schmelter

Câu trả lời:


108

Có thể đây không trực tiếp trả lời câu hỏi của bạn, nhưng trong .NET 4.5+, tôi muốn tuân theo các quy tắc sau khi thiết kế các API công khai hoặc được bảo vệ:

  • trả lại IEnumerable<T>, nếu chỉ có sẵn;
  • trả lại IReadOnlyCollection<T>nếu cả bảng liệt kê và số lượng mục đều có sẵn;
  • trả lại IReadOnlyList<T>, nếu liệt kê, số lượng mục và truy cập được lập chỉ mục có sẵn;
  • trả lại ICollection<T>nếu có sẵn bảng liệt kê, số lượng mục và sửa đổi;
  • trả lại IList<T>, nếu liệt kê, số lượng mục, truy cập được lập chỉ mục và sửa đổi có sẵn.

Hai tùy chọn cuối cùng giả định, phương thức đó không được trả về mảng dưới dạng IList<T>thực thi.


2
Không phải là một câu trả lời thực sự, nhưng quy tắc ngón tay cái tốt và rất hữu ích cho tôi :) Tôi đã đấu tranh rất nhiều với việc chọn những gì để trả lại gần đây. Chúc mừng!
Alexander Derck

1
Đây quả thực là nguyên tắc đúng đắn +1. Chỉ cần cho đầy đủ tôi sẽ thêm IReadOnlyCollection<T>ICollection<T>có sử dụng của họ cho container không lập chỉ mục (ví dụ sau này là de facto một tiêu chuẩn cho EF tính chuyển hướng bộ sưu tập thực thể phụ)
Ivan Stoev

@IvanStoev Bạn có thể chỉnh sửa câu trả lời với những người đã thêm không? Nó sẽ được tốt đẹp nếu cuối cùng tôi nhận được một danh sách các nơi để sử dụng những gì giao diện
Alexander Derck

3
Nó phải được lưu ý rằng IReadOnlyCollection<T>IReadOnlyList<T>có cùng một lỗ hổng như của C ++ const. Có hai khả năng: hoặc bộ sưu tập là bất biến, hoặc chỉ có cách bạn truy cập vào bộ sưu tập mới cấm bạn sửa đổi nó. Và bạn không thể nói với nhau.
ach

1
@Panzercrisis: Tất nhiên, tôi sẽ chọn tùy chọn thứ hai - IEnumerable<T>. Là một nhà phát triển API, bạn có đầy đủ kiến ​​thức về các trường hợp sử dụng được phép. Nếu kết quả của phương pháp chỉ nhằm mục đích liệt kê, thì không có lý do gì để tiết lộ IList<T>. Càng ít được phép làm với kết quả, thì việc thực hiện phương pháp càng linh hoạt. Ví dụ: exposing IEnumerable<T>cho phép bạn thay đổi triển khai thành yield returns, IList<T>không cho phép điều này.
Dennis

31

Không, vì người tiêu dùng nên biết IList chính xác là gì:

IList là hậu duệ của giao diện ICollection và là giao diện cơ sở của tất cả các danh sách không chung chung. Việc triển khai IList được chia thành ba loại: chỉ đọc, kích thước cố định và kích thước thay đổi. Không thể sửa đổi IList chỉ đọc. IList có kích thước cố định không cho phép thêm hoặc bớt các phần tử, nhưng nó cho phép sửa đổi các phần tử hiện có. IList có kích thước thay đổi cho phép thêm, bớt và sửa đổi các phần tử.

Bạn có thể kiểm tra IList.IsFixedSizeIList.IsReadOnlyvà làm những gì bạn muốn với nó.

Tôi nghĩ đây IListlà một ví dụ về giao diện béo và lẽ ra nó phải được chia thành nhiều giao diện nhỏ hơn và nó cũng vi phạm nguyên tắc thay thế Liskov khi bạn trả về một mảng dưới dạng một IList.

Đọc thêm nếu bạn muốn đưa ra quyết định về giao diện trả lại


CẬP NHẬT

Đào hơn và tôi thấy rằng IList<T>không thực hiện IListIsReadOnlycó thể truy cập thông qua giao diện cơ sở ICollection<T>nhưng không có IsFixedSizecho IList<T>. Đọc thêm về lý do tại sao IList chung <> không kế thừa IList không chung chung?


8
Những thuộc tính đó là tốt, nhưng nó vẫn đánh bại mục đích của một giao diện theo ý kiến ​​của tôi. Đối với tôi, giao diện là một hợp đồng, mà không nên phụ thuộc vào bất kỳ séc hoặc bất cứ điều gì
Alexander Derck

1
Nó thực sự sẽ tốt hơn cho nhà phát triển nếu họ cung cấp một giao diện bổ sung có List<T>ngữ nghĩa (kích thước không cố định / không chỉ đọc) , nhưng có lẽ có lý do hợp lý tại sao điều này không được bao gồm .... nhưng, vâng, tôi nghĩ ý tưởng của bạn về một số giao diện bao gồm các tính năng khác nhau sẽ là một lựa chọn sáng suốt hơn nhiều;
tiêu

1
IList<T>là một giao diện béo nhưng đó là giao diện nhiều nhất bạn có thể nhận được từ mảng danh sách. Vì vậy, nếu nó đã được chia thành nhiều giao diện, bạn có thể không sử dụng được một số phương thức với danh sách và mảng mà chỉ với một trong số chúng, ngay cả khi tất cả các phương thức bạn muốn sử dụng đều được hỗ trợ (như IList.Counthoặc trình lập chỉ mục). Vì vậy, theo tôi đó là một quyết định đúng đắn. Nếu nó gây ra một NotSupportedExceptiontrong rất ít trường hợp mà người ta muốn sử dụng Addtrên một mảng, thì đó là thỏa thuận.
Tim Schmelter

1
@dotctor Thật là một sự trùng hợp, tôi đã được đọc về nguyên tắc thay thế Liskov ngày hôm qua và điều đó khiến tôi suy nghĩ thực sự :) cảm ơn cho câu trả lời
Alexander Derck

6
Câu trả lời này kết hợp với các quy tắc Dennis chắc chắn có thể nâng cao hiểu biết về cách đối phó với tình huống khó xử này. Không cần phải nói rằng nguyên tắc thay thế Liskov bị vi phạm ở đây và ở đó trong .NET framework và nó không phải là điều gì đó đặc biệt đáng lo ngại.
Fabjan

13

Như với tất cả câu hỏi "giao diện so với triển khai", bạn sẽ phải nhận ra việc lộ thành viên công khai nghĩa là gì: nó xác định API công khai của lớp này.

Nếu bạn để lộ a List<T>với tư cách là một thành viên (trường, thuộc tính, phương thức, ...), bạn nói với người tiêu dùng của thành viên đó: loại có được bằng cách truy cập phương thức này là một List<T>hoặc một cái gì đó bắt nguồn từ đó.

Bây giờ nếu bạn để lộ một giao diện, bạn sẽ ẩn "chi tiết triển khai" của lớp bằng một kiểu cụ thể. Tất nhiên bạn có thể không nhanh chóng IList<T>, nhưng bạn có thể sử dụng một Collection<T>, List<T>, Mục từ đó hoặc kiểu riêng của bạn thực hiện IList<T>.

Câu hỏi thực tế"Tại sao Arraythực hiện IList<T>" , hoặc "Tại sao IList<T>giao diện có quá nhiều thành viên" .

Nó cũng phụ thuộc vào những gì bạn muốn người tiêu dùng của thành viên đó làm. Nếu bạn thực sự trả lại một thành viên nội bộ thông qua thành viên của mình Expose..., bạn sẽ muốn trả lại bằng cách new List<T>(internalMember)nào đó, vì nếu không, người tiêu dùng có thể thử chuyển họ đến IList<T>và sửa đổi thành viên nội bộ của bạn thông qua đó.

Nếu bạn chỉ mong đợi người tiêu dùng lặp lại kết quả, hãy hiển thị IEnumerable<T>hoặc IReadOnlyCollection<T>thay vào đó.


3
"Câu hỏi thực tế là" Tại sao Array triển khai IList <T> ", hoặc" Tại sao giao diện IList <T> lại có nhiều thành viên như vậy ".", Đó thực sự là điều tôi đang gặp khó khăn.
Alexander Derck

2
@Fabjan - IList<T>có các phương thức để thêm và xóa các mục, đó là lý do tại sao việc triển khai các mảng là không tốt. Các IsReadOnlybất động sản là một hack để che đậy sự khác biệt khi cần có được chia thành (ít nhất) hai giao diện.
Lee

@Lee Rất tiếc, bạn nói đúng, đang xóa nhận xét không chính xác của tôi
Fabjan

1
@AlexanderDerck, đó là điều tôi đã hỏi trước đây stackoverflow.com/q/5968708/706456
oleksii

@oleksii nó tương tự như thực sự, nhưng tôi sẽ không gọi nó là một trùng lặp
Alexander Derck

8

Hãy cẩn thận với các trích dẫn chung chung được đưa ra khỏi ngữ cảnh.

Trả lại một giao diện tốt hơn là trả lại một triển khai cụ thể

Trích dẫn này chỉ có ý nghĩa nếu nó được sử dụng trong ngữ cảnh của các nguyên tắc SOLID . Có 5 nguyên tắc nhưng với mục đích của cuộc thảo luận này, chúng tôi sẽ chỉ nói về 3 nguyên tắc cuối cùng.

Nguyên tắc nghịch đảo phụ thuộc

người ta nên “Phụ thuộc vào những Tóm tắt. Đừng phụ thuộc vào sự cụ thể hóa. "

Theo tôi, nguyên tắc này là khó hiểu nhất. Nhưng nếu bạn xem kỹ phần trích dẫn thì nó trông rất giống với câu trích dẫn ban đầu của bạn.

Phụ thuộc vào các giao diện (trừu tượng). Không phụ thuộc vào việc triển khai cụ thể (bê tông hóa).

Điều này vẫn còn hơi khó hiểu nhưng nếu chúng ta bắt đầu áp dụng các nguyên tắc khác cùng nhau, nó sẽ bắt đầu có ý nghĩa hơn rất nhiều.

Nguyên tắc thay thế Liskov

"Các đối tượng trong một chương trình phải có thể thay thế được bằng các thể hiện của kiểu con của chúng mà không làm thay đổi tính đúng đắn của chương trình đó."

Như bạn đã chỉ ra, trả về một Arrayhành vi rõ ràng khác với trả về một List<T>mặc dù cả hai đều thực hiện IList<T>. Đây chắc chắn là một sự vi phạm LSP.

Điều quan trọng cần nhận ra là giao diện là về người tiêu dùng . Nếu bạn đang trả lại một giao diện, bạn đã tạo một hợp đồng mà bất kỳ phương thức hoặc thuộc tính nào trên giao diện đó đều có thể được sử dụng mà không thay đổi hành vi của chương trình.

Nguyên tắc phân tách giao diện

“Nhiều giao diện dành riêng cho khách hàng tốt hơn một giao diện có mục đích chung”.

Nếu bạn đang trả lại một giao diện, bạn nên trả lại giao diện dành riêng cho ứng dụng khách nhất mà triển khai của bạn hỗ trợ. Nói cách khác, nếu bạn không mong đợi ứng dụng khách gọi Addphương thức, bạn không nên trả về giao diện có Addphương thức trên đó.

Thật không may, các giao diện trong khuôn khổ .NET (đặc biệt là các phiên bản đầu tiên) không phải lúc nào cũng là giao diện lý tưởng dành cho máy khách. Mặc dù như @Dennis đã chỉ ra trong câu trả lời của mình, có rất nhiều lựa chọn hơn trong .NET 4.5+.


Đọc lên trên nguyên tắc Liskov là những gì tôi đã nhầm lẫn ở nơi đầu tiên choIList<T>
Alexander Derck

Trong trường hợp cụ thể, đó là Mảng vi phạm LSP. Nếu bạn đang trả lại một IList, bạn không bao giờ nên trả lại một mảng vì nếu vi phạm điều này. Bạn đã thực hiện hợp đồng với người gọi rằng họ sẽ nhận lại một danh sách có thể có các mục được thêm và xóa.
craftworkgames

5

Trả lại một giao diện không nhất thiết phải tốt hơn việc trả lại một triển khai cụ thể của một tập hợp. Bạn phải luôn có lý do chính đáng để sử dụng giao diện thay vì một kiểu cụ thể. Trong ví dụ của bạn, làm như vậy có vẻ vô nghĩa.

Các lý do hợp lệ để sử dụng một giao diện có thể là:

  1. Bạn không biết việc triển khai các phương thức trả về giao diện sẽ như thế nào và có thể có rất nhiều, được phát triển theo thời gian. Nó có thể là người khác viết chúng, từ các công ty khác. Vì vậy, bạn chỉ muốn đồng ý về những điều cần thiết và để họ làm thế nào để triển khai chức năng.

  2. Bạn muốn hiển thị một số chức năng phổ biến độc lập với hệ thống phân cấp lớp của mình theo cách an toàn về kiểu. Các đối tượng thuộc các loại cơ sở khác nhau cung cấp các phương pháp giống nhau sẽ triển khai giao diện của bạn.

Người ta có thể tranh luận rằng 1 và 2 về cơ bản là cùng một lý do. Chúng là hai kịch bản khác nhau nhưng cuối cùng dẫn đến cùng một nhu cầu.

"Đó là một hợp đồng". Nếu hợp đồng với chính bạn và ứng dụng của bạn bị đóng về cả chức năng và thời gian, thì việc sử dụng giao diện thường không có ích gì.


1
Tôi không thực sự đồng ý, nếu có thể trả lại một giao diện, tôi sẽ trả lại một giao diện vì đó là thứ duy nhất cần thiết. Nếu một phương thức mong đợi một giao diện, tôi có thể chuyển một lớp chỉ triển khai giao diện đó cho nó, nhưng cũng có một lớp triển khai giao diện + vô số thứ khác. Với giao diện nó dễ dàng hơn để mở rộng ứng dụng của bạn mà không cần viết lại những thứ đang tồn tại
Alexander Derck

2
Có thể thực sự sáng suốt khi phát triển tất cả phần mềm của bạn (và đặc biệt là các lớp và thư viện lớp của bạn) với suy nghĩ "Điều gì sẽ xảy ra nếu ai đó sử dụng lớp (thư viện) này?" "Điều gì sẽ xảy ra nếu tôi muốn thêm chức năng vào loại cụ thể mà tôi muốn trả lại?" . Các loại BCL có thể được niêm phong, vì vậy bạn không thể mở rộng chúng. Sau đó, bạn bị mắc kẹt nếu bạn muốn trả lại thứ gì đó khác, vì API của bạn hứa hẹn sẽ hiển thị loại chính xác đó, trái ngược với một giao diện. Trả lại một giao diện hầu như luôn tốt hơn là trả về một triển khai cụ thể.
CodeCaster

1
Tôi đồng ý (thực sự), lẽ ra tôi nên thêm một lý do thứ ba: "giới hạn chức năng của một đối tượng trả về về bản chất của nó để làm rõ thông điệp cho người gọi". Tôi cũng trả lại các giao diện IReadOnlyXxx bất cứ khi nào có thể thay vì đối tượng chính thức đầy đủ được sử dụng để lắp ráp bộ sưu tập.
Martin Maat
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.