Ngày 3 — C# nâng cao
Mục tiêu ngày 3
Sau ngày 3 bạn phải:
- Hiểu Generic ở mức "vì sao tránh boxing" + viết được Generic method/class.
- Chọn đúng collection cho từng use case + biết complexity.
- Thành thạo LINQ + hiểu deferred execution ở mức state machine.
- Phân biệt Delegate / Action / Func / Event / Lambda ở mức implementation.
- Giải thích
async/awaitKHÔNG tạo thread mới + cơ chế state machine. - Biết khi nào Task vs Thread và bẫy
.Resultdeadlock.
1. Generic — code reuse + type safety
1.1. Vấn đề trước khi có Generic
ArrayList — lưu mọi thứ kiểu object:ArrayList list = new();
list.Add(1); // boxing! int → object
list.Add("hello");
list.Add(new User());
int x = (int)list[0]; // unboxing!
string s = (string)list[1]; // cast — nếu sai → runtime error
// list[0] = "hello"; — compile OK nhưng (int) sẽ runtime crash
- Boxing/unboxing đắt (đã học ngày 1).
- Không type-safe — bug chỉ phát hiện runtime.
1.2. Generic giải quyết
List<int> list = new();
list.Add(1); // không boxing — int lưu thẳng
// list.Add("hello"); // ❌ compile error — type safety
int x = list[0]; // không cast
3 lợi ích:
- Type safety compile-time — không cast.
- Không boxing với value type — fast.
- Code reuse — 1 implementation cho mọi type.
1.3. Generic method, class, interface
// Generic method
public static T Identity<T>(T x) => x;
Identity(42); // T = int (inferred)
Identity<string>("hi"); // T = string (explicit)
// Generic class
public class Box<T> {
public T Value { get; }
public Box(T value) => Value = value;
public Box<U> Map<U>(Func<T, U> fn) => new Box<U>(fn(Value));
}
var intBox = new Box<int>(42);
var strBox = intBox.Map(n => n.ToString()); // Box<string>
// Generic interface
public interface IRepository<T> where T : class {
Task<T?> GetByIdAsync(int id);
Task AddAsync(T entity);
}
1.4. Constraint — where T : ...
| Constraint | Ý nghĩa |
|---|---|
where T : class | T là reference type |
where T : struct | T là value type |
where T : new() | T có constructor không tham số |
where T : IComparable<T> | T implement interface |
where T : BaseClass | T kế thừa class cha |
where T : enum (C# 7.3+) | T là enum |
where T : INumber<T> (C# 11+) | T support operator math |
where T : U | T là U hoặc subtype |
// T phải có constructor để new() được
public class Factory<T> where T : new() {
public T Create() => new T();
}
// Combine — Repository chỉ chấp nhận class implement IEntity, có ctor
public abstract class Repository<T> where T : class, IEntity, new() {
public T CreateBlank() => new T();
// ...
}
1.5. 🔴 Generic specialization — internals
List<int> vs List<string> — CLR sinh 2 IL khác nhau:List<int>: dùngint(4 bytes) trực tiếp trong array nội bộ — không boxing.List<string>: dùngobject reference(8 bytes pointer) — share IL với mọi reference type.
List<Integer> runtime = List<Object>, vẫn boxing.2. Collections — chọn cho đúng
2.1. Bảng tổng hợp
| Khi cần | Collection | Get | Add | Remove | Note |
|---|---|---|---|---|---|
| Mảng động, index | List<T> | O(1) | O(1)* cuối, O(n) giữa | O(n) | Resize ×2 khi đầy |
| Key → Value | Dictionary<K,V> | O(1) avg | O(1) avg | O(1) avg | Hash collision |
| Tập unique | HashSet<T> | – | O(1) avg | O(1) avg | Membership test |
| FIFO | Queue<T> | – | O(1) Enqueue | O(1) Dequeue | |
| LIFO | Stack<T> | – | O(1) Push | O(1) Pop | |
| Sorted by key | SortedDictionary<K,V> | – | O(log n) | O(log n) | Red-black tree |
| Sorted set | SortedSet<T> | – | O(log n) | O(log n) | |
| Priority | PriorityQueue<T,P> | – | O(log n) | O(log n) | Min-heap default |
| Linked nodes | LinkedList<T> | O(n) | O(1) ở node | O(1) ở node | Doubly-linked |
| Đa luồng | ConcurrentDictionary | – | O(1) avg lock-free | – | Thread-safe |
*Add cuối O(1) amortized — vì resize chi phí được trải đều qua nhiều lần add.
2.2. 🟡 Resize của List
var list = new List<int>(); // Capacity = 0
list.Add(1); // Capacity = 4 (default initial)
list.Add(2); list.Add(3); list.Add(4); // Capacity = 4
list.Add(5); // Capacity = 8 (×2)
// ... đầy 8 → next 16 → 32 → 64...
var list = new List<int>(capacity: 10_000); // tránh resize 11 lần
2.3. Dictionary — hash collision
GetHashCode(key) % bucketCount → bucket index
bucket → list các (key, value) có cùng hash
Equals thì PHẢI override GetHashCode (đã học ngày 1) — nếu 2 key equals nhưng hash khác → Dictionary mất hẳn data.2.4. IEnumerable vs ICollection vs IList — kế thừa
IEnumerable<T> ← chỉ foreach
↓
ICollection<T> ← thêm Count, Add, Remove
↓
IList<T> ← thêm indexer [i], Insert, RemoveAt
↓
List<T> ← concrete
- Caller chỉ cần iterate → expose
IEnumerable<T>(read-only, lazy OK). - Caller cần Count →
IReadOnlyCollection<T>hoặcICollection<T>. - Caller cần index →
IReadOnlyList<T>hoặcIList<T>.
2.5. yield return — iterator lazy
public IEnumerable<int> EvenNumbers(int max) {
for (int i = 0; i <= max; i += 2) {
yield return i;
}
}
foreach (var n in EvenNumbers(10)) Console.WriteLine(n);
yield: compiler sinh ra state machine class implement IEnumerator<int>. Mỗi MoveNext() chạy đến yield return tiếp theo. State (i, vị trí execution) được lưu trong class.Lợi ích: lazy — chỉ generate khi caller cần. Có thể infinite:public IEnumerable<int> Naturals() {
int i = 1;
while (true) yield return i++;
}
// Lấy 10 số đầu, không tràn memory
foreach (var n in Naturals().Take(10)) Console.WriteLine(n);
3. LINQ — Language Integrated Query
3.1. LINQ là gì?
IEnumerable<T> và IQueryable<T> cho query data style functional. 2 cú pháp:// Method syntax (phổ biến)
var hcm = users.Where(u => u.City == "HCM")
.OrderBy(u => u.Age)
.Select(u => u.Name);
// Query syntax (giống SQL)
var hcm = from u in users
where u.City == "HCM"
orderby u.Age
select u.Name;
3.2. Method quan trọng
var users = new List<User> {
new("Alice", 25, "HCM"),
new("Bob", 30, "HN"),
new("Charlie", 22, "HCM"),
};
// Filter
users.Where(u => u.City == "HCM");
// Project
users.Select(u => new { u.Name, u.Age });
// Sort
users.OrderBy(u => u.City).ThenByDescending(u => u.Age);
// Group
users.GroupBy(u => u.City)
.Select(g => new { City = g.Key, Count = g.Count() });
// Aggregate
users.Count();
users.Sum(u => u.Age);
users.Average(u => u.Age);
users.Min(u => u.Age);
users.Max(u => u.Age);
// Predicate
users.Any(u => u.Age > 25); // có ai > 25?
users.All(u => u.Age >= 18); // tất cả >= 18?
// Element
users.First(u => u.Name == "Alice"); // không có → exception
users.FirstOrDefault(u => u.Name == "X"); // không có → null/default
users.Single(u => u.City == "HCM"); // CHÍNH XÁC 1 phần tử, khác → exception
// Set
users.Distinct();
users.Skip(2).Take(3); // pagination
// Join
var orders = new List<Order> { /*...*/ };
users.Join(orders,
u => u.Id, o => o.UserId,
(u, o) => new { u.Name, o.Total });
3.3. 🟡 IEnumerable vs IQueryable — phân biệt quan trọng
IEnumerable<T> | IQueryable<T> | |
|---|---|---|
| Query chạy ở đâu | In-memory (client) | Dịch sang SQL, chạy ở DB |
Khi gọi .Where(x => x.Age > 18) | Filter trong RAM | Generate WHERE age > 18 trong SQL |
| Lợi điểm | Đơn giản, dùng cho list nhỏ | Push filter xuống DB → ít data về client |
Ví dụ tai hại nếu nhầm:
// ❌ Lấy HẾT user về client rồi mới filter
IEnumerable<User> all = dbContext.Users; // .AsEnumerable() ép thành IEnumerable
var hcm = all.Where(u => u.City == "HCM").ToList();
// → SQL: SELECT * FROM Users (lấy hết 1 triệu user về RAM!)
// ✅ Filter ở DB
IQueryable<User> q = dbContext.Users; // mặc định là IQueryable
var hcm = q.Where(u => u.City == "HCM").ToList();
// → SQL: SELECT * FROM Users WHERE City = 'HCM' (chỉ lấy data cần)
IQueryable đến cuối cùng (right before .ToList()/.ToListAsync()). Đừng .ToList() sớm rồi mới .Where() — sẽ kéo hết về client.3.4. 🟡 Deferred execution
.Where()/.Select()/.OrderBy(). Chỉ chạy khi:- Iterate (
foreach). - Gọi terminator:
ToList(),ToArray(),Count(),First(),Any(),Sum()...
var q = users.Where(u => u.Age > 25); // CHƯA chạy
users.Add(new("Eve", 50, "HCM")); // thêm sau
foreach (var u in q) Console.WriteLine(u.Name);
// Eve xuất hiện! Vì query chạy lúc foreach.
Sai lầm phổ biến — chạy nhiều lần ngầm:
var q = dbContext.Users.Where(u => u.Active);
Console.WriteLine(q.Count()); // SELECT COUNT(*) FROM Users WHERE Active = 1
foreach (var u in q) { ... } // SELECT * FROM Users WHERE Active = 1 — gọi LẦN 2!
→ Fix: var list = q.ToList(); để materialize 1 lần, sau đó iterate nhiều lần OK.
3.5. Cơ chế bên trong — Where thực sự làm gì?
public static IEnumerable<T> Where<T>(this IEnumerable<T> src, Func<T, bool> pred) {
foreach (var item in src) {
if (pred(item)) yield return item;
}
}
→ Generator dùng yield return. Không tạo list mới — chỉ wrap source + filter on-demand.
Chain method = wrap nhiều generator:
src.Where(...).Select(...).Take(5)
// = Where wraps src
// = Select wraps Where
// = Take wraps Select
// Khi enumerate Take → pull 1 phần tử qua Select → qua Where → từ src
Đó là vì sao chain dài LINQ vẫn nhanh — không tạo list trung gian.
4. Delegate, Action, Func, Event, Lambda
4.1. Delegate — function pointer type-safe
this).// Định nghĩa delegate type
public delegate int MathOp(int a, int b);
// Tạo instance
MathOp add = (a, b) => a + b;
MathOp sub = (a, b) => a - b;
// Gọi như method
Console.WriteLine(add(2, 3)); // 5
Console.WriteLine(sub(5, 2)); // 3
// Truyền delegate vào method khác
int Apply(MathOp op, int x, int y) => op(x, y);
Apply(add, 10, 20); // 30
4.2. Action, Func, Predicate — built-in delegate
C# có sẵn delegate generic — không cần tự định nghĩa:
| Delegate | Signature | Khi dùng |
|---|---|---|
Action | void() | Không tham số, không return |
Action<T> | void(T) | 1 tham số, không return |
Action<T1, T2> | void(T1, T2) | 2 tham số, không return |
Func<TResult> | TResult() | Không tham số, có return |
Func<T, TResult> | TResult(T) | 1 tham số, có return |
Func<T, bool> | bool(T) | = Predicate<T> |
Predicate<T> | bool(T) | (legacy, ít dùng) |
Action<string> log = msg => Console.WriteLine(msg);
Func<int, int, int> mul = (a, b) => a * b;
Func<string, bool> isEmpty = s => string.IsNullOrEmpty(s);
// Truyền vào LINQ
var names = users.Select((Func<User, string>)(u => u.Name)); // hiếm khi cần cast — tự inferred
4.3. Lambda — cú pháp inline
// Lambda expression — 1 expression
Func<int, int> sq = x => x * x;
// Lambda statement — nhiều dòng, dùng {}
Func<int, int> abs = x => {
if (x < 0) return -x;
return x;
};
// Không tham số
Action greet = () => Console.WriteLine("Hi");
// Nhiều tham số
Func<int, int, int> add = (a, b) => a + b;
// Type explicit (đôi khi cần)
Func<int, int> doubled = (int x) => x * 2;
4.4. 🟡 Closure — bắt biến từ scope ngoài
int multiplier = 3;
Func<int, int> times = x => x * multiplier; // bắt `multiplier`
multiplier = 10;
Console.WriteLine(times(5)); // 50 — đọc giá trị MỚI của multiplier!
// Compiler sinh:
class __Closure {
public int multiplier;
public int Lambda(int x) => x * multiplier;
}
foreach, nhưng for vẫn cẩn thận):var actions = new List<Action>();
for (int i = 0; i < 3; i++) {
actions.Add(() => Console.WriteLine(i)); // bắt i!
}
foreach (var a in actions) a();
// Output: 3, 3, 3 — i = 3 khi loop kết thúc!
// Fix: tạo biến local trong loop
for (int i = 0; i < 3; i++) {
int local = i;
actions.Add(() => Console.WriteLine(local)); // bắt local — khác biến mỗi vòng
}
// Output: 0, 1, 2 ✅
4.5. Event — Observer pattern built-in
public class StockTicker {
// Event = delegate được wrap để class ngoài chỉ += / -=, không Invoke
public event EventHandler<PriceChangedEventArgs>? PriceChanged;
public void UpdatePrice(string symbol, decimal newPrice) {
// Class chủ tự fire event
PriceChanged?.Invoke(this, new PriceChangedEventArgs(symbol, newPrice));
}
}
public class PriceChangedEventArgs : EventArgs {
public string Symbol { get; }
public decimal NewPrice { get; }
public PriceChangedEventArgs(string s, decimal p) { Symbol = s; NewPrice = p; }
}
// Subscribe
var ticker = new StockTicker();
ticker.PriceChanged += (sender, e) => Console.WriteLine($"{e.Symbol}: {e.NewPrice}");
ticker.PriceChanged += SaveToDatabase; // method reference
ticker.UpdatePrice("AAPL", 150.5m);
// 2 subscriber chạy lần lượt
4.6. 🟡 Event vs Delegate public
public Action<string>? PriceChanged; // PUBLIC delegate
ticker.PriceChanged = null;— xoá hết subscriber khác.ticker.PriceChanged.Invoke(...);— fire event giả mạo.
event keyword wrap delegate, chỉ cho phép += / -= từ ngoài. Invoke + Reset chỉ class chủ làm được.5. Extension method
5.1. Cú pháp
public static class StringExtensions {
public static bool IsValidEmail(this string s) {
return s.Contains('@') && s.Contains('.');
}
public static string Repeat(this string s, int times) {
return string.Concat(Enumerable.Repeat(s, times));
}
}
// Dùng như method của string
"a@b.com".IsValidEmail(); // true
"Ha".Repeat(3); // "HaHaHa"
Quy tắc:
- Static class.
- Static method.
- Tham số đầu có
thismodifier.
5.2. Bên trong — chỉ là syntactic sugar
Compiler dịch:
"hi".Repeat(3)
// thành:
StringExtensions.Repeat("hi", 3)
Không có magic — chỉ là syntactic sugar.
5.3. LINQ chính là extension method
namespace System.Linq {
public static class Enumerable {
public static IEnumerable<T> Where<T>(this IEnumerable<T> src, Func<T, bool> pred) {...}
public static IEnumerable<U> Select<T, U>(this IEnumerable<T> src, Func<T, U> sel) {...}
// ...
}
}
→ Mọi IEnumerable<T> có Where/Select/etc. bất kỳ collection nào implement IEnumerable<T> đều dùng LINQ được — power lớn.
6. async / await — hiểu ĐÚNG
6.1. Câu hỏi gài: "async/await có tạo thread mới không?"
async/awaitkhông tạo thread mới.6.2. Cơ chế thực sự — state machine
public async Task<string> GetUserAsync(int id) {
var http = new HttpClient();
var json = await http.GetStringAsync($"/api/users/{id}");
return json;
}
Compiler dịch thành class state machine (giản lược):
public Task<string> GetUserAsync(int id) {
var sm = new GetUserStateMachine {
id = id,
state = -1
};
sm.MoveNext(); // start
return sm.Task;
}
class GetUserStateMachine : IAsyncStateMachine {
public int state;
public int id;
public TaskCompletionSource<string> Task;
public HttpClient http;
public TaskAwaiter<string> awaiter;
public void MoveNext() {
switch (state) {
case -1:
http = new HttpClient();
var task = http.GetStringAsync($"/api/users/{id}");
awaiter = task.GetAwaiter();
if (!awaiter.IsCompleted) {
state = 0;
awaiter.OnCompleted(MoveNext); // schedule callback, return!
return;
}
goto case 0;
case 0:
string json = awaiter.GetResult();
Task.SetResult(json);
return;
}
}
}
Hiểu nôm na:
- Khi gặp
await, method trảTaskngay → thread caller được giải phóng. - State machine schedule callback chạy khi I/O xong.
- Khi I/O xong, callback có thể chạy trên thread khác từ ThreadPool.
- State machine resume từ vị trí await, không phải đầu method.
async/await không tạo thread mới. Khi gặp await, method giải phóng thread hiện tại, return Task. Khi I/O xong (HTTP/DB response về), runtime schedule continuation chạy trên ThreadPool thread bất kỳ. Mục đích: giải phóng thread trong khi chờ I/O — không phải để parallel."6.3. Khi nào dùng async — I/O bound vs CPU bound
| Loại work | Dùng gì |
|---|---|
| I/O bound (HTTP, DB, file, network) | async/await trực tiếp — không cần Task.Run |
| CPU bound (heavy math, image processing) | Task.Run(() => ...) để offload sang ThreadPool |
| Long-running background | IHostedService, BackgroundService |
// ✅ I/O bound — async trực tiếp
public async Task<User> GetUserAsync(int id) {
return await _db.Users.FindAsync(id);
}
// ✅ CPU bound — offload
public async Task<int> CalculatePrimesAsync(int limit) {
return await Task.Run(() => CountPrimes(limit)); // CountPrimes nặng CPU
}
// ❌ Sai — Task.Run cho I/O work là vô nghĩa
public async Task<User> GetUserBad(int id) {
return await Task.Run(() => _db.Users.FindAsync(id)); // thêm 1 thread vô ích
}
6.4. Task vs Thread
new Thread(...). Manual lifecycle.await), cancellation, exception propagation.// Thread thủ công — hiếm dùng
var t = new Thread(() => HeavyWork());
t.Start();
t.Join(); // chờ xong
// Task — chuẩn hiện đại
var task = Task.Run(() => HeavyWork());
await task;
// Multiple parallel
var tasks = users.Select(u => FetchOrdersAsync(u.Id)).ToList();
await Task.WhenAll(tasks);
6.5. 🔴 Bẫy .Result / .Wait() — deadlock
.Result hoặc .Wait() trên Task → deadlock.Vì:- Caller block UI/request thread.
- Task continuation (sau await) cần resume trên SynchronizationContext gốc.
- Context gốc = thread đang bị block → continuation không chạy được → Task không complete → caller chờ vô tận.
// ❌ Deadlock kinh điển ASP.NET classic
public IActionResult Index() {
var user = GetUserAsync(1).Result; // deadlock!
return View(user);
}
// ✅ Async all the way
public async Task<IActionResult> Index() {
var user = await GetUserAsync(1);
return View(user);
}
6.6. async void — chỉ event handler
// ❌ Thường sai
public async void DoStuff() {
await SomeAsync();
}
// ✅ Đúng — không await được, exception khó catch
public async Task DoStuff() {
await SomeAsync();
}
// ✅ Event handler — exception case
button.Click += async (sender, e) => {
await SaveAsync(); // OK
};
async void có vấn đề:
- Không await được → caller không biết khi nào xong.
- Exception throw vào SynchronizationContext → có thể crash app.
6.7. ConfigureAwait(false) — library best practice
// Library code — không depend SynchronizationContext của caller
public async Task<User> GetUserAsync(int id) {
var data = await _http.GetStringAsync(...).ConfigureAwait(false);
return Parse(data);
}
ConfigureAwait(false) báo "không cần quay về SynchronizationContext gốc sau await" → tránh deadlock + tăng performance.
7. Câu hỏi tự test cuối ngày
- Vì sao có Generic? Giải quyết vấn đề gì so với ArrayList?
Dictionarylookup là O(?) — vì sao?ActionvsFunckhác nhau ra sao?
IEnumerable vs IQueryable khác gì? Cho ví dụ EF Core.
5. Deferred execution là gì? Cho ví dụ bug do enumerate query 2 lần.
6. Closure trong C# bắt VALUE hay VARIABLE? Cho ví dụ.
7. async/await có tạo thread mới không? Giải thích.
8. Khi nào dùng Task.Run cho CPU bound, khi nào KHÔNG dùng cho I/O?🟡 Mid:
9. yield return hoạt động ra sao? Compiler sinh code gì?
10. Vì sao event tốt hơn delegate public?
11. State machine của async/await — mô tả?
12. ConfigureAwait(false) để làm gì? Khi nào nên dùng?🔴 Senior:
13. Generic specialization — C# khác Java thế nào?
14. Deadlock với .Result trong ASP.NET classic — giải thích cơ chế.
15. Task.WhenAll vs Task.WaitAll — khác nhau cơ bản nào?
16. ChannelĐáp án: Ngân hàng câu hỏi C#.
➡️ Ngày mai: Ngày 4 — Database & SQL