games / lua
Métaprogrammation avancée
Explication
Aller plus loin que __index et __newindex de base
La leçon précédente a introduit les métatables pour construire la POO. Celle-ci explore des usages plus avancés du même mécanisme : intercepter et personnaliser des comportements que Lua exécuterait normalement de façon transparente.
Ce que vous allez apprendre
- Utiliser
rawget/rawsetpour éviter une récursion infinie dans une métaméthode personnalisée - Rendre une table appelable comme une fonction avec
__call - Construire une proxy table qui valide ou journalise chaque accès en lecture/écriture
- Combiner tables faibles et métatables pour un registre d'objets sans fuite mémoire
- Protéger une métatable contre toute modification externe avec
__metatable
Dans quel contexte ?
Un système de configuration serveur doit interdire toute modification directe de ses valeurs critiques (le nombre maximum de joueurs, le mode de difficulté) depuis n'importe quel autre script, tout en gardant une lecture normale et transparente. Construire une "table protégée" avec __newindex qui lève une erreur à toute tentative d'écriture est un exemple concret et fréquent de métaprogrammation avancée en environnement de production.
| Fonction | Rôle | Pourquoi elle est indispensable ici |
|---|---|---|
rawget(t, k) | Lecture directe, sans passer par __index | Évite la boucle infinie dans une métaméthode __index |
rawset(t, k, v) | Écriture directe, sans passer par __newindex | Évite la boucle infinie dans une métaméthode __newindex |
getmetatable(t) | Inspecte la métatable réelle (sauf si protégée) | Débogage, vérification |
Piège fréquent
À l'intérieur d'une métaméthode __index personnalisée, écrire t[key] au lieu de rawget(t, key) redéclenche __index indéfiniment, provoquant une récursion infinie qui plante le script. Toujours utiliser rawget/rawset pour l'accès interne à l'intérieur de ces métaméthodes.
Le piège de la récursion infinie et sa solution
Quand on définit une métaméthode __index personnalisée (par exemple pour logger chaque accès à un champ), il faut faire attention à ne pas accéder à la table elle-même de la façon habituelle À L'INTÉRIEUR de cette métaméthode — cela redéclencherait __index indéfiniment. Les fonctions rawget/rawset existent précisément pour ce cas : elles accèdent directement à la table SANS passer par les métaméthodes, cassant la boucle infinie potentielle.
Rendre une table "appelable" comme une fonction
La métaméthode __call permet d'écrire MaTable(...) comme si MaTable était une fonction, alors que c'est une table. C'est un pattern couramment utilisé pour créer une syntaxe naturelle d'instanciation d'objet, où appeler la "classe" directement produit une nouvelle "instance".
Les proxy tables, un outil de contrôle transparent
En combinant __index et __newindex personnalisés, on peut créer une table "proxy" qui intercepte chaque lecture et chaque écriture pour y ajouter de la validation ou du logging — sans que le code appelant ait besoin de savoir que cette interception existe. C'est une technique puissante pour imposer des règles (type de données, plages de valeurs) de façon centralisée et invisible pour l'utilisateur de la table.
Protéger sa propre métatable
__metatable permet de verrouiller une métatable contre toute modification externe non désirée — utile quand on distribue une bibliothèque et qu'on ne veut pas que le code appelant puisse altérer le comportement interne de vos objets.
Commandes & code
Métaprogrammation avancée
-- rawget / rawset : accès BRUT à une table, en contournant toute métaméthode __index/__newindex
-- Indispensable pour éviter une récursion infinie à l'intérieur même d'une métaméthode
local logged = setmetatable({}, {
__index = function(t, key)
print("lecture de la clé: " .. tostring(key))
return rawget(t, key) -- lecture brute, PAS t[key] (qui redéclencherait __index -> boucle infinie)
end,
__newindex = function(t, key, value)
print("écriture de la clé: " .. tostring(key) .. " = " .. tostring(value))
rawset(t, key, value) -- écriture brute, PAS t[key] = value (récursion infinie sinon)
end,
})
logged.score = 100 -- "écriture de la clé: score = 100"
print(logged.score) -- "lecture de la clé: score" puis 100-- __call : rend une table directement "appelable" comme une fonction
local Multiplier = setmetatable({ factor = 3 }, {
__call = function(self, value)
return value * self.factor
end,
})
print(Multiplier(10)) -- 30, la table se comporte comme une fonction
-- Cas d'usage réel : classes instanciables via un appel direct sur le "constructeur"
local Class = setmetatable({}, {
__call = function(cls, ...)
local instance = setmetatable({}, cls)
if instance.init then instance:init(...) end
return instance
end,
})-- Proxy table : intercepter TOUS les accès pour valider/logger/limiter, données réelles stockées à part
local function createValidatedTable(schema)
local data = {} -- stockage réel, inaccessible directement de l'extérieur
return setmetatable({}, {
__index = data,
__newindex = function(_, key, value)
local validator = schema[key]
if validator and not validator(value) then
error("valeur invalide pour '" .. key .. "': " .. tostring(value), 2)
end
data[key] = value
end,
})
end
local player = createValidatedTable({
hp = function(v) return type(v) == "number" and v >= 0 and v <= 100 end,
})
player.hp = 50 -- accepté
-- player.hp = -10 -- lèverait une erreur : "valeur invalide pour 'hp': -10"-- Tables faibles combinées à __mode pour un registre d'objets sans fuite mémoire
-- (référence croisée à la leçon "tables avancées", approfondie ici pour un cas expert)
local WeakRegistry = setmetatable({}, { __mode = "k" }) -- clés faibles
local function registerEntity(entity, metadata)
WeakRegistry[entity] = metadata -- si "entity" n'est plus référencé ailleurs, l'entrée disparaît du registre
end-- __tostring combiné à __concat pour une intégration transparente dans des chaînes construites dynamiquement
local Money = {}
Money.__index = Money
function Money.new(cents)
return setmetatable({ cents = cents }, Money)
end
Money.__tostring = function(m)
return string.format("%.2f€", m.cents / 100)
end
Money.__concat = function(a, b)
-- __concat reçoit les deux opérandes dans l'ordre, même si un seul est de type Money
local left = type(a) == "table" and tostring(a) or a
local right = type(b) == "table" and tostring(b) or b
return left .. right
end
local price = Money.new(1999)
print("Prix: " .. price) -- appelle __concat : "Prix: 19.99€"-- getmetatable / debug.getmetatable pour inspecter le comportement d'une table (utile en debug)
local mt = getmetatable(price)
print(mt == Money) -- true
-- Protéger une métatable contre modification externe avec __metatable
local Protected = setmetatable({}, { __metatable = "accès interdit" })
print(getmetatable(Protected)) -- "accès interdit" (la vraie métatable est masquée)
-- setmetatable(Protected, {}) -- lèverait une erreur : "cannot change a protected metatable"Résumé
rawget/rawsetévitent la récursion infinie à l'intérieur des métaméthodes__index/__newindex.__callpermet de rendre une table appelable, pattern utilisé pour instancier des "classes".- Un proxy table (
__index/__newindexcustom) permet la validation ou le logging transparent des accès. __metatableprotège une métatable contre toute modification externe non désirée.
Exercices pratiques
Mission : le serveur qui plante sur un simple log
Objectif : Corriger une métaméthode de logging qui provoque une récursion infinie, puis exposer une table de configuration en lecture seule appelable comme une fonction.
Contexte
Un développeur veut journaliser tous les accès à une table de statistiques joueur, et écrit :
local stats = setmetatable({}, {
__index = function(t, key)
print("lecture de la clé: " .. tostring(key))
return t[key] -- censé lire la vraie valeur
end,
})
stats.kills = 5
print(stats.kills) -- le serveur se fige, puis plante avec "stack overflow"Le serveur RP entier se fige dès que ce module est chargé. Par ailleurs, l'équipe veut aussi une table CONFIG protégée en lecture seule, et un système ClassFactory qu'on pourrait instancier en l'appelant directement comme une fonction.