Cast dan assertion

as, as?, try, dan not_nil! beserta risiko mengabaikan pemeriksaan tipe.

Daftar isi

Nilai bertipe union sering kali harus digunakan sebagai salah satu tipenya saja, misalnya elemen dari array campuran String dan Int32. Topik ini membahas empat alat Crystal untuk keperluan itu, yaitu cast turun dengan as, bentuk aman as?, metode try pada nilai nilable, dan assertion not_nil!, beserta risiko yang menyertai alat yang mengabaikan pemeriksaan compiler.

Cast turun dengan as#

Pernyataan as menyatakan klaim bahwa sebuah nilai bertipe tertentu. Klaim yang sudah mustahil ditolak compiler sejak waktu kompilasi dengan pesan can't cast, sedangkan klaim yang masih mungkin lolos kompilasi dan diverifikasi ulang saat runtime. Cast yang ternyata salah melontarkan TypeCastError, sehingga pemanggilnya perlu struktur begin dan rescue apabila kecocokan tipe memang belum pasti.

crystal konversi-as.cr
# Cast turun dengan as: klaim tipe diverifikasi ulang saat runtime.
# Jalankan dengan: crystal konversi-as.cr

kumpulan = [24, "delapan", 5] of String | Int32

kumpulan.each do |nilai|
  begin
    angka = nilai.as(Int32)
    puts "angka: #{angka}"
  rescue e : TypeCastError
    # pesan exception memuat lokasi berkas, dipotong agar ringkas
    puts "ditolak: #{e.message.to_s.split(", at").first}"
  end
end
bash
# menjalankan program cast turun
crystal konversi-as.cr
text Keluaran
angka: 24
ditolak: Cast from String to Int32 failed
angka: 5

Elemen kumpulan bertipe String | Int32 sehingga cast ke Int32 lolos kompilasi. Elemen pertama dan ketiga memang Int32 sehingga castnya berhasil, sedangkan elemen kedua berupa string sehingga runtime melontarkan TypeCastError dengan pesan Cast from String to Int32 failed. Klausa rescue menangkap exception itu sehingga perulangan dapat melanjutkan ke elemen berikutnya. Apabila cast ditujukan pada tipe yang sudah pasti tidak cocok, misalnya setelah pengecilan menunjukkan nilainya String, compiler menolaknya dengan pesan can't cast String to Int32 sebelum program sempat dijalankan.

Verifikasi ulang saat runtime diperlukan karena nilai sesungguhnya dari elemen sebuah array union baru diketahui ketika program berjalan. Compiler hanya mengetahui daftar tipe yang mungkin, sedangkan keputusan akhir menunggu nilai konkretnya. Pola dua lapis inilah yang membuat as tetap aman digunakan, yaitu klaim yang mustahil tertolak lebih awal dan klaim yang mungkin diuji saat pemakaiannya.

as? dan try#

Bentuk as? tidak melontarkan exception. Cast yang cocok mengembalikan nilainya, sedangkan cast yang gagal mengembalikan nil, sehingga hasilnya bertipe nilable dan dapat diperiksa seperti hasil pencarian. Metode try bekerja pada nilai nilable dengan menjalankan bloknya hanya ketika nilainya bukan nil, dan mengembalikan nil tanpa menjalankan blok apabila nilainya nil.

crystal aman.cr
# as? dan try: pemeriksaan tipe yang tidak melempar exception.
# Jalankan dengan: crystal aman.cr

nilai : String | Int32 = "tiga"
puts "as? String: #{nilai.as?(String).inspect}"
puts "as? Int32: #{nilai.as?(Int32).inspect}"

nama : String? = nil
puts "try pada nil: #{nama.try(&.size).inspect}"
nama = "Ali"
puts "try pada String: #{nama.try(&.size).inspect}"

bilangan : Int32? = 21
puts "try dengan blok: #{bilangan.try { |v| v * 2 }.inspect}"
bash
# menjalankan program pemeriksaan aman
crystal aman.cr
text Keluaran
as? String: "tiga"
as? Int32: nil
try pada nil: nil
try pada String: 3
try dengan blok: 42

Pada program di atas, as?(String) mengembalikan isi aslinya sedangkan as?(Int32) mengembalikan nil, dan keduanya diperiksa dengan inspect agar nil terlihat jelas. Metode try pada variabel nama tidak menjalankan size ketika namanya nil dan mengembalikan nil. Bentuk try &.size merupakan singkatan untuk satu panggilan metode, sedangkan bentuk dengan blok biasa cocok untuk ekspresi yang lebih panjang seperti v * 2.

Hasil as? maupun try sama-sama bertipe nilable sehingga keduanya dapat dirangkai dengan pemeriksaan berikutnya, misalnya percabangan if terhadap nil. Rangkaian seperti ini memindahkan penanganan ketidakcocokan tipe dari blok rescue ke alur data yang biasa, dan program tidak perlu menangkap exception untuk keadaan yang memang wajar.

Assertion dengan not_nil!#

Metode not_nil! mengubah nilai nilable menjadi nilai biasa dengan satu syarat, yaitu nilainya memang bukan nil. Apabila syarat itu dilanggar, metode ini melontarkan NilAssertionError dengan pesan Nil assertion failed. Nilai baliknya bertipe asli tanpa nil, sehingga pemanggilan metode yang menuntut tipe lengkap dapat dilakukan langsung setelahnya. Assertion, yaitu pernyataan paksa atas keadaan program, tepat digunakan ketika logika di sekitarnya sudah menjamin keberadaan nilai, misalnya setelah langkah pemeriksaan yang sudah dilakukan sebelumnya.

crystal not-nil.cr
# not_nil!: assertion yang mematikan pengecilan compiler.
# Jalankan dengan: crystal not-nil.cr

nama : String? = "Citra"
puts "panjang nama: #{nama.not_nil!.size}"

nama = nil
begin
  nama.not_nil!
rescue e : NilAssertionError
  puts "ditolak: #{e.message}"
end
bash
# menjalankan program assertion not_nil!
crystal not-nil.cr
text Keluaran
panjang nama: 5
ditolak: Nil assertion failed

Pemanggilan pertama berhasil karena namanya "Citra", dan hasilnya langsung digunakan untuk menghitung panjang. Pemanggilan kedua dilakukan pada variabel yang berisi nil, sehingga NilAssertionError terlempar dan ditangkap rescue beserta pesannya.

Risiko mengabaikan pengecilan compiler#

Percabangan is_a? mengecilkan tipe variabel di dalam cabangnya, dan kebenaran tipe itu dijamin compiler karena pemeriksaannya bersifat statis.

crystal Pengecilan tipe dengan is_a?
nilai : String | Int32 = "halo"
if nilai.is_a?(String)
  puts "teks: #{nilai.upcase}"
end
text Keluaran
teks: HALO

Pada contoh di atas, compiler tahu bahwa nilai bertipe String di dalam cabang if, sehingga pemanggilan upcase dijamin valid tanpa pemeriksaan apa pun saat runtime. Perilaku ini berbeda dari as dan not_nil!. Keduanya menghentikan pengecilan compiler karena tipe sudah diklaim secara paksa, dan klaim itu tidak diverifikasi ulang oleh compiler. Apabila klaimnya keliru, kegagalan baru terasa saat runtime dalam bentuk TypeCastError atau NilAssertionError.

Kaidah praktisnya, perlakukan as dan not_nil! sebagai klaim yang menuntut bukti. Bukti itu dapat berupa pemeriksaan yang sudah dilakukan, data yang bentuknya dijamin oleh kode lain, atau kesepakatan dengan pustaka eksternal. Tanpa bukti, pilih is_a?, as?, atau try karena ketiganya membiarkan compiler tetap memeriksa program. Kegagalan runtime dari klaim yang keliru masih dapat ditangkap rescue seperti exception lain, tetapi menangkapnya setelah keadaan program kacau jauh lebih sulit daripada mencegahnya sejak waktu kompilasi.

Ringkasan#

  • Pernyataan as menurunkan tipe dengan klaim yang diverifikasi saat runtime dan melontarkan TypeCastError ketika klaimnya salah.
  • Bentuk as? mengembalikan nil alih-alih exception, sedangkan try menjalankan bloknya hanya pada nilai yang bukan nil.
  • Metode not_nil! mengembalikan nilainya atau melontarkan NilAssertionError dengan pesan Nil assertion failed.
  • as dan not_nil! mematikan pengecilan compiler, sehingga is_a? dan percabangan biasa lebih aman untuk tipe yang belum pasti.

Referensi#