Menu

Substring trong C#: cắt một phần chuỗi an toàn

Cách String.Substring hoạt động trong C#: đối số chỉ số bắt đầu và độ dài, ngoại lệ ArgumentOutOfRangeException và cách tránh, cắt văn bản quanh kết quả của IndexOf, lấy n ký tự đầu hoặc cuối, và toán tử range từ C# 8.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Substring trả về một chuỗi mới tạo từ một phần của chuỗi có sẵn. Phương thức này có hai overload:

  • s.Substring(startIndex): từ startIndex đến cuối chuỗi.
  • s.Substring(startIndex, length): length ký tự, bắt đầu tại startIndex.

Chỉ số bắt đầu từ 0, và đối số thứ hai là độ dài, không phải vị trí kết thúc. Đó là điểm khác với substring(start, end) của JavaScript và substring(begin, end) của Java, và là nguồn gốc của rất nhiều bug lệch vài ký tự khi chuyển code từ ngôn ngữ khác sang.

Đầu ra:

4271-BLK
SHOE
4271
BLK
0
SHOE-4271-BLK

Như mọi phương thức chuỗi khác, Substring không sửa chuỗi gốc; nó trả về một chuỗi mới, nên hãy gán kết quả lại.

ArgumentOutOfRangeException và cách tránh

Substring ném ArgumentOutOfRangeException khi đoạn được yêu cầu không nằm gọn trong chuỗi: startIndex âm, startIndex lớn hơn Length, length âm, hoặc startIndex + length lớn hơn Length. Không có chuyện tự cắt ngắn lặng lẽ như slice của Python.

Đầu ra:

Substring(2, 4) -> "ffee"
Substring(2, 5) -> ArgumentOutOfRangeException
Substring(6) -> ""
Substring(7) -> ArgumentOutOfRangeException
Substring(-1) -> ArgumentOutOfRangeException
Substring(0, 10) -> ArgumentOutOfRangeException

Cách phòng thủ thông dụng là giới hạn độ dài theo phần còn lại của chuỗi bằng Math.Min:

Đầu ra:

ffee
ea
[]
[]

Gọi Substring trên một chuỗi null sẽ ném NullReferenceException, không phải ArgumentOutOfRangeException, vì vậy hàm hỗ trợ kiểm tra null trước tiên.

n ký tự đầu và cuối

Lấy phần đầu hoặc phần cuối là cách dùng Substring phổ biến nhất. Cả hai đều ném ngoại lệ khi chuỗi ngắn hơn n, nên code thực tế luôn kiểm tra độ dài:

Đầu ra:

**** 1111
An Unexpecte...
Hi
Hi

Khi điều bạn muốn là bỏ phần đầu chuỗi, chỉ cần Substring(n): "#FF8800".Substring(1) là "FF8800". Phương thức liên quan Remove(start, count) làm ngược lại với Substring: nó trả về chuỗi đã bỏ đi đoạn đó.

Substring kết hợp IndexOf

Phần lớn chuỗi con trong thực tế được tìm ra chứ không phải đếm ra. IndexOf trả về vị trí của một ký tự hoặc chuỗi (hoặc -1 nếu không có), còn LastIndexOf tìm từ cuối. Kết hợp chúng với Substring để cắt văn bản quanh một dấu phân cách:

Đầu ra:

maya.lopez
example.com
pdf
report.final.v2
WARN
Disk almost full

Phép tính tuân theo một quy tắc: để lấy văn bản giữa hai vị trí a và b (không bao gồm cả hai), điểm bắt đầu là a + 1 và độ dài là b - a - 1.

Nguy hiểm nằm ở chỗ IndexOf trả về -1. email.Substring(0, -1) ném ngoại lệ, và tệ hơn, email.Substring(-1 + 1) lặng lẽ trả về cả chuỗi. Luôn kiểm tra:

Đầu ra:

shop.io
(none)

Split, một cách thay thế

Khi chuỗi là một danh sách các trường có dấu phân cách, Split đơn giản hơn một chuỗi lời gọi IndexOf và Substring, và không có phép tính chỉ số nào để làm sai:

Đầu ra:

Ana Silva
Porto
maya.lopez
beach.jpg

Split cấp phát một mảng cộng thêm một chuỗi cho mỗi trường, nên với một lần cắt duy nhất trong vòng lặp chạy nhiều, IndexOf và Substring tốn ít hơn. Với mọi trường hợp khác, dễ đọc là ưu tiên. Các tùy chọn khác (nhiều dấu phân cách, bỏ phần tử rỗng, số phần tử tối đa) có trong các phương thức chuỗi. Với những mẫu như "các chữ số sau ID:", biểu thức chính quy thường là công cụ rõ ràng nhất.

Toán tử range (C# 8 trở đi)

C# 8 bổ sung cú pháp index và range, dùng được với chuỗi và mảng. ^n nghĩa là "thứ n tính từ cuối" và a..b là đoạn từ a đến b nhưng không bao gồm b, nên số thứ hai là chỉ số kết thúc, không phải độ dài:

// C# 8 and later
string sku = "SHOE-4271-BLK";

string first4 = sku[..4];      // "SHOE"        same as Substring(0, 4)
string middle = sku[5..9];     // "4271"        same as Substring(5, 4)
string last3  = sku[^3..];     // "BLK"         same as Substring(sku.Length - 3)
string noEnds = sku[1..^1];    // "HOE-4271-BL" drop first and last
char lastChar = sku[^1];       // 'K'

Trên chuỗi, range được biên dịch thành lời gọi Substring, nên nó cấp phát chuỗi mới và ném ArgumentOutOfRangeException trong cùng những trường hợp vượt giới hạn. Dùng cách nào là tùy phong cách; range dễ đọc hơn khi hai đầu được đo từ hai phía khác nhau ([1..^1]).

Chuỗi con không cần sao chép: Span

Mỗi lời gọi Substring cấp phát một chuỗi mới. Trong code phân tích cần hiệu năng cao (đọc tệp lớn, xử lý nhiều request), .NET Core 2.1 trở đi có ReadOnlySpan<char>, một khung nhìn vào một phần của chuỗi gốc mà không sao chép:

ReadOnlySpan<char> line = "2026-09-24,42.50,EUR".AsSpan();
ReadOnlySpan<char> amount = line.Slice(11, 5);          // "42.50", no allocation
decimal value = decimal.Parse(amount, provider: CultureInfo.InvariantCulture);

Span không thể lưu trong trường của các lớp thông thường hay dùng xuyên qua await, nên chúng thuộc về code đồng bộ, gọn và chạy nhiều. Với code ứng dụng thông thường, Substring là công cụ phù hợp.

Lỗi thường gặp

  • Truyền chỉ số kết thúc làm độ dài. s.Substring(2, 5) nghĩa là năm ký tự từ chỉ số 2, không phải "từ chỉ số 2 đến 5". Để lấy văn bản từ chỉ số a đến trước chỉ số b, độ dài là b - a.
  • Không kiểm tra IndexOf trả về -1. Kết quả là ngoại lệ, hoặc với + 1 thì vô tình lấy cả chuỗi.
  • Cho rằng chuỗi ngắn vẫn đủ dài. Một cái tên, một mã hay một dòng đọc từ tệp có thể ngắn hơn bạn nghĩ. Hãy giới hạn bằng Math.Min hoặc kiểm tra Length.
  • Bỏ quên kết quả. s.Substring(1); đứng riêng một dòng chẳng làm được gì hữu ích.

Câu hỏi thường gặp

Substring trong C# hoạt động thế nào?

s.Substring(start) trả về các ký tự từ chỉ số start đến cuối; s.Substring(start, length) trả về length ký tự bắt đầu từ start. Chỉ số bắt đầu từ 0, và đối số thứ hai là độ dài, không phải chỉ số kết thúc: "Hello".Substring(1, 3) là "ell". Chuỗi gốc không bị thay đổi.

Vì sao Substring ném ArgumentOutOfRangeException?

Vì start âm hoặc lớn hơn độ dài chuỗi, hoặc start + length vượt quá cuối chuỗi. Nguyên nhân phổ biến nhất là độ dài được tính cho một chuỗi dài hơn, hoặc dùng kết quả -1 của IndexOf làm điểm bắt đầu. Hãy giới hạn độ dài bằng Math.Min(length, s.Length - start) và kiểm tra kết quả của IndexOf trước khi dùng.

Làm sao lấy n ký tự cuối của chuỗi trong C#?

s.Substring(s.Length - n) trả về n ký tự cuối, với điều kiện n <= s.Length. Để an toàn với chuỗi ngắn, dùng s.Length <= n ? s : s.Substring(s.Length - n). Từ C# 8 trở đi bạn cũng có thể viết s[^n..], cách này cũng ném ngoại lệ tương tự khi n quá lớn.

Làm sao lấy chuỗi con nằm giữa hai ký tự trong C#?

Tìm cả hai vị trí bằng IndexOf, rồi lấy phần ở giữa: int start = s.IndexOf('(') + 1; int end = s.IndexOf(')', start); string inside = s.Substring(start, end - start);. Kiểm tra mỗi lần IndexOf đều tìm thấy (không phải -1) trước khi gọi Substring. Với những mẫu phức tạp, biểu thức chính quy sẽ rõ ràng hơn.

Substring trong C# có đếm từ 0 không?

Có. Ký tự đầu tiên ở chỉ số 0 và ký tự cuối ở Length - 1. Substring(0, 3) trả về ba ký tự đầu. Truyền chính Length làm điểm bắt đầu là hợp lệ và trả về chuỗi rỗng.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu