Pewarisan

Perantaian __index untuk pewarisan antarkelas beserta idiom klasiknya.

Daftar isi

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.

lua
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())
text Keluaran
1.5
sensor tangki utama

Field 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.

pemanggilan s:uraikan()objek sfield: nama, offseturaikan tidak adakelas SensorKalibrasimetode: baruuraikan tidak adakelas Dasarmetode: baru, uraikanuraikan ditemukan__index__index
Rantai pencarian metode melalui perantaian __index

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.

lua
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())
text Keluaran
24.0
25.5

Konstruktor 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.

lua Templat: idiom klasik pewarisan
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
end

Templat 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#