1. Giới thiệu
Nói thật nhé, mảng là thứ tuyệt vời, nhưng nó có giới hạn. Muốn đổi kích thước? Phải tạo mảng mới rồi tự tay copy từng phần tử! Muốn tìm nhanh phần tử theo key? Mảng sẽ bảo: "Thử tự làm xem nào".
Cuộc sống thực tế đầy những tình huống mà mảng rất bất tiện. Hãy tưởng tượng bạn đang viết app quản lý sách cho thư viện (hello, mini-system học tập của chúng ta!). Ban đầu bạn có 5 quyển sách — dùng mảng là đủ. Một tuần sau đã có 500 quyển, rồi lại có người trả sách, có người làm mất... Lúc này, collection xuất hiện cứu nguy!
Collection là những container biết:
- Tự động thay đổi kích thước.
- Cho phép tìm kiếm, thêm, xóa nhanh chóng.
- Cung cấp cách xử lý dữ liệu tiện hơn: sort, filter, xử lý nhóm.
Trong .NET có vài "gia đình" collection chính. Hôm nay mình sẽ giới thiệu các loại cơ bản: danh sách (List<T>), dictionary (Dictionary<TKey, TValue>), set (HashSet<T>), queue, stack, và cả các interface nền tảng của chúng.
2. Các loại collection chính
Dù chưa đi sâu vào từng loại collection generic, bạn cần hiểu có những loại collection chính nào và chúng hợp với bài toán nào. Hãy nghĩ về chúng như các loại "container" khác nhau, mỗi loại có đặc điểm riêng:
Danh sách (Lists)
Là gì? Một dãy phần tử có thứ tự. Giống như danh sách mua sắm hoặc danh sách sinh viên trong sổ điểm.
Đặc điểm chính: Phần tử có thứ tự (lấy theo index như mảng). Có thể chứa phần tử trùng lặp. Kích thước động.
Khi nào dùng? Khi thứ tự phần tử quan trọng, và bạn muốn thêm/xóa phần tử ở cuối nhanh, hoặc lấy phần tử theo vị trí.
Ví dụ thực tế: Hàng chờ ở quầy thanh toán, danh sách thành viên webinar, chuỗi frame trong animation.
Dictionary/Map (Sổ từ điển/Ánh xạ)
Là gì? Bộ các cặp "key-value". Giống như từ điển thật, mỗi từ (key) có một nghĩa (value).
Đặc điểm chính: Mỗi key là duy nhất. Key dùng để tìm value rất nhanh. Thứ tự phần tử thường không đảm bảo.
Khi nào dùng? Khi bạn cần tìm value theo một định danh duy nhất (key) thật nhanh.
Ví dụ thực tế: Sổ liên lạc (tên - số điện thoại), database sản phẩm (ID - mô tả), cài đặt app (tên setting - giá trị).
Set (Tập hợp)
Là gì? Bộ các phần tử duy nhất không có thứ tự. Giống như tập hợp trong toán học.
Đặc điểm chính: Không chứa phần tử trùng lặp. Thứ tự không đảm bảo. Tối ưu cho kiểm tra tồn tại và các phép toán tập hợp (union, intersection).
Khi nào dùng? Khi bạn cần lưu giá trị duy nhất và kiểm tra phần tử có trong tập không.
Ví dụ thực tế: Danh sách user duy nhất truy cập web, tập tag cho bài viết, danh sách từ để gợi ý (không lặp).
Queue (Hàng đợi)
Là gì? Collection hoạt động theo kiểu "vào trước ra trước" (FIFO - First-In, First-Out).
Đặc điểm chính: Thêm phần tử vào cuối, lấy ra từ đầu.
Khi nào dùng? Mô phỏng quy trình cần xử lý theo thứ tự, ví dụ hệ thống ticket support, hàng đợi in.
Stack (Ngăn xếp)
Là gì? Collection hoạt động kiểu "vào sau ra trước" (LIFO - Last-In, First-Out).
Đặc điểm chính: Thêm và lấy phần tử ở cùng một đầu (đỉnh stack).
Khi nào dùng? Theo dõi lịch sử thao tác (undo trong editor), xử lý cấu trúc lồng nhau, recursion.
So sánh các loại collection
| Loại collection | So sánh thực tế | Nguyên lý chính | Có truy cập theo index? | Có thứ tự phần tử? | Cho phép trùng lặp? | Thao tác chính |
|---|---|---|---|---|---|---|
| Mảng | Dãy ô số thứ tự | Kích thước cố định | Có | Có | Có | Lấy/đặt theo index |
| Danh sách | Danh sách mua sắm | Động, có thứ tự | Có | Có | Có | Thêm, xóa, tìm kiếm |
| Dictionary | Từ điển (key-value) | Key duy nhất | Không | Không (thường vậy) | Không (theo key) | Lấy theo key, thêm |
| Set | Tập hợp object duy nhất | Chỉ phần tử duy nhất | Không | Không | Không | Kiểm tra tồn tại, hợp nhất |
| Queue | Hàng chờ ở quầy | FIFO (Vào trước ra trước) | Không | Có | Có | Thêm cuối, lấy đầu |
| Stack | Chồng đĩa | LIFO (Vào sau ra trước) | Không | Có | Có | Thêm trên cùng, lấy trên cùng |
3. Danh sách: List<T>
Danh sách là collection được dùng nhiều nhất trong C#. Thậm chí còn nhiều hơn mảng. Danh sách là collection giống mảng nhưng biết tự mở rộng. Thêm/xóa phần tử không phải đau đầu.
using System;
using System.Collections.Generic;
var numbers = new List<int>(); // Tạo danh sách rỗng kiểu int
numbers.Add(10); // Thêm phần tử
numbers.Add(15);
numbers.Add(42);
Console.WriteLine(numbers[0]); // 10
numbers.Remove(15); // Xóa phần tử theo giá trị
foreach (var number in numbers)
{
Console.WriteLine(number);
}
// Sẽ in ra: 10 và 42
Tại sao List<T> lại ngon hơn mảng khi bạn không biết trước số lượng phần tử?
— Vì bạn không phải tự cấp phát thêm bộ nhớ và copy mảng khi thêm phần tử — collection lo hết!
Khi nào dùng List<T>?
- Danh sách động, cần thêm/xóa/sửa phần tử.
- Không cần tìm kiếm nhanh theo key (cái này để dictionary lo).
- Thường dùng với tập hợp có thứ tự.
4. Dictionary Dictionary<TKey, TValue>
Mảng (và danh sách) cho phép lưu danh sách giá trị trong các ô, mỗi ô có index. Nhưng dictionary là collection cho phép dùng string (tên) thay vì số (index). Tên này gọi là key.
Nếu bạn cần lấy value theo key thật nhanh (ví dụ số thẻ thư viện — tên người đọc), hãy dùng dictionary.
using System.Collections.Generic;
var phoneBook = new Dictionary<string, string>();
phoneBook["Anya"] = "+79992221133";
phoneBook["Maksim"] = "+79998887766";
Console.WriteLine(phoneBook["Anya"]); // +79992221133
// Có thể kiểm tra key tồn tại:
if (phoneBook.ContainsKey("Vasya"))
{
Console.WriteLine(phoneBook["Vasya"]);
}
else
{
Console.WriteLine("Không có số này!");
}
Fun fact: Dictionary còn gọi là "mảng liên kết". Nó dùng hash table nên tìm kiếm theo key cực nhanh (gần như tức thì, trừ khi bị collision — sẽ nói sau).
Đặc điểm chính
- Key là duy nhất: không có 2 key giống nhau trong dictionary.
- Value có thể trùng lặp.
- Tìm, thêm, xóa phần tử theo key cực nhanh.
5. Set: HashSet<T>
Ngoài danh sách và dictionary, set cũng rất phổ biến. Nó gần giống danh sách, nhưng đơn giản hơn: danh sách không có thứ tự cố định. Set chỉ lưu tập giá trị, dùng khi không quan tâm thứ tự.
Ví dụ, bạn chỉ cần biết một giá trị có trong tập không, không cần trùng lặp, không quan tâm thứ tự — dùng set là chuẩn.
using System.Collections.Generic;
var knownUsers = new HashSet<string>();
knownUsers.Add("admin");
knownUsers.Add("guest");
knownUsers.Add("admin"); // Thêm trùng — bị bỏ qua
Console.WriteLine(knownUsers.Contains("admin")); // True
Console.WriteLine(knownUsers.Count); // 2
Set kiểm tra phần tử tồn tại cực nhanh.
Set dùng để làm gì?
— Ví dụ, bạn muốn lưu tất cả user duy nhất mở app trong tháng, hoặc danh sách tác giả sách không trùng.
Đặc điểm
- Chỉ lưu phần tử duy nhất (thêm trùng bị bỏ qua).
- Không có index như danh sách.
- Kiểm tra tồn tại nhanh.
6. Queue và Stack
Còn có các cấu trúc cho trường hợp đặc biệt: queue và stack. Thực ra nó cũng giống danh sách, chỉ khác cách thêm/lấy phần tử.
Queue<T>: hàng đợi (vào trước ra trước)
Đôi khi bạn cần "hàng đợi" — phần tử mới vào cuối, lấy ra từ đầu.
using System.Collections.Generic;
var queue = new Queue<string>();
queue.Enqueue("Đầu tiên");
queue.Enqueue("Thứ hai");
queue.Enqueue("Thứ ba");
Console.WriteLine(queue.Dequeue()); // "Đầu tiên"
Console.WriteLine(queue.Peek()); // "Thứ hai", không xóa
Stack<T>: ngăn xếp (vào trước ra sau)
Stack thì ngược lại: vào sau ra trước. Dùng trong parser, xử lý gọi hàm, undo trong editor...
using System.Collections.Generic;
var stack = new Stack<string>();
stack.Push("Một");
stack.Push("Hai");
stack.Push("Ba");
Console.WriteLine(stack.Pop()); // "Ba"
Console.WriteLine(stack.Peek()); // "Hai"
7. Bảng: so sánh các collection chính
| Collection | Phần tử duy nhất | Truy cập theo index | Tìm nhanh theo key | Thao tác thêm/xóa |
|---|---|---|---|---|
|
Không | Có | Không | Nhanh ở cuối |
|
Key | Không | Có | Nhanh theo key |
|
Có | Không | Có* | Nhanh theo value |
|
Không | Không | Không | Nhanh (FIFO) |
|
Không | Không | Không | Nhanh (LIFO) |
Từng collection sẽ được học kỹ ở các bài sau. Và chúng ta sẽ bắt đầu với ký tự bí ẩn <T>...
GO TO FULL VERSION