Lời nói đầu : Câu trả lời này được viết trước khi chọn tham gia các đặc điểm tích hợp — cụ thể là các Copykhía cạnh — được triển khai. Tôi đã sử dụng dấu ngoặc kép để chỉ ra các phần chỉ áp dụng cho sơ đồ cũ (chương trình được áp dụng khi câu hỏi được hỏi).
Cũ : Để trả lời câu hỏi cơ bản, bạn có thể thêm trường đánh dấu lưu trữ một NoCopygiá trị . Ví dụ
struct Triplet {
one: int,
two: int,
three: int,
_marker: NoCopy
}
Bạn cũng có thể làm điều đó bằng cách có một trình hủy (thông qua triển khai Dropđặc điểm ), nhưng sử dụng các loại điểm đánh dấu sẽ được ưu tiên hơn nếu trình hủy không làm gì cả.
Các kiểu bây giờ di chuyển theo mặc định, nghĩa là khi bạn xác định một kiểu mới, nó không triển khai Copytrừ khi bạn triển khai rõ ràng nó cho kiểu của mình:
struct Triplet {
one: i32,
two: i32,
three: i32
}
impl Copy for Triplet {} // add this for copy, leave it out for move
Việc triển khai chỉ có thể tồn tại nếu mọi loại có trong mới structhoặc enumlà chính nó Copy. Nếu không, trình biên dịch sẽ in thông báo lỗi. Nó cũng chỉ có thể tồn tại nếu kiểu không có Droptriển khai.
Để trả lời câu hỏi mà bạn chưa hỏi ... "có gì trong các bước di chuyển và sao chép?":
Đầu tiên, tôi sẽ xác định hai "bản sao" khác nhau:
- một bản sao byte , chỉ là sao chép nông cạn một đối tượng từng byte, không theo sau các con trỏ, ví dụ nếu bạn có
(&usize, u64), nó là 16 byte trên máy tính 64 bit và một bản sao cạn sẽ lấy 16 byte đó và sao chép giá trị trong một số đoạn bộ nhớ 16 byte khác, mà không chạm vào giá trị usizeở đầu kia của &. Đó là, nó tương đương với việc gọi điện memcpy.
- một bản sao ngữ nghĩa , sao chép một giá trị để tạo một bản sao mới (phần nào) độc lập có thể được sử dụng riêng một cách an toàn với bản cũ. Ví dụ: một bản sao ngữ nghĩa của một
Rc<T>chỉ liên quan đến việc tăng số lượng tham chiếu và một bản sao ngữ nghĩa của một Vec<T>liên quan đến việc tạo một phân bổ mới, sau đó sao chép ngữ nghĩa từng phần tử được lưu trữ từ cũ sang mới. Đây có thể là các bản sao sâu (ví dụ Vec<T>) hoặc nông (ví dụ: Rc<T>không chạm vào phần được lưu trữ T), Cloneđược định nghĩa một cách lỏng lẻo là khối lượng công việc nhỏ nhất cần thiết để sao chép ngữ nghĩa một giá trị kiểu Ttừ bên trong a &Tđến T.
Rust giống như C, mỗi lần sử dụng theo giá trị của một giá trị là một bản sao byte:
let x: T = ...;
let y: T = x; // byte copy
fn foo(z: T) -> T {
return z // byte copy
}
foo(y) // byte copy
Chúng là các bản sao byte cho dù có Tdi chuyển hay không hoặc " có thể sao chép ngầm". (Nói rõ hơn, chúng không nhất thiết phải là các bản sao theo từng byte theo nghĩa đen tại thời điểm chạy: trình biên dịch có thể tự do tối ưu hóa các bản sao nếu hành vi của mã được bảo toàn.)
Tuy nhiên, có một vấn đề cơ bản với các bản sao byte: bạn kết thúc với các giá trị bị trùng lặp trong bộ nhớ, điều này có thể rất tệ nếu chúng có hàm hủy, ví dụ:
{
let v: Vec<u8> = vec![1, 2, 3];
let w: Vec<u8> = v;
} // destructors run here
Nếu wchỉ là một bản sao byte đơn thuần vthì sẽ có hai vectơ trỏ đến cùng một phân bổ, cả hai đều có hàm hủy giải phóng nó ... gây ra một miễn phí kép , đó là một vấn đề. NB. Điều này sẽ hoàn toàn ổn, nếu chúng ta tạo một bản sao ngữ nghĩa của vthành w, vì nó wsẽ là bản sao độc lập của riêng nó Vec<u8>và các hàm hủy sẽ không giẫm đạp lên nhau.
Có một số bản sửa lỗi có thể có ở đây:
- Hãy để lập trình viên xử lý nó, như C. (không có hàm hủy trong C, vì vậy nó không tệ lắm ... thay vào đó bạn chỉ bị rò rỉ bộ nhớ.: P)
- Thực hiện một bản sao ngữ nghĩa một cách ngầm định, để bản sao đó
wcó phân bổ riêng, như C ++ với các hàm tạo bản sao của nó.
- Hãy coi việc sử dụng theo giá trị như một sự chuyển giao quyền sở hữu, vì vậy nó
vkhông thể được sử dụng nữa và không chạy trình hủy của nó.
Cuối cùng là những gì Rust làm: một động thái chỉ là sử dụng theo giá trị trong đó nguồn không hợp lệ tĩnh, vì vậy trình biên dịch ngăn việc sử dụng thêm bộ nhớ hiện không hợp lệ.
let v: Vec<u8> = vec![1, 2, 3];
let w: Vec<u8> = v;
println!("{}", v); // error: use of moved value
Các loại có hàm hủy phải di chuyển khi được sử dụng theo giá trị (hay còn gọi là khi sao chép byte), vì chúng có quyền quản lý / quyền sở hữu một số tài nguyên (ví dụ: cấp phát bộ nhớ hoặc xử lý tệp) và rất ít khả năng bản sao byte sẽ sao chép chính xác điều này quyền sở hữu.
"Chà ... một bản sao ngầm là gì?"
Hãy nghĩ về một kiểu nguyên thủy như u8: một bản sao byte đơn giản, chỉ cần sao chép byte đơn, và bản sao ngữ nghĩa cũng đơn giản như vậy, sao chép byte đơn. Đặc biệt, một bản sao byte là một bản sao ngữ nghĩa ... Rust thậm chí còn có một đặc điểm tích hợpCopy là nắm bắt những loại có ngữ nghĩa và bản sao byte giống hệt nhau.
Do đó, đối với các Copyloại này, việc sử dụng theo giá trị cũng tự động là các bản sao ngữ nghĩa và vì vậy, hoàn toàn an toàn khi tiếp tục sử dụng nguồn.
let v: u8 = 1;
let w: u8 = v;
println!("{}", v); // perfectly fine
Cũ : Điểm NoCopyđánh dấu ghi đè hành vi tự động của trình biên dịch là giả định rằng các kiểu có thể là Copy(tức là chỉ chứa tổng hợp các nguyên thủy và &) là Copy. Tuy nhiên, điều này sẽ thay đổi khi các đặc điểm tích hợp lựa chọn được triển khai.
Như đã đề cập ở trên, các đặc điểm tích hợp chọn-in được thực hiện, vì vậy trình biên dịch không còn có hành vi tự động nữa. Tuy nhiên, quy tắc được sử dụng cho hành vi tự động trước đây là quy tắc giống nhau để kiểm tra xem nó có hợp pháp để thực hiện hay không Copy.