1 tháng học Rust - Quyền sở hữu

1 tháng học Rust - Quyền sở hữu

Tin ngắn hàng ngày dành cho bạn
  • Chà, quả là một bước tiến mới khi Microsoft vừa thông báo cập nhật cho mô hình Magma của họ. Cho những ai chưa biết thì đây là một mô hình dành cho AI agents, được thiết kế để xử lý các tương tác phức tạp trên cả môi trường ảo và thực.

    Để làm được điều đó, Magma được trang bị khả năng hiểu biết và dự đoán hành vi như con người. Magma có thể nhìn thấy, phân tích các thành phần và dự đoán công dụng cũng như các bước tiếp theo có thể xảy ra để hoàn thành một nhiệm vụ. Mình lấy ví dụ bạn sai khiến Magma vào Youtube, tìm kiếm một kênh nào đó rồi theo dõi. Cái này hoàn toàn có thể làm được bằng một số agent đã có từ trước, nhưng bản thân Magma là một agent rồi và nó biết được nó nên làm bước nào tiếp theo thay vì chúng ta phải viết mã để chỉ bảo từng bước cụ thể. Nghe hấp dẫn nhỉ.

    Chưa hết, Magma còn có thể "nhìn" theo thời gian thực, phân tích sự việc mà nó nhìn thấy để đưa ra hành động. Tưởng tượng ứng dụng vào công nghệ Robot thì sẽ thế nào? Chúng ta không cần viết mã từng li từng tí nữa mà chỉ cần ra lệnh bằng văn bản, bằng giọng nói thôi.

    Magma hiện chưa công bố mã nguồn, nhiều thông tin cho rằng cuối tháng này phiên bản đầu tiên sẽ được phát hành. Chúng ta cùng chờ xem nó ra sao nhé!

    » Xem thêm
  • Cũng giống như 12 Days of OpenAI - một chuỗi sự kiện diễn ra trong 12 ngày liên tiếp của OpenAI, mỗi ngày họ sẽ giới thiệu một công cụ "đột phá", và cứ như thế.

    DeepSeek đã bắt "trend" ngay sau đó với chuỗi 202502 Open-Source Week diễn ra ngay trong tuần sau. Mỗi ngày họ sẽ công bố một công cụ mã nguồn mở, trái được hoàn toàn với tính "Open" của AI. Chúng ta hãy chờ xem họ mang đến những dự án thú vị nào nhé 🤓. Chắc sẽ hấp dẫn lắm đây vì ai cũng biết từ lúc ra mô hình R1, Deepseek đã chiếm trọn tin tức nổi bật trên toàn thế giới.

    » Xem thêm
  • Grok 3 beta vừa ra mắt và cho mọi người dùng thử miễn phí có giới hạn số lần trong ngày (tài khoản trả phí hình như được dùng nhiều hơn). Trong đó có 2 tính năng nổi bật là Think và DeepSearch.

    Think thì chắc ai cũng biết hoặc dùng ở một số mô hình suy luận như ở ChatGPT rồi. Còn DeepSearch thì mới hơn, gõ điều bạn muốn vào thì nó sẽ tự lên mạng tìm kiếm thông tin rồi tổng hợp lại kết quả mà nó tìm thấy được. Khá hay nhưng chắc để tham khảo hoặc muốn tổng hợp thông tin nhanh chóng thôi chứ vẫn nên tự mình tìm kiếm thông tin 😅

    » Xem thêm

Vấn đề

Trong JavaScript hay một số ngôn ngữ khác, chúng ta nghe nhiều đến cụm từ "rò rỉ bộ nhớ" hay "memory leak". Điều này xảy ra khi ứng dụng chiếm bộ nhớ nhiều hơn những gì mà nó cần. Có nhiều nguyên nhân dẫn đến vấn đề này, một trong số đó là bộ nhớ không được giải phóng đúng cách.

Vấn đề của các ngôn ngữ lập trình sở hữu bộ thu gom rác tự động đó chính là làm sao để giải phóng bộ nhớ đã qua sử dụng đúng nơi và đúng lúc. Bởi chính từ trong tư tưởng của nó: "cứ chạy đi rồi rác cứ để tôi quét dọn cho". Nhưng làm sao để biết được một biến là có cần nữa hay không chứ? Đó chính là nhiệm vụ của thuật toán thu gom rác. Mặc dù chúng ta nghe đến nhiều đến thuật toán thu gom rác có đầy vẻ phấn khích, tuy nhiên đến một lúc nào đó, tràn bộ nhớ vẫn là vấn đề phải phải đối mặt một sớm một chiều.

Bộ dọn rác tự động

Tôi tin rằng có nhiều bạn ở đây biết đến cú pháp mallocfree trong ngôn ngữ lập trình C. malloc để cấp phát bộ nhớ cho một biến, còn free để giải phóng chúng. Lý do cho điều này bởi vì trong C không tích hợp sẵn một bộ thu gom rác tự động nên buộc chúng ta phải khai báo và giải phóng bộ nhớ một cách thủ công. Điều này giúp cho lập trình viên kiểm soát được lượng bộ nhớ mà chương trình được phép sử dụng. Tuy nhiên điều gì sẽ xảy ra nếu như họ quên free biến vừa malloc, hay free tận đến 2 lần?... Việc cấp phát thủ công sẽ dẫn đến sai sót mang yếu tố con người.

Trong Rust, chúng ta không có bộ thu gom rác tự động và cũng không có cơ chế cấp phát/giải phóng bộ nhớ thủ công. Vậy làm thế nào để Rust quản lý được bộ nhớ?

Quyền sở hữu

Hãy bắt đầu với một ví dụ đơn giản.

fn main() {
    let a: i32 = 1;
    let b: i32 = a;
    println!("a = {}", a);
    println!("b = {}", b);
}

Chạy chương trình trên, chúng ta thấy có 2 dòng in ra là "a = 1" và "b = 1". Cái này có gì lạ đâu, b = a thì rõ ràng là b cũng bằng 1 rồi còn gì nữa.

Được rồi, hãy sang ví dụ tiếp theo.

fn main() {
    let a = String::from("a");
    let b = a;
    println!("a = {}", a);
    println!("b = {}", b);
}

Dĩ nhiên kết quả là "a = a" và "b = a" rồi! Nhưng thực tế, chương trình trên báo lỗi.

Do cơ chế quyền sở hữu của Rust, giá trị a ban đầu là một String có giá trị "a", sau khi khai báo b = a tức là chúng ta đã nhượng quyền sở hữu String "a" từ a sang b, do đó, a lúc này không nắm giữ giá trị gì nữa và nó được giải phóng khỏi bộ nhớ.

Khoan đã, thế thì ở ví dụ đầu tiên chẳng phải a cũng được gán lại cho b sao? Hết sức đơn giản thôi, i32 là một giá trị được lưu trong stack, việc khai báo một biến bằng với một biến khác trong stack thì Rust sẽ lấy thẳng giá trị đó để gán. Nó giống như là khai báo b = 1.

Ngược lại, String là một giá trị được lưu trên heap. Rust có giải thích rằng, việc sao chép giá trị trong heap là tốn kém và không an toàn. Do đó, kết hợp với cả quyền sở hữu thì giá trị trên heap sẽ được chuyển giao lại cho biến được gán.

Bạn nên đọc tài liệu của Rust vì họ có sơ đồ mô phỏng sử dụng bộ nhớ và lúc nào thì nhượng quyền dữ liệu rất trực quan. Lấy một ví dụ.

let a = Box::new([0; 1_000_000]);
let b = a;

Và đây là cách họ trình bày Rust sử dụng bộ nhớ.

Rust sử dụng bộ nhớ

Tại L1, a sở hữu một giá trị trên heap. Sau đó a được gán cho b ở bước L2 do đó b sở hữu giá trị trên heap của a đồng thời a được giải phóng.

Khác với JavaScript, khi gán một biến với một biến tham chiếu đến kiểu dữ liệu là Object lập tức sinh ra hiện tượng references. Nghĩa là 2 biến đều trỏ đến một địa chỉ nhớ trong heap và cả 2 đều có quyền đọc/ghi dữ liệu có trên đó. Rust không cho phép làm điều đó.

Trong Rust có một số cách để tạo ra dữ liệu lưu trên heap. Có thể kể đến như Vec, String, HashMap hay là Box. Vì thế hãy cẩn trọng khi làm việc với bất kì dữ liệu nào lưu trên heap để tránh mất kiểm soát quyền sở hữu.

Vì vậy, quyền sở hữu trong Rust là một nguyên tắc quản lý heap. Bao gồm:

  • Tất cả dữ liệu trong heap phải được sở hữu bởi chính xác một biến.
  • Rust giải phóng dữ liệu heap khi chủ sở hữu của nó vượt quá phạm vi (scope).
  • Quyền sở hữu có thể được chuyển giao bằng cách gán lại hoặc truyền vào tham số hàm.
  • Dữ liệu heap chỉ có thể được truy cập thông qua chủ sở hữu hiện tại của nó.

Quay trở lại với ví dụ trên, có cách nào để gán giá trị mà không mất quyền sở hữu hay không? Đơn giản là hãy "sao chép" giá trị để tránh việc nhượng quyền bằng phương thức clone.

let a = Box::new([0; 1_000_000]);
let b = a.clone();

clone sao chép dữ liệu sang một ô nhớ khác và gán lại vào b. Do đó phép gán trên không phải là chuyển nhượng quyền sở hữu nữa và a tạm thời chưa được giải phóng.

Việc chuyển nhượng quyền sở hữu không chỉ diễn ra trong phép gán lại giá trị, mà còn trong cả tham số của hàm. Ví dụ.

fn main() {
    let first = String::from("Ferris");
    let full = add_suffix(first);
    println!("{full}, originally {first}"); // first is now used here
}

fn add_suffix(mut name: String) -> String {
    name.push_str(" Jr.");
    name
}

add_suffix là một hàm nhận vào name kiểu String. Nếu nhìn qua, chúng ta có thể dự đoán kết quả là Ferris Jr., originally Ferris. Tuy nhiên trên thực tế chương trình báo lỗi.

Thực tế từ khi gọi hàm add_suffix(first) thì first đã được chuyển nhượng quyền sở hữu cho add_suffix và sau đó nó được giải phóng, do đó dòng println! để in ra giá trị của first là không tồn tại nên bị lỗi.

Bạn đọc có thể thắc mắc tại sao tham số của add_suffix lại viết là mut name: String thì hãy nhớ lại tất cả các biến theo mặc định là bất biến, khi khai báo với mut thì chúng ta mới có thể gán lại giá trị cho nó. Thế nên trong thân hàm mới sử dụng được name.push_str(" Jr."); bởi vì push_str là một hàm gây đột biến (mutation) dữ liệu.

Để giải quyết vấn đề này, chúng ta có thể làm theo cách sau đây.

fn main() {
    let first = String::from("Ferris");
    let (first, full) = add_suffix(first);
    println!("{full}, originally {first}"); // first is now used here
}

fn add_suffix(name: String) -> (String, String) {
    let mut suffix_name = name.clone();
    suffix_name.push_str(" Jr.");
    (name, suffix_name)
}

Thay vì trả về mỗi mình name, add_suffix giờ đây trả về thêm cả namesuffix_name. Ý đồ ở đây là trả về cả tên cũ và tên mới để sau đó gán lại thông qua let (first, full) = add_suffix(first). Cuối cùng, chúng ta vẫn có firstfull tồn tại. Tuy nhiên cách làm này chưa phải là hay nhất, thay vào đó Rust giới thiệu cơ chế References and Borrowing (tham chiếu và vay mượn).

Chúng ta có thể tham chiếu đến các biến có dữ liệu trong heap và "mượn" chúng bằng cách sử dụng &.

fn main() {
    let m1 = String::from("Hello");
    let m2 = String::from("world");
    greet(&m1, &m2); 
    let s = format!("{} {}", m1, m2);
}

fn greet(g1: &String, g2: &String) {
    println!("{} {}!", g1, g2);
}

Mặc dù m1m2 đã được sử dụng cho tham số của greet nhưng nó chỉ là giá trị "vay mượn". Các biến được khai báo &m1, &m2 là các biến mượn, chúng chỉ trỏ đến các biến được mượn mà không thực sự sở hữu giá trị đó.

Rust mượn dữ liệu

Chà, có quá nhiều sự khó hiểu ở trong này, đừng lo lắng, tôi đã phải đọc đi đọc lại rất nhiều lần khái niệm quyền sở hữu này và cũng chưa hiểu thật rõ được chúng. Do đó, tôi khuyên bạn nên xem tài liệu Understanding Ownership để nghe giải thích một cách cặn kẽ hơn.

Cuối cùng, may mắn là Rust có tích hợp bộ kiểm tra mã rất mạnh mẽ. Hãy nhớ lại, trước khi chạy chương trình chúng ta cần build nó thành mã máy, ở trong bước build, Rust sẽ kiểm tra tất cả mọi thứ ví dụ như cú pháp và lỗi liên quan đến quyền sở hữu. Vì thế Rust sẽ nhắc nhở bạn ngay lập tức nếu bạn lỡ làm điều gì đó không đúng.

Tổng kết

Trong bài viết này tôi đã trình bày khái niệm về quyền sở hữu trong Rust, nói ngắn gọn đó là một nguyên tắc quản lý heap do Rust không có bộ thu gom rác tự động. Nhưng đó chưa phải là tất cả, chúng ta sẽ đi sâu vào nguyên lý của quyền sở hữu trong bài viết tiếp theo nhé!

Cao cấp
Hello

Tôi & khao khát "chơi chữ"

Bạn đã thử viết? Và rồi thất bại hoặc chưa ưng ý? Tại 2coffee.dev chúng tôi đã có quãng thời gian chật vật với công việc viết. Đừng nản chí, vì giờ đây chúng tôi đã có cách giúp bạn. Hãy bấm vào để trở thành hội viên ngay!

Bạn đã thử viết? Và rồi thất bại hoặc chưa ưng ý? Tại 2coffee.dev chúng tôi đã có quãng thời gian chật vật với công việc viết. Đừng nản chí, vì giờ đây chúng tôi đã có cách giúp bạn. Hãy bấm vào để trở thành hội viên ngay!

Xem tất cả

Đăng ký nhận thông báo bài viết mới

hoặc
* Bản tin tổng hợp được gửi mỗi 1-2 tuần, huỷ bất cứ lúc nào.

Bình luận (0)

Nội dung bình luận...