Menu

Dictionary trong C#: Add, TryGetValue, duyệt và sắp xếp

Dictionary<TKey, TValue> ánh xạ khóa sang giá trị và tra cứu rất nhanh. Học cách thêm và cập nhật phần tử, đọc an toàn bằng TryGetValue, duyệt các KeyValuePair, đếm số lần xuất hiện, bỏ qua chữ hoa chữ thường ở khóa, và giữ khóa luôn được sắp xếp với SortedDictionary.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Dictionary<TKey, TValue> lưu giá trị theo các khóa duy nhất và tìm một giá trị theo khóa trong thời gian gần như không đổi, dù có bao nhiêu phần tử đi nữa. Nó là phiên bản C# của hash map: danh bạ từ tên sang số điện thoại, bộ nhớ đệm từ ID sang bản ghi, số lần xuất hiện của mỗi từ.

Tạo dictionary và đọc giá trị

Đầu ra:

12
2.50
2
True
False

Hai dạng khởi tạo làm cùng một việc. Dạng ["key"] = value (C# 6) dùng indexer, nên khóa lặp lại sẽ ghi đè; dạng { key, value } gọi Add, nên khóa lặp lại sẽ ném lỗi khi dòng đó chạy.

ContainsKey là một lần tra cứu hash và rất nhanh. ContainsValue phải quét qua mọi phần tử, vì giá trị không được đánh chỉ mục.

Add, indexer và TryAdd

Có ba cách thêm một phần tử, và chúng chỉ khác nhau ở chuyện xảy ra khi khóa đã tồn tại:

Đầu ra:

26
Caught ArgumentException
True
False
31

Add ném lỗi khi trùng khóa là một tính năng: nó làm lộ ra dữ liệu lẽ ra phải duy nhất nhưng lại không. Hãy dùng indexer khi bạn muốn "chèn hoặc cập nhật", và TryAdd (.NET Core 2.0 trở lên) khi giá trị đầu tiên phải được giữ lại.

Khóa không thể là null. Add(null, ...) hoặc dict[null] ném ArgumentNullException. Giá trị có thể là null khi kiểu giá trị cho phép.

KeyNotFoundException và TryGetValue

Đọc một khóa không tồn tại bằng indexer sẽ ném KeyNotFoundException. Đây là lỗi dictionary phổ biến nhất, và cách sửa gần như luôn là TryGetValue.

Đầu ra:

Caught KeyNotFoundException
Found ana@example.com
Missing, value is null: True
no email

TryGetValue tra cứu hash một lần và báo kết quả bằng một bool. Mẫu if (dict.ContainsKey(k)) { var v = dict[k]; } vẫn chạy nhưng tra cứu khóa hai lần. Khi khóa không có, biến out nhận giá trị mặc định của kiểu (null, 0, false).

Từ .NET Core 2.0 trở lên còn có GetValueOrDefault(key, fallback), trả về giá trị dự phòng khi khóa không có: emails.GetValueOrDefault(103, "no email").

Cập nhật và xóa phần tử

Đầu ra:

2
True
False
1
0

cart["milk"] += 1 ném KeyNotFoundException nếu milk chưa có trong dictionary, vì nó đọc trước khi ghi. Remove trả về false thay vì ném lỗi khi khóa không có, nên không cần kiểm tra ContainsKey trước.

Duyệt dictionary: KeyValuePair, Keys và Values

Vòng lặp foreach trên dictionary cho ra các phần tử KeyValuePair<TKey, TValue>, mỗi phần tử có Key và Value.

Đầu ra:

Ana: 88
Ben: 72
Chloe: 95
Ana Ben Chloe 
Total 255
77

pair.Value chỉ đọc, nên muốn cập nhật giá trị thì phải ghi qua indexer. Vòng lặp cuối duyệt một bản sao List<string> của danh sách khóa, cách này luôn an toàn; duyệt trực tiếp scores.Keys trong khi ghi đè giá trị có sẵn được phép từ .NET Core 3.0 trở lên nhưng ném InvalidOperationException trên .NET Framework.

Thêm khóa mới bên trong vòng foreach trên chính dictionary đó ném InvalidOperationException ở mọi phiên bản. Xóa trong lúc duyệt ném lỗi trên .NET Framework và được phép từ .NET Core 3.0. Code phải chạy được ở mọi nơi thì gom các khóa cần xóa trước, rồi xóa chúng sau vòng lặp.

Với C# 7 và .NET Core 2.0 trở lên, KeyValuePair có thể được tách ngay trong phần đầu vòng lặp:

foreach (var (name, score) in scores)
{
    Console.WriteLine($"{name}: {score}");
}

Đếm bằng dictionary

Đếm số lần xuất hiện là cách dùng kinh điển. Đọc số đếm hiện tại bằng TryGetValue (khóa không có sẽ cho 0), rồi ghi lại.

Đầu ra:

the 3
cat 1
and 2
dog 1
bird 1

Cùng cấu trúc đó dùng để nhóm phần tử: Dictionary<string, List<Order>>, trong đó bạn lấy danh sách bằng TryGetValue, tạo và lưu một danh sách mới nếu chưa có, rồi Add vào đó. Để đếm hay nhóm một lần, LINQ làm được trong một biểu thức: words.GroupBy(w => w).ToDictionary(g => g.Key, g => g.Count()). Xem LINQ.

Khóa không phân biệt chữ hoa chữ thường với comparer

Khóa chuỗi mặc định được so sánh chính xác: "Apple" và "apple" là hai khóa. Truyền một IEqualityComparer<string> cho hàm khởi tạo để thay đổi điều đó.

Đầu ra:

False
text/html
1

StringComparer.OrdinalIgnoreCase là lựa chọn đúng cho các định danh như HTTP header, đuôi tệp và tên người dùng. Gọi .ToLower() trên mọi khóa trước khi lưu cũng được, nhưng rất dễ quên ở một chỗ nào đó.

Với khóa là lớp của riêng bạn, dictionary gọi GetHashCode và Equals của khóa. Lớp không ghi đè hai phương thức này sẽ so sánh theo tham chiếu, nên hai đối tượng riêng biệt có cùng các field vẫn là hai khóa khác nhau. Xem HashSet để biết cách viết cặp phương thức đó.

Thứ tự, sắp xếp và SortedDictionary

Dictionary không hứa hẹn gì về thứ tự duyệt. Trong thực tế, một dictionary chỉ toàn được thêm phần tử sẽ duyệt theo thứ tự chèn, nhưng sau một lần Remove, một lần Add sau đó có thể dùng lại chỗ trống và xuất hiện sớm hơn. Code không bao giờ nên dựa vào điều này.

Khi cần thứ tự, hãy sắp xếp ngay tại chỗ dùng hoặc dùng một collection có sắp xếp:

Đầu ra:

Cairo 210
Lima 340
Oslo 520
By value, highest first:
Oslo 520
Lima 340
Cairo 210
Berlin, Cairo, Lima, Oslo

SortedDictionary<TKey, TValue> luôn giữ khóa được sắp xếp (nó là một cây cân bằng), nên tra cứu và chèn tốn O(log n) thay vì O(1). Hãy dùng nó khi bạn thường xuyên duyệt theo thứ tự khóa; còn khi chỉ cần thứ tự một lần thì sắp xếp dictionary thường bằng LINQ. SortedList<TKey, TValue> là lựa chọn thứ ba, tốn ít bộ nhớ hơn nhưng chèn chậm khi kích thước lớn.

Tra cứu nhanh

Việc cần làmCode
Tạonew Dictionary<string, int>()
Chèn hoặc ghi đèd[k] = v
Chèn, ném lỗi nếu trùngd.Add(k, v)
Chỉ chèn nếu khóa mớid.TryAdd(k, v)
Đọc, ném lỗi nếu không cód[k]
Đọc an toànd.TryGetValue(k, out var v)
Khóa có tồn tại khôngd.ContainsKey(k)
Xóad.Remove(k) (trả về bool)
Kích thướcd.Count
Khóa, giá trịd.Keys, d.Values
Sắp xếp theo khóad.OrderBy(p => p.Key) hoặc SortedDictionary
Bỏ qua chữ hoa chữ thườngnew Dictionary<string, T>(StringComparer.OrdinalIgnoreCase)

Lỗi thường gặp

  • Đọc khóa không tồn tại bằng d[k]. Ném KeyNotFoundException; hãy dùng TryGetValue.
  • Gọi Add cho khóa có thể đã tồn tại. Ném ArgumentException; hãy dùng indexer hoặc TryAdd.
  • Thêm khóa bên trong foreach trên dictionary. Ném InvalidOperationException; hãy gom các thay đổi rồi áp dụng sau.
  • Dựa vào thứ tự duyệt. Hãy sắp xếp, hoặc dùng SortedDictionary.
  • Thay đổi field của đối tượng khóa sau khi đã chèn. Hash code của nó thay đổi và phần tử không còn tìm thấy được nữa.
  • ContainsKey rồi mới dùng indexer. Hai lần tra cứu; TryGetValue chỉ cần một.

Câu hỏi thường gặp

Dictionary.Add và indexer trong C# khác nhau thế nào?

dict.Add(key, value) chèn một phần tử mới và ném ArgumentException nếu khóa đã tồn tại. dict[key] = value chèn phần tử nếu khóa còn mới và ghi đè giá trị nếu khóa đã có, và không bao giờ ném lỗi vì khóa trùng. TryAdd(key, value) chỉ chèn khi khóa còn mới và trả về false trong trường hợp ngược lại.

TryGetValue trong C# hoạt động thế nào?

dict.TryGetValue(key, out var value) trả về true và gán value khi khóa tồn tại, còn khi không tồn tại thì trả về false và gán value bằng giá trị mặc định của kiểu. Nó chỉ tra cứu một lần, trong khi ContainsKey rồi dict[key] tra cứu hai lần, và nó không bao giờ ném KeyNotFoundException.

Làm sao duyệt một Dictionary trong C#?

foreach (KeyValuePair<string, int> pair in dict) cho bạn từng phần tử với pair.Key và pair.Value. Để chỉ duyệt khóa hoặc chỉ duyệt giá trị, hãy dùng dict.Keys hoặc dict.Values. Đừng thêm khóa vào dictionary bên trong vòng lặp đó: nó sẽ ném InvalidOperationException.

Dictionary trong C# có giữ thứ tự không?

Không có thứ tự nào được đảm bảo. Một dictionary chỉ toàn được thêm phần tử thường được duyệt theo thứ tự chèn, nhưng sau một lần Remove, phần tử mới có thể lấp vào chỗ trống, nên thứ tự thay đổi. Hãy sắp xếp khi bạn cần thứ tự: dict.OrderBy(p => p.Key), hoặc dùng SortedDictionary<TKey, TValue>, luôn duyệt theo khóa.

Làm sao để khóa của Dictionary không phân biệt chữ hoa chữ thường?

Truyền một comparer cho hàm khởi tạo: new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase). Khi đó "Apple" và "apple" là cùng một khóa với việc tra cứu, Add và ContainsKey. Comparer được cố định khi dictionary được tạo.

KeyValuePair trong C# là gì?

KeyValuePair<TKey, TValue> là struct mà dictionary đưa cho bạn ứng với mỗi phần tử khi bạn duyệt nó. Nó có các property Key và Value chỉ đọc, nên bạn không thể sửa phần tử qua nó; thay vào đó hãy viết dict[pair.Key] = newValue (sau vòng lặp, hoặc khi duyệt một bản sao của danh sách khóa).

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu