Pewarisan (inheritance) memungkinkan satu kelas memperoleh metode dari kelas lain tanpa menyalinnya. Di Lua, mekanismenya adalah perantaian __index antartable kelas. Topik ini membahas perantaian tersebut, urutan pencarian berlapis dari objek ke kelas turunan hingga kelas dasar, penimpaan (override) metode dengan nama sama, pemanggilan versi dasar dari metode yang ditimpa, serta idiom klasik yang merangkum semuanya.
Perantaian __index antarkelas#
Pewarisan terjadi ketika table kelas turunan membawa metatable yang __index-nya menunjuk ke kelas dasar, ditulis Sub = setmetatable({}, {__index = Dasar}). Kunci yang tidak ada pada kelas turunan kini jatuh ke kelas dasar, dan objek yang metatable-nya menggunakan kelas turunan pun mengikuti rantai tersebut. Pencarian bergerak dari objek, ke kelas turunan, lalu ke kelas dasar, dan berhenti pada kecocokan pertama. Contoh berikut membuat kelas SensorKalibrasi yang mewarisi Dasar.
local Dasar = {}
Dasar.__index = Dasar
function Dasar.baru(nama)
return setmetatable({nama = nama}, Dasar)
end
function Dasar:uraikan()
return "sensor " .. self.nama
end
local SensorKalibrasi = {}
SensorKalibrasi.__index = SensorKalibrasi
setmetatable(SensorKalibrasi, {__index = Dasar})
function SensorKalibrasi.baru(nama, offset)
local objek = setmetatable({nama = nama, offset = offset}, SensorKalibrasi)
return objek
end
local s = SensorKalibrasi.baru("tangki utama", 1.5)
print(s.offset)
print(s:uraikan())1.5
sensor tangki utamaField offset ditemukan langsung pada objek s, sedangkan metode uraikan tidak ada, baik pada objek s maupun pada SensorKalibrasi, sehingga pencariannya jatuh ke Dasar. Diagram berikut memetakan urutan pencarian pada pemanggilan s:uraikan() tersebut.
Kedua panah berlabel __index karena objek mencari lewat __index milik metatable-nya ke table kelas, dan kelas turunan meneruskan pencarian lewat __index milik metatable kelasnya ke kelas dasar. Penghentian pada diagram sesuai tiga tingkat table yang terlibat, yaitu objek, kelas turunan, dan kelas dasar. Bila tidak ada kecocokan sampai ujung rantai, hasil pencariannya nil dan pemanggilan berakhir dengan error. Karena pencarian berlangsung saat pemanggilan, metode yang ditambahkan pada Dasar setelah objek dibuat langsung dapat digunakan objek turunannya. Perilaku dinamis inilah yang membedakan perantaian __index dari penyalinan metode ke setiap kelas pada saat pembuatan objek.
Override metode dan pemanggilan metode dasar#
Karena pencarian berhenti pada kecocokan pertama, metode yang didefinisikan di kelas turunan menimpa versi dasar yang bernama sama. Versi dasar tidak hilang, karena masih dapat dipanggil secara eksplisit melalui Dasar.metode(self, argumen). Contoh berikut menimpa baca pada SensorKalibrasi, dan di dalamnya memanggil Sensor.baca(self) terlebih dahulu untuk mengambil pembacaan mentah.
local Sensor = {}
Sensor.__index = Sensor
function Sensor.baru(nilai)
return setmetatable({nilai = nilai}, Sensor)
end
function Sensor:baca()
return self.nilai
end
local SensorKalibrasi = {}
SensorKalibrasi.__index = SensorKalibrasi
setmetatable(SensorKalibrasi, {__index = Sensor})
function SensorKalibrasi.baru(nilai, offset)
local objek = Sensor.baru(nilai)
objek.offset = offset
return setmetatable(objek, SensorKalibrasi)
end
function SensorKalibrasi:baca()
local mentah = Sensor.baca(self)
return mentah + self.offset
end
local mentah = Sensor.baru(24.0)
local terkalibrasi = SensorKalibrasi.baru(24.0, 1.5)
print(mentah:baca())
print(terkalibrasi:baca())24.0
25.5Konstruktor SensorKalibrasi.baru memanfaatkan Sensor.baru untuk menyiapkan field dasar, lalu memindahkan kelas objek dengan setmetatable(objek, SensorKalibrasi). Pemanggilan terkalibrasi:baca() menemukan versi kalibrasi lebih dulu sehingga pembacaan mentah 24.0 diproses menjadi 25.5, sedangkan mentah:baca() tetap menggunakan versi dasar. Pemanggilan Sensor.baca(self) di dalam metode yang menimpa merupakan cara baku menyusun perilaku dasar alih-alih menduplikasi isinya.
Idiom klasik pewarisan#
Seluruh mekanisme di atas dirangkum oleh satu idiom yang lazim ditemui pada kode Lua. Kerangkanya memuat dua baris kunci, yaitu setmetatable(Sub, {__index = Dasar}) yang merantai kelas dan setmetatable(objek, Sub) yang memindahkan kelas objek. Metode yang menimpa versi dasar biasanya memanggil Dasar.metode(self, …) lebih dulu agar perilaku dasar tetap berjalan. Templat berikut berdiri sendiri dan dapat digunakan sebagai titik awal.
local Dasar = {}
Dasar.__index = Dasar
function Dasar.baru(...)
local objek = setmetatable({}, Dasar)
-- inisialisasi field milik kelas dasar di sini
return objek
end
function Dasar:metode(...)
-- perilaku dasar di sini
end
local Sub = {}
Sub.__index = Sub
setmetatable(Sub, {__index = Dasar})
function Sub.baru(...)
local objek = Dasar.baru(...) -- kerangka dari kelas dasar
return setmetatable(objek, Sub) -- pindahkan kelas objek ke Sub
end
function Sub:metode(...) -- menimpa metode dasar
local hasil = Dasar.metode(self, ...) -- panggil versi dasar bila diperlukan
-- kembangkan hasilnya di sini
return hasil
endTemplat ini tidak mencetak apa pun karena isinya masih kerangka, tetapi strukturnya sudah sah dan dapat langsung diisi. Rantai dapat diperpanjang ke tingkat berikutnya dengan mengaitkan kelas ketiga ke Sub melalui cara yang sama, dan pencarian akan terus naik sampai kecocokan ditemukan. Kedalaman rantai perlu dijaga tetap dangkal karena setiap tingkat menambah biaya pencarian dan mempersulit penelusuran perilaku program.
Kesalahan umum#
Tiga kesalahan paling sering muncul pada pewarisan berbasis __index. Pertama, baris setmetatable(Sub, {__index = Dasar}) terlupa, sehingga metode milik kelas dasar tidak pernah ditemukan dan pemanggilannya berakhir dengan pesan attempt to call a nil value. Perbaikannya cukup menambahkan baris tersebut setelah table kelas turunan didefinisikan.
Kedua, konstruktor kelas turunan memanggil Dasar.baru tetapi lupa memindahkan kelas objek dengan setmetatable(objek, Sub). Objeknya tetap berkelas Dasar, sehingga metode turunan tidak pernah terpakai dan override diam-diam gagal. Periksa kelas objek dengan getmetatable(objek) == Sub saat menguji konstruktor.
Ketiga, metode turunan memanggil versi dasar dengan bentuk titik dua, misalnya Dasar:baca(), sehingga self yang diterima versi dasar adalah table kelas Dasar dan bukan objeknya. Versi dasar membaca field dari kelas yang salah dan mengembalikan nilai nil. Bentuk yang benar adalah Dasar.baca(self), yaitu bentuk titik dengan self eksplisit sebagaimana diuraikan pada contoh SensorKalibrasi.
Ringkasan#
- Sub = setmetatable({}, {__index = Dasar}) merantai pencarian metode dari kelas turunan ke kelas dasar.
- Pencarian bergerak dari objek ke kelas turunan lalu ke kelas dasar dan berhenti pada kecocokan pertama.
- Metode turunan menimpa versi dasar, sedangkan versi dasar tetap dapat dipanggil dengan Dasar.metode(self, …).
- Objek dibangun lewat konstruktor kelas dasar lalu kelasnya dipindahkan dengan setmetatable(objek, Sub).
Referensi#
- Lua 5.4 Reference Manual — Lua.org — Bagian 2.4 menjelaskan pencarian __index berantai pada akses kunci yang tidak ada.
- Programming in Lua, edisi pertama — Lua.org — Bab 16 membahas kelas dan pewarisan tunggal beserta idiom pembangunannya.