Objek yang tugasnya hanya membawa beberapa nilai bersama sering muncul dalam program, misalnya satu baris pengukuran berisi lokasi dan derajat suhu. Menulis kelas penuh untuk kebutuhan semacam itu berlebihan, sehingga Ruby menyediakan Struct dan Data untuk membuat kelas record secara ringkas, yaitu kumpulan nilai yang terikat pada satu objek. Topik ini membahas Struct.new beserta aksesor dan kesetaraan nilainya, opsi keyword_init, Data.define sebagai record yang tidak dapat diubah beserta metode with, serta pertimbangan memilih di antara Struct, Data, dan kelas biasa.
Struct.new dan aksesor otomatis#
Struct.new(:nama, :nilai) menghasilkan kelas baru yang anggotanya sesuai dengan daftar simbol yang diberikan. Setiap anggota otomatis memperoleh metode pembaca dan penulis nilainya, dan konstruktor menerima nilai dalam urutan yang sama dengan daftar anggotanya. Kelas hasil Struct.new ditampung pada sebuah konstanta agar dapat digunakan seperti kelas lain, dan pemanggilan members menampilkan daftar anggota kelas tersebut. Konversi juga tersedia dalam bentuk to_a untuk array dan to_h untuk hash, sehingga isi record mudah diteruskan ke metode yang menerima bentuk lain.
# Struct untuk nilai ujian satu siswa
NilaiUjian = Struct.new(:nama, :nilai)
uji = NilaiUjian.new("Ali", 82)
puts uji.nama
puts uji.nilai
p NilaiUjian.members
puts ujiAli
82
[:nama, :nilai]
#<struct NilaiUjian nama="Ali", nilai=82>Berkas di atas dijalankan dengan perintah ruby nilai-ujian.rb. Instans uji menampung dua nilai, dan keduanya dibaca melalui uji.nama serta uji.nilai tanpa satu baris definisi metode pun. Metode members mengembalikan daftar simbol anggota, sedangkan pencetakan instans menampilkan bentuk struct lengkap dengan nama kelasnya. Semua persiapan ini biasanya memerlukan metode initialize dan attr_reader yang ditulis manual pada kelas biasa.
Kesetaraan berdasarkan nilai#
Perilaku Struct yang sering dimanfaatkan adalah perbandingannya. Metode == pada Struct menganggap dua instans sama apabila kelasnya sama dan setiap anggotanya bernilai sama, sedangkan kelas biasa secara bawaan membandingkan identitas objek. Kesetaraan nilai seperti ini membuat Struct cocok untuk data yang perlu dibandingkan isinya, misalnya dua baris hasil pencatatan yang sama. Nilai hash dihitung dari anggota yang sama, sehingga instans Struct juga dapat digunakan sebagai kunci Hash secara konsisten.
Suhu = Struct.new(:kota, :derajat)
a = Suhu.new("Bandung", 24.5)
b = Suhu.new("Bandung", 24.5)
c = Suhu.new("Jakarta", 24.5)
p a == b
p a == ctrue
falseInstans a dan b dibuat terpisah tetapi berisi nilai yang sama persis, sehingga a == b bernilai true. Instans c memiliki kota yang berbeda, dan perbandingannya bernilai false. Dengan kelas biasa, kesetaraan semacam ini hanya diperoleh setelah metode == ditulis sendiri. Hal lain yang perlu dicatat adalah sifatnya yang dapat berubah, karena penulis anggota seperti uji.nilai = 90 tersedia secara bawaan dan mengubah instansnya secara langsung.
keyword_init: true#
Opsi keyword_init: true pada Struct.new mengubah bentuk konstruktor menjadi menerima argumen keyword, yaitu argumen berlabel nama: "nilai". Pembuatan instans menjadi lebih terbaca karena setiap nilai menyertakan nama anggotanya, dan urutan penulisannya bebas. Tanpa opsi ini, konstruktor menerima nilai berurutan menurut daftar anggotanya, sehingga urutan nilai mudah tertukar pada pemanggilan dengan anggota yang banyak. Anggota yang tidak diberi nilai berisi nil, sedangkan keyword yang tidak dikenal membangkitkan ArgumentError.
# Struct dengan keyword_init menerima argumen berbentuk keyword
SuhuRuang = Struct.new(:lokasi, :derajat, keyword_init: true)
pagi = SuhuRuang.new(lokasi: "Ruang A", derajat: 24.5)
puts "#{pagi.lokasi}: #{pagi.derajat} derajat"Ruang A: 24.5 derajatBerkas di atas dijalankan dengan perintah ruby suhu-keyword.rb. Konstruktor menerima kedua nilai dengan labelnya masing-masing, sehingga maksud setiap argumen terlihat pada tempat pemanggilannya. Bentuk ini mirip dengan gaya konstruktor Data yang dibahas pada bagian berikutnya, sehingga perpindahan di antara keduanya tidak mengubah cara pemanggilan.
Data.define sebagai record immutable#
Kelas Data, tersedia sejak Ruby 3.2, menghasilkan record yang tidak dapat diubah (immutable) setelah dibuat. Data.define(:lokasi, :derajat) membuat kelas dengan aksesor pembaca, kesetaraan nilai, dan instans yang beku sejak pembuatannya. Perubahan isi dinyatakan dengan metode with, yang mengembalikan instans baru berisi salinan data lama dengan satu atau beberapa anggota yang diganti, sedangkan instans lama tetap utuh. Konversi menjadi hash tetap tersedia melalui to_h, sehingga isi record dapat diteruskan ke metode yang menerima hash.
# Data.define membuat record yang tidak dapat diubah
Suhu = Data.define(:lokasi, :derajat)
pagi = Suhu.new(lokasi: "Ruang A", derajat: 24.5)
puts "#{pagi.lokasi}: #{pagi.derajat} derajat"
siang = pagi.with(derajat: 27.0)
puts "#{siang.lokasi}: #{siang.derajat} derajat"
puts siang
begin
pagi.derajat = 30.0
rescue NoMethodError => e
puts e.class
endRuang A: 24.5 derajat
Ruang A: 27.0 derajat
#<data Suhu lokasi="Ruang A", derajat=27.0>
NoMethodErrorBerkas di atas dijalankan dengan perintah ruby suhu-data.rb. Instans siang berasal dari pagi.with(derajat: 27.0), dan keluarannya menunjukkan lokasi yang terbawa sementara derajatnya berganti, tanpa mengubah instans pagi. Percobaan mengisi pagi.derajat = 30.0 gagal dengan NoMethodError karena kelas Data tidak menyediakan aksesor penulis sama sekali. Bentuk pencetakan instansnya menggunakan awalan #<data sebagai penanda kelas Data.
Kapan Struct dan kapan Data#
Keduanya menghasilkan kelas record dengan biaya tulis yang sama, sedangkan perbedaannya terletak pada perubahan isi dan cara penyalinannya. Tabel berikut merangkum perbedaan yang paling menentukan pemilihan.
| Aspek | Struct | Data |
|---|---|---|
| Perubahan isi | aksesor penulis tersedia, misalnya uji.nilai = 90 | tidak ada aksesor penulis |
| Salin dengan perubahan | instans baru dibuat secara manual | with mengembalikan salinan baru |
| Keadaan instans | dapat berubah kapan pun | beku sejak pembuatan |
| Ketersediaan | bagian lama pustaka inti | tersedia sejak Ruby 3.2 |
Struct tepat untuk pembawa data yang memang diperbarui selama program berjalan, misalnya penghitung yang menampung nilai sementara. Data tepat untuk nilai yang seharusnya tidak berubah setelah dibuat, sehingga data yang sama dapat digunakan di banyak tempat tanpa risiko berubah tanpa sengaja. Adapun kelas biasa tetap menjadi pilihan ketika objek memerlukan metode perilaku sendiri, karena Struct dan Data memang dirancang untuk membawa data.
Kebebasan kelas biasa selalu disertai tanggung jawab, sebagaimana ditunjukkan contoh berikut.
class SuhuManual
attr_reader :lokasi, :derajat
def initialize(lokasi, derajat)
@lokasi = lokasi
@derajat = derajat
end
end
a = SuhuManual.new("Ruang A", 24.5)
b = SuhuManual.new("Ruang A", 24.5)
p a == bfalseKedua instans berisi nilai yang sama persis, tetapi a == b bernilai false karena kelas manual menggunakan perbandingan identitas bawaan Object. Struct dan Data memberikan hasil true untuk kasus yang sama tanpa satu baris tambahan. Itulah bagian pekerjaan yang diambil alih oleh kelas record, dan kelas manual memang hanya lebih tepat bila metode perilakunya benar-benar diperlukan.
Kesalahan umum#
Kesalahan pertama adalah mencoba mengubah instans Data melalui aksesor penulis. Kelas Data tidak memiliki metode penulis, sehingga pagi.derajat = 30.0 membangkitkan NoMethodError. Gunakan with untuk memperoleh instans baru dengan nilai yang diinginkan. Kesalahan ini biasanya muncul ketika kode yang semula menggunakan Struct diganti menjadi Data tanpa menyesuaikan penulisannya.
Kesalahan kedua adalah memanggil Struct.new berulang kali untuk konstanta yang sama, misalnya di dalam metode yang dipanggil berkali-kali. Setiap pemanggilan membuat kelas yang berbeda, dan penugasan ulang konstantanya memunculkan peringatan already initialized constant. Nyatakan Struct.new sekali di tingkat atas berkas, sama seperti pendefinisian kelas biasa.
Kesalahan ketiga adalah membandingkan dua instans dari hasil Struct.new yang dipanggil di tempat berbeda. Karena setiap pemanggilan menghasilkan kelas sendiri, perbandingan == bernilai false meskipun daftar anggota dan isinya identik. Bandingkan instans yang berasal dari satu definisi kelas yang sama, atau gunakan satu konstanta bersama sebagai definisinya.
Ringkasan#
Struct.new(:nama, :nilai)membuat kelas record dengan aksesor, kesetaraan berdasarkan nilai, dan bentuk teks secara otomatis.keyword_init: truemengubah konstruktorStructmenjadi menerima argumen berlabel, sedangkan anggota yang tidak diisi berisinil.Data.definemenghasilkan record beku yang tidak dapat diubah, danwithmengembalikan instans baru hasil penyalinan dengan penggantian anggota tertentu.Structdipilih bila isinya boleh berubah,Databila isinya harus tetap, dan kelas biasa bila objek memerlukan metode perilaku sendiri.
Referensi#
- Struct — Ruby — Dokumentasi kelas Struct beserta opsi keyword_init dan perilaku kesetaraannya.
- Data — Ruby — Dokumentasi kelas Data beserta Data.define dan metode with.