LINQ – Deferred operator và cơ chế thực hiện truy vấn
Cập nhật :16/10/2013
Các toán tử (extension method) trong LINQ được chia thành hai loại theo cách mà chúng được thực thi: deferred và non-deferred. Việc hiểu mà phân biệt được các toán tử này là một vấn đề cần thiết vì bạn có thể nhận được một kết quả ngoài dự đoán khi sử dụng nếu không cẩn thận.
Các toán tử (extension method) trong LINQ được chia thành hai loại theo cách mà chúng được thực thi: deferred và non-deferred. Việc hiểu mà phân biệt được các toán tử này là một vấn đề cần thiết vì bạn có thể nhận được một kết quả ngoài dự đoán khi sử dụng nếu không cẩn thận.
Cơ chế thực hiện truy vấn và các deferred operator
Trước tiên bạn hãy xem một ví dụ đơn giản sau và xem kết quả được xuất ra:
01 |
public static void Main(){ |
02 |
List< string > list= new List< string >(){ |
08 |
IEnumerable< string > items = from n in list |
12 |
foreach (var item in items) |
13 |
Console.WriteLine(item); |
15 |
Console.WriteLine( "---------------" ); |
18 |
foreach (var item in items) |
19 |
Console.WriteLine(item); |
Output:
Ruby
—————
Ruby
Lily
Ví dụ trên dùng để xuất các tên có độ dài bằng 4. Như bạn thấy, mặc dù hoàn toàn không thay đổi gì liên quan đến biến items giữa hai lần xuất kết quả, nhưng kết quả xuất ra của hai lần lại khác nhau khi ta thêm một phần tử vào list.
Từ ví dụ này ta có thể suy ra mỗi lần được sử dụng, câu query sẽ được thực thi lại. Trong LINQ, khi bạn tạo một câu query và gán cho một đối tượng, thì lúc đó câu query này sẽ được lưu lại dưới dạng delegate Func<> trong một static field của lớp hiện tại, trong trường hợp này là Func<string,bool>.
Note: Để dễ thấy bạn có chuyển sang cú pháp viết bằng phương thức, tham số của phương thức Where() có thể là Func<T,bool> hoặc Func<T,int,bool>, với T là kiểu dữ liệu của các phần tử trong collection mà bạn sử dụng phương thức này (generic):
IEnumerable<string> items = list.Where( n => n.Length==4);
Trong LINQ, cơ chế này được gọi là “query plan” và các toán tử hoạt động theo cơ chế này được goi là “deferred operator”. Các toán tử còn lại được gọi là “non-deferred operator”.
Nhận biết các toán tử này như thế, nào? Rất đơn giản, các toán tử deferred có kiểu trả về là IEnumerable<T> hoặc IOrderedEnumerable<T>
Có những trường hợp bạn muốn lấy toàn bộ kết quả của truy vấn, hãy sử dụng toán tử ToList,ToArray hoặc ToDictionary để chuyển chúng về một List<T> hoặc mảng. Bạn không phải lo lắng đến hiệu suất vì nghĩ rằng chúng sẽ thực hiện lặp lại nhiều lần để thực hiện truy vấn và tạo collection. Cơ chế thực hiện bên trong các toán tử sẽ giúp tối ưu mà tôi sẽ bàn đến ngay trong phần sau đây.
Note: Func<> và IEnumerable<T> không phải là cách duy nhất mà LINQ sử dụng để lưu trữ và thực hiện truy vấn. Bạn sẽ được tìm hiểu về IQueryable<T> và lớp này sử dụng Expression Tree để lưu trữ câu truy vấn thay cho Func<>. Vấn đề này sẽ được giới thiệu trong một bài viết khác.
Cơ chế yielding và các toán tử trong LINQ
Chuyển qua một ví dụ khác ta có thể thấy cách mà phương thức Where() này được viết.
01 |
public static void Main(){ |
02 |
string [] numbers={ "1" , "3" , "five" }; |
04 |
IEnumerable< string > items = from n in numbers |
08 |
foreach (var item in items) |
09 |
Console.WriteLine(item); |
13 |
Console.WriteLine( "Runtime exception: " +ex.Message); |
Output:
1
3
Runtime exception: Input string was not in a correct format.
Câu lệnh thực hiện query trong ví dụ trên xảy ra lỗi do chuyển đổi chuỗi “five” thành một số Int32. Theo cách thông thường thì lẽ ra câu lệnh này phải ném ra ngoại lệ trước khi vòng lặp foreach được thực hiện. Tuy nhiên theo kết quả xuất ra, hai giá trị đầu hợp lệ là 1 và 3 vẫn được xuất ra màn hình.
Để làm được điều này, phương thức Where() sử dụng kĩ thuật yielding để trả về lần lượt từng phần tử khi nó được cần đến, thay vì thực hiện và trả về tất cả kết quả một lượt. Điều này giúp cho tốc độ thực hiện truy vấn nhanh hơn và kết quả có được gần như tức thời, đặc biệt hữu ích trong những trường hợp dữ liệu của bạn quá lớn.
Dựa vào đây ta có thể suy ra được nội dung của phương thức Where() tương tự như sau:
1 |
public static IEnumerable<T> Where<T>( this IEnumerable<T> source,Func<T, bool > predicate) |
3 |
foreach (var item in source) |
Để kiểm chứng bạn có thể thực thi ví dụ sau, kết quả xuất ra tương tự như ví dụ trên:
02 |
using System.Collections.Generic; |
05 |
namespace LinqExample{ |
06 |
public static class Program |
08 |
public static void Main(){ |
09 |
string [] numbers={ "1" , "3" , "five" }; |
11 |
IEnumerable< string > items = numbers.Y2Where(n => int .Parse(n) < 5); |
13 |
foreach (var item in items) |
14 |
Console.WriteLine(item); |
18 |
Console.WriteLine( "Runtime exception: " +ex.Message); |
25 |
public static class Y2QueryEx{ |
26 |
public static IEnumerable<T> Y2Where<T>( this IEnumerable<T> source,Func<T, bool > predicate) |
28 |
foreach (var item in source) |
Output:
1
3
Runtime exception: Input string was not in a correct format.
Danh sách query operator theo thứ tự alphabet
By operator:
Operator |
Category |
Deferred
|
Aggregate |
Aggregate |
|
All |
Quantifiers |
|
Any |
Quantifiers |
|
AsEnumerable |
Conversion |
Yes
|
Average |
Aggregate |
|
Cast |
Conversion |
Yes
|
Concat |
Concatenation |
Yes
|
Contains |
Quantifiers |
|
Count |
Aggregate |
|
DefaultIfEmpty |
Element |
Yes
|
Distinct |
Set |
Yes
|
ElementAt |
Element |
|
ElementAtOrDefault |
Element |
Yes
|
Empty |
Generation |
Yes
|
Except |
Set |
Yes
|
First |
Element |
|
FirstORDefault |
Element |
|
GroupBy |
Grouping |
Yes
|
GroupJOin |
Join |
Yes
|
Intersect |
Set |
Yes
|
Join |
Join |
Yes
|
Last |
Element |
|
LastOrDefault |
Element |
|
LongCount |
Aggregate |
|
Max |
Aggregate |
|
Min |
Aggregate |
|
OfType |
Conversion |
Yes
|
OrderBy |
Ordering |
Yes
|
OrderByDescending |
Ordering |
Yes
|
Range |
Generation |
Yes
|
Reverse |
Ordering |
Yes
|
Repeat |
Generation |
Yes
|
Select |
Projection |
Yes
|
SelectMany |
Projection |
Yes
|
SequenceEqual |
Equality |
|
Single |
Element |
|
SingleOrDefault |
Element |
|
Skip |
Partitioning |
Yes
|
SkipWhile |
Partitioning |
Yes
|
Sum |
Aggregate |
|
Take |
Partitioning |
Yes
|
TakeWhile |
Partitioning |
Yes
|
ThenBy |
Ordering |
Yes
|
ThenByDescending |
Ordering |
Yes
|
ToDictionary |
Conversion |
|
ToArray |
Conversion |
|
ToList |
Conversion |
|
ToLookup |
Conversion |
|
Union |
Set |
Yes
|
Where |
Restriction |
Yes
|
By category:
Category |
Operator |
Deferred
|
Aggregate |
Aggregate |
|
Average |
|
Count |
|
LongCount |
|
Max |
|
Min |
|
Sum |
|
Concatenation |
Concat |
Yes
|
Conversion |
AsEnumerable |
Yes
|
Cast |
Yes
|
Conversion |
OfType |
Yes
|
ToDictionary |
|
ToArray |
|
ToList |
|
ToLookup |
|
Element |
DefaultIfEmpty |
Yes
|
ElementAt |
|
ElementAtOrDefault |
Yes
|
First |
|
FirstORDefault |
|
Last |
|
LastOrDefault |
|
Single |
|
SingleOrDefault |
|
Equality |
SequenceEqual |
|
Generation |
Empty |
Yes
|
Range |
Yes
|
Repeat |
Yes
|
Grouping |
GroupBy |
Yes
|
Join |
GroupJOin |
Yes
|
Join |
Yes
|
Ordering |
OrderBy |
Yes
|
OrderByDescending |
Yes
|
Reverse |
Yes
|
ThenBy |
Yes
|
ThenByDescending |
Yes
|
Partitioning |
Skip |
Yes
|
SkipWhile |
Yes
|
Take |
Yes
|
TakeWhile |
Yes
|
Projection |
Select |
Yes
|
SelectMany |
Yes
|
Quantifiers |
All |
|
Any |
|
Contains |
|
Restriction |
Where |
Yes
|
Set |
Distinct |
Yes
|
Except |
Yes
|
Intersect |
Yes
|
Union |
Yes
|
|