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 Copy
khí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 NoCopy
giá 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 Copy
trừ 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 struct
hoặc enum
là 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ó Drop
triể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 T
từ 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ó T
di 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 w
chỉ là một bản sao byte đơn thuần v
thì 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 v
thành w
, vì nó w
sẽ 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 đó
w
có 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ó
v
khô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 Copy
loạ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
.