Was ist eine Tabelle und Metatabelle in Lua?

API7.ai

October 11, 2022

OpenResty (NGINX + Lua)

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.