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ểm | Cách hoạt động | Cách dùng |
|---|---|---|
Copy | Ngầm định, tự động | Dữ liệu đơn giản chỉ nằm trên ngăn xếp |
Clone | Tườ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).
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ườngxvàycô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ảCopyvàCloneđể có thể sao chép ngầm định khi gán.NamedLocation: có trườngname(String) và các trườngx/y(i32) công khai. Vì struct này chứa mộtString, bạn chỉ có thể deriveClone, không thể deriveCopy. 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
GridPointcó 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
NamedLocationphải dùng.clone()để tạo bản sao mà vẫn giữ bản gốc hợp lệ
- Một
Đầ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})"
}
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
1Phương thức và hành vi
Nhập môn khối triển khaiTham số selfPhương thức có thể thay đổiHàm liên kếtNhiều khối triển khaiNối chuỗi phương thứcÔn tập: thao tác với hình chữ nhật4Dự án: Thú cưng ảo
Định nghĩa thú cưngCho thú cưng ăn7Các trait tiêu chuẩn
Trait DebugTrait DisplayClone và CopyCác trait so sánh bằngÔn tập: Điểm có thể in ra2Tính đóng gói và module
Kiến thức cơ bản về moduleTừ khóa publicTrường riêng tưHàm lấy giá trịHàm thiết lập giá trịÔn tập: tủ khóa an toàn5Kiểu tổng quát
Cấu trúc tổng quátPhương thức tổng quátNhiều kiểu tổng quátHàm tổng quátÔn tập: điểm tọa độTự luyện tập: Trình biên dịch Rust online