# C# Type System: Value Type, Reference Type, Stack, Heap và Boxing
Khi mới học C#, chúng ta thường được giải thích:
> Value Type nằm trên Stack, Reference Type nằm trên Heap.
Cách giải thích này dễ nhớ nhưng không hoàn toàn chính xác.
Để hiểu C# tốt hơn, cần tách hai vấn đề:
“`text
Value Type / Reference Type
→ semantics của type
Stack / Heap
→ cách runtime quản lý memory
“`
Bài viết này đi từ C# Type System đến những khái niệm memory cơ bản như Stack Frame, Managed Heap, Boxing và Garbage Collector.
## 1. Value Type là gì?
Ví dụ:
“`csharp
int a = 10;
int b = a;
b = 20;
Console.WriteLine(a); // 10
Console.WriteLine(b); // 20
“`
Khi:
“`csharp
int b = a;
“`
giá trị của `a` được copy sang `b`.
Có thể hình dung:
“`text
a
└── 10
b
└── 10
“`
Sau:
“`csharp
b = 20;
“`
ta có:
“`text
a
└── 10
b
└── 20
“`
Hai biến độc lập với nhau.
Một số Value Type phổ biến:
“`text
int
long
double
decimal
bool
char
enum
struct
“`
Điểm quan trọng nhất:
> Value Type có value semantics. Assignment sẽ copy giá trị.
## 2. Reference Type là gì?
Ví dụ:
“`csharp
public class User
{
public string Name { get; set; } = “”;
}
“`
Sau đó:
“`csharp
var user1 = new User
{
Name = “Vu”
};
var user2 = user1;
user2.Name = “David”;
Console.WriteLine(user1.Name);
“`
Kết quả:
“`text
David
“`
Tại sao?
Vì:
“`csharp
var user2 = user1;
“`
không copy toàn bộ `User`.
Nó copy reference.
Mental model:
“`text
user1 ─────┐
↓
User Object
Name = “Vu”
↑
user2 ─────┘
“`
Khi:
“`csharp
user2.Name = “David”;
“`
object thật được thay đổi.
Do đó cả `user1` và `user2` đều quan sát cùng một giá trị.
Điểm cần nhớ:
> Reference Type có reference semantics. Assignment copy reference, không copy object.
## 3. Value Type có luôn nằm trên Stack không?
Không.
Đây là một trong những hiểu nhầm phổ biến nhất.
Không nên định nghĩa:
“`text
Value Type = Stack
Reference Type = Heap
“`
Ví dụ:
“`csharp
public class Order
{
public int Quantity { get; set; }
}
“`
`Quantity` là `int`, tức Value Type.
Nhưng:
“`csharp
var order = new Order();
“`
`Order` object thường nằm trên Managed Heap.
`Quantity` nằm inline bên trong object đó.
Mental model:
“`text
Managed Heap
┌─────────────────────┐
│ Order │
│ │
│ Quantity = 0 │ ← int
│ │
└─────────────────────┘
“`
Như vậy Value Type hoàn toàn có thể tồn tại trong một object trên Heap.
Do đó nên nhớ:
“`text
Value Type
→ value semantics
Reference Type
→ reference semantics
“`
Stack và Heap là vấn đề khác.
## 4. `var` là gì?
Ví dụ:
“`csharp
var age = 30;
“`
Compiler nhìn expression bên phải và suy ra:
“`csharp
int age = 30;
“`
Do đó `var` không phải một kiểu đặc biệt.
Ví dụ:
“`csharp
var user = new User();
“`
Compiler hiểu:
“`csharp
User user = new User();
“`
Compile-time type của `user` là:
“`text
User
“`
Vì vậy:
“`csharp
user.Name = “Vu”;
“`
hoàn toàn hợp lệ.
Điểm quan trọng:
> `var` vẫn là static typing. Compiler chỉ giúp chúng ta suy ra type.
## 5. `object` khác `var` thế nào?
Ví dụ:
“`csharp
object user = new User();
“`
Lúc này:
“`text
Compile-time type = object
Runtime type = User
“`
Object thực tế vẫn là `User`.
Nhưng compiler đang nhìn biến `user` như một `object`.
Do đó:
“`csharp
user.ToString();
“`
hợp lệ vì `ToString()` thuộc `object`.
Nhưng:
“`csharp
user.Login();
“`
sẽ compile error nếu `Login()` chỉ tồn tại trên `User`.
Muốn sử dụng:
“`csharp
if (user is User actualUser)
{
actualUser.Login();
}
“`
Có thể nhớ:
“`text
var user = new User()
Compile-time = User
Runtime = User
“`
Trong khi:
“`text
object user = new User()
Compile-time = object
Runtime = User
“`
## 6. `dynamic` khác gì?
Ví dụ:
“`csharp
dynamic user = new User();
user.Login();
“`
Với `dynamic`, việc resolve member được trì hoãn đến runtime.
Thậm chí:
“`csharp
dynamic user = new User();
user.MethodDoesNotExist();
“`
có thể compile được.
Nhưng runtime sẽ throw exception.
Tóm tắt:
| Declaration | Compiler nhìn type như |
|—|—|
| `var x = new User()` | `User` |
| `User x = new User()` | `User` |
| `object x = new User()` | `object` |
| `dynamic x = new User()` | `dynamic` |
Có thể nhớ:
“`text
var
→ compile-time type inference
object
→ static type = object
dynamic
→ runtime binding
“`
## 7. Boxing là gì?
Boxing xảy ra khi Value Type được chuyển thành `object` hoặc một interface phù hợp.
Ví dụ:
“`csharp
int number = 10;
object obj = number;
“`
Ở đây:
“`text
int
↓
boxing
↓
object representation
“`
Runtime cần tạo một boxed representation của giá trị `10`.
Việc này thường liên quan đến Heap Allocation.
## 8. Reference Type → object có phải Boxing không?
Không.
Ví dụ:
“`csharp
User user = new User();
object obj = user;
“`
Đây không phải boxing.
Đây là implicit reference conversion.
Mental model:
“`text
user ────────┐
↓
User Object
↑
obj ─────────┘
“`
Không có một `User` object mới chỉ vì:
“`csharp
object obj = user;
“`
Do đó:
“`text
Value Type → object
= Boxing
Reference Type → object
= Reference Conversion
“`
## 9. Unboxing là gì?
Ngược lại với boxing:
“`csharp
object obj = 10;
int number = (int)obj;
“`
Đây là unboxing.
Một điều quan trọng là phải unbox đúng type.
Ví dụ:
“`csharp
object obj = 10;
long number = (long)obj;
“`
sẽ fail runtime.
Vì object đang chứa boxed:
“`text
int
“`
không phải:
“`text
long
“`
Nếu muốn convert:
“`csharp
long number = Convert.ToInt64(obj);
“`
thì đây là conversion, không đơn thuần là unboxing.
## 10. Nullable Value Type
Thông thường:
“`csharp
int age = null;
“`
không hợp lệ.
Muốn cho phép `null`:
“`csharp
int? age = null;
“`
`int?` tương ứng với:
“`csharp
Nullable
“`
Ví dụ:
“`csharp
if (age.HasValue)
{
Console.WriteLine(age.Value);
}
“`
Hoặc:
“`csharp
int actualAge = age ?? 0;
“`
## 11. Nullable Reference Type
Ví dụ:
“`csharp
string? name = null;
“`
Không nên hiểu:
“`text
string? = Nullable
“`
Điều đó sai.
Nullable<
Nullable Reference Types chủ yếu cung cấp thông tin cho compiler để phân tích khả năng `null`.
Ví dụ:
“`csharp
string name = null;
“`
compiler có thể cảnh báo.
Trong khi:
“`csharp
string? name = null;
“`
developer đang nói rõ:
> Biến này được phép nhận `null`.
## 12. Implicit Conversion
Ví dụ:
“`csharp
int number = 10;
long bigNumber = number;
“`
Compiler cho phép conversion tự động.
Lý do là conversion này về cơ bản không làm mất range của `int`.
## 13. Explicit Conversion
Ngược lại:
“`csharp
long number = 100;
int value = (int)number;
“`
phải explicit cast.
Vì `long` có range lớn hơn `int`.
Có nguy cơ mất dữ liệu.
Có thể sử dụng:
“`csharp
checked
{
int value = (int)number;
}
“`
để phát hiện overflow.
## 14. Stack là gì?
Mỗi thread có call stack phục vụ quá trình thực thi method.
Ví dụ:
“`csharp
Main()
{
GetUser();
}
“`
`GetUser()` lại gọi:
“`csharp
GetAddress();
“`
Có thể hình dung:
“`text
Call Stack
┌───────────────────┐
│ GetAddress() │
├───────────────────┤
│ GetUser() │
├───────────────────┤
│ Main() │
└───────────────────┘
“`
## 15. Stack Frame là gì?
Mỗi lần method được gọi sẽ có một execution context thường được mô hình hóa thành một Stack Frame.
Ví dụ:
“`csharp
void Calculate(int x)
{
int y = 20;
Console.WriteLine(x + y);
}
“`
Mental model:
“`text
Calculate Stack Frame
x = 10
y = 20
return information
temporary execution state
…
“`
Stack Frame có thể chứa:
“`text
parameters
local execution state
return information
temporary values
runtime bookkeeping
“`
JIT có thể giữ một số giá trị trong CPU register hoặc optimize chúng đi, nên đây là mental model chứ không phải layout tuyệt đối.
## 16. Khi method kết thúc thì Stack Frame sao?
Ví dụ:
“`csharp
void Test()
{
int x = 10;
}
“`
Khi method return:
“`text
Test Stack Frame
↓
pop
“`
Memory của stack frame được thu hồi rất rẻ.
Không cần Garbage Collector xử lý việc này.
## 17. Heap Allocation là gì?
Ví dụ:
“`csharp
var user = new User();
“`
`User` object thường được allocate trên Managed Heap.
Mental model:
“`text
Stack Frame
user
│
│ reference
▼
Managed Heap
┌───────────────┐
│ User Object │
└───────────────┘
“`
Runtime dành một vùng nhớ trên Managed Heap để chứa object.
Đó gọi là Heap Allocation.
## 18. Managed Heap do ai quản lý?
Managed Heap được quản lý bởi:
“`text
Garbage Collector
“`
hay:
“`text
GC
“`
Lifecycle đơn giản:
“`text
new User()
↓
Heap Allocation
↓
Object được sử dụng
↓
Không còn reachable
↓
Eligible for GC
↓
GC reclaim memory
“`
Điểm quan trọng:
> Object không bị xóa ngay khi một method kết thúc.
## 19. Method kết thúc nhưng object vẫn có thể sống
Ví dụ:
“`csharp
User CreateUser()
{
var user = new User();
return user;
}
“`
Trong `CreateUser()`:
“`text
Stack
user ───────→ User Object
↑
Heap
“`
Sau khi method return:
“`text
CreateUser Stack Frame
→ bị pop
“`
Nhưng reference được trả về caller.
Ví dụ:
“`csharp
var user = CreateUser();
“`
Bây giờ:
“`text
Caller Stack
user ───────→ User Object
“`
Object vẫn còn reachable.
Do đó GC không collect nó.
## 20. GC sử dụng Reachability
GC không hỏi:
> Method tạo object đã kết thúc chưa?
GC quan tâm:
> Object còn reachable từ GC Roots hay không?
Mental model:
“`text
GC Root
↓
User
↓
Address
↓
City
“`
Nếu vẫn tồn tại đường reference từ root đến object thì object vẫn được xem là sống.
Một số GC Root có thể đến từ:
“`text
active stack references
static references
runtime handles
CPU registers
“`
## 21. Gen 0, Gen 1, Gen 2
Managed GC sử dụng concept generations.
Mental model:
“`text
new object
↓
Gen 0
↓ survives
Gen 1
↓ survives
Gen 2
“`
Ý tưởng phía sau:
> Phần lớn object có lifetime khá ngắn.
Ví dụ trong một Web API request:
“`csharp
var dto = new UserDto();
var items = new List
var response = new Response();
“`
nhiều object chỉ tồn tại trong thời gian ngắn.
Vì vậy GC có thể tập trung collect các object trẻ trước.
## Stack và Heap khác nhau thế nào?
Một mental model hữu ích:
| Stack | Managed Heap |
|—|—|
| Phục vụ execution | Chứa object allocations |
| Có Stack Frame | Có managed objects |
| Lifetime thường gắn với method call | Lifetime dựa vào reachability |
| Frame pop khi method return | GC reclaim object |
| Allocation/deallocation rất rẻ | GC quản lý |
Nhưng không được suy ra:
“`text
Value Type = Stack
Reference Type = Heap
“`
Đây là hai khái niệm khác nhau.
## Ví dụ tổng hợp
“`csharp
static User CreateUser(int age)
{
var user = new User();
user.Age = age;
return user;
}
“`
Khi method đang chạy:
“`text
Call Stack
┌────────────────────────┐
│ CreateUser │
│ │
│ age = 30 │
│ user ───────────────────────────┐
└────────────────────────┘ │
│
▼
Managed Heap
┌────────────────┐
│ User │
│ Age = 30 │
└────────────────┘
“`
Khi `CreateUser()` return:
“`text
CreateUser frame
→ pop
“`
nhưng object `User` vẫn có thể sống vì caller nhận được reference.
## Những hiểu nhầm phổ biến
### Value Type luôn nằm Stack
Sai.
Value Type có thể nằm inline bên trong object trên Heap hoặc được boxed.
### Reference Type nghĩa là biến nằm Heap
Không chính xác.
Object thường nằm trên Managed Heap.
Variable chứa reference có thể là local execution state trong stack/register hoặc ở vị trí khác.
### `var` là dynamic
Sai.
“`csharp
var x = 10;
“`
compiler xác định:
“`text
x : int
“`
ngay tại compile time.
### `object user = new User()` là boxing
Sai.
Đây là reference conversion.
Boxing áp dụng cho Value Type:
“`csharp
int x = 10;
object obj = x;
“`
### Method kết thúc thì Heap Object bị xóa
Sai.
Object chỉ có thể được collect khi không còn reachable.
## Interview Questions
### 1. Value Type khác Reference Type thế nào?
Value Type có value semantics và assignment copy giá trị.
Reference Type có reference semantics và assignment copy reference.
### 2. Value Type có luôn nằm Stack không?
Không.
Memory location phụ thuộc execution/runtime context.
### 3. `var` có phải dynamic không?
Không.
`var` chỉ là compile-time type inference.
### 4. `object x = 10` xảy ra chuyện gì?
`10` là `int`, một Value Type.
Gán nó cho `object` gây boxing.
### 5. `object user = new User()` có boxing không?
Không.
Đây là implicit reference conversion.
### 6. Boxing có thể tạo Heap Allocation không?
Có.
Runtime cần boxed object representation cho Value Type.
### 7. Stack Frame là gì?
Là execution context của một method invocation.
### 8. Stack Frame có được GC thu hồi không?
Không.
Khi method return, frame được pop khỏi stack.
### 9. Heap Object sống bao lâu?
Cho đến khi nó không còn reachable và GC reclaim memory.
### 10. `var` và `object` khác nhau thế nào?
“`csharp
var user = new User();
“`
compiler biết type là `User`.
Trong khi:
“`csharp
object user = new User();
“`
compiler chỉ nhìn biến như `object`, dù runtime object vẫn là `User`.
## Key Takeaways
Các điểm quan trọng nhất cần nhớ:
“`text
1. Value Type / Reference Type nói về semantics.
2. Stack / Heap nói về memory/runtime behavior.
3. Value Type không đồng nghĩa Stack.
4. var là compile-time type inference.
5. object giữ compile-time type là object.
6. dynamic chuyển member binding sang runtime.
7. Value Type → object là Boxing.
8. Reference Type → object không phải Boxing.
9. Stack Frame gắn với một lần gọi method.
10. Managed Heap được Garbage Collector quản lý.
11. Heap Object sống dựa trên reachability.
12. Method return không đồng nghĩa object bị GC ngay.
“`
## Bài thực hành
Trước khi chạy code, hãy tự đoán kết quả.
“`csharp
int a = 10;
int b = a;
b = 20;
Console.WriteLine(a);
Console.WriteLine(b);
“`
Tiếp theo:
“`csharp
var user1 = new User
{
Name = “Vu”
};
var user2 = user1;
user2.Name = “David”;
Console.WriteLine(user1.Name);
“`
Sau đó giải thích:
“`text
Tại sao int copy khác User assignment?
“`
Cuối cùng:
“`csharp
var a = new User();
object b = new User();
dynamic c = new User();
“`
Hãy xác định:
“`text
compile-time type của a?
compile-time type của b?
runtime type của b?
dynamic khác hai cái trên ở đâu?
“`
Nếu có thể tự giải thích toàn bộ mà không nhìn lại bài viết, phần C# Type System cơ bản đã khá chắc.
—
**Bài tiếp theo:**
> **C# Class vs Struct vs Record: khác nhau về semantics, equality và khi nào nên sử dụng?**
Sau đó có thể đi tiếp:
“`text
Class vs Struct vs Record
↓
== vs Equals()
↓
ReferenceEquals()
↓
GetHashCode()
↓
IEquatable
“`
Đây sẽ là chuỗi tiếp theo trong series **C# Foundation**.