00
:
00
:
00
:
00
•Corso SEO AI - Usa SEOEMAIL al checkout per il 30% di sconto

Metatabelle e Metamethods in Lua

Le metatabelle sono uno dei meccanismi più potenti di Lua. Permettono di ridefinire il comportamento delle tabelle quando vengono utilizzate con operatori aritmetici, confronti, concatenazioni e accessi a chiavi inesistenti. Comprendere le metatabelle è fondamentale per sfruttare appieno le capacità del linguaggio.

Cosa Sono le Metatabelle

Una metatabella è una normale tabella Lua che viene associata a un’altra tabella per modificarne il comportamento. Quando Lua esegue certe operazioni su una tabella, controlla se questa ha una metatabella con specifiche chiavi (chiamate metamethods). Se il metamethod esiste, viene invocato al posto del comportamento predefinito.

local miaTabella = {}
local metatabella = {}

setmetatable(miaTabella, metatabella)

-- Recuperare la metatabella associata
local mt = getmetatable(miaTabella)
print(mt == metatabella) -- true

setmetatable e getmetatable

La funzione setmetatable(tabella, metatabella) associa una metatabella a una tabella e restituisce la tabella stessa. Questo permette di concatenare le operazioni. La funzione getmetatable(tabella) restituisce la metatabella associata, oppure nil se non ne ha una.

-- Concatenazione nella creazione
local t = setmetatable({}, {
    __tostring = function(self)
        return "Sono una tabella personalizzata"
    end
})

print(t) -- Sono una tabella personalizzata

Metamethods Aritmetici

I metamethods aritmetici permettono di definire il comportamento degli operatori matematici sulle tabelle.

local mt = {}

mt.__add = function(a, b)    -- operatore +
    return { valore = a.valore + b.valore }
end

mt.__sub = function(a, b)    -- operatore -
    return { valore = a.valore - b.valore }
end

mt.__mul = function(a, b)    -- operatore *
    return { valore = a.valore * b.valore }
end

mt.__div = function(a, b)    -- operatore /
    return { valore = a.valore / b.valore }
end

mt.__mod = function(a, b)    -- operatore %
    return { valore = a.valore % b.valore }
end

mt.__pow = function(a, b)    -- operatore ^
    return { valore = a.valore ^ b.valore }
end

mt.__unm = function(a)       -- operatore unario -
    return { valore = -a.valore }
end

local x = setmetatable({ valore = 10 }, mt)
local y = setmetatable({ valore = 3 }, mt)

local somma = x + y
print(somma.valore)   -- 13

local negato = -x
print(negato.valore)  -- -10

Metamethods di Confronto

I metamethods __eq, __lt e __le definiscono il comportamento degli operatori di confronto. Lua richiede che entrambi gli operandi condividano la stessa metatabella affinche il metamethod venga invocato.

local mt = {}

mt.__eq = function(a, b)    -- operatore ==
    return a.valore == b.valore
end

mt.__lt = function(a, b)    -- operatore <
    return a.valore < b.valore
end

mt.__le = function(a, b)    -- operatore <=
    return a.valore <= b.valore
end

local a = setmetatable({ valore = 5 }, mt)
local b = setmetatable({ valore = 10 }, mt)

print(a == b)   -- false
print(a < b)    -- true
print(a <= b)   -- true

__tostring

Il metamethod __tostring viene invocato dalla funzione tostring() e dalla funzione print(). Permette di definire una rappresentazione testuale personalizzata della tabella.

local Punto = {}
Punto.__index = Punto

function Punto.new(x, y)
    return setmetatable({ x = x, y = y }, Punto)
end

Punto.__tostring = function(self)
    return string.format("Punto(%g, %g)", self.x, self.y)
end

local p = Punto.new(3, 7)
print(p)             -- Punto(3, 7)
print(tostring(p))   -- Punto(3, 7)

__index

Il metamethod __index viene invocato quando si accede a una chiave che non esiste nella tabella. Puo’ essere una funzione oppure un’altra tabella. Questo metamethod e’ la base della programmazione ad oggetti in Lua.

-- __index come tabella (fallback)
local predefiniti = { colore = "rosso", dimensione = 10 }
local config = setmetatable({}, { __index = predefiniti })

print(config.colore)      -- rosso (trovato in predefiniti)
print(config.dimensione)  -- 10

config.colore = "blu"
print(config.colore)      -- blu (ora esiste nella tabella stessa)

-- __index come funzione
local t = setmetatable({}, {
    __index = function(self, chiave)
        return "Chiave '" .. chiave .. "' non trovata"
    end
})

print(t.nome)   -- Chiave 'nome' non trovata

__newindex

Il metamethod __newindex viene invocato quando si tenta di assegnare un valore a una chiave che non esiste nella tabella. E’ utile per intercettare le scritture e implementare tabelle di sola lettura o validazione.

-- Tabella di sola lettura
local function solaLettura(t)
    return setmetatable({}, {
        __index = t,
        __newindex = function(self, chiave, valore)
            error("Tentativo di modifica di una tabella di sola lettura", 2)
        end
    })
end

local costanti = solaLettura({ PI = 3.14159, E = 2.71828 })
print(costanti.PI)       -- 3.14159
-- costanti.PI = 3.14    -- Errore: Tentativo di modifica...

__call

Il metamethod __call permette di utilizzare una tabella come se fosse una funzione. Viene invocato quando si tenta di “chiamare” la tabella con le parentesi tonde.

local contatore = setmetatable({ valore = 0 }, {
    __call = function(self, incremento)
        self.valore = self.valore + (incremento or 1)
        return self.valore
    end
})

print(contatore())    -- 1
print(contatore())    -- 2
print(contatore(5))   -- 7

__len e __concat

Il metamethod __len ridefinisce l’operatore # (lunghezza), mentre __concat ridefinisce l’operatore .. (concatenazione).

local lista = setmetatable({ "a", "b", "c", filtrati = 1 }, {
    __len = function(self)
        local conteggio = 0
        for _ in pairs(self) do
            conteggio = conteggio + 1
        end
        return conteggio
    end,
    __concat = function(a, b)
        if type(a) == "table" then a = tostring(a) end
        if type(b) == "table" then b = tostring(b) end
        return a .. b
    end
})

print(#lista)             -- 4 (conta tutte le chiavi)

Esempio Pratico: Classe Vector2D

Ecco un esempio completo che utilizza le metatabelle per creare una classe Vector2D con supporto a tutti gli operatori principali.

local Vector2D = {}
Vector2D.__index = Vector2D

function Vector2D.new(x, y)
    return setmetatable({ x = x or 0, y = y or 0 }, Vector2D)
end

function Vector2D:lunghezza()
    return math.sqrt(self.x ^ 2 + self.y ^ 2)
end

function Vector2D:normalizza()
    local len = self:lunghezza()
    if len > 0 then
        return Vector2D.new(self.x / len, self.y / len)
    end
    return Vector2D.new(0, 0)
end

-- Metamethods aritmetici
Vector2D.__add = function(a, b)
    return Vector2D.new(a.x + b.x, a.y + b.y)
end

Vector2D.__sub = function(a, b)
    return Vector2D.new(a.x - b.x, a.y - b.y)
end

Vector2D.__mul = function(a, b)
    if type(a) == "number" then
        return Vector2D.new(a * b.x, a * b.y)
    elseif type(b) == "number" then
        return Vector2D.new(a.x * b, a.y * b)
    end
    return a.x * b.x + a.y * b.y  -- prodotto scalare
end

Vector2D.__unm = function(a)
    return Vector2D.new(-a.x, -a.y)
end

Vector2D.__eq = function(a, b)
    return a.x == b.x and a.y == b.y
end

Vector2D.__tostring = function(v)
    return string.format("Vector2D(%g, %g)", v.x, v.y)
end

-- Utilizzo
local v1 = Vector2D.new(3, 4)
local v2 = Vector2D.new(1, 2)

print(v1)              -- Vector2D(3, 4)
print(v1 + v2)         -- Vector2D(4, 6)
print(v1 - v2)         -- Vector2D(2, 2)
print(v1 * 2)          -- Vector2D(6, 8)
print(-v1)             -- Vector2D(-3, -4)
print(v1 == v2)        -- false
print(v1:lunghezza())  -- 5
print(v1:normalizza()) -- Vector2D(0.6, 0.8)

Conclusione

Le metatabelle sono il cuore della flessibilita di Lua. Attraverso i metamethods puoi personalizzare completamente il comportamento delle tabelle, implementare classi, creare strutture dati avanzate e definire DSL (Domain Specific Languages). Padroneggiare le metatabelle significa padroneggiare Lua nella sua interezza.