Mục lục
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Dùng được
.split(pattern)với pattern làchar,&str, hoặc closure để tách chuỗi thành các&str. - Biết khi nào cần
.splitn(N, pattern)— giới hạn số phần khi muốn giữ nguyên phần đuôi. - Biết
.rsplit()và một use case kinh điển: tách extension file. - Phân biệt
.split(' '),.split_whitespace()và.split_terminator()— ba kết quả khác nhau trên cùng input. - Viết được closure pattern cho các trường hợp split phức tạp hơn một ký tự cố định.
- Nắm idiom
.trim().split(',').map(|t| t.trim())để parse CSV sạch.
.split(char) Cơ Bản
str::split trả về một lazy Iterator<Item = &str> tách chuỗi tại mọi điểm khớp pattern. Vì lazy, nó không cấp phát Vec — bạn chỉ phải collect khi thật sự cần dữ liệu dạng mảng.
fn main() {
let csv = "apple,banana,cherry,date";
// Lặp trực tiếp, không allocate.
for token in csv.split(',') {
println!("- {token}");
}
// Khi cần Vec, collect explicit.
let items: Vec<&str> = csv.split(',').collect();
println!("{items:?}");
// ["apple", "banana", "cherry", "date"]
// Pattern cũng có thể là &str nhiều ký tự.
let log = "INFO::login::ok";
let parts: Vec<&str> = log.split("::").collect();
println!("{parts:?}");
// ["INFO", "login", "ok"]
}
Vài điểm cần ghi nhớ. Mỗi phần tử là &str — slice trỏ vào chuỗi gốc, không phải String mới, nên chuỗi gốc phải còn sống suốt thời gian dùng. Nếu hai separator đứng cạnh nhau, split sẽ trả ra một token rỗng ở giữa — đây là khác biệt then chốt với split_whitespace sau này. Pattern ',' (char) khác "," (&str) ở tốc độ; với một ký tự, dạng char hơi nhanh hơn vì không cần so khớp multi-byte.
.splitn(N, pattern) Giới Hạn N Phần Tử
.splitn(N, pattern) chỉ tách tối đa N phần: nó tìm pattern đúng N - 1 lần đầu, phần đuôi còn lại được giữ nguyên xi dù vẫn chứa pattern. Cực kỳ hữu ích khi parse key=value mà value có thể chứa thêm dấu =, hay tách HTTP/1.1 200 OK mà message phía sau có thể chứa khoảng trắng.
fn main() {
let line = "url=https://blogcode.vn/?a=1&b=2";
// Nếu split bình thường: vỡ tan vì có nhiều dấu '='.
let bad: Vec<&str> = line.split('=').collect();
println!("{bad:?}");
// ["url", "https://blogcode.vn/?a", "1&b", "2"]
// splitn(2, ...) chỉ cắt ở dấu '=' ĐẦU TIÊN.
let good: Vec<&str> = line.splitn(2, '=').collect();
println!("{good:?}");
// ["url", "https://blogcode.vn/?a=1&b=2"]
if let [key, value] = good.as_slice() {
println!("key={key}, value={value}");
}
}
Quan sát thêm: splitn(1, ...) trả đúng một phần tử — bản thân toàn bộ chuỗi gốc. splitn(0, ...) trả iterator rỗng. Khi bạn biết format input theo dạng head<sep>tail và tail có thể chứa sep, splitn(2, sep) gần như luôn là lựa chọn đúng — vừa nhanh, vừa không cần regex.
.rsplit() Từ Phải Sang Trái
Tất cả method trên đều có biến thể r- đi ngược hướng — quét từ cuối chuỗi về đầu. .rsplit(pattern) tách hệt như .split nhưng trả các phần theo thứ tự từ phải sang. Kết hợp .rsplitn(N, ...) đặc biệt hợp lý khi bạn quan tâm đến phần cuối chuỗi — kinh điển nhất là tách extension file.
fn main() {
let path = "/var/log/app/2026/06/09/error.log.gz";
// rsplit trả từng phần theo thứ tự từ phải.
let mut it = path.rsplit('/');
println!("{:?}", it.next()); // Some("error.log.gz")
println!("{:?}", it.next()); // Some("09")
// rsplitn(2, '.') để tách name vs extension cuối cùng.
let file = "archive.tar.gz";
let parts: Vec<&str> = file.rsplitn(2, '.').collect();
println!("{parts:?}");
// ["gz", "archive.tar"] (phải sang trái)
// Nếu cần extension thật sự là phần sau dấu '.' đầu tiên,
// dùng splitn(2, '.'): ["archive", "tar.gz"].
let parts2: Vec<&str> = file.splitn(2, '.').collect();
println!("{parts2:?}");
}
Lưu ý nhỏ: thứ tự kết quả của rsplit là ngược so với split. Khi bạn cần extension dạng "gz" (sau dấu . cuối), rsplitn(2, '.') trả vế cuối ở index 0. Đây là pattern thường gặp khi parse filename — hơn nữa khỏi cần regex hay crate path.
.split_whitespace() Mọi Whitespace
.split_whitespace() tách tại bất kỳ ký tự nào được Unicode coi là whitespace — space, tab, newline, carriage return, no-break space, và nhiều ký tự khác. Quan trọng hơn: nó bỏ qua các token rỗng, tức là nhiều khoảng trắng liên tiếp chỉ tính như một separator.
fn main() {
let msg = " Rust\t\t rất\n ổn ";
// split(' ') quan tâm đúng một ký tự space — tạo ra empty tokens.
let raw: Vec<&str> = msg.split(' ').collect();
println!("{raw:?}");
// ["", "", "Rust\t\t", "rất\n", "", "", "ổn", "", ""]
// split_whitespace tách theo MỌI whitespace Unicode và bỏ empty.
let clean: Vec<&str> = msg.split_whitespace().collect();
println!("{clean:?}");
// ["Rust", "rất", "ổn"]
// Đếm số từ trong một dòng.
let count = msg.split_whitespace().count();
println!("words = {count}"); // 3
}
Mỗi khi bạn muốn xử lý "đếm từ" hay tokenize text tự nhiên, split_whitespace gần như là lựa chọn mặc định. Nếu cần kiểm soát chặt hơn — ví dụ tách chỉ theo ASCII whitespace (bỏ qua các no-break space), có thêm .split_ascii_whitespace() nhanh hơn một chút vì không quét Unicode table.
.split_terminator(c) Tránh Empty Đầu Cuối
Một trong những "bẫy" hay gặp khi dùng split là khi chuỗi kết thúc bằng separator — bạn nhận thêm một token rỗng ở cuối. .split_terminator(pattern) giống hệt .split nhưng không trả empty đó. Cực kỳ hợp với dữ liệu kết thúc bằng terminator như log line, hay chuỗi tự build có dấu phẩy cuối.
fn main() {
let s = "a,b,c,";
// split trả thêm "" cuối vì chuỗi kết thúc bằng ','.
let a: Vec<&str> = s.split(',').collect();
println!("{a:?}");
// ["a", "b", "c", ""]
// split_terminator bỏ empty đó.
let b: Vec<&str> = s.split_terminator(',').collect();
println!("{b:?}");
// ["a", "b", "c"]
// Có ý nghĩa rõ với chuỗi kết thúc bằng newline.
let lines = "log1\nlog2\nlog3\n";
let arr: Vec<&str> = lines.split_terminator('\n').collect();
println!("{arr:?}");
// ["log1", "log2", "log3"]
}
Khác biệt nhỏ nhưng tránh được rất nhiều if !token.is_empty() rải rác trong code. Một quy tắc đơn giản để nhớ: nếu pattern đóng vai trò separator (phân tách giữa các phần tử) → dùng split; nếu nó đóng vai trò terminator (kết thúc mỗi phần tử) → dùng split_terminator.
Pattern Closure
Trait Pattern của Rust cho phép truyền cả char, &str, và closure FnMut(char) -> bool làm pattern. Đây là cách rất Rust-y để split theo điều kiện mà không cần dựng regex.
fn main() {
let raw = "Rust 2024; rất ổn, đúng không?";
// Split tại mọi ký tự ASCII punctuation (.,;:!?...).
let tokens: Vec<&str> = raw
.split(|c: char| c.is_ascii_punctuation())
.filter(|t| !t.is_empty())
.collect();
println!("{tokens:?}");
// ["Rust 2024", " rất ổn", " đúng không"]
// Split tại nhiều separator cùng lúc: ',', ';', '|'.
let csv = "a,b;c|d,e";
let parts: Vec<&str> = csv
.split(|c| c == ',' || c == ';' || c == '|')
.collect();
println!("{parts:?}");
// ["a", "b", "c", "d", "e"]
}
So với split('?') hay split("|") chỉ bắt được một pattern, closure cho bạn linh hoạt y như regex character class [,;|] nhưng vẫn ở mức stdlib, không phải thêm crate. Chọn closure khi tập separator có hơn một ký tự — code rõ ràng hơn nhiều so với chain nhiều replace.
Trim + Split Idiom
CSV thật ngoài đời gần như luôn có khoảng trắng thừa quanh các token — "a, b , c " chứ không phải "a,b,c" sạch tinh. Idiom Rust để xử lý sạch sẽ là .trim() ở hai mép chuỗi, .split(sep), rồi .map(|t| t.trim()) để cắt thêm khoảng trắng mỗi token.
fn parse_csv(line: &str) -> Vec<&str> {
line
.trim()
.split(',')
.map(|t| t.trim())
.filter(|t| !t.is_empty())
.collect()
}
fn main() {
let line = " apple , banana , ,cherry , ";
let items = parse_csv(line);
println!("{items:?}");
// ["apple", "banana", "cherry"]
}
Bốn bước trong pipeline:
.trim()cắt whitespace hai đầu của cả dòng trước khi split — tránh token đầu/cuối dính space..split(',')tách theo dấu phẩy..map(|t| t.trim())cắt whitespace mỗi token, đồng thời chuyển&strthành&strmới (vẫn slice gốc)..filter(|t| !t.is_empty())bỏ các ô rỗng (do,,liên tiếp hay khoảng trắng đơn lẻ).
Pipeline này hoàn toàn lazy cho đến khi .collect() — không Vec trung gian. Khi parse file lớn dòng-một-dòng, gọi parse_csv trong vòng lặp đọc BufRead::lines() là đủ hiệu năng cho hầu hết workload không cần crate csv.
Tổng Kết
.split(pattern)trả lazyIterator<&str>; pattern là char / &str / closure..splitn(N, p)giới hạn tối đa N phần — giữ nguyên phần đuôi; dùng khi tail có thể chứa pattern..rsplit()/.rsplitn()quét ngược — tiện tách extension hay phần cuối path..split_whitespace()tách theo mọi whitespace Unicode và bỏ token rỗng..split_terminator(c)giống split nhưng không trả empty cuối khi chuỗi kết thúc bằng separator.- Closure pattern
|c: char| c.is_ascii_punctuation()cho phép split theo predicate, không cần regex. - Idiom CSV:
.trim().split(',').map(|t| t.trim()).filter(|t| !t.is_empty()).
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Cho
"name=Rust=lang". Viết code parse thành key"name"và value"Rust=lang". Tại saosplit('=')sai? - Cho path
"data/2026/report.tar.gz". Tách thành (filename không extension, extension cuối) = ("report.tar","gz") — dùng method nào? - Trên input
" a b\tc\nd ", kết quả củasplit(' ').count()vàsplit_whitespace().count()khác nhau ở chỗ nào? Vì sao? - Một file log có dòng kết thúc bằng
\n. Bạn cần Vec các dòng không có dòng rỗng cuối. Dùngsplit('\n')haysplit_terminator('\n')? - Viết closure pattern split
"a1b2c3d4"tại mọi chữ số ASCII thành["a", "b", "c", "d"].
Đáp án
let mut it = s.splitn(2, '='); let (k, v) = (it.next().unwrap(), it.next().unwrap());.split('=')sai vì cắt cả dấu=trong value, trả 3 phần thay vì 2.let mut it = path.rsplitn(2, '.'); let ext = it.next().unwrap(); let name = it.next().unwrap();— kết quảext = "gz",name = "data/2026/report.tar".split(' ')đếm theo từng space đơn lẻ và trả cả token rỗng giữa các space liên tiếp; còn tab/newline không được tính là separator nên dính vào token.split_whitespacecoi mọi whitespace là separator và bỏ token rỗng — kết quả là số "từ" thật.split_terminator('\n').split('\n')sẽ trả thêm một""ở cuối vì file kết thúc bằng newline."a1b2c3d4".split(|c: char| c.is_ascii_digit()).collect::<Vec<_>>()— kết quả["a", "b", "c", "d"].
Bài Tiếp Theo
Bài 138: parse() — String To Number — sau khi đã tách được token &str, bước tiếp theo của hầu hết pipeline parse là chuyển từng token sang số. .parse::<i32>() trả Result<i32, ParseIntError>, dùng trait FromStr, gắn turbofish hoặc annotate qua let.
