Ngày 1 — C# cơ bản
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.
stringimmutable 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.
- 🟢 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?
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"
Đặc điểm stack:
| Tính chất | Giải thích |
|---|---|
| Cấu trúc LIFO | Vào sau ra trước — như chồng đĩa |
| Mỗi thread có 1 stack riêng | Stack default 1 MB / thread trên Windows, 8 MB trên Linux |
| Cấp phát / giải phóng cực nhanh | Chỉ là tăng/giảm 1 con trỏ |
| Không cần GC | Hế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.
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
}
┌──────────────┐
│ 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ý
Đặc điểm heap:
| Tính chất | Giải thích |
|---|---|
| Cấu trúc tự do | Object có thể nằm ở vị trí bất kỳ |
| Cả app dùng chung 1 (managed) heap | Mọi thread share heap, cần sync khi cấp phát parallel |
| Cấp phát chậm hơn stack | Phải tìm chỗ trống, update bookkeeping |
| Cần Garbage Collection | Object không còn reference → bị GC thu hồi |
| Size lớn hơn nhiều | Có 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).
- 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.
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.u→ con 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.
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ểm | Value type | Reference type |
|---|---|---|
| Ví dụ | int, double, bool, char, struct, enum, DateTime | class, string, array, delegate, interface |
| Lưu | Giá trị trực tiếp | Con trỏ → object trên heap |
| Vị trí giá trị thực | Stack (local), inline trong object (field) | Heap |
| Copy khi gán | Copy giá trị | Copy địa chỉ |
So sánh == mặc định | So sánh giá trị | So sánh địa chỉ (trừ string) |
| Giá trị mặc định | 0, false, '\0' | null |
| Cho phép null | Phả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ố
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
string là reference 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ả a và b 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ì?
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?
- Cấp phát object mới trên heap (~16 bytes overhead cho int).
- Copy giá trị từ stack vào object.
- Sau khi không dùng, object thành rác → GC phải dọn.
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
// ❌ 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
Nullable<T>:- Nếu
HasValue == false→ box thànhnull. - Nếu
HasValue == true→ box thànhT(không phảiNullable<T>).
4. string immutable & StringBuilder
4.1. Immutable nghĩa là gì?
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:
- Tạo chuỗi mới có độ dài tăng dần.
- Copy nội dung cũ + nội dung mới sang chuỗi mới.
- 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?
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}";
- 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.
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
| Method | Mặc định | Override đượ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 reference | Khô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
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);
}
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
6. const vs readonly vs static readonly
6.1. Bảng so sánh
const | readonly | static readonly | |
|---|---|---|---|
| Khi set giá trị | Compile-time | Constructor (mỗi instance) | Static constructor (1 lần) |
| Kiểu được phép | Primitive (int, string, bool…) + null | Mọi kiểu | Mọ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.dll có const Pi = 3.14 và App.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? là syntactic sugar cho Nullable<int> — một struct chứa:bool HasValueint Value
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
| Operator | Tác dụng | Ví dụ |
|---|---|---|
?? | Null-coalescing — trả vế phải nếu trái null | string n = name ?? "guest"; |
??= | Gán nếu đang null | name ??= "guest"; |
?. | Null-conditional — trả null thay vì throw | user?.Address?.City |
?[] | Null-conditional indexer | arr?[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
using là syntactic 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ĩ:
- Check chia hết 15 trước (vì 15 = 3 × 5).
- Sau đó check 3, rồi 5.
- 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);
}
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
- Stack và Heap khác nhau ra sao? Lưu cái gì?
- Value type và Reference type — định nghĩa + ví dụ.
nullvàundefined— C# có cả hai không? (Trả lời: chỉnull.)
string += trong loop chậm? Fix sao?
6. throw và throw 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#.