Move secara bawaan

Memahami bahwa nilai yang tak dapat disalin berpindah tanpa klon yang tak disengaja.

Daftar isi

Setiap nilai di Mojo dimiliki tepat satu pemilik pada satu waktu, dan bahasa ini tidak membuat salinan secara senyap. Topik ini membahas semantik nilai bawaan Mojo, yaitu cara penugasan variabel dan penyerahan nilai kepada fungsi menangani kepemilikan (ownership), yakni hak atas umur sebuah nilai beserta tanggung jawab membebaskannya. Setelah topik ini dibaca, penugasan antarvariabel, pesan error penyalinan implisit, dan operator transfer ^ dapat dibaca dengan benar.

Salinan hanya terjadi pada tipe yang mendukungnya#

Penugasan var b = a tidak selalu berarti menyalin. Compiler memeriksa trait ImplicitlyCopyable, yakni kemampuan sebuah tipe untuk disalin secara implisit, pada saat program dikompilasi. Tipe dasar seperti Int, Float64, dan String menuruti trait tersebut, sehingga penugasan di antara tipe-tipe tersebut membuat salinan sungguhan. Struct yang didefinisikan sendiri dan wadah seperti List tidak otomatis menurutinya, sehingga penugasan yang sama ditolak compiler. Penolakan seperti ini memang tampak merepotkan, tetapi ia menjaga dua hal sekaligus, yaitu aliran kepemilikan yang tetap terbaca dari kode dan penghematan karena program tidak membayar salinan yang tidak dibutuhkan.

Kepemilikan juga membawa tanggung jawab menyelesaikan umur nilai. Compiler mengakhiri umur sebuah nilai tepat setelah pemakaian terakhirnya, dan nilai yang mengelola sumber daya menjalankan destructor, yaitu metode __deinit__ yang dijalankan pada saat itu. Karena tidak ada salinan senyap, setiap nilai hanya memiliki satu pemilik pada satu waktu, sehingga pekerjaan penyelesaian umur juga berjalan tepat satu kali. Prinsip inilah yang membuat pembebasan memori pada pengelolaan manual dapat dititipkan kepada destructor alih-alih bergantung pada kedisiplinan penulis kode.

Jalankan berkas berikut untuk melihat bentuk penolakannya.

mojo salin-gagal.mojo
struct Kunci:
    var id: Int

    def __init__(out self, id: Int):
        self.id = id

def main():
    var a = Kunci(7)
    var b = a
    print(b.id)
bash
# jalankan berkas yang ditolak compiler, pangkas awalan path pada pesannya
mojo run salin-gagal.mojo 2>&1 | sed -e "s|$PWD/||" -e "s|$HOME/|~/|"
text Keluaran
salin-gagal.mojo:9:13: error: value of type 'Kunci' cannot be implicitly copied, it does not conform to 'ImplicitlyCopyable'
    var b = a
            ^
salin-gagal.mojo:9:13: note: consider transferring the value with '^'
    var b = a
            ^
             ^
~/.pixi/envs/mojo/bin/mojo: error: failed to parse the provided Mojo source module

Pesan error menyatakan masalahnya dengan tepat. Nilai bertipe Kunci tidak dapat disalin secara implisit karena tidak menuruti ImplicitlyCopyable, dan compiler menyertakan saran perbaikannya, yaitu mentransfer nilai dengan ^. Penolakan terjadi pada waktu kompilasi, sehingga penyalinan yang tidak disengaja tidak pernah lolos menjadi program yang berjalan. Bila struct memang perlu dapat disalin, kemampuan itu dinyatakan secara eksplisit melalui trait Copyable, sehingga keputusan menyalin berada di tangan penulis tipe tersebut.

Perbandingan ketiga bentuk penugasan dirangkum dalam tabel berikut.

PenugasanSyaratHasil
var b = atipe a menuruti ImplicitlyCopyablesalinan baru dibuat, a tetap terisi
var b = atipe a tidak menuruti ImplicitlyCopyableditolak compiler
var b = a^tipe a dapat dipindahkankepemilikan berpindah, a tidak terinisialisasi

Tabel ini sekaligus menegaskan bahwa ^ tidak menyalin apa pun, sehingga pilihan antara penugasan biasa dan transfer bukan soal selera melainkan soal kepemilikan. Struct biasa yang field-nya dapat dipindahkan mendukung ^ secara langsung tanpa pernyataan tambahan, sebagaimana Kunci pada contoh di atas.

Operator transfer ^#

Operator ^ memindahkan kepemilikan nilai dari variabel sumber ke penerima, sebuah operasi yang dalam dokumentasi disebut move. Sesudah transfer, variabel sumber tidak terinisialisasi, sehingga pemakaiannya lagi adalah error kompilasi. Perpindahan ini murah karena tidak ada data yang ditiru, hanya penanda kepemilikan yang berganti tangan. Tidak ada blok memori yang diduplikasi dan tidak ada alokasi baru yang dibuat, sehingga biayanya konstan untuk tipe sebesar apa pun. Untuk tipe yang dapat disalin, penugasan tanpa ^ tetap membuat salinan, sedangkan untuk tipe yang tidak dapat disalin, ^ menjadi satu-satunya cara mengirim nilai ke penerima baru.

Jalankan program berikut yang membandingkan kedua jalur tersebut.

mojo transfer.mojo
struct Kunci:
    var id: Int

    def __init__(out self, id: Int):
        self.id = id

def main():
    var angka = 10
    var salinan_angka = angka
    print(angka, salinan_angka)
    salinan_angka = 20
    print(angka, salinan_angka)

    var a = Kunci(7)
    var b = a^
    print(b.id)
bash
# jalankan program perbandingan salinan dan transfer
mojo run transfer.mojo
text Keluaran
10 10
10 20
7

Dua baris pertama menunjukkan salinan sungguhan pada angka bertipe Int. Perubahan pada salinan_angka tidak memengaruhi angka karena keduanya adalah dua nilai yang terpisah. Baris ketiga berasal dari print(b.id) yang membaca Kunci hasil transfer, dan transfer itulah satu-satunya cara agar penugasan var b = a pada bagian sebelumnya dapat diterima. Nilai hasil transfer memikul tanggung jawab penuh atas umurnya, karena tanggung jawab itu ikut berpindah bersama penanda kepemilikannya.

Menyentuh variabel sumber setelah transfer#

Bentuk pelanggaran yang paling sering terjadi adalah membaca variabel sumber sesudah nilainya ditransfer. Compiler menolak program semacam itu sebelum program berjalan, dan pesan errornya menunjuk tepat ke variabel yang sudah tidak terinisialisasi. Jalankan berkas berikut yang membaca a setelah kepemilikannya dipindahkan ke b.

mojo pakai-lagi.mojo
struct Kunci:
    var id: Int

    def __init__(out self, id: Int):
        self.id = id

def main():
    var a = Kunci(7)
    var b = a^
    print(a.id)
bash
# jalankan berkas yang membaca variabel sumber setelah transfer
mojo run pakai-lagi.mojo 2>&1 | sed -e "s|$PWD/||" -e "s|$HOME/|~/|"
text Keluaran
pakai-lagi.mojo:10:10: error: use of uninitialized value 'a'
    print(a.id)
         ^
pakai-lagi.mojo:8:9: note: 'a' declared here
    var a = Kunci(7)
        ^
pakai-lagi.mojo:9:14: warning: assignment to 'b' was never used; assign to '_' instead?
    var b = a^
             ^
~/.pixi/envs/mojo/bin/mojo: error: failed to run the pass manager

Pesan error menyatakan use of uninitialized value 'a', dan catatan note: 'a' declared here menunjuk kembali ke deklarasi variabelnya, sehingga letak pelanggarannya mudah dilacak. Baris peringatan tentang b adalah temuan yang berdiri sendiri dan bukan akibat error di atas, yaitu nilai yang ditugaskan kepada b tidak pernah dibaca di seluruh program sehingga compiler menyarankan penugasan ke _. Perbaikan disesuaikan dengan maksud kode, yaitu membaca nilai sebelum transfer, menyalinnya terlebih dahulu bila dua variabel memang harus tetap terisi, atau membaca dari variabel tujuan b karena di situlah nilai berada.

Aturan yang sama berlaku saat argumen diteruskan dengan ^ ke parameter pemilik, karena transfer yang terjadi sama dengan transfer pada penugasan. Setelah pemanggilan berjalan, pemanggil tidak lagi memegang nilai tersebut, sehingga baris apa pun yang membacanya ditolak dengan pesan yang sama. Konsistensi ini membuat aliran kepemilikan dapat dilacak sepenuhnya dari kode sumber, tanpa pemeriksaan tambahan saat program berjalan.

Kesalahan umum#

  • Memakai variabel sumber setelah transfer. Sesudah var b = a^, variabel a tidak terinisialisasi, dan compiler menolak pemakaian berikutnya dengan pesan use of uninitialized value 'a'. Perbaikannya adalah tidak menyentuh sumber lagi, atau menyalin nilainya terlebih dahulu bila kedua variabel memang harus tetap terisi.
  • Menganggap setiap penugasan menyalin. Kebiasaan dari bahasa lain, yaitu menduplikasi struktur data pada setiap penugasan, tidak berlaku di Mojo. Untuk struct dan wadah, penugasan tanpa ^ ditolak compiler, bukan berubah menjadi referensi atau salinan dangkal (shallow copy). Perbaikannya adalah memutuskan secara sadar, yaitu transfer dengan ^ bila kepemilikan memang berpindah, atau salinan eksplisit bila tipe yang bersangkutan menyediakannya.

Ringkasan#

  • Penugasan hanya menyalin bila tipe nilai menuruti trait ImplicitlyCopyable, sedangkan tipe lain ditolak compiler pada waktu kompilasi.
  • Operator ^ mentransfer kepemilikan nilai ke penerima tanpa meniru datanya, dan variabel sumber menjadi tidak terinisialisasi.
  • Pemakaian variabel sumber setelah transfer, baik pada penugasan maupun pada penerusan argumen, ditolak compiler dengan error use of uninitialized value.

Referensi#