Apakah mungkin membuat suatu jenis hanya dapat dipindahkan dan tidak dapat disalin?

96

Catatan editor : pertanyaan ini ditanyakan sebelum Rust 1.0 dan beberapa pernyataan dalam pertanyaan tersebut belum tentu benar di Rust 1.0. Beberapa jawaban telah diperbarui untuk menangani kedua versi tersebut.

Saya memiliki struct ini

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

Jika saya meneruskan ini ke suatu fungsi, itu secara implisit disalin. Sekarang, terkadang saya membaca bahwa beberapa nilai tidak dapat disalin dan oleh karena itu harus dipindahkan.

Apakah mungkin membuat struct ini Triplettidak dapat disalin? Misalnya, apakah mungkin untuk menerapkan sifat yang tidak dapat Tripletdisalin dan oleh karena itu "dapat dipindahkan"?

Saya membaca di suatu tempat bahwa seseorang harus menerapkan Clonesifat untuk menyalin hal-hal yang tidak dapat disalin secara implisit, tetapi saya tidak pernah membaca tentang sebaliknya, yaitu memiliki sesuatu yang secara implisit dapat disalin dan membuatnya tidak dapat disalin sehingga ia bergerak sebagai gantinya.

Apakah itu masuk akal?

Christoph
sumber
1
paulkoerbitz.de/posts/… . Penjelasan bagus di sini tentang mengapa pindah versus menyalin.
Sean Perry

Jawaban:

165

Kata Pengantar : Jawaban ini ditulis sebelum opt-in built-in sifat -Khusus yang Copyaspek -were dilaksanakan. Saya telah menggunakan tanda kutip blok untuk menunjukkan bagian yang hanya diterapkan pada skema lama (yang diterapkan saat pertanyaan diajukan).


Lama : Untuk menjawab pertanyaan dasar, Anda dapat menambahkan bidang penanda yang menyimpan NoCopynilai . Misalnya

struct Triplet {
    one: int,
    two: int,
    three: int,
    _marker: NoCopy
}

Anda juga dapat melakukannya dengan memiliki destruktor (melalui penerapan Dropsifat ), tetapi menggunakan jenis penanda lebih disukai jika destruktor tidak melakukan apa pun.

Tipe sekarang berpindah secara default, yaitu, ketika Anda mendefinisikan tipe baru yang tidak diimplementasikan Copykecuali Anda secara eksplisit mengimplementasikannya untuk tipe Anda:

struct Triplet {
    one: i32,
    two: i32,
    three: i32
}
impl Copy for Triplet {} // add this for copy, leave it out for move

Implementasi hanya bisa ada jika setiap tipe terdapat dalam new structatau enumitu sendiri Copy. Jika tidak, kompilator akan mencetak pesan kesalahan. Itu juga hanya bisa ada jika tipe tidak memiliki Dropimplementasi.


Untuk menjawab pertanyaan yang tidak Anda tanyakan ... "ada apa dengan gerakan dan salinan?":

Pertama saya akan menentukan dua "salinan" yang berbeda:

  • sebuah salinan byte , yang hanya dangkal menyalin obyek byte-by-byte, pointer tidak mengikuti, misalnya jika Anda memiliki (&usize, u64), itu adalah 16 byte pada komputer 64-bit, dan salinan dangkal akan mengambil orang-orang 16 byte dan mereplikasi mereka nilai di beberapa bagian memori 16-byte lainnya, tanpa menyentuh usizedi ujung lain dari &. Artinya, itu setara dengan menelepon memcpy.
  • a copy semantik , menduplikasi nilai untuk membuat (agak) misalnya independen baru yang dapat digunakan dengan aman secara terpisah dengan yang lama. Misalnya, salinan semantik dari suatu Rc<T>hanya melibatkan peningkatan jumlah referensi, dan salinan semantik a Vec<T>melibatkan pembuatan alokasi baru, dan kemudian secara semantik menyalin setiap elemen yang disimpan dari yang lama ke yang baru. Ini dapat berupa salinan dalam (mis. Vec<T>) Atau dangkal (mis. Rc<T>Tidak menyentuh yang disimpan T), Clonesecara longgar didefinisikan sebagai jumlah pekerjaan terkecil yang diperlukan untuk menyalin secara semantik nilai tipe Tdari dalam a &Tke T.

Rust seperti C, setiap nilai yang digunakan dari suatu nilai adalah salinan byte:

let x: T = ...;
let y: T = x; // byte copy

fn foo(z: T) -> T {
    return z // byte copy
}

foo(y) // byte copy

Mereka adalah salinan byte apakah Tbergerak atau tidak atau "dapat disalin secara implisit". (Untuk lebih jelasnya, mereka tidak harus benar-benar salinan byte-by-byte pada saat run-time: kompilator bebas untuk mengoptimalkan salinan jika perilaku kode dipertahankan.)

Namun, ada masalah mendasar dengan salinan byte: Anda berakhir dengan nilai duplikat di memori, yang bisa sangat buruk jika memiliki destruktor, mis.

{
    let v: Vec<u8> = vec![1, 2, 3];
    let w: Vec<u8> = v;
} // destructors run here

Jika whanya salinan byte biasa vmaka akan ada dua vektor menunjuk pada alokasi yang sama, keduanya dengan penghancur yang membebaskannya ... menyebabkan bebas ganda , yang merupakan masalah. NB. Ini akan baik-baik saja, jika kita melakukan salinan semantik vke w, karena itu wakan menjadi independennya sendiri Vec<u8>dan penghancurnya tidak akan menginjak-injak satu sama lain.

Ada beberapa kemungkinan perbaikan di sini:

  • Biarkan programmer menanganinya, seperti C. (tidak ada destruktor di C, jadi tidak seburuk ... Anda hanya mendapatkan kebocoran memori saja.: P)
  • Lakukan salinan semantik secara implisit, sehingga wmemiliki alokasinya sendiri, seperti C ++ dengan konstruktor salinannya.
  • Menganggap penggunaan nilai sebagai pengalihan kepemilikan, sehingga vtidak dapat digunakan lagi dan destruktornya tidak berjalan.

Yang terakhir adalah apa yang Rust lakukan: perpindahan hanyalah penggunaan nilai di mana sumber secara statis tidak valid, sehingga kompilator mencegah penggunaan lebih lanjut dari memori yang sekarang tidak valid.

let v: Vec<u8> = vec![1, 2, 3];
let w: Vec<u8> = v;
println!("{}", v); // error: use of moved value

Jenis yang memiliki destruktor harus dipindahkan ketika digunakan oleh nilai (alias ketika byte disalin), karena mereka memiliki manajemen / kepemilikan beberapa sumber daya (misalnya alokasi memori, atau pegangan file) dan sangat tidak mungkin salinan byte akan menduplikasi ini dengan benar kepemilikan.

"Nah ... apa itu salinan implisit?"

Pikirkan tentang tipe primitif seperti u8: salinan byte sederhana, cukup salin satu byte, dan salinan semantik sama sederhananya, salin satu byte. Secara khusus, salinan byte adalah salinan semantik ... Rust bahkan memiliki sifat bawaanCopy yang menangkap tipe mana yang memiliki salinan semantik dan byte yang identik.

Karenanya, untuk Copytipe ini penggunaan nilai menurut juga otomatis merupakan salinan semantik, sehingga sangat aman untuk terus menggunakan sumbernya.

let v: u8 = 1;
let w: u8 = v;
println!("{}", v); // perfectly fine

Lama : NoCopyMarker menimpa perilaku otomatis penyusun dengan mengasumsikan bahwa tipe yang bisa Copy(yaitu hanya berisi agregat primitif dan &) adalah Copy. Namun ini akan berubah ketika sifat bawaan keikutsertaan diterapkan.

Seperti disebutkan di atas, sifat bawaan keikutsertaan diimplementasikan, sehingga kompilator tidak lagi memiliki perilaku otomatis. Namun, aturan yang digunakan untuk perilaku otomatis di masa lalu adalah aturan yang sama untuk memeriksa apakah legal untuk diterapkan Copy.

huon
sumber
@dbaupp: Apakah Anda tahu di versi Rust mana ciri-ciri bawaan muncul? Saya akan berpikir 0,10.
Matthieu M.
@Bayu_joo itu belum diimplementasikan, dan sebenarnya baru-baru ini ada beberapa revisi yang diusulkan untuk desain opt-in built-in .
huon
Saya pikir kutipan lama itu harus dihapus.
Stargateur
1
# [derive (Copy, Clone)] harus digunakan pada Triplet bukan impl
shadowbq
6

Cara termudah adalah dengan menyematkan sesuatu dalam tipe Anda yang tidak dapat disalin.

Pustaka standar menyediakan "tipe penanda" untuk kasus penggunaan ini: NoCopy . Sebagai contoh:

struct Triplet {
    one: i32,
    two: i32,
    three: i32,
    nocopy: NoCopy,
}
BurntSushi5
sumber
15
Ini tidak berlaku untuk Rust> = 1.0.
malbarbo