Chuỗi trong C# là bất biến: s += "x" không nối dài s, mà tạo một chuỗi mới chứa các ký tự cũ cộng thêm các ký tự mới, và chuỗi cũ thành rác. Một lần thì chẳng đáng gì. Nhưng trong vòng lặp chạy hàng nghìn lần, mỗi lần lặp lại sao chép mọi thứ đã tạo đến lúc đó, nên tổng khối lượng công việc tăng theo bình phương độ dài.
StringBuilder, trong System.Text, là một bộ đệm có thể thay đổi. Append ghi vào chỗ trống ở cuối, thỉnh thoảng mở rộng bộ đệm, và ToString() tạo ra một chuỗi duy nhất ở cuối cùng.
Đầu ra:
Order #1042, total 59.97
Status: shipped
**********
51
Vì sao nối chuỗi trong vòng lặp lại chậm
Đây là cùng một văn bản được tạo theo cả hai cách. Thời gian đo khác nhau tùy máy, nên các con số dưới đây chỉ là ví dụ; điều quan trọng là khoảng cách, và nó càng lớn khi vòng lặp càng dài:
Đầu ra ví dụ:
string += 482 ms, length 188890
StringBuilder 1 ms, length 188890
Với 20.000 lần nối, phiên bản += sao chép tổng cộng gần 2 tỷ ký tự. StringBuilder sao chép mỗi ký tự khoảng hai lần, một lần khi thêm vào và một lần nữa trong ToString(), đó là lý do nó chỉ mất khoảng một mili giây.
Append, AppendLine và AppendFormat
Append có overload cho mọi kiểu có sẵn, và trả về chính builder đó, nên các lời gọi có thể nối tiếp nhau. AppendLine thêm một dấu xuống dòng sau đối số. AppendFormat nhận chuỗi composite format giống string.Format:
Đầu ra:
RECEIPT
-------
Espresso 2.40
Croissant 3.10
Orange juice 4.50
TOTAL 10.00
Append một chuỗi nội suy (sb.Append($"...")) là ổn và dễ đọc. Từ .NET 6 trở đi, trình biên dịch thậm chí ghi thẳng từng mảnh vào builder mà không tạo chuỗi trung gian.
AppendLine dùng Environment.NewLine: \n trên Linux và macOS, \r\n trên Windows. Khi định dạng tệp yêu cầu một kiểu xuống dòng cụ thể, hãy tự thêm nó.
Insert, Remove, Replace và indexer
Khác với chuỗi, StringBuilder có thể được sửa trực tiếp:
Đầu ra:
Hello, World
Hello, Maya
Maya
maya
maya!
[] length 0
Đặt Length về giá trị nhỏ hơn sẽ cắt bớt; đây là cách rẻ nhất để bỏ dấu phân cách thừa ở cuối. Clear() đặt độ dài về 0 và giữ lại bộ đệm đã cấp phát, nên một builder có thể được dùng lại qua các lần lặp của vòng lặp ngoài.
Indexer nhanh với những phần vừa thêm nhưng có thể chậm khi truy cập ngẫu nhiên vào một builder rất lớn, vì nội dung được lưu thành các khối liên kết. Nếu cần đọc ký tự, hãy gọi ToString() một lần rồi truy cập theo chỉ số trên chuỗi.
Vấn đề dấu phân cách thừa ở cuối
Một ứng dụng kinh điển là tạo danh sách phân cách bằng dấu phẩy. Thêm dấu phân cách sau mỗi phần tử sẽ để lại một dấu thừa ở cuối:
Đầu ra:
csharp, dotnet, linq
csharp, dotnet, linq
Khi các phần tử đã nằm sẵn trong một collection, string.Join ngắn gọn hơn và nhanh không kém. Hãy để StringBuilder cho văn bản có hình dạng phụ thuộc vào logic: các phần có điều kiện, vòng lặp lồng nhau, định dạng hỗn hợp.
Capacity
StringBuilder bắt đầu với chỗ cho 16 ký tự. Khi hết chỗ, nó nối thêm một khối lưu trữ mới lớn bằng toàn bộ những gì đang chứa, tối đa 8.000 ký tự mỗi khối, nên dung lượng của nó tăng gấp đôi khi còn nhỏ rồi tăng theo từng bước 8.000 ký tự. Việc mở rộng không bao giờ sao chép phần văn bản đã lưu. Nếu bạn biết đại khái kết quả sẽ dài bao nhiêu, hãy truyền capacity vào hàm khởi tạo và builder sẽ không bao giờ phải mở rộng:
Đầu ra:
12000
10000
True
True
Đặt trước kích thước là một tối ưu, không phải yêu cầu bắt buộc. Việc mở rộng cấp phát một khối mới nhưng không bao giờ di chuyển văn bản hiện có, nên giá trị mặc định là đủ, trừ khi bạn tạo nhiều chuỗi lớn trong đoạn code chạy liên tục.
Khi nào không nên dùng StringBuilder
StringBuilder không phải là bản thay thế nhanh hơn cho mọi dấu +. Trình biên dịch đã biến a + b + c + d trong cùng một biểu thức thành một lời gọi string.Concat, lời gọi này tính độ dài cuối cùng và sao chép mỗi mảnh một lần, đúng khối lượng công việc mà một builder làm nhưng với ít code hơn. Hãy dùng chuỗi thông thường khi:
- Số mảnh ít và cố định:
$"{first} {last} ({age})". - Các mảnh nằm trong một collection và cần dấu phân cách:
string.Join(", ", items). - Các mảnh không cần dấu phân cách:
string.Concat(parts). - Bạn chỉ nối vài lần, bên ngoài mọi vòng lặp.
Dùng StringBuilder khi bạn append bên trong vòng lặp, append với số lần không biết trước, hoặc sửa văn bản (chèn, thay thế, xóa) trong lúc tạo nó. Chuyển qua lại (sb.ToString() bên trong vòng lặp) sẽ làm mất hết lợi ích; hãy gọi nó một lần ở cuối.
Stream là lựa chọn còn lại: để tạo một tệp lớn, hãy ghi từng dòng vào StreamWriter ngay khi có, thay vì tạo toàn bộ nội dung trong bộ nhớ. Xem tệp.
Câu hỏi thường gặp
StringBuilder trong C# là gì?
System.Text.StringBuilder là một bộ đệm ký tự có thể thay đổi, dùng để tạo chuỗi qua nhiều bước. Append thêm vào cuối bộ đệm thay vì tạo chuỗi mới mỗi lần, và ToString() tạo ra chuỗi cuối cùng một lần duy nhất. Đây là công cụ chuẩn để tạo văn bản trong vòng lặp.
Khi nào nên dùng StringBuilder thay vì nối chuỗi?
Khi bạn thêm vào trong vòng lặp hoặc với số lần không biết trước, như tạo báo cáo, tệp CSV hay HTML từ một collection. Với vài mảnh cố định (a + b + c, hoặc một chuỗi nội suy), nối chuỗi thông thường nhanh không kém và dễ đọc hơn, vì trình biên dịch đã gộp nó thành một lời gọi string.Concat duy nhất.
Làm sao xóa ký tự cuối cùng của StringBuilder?
Giảm Length của nó: if (sb.Length > 0) sb.Length--; bỏ ký tự cuối mà không cần sao chép. sb.Remove(sb.Length - 1, 1) làm điều tương tự. Đây là cách thường dùng để bỏ dấu phẩy thừa ở cuối, dù string.Join tránh được dấu phân cách thừa ngay từ đầu.
StringBuilder có an toàn đa luồng không?
Không. Gọi Append trên cùng một StringBuilder từ nhiều luồng cùng lúc có thể làm hỏng nội dung hoặc ném ngoại lệ. Hãy cho mỗi luồng một builder riêng rồi gộp kết quả, hoặc bảo vệ builder dùng chung bằng lock.
AppendLine trong C# thêm những gì?
Văn bản kèm theo Environment.NewLine, là \r\n trên Windows và \n trên Linux và macOS. Nếu đầu ra cần một kiểu xuống dòng cụ thể, như \n cho một định dạng tệp hoặc \r\n cho HTTP header, hãy thêm nó tường minh bằng Append("\n").