Qu'est-ce qu'une table et une metatable en Lua ?
API7.ai
October 11, 2022
Aujourd'hui, nous allons apprendre à propos de la seule structure de données dans LuaJIT : la table
.
Contrairement à d'autres langages de script avec des structures de données riches, LuaJIT n'a qu'une seule structure de données, la table
, qui n'est pas distinguée entre les tableaux, les tables de hachage, les collections, etc., mais qui est plutôt un mélange de tout cela. Revenons sur l'un des exemples mentionnés précédemment.
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
Dans cet exemple, la table color
contient à la fois un tableau et une table de hachage, et on peut y accéder sans qu'ils interfèrent entre eux. Par exemple, vous pouvez utiliser la fonction ipairs
pour parcourir uniquement la partie tableau de la table.
$ resty -e 'local color = {first = "red", "blue", third = "green", "yellow"}
for k, v in ipairs(color) do
print(k)
end
'
Les opérations sur les table
sont si cruciales que LuaJIT étend la bibliothèque standard table
de Lua 5.1, et OpenResty étend encore davantage la bibliothèque table
de LuaJIT. Examinons chacune de ces fonctions de bibliothèque.
Les fonctions de la bibliothèque table
Commençons par les fonctions standard de la bibliothèque table
. Lua 5.1 n'a pas beaucoup de fonctions dans la bibliothèque table
, donc nous pouvons les parcourir rapidement.
table.getn
Obtenir le nombre d'éléments
Comme nous l'avons mentionné dans le chapitre Standard Lua et LuaJIT, obtenir le nombre correct de tous les éléments d'une table est un gros problème dans LuaJIT.
Pour les séquences, vous pouvez utiliser table.getn
ou l'opérateur unaire #
pour retourner le nombre correct d'éléments. L'exemple suivant retourne le nombre 3, comme nous nous y attendions.
$ resty -e 'local t = { 1, 2, 3 }
print(table.getn(t))
La valeur correcte ne peut pas être retournée pour les tables qui ne sont pas séquentielles. Dans le deuxième exemple, la valeur retournée est 1.
$ resty -e 'local t = { 1, a = 2 }
print(#t) '
Heureusement, ces fonctions difficiles à comprendre ont été remplacées par des extensions de LuaJIT, que nous mentionnerons plus tard. Donc, dans le contexte d'OpenResty, n'utilisez pas la fonction table.getn
et l'opérateur unaire #
à moins que vous sachiez explicitement que vous obtenez la longueur d'une séquence.
De plus, table.getn
et l'opérateur unaire #
ne sont pas de complexité temporelle O(1) mais O(n), ce qui est une autre raison de les éviter si possible.
table.remove
Supprime l'élément spécifié
La deuxième fonction est table.remove
, qui supprime des éléments dans la table en fonction des indices, c'est-à-dire que seuls les éléments de la partie tableau de la table peuvent être supprimés. Reprenons l'exemple de color
.
$ resty -e 'local color = {first = "red", "blue", third = "green", "yellow"}
table.remove(color, 1)
for k, v in pairs(color) do
print(v)
end'
Ce code supprimera le blue
avec l'indice 1. Vous pourriez demander, comment supprimer la partie table de hachage de la table ? C'est aussi simple que de définir la valeur correspondant à la clé à nil
. Ainsi, dans l'exemple color
, le green
correspondant à third
est supprimé.
$ 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
Fonction de concaténation d'éléments
La troisième fonction est table.concat
, qui concatène les éléments de la table en fonction des indices. Comme cela est à nouveau basé sur les indices, cela concerne toujours la partie tableau de la table. Reprenons l'exemple de color
.
$ resty -e 'local color = {first = "red", "blue", third = "green", "yellow"}
print(table.concat(color, ", "))'
Après avoir utilisé la fonction table.concat
, cela affiche blue, yellow
et la partie table de hachage est ignorée.
De plus, cette fonction peut également spécifier la position de départ de l'indice pour effectuer la concaténation ; par exemple, elle est écrite comme suit :
$ resty -e 'local color = {first = "red", "blue", third = "green", "yellow", "orange"}
print(table.concat(color, ", ", 2, 3))'
Cette fois, la sortie est yellow, orange
, en sautant blue
.
Ne sous-estimez pas cette fonction qui semble inutile, car elle peut avoir des effets inattendus lors de l'optimisation des performances et est l'un des principaux acteurs dans nos chapitres ultérieurs sur l'optimisation des performances.
table.insert
Insère un élément
Enfin, examinons la fonction table.insert
. Elle insère un nouvel élément à l'indice spécifié, ce qui affecte la partie tableau de la table. Pour illustrer, reprenons l'exemple de color
.
$ resty -e 'local color = {first = "red", "blue", third = "green", "yellow"}
table.insert(color, 1, "orange")
print(color[1])
'
Vous pouvez voir que le premier élément de color
devient orange
, mais bien sûr, vous pouvez laisser l'indice non spécifié, de sorte qu'il sera inséré à la fin de la file par défaut.
Je dois noter que table.insert
est une opération omniprésente, mais les performances ne sont pas bonnes. Si vous n'insérez pas d'éléments en fonction de l'indice spécifié, vous devrez appeler lj_tab_len
de LuaJIT à chaque fois pour obtenir la longueur du tableau afin d'insérer à la fin de la file. Comme table.getn
, la complexité temporelle pour obtenir la longueur de la table est O(n).
Donc, pour l'opération table.insert
, nous devrions essayer de l'éviter dans le code critique. Par exemple :
local t = {}
for i = 1, 10000 do
table.insert(t, i)
end
Les fonctions d'extension de la table de LuaJIT
Ensuite, examinons les fonctions d'extension de la table de LuaJIT. LuaJIT étend le Lua standard avec deux fonctions de table utiles pour créer et vider une table, que je vais décrire ci-dessous.
table.new(narray, nhash)
Crée une nouvelle table
La première fonction est table.new(narray, nhash)
. Au lieu de s'agrandir elle-même lors de l'insertion d'éléments, cette fonction pré-alloue la taille de l'espace pour le tableau et la table de hachage spécifiés, ce que signifient ses deux paramètres narray
et nhash
. L'auto-agrandissement est une opération coûteuse qui implique l'allocation d'espace, le resize
et le rehash
, et devrait être évitée à tout prix.
Notez ici que la documentation de table.new
ne se trouve pas sur le site web de LuaJIT, mais est profondément enfouie dans la documentation étendue du projet GitHub, donc il est difficile de la trouver même en la cherchant sur Google, donc peu d'ingénieurs la connaissent.
Voici un exemple simple, et je vais vous montrer comment cela fonctionne. Tout d'abord, cette fonction est étendue, donc avant de pouvoir l'utiliser, vous devez la require
.
local new_tab = require "table.new"
local t = new_tab(100, 0)
for i = 1, 100 do
t[i] = i
end
Comme vous pouvez le voir, ce code crée une nouvelle table avec 100 éléments de tableau et 0 éléments de table de hachage. Bien sûr, vous pouvez créer une nouvelle table avec 100 éléments de tableau et 50 éléments de table de hachage selon vos besoins, ce qui est légal.
local t = new_tab(100, 50)
Alternativement, si vous dépassez la taille de l'espace prédéfini, vous pouvez toujours l'utiliser normalement, mais les performances se dégraderont, et l'intérêt d'utiliser table.new
sera perdu.
Dans l'exemple suivant, nous avons une taille prédéfinie de 100, mais nous utilisons 200.
local new_tab = require "table.new"
local t = new_tab(100, 0)
for i = 1, 200 do
t[i] = i
end
Vous devez prédéfinir la taille de l'espace du tableau et de la table de hachage dans table.new
en fonction du scénario réel afin de trouver un équilibre entre les performances et l'utilisation de la mémoire.
table.clear()
Vide la table
La deuxième fonction est la fonction de vidage table.clear()
. Elle vide toutes les données d'une table mais ne libère pas la mémoire occupée par les parties tableau et table de hachage. Par conséquent, elle est utile lors du recyclage des tables Lua pour éviter les frais généraux de création et de destruction répétées des tables.
$ 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'
Cependant, il n'y a pas beaucoup de scénarios où cette fonction peut être utilisée, et dans la plupart des cas, nous devrions laisser cette tâche au GC de LuaJIT.
Les fonctions d'extension de la table d'OpenResty
Comme je l'ai mentionné au début, OpenResty maintient sa propre branche de LuaJIT, qui étend également la table, avec plusieurs nouvelles API : table.isempty
, table.isarray
, table.nkeys
et table.clone
.
Avant d'utiliser ces nouvelles API, veuillez vérifier la version d'OpenResty, car la plupart de ces API ne peuvent être utilisées que dans les versions d'OpenResty après 1.15.8.1. En effet, OpenResty n'a pas eu de nouvelle version pendant environ un an avant la version 1.15.8.1, et ces API ont été ajoutées pendant cet intervalle de version.
J'ai inclus un lien vers l'article, donc je vais utiliser table.nkeys
comme exemple. Les trois autres API sont faciles à comprendre à partir de leur nom, donc parcourez la documentation GitHub, et vous comprendrez. Je dois dire que la documentation d'OpenResty est de très haute qualité, y compris les exemples de code, qu'elle peut être JIT, ce qu'il faut rechercher, etc. Plusieurs ordres de grandeur meilleurs que la documentation de Lua et LuaJIT.
D'accord, revenons à la fonction table.nkeys
. Son nom peut vous embrouiller, mais c'est une fonction qui obtient la longueur de la table et retourne le nombre d'éléments de la table, y compris les éléments du tableau et de la partie table de hachage. Par conséquent, nous pouvons l'utiliser à la place de table.getn
, par exemple, comme suit.
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
Après avoir parlé de la fonction table
, examinons la metatable
dérivée de table
. La metatable est un concept unique en Lua, et est largement utilisée dans les projets réels. Il n'est pas exagéré de dire que vous pouvez la trouver dans presque toutes les bibliothèques lua-resty-*
.
Metatable
se comporte comme des surcharges d'opérateurs ; par exemple, nous pouvons surcharger __add
pour calculer la concaténation de deux tableaux Lua ou __tostring
pour définir des fonctions de conversion en chaînes de caractères.
Lua, d'autre part, fournit deux fonctions pour gérer les metatables.
- La première est
setmetatable(table, metatable)
, qui configure une metatable pour une table. - La seconde est
getmetatable(table)
, qui obtient la metatable de la table.
Après tout cela, vous pourriez être plus intéressé par ce qu'elle fait, alors examinons ce que la metatable est spécifiquement utilisée pour. Voici un morceau de code d'un projet réel.
$ 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))
'
Nous définissons d'abord une table nommée version
, et comme vous pouvez le voir, le but de ce code est d'imprimer le numéro de version dans version
. Cependant, nous ne pouvons pas imprimer directement version
. Vous pouvez essayer de le faire et voir que l'impression directe ne fera que sortir l'adresse de la table.
print(tostring(version))
Donc, nous devons personnaliser la fonction de conversion en chaîne de caractères pour cette table, qui est __tostring
, et c'est là que la metatable entre en jeu. Nous utilisons setmetatable
pour redéfinir la méthode __tostring
de la table version
pour imprimer le numéro de version : 1.1.1.
En plus de __tostring
, nous surchargeons souvent les deux métaméthodes suivantes dans la metatable dans les projets réels.
L'une d'elles est __index. Lorsque nous recherchons un élément dans une table, nous le recherchons d'abord directement dans la table, et si nous ne le trouvons pas, nous passons à __index
de la metatable.
Nous supprimons patch
de la table version
dans l'exemple suivant.
$ 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))
'
Dans ce cas, t.patch
n'obtient pas la valeur, donc il passe à la fonction __index
, qui imprime 1.1.2.
__index
peut être non seulement une fonction mais aussi une table, et si vous essayez d'exécuter le code suivant, vous verrez qu'ils atteignent le même résultat.
$ 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))
'
Une autre métaméthode est __call. Elle est similaire à un foncteur qui permet d'appeler une table.
Reprenons le code ci-dessus qui imprime le numéro de version et voyons comment appeler une table.
$ 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()
'
Dans ce code, nous utilisons setmetatable
pour ajouter une metatable à la table version
, et la métaméthode __call
à l'intérieur pointe vers la fonction print_version
. Donc, si nous essayons d'appeler version
comme une fonction, la fonction print_version
sera exécutée ici.
Et getmetatable
est l'opération jumelée avec setmetatable
pour obtenir la metatable qui a été configurée, comme dans le code suivant.
$ 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)
'
En plus de ces trois métaméthodes dont nous avons parlé aujourd'hui, il y a quelques métaméthodes moins fréquemment utilisées que vous pouvez consulter dans la documentation pour en savoir plus lorsque vous les rencontrez.
Programmation orientée objet
Enfin, parlons de la programmation orientée objet. Comme vous le savez peut-être, Lua n'est pas un langage orienté objet, mais nous pouvons utiliser la metatable pour implémenter la POO.
Examinons un exemple pratique. lua-resty-mysql est le client MySQL officiel d'OpenResty, et il utilise les metatables pour simuler les classes et les méthodes de classe, qui sont utilisées de la manière suivante.
$ resty -e 'local mysql = require "resty.mysql" -- première référence à la bibliothèque lua-resty
local db, err = mysql:new() -- Crée une nouvelle instance de la classe
db:set_timeout(1000) -- Appelle les méthodes de la classe
Vous pouvez exécuter le code ci-dessus directement avec la commande resty
. Ces lignes de code sont faciles à comprendre ; la seule chose qui pourrait vous poser problème est.
Lors de l'appel d'une méthode de classe, pourquoi utilise-t-on un deux-points au lieu d'un point ?
En fait, les deux-points et les points sont tous deux acceptables ici, et db:set_timeout(1000)
et db.set_timeout(db, 1000)
sont exactement équivalents. Le deux-points est un sucre syntaxique en Lua qui permet d'omettre le premier argument self
d'une fonction.
Comme nous le savons tous, il n'y a pas de secrets devant le code source, alors examinons l'implémentation concrète correspondant aux lignes de code ci-dessus afin que vous puissiez mieux comprendre comment modéliser la programmation orientée objet avec les metatables.
local _M = { _VERSION = '0.21' } -- Utilisation de la table pour simuler une classe
local mt = { __index = _M } -- mt est l'abréviation de metatable, __index fait référence à la classe elle-même
-- Constructeur de la classe
function _M.new(self)
local sock, err = tcp()
if not sock then
return nil, err
end
return setmetatable({ sock = sock }, mt) -- exemple de simulation de classes avec table et metatable
end
-- Fonctions membres d'une classe
function _M.set_timeout(self, timeout) -- Utilise l'argument self pour obtenir une instance de la classe sur laquelle vous souhaitez opérer
local sock = self.sock
if not sock then
return nil, "not initialized"
end
return sock:settimeout(timeout)
end
La table _M
simule une classe initialisée avec une seule variable membre _VERSION
et définit ensuite des fonctions membres comme _M.set_timeout
. Dans le constructeur _M.new(self)
, nous retournons une table dont la metatable est mt
, et la métaméthode __index
de mt
pointe vers _M
afin que la table retournée simule une instance de la classe _M
.
Résumé
Eh bien, cela conclut le contenu principal d'aujourd'hui. La table et la metatable sont largement utilisées dans les bibliothèques lua-resty-*
d'OpenResty et dans les projets open source basés sur OpenResty. J'espère que cette leçon vous permettra de lire et de comprendre plus facilement le code source.
Il y a d'autres fonctions standard dans Lua en plus de la table, que nous apprendrons ensemble dans la prochaine leçon.
Enfin, je voudrais vous laisser avec une question stimulante. Pourquoi la bibliothèque lua-resty-mysql
simule-t-elle la POO avec une couche d'emballage ? N'hésitez pas à discuter de cette question dans la section des commentaires, et n'hésitez pas à partager cet article avec vos collègues et amis afin que nous puissions communiquer et progresser ensemble.