Bất cứ ai có thể giải thích cho tôi tại sao tôi muốn sử dụng IList trên Danh sách trong C #?
Câu hỏi liên quan: Tại sao nó được coi là xấu để lộList<T>
Bất cứ ai có thể giải thích cho tôi tại sao tôi muốn sử dụng IList trên Danh sách trong C #?
Câu hỏi liên quan: Tại sao nó được coi là xấu để lộList<T>
Câu trả lời:
Nếu bạn đang trưng bày lớp của mình thông qua một thư viện mà người khác sẽ sử dụng, bạn thường muốn phơi bày nó thông qua các giao diện hơn là các triển khai cụ thể. Điều này sẽ giúp nếu bạn quyết định thay đổi việc thực hiện 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ỉ sử dụng nó trong nội bộ, bạn có thể không quan tâm lắm, và sử dụng List<T>
có thể ổn.
Câu trả lời ít phổ biến hơn là các lập trình viên muốn giả vờ phần mềm của họ sẽ được sử dụng lại trên toàn thế giới, khi mà phần lớn các dự án sẽ được duy trì bởi một số lượng nhỏ người và tuy nhiên các soundbites liên quan đến giao diện tốt, bạn đang ảo tưởng bản thân bạn.
Kiến trúc phi hành gia . Cơ hội bạn sẽ viết IList của riêng bạn để thêm bất cứ thứ gì vào những cái đã có trong .NET framework là rất xa đến nỗi các khối thạch lý thuyết dành riêng cho "các thực tiễn tốt nhất".
Rõ ràng nếu bạn đang được hỏi bạn sử dụng gì trong một cuộc phỏng vấn, bạn nói IList, mỉm cười và cả hai đều hài lòng với chính mình vì quá thông minh. Hoặc cho một API đối mặt công khai, IList. Hy vọng bạn có được quan điểm của tôi.
IEnumerable<string>
, bên trong hàm tôi có thể sử dụng một List<string>
cửa hàng sao lưu nội bộ để tạo bộ sưu tập của mình, nhưng tôi chỉ muốn người gọi liệt kê nội dung của nó, không thêm hoặc xóa. Việc chấp nhận một giao diện như một tham số truyền đạt một thông điệp tương tự "Tôi cần một bộ sưu tập các chuỗi, mặc dù vậy, đừng lo lắng, tôi sẽ không thay đổi nó."
Giao diện là một lời hứa (hoặc hợp đồng).
Vì nó luôn luôn với những lời hứa - càng nhỏ càng tốt .
IList
là lời hứa. Đó là lời hứa nhỏ hơn và tốt hơn ở đây? Điều này không hợp lý.
List<T>
, vì AddRange
không được xác định trên giao diện.
IList<T>
hứa rằng nó không thể giữ được. Chẳng hạn, có một Add
phương thức có thể ném nếu IList<T>
tình cờ chỉ đọc. List<T>
mặt khác luôn luôn có thể viết và do đó luôn giữ lời hứa. Ngoài ra, kể từ .NET 4.6, chúng tôi hiện có IReadOnlyCollection<T>
và IReadOnlyList<T>
hầu như luôn phù hợp hơn IList<T>
. Và họ là covariant. Tránh IList<T>
!
IList<T>
. Ai đó cũng có thể thực hiện tốt IReadOnlyList<T>
và làm cho mọi phương pháp cũng trở thành một ngoại lệ. Nó trái ngược với cách gõ vịt - bạn gọi nó là vịt, nhưng nó không thể quẫy như một con vịt. Đó là một phần của hợp đồng, ngay cả khi trình biên dịch không thể thực thi nó. Mặc dù vậy, tôi đồng ý 100% rằng IReadOnlyList<T>
hoặc IReadOnlyCollection<T>
nên được sử dụng để truy cập chỉ đọc.
Một số người nói "luôn luôn sử dụng IList<T>
thay vì List<T>
".
Họ muốn bạn thay đổi chữ ký phương thức của bạn từ void Foo(List<T> input)
thànhvoid Foo(IList<T> input)
.
Những người này là sai.
Nó nhiều sắc thái hơn thế. Nếu bạn đang trở lại mộtIList<T>
phần của giao diện công cộng cho thư viện của mình, bạn sẽ để lại cho mình các tùy chọn thú vị để có thể tạo một danh sách tùy chỉnh trong tương lai. Bạn có thể không bao giờ cần tùy chọn đó, nhưng đó là một đối số. Tôi nghĩ đó là toàn bộ đối số để trả về giao diện thay vì loại cụ thể. Điều đáng nói, nhưng trong trường hợp này nó có một lỗ hổng nghiêm trọng.
Là một đối tượng nhỏ, bạn có thể thấy mỗi người gọi đều cần một List<T>
mã và mã cuộc gọi bị lấp đầy.ToList()
Nhưng quan trọng hơn nhiều, nếu bạn chấp nhận IList như một tham số, bạn nên cẩn thận hơn, bởi vì IList<T>
và List<T>
không hành xử theo cùng một cách. Mặc dù có sự giống nhau về tên và mặc dù chia sẻ một giao diện, họ không để lộ cùng một hợp đồng .
Giả sử bạn có phương pháp này:
public Foo(List<int> a)
{
a.Add(someNumber);
}
Một đồng nghiệp hữu ích "tái cấu trúc" phương pháp để chấp nhận IList<int>
.
Mã của bạn hiện đã bị hỏng, vì int[]
thực hiện IList<int>
, nhưng có kích thước cố định. Hợp đồng cho ICollection<T>
(cơ sở IList<T>
) yêu cầu mã sử dụng nó để kiểm tra IsReadOnly
cờ trước khi cố gắng thêm hoặc xóa các mục khỏi bộ sưu tập. Hợp đồng List<T>
không.
Nguyên tắc thay thế Liskov (đơn giản hóa) nói rằng một loại dẫn xuất sẽ có thể được sử dụng thay cho loại cơ sở, không có điều kiện tiên quyết hoặc hậu điều kiện bổ sung.
Điều này cảm thấy như nó phá vỡ nguyên tắc thay thế Liskov.
int[] array = new[] {1, 2, 3};
IList<int> ilist = array;
ilist.Add(4); // throws System.NotSupportedException
ilist.Insert(0, 0); // throws System.NotSupportedException
ilist.Remove(3); // throws System.NotSupportedException
ilist.RemoveAt(0); // throws System.NotSupportedException
Nhưng nó không. Câu trả lời cho điều này là ví dụ đã sử dụng IList <T> / ICollection <T> sai. Nếu bạn sử dụng ICollection <T>, bạn cần kiểm tra cờ IsReadOnly.
if (!ilist.IsReadOnly)
{
ilist.Add(4);
ilist.Insert(0, 0);
ilist.Remove(3);
ilist.RemoveAt(0);
}
else
{
// what were you planning to do if you were given a read only list anyway?
}
Nếu ai đó chuyển cho bạn một Mảng hoặc Danh sách, mã của bạn sẽ hoạt động tốt nếu bạn kiểm tra cờ mỗi lần và có dự phòng ... Nhưng thực sự; ai làm điều đó? Bạn không biết trước nếu phương pháp của bạn cần một danh sách có thể có thêm thành viên; bạn không xác định rằng trong chữ ký phương thức? Chính xác thì bạn sẽ làm gì nếu bạn được thông qua một danh sách chỉ đọc như thế int[]
nào?
Bạn có thể thay thế một List<T>
mã sử dụng IList<T>
/ ICollection<T>
chính xác . Bạn không thể đảm bảo rằng bạn có thể thay thế một IList<T>
/ ICollection<T>
thành mã sử dụngList<T>
.
Có một sự hấp dẫn đối với Nguyên tắc phân chia trách nhiệm / nguyên tắc giao diện đơn trong rất nhiều đối số để sử dụng trừu tượng thay vì các loại cụ thể - phụ thuộc vào giao diện hẹp nhất có thể. Trong hầu hết các trường hợp, nếu bạn đang sử dụng a List<T>
và bạn nghĩ rằng bạn có thể sử dụng giao diện hẹp hơn - tại sao không IEnumerable<T>
? Điều này thường phù hợp hơn nếu bạn không cần thêm các mục. Nếu bạn cần thêm vào bộ sưu tập, hãy sử dụng loại bê tông List<T>
,.
Đối với tôi IList<T>
(và ICollection<T>
) là phần tồi tệ nhất của .NET framework. IsReadOnly
vi phạm nguyên tắc ít bất ngờ nhất. Một lớp, chẳng hạn như Array
, không bao giờ cho phép thêm, chèn hoặc xóa các mục không nên thực hiện giao diện với các phương thức Thêm, Chèn và Xóa. (xem thêm /software/306105/im THỰCing-an-interface-whoen-you-do-not-one-of-the -properies )
Là IList<T>
một phù hợp tốt cho tổ chức của bạn? Nếu một đồng nghiệp yêu cầu bạn thay đổi chữ ký phương thức để sử dụng IList<T>
thay vì List<T>
, hãy hỏi họ cách họ thêm một yếu tố vào một IList<T>
. Nếu họ không biết về IsReadOnly
(và hầu hết mọi người không biết), thì đừng sử dụng IList<T>
. Không bao giờ.
Lưu ý rằng cờ IsReadOnly xuất phát từ ICollection <T> và cho biết liệu các mục có thể được thêm hoặc xóa khỏi bộ sưu tập hay không; nhưng chỉ để thực sự nhầm lẫn mọi thứ, nó không cho biết liệu chúng có thể được thay thế hay không, trong trường hợp Mảng (trả lại IsReadOnlys == true).
Để biết thêm về IsReadOnly, hãy xem định nghĩa msd của ICollection <T> .IsReadOnly
IsReadOnly
là true
cho một IList
, bạn vẫn có thể sửa đổi các thành viên hiện tại của nó! Có lẽ tài sản đó nên được đặt tên IsFixedSize
?
Array
như một IList
, vì vậy đó là lý do tại sao tôi có thể sửa đổi các thành viên hiện tại)
IReadOnlyCollection<T>
và IReadOnlyList<T>
hầu như luôn phù hợp hơn IList<T>
nhưng không có ngữ nghĩa lười biếng nguy hiểm IEnumerable<T>
. Và họ là covariant. Tránh IList<T>
!
List<T>
là một triển khai cụ thể IList<T>
, là một thùng chứa có thể được xử lý giống như một mảng tuyến tính T[]
sử dụng một chỉ số nguyên. Khi bạn chỉ định IList<T>
làm kiểu đối số của phương thức, bạn chỉ xác định rằng bạn cần một số khả năng nhất định của vùng chứa.
Ví dụ, đặc tả giao diện không thực thi cấu trúc dữ liệu cụ thể được sử dụng. Việc thực hiện List<T>
xảy ra với cùng một hiệu suất để truy cập, xóa và thêm các phần tử dưới dạng một mảng tuyến tính. Tuy nhiên, thay vào đó, bạn có thể tưởng tượng một triển khai được hỗ trợ bởi một danh sách được liên kết, trong đó việc thêm các phần tử vào cuối sẽ rẻ hơn (thời gian không đổi) nhưng truy cập ngẫu nhiên đắt hơn nhiều. (Lưu ý rằng .NET LinkedList<T>
không không thực hiện IList<T>
.)
Ví dụ này cũng cho bạn biết rằng có thể có các tình huống khi bạn cần chỉ định triển khai, không phải giao diện, trong danh sách đối số: Trong ví dụ này, bất cứ khi nào bạn yêu cầu một đặc tính hiệu suất truy cập cụ thể. Điều này thường được đảm bảo cho việc triển khai cụ thể của một container ( List<T>
tài liệu: "Nó thực hiệnIList<T>
giao diện chung sử dụng một mảng có kích thước được tăng động theo yêu cầu.").
Ngoài ra, bạn có thể muốn xem xét phơi bày chức năng tối thiểu bạn cần. Ví dụ. nếu bạn không cần thay đổi nội dung của danh sách, có lẽ bạn nên cân nhắc sử dụng IEnumerable<T>
, phần IList<T>
mở rộng.
LinkedList<T>
so với lấy List<T>
so với IList<T>
tất cả giao tiếp một cái gì đó của đảm bảo hiệu suất được yêu cầu bởi mã được gọi.
Tôi sẽ chuyển câu hỏi xung quanh một chút, thay vì biện minh lý do tại sao bạn nên sử dụng giao diện trên triển khai cụ thể, hãy cố gắng giải thích lý do tại sao bạn sẽ sử dụng triển khai cụ thể thay vì giao diện. Nếu bạn không thể biện minh cho nó, hãy sử dụng giao diện.
Một nguyên tắc của TDD và OOP nói chung là lập trình cho một giao diện không phải là một triển khai.
Trong trường hợp cụ thể này vì về cơ bản bạn đang nói về một cấu trúc ngôn ngữ, không phải là một ngôn ngữ tùy chỉnh mà nó thường không thành vấn đề, nhưng nói ví dụ rằng bạn thấy Danh sách không hỗ trợ thứ bạn cần. Nếu bạn đã sử dụng IList trong phần còn lại của ứng dụng, bạn có thể mở rộng Danh sách với lớp tùy chỉnh của riêng mình và vẫn có thể vượt qua điều đó mà không cần cấu trúc lại.
Chi phí để làm điều này là tối thiểu, tại sao không tiết kiệm cho mình đau đầu sau này? Đó là những gì nguyên tắc giao diện là tất cả về.
public void Foo(IList<Bar> list)
{
// Do Something with the list here.
}
Trong trường hợp này, bạn có thể vượt qua trong bất kỳ lớp nào thực hiện giao diện IList <Bar>. Nếu bạn đã sử dụng Danh sách <Bar> thay vào đó, chỉ một thể hiện Danh sách <Bar> có thể được truyền vào.
Cách <Bar> của IList được ghép lỏng lẻo hơn so với cách Danh sách <Bar>.
Đáng ngạc nhiên là không có câu hỏi nào trong Danh sách này so với các câu hỏi (hoặc câu trả lời) của IList đề cập đến sự khác biệt về chữ ký. (Đó là lý do tại sao tôi tìm kiếm câu hỏi này trên SO!)
Vì vậy, đây là các phương thức có trong Danh sách không có trong IList, ít nhất là từ .NET 4.5 (khoảng năm 2015)
Reverse
và ToArray
là các phương thức mở rộng cho giao diện <T> của IE, mà IList <T> xuất phát từ đó.
List
s chứ không phải các triển khai khác IList
.
Trường hợp quan trọng nhất để sử dụng các giao diện trong quá trình triển khai là trong các tham số cho API của bạn. Nếu API của bạn có một tham số Danh sách, thì bất kỳ ai sử dụng nó đều phải sử dụng Danh sách. Nếu loại tham số là IList, thì người gọi có nhiều tự do hơn và có thể sử dụng các lớp bạn chưa từng nghe đến, thậm chí có thể không tồn tại khi mã của bạn được viết.
Điều gì nếu .NET 5.0 Thay thế System.Collections.Generic.List<T>
cho System.Collection.Generics.LinearList<T>
. .NET luôn sở hữu tên List<T>
nhưng họ đảm bảo rằngIList<T>
là hợp đồng. Vì vậy, IMHO chúng tôi (ít nhất là tôi) không được sử dụng tên của ai đó (mặc dù đó là .NET trong trường hợp này) và gặp rắc rối sau đó.
Trong trường hợp sử dụng IList<T>
, người gọi luôn có những thứ cần thiết để làm việc và người thực hiện có thể tự do thay đổi bộ sưu tập cơ bản thành bất kỳ triển khai cụ thể thay thế nào củaIList
Tất cả các khái niệm về cơ bản được nêu trong hầu hết các câu trả lời ở trên về lý do tại sao sử dụng giao diện trên các triển khai cụ thể.
IList<T> defines those methods (not including extension methods)
IList<T>
Liên kết MSDN
List<T>
thực hiện chín phương thức đó (không bao gồm các phương thức mở rộng), trên hết, nó có khoảng 41 phương thức công khai, cân nhắc trong việc xem xét sử dụng phương pháp nào trong ứng dụng của bạn.
List<T>
Liên kết MSDN
Bạn sẽ bởi vì việc xác định IList hoặc ICollection sẽ mở ra cho các triển khai khác của giao diện của bạn.
Bạn có thể muốn có một IOrderRep repository xác định một tập hợp các đơn đặt hàng trong IList hoặc ICollection. Sau đó, bạn có thể có các loại triển khai khác nhau để cung cấp danh sách các đơn hàng miễn là chúng tuân thủ "quy tắc" được xác định bởi IList hoặc ICollection của bạn.
IList <> hầu như luôn luôn được ưu tiên theo lời khuyên của người đăng khác, tuy nhiên lưu ý rằng có một lỗi trong .NET 3.5 sp 1 khi chạy IList <> qua hơn một chu kỳ tuần tự hóa / giải tuần tự hóa với WCF DataContractSerializer.
Hiện tại đã có SP để sửa lỗi này: KB 971030
Giao diện đảm bảo rằng bạn ít nhất có được các phương thức bạn đang mong đợi ; nhận thức được định nghĩa của giao diện tức là. tất cả các phương thức trừu tượng sẽ được thực hiện bởi bất kỳ lớp nào kế thừa giao diện. Vì vậy, nếu ai đó tạo ra một lớp lớn của riêng mình bằng một số phương thức bên cạnh các phương thức mà anh ta thừa hưởng từ giao diện cho một số chức năng bổ sung và những phương thức này không hữu dụng với bạn, tốt hơn là sử dụng tham chiếu đến một lớp con (trong trường hợp này là giao diện) và gán đối tượng lớp cụ thể cho nó.
lợi thế bổ sung là mã của bạn an toàn trước mọi thay đổi đối với lớp cụ thể khi bạn đăng ký chỉ một vài phương thức của lớp cụ thể và đó là những phương thức sẽ có ở đó miễn là lớp cụ thể kế thừa từ giao diện bạn đang có sử dụng. Vì vậy, nó an toàn cho bạn và tự do cho lập trình viên đang viết triển khai cụ thể để thay đổi hoặc thêm nhiều chức năng hơn vào lớp cụ thể của mình.
Bạn có thể xem xét lập luận này từ nhiều góc độ, bao gồm một trong những cách tiếp cận OO hoàn toàn nói rằng lập trình chống lại Giao diện không phải là triển khai. Với suy nghĩ này, việc sử dụng IList tuân theo hiệu trưởng giống như chuyển qua và sử dụng Giao diện mà bạn xác định từ đầu. Tôi cũng tin vào các yếu tố khả năng mở rộng và linh hoạt được cung cấp bởi một Giao diện nói chung. Nếu một lớp thực hiện IList <T> cần được mở rộng hoặc thay đổi, mã tiêu thụ không phải thay đổi; nó biết những gì hợp đồng Giao diện IList tuân thủ. Tuy nhiên, bằng cách sử dụng một triển khai cụ thể và Danh sách <T> trên một lớp thay đổi, có thể khiến mã cuộc gọi cũng cần phải được thay đổi. Điều này là do một lớp tuân thủ IList <T> đảm bảo một hành vi nhất định không được đảm bảo bởi một loại cụ thể sử dụng Danh sách <T>.
Cũng có sức mạnh để làm điều gì đó như sửa đổi các cài đặt mặc định của List <T> trên một lớp thực hiện IList <T> cho nói .Add, lệnh .remove hoặc bất kỳ phương pháp IList khác cung cấp cho các nhà phát triển một rất nhiều tính linh hoạt và sức mạnh, nếu không xác định trước theo danh sách <T>
Thông thường, một cách tiếp cận tốt là sử dụng IList trong API đối mặt công khai của bạn (khi thích hợp và cần có ngữ nghĩa danh sách), sau đó Liệt kê nội bộ để triển khai API. Điều này cho phép bạn thay đổi thành một triển khai IList khác mà không vi phạm mã sử dụng lớp của bạn.
Danh sách tên lớp có thể được thay đổi trong khung .net tiếp theo nhưng giao diện sẽ không bao giờ thay đổi vì giao diện là hợp đồng.
Lưu ý rằng, nếu API của bạn sẽ chỉ được sử dụng trong các vòng lặp foreach, v.v., thì bạn có thể muốn xem xét chỉ hiển thị IEnumerable thay thế.