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

✂️ Cắt Chuỗi và Indexing

Mục Tiêu Bài Học

Sau bài này, bạn sẽ:

  • ✅ Hiểu tại sao không thể index String với s[0]
  • ✅ Sử dụng string slices với &s[start..end]
  • ✅ Làm việc với chars() và bytes()
  • ✅ Truy cập chuỗi an toàn, tránh panic

🤔 Vấn Đề: Không Thể Index Trực Tiếp

Trong nhiều ngôn ngữ, bạn có thể làm:

# Python
s = "Hello"
print(s[0]) # 'H'

Nhưng trong Rust:

fn main() {
let s = String::from("Hello");
// ❌ Lỗi compile!
// let c = s[0];
}

Tại sao? Vì Rust lưu String dưới dạng UTF-8!

Ẩn Dụ

Hãy tưởng tượng String như một cuộn băng ghi âm:

📼 Băng ghi âm UTF-8:

  • Mỗi bài hát (ký tự) có độ dài khác nhau
  • Bài 1: 3 phút (3 bytes)
  • Bài 2: 2 phút (2 bytes)
  • Bài 3: 5 phút (5 bytes)

Không thể nói "Phút thứ 5" để chỉ bài hát cụ thể! ✅ Phải biết từng bài chiếm bao nhiêu phút

Đồ bảo hộ an toàn (Rust) không cho phép truy cập sai vị trí! 🛡️


📝 UTF-8 Là Gì?

UTF-8 là cách mã hóa Unicode:

  • Mỗi ký tự chiếm 1-4 bytes
  • Chữ cái tiếng Anh: 1 byte
  • Chữ có dấu (à, é, ô): 2-3 bytes
  • Emoji: 4 bytes
fn main() {
let english = "Hello";
let vietnamese = "Xin chào";
let emoji = "👋";

println!("English - Bytes: {}", english.len());
println!("Vietnamese - Bytes: {}", vietnamese.len());
println!("Emoji - Bytes: {}", emoji.len());
}

Kết quả:

English - Bytes: 5
Vietnamese - Bytes: 10
Emoji - Bytes: 4

Giải thích:

  • "Hello" = 5 bytes (5 ký tự × 1 byte)
  • "Xin chào" = 10 bytes (8 ký tự, nhưng 'à' và 'o' có dấu chiếm nhiều hơn)
  • "👋" = 4 bytes (1 emoji)

🔪 String Slicing

Dùng &s[start..end] để lấy slice (lát cắt) của String:

Với Chuỗi ASCII

fn main() {
let s = String::from("Hello World");

let hello = &s[0..5]; // "Hello"
let world = &s[6..11]; // "World"

println!("Part 1: {}", hello);
println!("Part 2: {}", world);
}

Kết quả:

Part 1: Hello
Part 2: World

Cú Pháp Ngắn Gọn

fn main() {
let s = String::from("Hello");

println!("{}", &s[0..5]); // Từ 0 đến 4
println!("{}", &s[..5]); // Từ đầu đến 4
println!("{}", &s[1..]); // Từ 1 đến cuối
println!("{}", &s[..]); // Toàn bộ
}

Kết quả:

Hello
Hello
ello
Hello

⚠️ Nguy Hiểm Với UTF-8

Cảnh báo: Slicing có thể panic nếu cắt giữa ký tự UTF-8!

fn main() {
let s = String::from("Xin chào");

// ✅ OK - Cắt đúng boundary
println!("{}", &s[0..3]); // "Xin"

// ❌ PANIC! - Cắt giữa ký tự 'à'
// println!("{}", &s[0..5]);
}

Lỗi runtime:

thread 'main' panicked at 'byte index 5 is not a char boundary'
Quy Tắc Vàng

KHÔNG slice bừa bãi với chuỗi UTF-8!

// ❌ Nguy hiểm - Có thể panic
let slice = &s[0..5];

// ✅ An toàn - Dùng chars()
let chars: Vec<char> = s.chars().collect();
let slice = &chars[0..5];

🔤 chars() - Lặp Qua Ký Tự

Cách an toàn để truy cập từng ký tự:

fn main() {
let s = String::from("Xin chào");

for (i, c) in s.chars().enumerate() {
println!("Ký tự {}: {}", i, c);
}
}

Kết quả:

Ký tự 0: X
Ký tự 1: i
Ký tự 2: n
Ký tự 3:
Ký tự 4: c
Ký tự 5: h
Ký tự 6: à
Ký tự 7: o

Lấy Ký Tự Tại Vị Trí

fn main() {
let s = String::from("Xin chào");

if let Some(c) = s.chars().nth(6) {
println!("Ký tự thứ 6: {}", c);
}
}

Kết quả:

Ký tự thứ 6: à

🔢 bytes() - Lặp Qua Bytes

Truy cập từng byte (không phải ký tự):

fn main() {
let s = String::from("Hi");

for (i, b) in s.bytes().enumerate() {
println!("Byte {}: {}", i, b);
}
}

Kết quả:

Byte 0: 72
Byte 1: 105

Giải thích:

  • 'H' = 72 (ASCII)
  • 'i' = 105 (ASCII)

Với Tiếng Việt

fn main() {
let s = String::from("à");

println!("Số bytes: {}", s.len());
println!("Số ký tự: {}", s.chars().count());

print!("Bytes: ");
for b in s.bytes() {
print!("{} ", b);
}
println!();
}

Kết quả:

Số bytes: 2
Số ký tự: 1
Bytes: 195 160

Giải thích:

  • 'à' chiếm 2 bytes trong UTF-8
  • Byte 1: 195
  • Byte 2: 160

🎯 Ví Dụ: Lấy N Ký Tự Đầu

fn lay_n_ky_tu_dau(s: &str, n: usize) -> String {
s.chars().take(n).collect()
}

fn main() {
let text = "Xin chào, tôi học Rust";

println!("5 ký tự đầu: {}", lay_n_ky_tu_dau(&text, 5));
println!("10 ký tự đầu: {}", lay_n_ky_tu_dau(&text, 10));
}

Kết quả:

5 ký tự đầu: Xin c
10 ký tự đầu: Xin chào,

🔍 Ví Dụ: Tìm Vị Trí Ký Tự

fn tim_vi_tri(s: &str, target: char) -> Option<usize> {
s.chars().position(|c| c == target)
}

fn main() {
let text = "Học Rust rất vui";

match tim_vi_tri(&text, 'R') {
Some(pos) => println!("Tìm thấy 'R' tại vị trí {}", pos),
None => println!("Không tìm thấy"),
}

match tim_vi_tri(&text, 'Z') {
Some(pos) => println!("Tìm thấy 'Z' tại vị trí {}", pos),
None => println!("Không tìm thấy 'Z'"),
}
}

Kết quả:

Tìm thấy 'R' tại vị trí 4
Không tìm thấy 'Z'

🎮 Ví Dụ Thực Tế: Kiểm Tra Palindrome

fn la_palindrome(s: &str) -> bool {
let clean: String = s.chars()
.filter(|c| c.is_alphanumeric())
.map(|c| c.to_lowercase().next().unwrap())
.collect();

let reversed: String = clean.chars().rev().collect();

clean == reversed
}

fn main() {
let tests = vec![
"radar",
"hello",
"A man a plan a canal Panama",
"race car",
];

for test in tests {
let result = if la_palindrome(test) {
"✅ Palindrome"
} else {
"❌ Không phải"
};
println!("{:30} -> {}", test, result);
}
}

Kết quả:

radar                          -> ✅ Palindrome
hello -> ❌ Không phải
A man a plan a canal Panama -> ✅ Palindrome
race car -> ✅ Palindrome

🔄 Đảo Ngược Chuỗi An Toàn

fn dao_nguoc(s: &str) -> String {
s.chars().rev().collect()
}

fn main() {
let text = "Xin chào";

println!("Gốc: {}", text);
println!("Đảo: {}", dao_nguoc(text));
}

Kết quả:

Gốc: Xin chào
Đảo: oàhc niX

Giải thích:

  • .chars() → Iterator qua các ký tự
  • .rev() → Đảo ngược iterator
  • .collect() → Thu thập thành String

📊 So Sánh: Array vs String Indexing

Đặc ĐiểmArray [i32]String
Index trực tiếparr[0]❌ Không có
Lý doPhần tử cố địnhUTF-8 biến đổi
Cách an toànarr[i] hoặc .get(i).chars().nth(i)
Slicing&arr[0..3]&s[0..3] (cẩn thận!)
fn main() {
// ✅ Array - OK
let arr = [1, 2, 3, 4, 5];
println!("{}", arr[2]); // 3

// ❌ String - Lỗi
let s = String::from("Hello");
// println!("{}", s[2]); // ❌ Không compile

// ✅ String - Dùng chars()
if let Some(c) = s.chars().nth(2) {
println!("{}", c); // 'l'
}
}

⚠️ Lỗi Thường Gặp

1. Slice Giữa Ký Tự UTF-8

fn main() {
let s = String::from("Xin chào");

// ❌ PANIC! - 8 không phải char boundary
// println!("{}", &s[0..8]);

// ✅ An toàn - Dùng chars()
let first_7: String = s.chars().take(7).collect();
println!("{}", first_7);
}

2. Nghĩ len() Là Số Ký Tự

fn main() {
let s = String::from("Xin chào");

// ❌ Sai - len() trả về bytes
println!("Độ dài: {}", s.len()); // 10

// ✅ Đúng - Đếm ký tự
println!("Số ký tự: {}", s.chars().count()); // 8
}

3. Dùng [] Thay Vì chars().nth()

fn main() {
let s = String::from("Hello");

// ❌ Lỗi compile
// let c = s[0];

// ✅ Đúng
if let Some(c) = s.chars().nth(0) {
println!("{}", c);
}
}

💪 Bài Tập Thực Hành

Bài 1: Lấy N Ký Tự Cuối

Viết function lấy n ký tự cuối chuỗi.

fn lay_n_ky_tu_cuoi(s: &str, n: usize) -> String {
// Viết code của bạn ở đây
}

fn main() {
let text = "Học Rust vui lắm";
println!("3 ký tự cuối: {}", lay_n_ky_tu_cuoi(&text, 3));
}
💡 Xem Đáp Án
fn lay_n_ky_tu_cuoi(s: &str, n: usize) -> String {
let total = s.chars().count();
if n >= total {
s.to_string()
} else {
s.chars().skip(total - n).collect()
}
}

fn main() {
let text = "Học Rust vui lắm";
println!("Gốc: {}", text);
println!("3 ký tự cuối: {}", lay_n_ky_tu_cuoi(&text, 3));
println!("5 ký tự cuối: {}", lay_n_ky_tu_cuoi(&text, 5));
}

Kết quả:

Gốc: Học Rust vui lắm
3 ký tự cuối: lắm
5 ký tự cuối: i lắm

Bài 2: Đếm Nguyên Âm

Viết function đếm số nguyên âm sử dụng chars().

fn dem_nguyen_am(s: &str) -> usize {
// Viết code của bạn ở đây
}

fn main() {
let text = "Học Rust rất thú vị";
println!("Số nguyên âm: {}", dem_nguyen_am(text));
}
💡 Xem Đáp Án
fn dem_nguyen_am(s: &str) -> usize {
let nguyen_am = "aeiouAEIOU";
s.chars()
.filter(|c| nguyen_am.contains(*c))
.count()
}

fn main() {
let text = "Học Rust rất thú vị";
println!("Văn bản: {}", text);
println!("Số nguyên âm: {}", dem_nguyen_am(text));
}

Kết quả:

Văn bản: Học Rust rất thú vị
Số nguyên âm: 5

Bài 3: Cắt Chuỗi An Toàn

Viết function cắt chuỗi n ký tự đầu, an toàn với UTF-8.

fn cat_chuoi_an_toan(s: &str, n: usize) -> String {
// Viết code của bạn ở đây
}

fn main() {
let text = "Xin chào, tôi học Rust";
println!("5 ký tự: {}", cat_chuoi_an_toan(&text, 5));
println!("10 ký tự: {}", cat_chuoi_an_toan(&text, 10));
}
💡 Xem Đáp Án
fn cat_chuoi_an_toan(s: &str, n: usize) -> String {
s.chars().take(n).collect()
}

fn main() {
let text = "Xin chào, tôi học Rust";
println!("Gốc: {}", text);
println!("5 ký tự: {}", cat_chuoi_an_toan(&text, 5));
println!("10 ký tự: {}", cat_chuoi_an_toan(&text, 10));
}

Kết quả:

Gốc: Xin chào, tôi học Rust
5 ký tự: Xin c
10 ký tự: Xin chào,

📝 Tóm Tắt

Khái NiệmCú PhápAn Toàn?
Direct indexs[0]❌ Không có
Slice&s[0..5]⚠️ Cẩn thận UTF-8
chars().chars().nth(i)✅ An toàn
bytes().bytes()✅ An toàn
Take n chars.chars().take(n)✅ An toàn

Quy Tắc Vàng:

  • KHÔNG dùng s[i] với String
  • ⚠️ Cẩn thận khi slice &s[start..end] với UTF-8
  • Dùng .chars() để an toàn
  • Dùng .bytes() nếu cần bytes

🎯 Best Practices

1. Ưu Tiên chars() Thay Vì Slicing

// ❌ Nguy hiểm
let first_5 = &s[0..5];

// ✅ An toàn
let first_5: String = s.chars().take(5).collect();

2. Kiểm Tra Boundary Trước Khi Slice

// ✅ Kiểm tra trước
if s.is_char_boundary(5) {
println!("{}", &s[0..5]);
} else {
println!("Không thể slice tại vị trí 5");
}

3. Dùng Iterators Cho UTF-8

// ✅ An toàn với mọi encoding
for (i, c) in s.chars().enumerate() {
println!("Ký tự {}: {}", i, c);
}

🎯 Kết Luận Intermediate Section

🎉 Chúc mừng! Bạn đã hoàn thành 13 bài Intermediate:

✅ Control Flow (if-else, match) ✅ Loops (loop, while, for) ✅ Collections (arrays, vectors, tuples) ✅ Functions (basics, parameters, closures) ✅ Strings (methods, slicing)

Bạn giờ đã:

  • Viết được control flow và loops
  • Xử lý collections linh hoạt
  • Tạo functions và closures
  • Làm việc với strings an toàn

🎯 Bước Tiếp Theo

Bạn đã vững Intermediate level rồi! 🎊

Tiếp theo là Advanced topics - chúng ta sẽ học về Ownership sâu hơn, Structs & Enums, Error Handling, và nhiều khái niệm mạnh mẽ khác! 🚀

➡️ Tiếp theo: Advanced Section - Ownership Basics

Nghỉ Ngơi!

Đây là lúc tốt để:

  • 🔄 Ôn lại các bài intermediate
  • 💪 Làm thêm bài tập
  • 🎮 Thử viết project nhỏ
  • ☕ Uống cà phê và tự hào về tiến bộ của bạn!
Loading comments...