Chuyển tới nội dung chính

Strings

Định nghĩa (Definition)

Một chuỗi (String) trong C# là một dãy ký tự Unicode bất biến (Immutable Sequence). Chuỗi là kiểu tham chiếu (Reference Type - thể hiện của System.String) nhưng hoạt động giống kiểu giá trị (Value Type) trong nhiều khía cạnh — so sánh bằng nhau sử dụng ngữ nghĩa giá trị (Value Semantics) và chúng bất biến (Immutable) theo thiết kế.

string greeting = "Hello, World!";
// string is an alias for System.String
System.String greeting2 = "Hello, World!";
Thực tế quan trọng

Chuỗi là kiểu tham chiếu (Reference Types) được lưu trữ trên managed heap, nhưng trình biên dịch xử lý chúng với ngữ nghĩa kiểu giá trị (Value-Type Semantics) cho so sánh bằng nhau (== so sánh nội dung, không phải danh tính tham chiếu).

Các khái niệm cốt lõi (Core Concepts)

Tính bất biến của chuỗi (String Immutability)

Một khi được tạo, đối tượng chuỗi không thể bị sửa đổi. Mọi thao tác có vẻ sửa đổi chuỗi thực tế tạo ra một thể hiện chuỗi mới.

string a = "hello";
string b = a;
a += " world";
// a = "hello world" (new string)
// b = "hello" (original unchanged)

Tại sao điều này quan trọng:

  • An toàn luồng (Thread-safe) mặc định — không cần đồng bộ hóa (Synchronization) cho việc đọc
  • Cho phép intern chuỗi (String Interning) — runtime tái sử dụng các hằng số chuỗi (String Literals) giống nhau để tiết kiệm bộ nhớ
  • Ngăn ngừa lỗi sửa đổi vô tình

Intern chuỗi (String Interning) có nghĩa là CLR duy trì một pool các hằng số chuỗi (String Literals) duy nhất. Các hằng số giống nhau trong mã chia sẻ cùng vị trí bộ nhớ:

string s1 = "hello";
string s2 = "hello";
// s1 and s2 reference the same object in the intern pool
Console.WriteLine(ReferenceEquals(s1, s2)); // True
cảnh báo

Chuỗi được xây dựng trong thời gian chạy (ví dụ: qua StringBuilder hoặc nối chuỗi) không tự động được intern trừ khi bạn gọi string.Intern().

Tạo và khởi tạo chuỗi (String Creation and Initialization)

// String literal
string fromLiteral = "Hello";

// From constructor (char array)
char[] chars = { 'H', 'e', 'l', 'l', 'o' };
string fromChars = new string(chars);

// From a single character repeated
string repeated = new string('a', 5); // "aaaaa"

// Empty string
string empty1 = "";
string empty2 = string.Empty; // preferred

Nối chuỗi (String Concatenation)

// + operator
string result = "Hello" + " " + "World";

// String.Concat
string concat = string.Concat("Hello", " ", "World");

// String.Join — joins with a separator
string joined = string.Join(", ", "apple", "banana", "cherry");
// "apple, banana, cherry"

Nội suy chuỗi (String Interpolation)

string name = "Alice";
int age = 30;
decimal price = 19.99m;

// Basic interpolation
string msg = $"My name is {name}, age {age}";

// Format specifiers
string formatted = $"Price: {price:F2}"; // "Price: 19.99"
string currency = $"Total: {price:C}"; // "Total: $19.99"
string padded = $"ID: {42:D5}"; // "ID: 00042"
string percent = $"Rate: {0.85:P0}"; // "Rate: 85 %"

Chuỗi nguyên văn (Verbatim Strings)

Thêm tiền tố @ để xử lý các chuỗi thoát (Escape Sequences) dưới dạng ký tự nguyên văn. Lý tưởng cho đường dẫn tệp (File Paths), mẫu regex (Regex Patterns) và văn bản nhiều dòng:

// File paths — no need to escape backslashes
string path = @"C:\Users\Alice\Documents\file.txt";

// Multi-line
string multiline = @"Line one
Line two
Line three";

// Regex patterns
string pattern = @"\d+\.\d+"; // matches decimal numbers

Hằng số chuỗi thô (Raw String Literals) (C# 11)

Đóng trong ba dấu ngoặc kép (hoặc nhiều hơn) """. Không cần thoát — hoàn hảo cho JSON, SQL và mã nhúng:

string json = """
{
"name": "Alice",
"age": 30,
"regex": "^\d+$"
}
""";

// With interpolation (add $ before """)
string sql = $"""
SELECT * FROM Users
WHERE Name = '{name}'
AND Age > {age}
""";

So sánh chuỗi (String Comparison)

string a = "Hello";
string b = "hello";

// == operator (ordinal, case-sensitive)
bool equal = a == b; // False

// String.Equals — supports StringComparison
bool ignoreCase = string.Equals(a, b, StringComparison.OrdinalIgnoreCase); // True

// String.Compare — returns -1, 0, or 1
int cmp = string.Compare(a, b, StringComparison.Ordinal);

// Best practice: always specify StringComparison
bool same = string.Equals(a, b, StringComparison.Ordinal); // byte-by-byte
bool sameCulture = string.Equals(a, b, StringComparison.CurrentCulture); // culture-aware
Hướng dẫn StringComparison
  • Sử dụng StringComparison.Ordinal hoặc StringComparison.OrdinalIgnoreCase cho so sánh theo chương trình (khóa từ điển, đường dẫn tệp).
  • Sử dụng StringComparison.CurrentCulture để hiển thị kết quả đã sắp xếp cho người dùng.

Các phương thức thao tác chuỗi (String Manipulation Methods)

string s = " Hello, World! ";

s.Substring(2, 5); // "Hello" (start, length)
s.Trim(); // "Hello, World!"
s.TrimStart(); // "Hello, World! "
s.ToUpper(); // " HELLO, WORLD! "
s.ToLower(); // " hello, world! "
s.Replace("World", "C#"); // " Hello, C#! "
s.Contains("Hello"); // True
s.StartsWith(" Hel"); // True
s.EndsWith("! "); // True
s.IndexOf("World"); // 9
s.Split(", "); // [" Hello", "World! "]
s.PadLeft(20, '.'); // "..... Hello, World! "
string.Join("-", s.Split(", ")); // " Hello-World! "

Ví dụ mã (Code Examples)

StringBuilder cho nối chuỗi hiệu quả (StringBuilder for Efficient Concatenation)

using System.Text;

var sb = new StringBuilder();

for (int i = 0; i < 10_000; i++)
{
sb.AppendLine($"Item {i}");
}

string result = sb.ToString();

// With initial capacity to reduce reallocations
var sb2 = new StringBuilder(capacity: 50_000);
sb2.Append("INSERT INTO Users (Name) VALUES ");
sb2.AppendLine("('Alice'),");
sb2.AppendFormat("('{0}'),", "Bob");
sb2.Replace("Alice", "Alicia");
sb2.Insert(0, "-- SQL Insert\n");
sb2.Remove(0, 16); // remove inserted comment

Nội suy nhận biết culture với FormattableString (Culture-Aware Interpolation with FormattableString)

decimal value = 1234.56m;

// FormattableString captures the format for deferred rendering
FormattableString fs = $"Value: {value:C}";

// Render with invariant culture (always uses '.' decimal separator)
string invariant = FormattableString.Invariant(fs); // "Value: 1234.56"

// Render with current culture
string local = fs.ToString(); // "Value: $1,234.56" (varies by culture)

Kiểm tra Null hoặc rỗng (Checking for Null or Empty)

string? input = null;

// Preferred methods
bool isNullOrEmpty = string.IsNullOrEmpty(input); // True
bool isNullOrWhiteSpace = string.IsNullOrWhiteSpace(" "); // True

Khi nào sử dụng (When to Use)

Tình huốngKhuyến nghị
Vài lần nối chuỗiToán tử + hoặc string.Concat
Nhiều lần nối chuỗi trong vòng lặpStringBuilder
Nhúng biến vào văn bảnNội suy chuỗi (String Interpolation) $""
Đường dẫn tệp, mẫu regexChuỗi nguyên văn (Verbatim Strings) @""
JSON, SQL, mẫu nhiều dòngHằng số chuỗi thô (Raw String Literals) """ """
Hiển thị nhận biết culture (Culture-Aware)StringComparison.CurrentCulture
So sánh theo chương trìnhStringComparison.Ordinal

Lỗi thường gặp (Common Pitfalls)

  1. Sử dụng + trong vòng lặp — Tạo một chuỗi mới mỗi lần lặp. Cấp phát bộ nhớ O(n^2). Sử dụng StringBuilder thay thế.

  2. Lỗi so sánh phân biệt hoa/thường== phân biệt hoa/thường. "hello" == "Hello"false. Sử dụng StringComparison.OrdinalIgnoreCase khi hoa/thường không quan trọng.

  3. Sử dụng == cho so sánh nhận biết culture== sử dụng so sánh thứ tự (Ordinal Comparison), không phải quy tắc culture. Sử dụng string.Equals(a, b, StringComparison.CurrentCulture) cho sắp xếp hướng người dùng.

  4. Substring vượt phạm vis.Substring(start, length) ném ArgumentOutOfRangeException nếu start + length > s.Length. Luôn xác thực giới hạn trước.

  5. Gọi .ToString() trên null — Ném NullReferenceException. Sử dụng Convert.ToString(obj) hoặc null-conditional obj?.ToString().

Tóm tắt要点 (Key Takeaways)

  • Chuỗi là bất biến (Immutable) — mọi sửa đổi tạo ra một thể hiện mới
  • Sử dụng StringBuilder để nối chuỗi trong vòng lặp hoặc xây dựng chuỗi lớn
  • Luôn chỉ định StringComparison rõ ràng khi so sánh
  • Sử dụng hằng số chuỗi thô (Raw String Literals) (""") cho nội dung nhiều dòng không cần thoát
  • Sử dụng string.IsNullOrEmpty()string.IsNullOrWhiteSpace() để kiểm tra null
  • Intern chuỗi (String Interning) tái sử dụng các hằng số giống nhau trong bộ nhớ nhưng không áp dụng cho chuỗi được xây dựng trong thời gian chạy

Câu hỏi phỏng vấn (Interview Questions)

Q: Tại sao chuỗi bất biến (Immutable) trong C#? A: Tính bất biến (Immutability) cung cấp an toàn luồng (Thread Safety - chuỗi có thể chia sẻ giữa các luồng mà không cần khóa), cho phép intern chuỗi (String Interning - tối ưu bộ nhớ bằng cách tái sử dụng hằng số giống nhau), đơn giản hóa ngữ nghĩa so sánh bằng nhau và ngăn ngừa lỗi sửa đổi khó theo dõi.

Q: Intern chuỗi (String Interning) là gì? A: CLR duy trì một pool nội bộ (Intern Pool) các hằng số chuỗi (String Literals) duy nhất. Khi cùng một hằng số xuất hiện nhiều lần trong mã, tất cả chúng đều tham chiếu đến cùng một đối tượng chuỗi trong bộ nhớ. Chuỗi được xây dựng trong thời gian chạy không tự động được intern.

Q: Khi nào nên sử dụng StringBuilder thay vì nối chuỗi (String Concatenation)? A: Sử dụng StringBuilder khi nối chuỗi trong vòng lặp hoặc xây dựng chuỗi động lớn (ví dụ: tạo SQL, HTML). Đối với nối chuỗi đơn giản một lần với vài chuỗi, toán tử + hoặc string.Concat là ổn — trình biên dịch thậm chí tối ưu các trường hợp đơn giản.

Q: Sự khác biệt giữa ==String.Equals là gì? A: Toán tử == trên chuỗi thực hiện so sánh thứ tự (Ordinal Comparison - byte-by-byte, phân biệt hoa/thường). String.Equals cung cấp các nạp chồng chấp nhận enum StringComparison, cho phép bạn kiểm soát culture và độ nhạy hoa/thường.

Q: StringComparison.Ordinal có nghĩa là gì? A: Nó thực hiện so sánh byte-by-byte sử dụng các code point ký tự gốc, bỏ qua ngôn ngữ culture. Đây là so sánh nhanh nhất và dễ dự đoán nhất — lý tưởng cho logic nội bộ, khóa từ điển và đường dẫn tệp.

Tài liệu tham khảo (References)