Lộ trình 7 ngày

Ngày 1 — C# cơ bản

Stack/heap, value vs reference type, boxing/unboxing, string/StringBuilder, exception. Giải thích từ con số 0 cho intern → kiến thức nâng cao cho senior.

Mục tiêu ngày 1

Sau ngày 1, bạn phải hiểu sâu (không chỉ đọc thuộc) các khái niệm sau, đến mức có thể vẽ hình giải thích cho người khác:

  • Bộ nhớ trong .NET hoạt động ra sao — Stack vs Heap là gì.
  • Value type khác Reference type ở mức bộ nhớ thực, không chỉ "lưu giá trị vs lưu địa chỉ".
  • Boxing/Unboxing thực chất là gì, gây hại như thế nào.
  • string immutable nghĩa là gì ở mức bộ nhớ.
  • Khi nào dùng const, readonly, static readonly.
  • Tránh sai lầm throw ex — vì sao mất stack trace.
Quy ước level (áp dụng xuyên suốt tài liệu):
  • 🟢 Intern / Fresher: định nghĩa, ví dụ cơ bản.
  • 🔵 Junior: dùng được trong code thực, biết gotcha thường gặp.
  • 🟡 Mid: hiểu trade-off, internals đủ để debug.
  • 🔴 Senior: implementation chi tiết, performance, edge case sâu.

1. Bộ nhớ trong .NET — nền tảng

1.1. Tại sao phải hiểu Stack và Heap?

🟢 Trước khi nói value/reference type, bạn phải biết Stack và Heap là gì. 90% fresher trả lời "value type lưu trên stack" mà không định nghĩa nổi stack là gì → interviewer biết bạn học vẹt.

Khi chương trình chạy, mỗi process được hệ điều hành cấp một vùng RAM. .NET chia vùng RAM này thành nhiều khu chức năng, trong đó 2 khu quan trọng nhất là:

  • Stack — vùng nhớ dạng "chồng đĩa" (LIFO — last in, first out).
  • Heap — vùng nhớ tự do, được quản lý bởi Garbage Collector (GC).
┌─────────────────────────────────────┐
│           PROCESS MEMORY            │
├─────────────────────────────────────┤
│  STACK (1MB mặc định / thread)      │  ← grows downward
│  ┌─────────────────────┐            │
│  │ Frame of Main()     │            │
│  ├─────────────────────┤            │
│  │ Frame of CalcSum()  │            │
│  ├─────────────────────┤            │
│  │ Frame of GetUser()  │  ← active  │
│  └─────────────────────┘            │
│         ...                         │
│                                     │
│         (free space)                │
│                                     │
│                                     │
│           HEAP                      │  ← grows upward
│  ┌─────────────┐                    │
│  │ User obj    │                    │
│  └─────────────┘                    │
│  ┌─────────────────────┐            │
│  │ string "hello"      │            │
│  └─────────────────────┘            │
│  ┌──┐                               │
│  │..│  Gen 0, Gen 1, Gen 2 (GC)     │
│  └──┘                               │
└─────────────────────────────────────┘

1.2. Stack — vùng nhớ "chồng đĩa"

🟢 Hình dung Stack như chồng đĩa rửa chén: đặt đĩa lên trên (push), lấy đĩa trên cùng xuống (pop). Bạn không lấy được đĩa giữa hay đáy chồng.

Đặc điểm stack:

Tính chấtGiải thích
Cấu trúc LIFOVào sau ra trước — như chồng đĩa
Mỗi thread có 1 stack riêngStack default 1 MB / thread trên Windows, 8 MB trên Linux
Cấp phát / giải phóng cực nhanhChỉ là tăng/giảm 1 con trỏ
Không cần GCHết scope thì biến mất tự động
Bị giới hạn kích thướcĐệ quy quá sâu → StackOverflowException

Cái gì được lưu trên stack:

  • Local variable kiểu value type của method (int x = 5; bên trong method).
  • Method parameter kiểu value type.
  • Reference (con trỏ) tới object trên heap. ⚠ Bản thân reference là 8 bytes (64-bit), nằm trên stack; object thực mới ở heap.
  • Return address — địa chỉ instruction tiếp theo khi method return.

Mỗi lần gọi method, .NET push một stack frame (khung) chứa các thông tin trên. Khi method return, frame bị pop — toàn bộ biến local biến mất.

🔵 Ví dụ trực quan:
void Foo() {
    int a = 10;          // a lên stack
    int b = 20;          // b lên stack
    Bar();               // gọi Bar — push frame mới
    // sau Bar return, frame của Bar pop, a và b vẫn còn
}

void Bar() {
    int c = 30;          // c lên stack (frame của Bar)
    int d = 40;          // d lên stack
    // Bar return → c, d pop tự động, không cần GC
}
Khi Foo gọi Bar, stack có dạng:
┌──────────────┐
│ Frame Bar    │ ← top
│  c = 30      │
│  d = 40      │
│  return addr │
├──────────────┤
│ Frame Foo    │
│  a = 10      │
│  b = 20      │
└──────────────┘

1.3. Heap — vùng nhớ tự do được GC quản lý

🟢 Hình dung Heap như bàn làm việc lớn: đặt giấy tờ ở chỗ trống bất kỳ. Khi không cần nữa, có người dọn dẹp (Garbage Collector) đi qua nhặt giấy không ai dùng nữa.

Đặc điểm heap:

Tính chấtGiải thích
Cấu trúc tự doObject có thể nằm ở vị trí bất kỳ
Cả app dùng chung 1 (managed) heapMọi thread share heap, cần sync khi cấp phát parallel
Cấp phát chậm hơn stackPhải tìm chỗ trống, update bookkeeping
Cần Garbage CollectionObject không còn reference → bị GC thu hồi
Size lớn hơn nhiềuCó thể vài GB tuỳ RAM

Cái gì được lưu trên heap:

  • Object kiểu reference type (class, array, string, delegate…).
  • Boxed value type (sẽ giải thích).
  • Field của object (tất cả field instance đều nằm cùng object trên heap, kể cả field kiểu value type).
🟡 Heap được chia thành "generations" để GC tối ưu:
  • Gen 0: object mới sinh — GC quét rất thường xuyên (mỗi vài ms).
  • Gen 1: object sống sót Gen 0 — quét ít hơn.
  • Gen 2: object sống sót Gen 1 — quét hiếm.
  • Large Object Heap (LOH): object > 85 KB — quét cùng Gen 2.
Giả định: object sống lâu thì sẽ sống tiếp. Tối ưu hoá cho web app (request-scoped object chết nhanh ở Gen 0).

1.4. Vẽ memory cho ví dụ thực

class User {
    public int Id;
    public string Name;
}

void Run() {
    int x = 5;
    User u = new User { Id = 1, Name = "Alice" };
}

Khi Run đang chạy, bộ nhớ trông như sau:

STACK (frame của Run)             HEAP
┌────────────────────┐           ┌─────────────────────────┐
│ x = 5              │           │ User object             │
│ u = 0x7f3a1c00 ────┼─────────► │   Id = 1                │
│ (con trỏ heap)     │           │   Name = 0x7f3a2400 ────┼──┐
└────────────────────┘           │   (con trỏ tới string)  │  │
                                 └─────────────────────────┘  │
                                 ┌─────────────────────────┐  │
                                 │ string "Alice" ◄────────┼──┘
                                 └─────────────────────────┘

Quan sát:

  • x = 5 (int, value type) → giá trị thực nằm trên stack.
  • ucon trỏ trên stack, trỏ tới object trên heap.
  • Id = 1 → field này nằm bên trong User object (trên heap), không phải stack riêng.
  • Name = "Alice" → field Name lưu con trỏ trỏ tới chuỗi "Alice", chuỗi này nằm chỗ khác trên heap.
🟡 Hiểu lầm phổ biến — "Value type luôn ở stack."Sai. Đúng hơn: "Value type lưu giá trị trực tiếp chỗ nào nó được khai báo." Nếu int là field của class → int đó nằm trong class trên heap, không phải stack.
class Container { public int Value; }   // Value nằm trên heap (cùng Container)

void Foo() {
    int x = 5;                          // x trên stack
    Container c = new Container();      // c.Value trên heap
}

2. Value Type vs Reference Type

2.1. Định nghĩa kèm ví dụ

🟢
  • Value type: biến lưu giá trị thực. Khi gán hoặc truyền, giá trị được copy.
  • Reference type: biến lưu địa chỉ của object trên heap. Khi gán hoặc truyền, địa chỉ được copy — 2 biến cùng trỏ tới 1 object.
Đặc điểmValue typeReference type
Ví dụint, double, bool, char, struct, enum, DateTimeclass, string, array, delegate, interface
LưuGiá trị trực tiếpCon trỏ → object trên heap
Vị trí giá trị thựcStack (local), inline trong object (field)Heap
Copy khi gánCopy giá trịCopy địa chỉ
So sánh == mặc địnhSo sánh giá trịSo sánh địa chỉ (trừ string)
Giá trị mặc định0, false, '\0'null
Cho phép nullPhải dùng int? (Nullable)Có sẵn (string?)

2.2. Demo bằng code — copy bằng giá trị

int a = 5;
int b = a;       // copy giá trị 5 vào b
b = 10;          // chỉ b đổi, a giữ nguyên
Console.WriteLine(a);    // 5
Console.WriteLine(b);    // 10

Bộ nhớ:

Trước b = 10:           Sau b = 10:
a = 5                   a = 5
b = 5                   b = 10

2.3. Demo bằng code — copy reference

int[] arr1 = { 1, 2, 3 };
int[] arr2 = arr1;       // arr2 và arr1 trỏ CÙNG mảng
arr2[0] = 99;            // sửa qua arr2
Console.WriteLine(arr1[0]);    // 99 (cùng object!)

Bộ nhớ:

STACK                    HEAP
arr1 ──┐                 ┌───────────────┐
       ├──► [1, 2, 3] →  │ [99, 2, 3]    │
arr2 ──┘                 └───────────────┘

Cả 2 biến cùng trỏ tới 1 array → sửa qua biến nào cũng thấy.

2.4. Bẫy: truyền tham số

🔵 Khi truyền value type vào method, một copy được tạo. Sửa parameter KHÔNG ảnh hưởng biến gốc.Khi truyền reference type, copy là copy của con trỏ — vẫn trỏ cùng object. Sửa field của object SẼ thấy bên ngoài. Nhưng gán parameter sang object khác thì không thấy.
void ModifyInt(int x) { x = 100; }
void ModifyArr(int[] a) { a[0] = 100; }
void ReplaceArr(int[] a) { a = new int[] { 99 }; }   // gán parameter

int n = 5;
ModifyInt(n);
Console.WriteLine(n);          // 5 — không đổi (value type copy)

int[] arr = { 1, 2, 3 };
ModifyArr(arr);
Console.WriteLine(arr[0]);     // 100 — thay đổi (sửa qua reference cùng object)

ReplaceArr(arr);
Console.WriteLine(arr[0]);     // 100 — không phải 99! ReplaceArr chỉ đổi con trỏ local

Vì sao ReplaceArr không đổi arr bên ngoài?

Trước ReplaceArr:
caller: arr ──► [100, 2, 3]
              ▲
ReplaceArr:   a ┘    (cùng object)

Sau `a = new int[]{99}` bên trong ReplaceArr:
caller: arr ──► [100, 2, 3]
ReplaceArr:   a ──► [99]   (a giờ trỏ object mới, không động arr)

Để đổi được, dùng ref keyword:

void ReplaceArrRef(ref int[] a) { a = new int[] { 99 }; }
int[] arr = { 1, 2, 3 };
ReplaceArrRef(ref arr);
Console.WriteLine(arr[0]);     // 99 — vì ref truyền địa chỉ của con trỏ

2.5. string — reference nhưng hành xử như value

🟡 stringreference type nhưng immutable + operator == được overload → hành xử như value type. Đây là bẫy hay được hỏi.
string a = "hello";
string b = "hello";
Console.WriteLine(a == b);              // True — overloaded == so sánh nội dung
Console.WriteLine(ReferenceEquals(a, b)); // True — string interning!

String interning: compiler nhận thấy "hello" xuất hiện 2 lần → chỉ tạo 1 chuỗi duy nhất trên heap, cả ab cùng trỏ tới. Tiết kiệm RAM cho literal hay lặp lại.

string c = new string("hello".ToCharArray());     // ép tạo object mới
Console.WriteLine(a == c);                        // True — nội dung giống
Console.WriteLine(ReferenceEquals(a, c));         // False — 2 object khác nhau

3. Boxing & Unboxing — chi phí ngầm

3.1. Boxing là gì?

🟢 Boxing = đóng gói value type vào object trên heap.Unboxing = lấy giá trị nguyên thuỷ ra khỏi object, ép kiểu về value type ban đầu.
int x = 42;          // x là value type, trên stack
object o = x;        // BOXING: tạo object trên heap chứa giá trị 42, o trỏ tới object đó
int y = (int)o;      // UNBOXING: ép o về int, copy giá trị ra

Bộ nhớ:

STACK              HEAP
x = 42             ┌──────────────┐
o ───────────────► │ box: int 42  │
y = 42             └──────────────┘

3.2. Vì sao boxing tốn?

🟡 Mỗi lần boxing:
  1. Cấp phát object mới trên heap (~16 bytes overhead cho int).
  2. Copy giá trị từ stack vào object.
  3. Sau khi không dùng, object thành rác → GC phải dọn.
Trong vòng lặp lớn → tốn RAM + áp lực GC → chậm cả app.

Khi nào boxing xảy ra (ngầm)?

// 1. Gán value type vào object hoặc interface
object o = 42;
IComparable c = 42;

// 2. Truyền vào method nhận object
Console.WriteLine("x = " + 42);     // 42 boxed thành object để + với string

// 3. Lưu vào collection non-generic (legacy)
ArrayList list = new ArrayList();
list.Add(42);                        // boxing!
list.Add(true);                      // boxing!

// 4. Format string nhiều khi
string s = string.Format("x = {0}", 42);   // 42 boxed (less so với interpolation modern)

3.3. Tránh boxing — dùng Generic

🔵 Generic ra đời (C# 2.0) chính là để tránh boxing với value type.
// ❌ Boxing mỗi lần Add
ArrayList legacy = new ArrayList();
for (int i = 0; i < 1_000_000; i++) {
    legacy.Add(i);          // 1 triệu lần boxing!
}

// ✅ Không boxing
List<int> modern = new List<int>();
for (int i = 0; i < 1_000_000; i++) {
    modern.Add(i);          // không boxing — int lưu thẳng trong array
}

Benchmark thực tế: List<int> nhanh hơn ArrayList ~5–10× cho thao tác mass với int.

3.4. Câu trick — int? x = null; có boxing không?

🔴 Nullable<int> (int?) là struct — không boxing khi gán cho biến int? khác. NHƯNG khi gán sang object thì có hành vi đặc biệt:
int? a = null;
object o = a;             // o = NULL chứ không phải boxed Nullable<int>
Console.WriteLine(o == null);   // True

int? b = 42;
object o2 = b;            // o2 = boxed int 42 (NOT boxed Nullable<int>)
Console.WriteLine(o2.GetType());  // System.Int32
CLR có cơ chế đặc biệt — boxing Nullable<T>:
  • Nếu HasValue == false → box thành null.
  • Nếu HasValue == true → box thành T (không phải Nullable<T>).
Đây là edge case senior hay hỏi để check depth.

4. string immutable & StringBuilder

4.1. Immutable nghĩa là gì?

🟢 Immutable = không thể sửa sau khi tạo. Mọi thao tác "sửa" thực ra tạo chuỗi mới, chuỗi cũ thành rác chờ GC.
string s = "Hello";
s = s + " World";        // KHÔNG sửa "Hello" — tạo chuỗi mới "Hello World"
                         // s giờ trỏ tới chuỗi mới
                         // "Hello" cũ thành rác

Bộ nhớ qua từng bước:

Bước 1 — string s = "Hello":
STACK            HEAP
s ─────────────► "Hello"

Bước 2 — s = s + " World":
STACK            HEAP
s ─────────────► "Hello World"   (mới)
                 "Hello"           (rác)
                 " World"          (literal, có thể intern)

4.2. Vì sao đắt trong loop?

string result = "";
for (int i = 0; i < 10_000; i++) {
    result += i.ToString();
}

Mỗi vòng:

  1. Tạo chuỗi mới có độ dài tăng dần.
  2. Copy nội dung cũ + nội dung mới sang chuỗi mới.
  3. Chuỗi cũ thành rác.

Tổng: ~10000 lần cấp phát + copy. Độ phức tạp O(n²) về thời gian, áp lực GC khủng khiếp.

4.3. StringBuilder — buffer mutable

🔵 StringBuilder dùng char[] mutable bên trong. Append mở rộng array (gấp đôi khi đầy) — chi phí amortized O(1) / append.
var sb = new StringBuilder();
for (int i = 0; i < 10_000; i++) {
    sb.Append(i);
}
string result = sb.ToString();        // 1 lần tạo chuỗi cuối

Bộ nhớ:

STACK            HEAP
sb ────────────► StringBuilder { char[] buffer = [...mutable...] }
                 Khi đầy → cấp array mới gấp đôi, copy sang

Benchmark thực tế (concat 10000 int):

  • string +=: ~150 ms.
  • StringBuilder: ~1 ms.

4.4. Khi nào dùng string thường?

🔵 StringBuilder có overhead khởi tạo. Concat ≤ 5 chuỗi cố định thì string + operator thường nhanh hơn:
// ✅ OK — compiler tối ưu thành 1 string.Concat call
string greeting = "Hello, " + name + "! Today is " + day;

// ✅ Tốt nhất — interpolation gọn + JIT có thể tối ưu
string greeting = $"Hello, {name}! Today is {day}";
Quy tắc:
  • Loop hoặc số phần > 10 → StringBuilder.
  • Cố định < 10 → string + hoặc interpolation.

4.5. StringBuilder.Capacity vs Length — câu hỏi sâu

🟡
  • Length: số ký tự đang chứa.
  • Capacity: dung lượng đã cấp phát cho internal array.
Khi append vượt Capacity → cấp array mới ~2× Capacity cũ, copy sang. Khởi tạo với capacity ước tính sẵn giúp tránh re-allocation:
var sb = new StringBuilder(capacity: 1024);   // pre-alloc 1024 ký tự

5. So sánh: ==, Equals, ReferenceEquals

5.1. Ba cách so sánh

MethodMặc địnhOverride được?
== (operator)Reference (trừ string, được overload)Có (overload operator)
.Equals(obj)Reference (Object.Equals)Có (override)
ReferenceEquals(a, b)Luôn so sánh referenceKhông — sealed

5.2. Demo

class Point { public int X, Y; }

var p1 = new Point { X = 1, Y = 2 };
var p2 = new Point { X = 1, Y = 2 };
var p3 = p1;

p1 == p2;            // False — 2 object khác nhau, không override ==
p1 == p3;            // True  — cùng object
p1.Equals(p2);       // False — default Equals = reference
ReferenceEquals(p1, p2);   // False
ReferenceEquals(p1, p3);   // True

5.3. Override Equals đúng cách

🟡 Override Equals thì BẮT BUỘC override GetHashCode.Vì sao? Vì Dictionary/HashSet dùng hash để tìm bucket trước, sau đó dùng Equals để confirm. Nếu 2 object Equals nhưng hash khác → Dictionary mất hẳn.
class Point : IEquatable<Point> {
    public int X, Y;
    
    public override bool Equals(object? obj) => obj is Point p && Equals(p);
    public bool Equals(Point? other) => other != null && X == other.X && Y == other.Y;
    public override int GetHashCode() => HashCode.Combine(X, Y);
    
    public static bool operator ==(Point a, Point b) => a.Equals(b);
    public static bool operator !=(Point a, Point b) => !a.Equals(b);
}
🔵 C# 9+ có record — tự sinh Equals, GetHashCode, ToString, deconstruct với value equality.
public record Point(int X, int Y);

var p1 = new Point(1, 2);
var p2 = new Point(1, 2);
p1 == p2;           // True — record tự dùng value equality
Cho mọi DTO, data class — dùng record. Code ngắn hơn nhiều.

6. const vs readonly vs static readonly

6.1. Bảng so sánh

constreadonlystatic readonly
Khi set giá trịCompile-timeConstructor (mỗi instance)Static constructor (1 lần)
Kiểu được phépPrimitive (int, string, bool…) + nullMọi kiểuMọi kiểu
Mỗi instance khác nhau?❌ (1 cho cả class)
Compile vào nơi dùng?✅ (inline)
Đổi cần build module gọi?✅ Phải build lại

6.2. Ví dụ minh hoạ

public class Config {
    public const double Pi = 3.14159;                       // compile inline
    public readonly DateTime CreatedAt;                     // per-instance
    public static readonly string Version = LoadVersion();  // 1 lần khi class load
    
    public Config() {
        CreatedAt = DateTime.UtcNow;                        // mỗi new Config(), CreatedAt khác
    }
    
    private static string LoadVersion() => "1.0.0";
}

6.3. Bẫy const cross-assembly

🟡 const được inline vào nơi dùng — nếu Library.dllconst Pi = 3.14App.dll reference nó, App build sẽ có giá trị 3.14 nhúng trực tiếp vào IL.Nếu sau đó update Library thành Pi = 3.14159mà KHÔNG build lại App, App vẫn dùng 3.14 cũ → bug khó debug.→ Quy tắc: với giá trị có thể đổi giữa các bản release của library, dùng static readonly thay vì const.

7. Nullable & null-safety

7.1. int? thực chất là gì?

🟢 int?syntactic sugar cho Nullable<int> — một struct chứa:
  • bool HasValue
  • int Value
Khi null, HasValue = false, truy cập .Value ném InvalidOperationException.
int? age = null;
Console.WriteLine(age.HasValue);     // False
// Console.WriteLine(age.Value);     // throws

age = 25;
Console.WriteLine(age.HasValue);     // True
Console.WriteLine(age.Value);        // 25

// Cú pháp tắt
int safeAge = age ?? 0;              // ?? trả 0 nếu null

7.2. Nullable reference type (C# 8+)

Trước C# 8, mọi reference type ngầm cho phép null. Từ C# 8, khi bật <Nullable>enable</Nullable> trong csproj:

  • string = không null (compiler cảnh báo nếu gán null).
  • string? = có thể null.
string name = null;       // ⚠ Compiler warning: CS8600
string? maybeName = null; // OK

void Print(string s) {
    Console.WriteLine(s.Length);          // OK — compiler tin s không null
}

void PrintMaybe(string? s) {
    Console.WriteLine(s.Length);          // ⚠ Warning: s có thể null
    Console.WriteLine(s?.Length ?? 0);    // OK — null-safe
}

7.3. Operators thường dùng

🔵
OperatorTác dụngVí dụ
??Null-coalescing — trả vế phải nếu trái nullstring n = name ?? "guest";
??=Gán nếu đang nullname ??= "guest";
?.Null-conditional — trả null thay vì throwuser?.Address?.City
?[]Null-conditional indexerarr?[0]
!Null-forgiving (tell compiler "I know")Console.WriteLine(name!.Length);
string? GetName() => /* maybe null */;

// Chain an toàn
int len = GetName()?.Trim().Length ?? 0;

// Nếu chắc chắn không null (vd. vừa check) — dùng ! để tắt warning
if (name != null) {
    var upper = name!.ToUpper();   // hoặc compiler tự suy luận sau check
}

8. Exception — try/catch/finally

8.1. Cú pháp cơ bản

try {
    DoSomething();
}
catch (FileNotFoundException ex) {
    // Bắt cụ thể trước
    logger.Warn($"Missing file: {ex.FileName}");
    throw;        // rethrow — giữ stack trace gốc
}
catch (IOException ex) when (ex.HResult == -2147024864) {
    // Filter — chỉ catch nếu điều kiện đúng
    HandleSharingViolation();
}
catch (Exception ex) {
    // Catch-all — cuối cùng
    logger.Error(ex);
    throw new ApplicationException("Wrapped", ex);  // wrap với inner
}
finally {
    Cleanup();    // luôn chạy, kể cả exception hay return
}

8.2. throw vs throw ex — bẫy kinh điển

🔵 throw; rethrow exception giữ nguyên stack trace.throw ex;reset stack trace về dòng throw ex → mất thông tin nguồn gốc lỗi.→ Luôn dùng throw; trừ khi cố ý wrap exception.
try {
    File.ReadAllText("/missing.txt");
}
catch (FileNotFoundException ex) {
    // ❌ throw ex;  → stack trace chỉ tới dòng này, mất chỗ ReadAllText
    
    // ✅ Đúng
    throw;
    
    // ✅ Hoặc wrap có chủ ý — giữ inner exception
    throw new DataLoadException("Couldn't load config", ex);
}

8.3. using — giải phóng tài nguyên

🟢 usingsyntactic sugar cho try / finally { obj.Dispose(); } — đảm bảo Dispose() chạy kể cả có exception.
// Cú pháp block
using (var stream = new FileStream("a.txt", FileMode.Open)) {
    // ...
}   // tự gọi stream.Dispose() ở đây

// C# 8+ — using declaration (tắt block, scope theo enclosing)
void Read() {
    using var stream = new FileStream("a.txt", FileMode.Open);
    // ... 
}   // stream.Dispose() khi method return

Tương đương:

{
    var stream = new FileStream("a.txt", FileMode.Open);
    try { /* ... */ }
    finally { stream.Dispose(); }
}

8.4. Exception filter — C# 6+

🟡 catch (ExType ex) when (condition) — chỉ catch nếu condition true. Khác chỗ:
  • Stack trace của exception không bị unwind khi condition false → giữ context gốc.
  • Có thể log mà không catch:
catch (Exception ex) when (LogAndReturn(ex, false)) { }
// ↑ LogAndReturn luôn return false → không catch, nhưng đã log

9. Bài tập coding — học cách suy nghĩ, không chỉ code

9.1. FizzBuzz

Đề: in 1 đến 100. Số chia hết 3 → "Fizz". 5 → "Buzz". Cả 3 và 5 → "FizzBuzz".

Cách suy nghĩ:

  1. Check chia hết 15 trước (vì 15 = 3 × 5).
  2. Sau đó check 3, rồi 5.
  3. Nếu không, in số.
for (int i = 1; i <= 100; i++) {
    if (i % 15 == 0) Console.WriteLine("FizzBuzz");
    else if (i % 3 == 0) Console.WriteLine("Fizz");
    else if (i % 5 == 0) Console.WriteLine("Buzz");
    else Console.WriteLine(i);
}
🔵 Bẫy fresher hay mắc: check 3 trước, rồi 5, không check 15 → số 15 chỉ in "Fizz". Interviewer sẽ thử số 15, 30.

9.2. Đảo chuỗi

Cách 1 — dùng Array.Reverse:

public static string Reverse(string s) {
    var arr = s.ToCharArray();
    Array.Reverse(arr);
    return new string(arr);
}

Cách 2 — two pointer (tự viết):

public static string ReverseManual(string s) {
    var arr = s.ToCharArray();
    int l = 0, r = arr.Length - 1;
    while (l < r) {
        (arr[l], arr[r]) = (arr[r], arr[l]);   // swap qua tuple
        l++;
        r--;
    }
    return new string(arr);
}

Complexity: O(n) thời gian, O(n) space (cho mảng char + chuỗi mới).

9.3. Đếm tần suất ký tự

public static Dictionary<char, int> CountChars(string s) {
    var dict = new Dictionary<char, int>();
    foreach (var c in s) {
        if (dict.ContainsKey(c)) dict[c]++;
        else dict[c] = 1;
    }
    return dict;
}

Hoặc gọn hơn với GetValueOrDefault:

public static Dictionary<char, int> CountChars(string s) {
    var dict = new Dictionary<char, int>();
    foreach (var c in s) {
        dict[c] = dict.GetValueOrDefault(c) + 1;
    }
    return dict;
}

Complexity: O(n) thời gian, O(k) space (k = số ký tự distinct).

9.4. Kiểm tra palindrome

public static bool IsPalindrome(string s) {
    int l = 0, r = s.Length - 1;
    while (l < r) {
        if (char.ToLower(s[l]) != char.ToLower(s[r])) return false;
        l++;
        r--;
    }
    return true;
}

Tại sao two pointer? Vì palindrome đối xứng — so sánh ký tự đối xứng từ 2 đầu vào giữa.

Complexity: O(n) thời gian, O(1) space.


10. Câu hỏi tự test cuối ngày

Level 🟢 Intern:
  1. Stack và Heap khác nhau ra sao? Lưu cái gì?
  2. Value type và Reference type — định nghĩa + ví dụ.
  3. nullundefined — C# có cả hai không? (Trả lời: chỉ null.)
Level 🔵 Junior: 4. Boxing/unboxing là gì? Khi nào xảy ra ngầm? 5. Vì sao string += trong loop chậm? Fix sao? 6. throwthrow ex khác nhau? Vì sao quan trọng? 7. const, readonly, static readonly — chọn cho Pi là gì? Cho DateTime.UtcNow?Level 🟡 Mid: 8. Override Equals thì cần override gì nữa? Vì sao? 9. Vẽ memory cho: int x = 5; var u = new User { Name = "A" };. 10. int? x = null; object o = x;o là gì? Vì sao?Level 🔴 Senior: 11. .NET GC dùng generation nào? Vì sao chia generation? 12. LOH (Large Object Heap) khác Gen 0/1/2 ra sao? 13. Trong record, equality dùng cơ chế gì để generate? 14. Exception filter when khác bình thường ra sao về stack trace?

Đáp án: xem Ngân hàng câu hỏi C#.


➡️ Sang ngày mai: Ngày 2 — OOP & SOLID

© 2026 .NET Fresher Guide. All rights reserved.

Về Trang Web

Hướng dẫn toàn diện để chuẩn bị phỏng vấn vị trí .NET Fresher với nội dung từ lý thuyết đến thực hành.