Struct

Mendefinisikan struct yang tidak dapat diubah dan mutable struct yang dapat diubah, beserta tipe field-nya.

Daftar isi

Struct adalah tipe konkret buatan pengguna yang mengelompokkan beberapa nilai ke dalam satu kesatuan bernama field. Topik ini membahas cara mendefinisikan struct, perilaku konstruktor bawaannya, perbedaan struct yang tidak dapat diubah dengan mutable struct, serta konsekuensi menempatkan tipe abstrak sebagai tipe field. Contoh yang dipakai berupa titik pada bidang, penghitung sederhana, dan catatan nilai.

Mendefinisikan struct#

Definisi struct dimulai dengan kata kunci struct, diikuti nama tipe, kemudian daftar field beserta tipe masing-masing setelah tanda titik dua. Konvensi Julia menuliskan nama tipe dengan huruf kapital pada awal setiap kata. Definisi sekaligus menghasilkan konstruktor bawaan (default constructor) yang dipanggil dengan nama struct dan satu argumen untuk setiap field. Contoh berikut membuat tipe Titik dengan dua field bertipe Float64.

julia definisi-titik.jl
struct Titik
    x::Float64
    y::Float64
end

p = Titik(2.5, 1.0)
println(p)
println(p.x + p.y)
println(Titik(3, 4))
text Keluaran
Titik(2.5, 1.0)
3.5
Titik(3.0, 4.0)

Nilai field dibaca dengan tanda titik, misalnya p.x, tanpa perlu fungsi pembaca khusus. Pemanggilan Titik(3, 4) memberi argumen bertipe Int64, tetapi hasilnya Titik(3.0, 4.0). Konstruktor bawaan mengonversi setiap argumen ke tipe field-nya selama konversi itu tidak menghilangkan informasi. Perilaku ini menjadikan konstruktor bawaan toleran terhadap tipe bilangan yang berbeda, sebagaimana diperlihatkan contoh berikut.

Struktur field sebuah tipe juga dapat diperiksa tanpa membuat objeknya. Fungsi fieldnames mendaftar nama seluruh field, sedangkan fieldtype membaca tipe satu field tertentu. Keduanya berguna saat menyelidiki tipe buatan pustaka lain.

julia Pemeriksaan field tanpa membuat objek
struct Titik
    x::Float64
    y::Float64
end

println(fieldnames(Titik))
println(fieldtype(Titik, :x))
text Keluaran
(:x, :y)
Float64
julia Argumen yang tidak dapat dikonversi
struct Titik
    x::Float64
    y::Float64
end

try
    Titik("a", 1.0)
catch e
    println(typeof(e))
    println(split(sprint(showerror, e), "\n")[1])
end
text Keluaran
MethodError
MethodError: Cannot `convert` an object of type String to an object of type Float64

Argumen String tidak dapat dikonversi menjadi Float64, sehingga konstruktor memunculkan MethodError. Batasan ini menjaga kebenaran tipe field sejak objek dibuat. Aturan konversi di balik pesan tersebut memakai fungsi convert, dan aturan lengkapnya dibahas pada bagian akhir modul ini.

Struct tidak dapat diubah#

Struct biasa bersifat tidak dapat diubah (immutable). Setelah objek dibuat, field-nya tidak dapat ditulis ulang, dan percobaan penulisan memunculkan ErrorException. Sifat ini membuat struct aman dipakai bersama karena isinya tidak dapat berubah tanpa ketahuan, serta memberi Julia kebebasan menyimpan nilainya secara efisien. Pembaruan nilai tetap dimungkinkan dengan membuat objek baru, dan objek lama ditinggalkan begitu tidak lagi dirujuk.

julia Percobaan mengubah field struct
struct Titik
    x::Float64
    y::Float64
end

p = Titik(2.5, 1.0)
try
    p.x = 9.0
catch e
    println(typeof(e))
    println(split(sprint(showerror, e), "\n")[1])
end
text Keluaran
ErrorException
setfield!: immutable struct of type Titik cannot be changed

Pesan error menyebut fungsi setfield! yang dipanggil Julia saat penulisan p.x = 9.0 dieksekusi. Apabila nilai titik baru diperlukan, pola yang lazim adalah membuat objek Titik baru daripada memodifikasi objek lama. Batasan ini berlaku pada setiap penulisan field, termasuk penulisan dari dalam fungsi lain, karena mekanisme di baliknya sama.

mutable struct#

Apabila field memang perlu berubah selama program berjalan, definisikan tipe dengan kata kunci mutable struct. Seluruh field menjadi dapat ditulis, dan penulisan dilakukan langsung pada objek yang sama, bukan dengan membuat objek baru. Contoh penghitung berikut menaikkan field nilai dua kali, lalu membandingkannya dengan field batas.

julia penghitung.jl
mutable struct Penghitung
    nilai::Int64
    batas::Int64
end

c = Penghitung(0, 3)
println(c)
c.nilai += 1
c.nilai += 1
println(c)
println(c.nilai < c.batas)
text Keluaran
Penghitung(0, 3)
Penghitung(2, 3)
true

Ekspresi c.nilai += 1 membaca field nilai, menambahkannya dengan satu, lalu menulis hasilnya kembali ke field yang sama. Perubahan itu terjadi pada objek yang dirujuk variabel c, sehingga pemegang rujukan lain pada objek tersebut melihat nilai terbaru. Sifat seperti ini cocok untuk penghitung, antrean loket, dan status yang berlangsung sepanjang waktu.

Perlu dibedakan antara objek dan nama yang merujuknya. Penulisan field mengubah isi objek, sedangkan penugasan seperti c = Penghitung(5, 3) mengarahkan nama c ke objek baru tanpa menyentuh objek lama. Mutabilitas melekat pada objek, bukan pada nama yang merujuk objek itu.

Field dengan tipe abstrak#

Field tidak wajib bertipe konkret. Menuliskan tipe abstrak seperti Number pada field mengizinkan satu definisi struct menerima bermacam tipe nilai, sepanjang nilainya termasuk dalam tipe abstrak tersebut. Contoh Catatan berikut menyimpan nama bertipe String dan nilai bertipe Number.

julia Struct dengan field bertipe abstrak
struct Catatan
    nama::String
    nilai::Number
end

a = Catatan("gempa", 5.8)
b = Catatan("jawaban", 42)
println(a)
println(b)
println(typeof(a.nilai))
println(typeof(b.nilai))
text Keluaran
Catatan("gempa", 5.8)
Catatan("jawaban", 42)
Float64
Int64

Dua objek Catatan menampung nilai dengan tipe konkret berbeda, yaitu Float64 dan Int64. Konsekuensinya, tipe persis setiap field hanya diketahui saat program berjalan, sehingga akses field tidak dapat disiapkan lebih dahulu oleh compiler. Field bertipe konkret, seperti x::Float64 pada Titik, memberi tipe yang pasti dan menjadi pilihan yang lebih baik apabila keragaman tipe tidak diperlukan.

Fungsi fieldtype(Catatan, :nilai) mengembalikan Number, yaitu tipe yang dianotasi, bukan tipe konkret setiap objek. Tipe konkret nilai yang tersimpan diperiksa dari objeknya dengan typeof sebagaimana diperlihatkan contoh. Anotasi abstrak pun tetap menjaga batas, karena nilai di luar Number tetap ditolak oleh konstruktor. Menggabungkan field konkret dan abstrak dalam satu struct juga sah, seperti nama::String yang tetap konkret pada Catatan.

Kesalahan umum#

Tiga kesalahan berikut kerap muncul saat bekerja dengan struct. Penyebabnya selalu berkaitan dengan tipe field atau mutabilitas tipe.

  1. Mengubah field struct biasa. Penulisan seperti p.x = 9.0 memunculkan ErrorException, sehingga tipe perlu dideklarasikan ulang sebagai mutable struct apabila perubahan memang diperlukan.
  2. Mengira konstruktor bawaan menerima tipe apa pun. Argumen yang tidak dapat dikonversi ke tipe field memunculkan MethodError, sehingga argumen perlu dikonversi secara eksplisit terlebih dahulu.
  3. Membiarkan field tanpa anotasi tipe. Field tanpa tanda :: bertipe Any, sehingga manfaat sistem tipe hilang. Tuliskan tipe pada setiap field, dan gunakan tipe abstrak hanya bila keragaman tipe memang diinginkan.

Ketiga kesalahan ini menunjukkan pola yang sama, yaitu tipe field menentukan perilaku struct sejak objek dibuat. Mendeklarasikan tipe secara eksplisit dan memilih mutabilitas dengan sengaja membuat perilaku struct lebih mudah diprediksi oleh bagian kode lainnya. Pesan error yang muncul pun selalu menyebut tipe struct dan operasinya, sehingga sumber kesalahannya mudah ditemukan.

Ringkasan#

  • struct mendefinisikan tipe konkret dengan field beranotasi tipe, dan konstruktor bawaannya mengonversi argumen yang konversinya tepat.
  • Struct biasa tidak dapat diubah, sedangkan mutable struct mengizinkan penulisan field pada objek yang sama.
  • Field bertipe abstrak melayani banyak tipe sekaligus dengan konsekuensi tipe persis hanya diketahui saat program berjalan.

Referensi#