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

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

Những mẩu tin ngắn hàng ngày dành cho bạn
  • Đây! Một vấn đề mà từ xưa đến nay mình cứ thắc mắc mãi, và cho đến hôm qua thì mọi thứ đã sáng tỏ.

    Bình thường mọi người dùng height: 100vh để đặt chiều cao bằng với viewport của màn hình. Trên máy tính thì không vấn đề gì, thậm chí giả lập kích thước của điện thoại thông minh thì mọi thứ vẩn ổn. Nhưng khi mở trên điện thoại thì height 100vh lúc nào cũng vượt quá viewport. Ủa!? Là sao???

    Lý giải cho điều này là do trên thiết bị di động có cách tính viewport khác với máy tính. Nó thường bị can thiệp hay ảnh hưởng bởi thanh địa chỉ, thanh điều hướng của nền tảng mà bạn đang sử dụng. Vậy nên nếu muốn 100vh trên di động đúng bằng viewport thì cần phải làm thêm một bước thiết lập lại viewport.

    Dễ lắm, đầu tiên cần tạo một css variable --vh ở ngay thẻ script đầu trang.

    function updateViewportHeight() { const viewportHeight = globalThis.innerHeight; document.documentElement.style.setProperty('--vh', `${viewportHeight * 0.01}px`); } updateViewportHeight(); window.addEventListener('resize', updateViewportHeight);

    Sau đó thay vì dùng height: 100vh thì chuyển thành height: calc(var(--vh, 1vh) * 100). Thế là xong.

    » Xem thêm
  • Cả ngày hôm nay mình dành thời gian để làm giao diện tiếp thị cho gói hội viên của 2coffee.dev. Vậy là cuối cùng thì cũng chính thức đi vào vào con đường mà 5 năm trước cũng không ngờ đến được: "Bán một cái gì đó". Người ta thường nói "Cho đi để nhận lại", bên cạnh đó cũng có câu "Nếu giỏi một cái gì đó, đừng làm nó miễn phí". Nếu theo dõi đủ lâu, bạn đọc sẽ thấy chẳng có gì mình giấu giếm. Biết gì viết nấy, và đôi khi nhờ viết ra mà nhận lại được sự góp ý của độc giả. Từ đó giúp mình hoàn thiện bản thân nhiều hơn.

    Membership là tính năng mà mình sắp sửa giới thiệu. Trở thành hội viên của blog, bạn sẽ có một số đặc quyền nhất định, ví dụ như truy cập vào các bài viết chỉ dành riêng cho hội viên. Các bài viết này về các chủ đề chuyên sâu và được hệ thống hoá sao cho dễ đọc và dễ nắm bắt nhất. Qua đó cung cấp thêm nhiều kiến thức và trau dồi kỹ năng cho bạn đọc.

    Để đạt được đến ngày hôm nay là công rất lớn của các bạn đọc giả, của những người yêu mến 2coffee.dev. Nhờ các bạn mà blog mới có ngày hôm nay. Bên cạnh đó, bản thân mình cũng phải thay đổi liên tục, phải vượt ra khỏi vùng an toàn, làm những điều mà trước nay không dám. Dù sao đi nữa thì đây cũng mới là khởi đầu cho mọi sự gian nan. Nhưng đừng bao giờ nản nha các bạn ơi 😄

    » Xem thêm
  • Ngày nay, 1 triệu (1M) tác vụ đồng thời sẽ tiêu tốn bao nhiêu bộ nhớ? Đó là câu hỏi của hez2010 và anh đã quyết định đi tìm câu trả lời, bằng cách thử nghiệm một chương trình đơn giản trên nhiều ngôn ngữ lập trình khác nhau: How Much Memory Do You Need in 2024 to Run 1 Million Concurrent Tasks?

    Tóm tắt lại thì Rust vẫn vô đối, nhưng vị trí thứ 2 mới làm tôi cảm thấy ngạc nhiên 😳

    » 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.
Author

Xin chào, tôi tên là Hoài - một anh Dev kể chuyện bằng cách viết ✍️ và làm sản phẩm 🚀. Với nhiều năm kinh nghiệm lập trình, tôi đã đóng góp một phần công sức cho nhiều sản phẩm mang lại giá trị cho người dùng tại nơi đang làm việc, cũng như cho chính bản thân. Sở thích của tôi là đọc, viết, nghiên cứu... Tôi tạo ra trang Blog này với sứ mệnh mang đến những bài viết chất lượng cho độc giả của 2coffee.dev.Hãy theo dõi tôi qua các kênh LinkedIn, Facebook, Instagram, Telegram.

Bạn thấy bài viết này có ích?
Không

Bình luận (0)

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