Retour au cours

games / lua

Métaprogrammation avancée

Leçon 71 exercice

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/rawset pour é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.

FonctionRôlePourquoi 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

lua
-- 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
lua
-- __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,
})
lua
-- 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"
lua
-- 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
lua
-- __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€"
lua
-- 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.
  • __call permet de rendre une table appelable, pattern utilisé pour instancier des "classes".
  • Un proxy table (__index/__newindex custom) permet la validation ou le logging transparent des accès.
  • __metatable protège une métatable contre toute modification externe non désirée.

Exercices pratiques

1 disponible
1

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 :

lua
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.

Résoudre l’exercice →