Apa itu table dan metatable dalam Lua?

API7.ai

October 11, 2022

OpenResty (NGINX + Lua)

Hari ini kita akan mempelajari tentang satu-satunya struktur data di LuaJIT: table.

Tidak seperti bahasa skrip lain yang memiliki struktur data yang kaya, LuaJIT hanya memiliki satu struktur data, yaitu table, yang tidak dibedakan antara array, hash, koleksi, dan sebagainya, melainkan agak tercampur. Mari kita tinjau salah satu contoh yang disebutkan sebelumnya.

local color = {first = "red", "blue", third = "green", "yellow"} print(color["first"]) --> output: red print(color[1]) --> output: blue print(color["third"]) --> output: green print(color[2]) --> output: yellow print(color[3]) --> output: nil

Dalam contoh ini, tabel color berisi array dan hash, dan dapat diakses tanpa saling mengganggu. Misalnya, Anda dapat menggunakan fungsi ipairs untuk mengiterasi hanya bagian array dari tabel.

$ resty -e 'local color = {first = "red", "blue", third = "green", "yellow"} for k, v in ipairs(color) do print(k) end '

Operasi table sangat penting sehingga LuaJIT memperluas library tabel standar Lua 5.1, dan OpenResty memperluas library tabel LuaJIT lebih jauh lagi. Mari kita lihat masing-masing fungsi library ini.

Fungsi library tabel

Mari kita mulai dengan fungsi library tabel standar. Lua 5.1 tidak memiliki banyak fungsi library tabel, jadi kita bisa melihatnya sekilas.

table.getn Mendapatkan jumlah elemen

Seperti yang kami sebutkan di bab Standard Lua dan LuaJIT, mendapatkan jumlah elemen tabel yang benar adalah masalah besar di LuaJIT.

Untuk urutan, Anda dapat menggunakan table.getn atau operator unary # untuk mengembalikan jumlah elemen yang benar. Contoh berikut mengembalikan angka 3 yang kita harapkan.

$ resty -e 'local t = { 1, 2, 3 } print(table.getn(t))

Nilai yang benar tidak dapat dikembalikan untuk tabel yang tidak berurutan. Pada contoh kedua, nilai yang dikembalikan adalah 1.

$ resty -e 'local t = { 1, a = 2 } print(#t) '

Untungnya, fungsi yang sulit dipahami seperti ini telah digantikan oleh ekstensi LuaJIT, yang akan kami sebutkan nanti. Jadi dalam konteks OpenResty, jangan gunakan fungsi table.getn dan operator unary # kecuali Anda tahu secara eksplisit bahwa Anda sedang mendapatkan panjang urutan.

Selain itu, table.getn dan operator unary # bukanlah kompleksitas waktu O(1) melainkan O(n), yang merupakan alasan lain untuk menghindarinya jika memungkinkan.

table.remove Menghapus elemen yang ditentukan

Yang kedua adalah fungsi table.remove, yang menghapus elemen dalam tabel berdasarkan subskrip, yaitu hanya elemen di bagian array tabel yang dapat dihapus. Mari kita lihat contoh color lagi.

$ resty -e 'local color = {first = "red", "blue", third = "green", "yellow"} table.remove(color, 1) for k, v in pairs(color) do print(v) end'

Kode ini akan menghapus blue dengan subskrip 1. Anda mungkin bertanya, bagaimana cara menghapus bagian hash dari tabel? Caranya sederhana, yaitu dengan mengatur nilai yang sesuai dengan kunci ke nil. Jadi, dalam contoh color, green yang sesuai dengan third dihapus.

$ resty -e 'local color = {first = "red", "blue", third = "green", "yellow"} color.third = nil for k, v in pairs(color) do print(v) end'

table.concat Fungsi penggabungan elemen

Yang ketiga adalah fungsi penggabungan elemen table.concat. Ini menggabungkan elemen-elemen tabel berdasarkan subskrip. Karena ini lagi-lagi berdasarkan subskrip, ini masih untuk bagian array dari tabel. Sekali lagi dengan contoh color.

$ resty -e 'local color = {first = "red", "blue", third = "green", "yellow"} print(table.concat(color, ", "))'

Setelah menggunakan fungsi table.concat, ini akan mengeluarkan blue, yellow dan bagian hash dilewati.

Selain itu, fungsi ini juga dapat menentukan posisi awal subskrip untuk melakukan penggabungan; misalnya, ditulis seperti berikut

$ resty -e 'local color = {first = "red", "blue", third = "green", "yellow", "orange"} print(table.concat(color, ", ", 2, 3))'

Kali ini outputnya adalah yellow, orange, melewatkan blue.

Jangan meremehkan fungsi yang tampaknya tidak berguna ini, tetapi ini dapat memiliki efek yang tidak terduga saat mengoptimalkan kinerja dan merupakan salah satu karakter utama dalam bab optimasi kinerja kami nanti.

table.insert Menyisipkan elemen

Terakhir, mari kita lihat fungsi table.insert. Ini menyisipkan elemen baru di subskrip yang ditentukan, yang memengaruhi bagian array dari tabel. Untuk menggambarkan, sekali lagi, menggunakan contoh color.

$ resty -e 'local color = {first = "red", "blue", third = "green", "yellow"} table.insert(color, 1, "orange") print(color[1]) '

Anda dapat melihat bahwa elemen pertama color menjadi orange, tetapi tentu saja, Anda dapat meninggalkan subskrip tidak ditentukan sehingga akan disisipkan di akhir antrian secara default.

Saya harus mencatat bahwa table.insert adalah operasi yang umum, tetapi kinerjanya tidak baik. Jika Anda tidak menyisipkan elemen berdasarkan skrip yang ditentukan, maka Anda perlu memanggil lj_tab_len LuaJIT setiap kali untuk mendapatkan panjang array untuk disisipkan di akhir antrian. Seperti table.getn, kompleksitas waktu untuk mendapatkan panjang tabel adalah O(n).

Jadi, untuk operasi table.insert; kita harus mencoba menghindari penggunaannya dalam kode panas. Misalnya:

local t = {} for i = 1, 10000 do table.insert(t, i) end

Fungsi ekstensi tabel LuaJIT

Selanjutnya, mari kita lihat fungsi ekstensi tabel LuaJIT. LuaJIT memperluas Lua standar dengan dua fungsi tabel yang bermanfaat untuk membuat dan mengosongkan tabel, yang akan saya jelaskan di bawah ini.

table.new(narray, nhash) Membuat tabel baru

Yang pertama adalah fungsi table.new(narray, nhash). Alih-alih tumbuh sendiri saat menyisipkan elemen, fungsi ini akan mengalokasikan ukuran ruang array dan hash yang ditentukan, yang merupakan arti dari dua parameter narray dan nhash. Pertumbuhan sendiri adalah operasi yang mahal yang melibatkan alokasi ruang, resize dan rehash, dan harus dihindari sebisa mungkin.

Perhatikan di sini bahwa dokumentasi untuk table.new tidak ada di situs web LuaJIT tetapi tersembunyi di dokumentasi ekstensi proyek GitHub, sehingga sulit ditemukan bahkan jika Anda mencarinya di Google, jadi tidak banyak insinyur yang mengetahuinya.

Berikut adalah contoh sederhana, dan saya akan menunjukkan cara kerjanya. Pertama-tama, fungsi ini adalah ekstensi, jadi sebelum Anda dapat menggunakannya, Anda perlu require.

local new_tab = require "table.new" local t = new_tab(100, 0) for i = 1, 100 do t[i] = i end

Seperti yang Anda lihat, kode ini membuat tabel baru dengan 100 elemen array dan 0 elemen hash. Tentu saja, Anda dapat membuat tabel baru dengan 100 elemen array dan 50 elemen hash sesuai kebutuhan, yang legal.

local t = new_tab(100, 50)

Atau, jika Anda melampaui ukuran ruang yang telah ditentukan, Anda masih dapat menggunakannya seperti biasa, tetapi kinerjanya akan menurun, dan tujuan menggunakan table.new akan hilang.

Dalam contoh berikut, kami memiliki ukuran yang telah ditentukan sebesar 100, tetapi kami menggunakan 200.

local new_tab = require "table.new" local t = new_tab(100, 0) for i = 1, 200 do t[i] = i end

Anda perlu menentukan ukuran ruang array dan hash di table.new sesuai dengan skenario aktual sehingga Anda dapat menemukan keseimbangan antara kinerja dan penggunaan memori.

table.clear() Mengosongkan tabel

Yang kedua adalah fungsi clear table.clear(). Ini mengosongkan semua data dalam tabel tetapi tidak membebaskan memori yang digunakan oleh bagian array dan hash. Oleh karena itu, ini sangat bermanfaat saat mendaur ulang tabel Lua untuk menghindari overhead pembuatan dan penghancuran tabel berulang kali.

$ resty -e 'local clear_tab =require "table.clear" local color = {first = "red", "blue", third = "green", "yellow"} clear_tab(color) for k, v in pairs(color) do print(k) end'

Namun, tidak banyak skenario di mana fungsi ini dapat digunakan, dan dalam kebanyakan kasus, kita harus menyerahkan tugas ini kepada GC LuaJIT.

Fungsi ekstensi tabel OpenResty

Seperti yang saya sebutkan di awal, OpenResty mempertahankan cabang LuaJIT-nya sendiri, yang juga memperluas tabel, dengan beberapa API baru: table.isempty, table. isarray, table.nkeys dan table.clone.

Sebelum menggunakan API baru ini, harap periksa versi OpenResty, karena sebagian besar API ini hanya dapat digunakan dalam versi OpenResty setelah 1.15.8.1. Ini karena OpenResty tidak memiliki rilis baru selama sekitar satu tahun sebelum versi 1.15.8.1, dan API ini ditambahkan dalam interval rilis tersebut.

Saya telah menyertakan tautan ke artikel, jadi saya akan menggunakan table.nkeys sebagai contoh. Tiga API lainnya cukup mudah dipahami dari sudut pandang penamaan, jadi lihat dokumentasi GitHub, dan Anda akan memahaminya. Saya harus mengatakan bahwa dokumentasi OpenResty sangat berkualitas tinggi, termasuk contoh kode, apakah itu dapat di-JIT, apa yang harus diperhatikan, dll. Beberapa tingkat lebih baik daripada dokumentasi Lua dan LuaJIT.

Oke, kembali ke fungsi table.nkeys. Penamaannya mungkin membingungkan Anda, tetapi ini adalah fungsi yang mendapatkan panjang tabel dan mengembalikan jumlah elemen tabel, termasuk elemen array dan bagian hash. Oleh karena itu, kita dapat menggunakannya sebagai pengganti table.getn, misalnya, sebagai berikut.

local nkeys = require "table.nkeys" print(nkeys({})) -- 0 print(nkeys({ "a", nil, "b" })) -- 2 print(nkeys({ dog = 3, cat = 4, bird = nil })) -- 2 print(nkeys({ "a", dog = 3, cat = 4 })) -- 3

Metatable

Setelah membahas fungsi tabel, mari kita lihat metatable yang berasal dari table. Metatable adalah konsep unik di Lua, dan banyak digunakan dalam proyek-proyek nyata. Tidak berlebihan untuk mengatakan bahwa Anda dapat menemukannya di hampir semua library lua-resty-*.

Metatable berperilaku seperti operator overloading; misalnya, kita dapat meng-overload __add untuk menghitung penggabungan dua array Lua atau __tostring untuk mendefinisikan fungsi yang mengubah ke string.

Lua, di sisi lain, menyediakan dua fungsi untuk menangani metatable.

  • Yang pertama adalah setmetatable(table, metatable), yang mengatur metatable untuk tabel.
  • Yang kedua adalah getmetatable(table), yang mendapatkan metatable dari tabel.

Setelah semua ini, Anda mungkin lebih tertarik pada apa yang dilakukannya, jadi mari kita lihat apa yang sebenarnya digunakan metatable. Berikut adalah potongan kode dari proyek nyata.

$ resty -e ' local version = { major = 1, minor = 1, patch = 1 } version = setmetatable(version, { __tostring = function(t) return string.format("%d.%d.%d", t.major, t.minor, t.patch) end }) print(tostring(version)) '

Kami pertama-tama mendefinisikan tabel bernama version, dan seperti yang Anda lihat, tujuan kode ini adalah untuk mencetak nomor versi dalam version. Namun, kami tidak dapat mencetak version secara langsung. Anda dapat mencoba melakukan ini dan melihat bahwa mencetak langsung hanya akan mengeluarkan alamat tabel.

print(tostring(version))

Jadi, kami perlu menyesuaikan fungsi konversi string untuk tabel ini, yaitu __tostring, dan di sinilah metatable masuk. Kami menggunakan setmetatable untuk mengatur ulang metode __tostring dari tabel version untuk mencetak nomor versi: 1.1.1.

Selain __tostring, kami sering meng-override dua metamethode berikut dalam metatable dalam proyek nyata.

Salah satunya adalah __index. Ketika kami mencari elemen dalam tabel, kami pertama-tama mencarinya langsung dari tabel, dan jika tidak menemukannya, kami melanjutkan ke __index dari metatable.

Kami menghapus patch dari tabel version dalam contoh berikut.

$ resty -e ' local version = { major = 1, minor = 1 } version = setmetatable(version, { __index = function(t, key) if key == "patch" then return 2 end end, __tostring = function(t) return string.format("%d.%d.%d", t.major, t.minor, t.patch) end }) print(tostring(version)) '

Dalam kasus ini, t.patch tidak mendapatkan nilai, jadi ia pergi ke fungsi __index, yang mencetak 1.1.2.

__index tidak hanya bisa berupa fungsi tetapi juga tabel, dan jika Anda mencoba menjalankan kode berikut, Anda akan melihat bahwa mereka mencapai hasil yang sama.

$ resty -e ' local version = { major = 1, minor = 1 } version = setmetatable(version, { __index = {patch = 2}, __tostring = function(t) return string.format("%d.%d.%d", t.major, t.minor, t.patch) end }) print(tostring(version)) '

Metamethode lainnya adalah __call. Ini mirip dengan functor yang memungkinkan tabel untuk dipanggil.

Mari kita bangun di atas kode yang mencetak nomor versi dan lihat bagaimana cara memanggil tabel.

$ resty -e ' local version = { major = 1, minor = 1, patch = 1 } local function print_version(t) print(string.format("%d.%d.%d", t.major, t.minor, t.patch)) end version = setmetatable(version, {__call = print_version}) version() '

Dalam kode ini, kami menggunakan setmetatable untuk menambahkan metatable ke tabel version, dan metamethode __call di dalamnya menunjuk ke fungsi print_version. Jadi, jika kami mencoba memanggil version sebagai fungsi, fungsi print_version akan dieksekusi di sini.

Dan getmetatable adalah operasi yang dipasangkan dengan setmetatable untuk mendapatkan metatable yang telah diatur, seperti kode berikut.

$ resty -e ' local version = { major = 1, minor = 1 } version = setmetatable(version, { __index = {patch = 2}, __tostring = function(t) return string.format("%d.%d.%d", t.major, t.minor, t.patch) end }) print(getmetatable(version).__index.patch) '

Selain tiga metamethode yang kami bahas hari ini, ada beberapa metamethode yang jarang digunakan yang dapat Anda konsultasikan di dokumentasi untuk mempelajari lebih lanjut saat Anda menemukannya.

Pemrograman Berorientasi Objek

Terakhir, mari kita bicara tentang pemrograman berorientasi objek. Seperti yang Anda ketahui, Lua bukanlah bahasa Pemrograman Berorientasi Objek, tetapi kita dapat menggunakan metatable untuk mengimplementasikan OO.

Mari kita lihat contoh praktis. lua-resty-mysql adalah klien MySQL resmi OpenResty, dan ia menggunakan metatable simulasi kelas dan metode kelas, yang digunakan dengan cara berikut.

$ resty -e 'local mysql = require "resty.mysql" -- pertama-tama referensikan library lua-resty local db, err = mysql:new() -- Buat instance baru dari kelas db:set_timeout(1000) -- Memanggil metode kelas

Anda dapat menjalankan kode di atas langsung dengan perintah resty. Baris-baris kode ini mudah dipahami; satu-satunya hal yang mungkin membuat Anda bingung adalah.

Saat memanggil metode kelas, mengapa menggunakan titik dua alih-alih titik?

Sebenarnya, titik dua dan titik sama-sama baik di sini, dan db:set_timeout(1000) dan db.set_timeout(db, 1000) persis sama. Titik dua adalah gula sintaksis di Lua yang memungkinkan menghilangkan argumen pertama self dari suatu fungsi.

Seperti yang kita semua tahu, tidak ada rahasia di depan kode sumber, jadi mari kita lihat implementasi konkret yang sesuai dengan baris kode di atas sehingga Anda dapat lebih memahami cara memodelkan pemrograman berorientasi objek dengan metatable.

local _M = { _VERSION = '0.21' } -- Menggunakan tabel simulasi kelas local mt = { __index = _M } -- mt adalah singkatan dari metatable, __index merujuk ke kelas itu sendiri -- Konstruktor kelas function _M.new(self) local sock, err = tcp() if not sock then return nil, err end return setmetatable({ sock = sock }, mt) -- contoh simulasi kelas menggunakan tabel dan metatable end -- Fungsi anggota kelas function _M.set_timeout(self, timeout) -- Gunakan argumen self untuk mendapatkan instance kelas yang ingin Anda operasikan local sock = self.sock if not sock then return nil, "not initialized" end return sock:settimeout(timeout) end

Tabel _M mensimulasikan kelas yang diinisialisasi dengan satu variabel anggota _VERSION dan selanjutnya mendefinisikan fungsi anggota seperti _M.set_timeout. Dalam konstruktor _M.new(self), kami mengembalikan tabel yang metatable-nya adalah mt, dan metamethode __index dari mt menunjuk ke _M sehingga tabel yang dikembalikan mensimulasikan instance dari kelas _M.

Ringkasan

Baiklah, itu menyimpulkan konten utama untuk hari ini. Tabel dan metatable banyak digunakan dalam library lua-resty-* OpenResty dan proyek open source berbasis OpenResty. Saya harap pelajaran ini akan memudahkan Anda untuk membaca dan memahami kode sumber.

Ada fungsi standar lain di Lua selain tabel, yang akan kita pelajari bersama dalam pelajaran berikutnya.

Terakhir, saya ingin meninggalkan Anda dengan pertanyaan yang merenungkan. Mengapa library lua-resty-mysql meniru OO sebagai lapisan pembungkus? Silakan diskusikan pertanyaan ini di bagian komentar, dan silakan bagikan artikel ini dengan rekan dan teman Anda sehingga kita dapat berkomunikasi dan berkembang bersama.