Class dan struct sama-sama mengelompokkan variabel instans dan metode, tetapi keduanya berbeda dalam cara nilai disimpan dan diteruskan. Struct adalah tipe nilai (value type), yaitu tipe yang nilainya disimpan secara langsung dan disalin pada setiap penugasan, sedangkan class adalah tipe referensi. Topik ini membahas struct, semantik salinannya, batasan pewarisannya, dan pertimbangan memilih struct dibandingkan class.
Struct sebagai tipe nilai#
Struct didefinisikan dengan kata kunci struct menggantikan class, lengkap dengan variabel instans, initialize, dan metode seperti biasa. Perbedaannya muncul pada saat nilai diteruskan: penugasan satu variabel struct ke variabel lain menyalin seluruh isinya, sehingga perubahan pada salinan tidak menyentuh data aslinya. Program berikut membandingkan perilaku itu dengan class pada situasi yang sama.
struct Titik
property x : Int32
property y : Int32
def initialize(@x : Int32, @y : Int32)
end
def geser(dx : Int32, dy : Int32) : Titik
Titik.new(x + dx, y + dy)
end
end
class TitikReferensi
property x : Int32
def initialize(@x : Int32)
end
end
a = Titik.new(2, 3)
b = a
b.x = 50
puts a.x
puts b.x
puts Titik.new(2, 3) == Titik.new(2, 3)
puts TitikReferensi.new(2) == TitikReferensi.new(2)
p = TitikReferensi.new(2)
q = p
q.x = 50
puts p.x# jalankan program titik.cr
crystal run titik.cr2
50
true
false
50Tiga percobaan pada program tersebut menutup seluruh perbedaannya. Penugasan b = a menyalin nilai struct, sehingga b.x = 50 mengubah salinan dan a.x tetap 2. Kesamaan struct diperiksa dari nilainya, sehingga dua Titik berfield sama dianggap sama, sedangkan dua objek class yang baru dibuat tidak sama karena perbandingannya berpijak pada identitas objek. Penugasan q = p pada class hanya menyalin referensinya, sehingga perubahan melalui q terbaca juga melalui p.
Konsekuensi semantik nilai#
Salinan itu terjadi di tempat lain pula, bukan hanya pada penugasan. Struct yang diteruskan sebagai argumen metode ikut disalin, dan struct yang dibaca dari elemen Array juga dikembalikan sebagai salinan. Metode geser pada struct Titik memanfaatkan sifat ini secara sadar: metode itu tidak mengubah self, melainkan mengembalikan Titik baru, sehingga gaya pemrogramannya berupa pembentukan nilai baru alih-alih mutasi keadaan.
Dari sisi memori, struct memberi keuntungan yang nyata untuk kasus lokal. Variabel lokal bertipe struct umumnya disimpan langsung di stack, tanpa alokasi heap dan tanpa beban pengumpulan sampah memori (garbage collection), sehingga membuat banyak nilai struct kecil itu murah. Tabel berikut merangkum perbandingannya.
| Aspek | Class | Struct |
|---|---|---|
| Semantik isi | referensi ke objek yang sama | salinan nilai pada setiap penugasan |
Kesamaan (==) | identitas objek | kesamaan isi variabel instans |
| Pewarisan | didukung | tidak didukung |
| Penyimpanan | alokasi di heap | umumnya di stack untuk variabel lokal |
Batasan struct#
Kehandalan semantik nilai itu dibayar dengan batasan pewarisan. Struct tidak dapat menurunkan struct lain, dan class tidak dapat menurunkan struct, sehingga hierarki seperti pada topik pewarisan tidak dapat dibangun di atas struct. Program berikut mencoba menurunkan struct Titik oleh class Titik3D.
struct Titik
property x : Int32
def initialize(@x : Int32)
end
end
class Titik3D < Titik
end
Titik3D.newShowing last frame. Use --error-trace for full trace.
In struct_rusak.cr:8:7
8 | class Titik3D < Titik
^
Error: can't make class 'Titik3D' inherit struct 'Titik'Selain tidak dapat diwariskan, struct tetap boleh melakukan include terhadap module, sehingga mixin dari topik Module berlaku juga untuk tipe nilai. Penulisan struct yang isinya sekadar data dapat dipersingkat dengan macro record, yang mendefinisikan struct lengkap dengan variabel instans, initialize, dan metode pembacanya dalam satu baris. Macro itu tidak menghasilkan metode pengisi, sehingga nilai field record tidak dapat diubah dari luar dan perubahannya dilakukan dengan membentuk record baru. Kebutuhan yang menuntut hierarki atau identitas objek sebaiknya kembali ke class.
Kapan struct lebih tepat#
Struct paling tepat untuk nilai kecil yang sering dibuat dan dibandingkan dari isinya, misalnya koordinat, jumlah uang beserta mata uangnya, atau warna RGB. Pada nilai semacam itu, yang diinginkan adalah kesetaraan nilai, bukan identitas: dua koordinat dengan angka yang sama adalah dua hal yang sama. Salinannya yang kecil juga lebih murah daripada alokasi heap untuk setiap objek.
Contoh pemodelan uang memperlihatkan kecocokan itu dengan lebih jelas. Struct JumlahUang dengan variabel instans @nilai : Int64 dan @kode : String membandingkan dua saldo dari kedua field sekaligus, sehingga 1000 rupiah tidak pernah dianggap sama dengan 1000 dolar. Nilai uang yang diteruskan antar metode juga tidak dapat diubah diam-diam oleh penerima, karena yang diterima adalah salinan. Kedua sifat ini adalah jaminan yang justru sulit dicapai dengan class tanpa disiplin tambahan.
Class tetap menjadi pilihan apabila objeknya memiliki siklus hidup, keadaan yang diubah bersama oleh banyak pemegang referensi, atau hierarki tipe dengan metode abstrak. Patokan praktisnya, mulailah dengan class untuk entitas yang punya identitas, lalu pindahkan ke struct apabila data yang dimodelkan berperan sebagai nilai murni. Keputusan ini memengaruhi pembaca kode lebih dalam daripada kesannya, karena semantik salinan mengubah arti setiap penugasan.
Kesalahan umum#
Kesalahan pertama adalah menyangka metode yang mengembalikan struct telah mengubah objek asalnya. Pada struct Titik, pemanggilan titik.geser(10, 0) yang nilai baliknya tidak ditampung tidak meninggalkan jejak apa pun, karena hasilnya adalah Titik baru yang langsung dibuang. Perbaikannya adalah menyimpan nilai balik itu kembali ke variabelnya, misalnya titik = titik.geser(10, 0), sebagaimana angka atau string diperbarui.
Kesalahan kedua adalah mencoba membangun hierarki di atas struct. Menurunkan struct dengan < ditolak compiler tanpa kecuali, baik struct menurunkan struct maupun class menurunkan struct. Perbaikannya mengikuti kebutuhan pemodelan, yaitu menggunakan class apabila variasi perilaku antar turunan memang diperlukan, atau menambah field pada struct yang sama apabila perbedaannya hanya pada data.
Ringkasan#
- Struct adalah tipe nilai yang disalin pada setiap penugasan, pemanggilan metode dengan argumen struct, dan pembacaan elemen Array.
- Perbandingan struct memeriksa isi variabel instansnya, sedangkan perbandingan class memeriksa identitas objek.
- Struct tidak dapat diwariskan, baik oleh struct maupun oleh class, tetapi tetap dapat menyertakan module.
- Struct cocok untuk nilai kecil yang sering dibuat seperti koordinat, uang, dan warna, sedangkan class cocok untuk objek beridentitas.
Referensi#
- The Crystal Programming Language Reference — crystal-lang.org — Dokumentasi resmi sintaks dan semantik bahasa, termasuk struct dan semantik tipe nilainya.
- Dokumentasi API Crystal 1.22.1 — crystal-lang.org — Rujukan pustaka standar untuk Struct, Record, dan metode perbandingannya.