CodeGym /Các khóa học /C# SELF /Các thao tác Union, Intersect, Except trong LINQ

Các thao tác Union, Intersect, Except trong LINQ

C# SELF
Mức độ, Bài học
Có sẵn

1. Giới thiệu

Giờ mình chuyển sang một chủ đề rất hay và cực kỳ thực tế: các thao tác tập hợp trong LINQ — Union, Intersect, Except. Mấy cái này giúp bạn làm việc với collection như thể đang học đại số tập hợp (hoặc đếm sticker trong hai hộp trên bàn lập trình viên — cũng gần giống vậy thôi). Nếu bạn chưa từng nghe về đại số tập hợp (hoặc quên rồi), nghe có vẻ hơi đáng sợ. Nhưng thực ra nó đơn giản lắm: tưởng tượng hai túi trái cây. Đại số tập hợp là cách để biết trái cây nào có trong cả hai túi, cái nào chỉ có ở một túi, và cái nào sẽ có nếu gộp hai túi lại. Kiểu như chơi sticker: gom hết lại, tìm cái chung hoặc loại trừ cái này khỏi cái kia. Vậy thôi.

Thực hành: Dùng mấy method này rất tiện khi bạn cần gộp kết quả của hai query khác nhau, tìm phần tử chung hoặc xác định sự khác biệt giữa các collection. Ví dụ:

  • Lập danh sách tất cả sản phẩm xuất hiện trong danh sách bán hoặc mua.
  • Tìm sản phẩm có ở cả hai bên — vị trí chung.
  • Hoặc xác định sản phẩm nào còn trong kho mà chưa ai mua — chắc chắn bị tồn kho rồi!

Bài toán thực tế: Đại số tập hợp gặp ở khắp nơi: lọc user theo subscription, tìm ai từng ở nhiều group, tìm order unique hoặc giao nhau, so sánh kết quả hai query khác nhau với database và còn nhiều nữa.

2. Thao tác Union — gộp collection

Dễ thôi, vào thực hành luôn nhé. Giả sử mình có hai list sản phẩm:

List<string> warehouseProducts = new List<string> { "Sữa", "Bánh mì", "Phô mai", "Trứng" };
List<string> recentlySold = new List<string> { "Bánh mì", "Phô mai", "Salami", "Trà" };

Union làm gì?

Union trả về các phần tử duy nhất xuất hiện ít nhất trong một collection.
Nói cách khác là — gộp lại.

var allProducts = warehouseProducts.Union(recentlySold);

foreach (var product in allProducts)
{
    Console.WriteLine(product);
}
// Sẽ in ra: Sữa, Bánh mì, Phô mai, Trứng, Salami, Trà

Nhìn trực quan thì như này:

Kho Đã bán Union (Gộp)
Sữa Bánh mì Sữa
Bánh mì Phô mai Bánh mì
Phô mai Salami Phô mai
Trứng Trà Trứng
Salami
Trà

Theo đại số tập hợp, Union là phép "hoặc": cho mình tất cả những gì có ở đâu đó. Tưởng tượng hai hộp trà với các loại khác nhau — nếu bạn muốn thử tất cả vị, không quan trọng loại nào trùng — bạn chỉ uống mỗi loại một lần thôi.

Union tự động loại bỏ phần tử trùng (dựa vào implement của Equals và GetHashCode cho kiểu phần tử). Nếu bạn tự làm union cho class của mình (ví dụ Product), nhớ implement đúng hai method này, không thì Union sẽ xử lý kỳ lắm: phần tử giống nhau mà lại bị coi là khác!

Đừng quên: thứ tự phần tử trong collection kết quả sẽ giữ thứ tự từ collection đầu, còn phần tử mới — thêm vào cuối (theo thứ tự xuất hiện lần đầu ở collection thứ hai).

Ví dụ với object

Tiếp tục app "Cửa hàng". Có hai list Product:

public class Product
{
    public string Name { get; set; }
    public string Category { get; set; }

    // Để Union/Intersect/Except chạy đúng cần implement chuẩn Equals và GetHashCode!
    public override bool Equals(object? obj) =>
        obj is Product other && Name == other.Name && Category == other.Category;

    public override int GetHashCode() => HashCode.Combine(Name, Category);
}

List<Product> stock = new()
{
    new Product { Name = "Sữa", Category = "Sữa" },
    new Product { Name = "Bánh mì", Category = "Bánh mì" },
};

List<Product> sold = new()
{
    new Product { Name = "Bánh mì", Category = "Bánh mì" },
    new Product { Name = "Salami", Category = "Xúc xích" },
};

var all = stock.Union(sold);

// Lưu ý: phần tử không bị trùng,
// dù "nhìn giống" nhưng phải bằng nhau theo logic.

foreach (var p in all)
    Console.WriteLine($"{p.Name} ({p.Category})");

Lỗi phổ biến: Nếu không override Equals/GetHashCode, Union sẽ coi các instance khác nhau nhưng giá trị giống nhau là phần tử khác!

3. Thao tác Intersect — giao nhau collection

Intersect trả về các phần tử xuất hiện ở tất cả collection gốc. Kiểu như phép "và" trong đại số tập hợp.

Ví dụ

Nhớ lại list ở ví dụ trước:

var commonProducts = warehouseProducts.Intersect(recentlySold);
foreach (var product in commonProducts)
{
    Console.WriteLine(product);
}
// Sẽ in ra: Bánh mì, Phô mai

Trực quan:

Kho Đã bán Intersect (Chung)
Bánh mì Bánh mì Bánh mì
Phô mai Phô mai Phô mai

Dùng khi nào?

  • Bạn muốn biết sản phẩm nào trong kho đã được bán hôm nay.
  • Phỏng vấn hay hỏi: "Làm sao tìm giao nhau hai list?" — LINQ chỉ cần một dòng!
  • Trong app business, giao nhau thường dùng để lọc theo nhiều tiêu chí cùng lúc.

Lưu ý và lỗi phổ biến

Nếu phần tử xuất hiện nhiều lần trong collection, kết quả chỉ có một instance của phần tử đó.
Với object phức tạp (như class Product) lại phải implement đúng Equals/GetHashCode.

Ví dụ với object

var commonObjects = stock.Intersect(sold);
foreach (var p in commonObjects)
    Console.WriteLine($"{p.Name} ({p.Category})");
// Sẽ in ra chỉ Bánh mì (Bánh mì)

4. Thao tác Except — trừ collection

Except trả về phần tử chỉ có trong collection đầu, không có trong collection thứ hai.

Ví dụ

var productsOnlyInStock = warehouseProducts.Except(recentlySold);
foreach (var product in productsOnlyInStock)
{
    Console.WriteLine(product);
}
// Sẽ in ra: Sữa, Trứng

Tức là, đây là sản phẩm còn trong kho, nhưng chưa bán.

Trực quan:

Kho Đã bán Except (chỉ kho)
Sữa Sữa
Trứng Trứng
Bánh mì Bánh mì (bỏ qua)
Phô mai Phô mai (bỏ qua)

So sánh vui

Kiểu như bạn loại khỏi xấp giấy của mình tất cả giấy đã gửi đi — chỉ còn lại cái gì vẫn còn ở bạn và chưa ở đâu khác.

Ví dụ với object

var unsold = stock.Except(sold);
foreach (var p in unsold)
    Console.WriteLine($"{p.Name} ({p.Category})");
// Sẽ in ra: Sữa (Sữa)

! Những điểm không rõ ràng

Method Except nhạy với thứ tự: A.Except(B) — không giống B.Except(A) đâu nhé! Collection đầu là "bị trừ", collection sau là "trừ đi".

5. Kết hợp và phối hợp nhiều thao tác LINQ

Có lúc một thao tác không đủ. Ví dụ, bạn muốn in ra sản phẩm chỉ có ở kho hoặc chỉ có ở đã bán — nhưng không có ở cả hai cùng lúc (gọi là "khác biệt đối xứng").

Khác biệt đối xứng ("XOR" cho tập hợp):

var onlyInOne = warehouseProducts.Except(recentlySold)
                   .Union(recentlySold.Except(warehouseProducts));
foreach (var product in onlyInOne)
{
    Console.WriteLine(product);
}
// Sẽ in ra: Sữa, Trứng, Salami, Trà

Logic phức tạp thì phối hợp LINQ-method rất tiện:

// Tìm sản phẩm không có trong kho, không có trong đã bán, chỉ có trong list "dự kiến nhập"
List<string> expected = new() { "Cà phê", "Trà", "Sữa" };
var onlyExpected = expected.Except(warehouseProducts.Union(recentlySold));
foreach (var product in onlyExpected)
    Console.WriteLine(product);
// Sẽ in ra: Cà phê

6. Làm việc với kiểu custom và IEqualityComparer

Đôi khi bạn không muốn so sánh object theo tất cả field: ví dụ chỉ quan tâm tên sản phẩm, còn category thì không. LINQ-method hỗ trợ thêm tham số — IEqualityComparer<T>, cho phép bạn định nghĩa cách so sánh phần tử.

Ví dụ custom comparer:

class ProductNameComparer : IEqualityComparer<Product>
{
    public bool Equals(Product? x, Product? y) => x?.Name == y?.Name;
    public int GetHashCode(Product obj) => obj.Name.GetHashCode();
}

var comp = new ProductNameComparer();
var uniqueByName = stock.Union(sold, comp);
foreach (var p in uniqueByName)
    Console.WriteLine(p.Name); // Sữa, Bánh mì, Salami

Cực kỳ tiện nếu bạn không muốn sửa Equals/GetHashCode cho cả model, mà chỉ muốn so sánh object theo luật riêng một lần.

7. Sơ đồ trực quan và bảng

Tóm tắt ứng dụng (cho list A và B):

Thao tác Kết quả
A.Union(B)
Tất cả phần tử có trong A hoặc B (phần tử duy nhất).
A.Intersect(B)
Tất cả phần tử có ở cả (A và B).
A.Except(B)
Tất cả phần tử chỉ có trong A, không có trong B.

Sơ đồ (Venn Diagram, nếu mình vẽ được):


      [A]        [B]
      oooooooooo
      oooooooo oooo
      ooooo oo   ooo
      ooo    ooo  oo
      oo      o  ooo
      ooo    ooo ooo
  • Union: tất cả phần tử trong cả hai vòng tròn.
  • Intersect: chỉ phần giao nhau (ở giữa).
  • Except: chỉ phần trong vòng A, không tính phần giao với B.

8. Lỗi phổ biến khi dùng Union, Intersect và Except

Lỗi #1: dùng với object mà không có Equals và GetHashCode.
Nếu class của bạn không override hai method này, các method Union, Intersect và Except sẽ chạy sai: object giống nhau về nội dung vẫn bị coi là khác. Kết quả là bạn sẽ nhận được kết quả bất ngờ (và thường là vô dụng).

Lỗi #2: so sánh object theo một phần field mà không dùng IEqualityComparer.
Ví dụ, bạn chỉ muốn so sánh sản phẩm theo tên, không phải toàn bộ structure, Intersect sẽ không hiểu đâu. Không có IEqualityComparer rõ ràng thì kết quả sẽ không như mong đợi.

Lỗi #3: kỳ vọng sai về thứ tự phần tử.
Nhiều bạn nghĩ collection kết quả sẽ giữ thứ tự gộp hoặc giao nhau. Nhưng thực tế tuỳ method: Union giữ thứ tự collection đầu, còn Intersect và Except có thể trả về phần tử theo thứ tự không đoán trước được. Tốt nhất đừng dựa vào thứ tự.

Lỗi #4: bỏ qua hiệu năng khi làm với collection lớn.
Nếu dữ liệu nhiều, các method này có thể chạy chậm. Hãy nghĩ đến việc gom nhóm, lọc trước hoặc dùng cấu trúc hash (ví dụ HashSet) để tăng tốc.

2
Nhiệm vụ
C# SELF, mức độ, bài học
Đã khóa
Sử dụng Intersect để tìm các phần tử chung
Sử dụng Intersect để tìm các phần tử chung
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION