Sự khác biệt giữa .ToList (), .AsEnumerable (), AsQueryable () là gì?


182

Tôi biết một số khác biệt của LINQ với Thực thể và LINQ với Đối tượng mà lần đầu tiên thực hiện IQueryablevà lần thực hiện thứ hai IEnumerablevà phạm vi câu hỏi của tôi nằm trong EF 5.

Câu hỏi của tôi là sự khác biệt kỹ thuật của 3 phương pháp đó là gì? Tôi thấy rằng trong nhiều tình huống tất cả đều hoạt động. Tôi cũng thấy sử dụng kết hợp của họ như thế nào .ToList().AsQueryable().

  1. Những phương pháp đó có nghĩa là gì?

  2. Có bất kỳ vấn đề hiệu suất hoặc một cái gì đó sẽ dẫn đến việc sử dụng cái này hơn cái kia không?

  3. Tại sao một người sẽ sử dụng, ví dụ, .ToList().AsQueryable()thay vì .AsQueryable()?


Câu trả lời:


354

Có rất nhiều điều để nói về điều này. Hãy để tôi tập trung AsEnumerableAsQueryablevà đề cập ToList()trên đường đi.

Những phương pháp này làm gì?

AsEnumerableAsQueryableđúc hoặc chuyển đổi thành IEnumerablehoặc IQueryable, tương ứng. Tôi nói diễn viên hoặc chuyển đổi với một lý do:

  • Khi đối tượng nguồn đã thực hiện giao diện đích, chính đối tượng nguồn được trả về nhưng được truyền tới giao diện đích. Nói cách khác: loại không thay đổi, nhưng loại thời gian biên dịch là.

  • Khi đối tượng nguồn không thực hiện giao diện đích, đối tượng nguồn được chuyển đổi thành đối tượng thực hiện giao diện đích. Vì vậy, cả loại và thời gian biên dịch được thay đổi.

Hãy để tôi chỉ ra điều này với một số ví dụ. Tôi đã có phương pháp nhỏ này báo cáo loại thời gian biên dịch và loại thực tế của một đối tượng ( lịch sự Jon Skeet ):

void ReportTypeProperties<T>(T obj)
{
    Console.WriteLine("Compile-time type: {0}", typeof(T).Name);
    Console.WriteLine("Actual type: {0}", obj.GetType().Name);
}

Chúng ta hãy thử một linq-to-sql tùy ý Table<T>, thực hiện IQueryable:

ReportTypeProperties(context.Observations);
ReportTypeProperties(context.Observations.AsEnumerable());
ReportTypeProperties(context.Observations.AsQueryable());

Kết quả:

Compile-time type: Table`1
Actual type: Table`1

Compile-time type: IEnumerable`1
Actual type: Table`1

Compile-time type: IQueryable`1
Actual type: Table`1

Bạn thấy rằng chính lớp bảng luôn được trả về, nhưng biểu diễn của nó thay đổi.

Bây giờ một đối tượng thực hiện IEnumerable, không phải IQueryable:

var ints = new[] { 1, 2 };
ReportTypeProperties(ints);
ReportTypeProperties(ints.AsEnumerable());
ReportTypeProperties(ints.AsQueryable());

Kết quả:

Compile-time type: Int32[]
Actual type: Int32[]

Compile-time type: IEnumerable`1
Actual type: Int32[]

Compile-time type: IQueryable`1
Actual type: EnumerableQuery`1

Nó đây rồi AsQueryable()đã chuyển đổi mảng thành một EnumerableQuery, trong đó "đại diện cho một IEnumerable<T>bộ sưu tập dưới dạng IQueryable<T>nguồn dữ liệu." (MSDN).

Công dụng là gì?

AsEnumerablethường được sử dụng để chuyển từ bất kỳ IQueryabletriển khai nào sang LINQ sang các đối tượng (L2O), chủ yếu là do trước đây không hỗ trợ các chức năng mà L2O có. Để biết thêm chi tiết, xem Hiệu ứng của AsEnumerable () trên Thực thể LINQ là gì? .

Ví dụ: trong truy vấn Entity Framework, chúng ta chỉ có thể sử dụng một số phương thức bị hạn chế. Vì vậy, nếu, ví dụ, chúng ta cần sử dụng một trong các phương thức của riêng mình trong một truy vấn, chúng ta thường sẽ viết một cái gì đó như

var query = context.Observations.Select(o => o.Id)
                   .AsEnumerable().Select(x => MySuperSmartMethod(x))

ToList - mà chuyển đổi một IEnumerable<T>thành List<T>- thường được sử dụng cho mục đích này. Ưu điểm của việc sử dụng AsEnumerablevs. ToListAsEnumerablekhông thực hiện truy vấn. AsEnumerablebảo tồn thực thi hoãn lại và không xây dựng một danh sách trung gian thường vô dụng.

Mặt khác, khi thực hiện bắt buộc một truy vấn LINQ là mong muốn, ToListcó thể là một cách để làm điều đó.

AsQueryablecó thể được sử dụng để tạo một bộ sưu tập có thể chấp nhận các biểu thức trong các câu lệnh LINQ. Xem ở đây để biết thêm chi tiết: Tôi có thực sự cần sử dụng AsQueryable () trên bộ sưu tập không? .

Lưu ý về lạm dụng chất!

AsEnumerablehoạt động như một loại thuốc Đó là một sửa chữa nhanh chóng, nhưng với chi phí và nó không giải quyết được vấn đề tiềm ẩn.

Trong nhiều câu trả lời Stack Overflow, tôi thấy mọi người áp dụng AsEnumerableđể khắc phục mọi vấn đề với các phương thức không được hỗ trợ trong các biểu thức LINQ. Nhưng giá không phải lúc nào cũng rõ ràng. Chẳng hạn, nếu bạn làm điều này:

context.MyLongWideTable // A table with many records and columns
       .Where(x => x.Type == "type")
       .Select(x => new { x.Name, x.CreateDate })

... mọi thứ được dịch gọn gàng thành một câu lệnh SQL lọc ( Where) và dự án ( Select). Nghĩa là, cả chiều dài và chiều rộng tương ứng của tập kết quả SQL đều bị giảm.

Bây giờ giả sử người dùng chỉ muốn xem phần ngày của CreateDate. Trong Entity Framework, bạn sẽ nhanh chóng khám phá ra rằng ...

.Select(x => new { x.Name, x.CreateDate.Date })

... không được hỗ trợ (tại thời điểm viết). Ah, may mắn thay, có AsEnumerablesửa chữa:

context.MyLongWideTable.AsEnumerable()
       .Where(x => x.Type == "type")
       .Select(x => new { x.Name, x.CreateDate.Date })

Chắc chắn, nó chạy, có lẽ. Nhưng nó kéo toàn bộ bảng vào bộ nhớ và sau đó áp dụng bộ lọc và các phép chiếu. Chà, hầu hết mọi người đều đủ thông minh để làm việc Wheređầu tiên:

context.MyLongWideTable
       .Where(x => x.Type == "type").AsEnumerable()
       .Select(x => new { x.Name, x.CreateDate.Date })

Nhưng tất cả các cột vẫn được tìm nạp trước và phép chiếu được thực hiện trong bộ nhớ.

Các sửa chữa thực sự là:

context.MyLongWideTable
       .Where(x => x.Type == "type")
       .Select(x => new { x.Name, DbFunctions.TruncateTime(x.CreateDate) })

(Nhưng điều đó đòi hỏi thêm một chút kiến ​​thức ...)

Những phương pháp này KHÔNG làm gì?

Khôi phục các khả năng của IQueryable

Bây giờ một cảnh báo quan trọng. Khi bạn làm

context.Observations.AsEnumerable()
                    .AsQueryable()

bạn sẽ kết thúc với đối tượng nguồn được biểu diễn dưới dạng IQueryable. (Bởi vì cả hai phương pháp chỉ truyền và không chuyển đổi).

Nhưng khi bạn làm

context.Observations.AsEnumerable().Select(x => x)
                    .AsQueryable()

kết quả sẽ ra sao?

Việc Selectsản xuất a WhereSelectEnumerableIterator. Đây là một lớp .Net nội bộ thực hiện IEnumerable, không phảiIQueryable . Vì vậy, một chuyển đổi sang loại khác đã diễn ra và sau đó AsQueryablekhông bao giờ có thể trả lại nguồn ban đầu nữa.

Hàm ý của việc này là sử dụng AsQueryablekhông phải là một cách để kỳ diệu bơm một nhà cung cấp truy vấn với cụ thể của nó tính năng vào một đếm được. Giả sử bạn làm

var query = context.Observations.Select(o => o.Id)
                   .AsEnumerable().Select(x => x.ToString())
                   .AsQueryable()
                   .Where(...)

Điều kiện nơi sẽ không bao giờ được dịch sang SQL. AsEnumerable()theo sau là các câu lệnh LINQ dứt khoát cắt kết nối với nhà cung cấp truy vấn khung thực thể.

Tôi cố tình đưa ra ví dụ này bởi vì tôi đã thấy các câu hỏi ở đây, ví dụ như mọi người cố gắng 'đưa' các Includekhả năng vào một bộ sưu tập bằng cách gọi AsQueryable. Nó biên dịch và chạy, nhưng nó không làm gì cả vì đối tượng cơ bản không còn Includethực hiện nữa.

Hành hình

Cả hai AsQueryableAsEnumerablekhông thực thi (hoặc liệt kê ) đối tượng nguồn. Họ chỉ thay đổi loại hoặc đại diện của họ. Cả hai giao diện liên quan, IQueryableIEnumerable, không có gì ngoài "một bảng liệt kê đang chờ xảy ra". Họ không được thực hiện trước khi họ bị buộc phải làm như vậy, ví dụ, như đã đề cập ở trên, bằng cách gọi ToList().

Điều đó có nghĩa là thực thi một IEnumerablethu được bằng cách gọi AsEnumerabletrên một IQueryableđối tượng, sẽ thực hiện bên dưới IQueryable. Một thực hiện tiếp theo của di IEnumerablechúc một lần nữa thực hiện IQueryable. Mà có thể rất tốn kém.

Triển khai cụ thể

Cho đến nay, đây chỉ là về các phương pháp Queryable.AsQueryableEnumerable.AsEnumerablemở rộng. Nhưng tất nhiên ai cũng có thể viết phương thức cá thể hoặc phương thức mở rộng có cùng tên (và hàm).

Trong thực tế, một ví dụ phổ biến của một AsEnumerablephương pháp mở rộng cụ thể là DataTableExtensions.AsEnumerable. DataTablekhông triển khai IQueryablehoặc IEnumerable, vì vậy các phương thức mở rộng thông thường không áp dụng.


Cảm ơn câu trả lời, bạn có thể vui lòng chia sẻ câu trả lời của mình cho câu hỏi thứ 3 của OP - 3. Tại sao một người sẽ sử dụng, ví dụ: .ToList (). AsQueryable () thay vì .AsQueryable ()? ?
kgzdev

@ikram Tôi không thể nghĩ ra bất cứ điều gì có ích. Như tôi đã giải thích, áp dụng AsQueryable()thường dựa trên quan niệm sai lầm. Nhưng tôi sẽ để nhỏ lửa sau gáy một lúc và xem liệu tôi có thể thêm một số phạm vi bảo hiểm cho câu hỏi đó không.
Gert Arnold

1
Câu trả lời chính xác. Bạn có thể vui lòng chính xác điều gì sẽ xảy ra nếu IEnumerable có được bằng cách gọi AsEnumerable () trên IQueryable được liệt kê nhiều lần không? Truy vấn sẽ được thực hiện nhiều lần, hoặc dữ liệu đã được tải từ cơ sở dữ liệu vào bộ nhớ sẽ được sử dụng lại?
antoninod

@antoninod Ý kiến ​​hay. Làm xong.
Gert Arnold

46

Liệt kê()

  • Thực hiện truy vấn ngay lập tức

Vô số ()

  • lười biếng (thực hiện truy vấn sau)
  • Tham số: Func<TSource, bool>
  • Tải MỌI bản ghi vào bộ nhớ ứng dụng, rồi xử lý / lọc chúng. (ví dụ: Where / Take / Skip, nó sẽ chọn * từ bảng1, vào bộ nhớ, sau đó chọn các phần tử X đầu tiên) (Trong trường hợp này, nó đã làm gì: Linq-to-SQL + Linq-to-Object)

Truy vấn ()

  • lười biếng (thực hiện truy vấn sau)
  • Tham số: Expression<Func<TSource, bool>>
  • Chuyển đổi Biểu thức thành T-SQL (với nhà cung cấp cụ thể), truy vấn từ xa và tải kết quả vào bộ nhớ ứng dụng của bạn.
  • Đó là lý do tại sao DbSet (trong Entity Framework) cũng kế thừa IQueryable để có được truy vấn hiệu quả.
  • Không tải mọi bản ghi, ví dụ: Nếu Take (5), nó sẽ tạo ra 5 * SQL hàng đầu được chọn trong nền. Điều này có nghĩa là loại này thân thiện hơn với Cơ sở dữ liệu SQL và đó là lý do tại sao loại này thường có hiệu suất cao hơn và được khuyên dùng khi làm việc với cơ sở dữ liệu.
  • Vì vậy, AsQueryable()thường hoạt động nhanh hơn nhiều so với AsEnumerable()lúc đầu nó tạo T-SQL, bao gồm tất cả các điều kiện nơi bạn ở trong Linq.

14

ToList () sẽ là mọi thứ trong bộ nhớ và sau đó bạn sẽ làm việc với nó. vì vậy, ToList (). trong đó (áp dụng một số bộ lọc) được thực thi cục bộ. AsQueryable () sẽ thực thi mọi thứ từ xa, tức là một bộ lọc trên nó được gửi đến cơ sở dữ liệu để áp dụng. Truy vấn không làm bất cứ điều gì cho đến khi bạn thực hiện nó. ToList, tuy nhiên thực thi ngay lập tức.

Ngoài ra, hãy xem câu trả lời này Tại sao nên sử dụng AsQueryable () thay vì List ()? .

EDIT: Ngoài ra, trong trường hợp của bạn một khi bạn thực hiện ToList () thì mọi hoạt động tiếp theo là cục bộ bao gồm AsQueryable (). Bạn không thể chuyển sang điều khiển từ xa khi bạn bắt đầu thực thi cục bộ. Hy vọng điều này làm cho nó rõ ràng hơn một chút.


2
"AsQueryable () sẽ thực thi mọi thứ từ xa" Chỉ khi có thể truy vấn được. Mặt khác, điều đó là không thể, và mọi thứ vẫn chạy cục bộ. Câu hỏi có ".... ToList (). AsQueryable ()", có thể sử dụng một số làm rõ trong câu trả lời của bạn, IMO.

2

Đã gặp một hiệu suất xấu trên mã dưới đây.

void DoSomething<T>(IEnumerable<T> objects){
    var single = objects.First(); //load everything into memory before .First()
    ...
}

Đã sửa với

void DoSomething<T>(IEnumerable<T> objects){
    T single;
    if (objects is IQueryable<T>)
        single = objects.AsQueryable().First(); // SELECT TOP (1) ... is used
    else
        single = objects.First();

}

Đối với một IQueryable, hãy ở trong IQueryable khi có thể, thử không được sử dụng như IEnumerable.

Cập nhật . Nó có thể được đơn giản hóa hơn nữa trong một biểu thức, cảm ơn Gert Arnold .

T single =  objects is IQueryable<T> q? 
                    q.First(): 
                    objects.First();
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.