C# Day 5: Collections
Sau Generics, ngày 5 tập trung vào Collections trong C#.
Mục tiêu không chỉ là nhớ syntax, mà phải biết chọn đúng collection theo use case.
Các type chính:
T[]
List<T>
Dictionary<TKey, TValue>
HashSet<T>
Queue<T>
Stack<T>
IEnumerable<T>
ICollection<T>
IList<T>
IReadOnlyCollection<T>
IReadOnlyList<T>
Mental model:
Array
→ fixed size
List<T>
→ ordered dynamic sequence
Dictionary<TKey, TValue>
→ lookup theo key
HashSet<T>
→ unique values
Queue<T>
→ FIFO
Stack<T>
→ LIFO
IEnumerable<T>
→ có thể enumerate bằng foreach
1. Array
Array có kích thước cố định.
int[] numbers = new int[3];
numbers[0] = 10;
numbers[1] = 20;
numbers[2] = 30;
Hoặc:
int[] numbers = { 10, 20, 30 };
Truy cập theo index:
Console.WriteLine(numbers[1]);
Mental model:
Array
→ fixed-size sequence
→ index access
Dùng array khi:
- size cố định hoặc đã biết rõ
- cần cấu trúc đơn giản
- API yêu cầu array
- không cần Add/Remove linh hoạt
2. List<T>
List<T> là collection có thứ tự và kích thước động.
var users = new List<string>();
users.Add("David");
users.Add("Vu");
Truy cập:
Console.WriteLine(users[0]);
Remove:
users.Remove("David");
Mental model:
List<T>
→ dynamic array-like collection
→ ordered
→ index-based
Count vs Capacity
Count
→ số item hiện có
Capacity
→ sức chứa của internal storage hiện tại
Khi vượt capacity, List<T> có thể cần cấp storage lớn hơn và copy dữ liệu.
Dynamic size không có nghĩa resize hoàn toàn miễn phí.
3. Lookup trong List<T>
Ví dụ:
var user = users.FirstOrDefault(
x => x.Id == userId
);
Về bản chất thường phải scan các phần tử.
List search
→ thường O(n)
Nếu use case chính là lookup theo Id, Dictionary có thể phù hợp hơn.
4. Dictionary<TKey, TValue>
Dictionary lưu dữ liệu theo:
key → value
Ví dụ:
var usersById =
new Dictionary<Guid, User>();
Thêm:
usersById.Add(user.Id, user);
Hoặc:
usersById[user.Id] = user;
Lookup:
User user = usersById[userId];
Mental model:
Dictionary<TKey, TValue>
→ lookup table theo key
Average lookup thường gần:
O(1)
TryGetValue
Thường nên dùng:
if (usersById.TryGetValue(
userId,
out var user))
{
Console.WriteLine(user.Name);
}
Mental model:
TryGetValue
→ lookup + kiểm tra tồn tại
Duplicate key
usersById.Add(id, user1);
usersById.Add(id, user2);
lần thứ hai sẽ lỗi vì key đã tồn tại.
Nhưng:
usersById[id] = user2;
sẽ thay value của key đó.
5. HashSet<T>
HashSet<T> tập trung vào unique values.
var roles = new HashSet<string>();
roles.Add("Admin");
roles.Add("Merchant");
roles.Add("Admin");
Set chỉ chứa một "Admin".
Mental model:
HashSet<T>
→ set
→ không duplicate theo equality semantics
Ví dụ kiểm tra đã xử lý ID chưa:
var processedIds =
new HashSet<Guid>();
if (!processedIds.Add(id))
{
Console.WriteLine("Already processed");
}
Add trả:
true
→ item mới
false
→ item đã tồn tại
Use cases:
- deduplication
- membership check
- processed IDs
- permissions
- tags unique
6. Dictionary vs HashSet
Dictionary<TKey, TValue>
→ key + value
HashSet<T>
→ unique value
Ví dụ:
Dictionary<Guid, User>
khi cần:
userId → User
Còn:
HashSet<Guid>
khi chỉ cần:
ID này đã tồn tại chưa?
7. Queue<T>
Queue hoạt động theo:
FIFO
First In, First Out
Ví dụ:
var queue = new Queue<string>();
queue.Enqueue("A");
queue.Enqueue("B");
queue.Enqueue("C");
var item = queue.Dequeue();
item là:
A
Use cases:
- job processing
- request processing
- BFS
- workflow theo thứ tự đến
Lưu ý:
Queue<T> trong memory
≠
message broker
RabbitMQ, Azure Service Bus, Kafka là bài toán khác.
8. Stack<T>
Stack hoạt động theo:
LIFO
Last In, First Out
Ví dụ:
var stack = new Stack<string>();
stack.Push("A");
stack.Push("B");
stack.Push("C");
var item = stack.Pop();
item là:
C
Use cases:
- undo
- backtracking
- DFS
- navigation history
- expression parsing
9. Peek
Queue và Stack đều có Peek().
queue.Peek();
stack.Peek();
Mental model:
Peek
→ xem item tiếp theo
Dequeue / Pop
→ lấy và remove
10. IEnumerable<T>
IEnumerable<T> đại diện cho một sequence có thể được enumerate.
IEnumerable<User> users;
Có thể:
foreach (var user in users)
{
Console.WriteLine(user.Name);
}
Mental model:
IEnumerable<T>
→ "Tôi có thể enumerate các T"
Nó không đảm bảo rằng:
- có
Add - có
Remove - có index
Countlà O(1)- dữ liệu đã nằm trong memory
- query đã execute
List<T> implement IEnumerable<T>, nhưng IEnumerable<T> không có nghĩa là List<T>.
11. ICollection<T>
ICollection<T> thêm các collection operation:
Count
Add
Remove
Contains
Clear
Mental model:
IEnumerable<T>
→ enumerate
ICollection<T>
→ enumerate + collection operations
12. IList<T>
IList<T> thêm list semantics và index access.
IList<User> users;
Có thể:
var first = users[0];
Mental model:
IList<T>
→ ICollection<T>
+ positional/index access
Simplified hierarchy:
IEnumerable<T>
↑
ICollection<T>
↑
IList<T>
13. Read-only Collection Interfaces
Nếu caller chỉ cần enumerate và Count:
IReadOnlyCollection<User>
Nếu caller cần thêm index access:
IReadOnlyList<User>
Ví dụ:
private readonly List<User> _users = new();
public IReadOnlyList<User> Users
=> _users;
Mental model:
IReadOnlyList<T>
→ read-only collection API surface
Nhưng:
read-only collection
≠
deep immutable objects
Nếu User.Name có setter:
Users[0].Name = "Changed";
vẫn có thể hợp lệ.
14. Equality và Hashing
Dictionary<TKey, TValue> và HashSet<T> phụ thuộc vào equality/hash semantics.
Conceptual flow:
key/value
↓
GetHashCode()
↓
candidate bucket
↓
Equals()
↓
same / different
Rule quan trọng:
Nếu a.Equals(b) == true
→ a và b phải có cùng hash code
Nhưng:
same hash
≠
chắc chắn Equals == true
Hai object khác nhau có thể bị hash collision.
15. Mutable Dictionary Key có thể gây Bug
Ví dụ:
public class UserKey
{
public string Email { get; set; }
}
Giả sử equality/hash phụ thuộc vào Email.
var key = new UserKey
{
Email = "a@test.com"
};
dictionary[key] = user;
Sau đó:
key.Email = "b@test.com";
Hash/equality có thể thay đổi trong khi dictionary đã lưu key dựa trên trạng thái cũ.
Kết quả:
object vẫn ở dictionary
nhưng lookup có thể thất bại
Rule:
Key của hash-based collection nên có equality/hash ổn định trong thời gian tồn tại trong collection.
16. String Comparer
Ví dụ:
var emails =
new HashSet<string>(
StringComparer.OrdinalIgnoreCase
);
emails.Add("test@example.com");
emails.Add("TEST@example.com");
Hai chuỗi được xem là cùng value theo comparer đó.
Tương tự với Dictionary:
var users =
new Dictionary<string, User>(
StringComparer.OrdinalIgnoreCase
);
17. Big-O Mental Model
Big-O mô tả chi phí operation tăng như thế nào khi dữ liệu tăng.
O(1)
→ gần constant-time
O(n)
→ tăng tuyến tính theo số item
Bảng mental model:
| Operation | Typical Complexity |
|---|---|
| Array index | O(1) |
| List index | O(1) |
| List scan/search | O(n) |
| Dictionary lookup | average O(1) |
| HashSet membership | average O(1) |
| Queue enqueue/dequeue | typically O(1) |
| Stack push/pop | typically O(1) |
Đây là mental model để reasoning, không phải benchmark tuyệt đối.
18. List vs Dictionary
Ví dụ:
List<User> users;
Lookup:
users.FirstOrDefault(
x => x.Id == userId
);
thường:
O(n)
Nếu:
Dictionary<Guid, User> usersById;
lookup:
usersById.TryGetValue(
userId,
out var user
);
average gần:
O(1)
Nhưng Dictionary có trade-off:
- dùng thêm memory
- cần key
- key phải có equality/hash phù hợp
- không phải use case nào cũng cần lookup table
19. Đừng Optimize Quá Sớm
Nếu collection có 5 phần tử và lookup một lần, dùng Dictionary chỉ vì O(1) chưa chắc tốt hơn về design.
Hãy cân nhắc:
- số lượng dữ liệu
- frequency của operation
- readability
- correctness
- memory
- profile thực tế
Big-O là công cụ reasoning, không phải lý do duy nhất.
20. Production Use Cases
UI list
List<UserDto>
Repeated lookup theo ID
Dictionary<Guid, User>
Unique event IDs
HashSet<Guid>
FIFO jobs
Queue<Job>
Undo history
Stack<Command>
21. API Design — Expose Capability Tối Thiểu
Không phải lúc nào cũng nên expose:
public List<User> Users { get; }
Caller có thể:
service.Users.Clear();
Nếu không muốn caller mutate:
public IReadOnlyList<User> Users
=> _users;
Mental model:
Expose capability tối thiểu mà caller cần
Nếu method chỉ cần enumerate:
public void PrintUsers(
IEnumerable<User> users)
{
foreach (var user in users)
{
Console.WriteLine(user.Name);
}
}
Không nhất thiết phải yêu cầu List<User>.
22. Common Mistake — Dùng List cho Lookup Lặp Lại
Ví dụ:
foreach (var order in orders)
{
var user = users.FirstOrDefault(
x => x.Id == order.UserId
);
}
Nếu orders và users đều lớn, đây là repeated scan.
Có thể build lookup:
var usersById =
users.ToDictionary(
x => x.Id
);
Sau đó:
foreach (var order in orders)
{
if (usersById.TryGetValue(
order.UserId,
out var user))
{
// use user
}
}
Mental model:
Build lookup once
→ repeated O(1)-average key lookup
23. Common Mistake — Modify khi đang foreach
Ví dụ:
foreach (var user in users)
{
users.Remove(user);
}
Nhiều collection sẽ throw:
InvalidOperationException
vì collection bị thay đổi trong lúc enumerator đang hoạt động.
Thường nên:
- collect item cần remove rồi remove sau
- iterate bằng index theo cách phù hợp
- tạo collection mới
- dùng API phù hợp
24. Common Mistake — Nghĩ IEnumerable<T> luôn nằm trong Memory
Không đúng.
IEnumerable<T> chỉ nói rằng sequence có thể được enumerate.
Nguồn có thể là:
List<T>- array
- iterator
- lazy sequence
- query result
Deferred execution sẽ học kỹ hơn khi tới LINQ.
25. Common Mistake — Gọi Count() mà không hiểu Source
Với:
IEnumerable<T>
Count() có thể phải enumerate source tùy implementation.
Trong khi:
ICollection<T>.Count
là property của collection.
Không nên mặc định mọi IEnumerable<T> đều có cheap count.
26. Collection Expression trong C# hiện đại
Ví dụ:
int[] numbers = [1, 2, 3];
Hoặc:
List<string> names =
[
"David",
"Vu"
];
Đây là syntax convenience.
Nó không thay đổi bản chất của data structure.
27. Thread Safety
Các collection thông thường như:
List<T>
Dictionary<TKey, TValue>
HashSet<T>
không nên mặc định coi là thread-safe khi nhiều thread cùng mutate.
.NET có các type như:
ConcurrentDictionary<TKey, TValue>
ConcurrentQueue<T>
nhưng phần này để học trong Concurrency.
Mental note:
normal collection
≠
automatic thread safety
Bảng Chọn Collection Nhanh
| Requirement | Collection |
|---|---|
| Fixed-size sequence | Array |
| Ordered dynamic sequence | List<T> |
| Lookup theo unique key | Dictionary<TKey,TValue> |
| Unique values / membership | HashSet<T> |
| FIFO | Queue<T> |
| LIFO | Stack<T> |
| Chỉ cần enumerate | IEnumerable<T> |
| Collection operations | ICollection<T> |
| Index-based abstraction | IList<T> |
| Read-only enumerate + Count | IReadOnlyCollection<T> |
| Read-only + index | IReadOnlyList<T> |
Mental Model Ngày 5
Array
→ fixed size
List<T>
→ ordered dynamic sequence
Dictionary<TKey, TValue>
→ key → value lookup
HashSet<T>
→ unique set
Queue<T>
→ FIFO
Stack<T>
→ LIFO
IEnumerable<T>
→ enumerate capability
ICollection<T>
→ collection operations
IList<T>
→ index/list capability
IReadOnlyList<T>
→ expose read-only list API
Equality:
Dictionary / HashSet
→ hashing + equality
Performance:
List search
→ usually O(n)
Dictionary lookup
→ average O(1)
HashSet Contains
→ average O(1)
Câu Hỏi Ôn Tập
1. Khi nào dùng Array thay vì List<T>?
Khi size cố định hoặc đã biết rõ và không cần add/remove linh hoạt.
2. List<T> có giữ thứ tự không?
Có.
3. Search trong List thường có complexity gì?
Thường O(n).
4. Dictionary dùng để làm gì?
Mapping unique key tới value để lookup theo key hiệu quả.
5. TryGetValue dùng để làm gì?
Vừa kiểm tra key tồn tại vừa lấy value.
6. HashSet<T> giải quyết bài toán gì?
Unique values và membership check.
7. Queue hoạt động theo nguyên tắc gì?
FIFO
First In, First Out
8. Stack hoạt động theo nguyên tắc gì?
LIFO
Last In, First Out
9. IEnumerable<T> có đảm bảo index không?
Không.
10. IEnumerable<T> có đảm bảo Add() không?
Không.
11. ICollection<T> thêm gì?
Các collection operation như Count, Add, Remove, Contains.
12. IList<T> thêm gì?
Index access và list semantics.
13. IReadOnlyList<T> có làm object bên trong immutable không?
Không.
14. Vì sao Dictionary cần GetHashCode() và Equals()?
Hash giúp tìm candidate bucket, equality xác định key thực sự có bằng nhau hay không.
15. Hai object cùng hash có chắc bằng nhau không?
Không. Có thể là hash collision.
16. Nếu Equals() là true thì hash code phải thế nào?
Phải giống nhau.
17. Vì sao mutable key nguy hiểm?
Mutation có thể thay đổi hash/equality sau khi key đã được insert.
18. HashSet.Contains() và List.Contains() khác gì về performance mental model?
HashSet
→ average O(1)
List
→ usually O(n)
19. Có nên luôn dùng Dictionary vì O(1) không?
Không. Phải cân nhắc use case, memory, readability và frequency lookup.
20. Vì sao method đôi khi nên nhận IEnumerable<T> thay vì List<T>?
Nếu method chỉ cần enumerate, yêu cầu List<T> sẽ bắt caller cung cấp nhiều capability hơn mức thực sự cần.
Bài Tập Ngày 5
Bài 1 — List
Tạo:
List<User>
và:
- add 5 users
- remove 1 user
- tìm user theo
Id - access theo index
Bài 2 — Dictionary
Từ:
List<User>
tạo:
Dictionary<Guid, User>
với User.Id làm key.
Lookup bằng:
TryGetValue
Bài 3 — HashSet
Cho dữ liệu:
1, 2, 2, 3, 3, 3, 4
dùng:
HashSet<int>
để còn:
1, 2, 3, 4
Sau đó giải thích vì sao duplicate bị loại.
Bài 4 — Queue
Process:
Job A
Job B
Job C
theo đúng FIFO.
Bài 5 — Stack
Dùng:
/home
/products
/products/1
để mô phỏng nút Back bằng Stack<string>.
Bài 6 — Equality Bug
Tạo custom dictionary key có mutable property.
Override:
Equals()
GetHashCode()
dựa trên property đó.
Insert vào Dictionary, sau đó mutate property và thử lookup lại.
Mục tiêu:
Hiểu vì sao hash-based key cần stable equality/hash
Production Exercise
Giả sử:
List<User> users;
List<Order> orders;
Mỗi order có:
Guid UserId;
Ban đầu:
foreach (var order in orders)
{
var user = users.FirstOrDefault(
x => x.Id == order.UserId
);
if (user is null)
{
continue;
}
Console.WriteLine(
$"{order.Id} - {user.Name}"
);
}
Refactor bằng:
Dictionary<Guid, User>
Sau đó tự giải thích trade-off:
+ repeated lookup nhanh hơn
+ intent "lookup by ID" rõ hơn
- thêm memory
- cần build dictionary
- key phải unique và stable
Key Takeaways
Không có collection tốt nhất.
Có collection phù hợp hơn với use case.
Nhớ:
Sequence
→ List<T>
Key lookup
→ Dictionary<TKey,TValue>
Uniqueness / membership
→ HashSet<T>
FIFO
→ Queue<T>
LIFO
→ Stack<T>
Enumeration abstraction
→ IEnumerable<T>
Và quan trọng nhất:
Data structure
→ ảnh hưởng trực tiếp tới
algorithm
performance
API design
correctness
Ngày tiếp theo:
Day 6
→ Delegates
→ Func
→ Action
→ Lambda
→ Events
Sau đó:
Day 7
→ LINQ