Par défaut, une table Lua est créée sans métatable. L'appel à getmetatable(t) sur une table vierge renverra donc nil. N'importe quelle table peut servir de métatable à d'autres tables. Plusieurs structures peuvent partager une même métatable, et une table peut même agir comme sa propre métatable.
Métaméthodes Arithmétiques
Les métaméthodes arithmétiques permettent de surcharger les opérateurs mathématiques standards pour des structures personnalisées. Les identifiants disponibles sont __add (+), __sub (-), __mul (*), __div (/), __unm (négation), __pow (^), et __concat (..).
Pour illustrer ce mécanisme, voici une implémentation gérant l'addition de vecteurs en deux dimensions. Il est crucial de valider les types des opérandes pour éviter des erreurs d'exécution inattendues lors des opérations.
local Vector2D = {}
Vector2D.mt = {}
function Vector2D.create(x, y)
local vec = {x = x or 0, y = y or 0}
setmetatable(vec, Vector2D.mt)
return vec
end
function Vector2D.add(v1, v2)
if getmetatable(v1) ~= Vector2D.mt or getmetatable(v2) ~= Vector2D.mt then
error("Type invalide : opérande Vector2D attendu", 2)
end
return Vector2D.create(v1.x + v2.x, v1.y + v2.y)
end
Vector2D.mt.__add = Vector2D.add
local vecA = Vector2D.create(10, 15)
local vecB = Vector2D.create(5, 25)
local vecC = vecA + vecB -- Déclenche Vector2D.add
Métaméthodes Relationnelles
La redéfinition des comparaisons s'effectue via __eq (==), __lt (<) et __le (<=). L'existence distincte de __le s'explique par un problème historique. Avant Lua 4.0, l'opérateur <= était évalué comme not (b < a). Cette logique échoue face à la valeur NaN (Not a Number). Puisque NaN < x est systématiquement faux, not (x < NaN) devient vrai, ce qui est une aberration mathématique. L'introduction de __le a permis de résoudre cette ambiguïté.
L'exemple suivant montre comment comparer précisément des fractions rationnelles.
local Fraction = {}
Fraction.mt = {}
function Fraction.new(num, den)
return setmetatable({n = num, d = den}, Fraction.mt)
end
Fraction.mt.__le = function(a, b)
return (a.n * b.d) <= (b.n * a.d)
end
Fraction.mt.__lt = function(a, b)
return (a.n * b.d) < (b.n * a.d)
end
Fraction.mt.__eq = function(a, b)
return (a.n * b.d) == (b.n * a.d)
end
local f1 = Fraction.new(1, 2)
local f2 = Fraction.new(2, 4)
-- L'expression (f1 == f2) évaluera à true
Lors d'une comparaison entre deux variables possédant des métaméthodes différentes, l'opérateur == renvoie toujours false, tandis que les opérateurs d'inégalité lèveront une erreur.
Contrôle des Représentations et Protection
La métaméthode __tostring permet de formater l'afficahge d'un objet lors de l'appel à print. Parallèlement, __metatable sert de verrou de sécurité. Une fois défini, getmetatable renvoie la valeur de __metatable au lieu de la métatable elle-même, et toute tetnative de modification via setmetatable provoquera une erreur fatale.
Fraction.mt.__tostring = function(f)
return string.format("%d/%d", f.n, f.d)
end
Fraction.mt.__metatable = "Accès interdit"
local frac = Fraction.new(3, 8)
print(frac) -- Affiche: 3/8
print(getmetatable(frac)) -- Affiche: Accès interdit
setmetatable(frac, {}) -- Lève une erreur : cannot change protected metatable
Interception des Accès aux Tables
Héritage via __index
Lorsqu'une clé inexistante est requêtée, Lua consulte la métaméthode __index. Si celle-ci contient une table ou une fonction valide, le moteur y délègue la recherche. Assigner une table directement à __index est l'approche la plus courante pour simuler l'héritage de prototype.
local DefaultSettings = {resolution = "1080p", volume = 75, fullscreen = true}
local SettingsMT = { __index = DefaultSettings }
local userSettings = {volume = 100}
setmetatable(userSettings, SettingsMT)
print(userSettings.resolution) -- Affiche: 1080p
print(rawget(userSettings, "resolution")) -- Affiche: nil (contourne __index)
La fonction native rawget permet de forcer une lecture directe sans déclencher la métaméthode, mais elle n'offre aucun avantage en termes de performances.
Surveillance des Mutations avec __newindex
La métaméthode __newindex intercepte l'assignation de valeurs à des clés absentes. Pour que l'assignation ait réellement lieu sans créer de boucle infinie, il faut utiliser rawset.
local SecureStore = {}
local StoreMT = {}
function StoreMT.__newindex(tbl, key, value)
print(string.format("Journal : attribution de '%s' à la clé '%s'", tostring(value), key))
rawset(tbl, key, value)
end
setmetatable(SecureStore, StoreMT)
SecureStore.status = "actif" -- Déclenche le journal et assigne la valeur
Tables avec Valeurs par Défaut
Il est possible d'altérer le comportement standard renvoyant nil pour les clés inexistantes. En utilisant une clé privée comme référence, plusieurs tables peuvent partager une même métatable tout en conservant des valeurs par défaut distinctes sans risque de collsiion.
local private_key = {}
local DefaultMT = {
__index = function(t) return t[private_key] end
}
function setDefaultValue(t, default)
t[private_key] = default
setmetatable(t, DefaultMT)
end
local stats = {score = 10}
setDefaultValue(stats, 0)
print(stats.lives) -- Affiche 0 au lieu de nil
Suivi des Opérations (Proxy)
Puisque __index et __newindex ne s'activent que sur des clés manquantes, le suivi exhaustif d'une table existante exige l'utilisation d'un proxy vide. La table d'origine est encapsulée via une référence privée, forçant chaque interaction à passer par les métaméthodes du proxy.
local function createTracker(originalTable)
local hidden_ref = {}
local trackerMT = {
__index = function(t, k)
print("Lecture détectée : " .. tostring(k))
return t[hidden_ref][k]
end,
__newindex = function(t, k, v)
print("Écriture détectée : " .. tostring(k))
t[hidden_ref][k] = v
end
}
local proxy = {}
proxy[hidden_ref] = originalTable
return setmetatable(proxy, trackerMT)
end
local trackedData = createTracker({})
trackedData.health = 100
print(trackedData.health)
Structures Immuables (Lecture Seule)
La création d'une table en lecture seule repose sur le même principe de proxy. La métaméthode __newindex est configurée pour rejeter systématiquement toute écriture, tandis que __index pointe vers la table de données légitime.
local function makeReadOnly(target)
local proxy = {}
local roMT = {
__index = target,
__newindex = function()
error("Tentative de modification d'une structure immuable", 2)
end
}
return setmetatable(proxy, roMT)
end
local appConstants = makeReadOnly({MAX_USERS = 50, TIMEOUT = 30})
print(appConstants.MAX_USERS) -- Affiche 50
appConstants.TIMEOUT = 60 -- Lève une erreur d'exécution