Hampir semua nilai di Crystal adalah objek yang berasal dari sebuah class, sehingga pemodelan data dimulai dari mendefinisikan class sendiri. Topik ini membahas cara mendefinisikan class, peran variabel instans (instance variable) sebagai data internal objek, sintaks singkat pada parameter initialize, serta aturan inferensi tipe yang menyertainya. Setelah membaca topik ini, class sederhana dengan data internal yang tipe tiap variabelnya diperiksa compiler dapat ditulis tanpa menebak-nebak.
Mendefinisikan class#
Class didefinisikan dengan kata kunci class diikuti nama yang selalu diawali huruf kapital dan mengikuti gaya CamelCase, misalnya Barang atau LaporanStok. Data internal yang melekat pada setiap objek disebut variabel instans, yaitu variabel yang diawali tanda @ dan hanya dapat dibaca atau diubah dari metode milik class tersebut. Nilai awalnya diberikan pada metode initialize, yaitu metode khusus yang dipanggil otomatis oleh new pada saat objek dibuat. Metode lain di dalam class yang sama bebas membaca variabel instans itu, sehingga seluruh metode bekerja di atas data yang konsisten.
Program berikut mendefinisikan class Barang untuk catatan stok dapur. Variabel instans @nama dan @stok diisi melalui parameter initialize, sedangkan @keluar diberi nilai awal 0 langsung di dalam badan initialize. Metode keluar mengurangi stok dan mencatat jumlah yang keluar, lalu metode ringkasan merangkum keadaan objek dalam satu baris teks.
class Barang
def initialize(@nama : String, @stok : Int32)
@keluar = 0
end
def keluar(jumlah : Int32) : Nil
@stok -= jumlah
@keluar += jumlah
end
def ringkasan : String
"#{@nama}: stok #{@stok}, keluar #{@keluar}"
end
end
beras = Barang.new("Beras", 10)
beras.keluar(3)
puts beras.ringkasan
puts Barang.new("Gula", 24).ringkasanJalankan berkasnya dengan perintah berikut.
# jalankan program stok.cr
crystal run stok.crBeras: stok 7, keluar 3
Gula: stok 24, keluar 0Setiap objek menyimpan salinan variabel instans miliknya sendiri. Pemanggilan beras.keluar(3) mengubah @stok objek beras dari 10 menjadi 7 sekaligus menambah @keluar menjadi 3, sedangkan objek kedua tetap berada pada keadaan awalnya. Anotasi : Nil pada metode keluar menyatakan bahwa metode itu hanya mengubah keadaan dan tidak mengembalikan nilai apa pun, sebuah konvensi yang lazim untuk metode mutasi.
Sintaks singkat @nama pada parameter#
Parameter initialize yang ditulis sebagai @nama : String melakukan dua pekerjaan sekaligus. Parameter itu mendeklarasikan variabel instans @nama bertipe String, lalu mengisinya dengan nilai argumen yang diterima. Bentuk panjang yang setara ditulis seperti berikut.
# tidak-dijalankan
# Setara dengan def initialize(@nama : String, @stok : Int32)
def initialize(nama : String, stok : Int32)
@nama = nama
@stok = stok
endSingkatan ini menjadi idiom yang lazim di kode Crystal karena menghilangkan pengulangan yang rawan keliru. Setiap parameter yang diawali @ otomatis melekat pada objek, sehingga badan initialize dapat dikosongkan apabila tidak ada persiapan lain yang diperlukan. Sebaliknya, parameter tanpa @ tetap berlaku sebagai variabel lokal biasa yang hilang setelah metode selesai. Perbedaannya menentukan: variabel lokal nama tidak dapat diakses metode lain, sedangkan @nama menyertai objek sepanjang hidupnya. Kebiasaan ini juga membuat baris parameter initialize sekaligus berperan sebagai daftar lengkap data yang dimiliki objek.
Tipe yang disimpulkan dari initializer#
Variabel instans tidak wajib diberi anotasi tipe. Apabila variabelnya diisi langsung dengan sebuah ekspresi, compiler menyimpulkan tipenya dari ekspresi initializer itu. Pengisian @keluar = 0 pada class Barang menghasilkan tipe Int32, sedangkan pengisian @nama = "kertas" akan menghasilkan String. Setelah tipenya terbentuk, setiap operasi pada variabel itu diperiksa terhadap tipe yang sama, sehingga @keluar += jumlah hanya sah apabila jumlah juga bertipe Int32.
Class Penghitung berikut mengandalkan inferensi sepenuhnya. Variabel @total diisi 0 tanpa anotasi, dan metode total mengembalikan isinya tanpa menyebut tipe. Pemanggilan typeof meminta tipe suatu ekspresi pada saat compile, sehingga baris pertama keluaran menunjukkan tipe yang disimpulkan compiler untuk @total.
class Penghitung
@total = 0
def tambah(n : Int32) : Nil
@total += n
end
def total
@total
end
end
hitung = Penghitung.new
hitung.tambah(12)
puts typeof(hitung.total)
puts hitung.totalInt32
12Sifat ini merupakan bagian dari inferensi tipe global, yaitu proses compiler menyimpulkan tipe seluruh program sekaligus sebelum menerjemahkannya. Compiler mengumpulkan semua pengisian yang mungkin terjadi, membentuk tipe akhirnya, lalu menolak program yang membaca variabel dengan tipe yang tidak cocok. Cara inilah yang menjelaskan mengapa anotasi : Int32 tidak pernah ditulis pada @total: tipe itu sudah pasti dari ekspresinya. Anotasi tetap diperlukan pada variabel yang hanya dideklarasikan tanpa nilai awal, seperti @isi : String pada bagian akhir, karena tidak ada ekspresi yang bisa disimpulkan compiler dari keadaan itu.
Variabel instans tanpa inisialisasi#
Pembacaan variabel instans diatur compiler dengan aturan yang ketat. Variabel instans yang belum pernah diisi wajib diberi anotasi tipe, dan compiler memeriksa bahwa setiap variabel instans non-nilable benar-benar diinisialisasi di semua metode initialize. Pengisian yang dilakukan metode lain tidak memenuhi syarat itu. Program berikut mendeklarasikan @isi : String tanpa pernah mengisinya di initialize, lalu mencoba membacanya dari metode cetak.
class Laporan
@isi : String
def cetak : String
@isi
end
end
puts Laporan.new.cetakShowing last frame. Use --error-trace for full trace.
In laporan.cr:2:3
2 | @isi : String
^---
Error: instance variable '@isi' of Laporan was not initialized directly in all of the 'initialize' methods, rendering it nilable. Indirect initialization is not supported.Kompilasi ditolak dengan alasan yang persis seperti aturannya: @isi tidak pernah diinisialisasi langsung di metode initialize, sehingga variabel itu berpotensi bernilai nil dan tidak boleh dianggap String. Ada dua perbaikan yang lazim. Pertama, isi variabelnya di initialize agar tipenya tetap non-nilable. Kedua, nyatakan variabelnya sebagai nilable dengan akhiran tanda tanya, misalnya @isi : String? yang merupakan bentuk singkat dari String | Nil, sehingga membacanya sebelum diisi menghasilkan nil dan setiap penggunaannya diperiksa compiler. Pemeriksaan ini terasa ketat pada awalnya, tetapi setiap pesan errornya mencegah bacaan nilai yang belum tentu ada, dan aturan yang sama inilah yang membuat variabel instans dapat diperlakukan sebagai data yang dijamin ada.
Ringkasan#
- Class didefinisikan dengan
class, dan objeknya dibuat melaluinewyang memanggilinitialize. - Parameter
initializeberbentuk@nama : Stringsekaligus mendeklarasikan dan mengisi variabel instans tersebut. - Tipe variabel instans disimpulkan dari ekspresi initializer, misalnya
@total = 0menghasilkan Int32, dan dapat melebar menjadi union apabila metode lain mengisi tipe berbeda. - Variabel instans non-nilable wajib diinisialisasi langsung di semua metode
initializesebelum boleh dibaca.
Referensi#
- The Crystal Programming Language Reference — crystal-lang.org — Dokumentasi resmi sintaks dan semantik bahasa, termasuk aturan variabel instans dan inferensinya.
- Dokumentasi API Crystal 1.22.1 — crystal-lang.org — Rujukan pustaka standar untuk tipe bawaan seperti Int32, String, dan Nil.