# C# String Concatenation: `+=` khác gì `StringBuilder` và khi nào nên dùng?
Khi làm việc với chuỗi trong C#, chúng ta thường gặp hai cách phổ biến để nối chuỗi:
“`csharp
result += value;
“`
và:
“`csharp
StringBuilder builder = new StringBuilder();
builder.Append(value);
“`
Nhìn bên ngoài, cả hai đều tạo ra một chuỗi cuối cùng. Tuy nhiên, cách chúng hoạt động bên trong hoàn toàn khác nhau.
Hiểu được sự khác biệt này rất quan trọng khi viết code hiệu quả, đặc biệt khi xử lý vòng lặp, generate text lớn hoặc khi đi phỏng vấn .NET.
—
## 1. `string` trong C# là immutable
Điểm quan trọng nhất cần nhớ:
> `string` trong C# là immutable.
Immutable có nghĩa là sau khi một object `string` được tạo ra, nội dung của nó không thể thay đổi trực tiếp.
Ví dụ:
“`csharp
string text = “Hello”;
text += ” World”;
“`
Nhìn vào code, chúng ta có cảm giác chuỗi `”Hello”` được sửa thành `”Hello World”`.
Nhưng thực tế không phải vậy.
Runtime phải tạo một string mới:
“`text
“Hello”
|
| concatenate ” World”
v
“Hello World”
“`
Sau đó biến `text` được trỏ tới object mới.
Object `”Hello”` cũ không bị sửa đổi.
Nếu không còn reference nào tới nó thì sau này Garbage Collector sẽ thu hồi bộ nhớ.
—
## 2. Điều gì xảy ra khi dùng `+=` trong loop?
Giả sử chúng ta có đoạn code:
“`csharp
string result = “”;
for (int i = 0; i < 5; i++) { result += i; } ``` Kết quả cuối cùng: ```text 01234 ``` Nhưng bên trong, quá trình có thể diễn ra như sau: ```text "" "0" "01" "012" "0123" "01234" ``` Mỗi lần thực hiện: ```csharp result += i; ``` runtime phải tạo một chuỗi mới. Có thể hình dung như sau: ```text result = "0123" + "4" ↓ new string = "01234" ``` Sau đó: ```text result ↓ "01234" ``` Chuỗi `"0123"` cũ trở thành object không còn cần thiết. Nếu vòng lặp chạy hàng nghìn hoặc hàng triệu lần, sẽ có rất nhiều object tạm thời được tạo ra. --- # 3. Vấn đề về performance Giả sử: ```csharp string result = ""; for (int i = 0; i < 100000; i++) { result += i; } ``` Mỗi vòng lặp có thể cần: 1. Allocate một vùng nhớ mới. 2. Copy nội dung string cũ. 3. Append dữ liệu mới. 4. Tạo object string mới. 5. Để object cũ chờ Garbage Collector xử lý. Điều này tạo ra hai vấn đề lớn: ```text More allocations + More copying + More work for GC ``` Khi dữ liệu lớn, performance có thể giảm đáng kể. --- # 4. StringBuilder hoạt động như thế nào? `StringBuilder` được thiết kế để giải quyết vấn đề này. Ví dụ: ```csharp var builder = new StringBuilder(); for (int i = 0; i < 100000; i++) { builder.Append(i); } string result = builder.ToString(); ``` Thay vì tạo string mới sau mỗi lần nối, `StringBuilder` duy trì vùng nhớ có thể mở rộng. Có thể hình dung: ```text Buffer [0][1][2][3][4][5][6][7]... ↑ append tiếp vào đây ``` Khi gọi: ```csharp builder.Append(value); ``` dữ liệu được thêm vào buffer hiện tại. Không cần tạo một object `string` mới cho mỗi lần append. Chỉ đến cuối cùng: ```csharp builder.ToString(); ``` một `string` hoàn chỉnh mới được tạo ra. --- # 5. So sánh `+=` và `StringBuilder` | Đặc điểm | `string +=` | `StringBuilder` | |---|---|---| | Mutable | Không | Có | | Tạo string mới khi nối | Thường có | Không ở mỗi lần Append | | Phù hợp với vài phép nối | Rất tốt | Không cần thiết | | Phù hợp với loop lớn | Không tối ưu | Tốt | | Memory allocation | Có thể nhiều | Ít hơn | | Code đơn giản | Rất đơn giản | Dài hơn | | Generate text lớn | Không lý tưởng | Phù hợp | --- # 6. Khi nào nên dùng `+` hoặc `+=`? Không phải lúc nào cũng cần `StringBuilder`. Ví dụ: ```csharp string fullName = firstName + " " + lastName; ``` Đây là cách hoàn toàn bình thường. Hoặc: ```csharp string message = "Hello " + userName; ``` Không có lý do gì để viết: ```csharp var builder = new StringBuilder(); builder.Append("Hello "); builder.Append(userName); string message = builder.ToString(); ``` Code dài hơn mà gần như không mang lại lợi ích thực tế. --- # 7. String interpolation cũng rất phù hợp cho chuỗi nhỏ Trong C#, cách dễ đọc hơn thường là: ```csharp string message = $"Hello {userName}"; ``` Hoặc: ```csharp string fullName = $"{firstName} {lastName}"; ``` Đối với một số lượng nhỏ các giá trị cần ghép, string interpolation thường là lựa chọn tốt nhất về readability. --- # 8. Khi nào nên dùng StringBuilder? `StringBuilder` phù hợp nhất khi chúng ta append nhiều lần. Ví dụ: ```csharp var builder = new StringBuilder(); foreach (var user in users) { builder.Append(user.Name); builder.Append(","); } string result = builder.ToString(); ``` Các use case phổ biến bao gồm: - Loop qua collection lớn. - Generate HTML. - Generate report. - Generate CSV. - Generate log text lớn. - Build dynamic text. - Build query hoặc command text phức tạp. - Xử lý một lượng lớn dữ liệu text. --- # 9. Ví dụ thực tế trong vòng lặp Giả sử chúng ta cần nối các action: ```csharp var actions = new[] { "Create", "Update", "Delete" }; ``` Cách thứ nhất: ```csharp string result = ""; foreach (var action in actions) { result += action; } ``` Kết quả: ```text CreateUpdateDelete ``` Đối với chỉ ba phần tử thì không có vấn đề gì đáng kể. Nhưng nếu collection có: ```text 100,000 items ``` thì việc sử dụng `StringBuilder` hợp lý hơn: ```csharp var builder = new StringBuilder(); foreach (var action in actions) { builder.Append(action); } string result = builder.ToString(); ``` --- # 10. Có phải `+=` lúc nào cũng chậm? Không. Đây là một điểm quan trọng khi trả lời phỏng vấn. Ví dụ: ```csharp string result = a + b + c; ``` Compiler và runtime có thể tối ưu phép nối này khá tốt. Vì vậy không nên kết luận: > Never use `+` for strings.
Đó là một nhận định quá cực đoan.
Cách hiểu chính xác hơn là:
> `StringBuilder` đặc biệt hữu ích khi thực hiện repeated concatenation, đặc biệt trong loop hoặc khi không biết trước số lần nối.
—
# 11. Big O và lý do StringBuilder nhanh hơn
Giả sử ta liên tục append vào một chuỗi:
“`text
A
AB
ABC
ABCD
ABCDE
…
“`
Với string immutable, mỗi lần nối runtime phải copy lại phần dữ liệu cũ.
Ví dụ:
“`text
1 character copied
2 characters copied
3 characters copied
4 characters copied
…
n characters copied
“`
Tổng lượng dữ liệu phải copy xấp xỉ:
“`text
1 + 2 + 3 + … + n
“`
Tức là:
“`text
O(n²)
“`
Trong nhiều tình huống repeated concatenation.
Trong khi đó, `StringBuilder` sử dụng buffer và mở rộng capacity theo từng giai đoạn nên tổng chi phí thường gần hơn với:
“`text
O(n)
“`
Điều này tạo khác biệt lớn khi xử lý lượng dữ liệu lớn.
—
# 12. Capacity trong StringBuilder
`StringBuilder` có khái niệm `Capacity`.
Ví dụ:
“`csharp
var builder = new StringBuilder(1000);
“`
Nghĩa là ban đầu chúng ta cấp buffer có khả năng chứa khoảng 1000 ký tự.
Nếu biết trước kích thước tương đối của dữ liệu, việc chỉ định capacity có thể giảm số lần buffer phải resize.
Ví dụ:
“`csharp
var builder = new StringBuilder(users.Count * 20);
“`
Tuy nhiên, không nên cố gắng tính toán capacity quá chi tiết nếu không cần thiết.
—
# 13. Một lỗi phổ biến khi phỏng vấn
Interviewer hỏi:
> Trong loop, em sẽ dùng `+=` hay `StringBuilder`?
Một câu trả lời yếu là:
> StringBuilder vì nó nhanh hơn.
Câu trả lời tốt hơn:
> I would normally use StringBuilder when the loop performs repeated string concatenation, especially if the number of iterations can be large. Strings in C# are immutable, so repeated `+=` operations may create many intermediate string objects and additional allocations. StringBuilder uses a mutable buffer, which reduces those allocations.
Điểm quan trọng là giải thích **tại sao**.
—
# 14. Interview question: String immutable nghĩa là gì?
Một câu hỏi phổ biến:
> Why is string immutable in C#?
Có thể trả lời:
> Once a string object is created, its content cannot be modified. Operations that appear to modify a string actually create a new string object.
Ví dụ:
“`csharp
string x = “Hello”;
x += ” World”;
“`
Không sửa object `”Hello”`.
Mà tạo:
“`text
“Hello World”
“`
rồi gán reference mới cho `x`.
—
# 15. Interview question: Vậy tại sao C# thiết kế string immutable?
String immutable mang lại nhiều lợi ích:
### Thread safety
Một string không bị thay đổi sau khi được tạo.
Điều này làm việc chia sẻ string giữa nhiều thread an toàn hơn.
### Hashing
String thường được dùng làm key:
“`csharp
Dictionary
“`
Nếu string có thể bị thay đổi sau khi hash, dictionary sẽ gặp vấn đề.
### Security
Các giá trị như:
“`text
connection string
URL
file path
token
“`
không thể bị thay đổi trực tiếp thông qua một reference khác.
### String interning
Runtime có thể tái sử dụng một số string literal.
Ví dụ:
“`csharp
string a = “hello”;
string b = “hello”;
“`
Trong một số trường hợp, cả hai có thể dùng cùng một interned string.
Immutability giúp cơ chế này an toàn.
—
# 16. Một số lựa chọn khác ngoài StringBuilder
Không phải mọi bài toán nối chuỗi đều cần `StringBuilder`.
Ví dụ muốn nối collection:
“`csharp
var names = new[]
{
“John”,
“Anna”,
“David”
};
“`
Thay vì:
“`csharp
var builder = new StringBuilder();
foreach (var name in names)
{
builder.Append(name);
builder.Append(“,”);
}
“`
có thể đơn giản dùng:
“`csharp
string result = string.Join(“,”, names);
“`
Kết quả:
“`text
John,Anna,David
“`
Code ngắn hơn và thể hiện đúng ý định hơn.
—
# 17. Rule of thumb
Có thể ghi nhớ quy tắc đơn giản sau:
“`text
1–5 phép nối
↓
+, interpolation
Repeated concatenation / loop lớn
↓
StringBuilder
Join collection
↓
string.Join
“`
Ví dụ:
“`csharp
var message = $”{firstName} {lastName}”;
“`
Dùng interpolation.
“`csharp
var result = string.Join(“,”, names);
“`
Dùng `string.Join`.
“`csharp
var builder = new StringBuilder();
foreach (var item in hugeCollection)
{
builder.Append(item.Value);
}
“`
Dùng `StringBuilder`.
—
# 18. Câu trả lời ngắn để dùng trong phỏng vấn
Nếu interviewer hỏi:
> What is the difference between string concatenation and StringBuilder?
Có thể trả lời:
> Strings in C# are immutable. When we repeatedly concatenate strings using `+` or `+=`, new string objects may be created, which causes additional memory allocations and copying. StringBuilder uses a mutable internal buffer, so it is more efficient for repeated concatenation, especially inside large loops. For a small number of concatenations, I normally prefer string interpolation or `+` because the code is simpler and the runtime can optimize it well.
Đây là câu trả lời vừa đủ sâu mà không quá dài.
—
# 19. Kết luận
Điểm quan trọng nhất không phải là:
“`text
StringBuilder faster than string
“`
mà là hiểu bản chất:
“`text
string
↓
immutable
repeated +=
↓
create intermediate strings
↓
more allocations + copying
“`
Trong khi:
“`text
StringBuilder
↓
mutable buffer
↓
Append()
↓
reuse / expand buffer
↓
ToString() once at the end
“`
Vì vậy:
> Hãy dùng `+` hoặc string interpolation cho các phép nối nhỏ và rõ ràng. Dùng `StringBuilder` khi phải nối chuỗi nhiều lần, đặc biệt trong vòng lặp hoặc khi xây dựng một lượng text lớn.
Đừng tối ưu một cách máy móc. Hãy chọn công cụ dựa trên quy mô dữ liệu, số lần nối chuỗi và độ dễ đọc của code.