Collections Thông Dụng Trong Rust (Vector, HashMap, String)

lúc 18:23 30 tháng 8, 2026
23 views
Collections Thông Dụng Trong Rust (Vector, HashMap, String)

Tiếp nối bài học trước về GenericsTraits để thiết kế các abstraction linh hoạt, chúng ta sẽ bước vào một trong những chủ đề thực chiến không thể thiếu khi viết ứng dụng lớn: Collections (Các cấu trúc tập hợp dữ liệu chuẩn).

Khác với các kiểu mảng tĩnh (array) có kích thước cố định nằm trọn trên Stack, các collections tiêu chuẩn trong thư viện chuẩn của Rust được phân bổ động trên Heap, cho phép chương trình tự động co giãn, thêm bớt phần tử linh hoạt trong suốt vòng đời hoạt động. Cùng mổ xẻ chi tiết ba bộ sưu tập dữ liệu quan trọng nhất ngay dưới đây.

📦 1. Vector (Vec<T>) — Danh sách động mạnh mẽ

Vector (được ký hiệu là Vec<T>) là cấu trúc dữ liệu dạng mảng động, cho phép lưu trữ một danh sách gồm nhiều giá trị nằm liên tiếp nhau trong bộ nhớ Heap. Khi bạn cần một danh sách có thể thay đổi kích thước liên tục (thêm phần tử mới, xóa bớt phần tử cũ), Vec<T> luôn là lựa chọn hàng đầu.

A. Khởi tạo và thao tác cơ bản

Code
fn main() {
    // 1. Khởi tạo một Vector rỗng (cần ép kiểu tường minh nếu chưa thêm phần tử)
    let mut numbers: Vec<i32> = Vec::new();

    // Hoặc sử dụng macro vec! tiện lợi để khởi tạo nhanh kèm dữ liệu sẵn có
    let mut fruits = vec![String::from("Táo"), String::from("Chuối")];

    // 2. Thêm phần tử mới vào cuối Vector bằng phương thức push
    fruits.push(String::from("Cam"));
    fruits.push(String::from("Xoài"));

    // 3. Xóa phần tử cuối cùng bằng pop (trả về Option<T>)
    let removed = fruits.pop(); 
    println!("Đã xóa phần tử: {:?}", removed); // In ra Some("Xoài")
}

B. Đọc dữ liệu từ Vector an toàn

Trong Rust, việc truy cập phần tử trong Vector đòi hỏi sự cẩn trọng để tránh lỗi tràn bộ nhớ (buffer overflow). Có hai cách chính:

Code
fn main() {
    let v = vec![10, 20, 30, 40, 50];

    // Cách 1: Dùng cú pháp indexing truyền thống [] 
    // ⚠️ Nếu bạn truyền một chỉ mục không tồn tại (ví dụ v[10]), chương trình sẽ lập tức panics (sập)
    let third: &i32 = &v[2];
    println!("Phần tử thứ 3 là: {third}");

    // Cách 2: Dùng phương thức .get() trả về Option an toàn tuyệt đối
    match v.get(10) {
        Some(val) => println!("Tìm thấy giá trị: {val}"),
        None => println!("⚠️ Chỉ mục vượt quá giới hạn vector! Chương trình vẫn chạy an toàn."),
    }
}

🗂️ 2. HashMap<K, V> — Bảng ánh xạ khóa - giá trị

Khi bạn cần lưu trữ dữ liệu dưới dạng tra cứu theo từ khóa định danh (giống như Dictionary trong Python hay Map trong JavaScript/Java), HashMap<K, V> là công cụ tối ưu. Khóa K đóng vai trò định danh duy nhất, còn giá trị V chứa dữ liệu tương ứng.

A. Khởi tạo, chèn và xử lý khóa trùng lặp

Để sử dụng HashMap, bạn bắt buộc phải gọi nó từ module std::collections::HashMap.

Code
use std::collections::HashMap;

fn main() {
    // Khởi tạo một HashMap lưu điểm số học sinh
    let mut scores = HashMap::new();

    // Chèn dữ liệu bằng phương thức insert
    scores.insert(String::from("An"), 85);
    scores.insert(String::from("Bình"), 92);

    // Ghi đè: Nếu chèn một khóa đã tồn tại, giá trị cũ sẽ bị thay thế bằng giá trị mới
    scores.insert(String::from("An"), 90); 

    // Kiểm tra và chèn thông minh bằng phương thức .entry()
    // Chỉ chèn nếu khóa "Chi" chưa hề tồn tại trước đó
    scores.entry(String::from("Chi")).or_insert(78);
    // Khóa "An" đã tồn tại, lệnh này sẽ bị bỏ qua và giữ nguyên điểm 90
    scores.entry(String::from("An")).or_insert(100); 

    println!("Bảng điểm hiện tại: {:?}", scores);
}

B. Tra cứu và duyệt dữ liệu trong HashMap

Code
use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert(String::from("An"), 90);
    scores.insert(String::from("Bình"), 88);

    let student_name = String::from("An");
    
    // Phương thức .get() trả về một Option<&V>
    match scores.get(&student_name) {
        Some(&score) => println!("Điểm của học sinh {student_name} là: {score}"),
        None => println!("Không tìm thấy học sinh trong hệ thống!"),
    }

    // Duyệt qua toàn bộ các cặp khóa - giá trị trong HashMap
    for (key, value) in &scores {
        println!("Học sinh: {key} - Điểm: {value}");
    }
}

📝 3. String Nâng Cao — Bản chất chuỗi trong Rust

Nhiều lập trình viên mới làm quen với Rust thường bối rối vì sự phân tách giữa kiểu &str (string slice tham chiếu tĩnh) và String (chuỗi động trên Heap). Để làm chủ hoàn toàn, bạn cần thấu hiểu bản chất bên dưới:

  • Bản chất của String: Thực chất String là một lớp bọc an toàn bao quanh một Vector chứa các byte dữ liệu thô (Vec<u8>), với điều kiện nghiêm ngặt là toàn bộ các byte đó phải tuân thủ chuẩn mã hóa UTF-8.
  • Tại sao không thể index trực tiếp (s[0]): Vì các ký tự trong chuẩn Unicode (đặc biệt là tiếng Việt có dấu hoặc emoji) có độ dài biến đổi từ 1 đến 4 byte, việc cho phép truy cập ký tự bằng chỉ mục số nguyên (s[0]) có thể vô tình cắt ngang một ký tự, dẫn đến lỗi hỏng dữ liệu. Rust chặn đứng điều này ngay từ khâu biên dịch.

Cách thao tác và cắt chuỗi an toàn trong thực tế:

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

    // Lấy slice an toàn (bạn phải tính toán chính xác ranh giới byte UTF-8)
    // Chữ "Xin" chiếm đúng 3 byte đầu tiên
    let s = &hello[0..3]; 
    println!("Cắt chuỗi: {s}");

    // Cách tốt nhất để đọc từng ký tự chuẩn Unicode là dùng phương thức .chars()
    println!("Duyệt từng ký tự:");
    for c in hello.chars() {
        println!(" - {c}");
    }
}

Thông qua các bộ sưu tập dữ liệu mạnh mẽ như Vector, HashMap và cấu trúc String chuẩn UTF-8, bạn đã sở hữu trọn vẹn những khối xây dựng cơ bản để xử lý mọi bài toán thao tác dữ liệu phức tạp trong các dự án thực tế.

Ở bài viết tiếp theo (Bài 11), chúng ta sẽ bước sang một chủ đề cực kỳ quan trọng đối với lập trình viên backend hiện đại: Xử lý bất đồng bộ trong Rust với Tokio và async/await!

Bình luận

Đăng nhập để để lại bình luận.
Chưa có bình luận nào cho bài viết này.

Bài viết liên quan