Có thể tạo một kiểu chỉ có thể di chuyển và không thể sao chép được không?


96

Lưu ý của người biên tập : câu hỏi này đã được hỏi trước Rust 1.0 và một số khẳng định trong câu hỏi không nhất thiết đúng trong Rust 1.0. Một số câu trả lời đã được cập nhật để giải quyết cả hai phiên bản.

Tôi có cấu trúc này

struct Triplet {
    one: i32,
    two: i32,
    three: i32,
}

Nếu tôi chuyển nó cho một hàm, nó sẽ được sao chép ngầm. Bây giờ, đôi khi tôi đọc rằng một số giá trị không thể sao chép và do đó phải di chuyển.

Có thể làm cho cấu trúc này Tripletkhông thể sao chép được không? Ví dụ, liệu có thể triển khai một đặc điểm làm cho Tripletkhông thể sao chép và do đó "có thể di chuyển" được không?

Tôi đã đọc ở đâu đó rằng người ta phải triển khai Cloneđặc điểm để sao chép những thứ không thể sao chép hoàn toàn, nhưng tôi chưa bao giờ đọc về cách khác, đó là có một thứ gì đó có thể sao chép ngầm và làm cho nó không thể sao chép được để nó di chuyển thay thế.

Điều đó thậm chí có ý nghĩa?


1
paulkoerbitz.de/posts/… . Giải thích tốt ở đây về lý do tại sao di chuyển so với sao chép.
Sean Perry

Câu trả lời:


164

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).


: Để 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ôngDroptriể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 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

: Đ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.


@dbaupp: Bạn có tình cờ biết phiên bản Rust tích hợp sẵn các đặc điểm tùy chọn xuất hiện không? Tôi sẽ nghĩ rằng 0,10.
Matthieu M.

@MatthieuM. nó vẫn chưa được triển khai và thực sự gần đây đã có một số sửa đổi được đề xuất đối với thiết kế của tích hợp sẵn chọn tham gia .
huon

Tôi nghĩ rằng câu nói cũ nên được xóa.
Stargateur

1
# [Rút ra (Sao chép, Clone)] nên được sử dụng trên Triplet không impl
shadowbq

6

Cách dễ nhất là nhúng một cái gì đó vào loại của bạn mà không thể sao chép.

Thư viện tiêu chuẩn cung cấp một "loại điểm đánh dấu" cho chính xác trường hợp sử dụng này: NoCopy . Ví dụ:

struct Triplet {
    one: i32,
    two: i32,
    three: i32,
    nocopy: NoCopy,
}

15
Điều này không hợp lệ cho Rust> = 1.0.
malbarbo
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.