[Hướng dẫn Rust] 4.3. Sở hữu và Hàm

08 tháng 4, 2026·6 phút đọc

Bài viết này đi sâu vào cơ chế sở hữu (ownership) của Rust khi làm việc với hàm. Chúng ta sẽ tìm hiểu về việc truyền tham số (di chuyển hay sao chép), cách quyền sở hữu chuyển đổi qua giá trị trả về, và giải pháp tạm thời khi muốn sử dụng dữ liệu mà không mất quyền sở hữu.

[Hướng dẫn Rust] 4.3. Sở hữu và Hàm

Sau khi đã nắm được các khái niệm lập trình chung của Rust, chúng ta đến với phần quan trọng nhất của ngôn ngữ này đó là sở hữu (ownership). Đây là một khái niệm khá khác biệt so với các ngôn ngữ khác và nhiều người mới bắt đầu thường thấy khó khăn khi học. Chương này nhằm giúp các bạn làm chủ hoàn toàn tính năng này.

Chương này gồm ba phần nhỏ:

  • Sở hữu: Bộ nhớ Stack vs Bộ nhớ Heap
  • Quy tắc Sở hữu, Bộ nhớ và Cấp phát
  • Sở hữu và Hàm (bài viết này)

Nếu bạn thấy bài viết hữu ích, hãy like, bookmark và theo dõi để tiếp tục đồng hành cùng series này.

Truyền giá trị cho hàm

Về mặt ngữ nghĩa, việc truyền một giá trị cho hàm tương tự như việc gán giá trị đó cho một biến. Nói một cách ngắn gọn: việc truyền tham số cho hàm hoạt động y hệt như gán biến.

Cụ thể hơn, việc truyền giá trị cho hàm sẽ gây ra một trong hai hành vi: di chuyển (move) hoặc sao chép (copy).

  • Đối với các kiểu dữ liệu triển khai Copy trait, hành vi sao chép sẽ xảy ra, do đó biến gốc không bị ảnh hưởng và vẫn có thể tiếp tục sử dụng.
  • Đối với các kiểu dữ liệu không triển khai Copy trait, hành vi di chuyển sẽ xảy ra, biến gốc sẽ bị vô hiệu hóa và không thể sử dụng được nữa.

(Một giới thiệu chi tiết về Copy trait, move và copy đã được trình bày trong bài viết trước 4.2. Quy tắc Sở hữu, Bộ nhớ và Cấp phát, nên tôi sẽ không lặp lại ở đây).

fn main() {
    let machine = String::from("6657");
    wjq(machine);

    let x = 6657;
    wjq_copy(x);
    println!("x is: {}", x);
}

fn wjq(some_string: String) {
    println!("{}", some_string);
}

fn wjq_copy(some_number: i32) {
    println!("{}", some_number);
}
  • Đối với biến machine:

    • String là kiểu dữ liệu phức tạp, được cấp phát trên heap và không triển khai Copy trait.
    • Khi machine được truyền vào hàm wjq, một hành vi di chuyển (move) xảy ra, nghĩa là quyền sở hữu được chuyển từ biến machine sang tham số hàm some_string.
    • Lúc này, quyền sở hữu của machine đã bị chuyển đi. Hàm wjq có thể sử dụng nó bình thường, nhưng biến gốc machine không còn khả dụng nữa. Nếu bạn cố gắng sử dụng machine sau đó, trình biên dịch sẽ báo lỗi.
  • Đối với biến x:

    • i32 là kiểu dữ liệu cơ bản có kích thước cố định, được cấp phát trên stack và triển khai Copy trait.
    • Khi x được truyền vào hàm wjq_copy, hành vi sao chép (copy) xảy ra, nghĩa là giá trị của x được sao chép và truyền vào tham số hàm some_number.
    • Vì đây chỉ là sao chép giá trị, biến gốc x không bị ảnh hưởng và vẫn có thể sử dụng sau khi gọi hàm.
  • Đối với biến some_string:

    • Phạm vi của nó bắt đầu khi được khai báo ở dòng 10 và kết thúc khi gặp dấu } ở dòng 12.
    • Khi ra khỏi phạm vi, Rust tự động gọi hàm drop để giải phóng bộ nhớ mà some_string đang chiếm giữ.
  • Đối với biến some_number:

    • Phạm vi của nó bắt đầu khi được khai báo ở dòng 14 và kết thúc khi gặp dấu } ở dòng 16.
    • Không có gì đặc biệt xảy ra khi nó ra khỏi phạm vi, vì các kiểu triển khai Copy trait không gọi Drop khi hết phạm vi.

Giá trị trả về và Phạm vi

Quyền sở hữu cũng được chuyển giao trong quá trình trả về giá trị từ một hàm.

fn main() {
    let s1 = give_ownership();
    let s2 = String::from("6657");
    let s3 = takes_and_gives_back(s2);
}

fn give_ownership() -> String {
    let some_string = String::from("machine");
    some_string
}

fn takes_and_gives_back(a_string: String) -> String {
    a_string
}
  • Hành vi của hàm give_ownership:

    • Hàm give_ownership tạo ra một biến String tên là some_string, và quyền sở hữu nó thuộc về hàm give_ownership.
    • Khi some_string được trả về làm giá trị trả về của hàm, quyền sở hữu được chuyển cho người gọi, cụ thể là biến s1.
    • Kết quả là some_string sẽ không bị drop sau khi ra khỏi phạm vi của give_ownership, vì quyền sở hữu của nó đã được chuyển cho s1.
  • Hành vi của hàm takes_and_gives_back:

    • Hàm takes_and_gives_back nhận một tham số Stringa_string. Khi hàm được gọi, quyền sở hữu của đối số truyền vào (s2) được chuyển cho tham số hàm a_string.
    • Khi hàm trả về a_string, quyền sở hữu lại được chuyển một lần nữa từ a_string sang người gọi, cụ thể là biến s3.
    • Lúc này, s2 không còn khả dụng nữa, vì quyền sở hữu của nó đã được chuyển cho takes_and_gives_back, và giá trị trả về của hàm được gán cho s3.

Quyền sở hữu của một biến luôn tuân theo cùng một mẫu:

  • Gán giá trị cho một biến khác gây ra di chuyển (move). Chỉ các kiểu triển khai Copy trait, như các kiểu cơ bản i32f64, mới được sao chép (copy) khi gán.
  • Khi một biến chứa dữ liệu trên heap ra khỏi phạm vi, giá trị của nó sẽ được dọn dẹp bởi hàm drop, trừ khi quyền sở hữu của dữ liệu đó đã được di chuyển sang một biến khác.

Để hàm sử dụng giá trị mà không lấy quyền sở hữu

Đôi khi mục đích của mã là để một hàm sử dụng một biến, nhưng bạn không muốn mất quyền sử dụng dữ liệu đó. Trong trường hợp đó, bạn có thể viết như sau:

fn main() {
    let s1 = String::from("Hello");
    let (s2, len) = calculate_length(s1);
    println!("The length of '{}' is {}", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len();
    (s, length)
}

Trong ví dụ này, s1 phải chuyển quyền sở hữu cho s, nhưng khi hàm này trả về, nó cũng trả về s nguyên vẹn và chuyển quyền sở hữu dữ liệu cho s2. Theo cách này, quyền sở hữu của dữ liệu được trả lại cho một biến trong hàm main, cho phép dữ liệu của s1 được sử dụng lại trong main (dù tên biến đã thay đổi).

Cách tiếp cận này quá rắc rối và vụng về. Rust cung cấp một tính năng cho tình huống này gọi là tham chiếu (reference), cho phép một hàm sử dụng một giá trị mà không cần lấy quyền sở hữu của nó. Tính năng này sẽ được giải thích trong bài viết tiếp theo.

Bài viết được tổng hợp và biên soạn bằng AI từ các nguồn tin tức công nghệ. Nội dung mang tính tham khảo. Xem bài gốc ↗