Danh sách so với danh sách - Sử dụng cái gì? Họ làm việc như thế nào?


675

Tôi có một số nghi ngờ về cách thức hoạt động của En Countators và LINQ. Hãy xem xét hai lựa chọn đơn giản sau:

List<Animal> sel = (from animal in Animals 
                    join race in Species
                    on animal.SpeciesKey equals race.SpeciesKey
                    select animal).Distinct().ToList();

hoặc là

IEnumerable<Animal> sel = (from animal in Animals 
                           join race in Species
                           on animal.SpeciesKey equals race.SpeciesKey
                           select animal).Distinct();

Tôi đã thay đổi tên của các đối tượng ban đầu của tôi để nó trông giống như một ví dụ chung hơn. Các truy vấn không phải là quan trọng. Điều tôi muốn hỏi là đây:

foreach (Animal animal in sel) { /*do stuff*/ }
  1. Tôi nhận thấy rằng nếu tôi sử dụng IEnumerable, khi tôi gỡ lỗi và kiểm tra "sel", trong trường hợp đó là IEnumerable, nó có một số thành viên thú vị: "bên trong", "bên ngoài", "InternalKeySelector" và "outsKeySelector", 2 cái cuối cùng này xuất hiện làm đại biểu. Thành viên "bên trong" không có phiên bản "Động vật" trong đó, mà là các trường hợp "Loài", điều này rất lạ đối với tôi. Thành viên "bên ngoài" có chứa các trường hợp "Động vật". Tôi đoán rằng hai đại biểu xác định cái nào đi vào và cái gì đi ra khỏi nó?

  2. Tôi nhận thấy rằng nếu tôi sử dụng "Khác biệt", "bên trong" chứa 6 mục (điều này không chính xác vì chỉ có 2 là Khác biệt), nhưng "bên ngoài" có chứa các giá trị chính xác. Một lần nữa, có lẽ các phương thức được ủy nhiệm xác định điều này nhưng điều này nhiều hơn một chút so với những gì tôi biết về IEnumerable.

  3. Quan trọng nhất, lựa chọn nào trong hai tùy chọn là hiệu suất tốt nhất?

Danh sách ác chuyển đổi qua .ToList()?

Hoặc có thể sử dụng điều tra viên trực tiếp?

Nếu bạn có thể, vui lòng giải thích một chút hoặc ném một số liên kết giải thích việc sử dụng IEnumerable này.

Câu trả lời:


737

IEnumerablemô tả hành vi, trong khi Danh sách là việc thực hiện hành vi đó. Khi bạn sử dụng IEnumerable, bạn cung cấp cho trình biên dịch một cơ hội để trì hoãn công việc cho đến sau này, có thể tối ưu hóa trên đường đi. Nếu bạn sử dụng ToList (), bạn buộc trình biên dịch xác nhận lại kết quả ngay lập tức.

Bất cứ khi nào tôi "xếp chồng" các biểu thức LINQ, tôi đều sử dụng IEnumerable, bởi vì chỉ xác định hành vi tôi cho LINQ cơ hội trì hoãn đánh giá và có thể tối ưu hóa chương trình. Hãy nhớ cách LINQ không tạo SQL để truy vấn cơ sở dữ liệu cho đến khi bạn liệt kê nó? Xem xét điều này:

public IEnumerable<Animals> AllSpotted()
{
    return from a in Zoo.Animals
           where a.coat.HasSpots == true
           select a;
}

public IEnumerable<Animals> Feline(IEnumerable<Animals> sample)
{
    return from a in sample
           where a.race.Family == "Felidae"
           select a;
}

public IEnumerable<Animals> Canine(IEnumerable<Animals> sample)
{
    return from a in sample
           where a.race.Family == "Canidae"
           select a;
}

Bây giờ bạn có một phương pháp chọn một mẫu ban đầu ("AllSpiated"), cộng với một số bộ lọc. Vì vậy, bây giờ bạn có thể làm điều này:

var Leopards = Feline(AllSpotted());
var Hyenas = Canine(AllSpotted());

Vì vậy, nó là nhanh hơn để sử dụng Danh sách hơn IEnumerable? Chỉ khi bạn muốn ngăn truy vấn được thực thi nhiều lần. Nhưng nó là tốt hơn về tổng thể? Cũng như ở trên, Leopards và Hyenas được chuyển đổi thành các truy vấn SQL duy nhất và mỗi cơ sở dữ liệu chỉ trả về các hàng có liên quan. Nhưng nếu chúng tôi đã trả về một Danh sách từ AllSpotted()đó, thì nó có thể chạy chậm hơn vì cơ sở dữ liệu có thể trả về nhiều dữ liệu hơn mức thực sự cần thiết và chúng tôi lãng phí các chu trình thực hiện việc lọc trong máy khách.

Trong một chương trình, có thể tốt hơn là trì hoãn chuyển đổi truy vấn của bạn thành một danh sách cho đến khi kết thúc, vì vậy nếu tôi sẽ liệt kê thông qua Leopards và Hyenas hơn một lần, tôi sẽ làm điều này:

List<Animals> Leopards = Feline(AllSpotted()).ToList();
List<Animals> Hyenas = Canine(AllSpotted()).ToList();

11
Tôi nghĩ rằng họ đề cập đến hai mặt của một tham gia. Nếu bạn thực hiện "CHỌN * TỪ Động vật THAM GIA ..." thì phần bên trong của phép nối là Động vật và phần bên ngoài là Loài.
Chris Wenham

10
Khi tôi đọc câu trả lời về: IEnumerable <T> vs IQueryable <T> Tôi đã thấy giải thích tương tự, do đó IEnumerable tự động buộc bộ thực thi sử dụng LINQ to Object để truy vấn bộ sưu tập. Vì vậy, tôi nhầm lẫn giữa 3 loại này. stackoverflow.com/questions/2876616/
Mạnh

4
@Bronek Câu trả lời bạn liên kết là đúng. IEnumerable<T>sẽ là LINQ-To-Object sau phần đầu tiên có nghĩa là tất cả các đốm được phát hiện sẽ phải được trả về để chạy Feline. Mặt khác, một IQuertable<T>ý chí sẽ cho phép truy vấn được tinh chỉnh, chỉ kéo xuống Spốm Felines.
Nate

21
Câu trả lời này rất sai lệch! Nhận xét của @ Nate giải thích tại sao. Nếu bạn đang sử dụng IEnumerable <T>, bộ lọc sẽ xảy ra ở phía máy khách bất kể điều gì.
Hans

5
Có AllSpiated () sẽ được chạy hai lần. Vấn đề lớn hơn với câu trả lời này là câu lệnh sau: "Vâng, ở trên, Leopards và Hyenas được chuyển đổi thành các truy vấn SQL duy nhất và mỗi cơ sở dữ liệu chỉ trả về các hàng có liên quan." Điều này là sai, vì mệnh đề where đang được gọi trên IEnumerable <> và điều đó chỉ biết cách lặp qua các đối tượng đã đến từ cơ sở dữ liệu. Nếu bạn đã trả lại AllSpiated () và các tham số của Feline () và Canine () thành IQueryable, thì bộ lọc sẽ xảy ra trong SQL và câu trả lời này sẽ có ý nghĩa.
Hans

178

Có một bài viết rất hay được viết bởi: Claudio Bernasconi's TechBlog tại đây: Khi nào nên sử dụng IEnumerable, ICollection, IList và List

Dưới đây là một số điểm cơ bản về các kịch bản và chức năng:

nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây


25
Cần phải chỉ ra rằng bài viết này chỉ dành cho các phần phải đối mặt công khai trong mã của bạn, không phải cho các hoạt động nội bộ. Listlà một thực hiện IListvà như vậy có chức năng bổ sung trên đầu trang của những người trong IList(ví dụ Sort, Find, InsertRange). Nếu bạn buộc mình để sử dụng IListtrên List, bạn mất những phương pháp mà bạn có thể yêu cầu
Jonathan giống chim hồng tước

4
Đừng quênIReadOnlyCollection<T>
Dandré

2
Nó có thể hữu ích để bao gồm một mảng đơn giản []ở đây là tốt.
jbyrd

Trong khi nó có thể được tán thành, cảm ơn bạn đã chia sẻ đồ họa và bài viết này
Daniel

133

Một lớp thực hiện IEnumerablecho phép bạn sử dụng foreachcú pháp.

Về cơ bản nó có một phương pháp để có được mục tiếp theo trong bộ sưu tập. Nó không cần toàn bộ bộ sưu tập trong bộ nhớ và không biết có bao nhiêu vật phẩm trong đó, foreachchỉ cần tiếp tục lấy vật phẩm tiếp theo cho đến khi hết.

Điều này có thể rất hữu ích trong một số trường hợp nhất định, ví dụ trong một bảng cơ sở dữ liệu lớn mà bạn không muốn sao chép toàn bộ vào bộ nhớ trước khi bắt đầu xử lý các hàng.

Bây giờ Listthực hiện IEnumerable, nhưng đại diện cho toàn bộ bộ sưu tập trong bộ nhớ. Nếu bạn có một IEnumerablevà bạn gọi .ToList()bạn tạo một danh sách mới với nội dung liệt kê trong bộ nhớ.

Biểu thức linq của bạn trả về một bảng liệt kê và theo mặc định, biểu thức sẽ thực thi khi bạn lặp lại bằng cách sử dụng foreach. Một IEnumerablecâu lệnh linq thực thi khi bạn lặp lại foreach, nhưng bạn có thể buộc nó lặp lại sớm hơn bằng cách sử dụng .ToList().

Ý tôi là đây:

var things = 
    from item in BigDatabaseCall()
    where ....
    select item;

// this will iterate through the entire linq statement:
int count = things.Count();

// this will stop after iterating the first one, but will execute the linq again
bool hasAnyRecs = things.Any();

// this will execute the linq statement *again*
foreach( var thing in things ) ...

// this will copy the results to a list in memory
var list = things.ToList()

// this won't iterate through again, the list knows how many items are in it
int count2 = list.Count();

// this won't execute the linq statement - we have it copied to the list
foreach( var thing in list ) ...

2
Nhưng điều gì xảy ra nếu bạn thực hiện một foreach trên IEnumerable mà không chuyển đổi nó thành Danh sách trước ? Liệu nó mang lại toàn bộ bộ sưu tập trong bộ nhớ? Hoặc, nó khởi tạo từng phần tử một, khi nó lặp qua vòng lặp foreach? cảm ơn
Pap

@Pap cái sau: nó thực thi lại, không có gì được tự động lưu trong bộ nhớ.
Keith

có vẻ như khóa diff là 1) toàn bộ trong bộ nhớ hay không. 2) Vô số cho phép tôi sử dụng foreachtrong khi Danh sách sẽ đi theo chỉ số. Bây giờ, nếu tôi muốn biết đếm / chiều dài của thingtrước, IEnumerable sẽ không thể giúp ích, phải không?
Jeb50

@ Jeb50 Không chính xác - cả ListArraythực hiện IEnumerable. Bạn có thể nghĩ về IEnumerablemẫu số chung thấp nhất hoạt động cho cả trong bộ sưu tập bộ nhớ và mẫu lớn có được một mục tại một thời điểm. Khi bạn gọi, IEnumerable.Count()bạn có thể đang gọi một .Lengthtài sản nhanh hoặc đi qua toàn bộ bộ sưu tập - vấn đề là IEnumerablebạn không biết. Đó có thể là một vấn đề, nhưng nếu bạn chỉ đi đến foreachđó thì bạn không quan tâm - mã của bạn sẽ hoạt động với một Arrayhoặc DataReadergiống nhau.
Keith

1
@MFouadKajj Tôi không biết bạn đang sử dụng stack nào, nhưng gần như chắc chắn không đưa ra yêu cầu với mỗi hàng. Máy chủ chạy truy vấn và tính điểm bắt đầu của tập kết quả, nhưng không nhận được toàn bộ. Đối với các tập kết quả nhỏ, đây có thể là một chuyến đi, đối với những người lớn bạn đang gửi yêu cầu cho nhiều hàng hơn từ kết quả, nhưng nó không chạy lại toàn bộ truy vấn.
Keith

97

Không ai đề cập đến một sự khác biệt quan trọng, mỉa mai trả lời về một câu hỏi đóng như là một bản sao của điều này.

IEnumerable là chỉ đọc và Danh sách thì không.

Xem sự khác biệt thực tế giữa Danh sách và IEnumerable


Theo dõi, đó là do khía cạnh Giao diện hay vì khía cạnh Danh sách? tức là IList cũng chỉ đọc?
Jason Masters

IList không chỉ đọc - docs.microsoft.com/en-us/dotnet/api/iến IEnumerable là chỉ đọc vì nó thiếu bất kỳ phương thức nào để thêm hoặc xóa bất cứ thứ gì khi nó được xây dựng, nó là một trong những giao diện cơ bản IList mở rộng (xem liên kết)
CAD bloke

67

Điều quan trọng nhất cần nhận ra là, bằng cách sử dụng Linq, truy vấn không được đánh giá ngay lập tức. Nó chỉ được chạy như một phần của việc lặp lại thông qua kết quả IEnumerable<T>foreach- đó là điều mà tất cả các đại biểu kỳ lạ đang làm.

Vì vậy, ví dụ đầu tiên đánh giá truy vấn ngay lập tức bằng cách gọi ToListvà đưa kết quả truy vấn vào danh sách.
Ví dụ thứ hai trả về một IEnumerable<T>chứa tất cả thông tin cần thiết để chạy truy vấn sau này.

Về hiệu suất, câu trả lời là phụ thuộc . Nếu bạn cần đánh giá kết quả ngay lập tức (giả sử, bạn đang thay đổi cấu trúc bạn truy vấn sau này hoặc nếu bạn không muốn lặp đi lặp lại trong IEnumerable<T>một thời gian dài), hãy sử dụng danh sách. Khác sử dụng một IEnumerable<T>. Mặc định nên sử dụng đánh giá theo yêu cầu trong ví dụ thứ hai, vì thông thường sử dụng ít bộ nhớ hơn, trừ khi có một lý do cụ thể để lưu trữ kết quả trong danh sách.


Xin chào và cảm ơn vì đã trả lời :: -). Điều này đã xóa tan gần như tất cả những nghi ngờ của tôi. Bất cứ ý tưởng tại sao Enumerable được "chia" thành "bên trong" và "bên ngoài"? Điều này xảy ra khi tôi kiểm tra phần tử ở chế độ gỡ lỗi / ngắt thông qua chuột. Đây có lẽ là đóng góp của Visual Studio? Việc liệt kê tại chỗ và chỉ ra đầu vào và đầu ra của Enum?
Axonn

5
Đó là công Joinviệc của nó - bên trong và bên ngoài là hai mặt của sự tham gia. Nói chung, đừng lo lắng về những gì thực sự trong IEnumerables, vì nó sẽ hoàn toàn khác với mã thực tế của bạn. Chỉ lo lắng về sản lượng thực tế khi bạn lặp lại nó :)
thecoop

40

Ưu điểm của IEnumerable là thực thi hoãn lại (thường là với cơ sở dữ liệu). Truy vấn sẽ không được thực hiện cho đến khi bạn thực sự lặp qua dữ liệu. Đó là một truy vấn chờ cho đến khi nó cần thiết (còn gọi là lười tải).

Nếu bạn gọi ToList, truy vấn sẽ được thực thi hoặc "cụ thể hóa" như tôi muốn nói.

Cả hai đều có ưu điểm và nhược điểm riêng. Nếu bạn gọi ToList, bạn có thể xóa một số bí ẩn khi truy vấn được thực thi. Nếu bạn gắn bó với IEnumerable, bạn sẽ có được lợi thế là chương trình không thực hiện bất kỳ công việc nào cho đến khi nó thực sự cần thiết.


25

Tôi sẽ chia sẻ một khái niệm bị lạm dụng mà tôi rơi vào một ngày:

var names = new List<string> {"mercedes", "mazda", "bmw", "fiat", "ferrari"};

var startingWith_M = names.Where(x => x.StartsWith("m"));

var startingWith_F = names.Where(x => x.StartsWith("f"));


// updating existing list
names[0] = "ford";

// Guess what should be printed before continuing
print( startingWith_M.ToList() );
print( startingWith_F.ToList() );

Kết quả mong đợi

// I was expecting    
print( startingWith_M.ToList() ); // mercedes, mazda
print( startingWith_F.ToList() ); // fiat, ferrari

Kết quả thực tế

// what printed actualy   
print( startingWith_M.ToList() ); // mazda
print( startingWith_F.ToList() ); // ford, fiat, ferrari

Giải trình

Theo các câu trả lời khác, việc đánh giá kết quả đã được hoãn lại cho đến khi gọi ToListhoặc các phương thức gọi tương tự chẳng hạn ToArray.

Vì vậy, tôi có thể viết lại mã trong trường hợp này là:

var names = new List<string> {"mercedes", "mazda", "bmw", "fiat", "ferrari"};

// updating existing list
names[0] = "ford";

// before calling ToList directly
var startingWith_M = names.Where(x => x.StartsWith("m"));

var startingWith_F = names.Where(x => x.StartsWith("f"));

print( startingWith_M.ToList() );
print( startingWith_F.ToList() );

Chơi arround

https://repl.it/E8Ki/0


1
Đó là do các phương thức linq (phần mở rộng) mà trong trường hợp này đến từ IEnumerable, nơi chỉ tạo một truy vấn nhưng không thực hiện nó (đằng sau hậu trường, các cây biểu thức được sử dụng). Bằng cách này, bạn có thể thực hiện nhiều việc với truy vấn đó mà không cần chạm vào dữ liệu (trong trường hợp này là dữ liệu trong danh sách). Phương thức danh sách lấy truy vấn đã chuẩn bị và thực hiện nó dựa vào nguồn dữ liệu.
Bronek

2
Trên thực tế, tôi đã đọc tất cả các câu trả lời, và câu trả lời của bạn là câu tôi đã bình chọn, bởi vì nó nêu rõ sự khác biệt giữa hai câu hỏi mà không nói cụ thể về LINQ / SQL. Điều cần thiết là phải biết tất cả những điều này TRƯỚC KHI bạn đến với LINQ / SQL. Ngưỡng mộ.
BeemerGuy

Đó là một sự khác biệt quan trọng để giải thích nhưng "kết quả mong đợi" của bạn không thực sự được mong đợi. Bạn đang nói nó giống như một loại gotcha chứ không phải là thiết kế.
Neme

@Neme, vâng Đó là mong đợi của tôi trước khi tôi hiểu cách thức IEnumerablehoạt động, nhưng bây giờ tôi không biết nhiều hơn;)
amd

15

Nếu tất cả những gì bạn muốn làm là liệt kê chúng, hãy sử dụng IEnumerable.

Mặc dù vậy, hãy coi chừng việc thay đổi bộ sưu tập gốc đang được liệt kê là một hoạt động nguy hiểm - trong trường hợp này, bạn sẽ muốn ToList trước tiên. Điều này sẽ tạo ra một thành phần danh sách mới cho mỗi thành phần trong bộ nhớ, liệt kê IEnumerablevà do đó ít hiệu suất hơn nếu bạn chỉ liệt kê một lần - nhưng an toàn hơn và đôi khi các Listphương thức rất tiện dụng (ví dụ như trong truy cập ngẫu nhiên).


1
Tôi không chắc chắn rằng việc tạo một danh sách có nghĩa là hiệu suất thấp hơn.
Steven Sudit

@ Steven: thực sự như thecoop và Chris đã nói, đôi khi có thể cần phải sử dụng Danh sách. Trong trường hợp của tôi, tôi đã kết luận là không. @ Daren: ý của bạn là "điều này sẽ tạo ra một danh sách mới cho mỗi thành phần trong bộ nhớ"? Có lẽ bạn có nghĩa là một "danh sách mục"? :: -).
Axonn

@Axonn vâng, tôi đề cập đến danh sách. đã sửa.
Daren Thomas

@Steven Nếu bạn có kế hoạch lặp lại các yếu tố trong IEnumerable, sau đó tạo danh sách trước (và lặp lại điều đó) có nghĩa là bạn lặp lại các yếu tố hai lần . Vì vậy, trừ khi bạn muốn thực hiện các hoạt động hiệu quả hơn trong danh sách, điều này thực sự có nghĩa là hiệu suất thấp hơn.
Daren Thomas

3
@jerhewet: không bao giờ là một ý tưởng tốt để sửa đổi một chuỗi được lặp đi lặp lại. Những điều tồi tệ sẽ xảy ra. Trừu tượng sẽ bị rò rỉ. Ác quỷ sẽ đột nhập vào không gian của chúng ta và tàn phá. Vì vậy, có, .ToList()giúp đỡ ở đây;)
Daren Thomas

5

Ngoài tất cả các câu trả lời được đăng ở trên, đây là hai xu của tôi. Có nhiều loại khác ngoài Danh sách thực hiện IEnumerable như ICollection, ArrayList, v.v. Vì vậy, nếu chúng ta có IEnumerable là tham số của bất kỳ phương thức nào, chúng ta có thể chuyển bất kỳ loại bộ sưu tập nào cho hàm. Tức là chúng ta có thể có phương pháp để hoạt động trừu tượng chứ không phải thực hiện cụ thể.


1

Có nhiều trường hợp (chẳng hạn như danh sách vô hạn hoặc danh sách rất lớn) trong đó IEnumerable không thể được chuyển đổi thành Danh sách. Các ví dụ rõ ràng nhất là tất cả các số nguyên tố, tất cả người dùng facebook với các chi tiết của họ hoặc tất cả các mục trên ebay.

Sự khác biệt là các đối tượng "Danh sách" được lưu trữ "ngay tại đây và ngay bây giờ", trong khi các đối tượng "IEnumerable" hoạt động "chỉ một lần". Vì vậy, nếu tôi xem qua tất cả các mục trên ebay, mỗi lần một thứ sẽ là thứ mà ngay cả một máy tính nhỏ cũng có thể xử lý, nhưng ".ToList ()" chắc chắn sẽ khiến tôi hết bộ nhớ, cho dù máy tính của tôi có lớn đến đâu. Không có máy tính nào có thể tự chứa và xử lý một lượng dữ liệu khổng lồ như vậy.

[Chỉnh sửa] - Không cần phải nói - không phải "cái này hay cái kia". thông thường sẽ rất hợp lý khi sử dụng cả danh sách và IEnumerable trong cùng một lớp. Không có máy tính nào trên thế giới có thể liệt kê tất cả các số nguyên tố, bởi vì theo định nghĩa, điều này sẽ đòi hỏi một lượng bộ nhớ vô hạn. Nhưng bạn có thể dễ dàng nghĩ ra cái class PrimeContainernào chứa một cái IEnumerable<long> primes, vì lý do rõ ràng cũng chứa a SortedList<long> _primes. tất cả các số nguyên tố tính toán cho đến nay. nguyên tố tiếp theo được kiểm tra sẽ chỉ được chạy theo các số nguyên tố hiện có (tối đa căn bậc hai). Bằng cách đó, bạn có được cả hai - số nguyên tố một lần (IEnumerable) và một danh sách tốt "số nguyên tố cho đến nay", đó là một xấp xỉ khá tốt của toàn bộ danh sách (vô hạ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.