Was ist eine Tabelle und Metatabelle in Lua?
API7.ai
October 11, 2022
Heute lernen wir über die einzige Datenstruktur in LuaJIT: table
.
Im Gegensatz zu anderen Skriptsprachen mit reichhaltigen Datenstrukturen hat LuaJIT nur eine Datenstruktur, table
, die nicht zwischen Arrays, Hashes, Sammlungen usw. unterschieden wird, sondern eher gemischt ist. Schauen wir uns eines der zuvor erwähnten Beispiele an.
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
In diesem Beispiel enthält die Tabelle color
ein Array und einen Hash und kann ohne gegenseitige Beeinflussung darauf zugegriffen werden. Zum Beispiel können Sie die Funktion ipairs
verwenden, um nur den Array-Teil der Tabelle zu durchlaufen.
$ resty -e 'local color = {first = "red", "blue", third = "green", "yellow"}
for k, v in ipairs(color) do
print(k)
end
'
Die table
-Operationen sind so entscheidend, dass LuaJIT die Standard-Lua-5.1-Table-Bibliothek erweitert und OpenResty die LuaJIT-Table-Bibliothek noch weiter erweitert. Schauen wir uns jede dieser Bibliotheksfunktionen an.
Die Table-Bibliotheksfunktionen
Beginnen wir mit den Standard-Table-Bibliotheksfunktionen. Lua 5.1 hat nicht viele Table-Bibliotheksfunktionen, daher können wir sie schnell durchgehen.
table.getn
Anzahl der Elemente ermitteln
Wie wir im Kapitel Standard Lua und LuaJIT erwähnt haben, ist das Ermitteln der korrekten Anzahl aller Tabellenelemente ein großes Problem in LuaJIT.
Für Sequenzen können Sie table.getn
oder den unären Operator #
verwenden, um die korrekte Anzahl der Elemente zurückzugeben. Das folgende Beispiel gibt die erwartete Anzahl von 3 zurück.
$ resty -e 'local t = { 1, 2, 3 }
print(table.getn(t))
Für Tabellen, die nicht sequentiell sind, kann der korrekte Wert nicht zurückgegeben werden. Im zweiten Beispiel ist der zurückgegebene Wert 1.
$ resty -e 'local t = { 1, a = 2 }
print(#t) '
Glücklicherweise wurden solche schwer verständlichen Funktionen durch Erweiterungen von LuaJIT ersetzt, auf die wir später eingehen werden. Verwenden Sie im Kontext von OpenResty daher nicht die Funktion table.getn
und den unären Operator #
, es sei denn, Sie wissen explizit, dass Sie die Sequenzlänge ermitteln.
Außerdem sind table.getn
und der unäre Operator #
nicht O(1)-zeitkomplex, sondern O(n), was ein weiterer Grund ist, sie nach Möglichkeit zu vermeiden.
table.remove
Entfernt das angegebene Element
Die zweite Funktion ist table.remove
, die Elemente in der Tabelle basierend auf Indizes entfernt, d.h., nur die Elemente im Array-Teil der Tabelle können entfernt werden. Schauen wir uns noch einmal das color
-Beispiel an.
$ resty -e 'local color = {first = "red", "blue", third = "green", "yellow"}
table.remove(color, 1)
for k, v in pairs(color) do
print(v)
end'
Dieser Code entfernt das blue
mit dem Index 1. Sie fragen sich vielleicht, wie ich den Hash-Teil der Tabelle löschen kann? Es ist so einfach wie das Setzen des Werts, der dem Schlüssel entspricht, auf nil
. Somit wird im color
-Beispiel das green
, das third
entspricht, gelöscht.
$ 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
Elementverkettungsfunktion
Die dritte Funktion ist die table.concat
-Elementverkettungsfunktion. Sie verkettet die Elemente der Tabelle gemäß den Indizes. Da dies wieder auf Indizes basiert, gilt es immer noch für den Array-Teil der Tabelle. Wieder mit dem color
-Beispiel.
$ resty -e 'local color = {first = "red", "blue", third = "green", "yellow"}
print(table.concat(color, ", "))'
Nach der Verwendung der table.concat
-Funktion gibt es blue, yellow
aus, und der Hash-Teil wird übersprungen.
Darüber hinaus kann diese Funktion auch die Startposition des Index angeben, um die Verkettung durchzuführen; zum Beispiel wird sie wie folgt geschrieben:
$ resty -e 'local color = {first = "red", "blue", third = "green", "yellow", "orange"}
print(table.concat(color, ", ", 2, 3))'
Diesmal ist die Ausgabe yellow, orange
, wobei blue
übersprungen wird.
Unterschätzen Sie diese scheinbar nutzlose Funktion nicht, aber sie kann unerwartete Effekte bei der Leistungsoptimierung haben und ist einer der Hauptakteure in unseren späteren Kapiteln zur Leistungsoptimierung.
table.insert
Fügt ein Element ein
Schließlich schauen wir uns die table.insert
-Funktion an. Sie fügt ein neues Element am angegebenen Index ein, was den Array-Teil der Tabelle beeinflusst. Zur Veranschaulichung verwenden wir wieder das color
-Beispiel.
$ resty -e 'local color = {first = "red", "blue", third = "green", "yellow"}
table.insert(color, 1, "orange")
print(color[1])
'
Sie können sehen, dass das erste Element von color
zu orange
wird, aber natürlich können Sie den Index auch nicht angeben, sodass es standardmäßig am Ende der Warteschlange eingefügt wird.
Ich sollte darauf hinweisen, dass table.insert
eine weit verbreitete Operation ist, aber die Leistung ist nicht gut. Wenn Sie Elemente nicht basierend auf dem angegebenen Skript einfügen, müssen Sie jedes Mal LuaJITs lj_tab_len
aufrufen, um die Array-Länge zu erhalten, um am Ende der Warteschlange einzufügen. Wie bei table.getn
ist die Zeitkomplexität zum Ermitteln der Tabellenlänge O(n).
Daher sollten wir versuchen, die Verwendung von table.insert
in Hot-Code zu vermeiden. Zum Beispiel:
local t = {}
for i = 1, 10000 do
table.insert(t, i)
end
LuaJITs Table-Erweiterungsfunktionen
Als nächstes schauen wir uns die Table-Erweiterungsfunktionen von LuaJIT an. LuaJIT erweitert das Standard-Lua mit zwei nützlichen Table-Funktionen zum Erstellen und Leeren einer Tabelle, die ich im Folgenden beschreiben werde.
table.new(narray, nhash)
Erstellt eine neue Tabelle
Die erste Funktion ist table.new(narray, nhash)
. Anstatt sich selbst zu vergrößern, wenn Elemente eingefügt werden, wird diese Funktion den Speicherplatz der angegebenen Array- und Hash-Größe vorab zuweisen, was ihre beiden Parameter narray
und nhash
bedeuten. Selbstvergrößerung ist eine kostspielige Operation, die Speicherzuweisung, resize
und rehash
beinhaltet und nach Möglichkeit vermieden werden sollte.
Beachten Sie hier, dass die Dokumentation für table.new
nicht auf der LuaJIT-Website zu finden ist, sondern tief in der erweiterten Dokumentation des GitHub-Projekts, sodass es schwer zu finden ist, selbst wenn Sie danach googeln, daher kennen nicht viele Ingenieure diese Funktion.
Hier ist ein einfaches Beispiel, und ich zeige Ihnen, wie es funktioniert. Zunächst ist diese Funktion erweitert, daher müssen Sie sie vor der Verwendung require
.
local new_tab = require "table.new"
local t = new_tab(100, 0)
for i = 1, 100 do
t[i] = i
end
Wie Sie sehen können, erstellt dieser Code eine neue Tabelle mit 100 Array-Elementen und 0 Hash-Elementen. Natürlich können Sie je nach Bedarf eine neue Tabelle mit 100 Array-Elementen und 50 Hash-Elementen erstellen, was legal ist.
local t = new_tab(100, 50)
Alternativ können Sie, wenn Sie die voreingestellte Speicherplatzgröße überschreiten, sie weiterhin normal verwenden, aber die Leistung wird sich verschlechtern, und der Sinn der Verwendung von table.new
geht verloren.
Im folgenden Beispiel haben wir eine voreingestellte Größe von 100, verwenden aber 200.
local new_tab = require "table.new"
local t = new_tab(100, 0)
for i = 1, 200 do
t[i] = i
end
Sie müssen die Größe des Array- und Hash-Speicherplatzes in table.new
gemäß dem tatsächlichen Szenario voreinstellen, um eine Balance zwischen Leistung und Speichernutzung zu finden.
table.clear()
Leert die Tabelle
Die zweite Funktion ist die Clear-Funktion table.clear()
. Sie löscht alle Daten in einer Tabelle, gibt jedoch den von den Array- und Hash-Teilen belegten Speicher nicht frei. Daher ist sie nützlich, wenn Lua-Tabellen recycelt werden, um den Overhead durch wiederholtes Erstellen und Zerstören von Tabellen zu vermeiden.
$ 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'
Es gibt jedoch nicht viele Szenarien, in denen diese Funktion verwendet werden kann, und in den meisten Fällen sollten wir diese Aufgabe dem LuaJIT-GC überlassen.
OpenResty's Table-Erweiterungsfunktionen
Wie ich am Anfang erwähnt habe, pflegt OpenResty seinen eigenen LuaJIT-Zweig, der auch Table erweitert, mit mehreren neuen APIs: table.isempty,
table. isarray
, table.nkeys
und table.clone
.
Bevor Sie diese neuen APIs verwenden, überprüfen Sie bitte die Version von OpenResty, da die meisten dieser APIs nur in Versionen von OpenResty nach 1.15.8.1 verwendet werden können. Dies liegt daran, dass OpenResty etwa ein Jahr vor Version 1.15.8.1 keine neue Version hatte und diese APIs in diesem Release-Intervall hinzugefügt wurden.
Ich habe einen Link zum Artikel hinzugefügt, daher verwende ich table.nkeys
als Beispiel. Die anderen drei APIs sind aus Namenssicht leicht zu verstehen, daher schauen Sie sich die GitHub-Dokumentation an, und Sie werden sie verstehen. Ich muss sagen, dass die Dokumentation von OpenResty von sehr hoher Qualität ist, einschließlich Codebeispielen, ob sie JIT-fähig sind, worauf zu achten ist usw. Sie ist um mehrere Größenordnungen besser als die Dokumentation von Lua und LuaJIT.
Okay, zurück zur table.nkeys
-Funktion. Ihr Name mag Sie verwirren, aber es ist eine Funktion, die die Länge der Tabelle ermittelt und die Anzahl der Elemente der Tabelle zurückgibt, einschließlich der Elemente des Arrays und des Hash-Teils. Daher können wir sie anstelle von table.getn
verwenden, zum Beispiel wie folgt.
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
Nachdem wir über die Table-Funktion gesprochen haben, schauen wir uns die aus table
abgeleitete metatable
an. Die Metatable ist ein einzigartiges Konzept in Lua und wird in realen Projekten weit verbreitet verwendet. Es ist keine Übertreibung zu sagen, dass Sie sie in fast jeder lua-resty-*
-Bibliothek finden können.
Metatable
verhält sich wie Operatorüberladungen; zum Beispiel können wir __add
überladen, um die Verkettung von zwei Lua-Arrays zu berechnen, oder __tostring
, um Funktionen zu definieren, die in Zeichenketten umgewandelt werden.
Lua bietet andererseits zwei Funktionen zur Handhabung von Metatabellen.
- Die erste ist
setmetatable(table, metatable)
, die eine Metatable für eine Tabelle einrichtet. - Die zweite ist
getmetatable(table)
, die die Metatable der Tabelle abruft.
Nach all dem sind Sie vielleicht mehr daran interessiert, was sie tut, also schauen wir uns an, wofür Metatable speziell verwendet wird. Hier ist ein Codeausschnitt aus einem realen Projekt.
$ 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))
'
Wir definieren zuerst eine Tabelle namens version
, und wie Sie sehen können, besteht der Zweck dieses Codes darin, die Versionsnummer in version
auszugeben. Wir können jedoch nicht direkt die version
ausgeben. Sie können versuchen, dies zu tun, und Sie werden sehen, dass die direkte Ausgabe nur die Adresse der Tabelle ausgibt.
print(tostring(version))
Daher müssen wir die Zeichenkettenkonvertierungsfunktion für diese Tabelle anpassen, was __tostring
ist, und hier kommt die Metatable ins Spiel. Wir verwenden setmetatable
, um die __tostring
-Methode der Tabelle version
zurückzusetzen, um die Versionsnummer auszugeben: 1.1.1.
Zusätzlich zu __tostring
überschreiben wir in realen Projekten oft die folgenden zwei Metamethoden in der Metatable.
Eine davon ist __index. Wenn wir ein Element in einer Tabelle nachschlagen, suchen wir es zuerst direkt in der Tabelle, und wenn wir es nicht finden, gehen wir weiter zum __index
der Metatable.
Wir entfernen das patch
aus der version
-Tabelle im folgenden Beispiel.
$ 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))
'
In diesem Fall erhält t.patch
keinen Wert, daher geht es zur __index
-Funktion, die 1.1.2 ausgibt.
__index
kann nicht nur eine Funktion, sondern auch eine Tabelle sein, und wenn Sie den folgenden Code ausführen, werden Sie sehen, dass sie dasselbe Ergebnis erzielen.
$ 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))
'
Eine weitere Metamethode ist __call. Sie ähnelt einem Funktor, der es einer Tabelle ermöglicht, aufgerufen zu werden.
Lassen Sie uns auf dem obigen Code aufbauen, der die Versionsnummer ausgibt, und sehen, wie man eine Tabelle aufruft.
$ 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()
'
In diesem Code verwenden wir setmetatable
, um der Tabelle version
eine Metatable hinzuzufügen, und die __call
-Metamethode darin verweist auf die Funktion print_version
. Wenn wir also versuchen, version
als Funktion aufzurufen, wird hier die Funktion print_version
ausgeführt.
Und getmetatable
ist die Operation, die mit setmetatable
gepaart ist, um die bereits gesetzte Metatable zu erhalten, wie im folgenden Code.
$ 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)
'
Zusätzlich zu diesen drei Metamethoden, über die wir heute gesprochen haben, gibt es einige weniger häufig verwendete Metamethoden, die Sie in der Dokumentation nachschlagen können, um mehr darüber zu erfahren, wenn Sie auf sie stoßen.
Objektorientierung
Schließlich sprechen wir über Objektorientierung. Wie Sie vielleicht wissen, ist Lua keine objektorientierte Sprache, aber wir können Metatabellen verwenden, um OO zu implementieren.
Schauen wir uns ein praktisches Beispiel an. lua-resty-mysql ist der offizielle MySQL-Client von OpenResty und verwendet Metatabellen zur Simulation von Klassen und Klassenmethoden, die wie folgt verwendet werden.
$ resty -e 'local mysql = require "resty.mysql" -- zuerst die lua-resty-Bibliothek referenzieren
local db, err = mysql:new() -- Eine neue Instanz der Klasse erstellen
db:set_timeout(1000) -- Methoden einer Klasse aufrufen
Sie können den obigen Code direkt mit der resty
-Befehlszeile ausführen. Diese Codezeilen sind leicht zu verstehen; das einzige, was Sie möglicherweise verwirren könnte, ist.
Warum ist es bei einem Klassenmethodenaufruf ein Doppelpunkt statt eines Punkts?
Tatsächlich sind sowohl Doppelpunkte als auch Punkte hier in Ordnung, und db:set_timeout(1000)
und db.set_timeout(db, 1000)
sind genau gleichwertig. Der Doppelpunkt ist ein syntaktischer Zucker in Lua, der es ermöglicht, das erste Argument self
einer Funktion auszulassen.
Wie wir alle wissen, gibt es vor dem Quellcode keine Geheimnisse, also schauen wir uns die konkrete Implementierung an, die den obigen Codezeilen entspricht, damit Sie besser verstehen können, wie man objektorientiert mit Metatabellen modelliert.
local _M = { _VERSION = '0.21' } -- Verwenden der Tabelle zur Simulation einer Klasse
local mt = { __index = _M } -- mt ist kurz für metatable, __index verweist auf die Klasse selbst
-- Konstruktor der Klasse
function _M.new(self)
local sock, err = tcp()
if not sock then
return nil, err
end
return setmetatable({ sock = sock }, mt) -- Beispiel für die Simulation von Klassen mit Tabelle und Metatable
end
-- Memberfunktionen einer Klasse
function _M.set_timeout(self, timeout) -- Verwenden des self-Arguments, um eine Instanz der Klasse zu erhalten, die Sie bearbeiten möchten
local sock = self.sock
if not sock then
return nil, "not initialized"
end
return sock:settimeout(timeout)
end
Die Tabelle _M
simuliert eine Klasse, die mit einer einzelnen Membervariablen _VERSION
initialisiert wird, und definiert anschließend Memberfunktionen wie _M.set_timeout
. Im Konstruktor _M.new(self)
geben wir eine Tabelle zurück, deren Metatable mt
ist, und die __index
-Metamethode von mt
verweist auf _M
, sodass die zurückgegebene Tabelle eine Instanz der Klasse _M
simuliert.
Zusammenfassung
Nun, das schließt den Hauptinhalt für heute ab. Table und Metatable werden in OpenResty's lua-resty-*
-Bibliothek und OpenResty-basierten Open-Source-Projekten stark verwendet. Ich hoffe, diese Lektion erleichtert es Ihnen, den Quellcode zu lesen und zu verstehen.
Es gibt neben der Table noch andere Standardfunktionen in Lua, die wir in der nächsten Lektion gemeinsam lernen werden.
Abschließend möchte ich Ihnen eine zum Nachdenken anregende Frage stellen. Warum simuliert die lua-resty-mysql
-Bibliothek OO als eine Schicht der Verpackung? Willkommen, diese Frage in den Kommentaren zu diskutieren, und willkommen, diesen Artikel mit Ihren Kollegen und Freunden zu teilen, damit wir uns austauschen und gemeinsam vorankommen können.