1. Giới thiệu
Hãy tưởng tượng bạn là chủ một tiệm tạp hóa nhỏ, và bạn có một danh sách hàng hóa. Mỗi món đều có mã hàng riêng biệt (ví dụ ART-001, ART-002) và tất nhiên là tên, giá, số lượng trong kho.
Nếu bạn lưu hết mấy thứ này trong List<T>, với T là class Product tự chế chẳng hạn, thì để tìm món có mã ART-005, bạn phải duyệt từng phần tử:
"Đây là ART-001? Không. Đây là ART-002? Không... À đây rồi ART-005! Tìm thấy!"
Nếu chỉ có 10 món thì không sao. Nhưng nếu có 10 000 món? Hay 100 000? Tìm từng món sẽ tốn thời gian cực kỳ luôn. Khách hàng của bạn sẽ phải chờ dài cổ chỉ để mua gói bánh quy yêu thích. Không ổn tí nào!
Chúng ta cần một cách để nhảy thẳng đến món cần tìm nếu biết mã hàng, không phải duyệt hết. Tức là cần một "khóa" chỉ thẳng đến "giá trị" mong muốn.
Làm quen với Dictionary
Và đây, Dictionary<TKey, TValue> xuất hiện! Hãy tưởng tượng nó không chỉ là một list, mà là cuốn danh bạ điện thoại siêu thông minh. Trong danh bạ, bạn tìm số điện thoại (giá trị) theo tên người (khóa). Bạn mở mục "A", tìm "Alexey", và có ngay số của anh ấy. Không cần lật từng số một.
Y chang vậy, Dictionary (dịch ra là "Từ điển") lưu dữ liệu dưới dạng cặp "khóa-giá trị".
- Khóa (TKey): Đây là định danh duy nhất cho mỗi phần tử. Nó giống như tên trong danh bạ hoặc mã hàng hóa. Bạn sẽ dùng khóa này để tìm giá trị. Khóa phải duy nhất trong dictionary. Nếu bạn thử thêm phần tử với khóa đã có, Dictionary sẽ không cho đâu.
- Giá trị (TValue): Đây là dữ liệu bạn muốn lưu. Có thể là số điện thoại, giá sản phẩm, mô tả thuật ngữ — bất cứ thứ gì!
Chữ TKey và TValue trong dấu <TKey, TValue> nghĩa là Dictionary là collection generic. Bạn tự chọn kiểu dữ liệu cho khóa và giá trị. Có thể là string (tên, mã hàng), int (ID user), hoặc class tự chế. Giá trị cũng có thể là int, string, double hoặc object luôn.
Lợi ích? Truy cập siêu nhanh! Nhờ cấu trúc bên trong đặc biệt (hash table, nghe hơi học thuật nhưng bạn chưa cần biết chi tiết đâu), Dictionary cho phép tìm giá trị theo khóa cực nhanh, bất kể có 10 hay 1 triệu phần tử. Nó như siêu chỉ mục trong thư viện khổng lồ: bạn nói "cho mình sách về C#", là chỉ ngay chỗ, khỏi phải lật từng kệ.
Vào việc luôn nhé! Mình sẽ phát triển project nhỏ, tạo "Từ điển thuật ngữ C#" tương tác, giúp bạn nhớ khái niệm mới dễ dàng.
2. Cơ bản về cú pháp: tạo dictionary
Bắt đầu như thường lệ: khai báo biến, nhưng giờ phải chỉ rõ hai kiểu — kiểu khóa (TKey) và kiểu giá trị (TValue):
// Dictionary đơn giản: khóa - string (login), giá trị - string (email)
Dictionary<string, string> userEmails = new Dictionary<string, string>();
// Hoặc ngắn gọn hơn với var
var userEmails = new Dictionary<string, string>();
Tại sao phải chỉ rõ cả hai kiểu?
Vì C# là ngôn ngữ kiểu tĩnh, dictionary cần biết bạn định dùng kiểu gì cho khóa và giá trị.
Thêm phần tử
Để thêm cặp "khóa-giá trị" mới, dùng method Add. Khóa phải là duy nhất nhé!
userEmails.Add("vasya", "vasya@example.com");
userEmails.Add("petya", "petya@gmail.com");
Nếu bạn thử thêm lần nữa với cùng khóa — dictionary sẽ nổi giận và ném exception.
Truy cập giá trị qua khóa
Điều hay nhất ở dictionary là lấy giá trị qua khóa:
string email = userEmails["vasya"];
Console.WriteLine(email); // vasya@example.com
Nếu truy cập bằng khóa không tồn tại, chương trình sẽ la lên (ném exception KeyNotFoundException). Để an toàn, lát nữa mình sẽ chỉ cách kiểm tra khóa trước khi lấy giá trị.
Sửa giá trị theo khóa
Nếu khóa đã có, chỉ cần gán giá trị mới:
userEmails["vasya"] = "vasya@newmail.ru"; // giờ email của Vasya đã đổi
Nếu khóa chưa có — gán như vậy sẽ tạo phần tử mới trong dictionary.
Ví dụ với user
Cùng nâng cấp app học tập của mình chút nhé. Trước đó mình lưu danh sách task (List<string> tasks;) cho app ToDo. Giờ muốn thêm "đăng nhập": mỗi user cần có email.
Ví dụ như này:
// UserId là string, Email cũng string
var users = new Dictionary<string, string>();
users.Add("admin", "admin@myapp.com");
users.Add("alice", "alice@wonderland.com");
users.Add("bob", "bob@builder.com");
Giờ bạn luôn có thể tra nhanh email của bất kỳ user nào qua login:
Console.WriteLine(users["alice"]); // => alice@wonderland.com
3. Các method và property cơ bản của Dictionary
| Method/Property | Mô tả |
|---|---|
|
Thêm cặp "khóa-giá trị" mới. |
|
Xóa phần tử theo khóa. |
|
Kiểm tra có khóa này không. |
|
Kiểm tra có giá trị này không (chậm lắm!). |
|
Lấy giá trị theo khóa an toàn, không ném exception. |
|
Số cặp "khóa-giá trị" trong dictionary. |
|
Collection tất cả khóa. |
|
Collection tất cả giá trị. |
Kiểm tra khóa có tồn tại không
Cách phổ biến (và an toàn) nhất là kiểm tra trước xem có khóa không:
if (users.ContainsKey("dasha"))
{
Console.WriteLine(users["dasha"]);
}
else
{
Console.WriteLine("Người dùng dasha không tìm thấy!");
}
Cách an toàn: TryGetValue
Method TryGetValue giúp tránh exception:
if (users.TryGetValue("bob", out string email))
{
Console.WriteLine($"Email của Bob: {email}");
}
else
{
Console.WriteLine("Bob không tìm thấy!");
}
Đây là best practice và hay bị hỏi khi phỏng vấn — học luôn đi nhé! Ngoài ra, method này còn nhanh hơn combo ContainsKey + truy cập qua index.
4. Duyệt dictionary: vòng lặp foreach
Nếu muốn duyệt tất cả cặp, dùng vòng foreach. Mỗi phần tử là object kiểu KeyValuePair<TKey, TValue>:
foreach (var pair in users)
{
Console.WriteLine($"Login: {pair.Key}, Email: {pair.Value}");
}
Hoặc nếu muốn ngầu hơn:
foreach (var (login, email) in users)
{
Console.WriteLine($"{login}: {email}");
}
// Cú pháp này nhờ destructuring tuple (C# 7+).
5. Xóa và sửa giá trị
Xóa user theo login rất đơn giản:
users.Remove("alice");
Nếu không có khóa đó — trả về false. Cứ xóa thoải mái, không sợ exception.
Sửa email user:
users["bob"] = "bob@constructor.com";
Nếu chưa có khóa đó — sẽ tạo cặp mới luôn!
6. Property tiện lợi Keys và Values
Nếu chỉ cần danh sách login (khóa) hoặc chỉ email (giá trị), dùng collection Keys và Values:
foreach (string login in users.Keys)
{
Console.WriteLine("Login: " + login);
}
foreach (string email in users.Values)
{
Console.WriteLine("Email: " + email);
}
7. Những lưu ý quan trọng về dictionary
Khóa phải duy nhất
Tức là không thể thêm hai khóa giống nhau. Nếu thử — sẽ bị exception. Tính duy nhất này giúp bảo vệ dữ liệu: một user không thể có hai email cùng lúc (mỗi record chỉ một).
Khóa không được là null (với string)
Với khóa kiểu string, nếu thử thêm khóa null sẽ bị lỗi (ArgumentNullException). Nếu bạn không có khóa, hãy nghĩ lại — có thể logic dữ liệu đang có vấn đề.
Tại sao tìm kiếm trong dictionary lại nhanh vậy?
Dictionary bên trong dùng hash table. Nghĩa là tìm theo khóa không phải duyệt từng phần tử, mà là tính toán "hash function" rồi nhảy thẳng đến ô chứa giá trị.
Có thể dùng gì làm khóa?
- Bất kỳ kiểu nào có so sánh bằng và lấy mã duy nhất (method Equals và GetHashCode()).
- Thường là string, int, Guid hoặc kiểu tự chế (nhưng nhớ override Equals/GetHashCode cẩn thận, không là bug vui lắm đấy).
8. Thêm dictionary vào app
Trong mini-app ToDo, mình sẽ thêm dictionary user, và làm chức năng tìm email theo login có xử lý lỗi:
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// Dictionary user: login => email
var users = new Dictionary<string, string>
{
{ "admin", "admin@myapp.com" },
{ "alice", "alice@wonderland.com" },
{ "bob", "bob@builder.com" }
};
Console.WriteLine("Nhập login user để tìm email:");
string login = Console.ReadLine();
// Tìm email an toàn
if (users.TryGetValue(login, out string email))
{
Console.WriteLine($"Email của user {login}: {email}");
}
else
{
Console.WriteLine($"User {login} không tìm thấy.");
}
// Duyệt tất cả user
Console.WriteLine("\nDanh sách tất cả user:");
foreach (var pair in users)
{
Console.WriteLine($"{pair.Key} => {pair.Value}");
}
}
}
9. Lỗi và bẫy newbie hay gặp
Đôi khi bạn sẽ muốn làm như này:
// Nghĩ rằng nếu không có khóa thì sẽ ổn
string value = users["nonexistent"]; // Bùm! KeyNotFoundException!
Nhớ nhé: luôn kiểm tra khóa (ContainsKey hoặc TryGetValue) nếu không chắc chắn nó tồn tại.
Cũng lưu ý: duyệt giá trị không đảm bảo chúng duy nhất! Một email có thể thuộc hai login (nếu bạn không kiểm soát tính duy nhất của giá trị, chỉ của khóa).
Hay nhầm method — ví dụ, thử xóa theo giá trị:
users.Remove("bob@builder.com"); // Không xóa được! Phải truyền khóa, không phải giá trị.
GO TO FULL VERSION