1 tháng học Rust - Đột biến dữ liệu và vay mượn quyền sở hữu

1 tháng học Rust - Đột biến dữ liệu và vay mượn quyền sở hữu

Tin ngắn hàng ngày dành cho bạn
  • Bắt đầu kỳ nghỉ tết rồi nên mình cũng không đăng bài nữa. Hẹn gặp lại các bạn qua tết nha 😁

    » Xem thêm
  • Tiếp tục về jj. Đang thắc mắc là nó mới thế liệu có ai làm mấy phần mềm dạng GUI cho dễ nhìn chưa. Kiểu giống như git thì có quá nhiều rồi không đếm xuể.

    May quá, tác giả tổng hợp lại luôn rồi Community-built tools around Jujutsu 🥳

    » Xem thêm
  • Turso thông báo rằng họ đang viết lại SQLite bằng Rust. Thế là lại có thêm một bằng chứng nữa cũng cố cho câu nói Rust đang "tái định nghĩa" lại nhiều thứ.

    Nhưng nguyên nhân sâu xa mới thú vị. Tại sao họ lại làm vậy? Ai cũng biết SQLite là nguồn mở, ai cũng có thể tạo bản sao (fork) để chỉnh sửa lại theo ý mình. Lẽ nào nhóm của Turso không thích hoặc không tin vào C - vốn là ngôn ngữ dùng để cấu thành SQLite.

    Mình xin kể chuyện một chút. Turso là một bên cung cấp dịch vụ máy chủ cơ sở dữ liệu dựa trên SQLite, họ đã thực hiện một vài tùy chỉnh trên bản sao của SQLite để phục vụ cho mục đích của mình, gọi nó là libSQL. Họ "hào phóng" cho cộng đồng đóng góp thoải mái.

    Quay trở lại SQLite là mã nguồn mở chứ không phải là đóng góp mở. Chỉ có một nhóm người đứng đằng sau duy trì mã nguồn này, và họ không tiếp nhận yêu cầu kéo (pull request) từ những người khác. Đồng nghĩa mọi thay đổi hoặc tính năng đều là của nhóm người này tạo ra. Có vẻ như SQLite rất phổ biến nhưng cộng đồng không thể làm điều mà họ muốn là đóng góp cho sự phát triển của nó.

    Chúng ta biết rằng hầu hết ứng dụng mã nguồn mở thường đi kèm với một thư mục "tests" với các bài kiểm tra rất nghiêm ngặt. Điều đó giúp cho sự cộng tác trong phát triển trở nên dễ dàng hơn. Nếu muốn chỉnh sửa hoặc thêm một tính năng mới, trước hết bạn cần phải đảm bảo sự thay đổi vượt qua được tất cả bài kiểm tra. Nhiều thông tin cho rằng SQLite không công khai bộ kiểm tra này. Điều này vô tình gây khó khăn cho những ai muốn chỉnh sửa mã nguồn. Vì họ không chắc chắn rằng liệu triển khai mới của mình có phù hợp với những tính năng cũ hay không.

    tursodatabase/limbo là dự án viết lại SQLite bằng Rust đã nhắc đến ở đầu bài. Họ nói rằng nó hoàn toàn tương thích với SQLite và nguồn mở hoàn toàn. limbo đang trong giai đoạn hoàn thiện. Chúng ta hãy chờ xem kết quả trong tương lai thế nào nhé. Bài viết chi tiết tại Introducing Limbo: A complete rewrite of SQLite in Rust.

    » Xem thêm

Vấn đề

Ở bài viết trước chúng ta biết về khái niệm quyền sở hữu trong Rust. Nhờ có nó, Rust biết khi nào một biến là không dùng nữa và giải phóng giá trị khỏi bộ nhớ. Bằng cách sử dụng & trước tên biến, chúng ta khai báo với Rust rằng chỉ “mượn” tạm giá trị mà không chuyển quyền sở hữu nên biến được mượn vẫn tồn tại sau khi mượn, chỉ có điều nó mất đi một số quyền.

Bài viết ngày hôm nay sẽ đi sâu vào cách Rust hoạt động với hành vi mượn dữ liệu. Làm thế nào để nó ngăn chặn được hành vi “không chuẩn mực” với dữ liệu được mượn và bị mượn trong thời gian biên dịch chứ không phải là thời gian chạy (runtime).

Rust tránh việc mượn và đột biến dữ liệu đồng thời

Đầu tiên hãy xem qua cách mà Rust xử lý bộ nhớ trong chương trình này.

let mut v: Vec<i32> = vec![1, 2, 3];
v.push(4);

Rust tránh việc mượn và đột biến dữ liệu đồng thời

Tại L2, hành động thêm một phần tử vào một danh sách đã gây nên đột biến, nhưng thay vì thêm 4 vào ô nhớ tiếp theo thì Rust đã sao chép toàn bộ dữ liệu sang một vùng nhớ khác, điều đó khiến cho dữ liệu tại heap của v tại L1 đã bị giải phóng, v lúc này trỏ đến một ô nhớ khác.

Nếu như vậy thì điều gì sẽ xảy ra với chương trình dưới đây.

let mut v: Vec<i32> = vec![1, 2, 3];
let num: &i32 = &v[2];
v.push(4);
println!("Third element is {}", *num);

Chúng ta thấy num đang mượn giá trị tại vị trí thứ 2 của v thông qua cách gọi &v[2]. Nhưng sau hành động push, giá trị heap của v ban đầu đã bị hủy bỏ, vậy thì num lúc này trỏ đến đâu? Bạn nghĩ đúng rồi đấy, chương trình trên báo lỗi.

Rust tránh việc mượn và đột biến dữ liệu đồng thời - 2

Do đó, Rust phát biểu nguyên tắc an toàn của con trỏ: Dữ liệu không bao giờ được đặt bí danh (vay mượn) và thay đổi cùng một lúc.

Vì sinh ra cơ chế vay mượn, cho nên Rust cần tạo nên các quy tắc để đảm bảo "Nguyên tắc an toàn của con trỏ", đó chính là công cụ kiểm tra vay mượn (borrow checker).

Borrow checker

Ý tưởng cốt lõi Rust sinh ra loại 3 quyền sở hữu dữ liệu của một biến.

  • Read (R): Dữ liệu có thể được sao chép sang vị trí khác.
  • Write (W): Dữ liệu có thể được thay đổi.
  • Own (O): Dữ liệu có thể được di chuyển hoặc xóa.

Các quyền này chỉ tồn tại tại thời điểm biên dịch, tức là trình biên dịch "tự biên tự diễn" ra các quyền này để kiểm tra trương trình của bạn có hợp lệ hay không trước khi build thành binary.

Mặc định một biến có các quyền R, O trên dữ liệu của nó. Nếu một biến khai báo với mut, nó sẽ có thêm quyền W.

Hãy xem xét cách Rust kiểm tra quyền dữ liệu của biến trong chương trình sau.

Rust kiểm tra quyền

Đầu tiên, v được khai báo với mut nên nó có 3 quyền R, O, W. Sau đó num mượn giá trị của v, lúc này v bị mất 2 quyền W, O, bù lại num có quyền R, O trên dữ liệu mà nó mới mượn được. *num là biến đọc giá trị tại nơi vay mượn, nó chỉ có quyền R.

Ngay sau lệnh println!, num được giải phóng do đó v được khôi phục lại tất cả các quyền còn num*num mất hết quyền trên dữ liệu mà nó mượn.

Sau lệnh push, v không còn được dùng nữa cho nên nó cũng mất hết quyền trên dữ liệu của nó.

Rust gọi tất cả thứ nằm bên trái của phép gán (=) là paths. Từ đó, quyền sở hữu dữ liệu được xác định trên paths chứ không đơn thuần là trên các biến nữa. paths bao gồm:

  • Các biến, như a.
  • *a.
  • Truy cập vào phần tử trong mảng: a[0].
  • a.0 của Tuble hoặc a.field của Structs.
  • Và một số truy cập phức tạp khác: *((*a)[0].1) !?

Vậy thì một biến mượn dữ liệu của biến khác, nó có thay đổi được dữ liệu đó không? Hãy nhớ lại khái niệm: “Dữ liệu không bao giờ được đặt bí danh (vay mượn) và thay đổi cùng một lúc”, tức là chỉ 1 trong 2 được phép xảy ra cùng một lúc.

let mut v: Vec<i32> = vec![1, 2, 3];
let num: &mut i32 = &mut v[2];
*num += 1;
println!("Third element is {}", *num);
println!("Vector is now {:?}", v);

Rust kiểm tra quyền - 2

Thay vì &v[2], giờ đây chúng ta khai báo &mut v[2] để thông báo rằng num có quyền O với dữ liệu nó mượn. Biến *num đọc thẳng dữ liệu trong heap và tăng nó lên 1. Đến cuối cùng, v cũng bị thay đổi theo num.

So với ví dụ ban đầu, v bị mất hoàn toàn tất cả quyền khi num mượn một dữ liệu có quyền O. Điều này đảm bảo an toàn dữ liệu và tuân thủ nguyên tắc “Dữ liệu không bao giờ được đặt bí danh (vay mượn) và thay đổi cùng một lúc”.

Dữ liệu phải tồn tại lâu hơn các tham chiếu của nó

Hãy bắt đầu với một ví dụ:

let s = String::from("Hello world");
let s_ref = &s;
drop(s);
println!("{}", s_ref);

Đoạn mã trên đang cố gắng xóa s trong khi s_ref thì đang mượn giá trị của s. Nhớ lại kiến thức ở trên, s lúc này tạm thời bị mất quyền O cho nên drop không làm gì được vì nó yêu cầu phải có quyền O mới được phép. Chính vì thế chương trình trên báo lỗi.

Chúng ta, cũng như Rust có thể nhận biết được thời gian tồn tại của s bằng mắt thường một cách nhanh chóng, tuy nhiên, chương trình không phải lúc nào cũng đơn giản, buộc Rust phải có cách nhận biết được thời gian tồn tại đủ lâu của một biến trong chương trình để tránh trường hợp như trên.

Hãy xem xét ví một hàm này.

fn first_or(strings: &Vec<String>, default: &String) -> &String {
    if strings.len() > 0 {
        &strings[0]
    } else {
        default
    }
}

Hàm có thể trả về String đầu tiên của tham số strings hoặc cũng có thể trả về tham số default. Sẽ ra sao nếu như chúng ta gọi hàm như thế này.

fn main() {
    let strings = vec![];
    let default = String::from("default");
    let s = first_or(&strings, &default);
    drop(default);
    println!("{}", s);
}

strings là rỗng, nên s bằng tham chiếu của default, tuy nhiên một lệnh drop(default) được gọi trước khi in ra s. Do đó chương trình này là không an toàn và nó bị lỗi.

Một ví dụ khác về chương trình không an toàn.

fn return_a_string() -> &String {
    let s = String::from("Hello world");
    let s_ref = &s;
    s_ref
}

s_ref đang cố gắng mượn giá trị của s, tuy nhiên kết thúc hàm thì s sẽ được giải phóng, việc trả về s_ref lúc này là vô nghĩa vì thế chương trình trên báo lỗi.

Tổng kết

Vậy là kết thúc bài viết nói về quyền sở hữu và cách Rust kiểm tra quyền vay mượn dữ liệu. Theo cảm nhận của tôi thì đây là phần khái niệm khó hiểu nhất của Rust, nhưng nó lại là khái niệm quan trọng nếu muốn viết và gỡ lỗi chương trình. Bạn đọc có thể tiếp tục đọc thêm bài viết Fixing Ownership Errors để biết cách sửa một số lỗi thường gặp với quyền sở hữu.

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 (2)

Nội dung bình luận...
Avatar
Ẩn danh1 năm trước
"lúc này v bị mất 2 quyền W, O, bù lại num có quyền R, O trên dữ liệu mà nó mới mượn được". References không chiếm quyền sở hữu, nó chỉ mượn (borrowing), giống như biến, ref cũng có 2 kiểu mượn là immutable vs mutable nhưng có phụ thuộc vào owner, nếu owner khai bao với mutable thì ref với được mutable
Trả lời
Avatar
Ẩn danh1 năm trước
"Tại L2, hành động thêm một phần tử vào một danh sách đã gây nên đột biến, nhưng thay vì thêm 4 vào ô nhớ tiếp theo thì Rust đã sao chép toàn bộ dữ liệu sang một vùng nhớ khác". chỗ này không phải lúc nào cũng cũng được cấp phát mới vùng nhớ, nó phục thuộc vào cái cap của Vec, khi len > cap thì Vec mới được cấp phát với cap mới là x2, vì vậy để tối ưu chương trình nếu biết len của Vec cần sử dụng thì thường người ta sẽ khởi tạo nó với cap được xác định trước để tránh bị cấp phái lại nhiều lần
Trả lời
Bấm hoặc cuộn mạnh để sang bài mới