Sự khác nhau giữa việc xử lí chuỗi với String
và StringBuilder
, phân tích ưu nhược điểm của cả hai nhằm giúp độc giả có cái nhìn tổng quan về hai đối tượng này. Bài viết sử dụng ngôn ngữ lập trình C# (nền tảng .NET 4.0 trở lên) để minh họa cho các ví dụ.
* Ngoài ra bạn có thể xem như tương tự cho sự khác biệt giữa String và StringBuilder trong Java.
Nhu cầu xử lí chuỗi
Trong lập trình nói chung, nhu cầu xử lí thông tin là vô cùng cần thiết. Những thông tin này, còn gọi là Input, đều được người dùng đưa vào dưới dạng các chuỗi kí tự được chúng ta định sẵn. Do đó, việc xử lí chuỗi kí tự trong lập trình đã trở thành bước quan trọng nhưng cơ bản nhất để ứng dụng có thể "hiểu" những yêu cầu từ người dùng.
Trong các ngôn ngữ phát triển đi trước như C, việc xử lí chuỗi được các lập trình viên xử lí thông qua việc xử lí trên mảng kí tự char[]
đi kèm với các phương thức xử lí cần thiết. Tuy nhiên đến thế hệ các ngôn ngữ lập trình hướng đối tượng, chúng ta phát sinh ra nhu cầu, tại sao không hiện thực một đối tượng sao cho bản thân đối tượng ấy có khả năng nhận được dữ liệu dạng chuỗi kí tự và đi kèm theo là các phương thức xử lí mạnh mẽ được tích hợp đồng bộ đi kèm. Như thế, một đối tượng với tên gọi String
được ra đời.
String và StringBuilder
String
String
là một đối tượng trong C# (tương tự như một số ngôn ngữ lập trình hướng đối tượng khác) dùng để hiện thực một chuỗi kí tự Unicode (rỗng hoặc khác rỗng).
StringBuilder
String
không tốt cho các trường hợp đồi hỏi hiệu năng, vì vậy StringBuilder
ra đời, là một đối tượng chuyên xử lí chuỗi với hiệu năng cao hơn.
Vấn đề xử lí chuỗi với String và StringBuilder
Cấu trúc tổ chức String và StringBuilder
String
Là một đối tượng kiểu immutable (bất biến) và sealed (bị niêm phong, không có khả năng tạo ra kế thừa), nghĩa là khi bạn tạo mới một đối tượng, các giá trị của đối tượng này hoàn toàn không thể thay đổi.
Sự thay đổi giá trị của String nếu diễn ra sẽ diễn ra trên một đối tượng mới.
StringBuilder
public sealed class StringBuilder : ISerializable
{
// Fields
private const string CapacityField = "Capacity";
internal const int DefaultCapacity = 0x10;
internal char[] m_ChunkChars;
internal int m_ChunkLength;
internal int m_ChunkOffset;
internal StringBuilder m_ChunkPrevious;
internal int m_MaxCapacity;
private const string MaxCapacityField = "m_MaxCapacity";
internal const int MaxChunkSize = 0x1f40;
private const string StringValueField = "m_StringValue";
private const string ThreadIDField = "m_currentThread";
}
Nhìn vào cấu trúc trên ta có vài nhận xét như sau:
internal char[] m_ChunkChars
:StringBuilder
dùng một mảng char để lưu trữ các dữ liệu.internal StringBuilder m_ChunkPrevious
:StringBuilder
được tổ chức như một danh sách liên kết, do đó việc thao tác chuỗi cơ bản như chèn, xóa, cắt, tìm kiếm chuỗi… sẽ là những thao tác trên danh sách liên kết.internal int m_MaxCapacity
:StringBuilder
cũng có giới hạn sức chứa dữ liệu.
Hiệu năng của String và StringBuilder
Hiệu năng của String
Trước khi vào hiệu năng của StringBuilder
, hãy xem xét một chút vấn đề hiệu năng của String
để hiểu rõ hơn sự khác nhau biệt với StringBuilder
.
Tạo một đối tượng String
và gán cho nó một giá trị là Hello
.
String MyExampleString = new String();
MyExampleString = "Hello";
MyExampleString
được khởi tạo và cấp phát một vùng nhớ tại 0x90000
.
Nối thêm chuỗi choMyExampleString
:
MyExampleString = MyExampleString + ", is it me you're looking for";
MyExampleString
tham chiếu đến một đối tượng khác (tạm gọi là UndentifyString) tại một vùng nhớ mới 0x83320
và gán giá trị mới vào.
Sau khi quá trình gán giá trị hoàn tất tại UndentifyString, MyExampleString
sẽ tham chiếu đến UndentifyString. Vùng nhớ cũ 0x90000
sẽ được Garbage Collector dọn dẹp.
Hiệu năng của StringBuilder
Minh họa 1
Tạo một đối tượng StringBuilder MyExampleStringBuilder
và gán giá trị tương tự như ví dụ trên.
StringBuilder MyExampleStringBuilder = new StringBuilder();
MyExampleStringBuilder.Append(“Hello”);
MyExampleStringBuilder
được khởi tạo và cấp phát một vùng nhớ là 0x90000
(giả sử vùng nhớ 0x90000
ở ví dụ trên đã được thu hồi).
Thêm dữ liệu vào để MyExampleStringBuilder
thành Hello, is it me you're looking for
MyExampleStringBuilder.Append(", is it me you're looking for");
StringBuilder
lại có cơ chế hoàn toàn khác. Thay vì tạo mới vùng nhớ như String
, StringBuilder
có khả năng thao tác trên chính vùng nhớ được cấp phát.
Minh họa 2
Trong minh họa này, tôi sẽ tính toán thời gian thực thi của String
và StringBuilder
với cùng một yêu cầu.
Cụ thể tôi sẽ tạo 2 đối tượng String
và StringBuilder
chứa kí tự rỗng. Lần lượt cộng giá trị “X” vào giá trị của 2 đối tượng trên với số lần cộng đều là 100 ngàn lần. Hãy xem thời gian thực thi phương thức trên của 2 đối tượng String
và StringBuilder
.
Khởi tạo các biến cần thiết.
const int Length = 1;
const int Loops = 100000;
DateTime StartTime;
DateTime EndTime;
int i;
string TextSource = new String('X', Lenght);
string TextDest = "";
Thực thi cộng chuỗi choTestDest
bằng toán tử+
củaString
với số lần làLoops
và tính toán thời gian thực thi vớiStartTime
vàEndTime
.
StartTime = DateTime.Now;
for (i = 0; i < Loops; i++)
TextDest += TextSource;
EndTime = DateTime.Now;
Console.WriteLine("String took " + (EndTime - StartTime).TotalSeconds + " seconds.");
Kết quả nhận được: (EndTime - StartTime).TotalSeconds
=~ 2.5 giây.
Thực thi cộng chuỗi cho TestDest
bằng phương thức Append()
của StringBuilder
với số lần là Loops
và tính toán thời gian thực thi với StartTime
và EndTime
.
StartTime = DateTime.Now;
StringBuilder sb = new StringBuilder((int)(Lenght * Loops * 1.1));
for (i = 0; i < Loops; i++)
sb.Append(TextSource);
EndTime = DateTime.Now;
Console.WriteLine("String Builder took " + (EndTime - StartTime).TotalSeconds + " seconds.");
Kết quả nhận được: (EndTime - StartTime).TotalSeconds
=~ 0.001 giây.
Kết quả thời gian thực thi lệnh với cùng một điều kiện của String
và StringBuilder
rất chênh lệch.
Với các thao tác chuỗi mà có sự thay đổi giá trị với String
, cứ mỗi lệnh thao tác chuỗi được thực thi, String
sẽ tạo hẳn một đối tượng mới đi kèm là vùng nhớ mới đồng thời sao chép giá trị của đối tượng cũ ở vùng nhớ cũ và kết quả mà lệnh thao tác chuỗi thực thi vào đối tượng mới và vùng nhớ mới. Ví dụ như ở minh họa trên:
- Khi
i = 0
,TextDest = ""
, giả sử vùng nhớTestDest
làA
- Gọi lệnh
TextDest += TextSource
TextDest
tạo ra một đối tượng String khác (tạm gọi là UndentifyString) được cấp vùng nhớ mới là B và giá trị vùng nhớ này là kết quả của việc sao chép giá trị hiện tại củaTextDest
(vùng nhớA
) là giá trị rỗng""
cộng với giá trị mới được đưa vào củaTextSource
là"X"
. Sau khi quá trình sao chép hoàn tất,TestDest
tham chiếu tới vùng nhớ của UndentifyString.
Vậy nên giá trị mới của TextDest
tại vùng nhớ B
là “X”
. Vùng nhớ cũ TestDest
sẽ được Garbage Collector dọn dẹp.
Việc này được lặp lại cho đến khi vòng lặp kết thúc.
Một số lượng rất lớn đối tượng phụ đi kèm là những vùng nhớ mới được tạo ra để phục vụ thao tác cộng chuỗi đơn giản. Sau đó là quá trình sao chép dữ liệu sang đối tượng mới, làm tốn tài nguyên máy tính lẫn mang nhiều nguy cơ tiềm ẩn trong quá trình thực thi chương trình, cũng như thời gian thực thi lệnh sẽ rất dài.
Tuy nhiên, các thao chuỗi của StringBuilder
lại chỉ diễn ra trên vùng nhớ chúng được cấp phát do chúng dùng một mảng dữ liệu để lưu trữ. Điều này nghĩa là sự thay đổi về giá trị của StringBuilder
hoàn toàn được xử lí “nội bộ”. Do vậy tài nguyên cũng như thời gian thực thi của StringBuilder
là tốt hơn String
.
Kết luận
- Việc thao tác với chuỗi (gán, cắt, thêm…) với
StringBuilder
diễn ra ngay trên đối tượng được cấp phát. - Ngược lại,
String
sẽ tạo ra đối tượng mới. - Về hiệu năng,
StringBuilder
tỏ ra vượt trội hơn so vớiString
khi nghiệp vụ bạn đang xử lí đòi hỏi việc thao tác chuỗi mà giá trị chuỗi biến động với số lượng lớn. - Tuy nhiên
StringBuilder
không hỗ trợ vấn đề thao tác chuỗi mạnh mẽ nhưString
, như nhiều toán tử thao tác như+
,+=
,=
, … cũng như các phương thức nhưContains
,ToList
,ToArray
, ... - Nếu nghiệp vụ bạn đang xử lí yêu cầu việc thao tác chuỗi mà giá trị của chuỗi biến động ít,
String
hoàn toàn là sự lựa chọn hợp lí bởi sự hỗ trợ đa dạng và đầy đủ các toán tử thao tác chuỗi và phương thức tích hợp đi kèm và cả tốc độ lập trình.