1. Giới thiệu
Nếu với IEnumerable<T> bạn chỉ có thể đi dạo qua nội dung đã có của collection, thì interface ICollection<T> — chính là vé vào thế giới quản lý collection: thêm, xóa, kiểm tra số lượng phần tử, copy sang mảng và thậm chí theo dõi thay đổi (kiểu như ngoài đời khi bạn cố kiểm soát giỏ hàng của mình ấy).
ICollection<T> được tất cả các collection có thể thay đổi trong .NET implement, như List<T>, HashSet<T>, Dictionary<TKey, TValue>.ValueCollection và cả mấy loại ít phổ biến hơn như Queue<T>, Stack<T> (ừ, cả queue lẫn stack luôn nhé!). Đây là hợp đồng cơ bản cho mọi class collection cho phép bạn sửa nội dung bên trong.
Gia đình interface collection
graph TD
A[IEnumerable<T>]
B[ICollection<T>]
C[IList<T>]
D[IDictionary<TKey, TValue>]
E[Queue<T>, Stack<T>, List<T>, HashSet<T>...]
A --> B
B --> C
B --> D
B --> E
2. Interface ICollection<T> gồm những gì?
Khác với IEnumerable<T> chỉ lo việc duyệt (foreach), ICollection<T> — như con dao đa năng Thụy Sĩ trong thế giới interface: mỗi việc đều có method riêng!
Dưới đây là những thứ chính của nó:
public interface ICollection<T> : IEnumerable<T>
{
int Count { get; }
bool IsReadOnly { get; }
void Add(T item);
void Clear();
bool Contains(T item);
void CopyTo(T[] array, int arrayIndex);
bool Remove(T item);
}
Danh sách method và property:
- Count — số lượng phần tử trong collection. Giống như hàm đếm yêu thích của bạn.
- IsReadOnly — collection có cho phép sửa không? Nếu true, collection chỉ đọc (ví dụ một số wrapper).
- Add(T item) — thêm phần tử vào collection. Một trong những tool chính của bạn!
- Clear() — xóa sạch mọi phần tử. Dọn dẹp toàn bộ.
- Contains(T item) — kiểm tra nhanh: collection có phần tử này không?
- CopyTo(T[] array, int arrayIndex) — copy phần tử sang mảng thường, bắt đầu từ index chỉ định. Hữu ích khi cần truyền data cho API ngoài hoặc mấy method "cổ lỗ sĩ".
- Remove(T item) — xóa phần tử (nếu có).
3. Bài toán thực tế của lập trình viên
Tại sao phải hiểu interface này hoạt động thế nào? Thực tế bạn hay viết code generic làm việc với mọi loại collection. Ví dụ, một function có thể nhận bất kỳ list, queue hay set nào — không quan trọng kiểu collection cụ thể. Miễn là collection đó implement ICollection<T>, bạn có thể thêm, xóa phần tử, kiểm tra size và copy sang mảng. Đây là nền tảng cho thư viện linh hoạt và thuật toán đa năng.
Ví dụ đời thường: interface ICollection<T> hay xuất hiện trong parameter của method và property API trả về collection để sửa. Ví dụ, trong các ORM nổi tiếng (Entity Framework, Dapper), property kiểu ICollection<T> dùng để mô tả collection các object liên kết.
Ví dụ sử dụng
Thử viết một method generic nhận bất kỳ collection nào và thêm phần tử vào đó. Điều kiện duy nhất — nó phải implement ICollection<T>.
// Method thêm phần tử đa năng
void AppendItem<T>(ICollection<T> collection, T item)
{
collection.Add(item);
}
Giờ bạn có thể dùng method này với cả List<T>, HashSet<T>, thậm chí với implementation riêng của bạn! Ví dụ dùng:
var numbers = new List<int> { 1, 2, 3 };
AppendItem(numbers, 42);
var uniqueNames = new HashSet<string> { "Alice", "Bob" };
AppendItem(uniqueNames, "Charlie");
Thật buồn cười — không quan trọng là list hay set, miễn là ICollection<T>. Đừng ngạc nhiên, các method đa năng trong .NET cũng thường như vậy!
4. Liên kết với các collection khác
ICollection<T> — không chỉ là hợp đồng lý thuyết đẹp, mà còn là nền tảng thực tế cho gần như mọi collection bạn gặp trong .NET.
Để thấy rõ tính đa năng, thử code sau:
void ShowCollectionInfo<T>(ICollection<T> collection)
{
Console.WriteLine($"Số lượng phần tử: {collection.Count}");
Console.WriteLine($"Có thể sửa: {!collection.IsReadOnly}");
Console.WriteLine("Các phần tử:");
foreach (var item in collection)
{
Console.WriteLine(item);
}
}
// Với List<T>
var list = new List<string> { "táo", "cam" };
ShowCollectionInfo(list);
// Với HashSet<T>
var set = new HashSet<string> { "táo", "cam" };
ShowCollectionInfo(set);
Kết quả sẽ đúng cho mọi loại collection implement interface này. Thử với queue, stack, thậm chí mảng được wrap!
5. Collection chỉ đọc
Interface ICollection<T> có property IsReadOnly cho biết collection có thể sửa không. Đừng nhầm với property quen thuộc của mảng nhé! Nếu gặp collection mà IsReadOnly == true, cố thêm hoặc xóa phần tử sẽ ném exception.
Ví dụ:
// Tạo mảng và wrap bằng ReadOnlyCollection
var array = new int[] { 1, 2, 3 };
ICollection<int> readOnly = Array.AsReadOnly(array);
// Thử thêm phần tử
try
{
readOnly.Add(4); // Sẽ ném NotSupportedException
}
catch (NotSupportedException)
{
Console.WriteLine("Không thể thêm phần tử: collection chỉ đọc!");
}
Trường hợp này thường gặp khi bạn nhận collection từ nguồn ngoài và không được phép sửa — nhưng vẫn có thể đọc phần tử và biết số lượng.
6. Bảng so sánh method của các collection phổ biến qua lăng kính ICollection<T>
| Collection | Add() | Remove() | Contains() | Clear() | CopyTo() | IsReadOnly |
|---|---|---|---|---|---|---|
|
Có | Có | Có | Có | Có | Không |
|
Có* | Có | Có | Có | Có | Không |
|
Không/Không | Không/Không | Không/Không | Có/Có | Có/Có | Không |
|
Không | Không | Có | Không | Có | Có |
|
Không | Không | Có | Không | Có | Có/Không* |
Với HashSet<T> method Add trả về true nếu phần tử được thêm, ngược lại false.
7. CopyTo — dùng để làm gì?
Method CopyTo giúp bạn chuyển nhanh nội dung collection sang mảng thường. Hữu ích khi cần trả data cho function API cũ chỉ nhận mảng, hoặc xử lý hàng loạt bằng array.
var contactsArray = new Contact[book.Count];
((ICollection<Contact>)book).CopyTo(contactsArray, 0);
// Giờ có thể dùng contactsArray kiểu truyền thống
8. Ví dụ
Trong ứng dụng console học tập của mình, giả sử bạn bắt đầu làm sổ địa chỉ. Có class Contact, còn sổ địa chỉ có thể dùng bất kỳ collection nào miễn là hỗ trợ thêm, xóa và duyệt.
public class Contact
{
public string Name { get; set; }
public string Phone { get; set; }
}
public class AddressBook
{
private readonly ICollection<Contact> _contacts;
public AddressBook(ICollection<Contact> contacts)
{
_contacts = contacts;
}
public void AddContact(Contact contact)
{
_contacts.Add(contact);
}
public bool RemoveContact(Contact contact)
{
return _contacts.Remove(contact);
}
public int Count => _contacts.Count;
public void PrintAll()
{
foreach (var contact in _contacts)
{
Console.WriteLine($"{contact.Name} — {contact.Phone}");
}
}
}
Giờ sổ địa chỉ của bạn có thể chạy với List<Contact>, HashSet<Contact>, thậm chí sau này là collection tự làm dựa trên database hay file! Ví dụ dùng:
var book = new AddressBook(new List<Contact>());
book.AddContact(new Contact { Name = "Ivan", Phone = "+7-123-456" });
book.AddContact(new Contact { Name = "Maria", Phone = "+7-987-654" });
Console.WriteLine($"Tổng số liên hệ: {book.Count}");
book.PrintAll();
Nếu muốn loại bỏ trùng lặp, chỉ cần truyền vào AddressBook một set thay vì list:
var book = new AddressBook(new HashSet<Contact>());
(yêu cầu Contact phải implement so sánh đúng, nhưng phần này sẽ có bài riêng!)
9. Đặc điểm method và lỗi thường gặp
Lưu ý ngay: không phải collection nào implement ICollection<T> cũng hành xử giống nhau! Ví dụ, cố sửa collection khi IsReadOnly == true sẽ ném exception. Với HashSet<T>, method Add trả về true chỉ khi phần tử thực sự được thêm (trước đó chưa có). Ngoài ra, các collection khác nhau có thể implement method với tốc độ khác nhau — interface không quy định chuyện này.
Một điểm nữa: nếu collection được truy cập bởi nhiều thread, sửa ở thread này và duyệt ở thread khác có thể gây exception. Với đa luồng, nên dùng collection chuyên biệt (ConcurrentBag<T>, ConcurrentQueue<T> v.v. — sẽ nói sau).
Một cái bẫy nữa: nếu bạn copy collection bằng CopyTo, sau đó thay đổi mảng và collection sẽ không liên quan nhau nữa. Mảng là vùng nhớ riêng biệt.
GO TO FULL VERSION