Menu
CoddyTech

Clone và Copy

Thuộc phần Lập trình hướng đối tượng trong hành trình Rust của Coddy. Bài học 37 trên 61.

Trong Rust, khi bạn gán một giá trị cho biến (variable) khác, quyền sở hữu (ownership) thường được chuyển đi: biến ban đầu trở nên không hợp lệ. Tuy nhiên, hai trait tiêu chuẩn sẽ thay đổi hành vi này: Clone và Copy.

Trait Copy cho phép sao chép bit ngầm định. Khi một kiểu dữ liệu triển khai Copy, việc gán nó cho một biến khác sẽ tự động tạo bản sao thay vì chuyển quyền sở hữu:

#[derive(Copy, Clone)]
struct Point {
    x: i32,
    y: i32,
}

let p1 = Point { x: 10, y: 20 };
let p2 = p1;  // p1 được sao chép, không bị chuyển đi

println!("{}, {}", p1.x, p2.x);  // Cả hai đều hợp lệ!

Trait Clone cung cấp khả năng sao chép sâu tường minh thông qua phương thức .clone(). Trait này là yêu cầu bắt buộc khi bạn suy ra Copy, nhưng cũng có thể dùng riêng cho các kiểu cần sao chép tường minh:

let p3 = p1.clone();  // Sao chép tường minh

Có một hạn chế quan trọng: chỉ có thể tự động triển khai Copy cho các kiểu mà mọi trường đều triển khai Copy. Các kiểu đơn giản như số nguyên và số thực triển khai Copy, nhưng String thì không. Kiểu này quản lý bộ nhớ trên heap. Nếu cấu trúc của bạn chứa String, bạn chỉ có thể tự động triển khai Clone, không thể tự động triển khai Copy.

Đặc điểmCách hoạt độngCách dùng
CopyNgầm định, tự độngDữ liệu đơn giản chỉ nằm trên ngăn xếp
CloneTường minh qua .clone()Mọi dữ liệu có thể sao chép

Với các cấu trúc đơn giản (struct) có trường kiểu nguyên thủy, việc tự động triển khai (derive) cả hai đặc tính (trait) giúp bạn tự do gán giá trị mà không phải lo về quyền sở hữu (ownership).

challenge icon

Thử thách

Dễ

Hãy xây dựng một hệ tọa độ để minh họa sự khác nhau giữa các trait Copy và Clone! Bạn sẽ tạo hai struct, một struct có thể được sao chép ngầm định và một struct cần được nhân bản tường minh, để xem Rust xử lý việc sao chép khác nhau như thế nào dựa trên các trait mà bạn derive.

Bạn sẽ sắp xếp code trong hai tệp:

  • coordinates.rs: Định nghĩa hai struct công khai đại diện cho các kiểu tọa độ khác nhau:
    • GridPoint: có các trường x và y công khai (cả hai đều là i32). Vì struct này chỉ chứa các kiểu nguyên thủy, hãy derive cả Copy và Clone để có thể sao chép ngầm định khi gán.
    • NamedLocation: có trường name (String) và các trường x/y (i32) công khai. Vì struct này chứa một String, bạn chỉ có thể derive Clone, không thể derive Copy. Struct này cần các lời gọi .clone() tường minh để tạo bản sao.
  • main.rs: Đưa module coordinates vào và minh họa cách mỗi struct hoạt động khác nhau khi được sao chép. Tạo các instance bằng những đầu vào đã cho, sau đó cho thấy rằng:
    • Một GridPoint có thể được gán cho một biến khác mà cả hai vẫn hợp lệ (sao chép ngầm định)
    • Một NamedLocation phải dùng .clone() để tạo bản sao mà vẫn giữ bản gốc hợp lệ

Đầu ra của bạn cần hiển thị cả giá trị gốc và giá trị đã sao chép/nhân bản để chứng minh cả hai biến vẫn hợp lệ sau khi sao chép:

Original point: ({x}, {y})
Copied point: ({x}, {y})
Original location: {name} at ({x}, {y})
Cloned location: {name} at ({x}, {y})

Ví dụ, với các đầu vào 5, 10 và Home:

Original point: (5, 10)
Copied point: (5, 10)
Original location: Home at (5, 10)
Cloned location: Home at (5, 10)

Bạn sẽ nhận ba đầu vào: tọa độ x (phân tích thành i32), tọa độ y (phân tích thành i32) và tên địa điểm.

Tự làm thử

mod coordinates;

use coordinates::{GridPoint, NamedLocation};

fn main() {
    // Đọc các đầu vào
    let mut input1 = String::new();
    std::io::stdin().read_line(&mut input1).expect("Failed to read line");
    let x: i32 = input1.trim().parse().expect("Invalid number");

    let mut input2 = String::new();
    std::io::stdin().read_line(&mut input2).expect("Failed to read line");
    let y: i32 = input2.trim().parse().expect("Invalid number");

    let mut input3 = String::new();
    std::io::stdin().read_line(&mut input3).expect("Failed to read line");
    let name = input3.trim().to_string();

    // TODO: Tạo một instance GridPoint với x và y

    // TODO: Minh họa cách sao chép ngầm định bằng cách gán cho một biến khác
    // (GridPoint triển khai Copy, nên thao tác này tự động tạo một bản sao)

    // TODO: In ra điểm gốc và điểm đã sao chép
    // Định dạng: "Original point: ({x}, {y})"
    // Định dạng: "Copied point: ({x}, {y})"

    // TODO: Tạo một instance NamedLocation với name, x và y

    // TODO: Minh họa cách clone tường minh (NamedLocation chỉ triển khai Clone, không triển khai Copy)
    // Dùng .clone() để tạo một bản sao

    // TODO: In ra vị trí gốc và vị trí đã clone
    // Định dạng: "Original location: {name} at ({x}, {y})"
    // Định dạng: "Cloned location: {name} at ({x}, {y})"
}
quiz iconTự kiểm tra

Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.

Tất cả bài học trong Lập trình hướng đối tượng

Tự luyện tập: Trình biên dịch Rust online