LINQ (Language Integrated Query) là một thư viện phương thức để lọc, biến đổi, sắp xếp, nhóm và tổng hợp các chuỗi phần tử. Thêm using System.Linq; là mọi mảng, List<T>, Dictionary và các IEnumerable<T> khác có thêm những phương thức như Where, Select và OrderBy, mỗi phương thức nhận một lambda mô tả việc cần làm với một phần tử.
Truy vấn đầu tiên: method syntax và query syntax
LINQ có hai cách viết. Method syntax nối các lời gọi; query syntax đọc giống SQL. Trình biên dịch chuyển query syntax thành chính các lời gọi phương thức đó, nên hai cách cho kết quả giống hệt nhau.
Đầu ra:
95, 91, 88
95, 91, 88
Method syntax phổ biến hơn trong code hiện đại, và một số toán tử (Take, Distinct, Count, First, ToList) chỉ có dạng phương thức. Query syntax nổi bật với phép join và với truy vấn cần tạo biến trung gian bằng let. Phần còn lại của trang dùng method syntax và chỉ đưa query syntax vào khi nó có ích.
Where và Select: lọc và chiếu
Where giữ các phần tử khớp một điều kiện. Select biến mỗi phần tử thành một thứ khác. Hai toán tử này đủ cho hầu hết truy vấn hằng ngày.
Đầu ra:
Mug, Kettle
MUG $8.50 | LAMP $32.00 | KETTLE $27.00 | RUG $120.00
Lamp: 38.40
Rug: 144.00
1. Mug 2. Lamp 3. Kettle 4. Rug
Where không bao giờ thay đổi phần tử, còn Select không bao giờ thay đổi số lượng phần tử. new { p.Name, WithTax = ... } tạo một kiểu ẩn danh (anonymous type), một lớp do trình biên dịch viết hộ bạn, có các thuộc tính đọc bằng cú pháp dấu chấm thông thường. Kiểu này không có tên để bạn viết ra, nên biến chứa nó phải khai báo bằng var, và nó hợp với kết quả chỉ dùng trong một phương thức.
OrderBy, ThenBy và sắp xếp theo nhiều khóa
Đầu ra:
Ana 60
Ana 15
Ben 90
Ben 40
Chloe 25
Ben, Ana, Ben, Chloe, Ana
OrderBy là sắp xếp ổn định: các phần tử có khóa bằng nhau giữ nguyên thứ tự ban đầu. Dùng ThenBy hoặc ThenByDescending cho khóa thứ hai. Một OrderBy thứ hai sẽ sắp xếp lại tất cả theo khóa của nó, khiến khóa đầu chỉ còn để phân định khi bằng nhau: thứ tự ưu tiên bị đảo ngược. Trong query syntax, các khóa ngăn cách bằng dấu phẩy: orderby o.Customer, o.Total descending.
Khác với List<T>.Sort, OrderBy giữ nguyên nguồn và trả về một chuỗi mới.
First, Single, Last và các phiên bản OrDefault
Các toán tử này trả về một phần tử thay vì một chuỗi. Phiên bản thường ném ngoại lệ khi không có gì để trả về; phiên bản OrDefault trả về giá trị mặc định của kiểu.
Đầu ra:
Ana
Ben
Bea
True
First: InvalidOperationException
Single: InvalidOperationException
0
Ngoại lệ mà First ném trên chuỗi rỗng là InvalidOperationException với thông báo "Sequence contains no elements" (hoặc "Sequence contains no matching element" khi không phần tử nào khớp điều kiện), một trong những lỗi C# được tìm kiếm nhiều nhất. Dùng FirstOrDefault khi "không có" là kết quả bình thường và kiểm tra null.
Dòng cuối cho thấy cái bẫy với kiểu giá trị: với chuỗi int, FirstOrDefault trả về 0 khi không có gì khớp, và không thể phân biệt nó với một số 0 thật. Hãy lọc bằng Where và kiểm tra Any() trước, hoặc từ .NET 6 trở lên thì truyền giá trị mặc định của riêng bạn: scores.FirstOrDefault(s => s > 100, -1).
Single dành cho trường hợp "đúng một": nó ném ngoại lệ nếu không có hoặc có nhiều hơn một phần tử, nên vừa là truy vấn vừa là một phép khẳng định.
Any, All, Count và Contains
Đầu ra:
True
True
True
3
True
True
Any() dừng ở phần tử khớp đầu tiên, nên items.Any() là cách đúng để hỏi "có gì không?". items.Count() > 0 đọc thuộc tính Count khi nguồn là collection như list hay mảng, nhưng với một truy vấn hoặc chuỗi lazy khác thì nó duyệt hết mọi phần tử trước. All trên chuỗi rỗng trả về true, vì không có phần tử nào vi phạm điều kiện.
Sum, Average, Min và Max
Đầu ra:
Total: 28.25
Items: 7
Priciest: 7.25
Average price: 4.92
0
Average: InvalidOperationException
Sum của chuỗi rỗng là 0, nhưng Average, Min và Max của một chuỗi số rỗng sẽ ném InvalidOperationException: không có câu trả lời hợp lý nào. Hãy chặn trước bằng Any(), hoặc dùng DefaultIfEmpty() trước: empty.DefaultIfEmpty(0).Average().
Distinct, Take và Skip
Đầu ra:
home, shop, cart
Page 1: 1 2 3 4 5 6 7 8 9 10
Page 2: 11 12 13 14 15 16 17 18 19 20
Page 3: 21 22 23
12, 15
Distinct giữ lần xuất hiện đầu tiên của mỗi giá trị, theo đúng thứ tự. Skip(n).Take(m) là mẫu phân trang chuẩn, còn TakeWhile/SkipWhile dừng hoặc bắt đầu ở phần tử đầu tiên không thỏa điều kiện. Enumerable.Range(start, count) sinh một chuỗi số nguyên mà không cần mảng.
GroupBy
GroupBy chia phần tử vào các nhóm theo một khóa. Mỗi nhóm có một Key và bản thân là một chuỗi, nên bạn có thể tổng hợp trên nó.
Đầu ra:
North: 3 sales, total 600
South: 2 sales, total 400
East: 1 sales, total 350
Top rep: Ana 400
North -> Ana/Chloe/Ana; South -> Ben/Eli; East -> Dev
Các nhóm xuất hiện theo thứ tự khóa của chúng xuất hiện lần đầu trong nguồn (North, South, East), không được sắp xếp; thêm OrderBy(g => g.Key) nếu bạn cần. Trong query syntax, group X by K chọn thứ được đưa vào mỗi nhóm (ở đây chỉ là tên nhân viên) và into g đặt tên cho nhóm để truy vấn có thể tiếp tục.
ToList, ToArray và ToDictionary
Các phương thức To... chạy truy vấn và lưu kết quả.
Đầu ra:
ben@x.com
2
ToDictionary: ArgumentException
ToDictionary ném ArgumentException khi hai phần tử cho ra cùng một khóa, giống Dictionary.Add. Nếu có thể có trùng lặp, hãy nhóm trước (như wordCounts đã làm) hoặc dùng ToLookup, toán tử ánh xạ mỗi khóa sang một chuỗi giá trị.
Thực thi trì hoãn
Where, Select, OrderBy, GroupBy và hầu hết toán tử khác trả về một đối tượng truy vấn và không làm gì cho đến khi có thứ duyệt nó. Điều này dẫn đến hai hệ quả đáng xem:
Đầu ra:
25, 40, 99
converting 10
converting 25
converting 40
converting 99
Count: 4
converting 10
converting 25
converting 40
converting 99
Max: 9900
converting 10
converting 25
converting 40
converting 99
Cached: 4, 9900
Truy vấn đầu tiên thấy 99 dù số này được thêm vào sau khi truy vấn được viết: bộ lọc chạy khi string.Join duyệt nó. Truy vấn thứ hai in các lần chuyển đổi hai lần vì Count() và Max() mỗi cái duyệt lại từ đầu. Với một lambda rẻ thì đó chỉ là công sức lãng phí; với truy vấn cơ sở dữ liệu, đọc tệp hoặc số ngẫu nhiên bên trong lambda, hai lần duyệt có thể trả về dữ liệu khác nhau. Khi bạn dùng một kết quả nhiều lần, hãy hiện thực hóa nó bằng ToList() hoặc ToArray().
Các toán tử trả về một giá trị duy nhất (Count, Sum, First, Any, Max) và các phương thức To... chạy ngay lập tức. Xem IEnumerable và yield để biết sự lười biếng này được xây dựng thế nào.
Tra cứu nhanh
| Toán tử | Làm gì | Trả về |
|---|---|---|
Where(x => cond) | Lọc | Chuỗi (trì hoãn) |
Select(x => expr) | Biến đổi từng phần tử | Chuỗi (trì hoãn) |
SelectMany(x => seq) | Làm phẳng chuỗi lồng nhau | Chuỗi (trì hoãn) |
OrderBy, ThenBy (+Descending) | Sắp xếp ổn định | Chuỗi (trì hoãn) |
GroupBy(x => key) | Chia nhóm theo khóa | Chuỗi các nhóm (trì hoãn) |
Distinct, Take, Skip | Bỏ trùng, phân trang | Chuỗi (trì hoãn) |
First, Single, Last | Một phần tử, ném lỗi nếu không có | Phần tử |
FirstOrDefault v.v. | Một phần tử hoặc giá trị mặc định | Phần tử |
Any, All, Contains | Kiểm tra | bool |
Count, Sum, Min, Max, Average | Tổng hợp | Số |
ToList, ToArray, ToDictionary | Chạy và lưu | Collection |
Lỗi thường gặp
First()trên chuỗi có thể rỗng. Ném "Sequence contains no elements"; hãy dùngFirstOrDefaultvà kiểm tra.- Gọi
OrderByhai lần. Khóa thứ hai trở thành khóa chính; hãy dùngThenBy. - Duyệt một truy vấn nhiều lần. Công việc bị lặp lại; gọi
ToList()một lần. - Tưởng truy vấn chụp lại nguồn. Nó đọc nguồn lúc được duyệt, không phải lúc được viết.
Average,Min,Maxtrên chuỗi rỗng. Chúng ném ngoại lệ; hãy chặn bằngAny()hoặcDefaultIfEmpty.- Dùng
Count() > 0để kiểm tra có phần tử.Any()dừng ngay ở phần tử đầu tiên.
Câu hỏi thường gặp
LINQ trong C# là gì?
LINQ (Language Integrated Query) là tập hợp các extension method trong System.Linq, như Where, Select, OrderBy và GroupBy, dùng được với mọi IEnumerable<T>: mảng, list, dictionary và kết quả của các truy vấn khác. C# còn có query syntax (from x in items where ... select ...) mà trình biên dịch dịch thành chính các lời gọi phương thức đó.
Select và Where trong LINQ khác nhau thế nào?
Where lọc: nó giữ lại các phần tử mà lambda trả về true và để nguyên chúng. Select chiếu: nó biến mỗi phần tử thành một thứ khác, như một thuộc tính hay một đối tượng mới, và giữ nguyên số lượng. Hai toán tử thường được nối với nhau: people.Where(p => p.Age >= 18).Select(p => p.Name).
First và FirstOrDefault khác nhau thế nào?
First() trả về phần tử đầu tiên (hoặc phần tử đầu tiên khớp điều kiện) và ném InvalidOperationException ("Sequence contains no elements" hoặc "no matching element") khi không có phần tử nào. FirstOrDefault() thì trả về giá trị mặc định của kiểu: null với lớp và 0 với int, nên hãy kiểm tra kết quả trước khi dùng.
Làm sao sắp xếp theo hai trường bằng LINQ?
Nối ThenBy sau OrderBy: orders.OrderBy(o => o.Customer).ThenByDescending(o => o.Total). Gọi OrderBy hai lần thì không được: lần gọi thứ hai sắp xếp lại toàn bộ chuỗi, nên khóa của nó trở thành khóa chính và khóa đầu chỉ còn dùng để phân định khi bằng nhau, ngược với ý định ban đầu.
GroupBy trong LINQ hoạt động thế nào?
GroupBy(x => key) trả về một nhóm cho mỗi khóa khác nhau. Mỗi nhóm là một IGrouping<TKey, TElement>: có thuộc tính Key và bản thân nó là một chuỗi các phần tử có khóa đó, nên bạn có thể gọi Count(), Sum() hoặc Select trên nó. Các nhóm xuất hiện theo thứ tự khóa của chúng xuất hiện lần đầu.
Thực thi trì hoãn trong LINQ là gì?
Phần lớn toán tử LINQ không chạy khi bạn gọi chúng; chúng trả về một truy vấn sẽ chạy mỗi lần được duyệt (bằng foreach, ToList(), Count(), v.v.). Vì vậy truy vấn thấy được các thay đổi trên nguồn sau khi nó được định nghĩa, và duyệt hai lần thì làm việc hai lần. Gọi ToList() hoặc ToArray() để chạy một lần và giữ kết quả.