__index: cuando la clave no existe
El metamétodo que define la lectura fallida: la versión con tabla como delegación, la versión con función como cálculo, la cadena de búsqueda con su límite real y el coste que introduce cada eslabón.
__index es el metamétodo que más se usa y el que peor se entiende, porque casi todo el mundo lo conoce por su aplicación —así se hacen las clases en Lua— y no por su definición. Su definición es mucho más humilde y mucho más general: describe qué debe ocurrir cuando una lectura no encuentra nada. Solo eso. Que de ahí salgan la herencia, los módulos con carga perezosa, las propiedades calculadas, los valores por defecto y los objetos remotos es consecuencia de lo bien elegido que está el punto de intervención, no de que el metamétodo sepa nada de objetos.
- Precisar la condición exacta que dispara
__indexy la que no. - Distinguir la versión con tabla —delegación— de la versión con función —cálculo—.
- Construir y razonar una cadena de búsqueda de varios eslabones.
- Medir el coste de la cadena y saber cuándo aplanarla.
La condición exacta
El disparo de __index está gobernado por una regla de tres partes que conviene memorizar literalmente, porque cada una de las tres explica un error clásico.
El intérprete busca la clave en la parte cruda de la tabla. Si la encuentra con un valor distinto de nil, devuelve ese valor y ahí termina todo: la metatabla no se consulta jamás en el caso feliz. Si el resultado crudo es nil, mira si la tabla tiene metatabla; si no la tiene, devuelve nil. Si la tiene, busca en ella —en crudo— el campo __index; si tampoco está, devuelve nil. Y solo si ese campo existe, actúa según su tipo.
flowchart TD A[Evaluar t indexado por k] --> B[Buscar k en la parte cruda de t] B -->|valor no nil| C[Devolver ese valor] B -->|resultado nil| D[Tiene t metatabla] D -->|no| E[Devolver nil] D -->|si| F[Buscar __index en la metatabla] F -->|ausente| E F -->|es una funcion| G[Llamarla con t y k y devolver su resultado] F -->|es una tabla| H[Reintentar toda la busqueda sobre esa tabla] H --> B style C fill:#a6e3a1,color:#11111b style E fill:#f38ba8,color:#11111b style G fill:#89b4fa,color:#11111b
De aquí salen tres consecuencias no evidentes.
Guardar nil es indistinguible de no guardar nada. Si haces t.x = nil, la siguiente lectura de x volverá a disparar __index. Lua no tiene el concepto de clave presente con valor nulo; el nil es la ausencia. Cualquier diseño que necesite distinguir “no configurado” de “configurado a nada” tiene que inventarse un centinela.
Un __index bien puesto no cuesta nada en el caso normal. Toda la maquinaria vive en la rama fría. Una tabla con metatabla se lee exactamente igual de rápido que una sin ella mientras las claves estén presentes; el precio se paga en el fallo, no en el acierto.
El campo __index se busca en crudo en la metatabla. Poner un __index en la metatabla de la metatabla no hace nada para este evento. La recursión de la que hablaremos luego ocurre por otra vía.
La versión con tabla: delegación
Si __index es una tabla, el intérprete repite la búsqueda completa sobre ella —incluida su metatabla—. Es delegación pura, y es el mecanismo con el que se construye la herencia.
local Animal = {}
Animal.__index = Animal -- las instancias delegan en Animal
function Animal.nuevo(nombre)
return setmetatable({nombre = nombre}, Animal)
end
function Animal:describir()
return self.nombre .. " hace " .. self:sonido()
end
function Animal:sonido()
return "un ruido indefinido"
end
local Perro = setmetatable({}, {__index = Animal}) -- Perro delega en Animal
Perro.__index = Perro -- y sus instancias en Perro
function Perro.nuevo(nombre)
return setmetatable({nombre = nombre}, Perro)
end
function Perro:sonido()
return "guau"
end
print(Perro.nuevo("Nut"):describir()) --> Nut hace guau
Merece la pena seguir la trayectoria de esa última línea, porque hay dos cadenas distintas trabajando a la vez. El objeto no tiene describir, así que su metatabla Perro responde por él; Perro tampoco lo tiene en crudo, pero Perro es a su vez una tabla con metatabla, cuyo __index apunta a Animal, y allí está. Dentro de describir, la llamada self:sonido() vuelve a empezar la búsqueda desde el objeto, y esta vez se detiene en Perro. Eso es despacho dinámico: no lo implementa nada, emerge de que la búsqueda siempre arranca en la instancia.
Este esquema tiene una propiedad que sorprende a quien viene de lenguajes compilados: es vivo. Si añades un método a Animal después de haber creado mil perros, los mil perros lo tienen inmediatamente, porque nada se copió nunca. La jerarquía es una red de punteros que se recorre en cada fallo, no una estructura congelada en la creación.
La versión con función: cálculo
Si __index es una función, el intérprete la llama con dos argumentos —la tabla original y la clave— y devuelve lo que ella devuelva. Aquí ya no hay delegación: hay una respuesta fabricada.
-- Propiedades calculadas: la clave nunca existe, siempre se computa
local Circulo = {}
Circulo.__index = function(self, clave)
if clave == "area" then
return math.pi * self.radio^2
elseif clave == "perimetro" then
return 2 * math.pi * self.radio
end
return rawget(Circulo, clave) -- si no, los métodos normales
end
local c = setmetatable({radio = 2}, Circulo)
print(c.area, c.perimetro) --> 12.566370614359 12.566370614359
La función puede hacer literalmente cualquier cosa: consultar una base de datos, leer un fichero de configuración, cargar un módulo la primera vez que alguien lo nombra, o lanzar un error para prohibir las lecturas de campos no declarados —una forma barata y muy efectiva de detectar erratas en nombres de campos—.
-- Un objeto estricto: leer un campo inexistente es un fallo, no un nil
local estricto = setmetatable({}, {
__index = function(_, clave)
error("campo desconocido: " .. tostring(clave), 2)
end,
})
La elección entre tabla y función no es estilística. La tabla es más rápida —el bucle vive en C, sin llamada a Lua—, es la que usa el 95% del código real y es la única que se compone bien en jerarquías. La función es más expresiva y es la única opción cuando la respuesta no está guardada en ninguna parte. La regla práctica: tabla si el valor existe en otro sitio, función si el valor hay que inventarlo.
La cadena y su coste
Una cadena de tablas se recorre en el bucle interno del intérprete, no por recursión de la pila de Lua, así que es barata pero no gratis. Cada eslabón es una consulta de hash fallida más un salto. Con tres o cuatro niveles de jerarquía, un método muy llamado paga tres o cuatro fallos en cada invocación.
Y la cadena tiene un límite. El intérprete cuenta las iteraciones y, superado un umbral interno de dos mil vueltas, aborta con un mensaje inequívoco.
local a, b = {}, {}
setmetatable(a, {__index = b})
setmetatable(b, {__index = a}) -- ciclo
print(a.loquesea) --> error: '__index' chain too long; possibly looping
Que sea un error controlado y no un desbordamiento de pila es una decisión de diseño deliberada: el ciclo es un fallo del programador, y el intérprete prefiere delatarlo a colgarse. Con __index como función, en cambio, un ciclo sí produce una recursión real y un desbordamiento de pila, porque cada vuelta es una llamada de Lua de verdad.
Cuando la cadena importa de verdad, se aplana. El truco canónico consiste en copiar los métodos heredados a la clase derivada en el momento de definirla, cambiando lecturas repetidas por una copia única.
-- Aplanar: se paga una vez, se ahorra en cada llamada
local function derivar(base)
local clase = {}
for k, v in pairs(base) do clase[k] = v end
clase.__index = clase
return clase
end
El coste es perder la vivacidad: si luego cambias la base, la derivada no se entera. Es el intercambio clásico entre indirección y copia, y en Lua lo tienes explícito y en tus manos, en cuatro líneas que puedes leer.
Vale la pena decirlo sin adornos: en Lua no hay herencia. Hay una regla de tres líneas sobre qué hacer cuando una lectura devuelve nil, y todo lo que llamamos orientación a objetos es el efecto acumulado de aplicar esa regla en cadena. Esto no es una curiosidad terminológica, es la clave para diseñar bien en este lenguaje. Cuando entiendes que la jerarquía de clases y el objeto con valores por defecto y el módulo que se carga la primera vez que lo tocas y el registro de configuración que consulta variables de entorno son el mismo programa con distinto contenido en un campo, dejas de preguntarte cuál es la forma correcta de hacer clases en Lua —no hay una, hay un mecanismo— y empiezas a preguntarte qué debe pasar exactamente cuando alguien pide algo que no está. Esa pregunta es la que tiene respuestas interesantes: delegar en un padre, calcular, cargar, fallar ruidosamente, devolver un neutro, o registrar el intento para diagnosticar. Cada una de esas respuestas es un patrón distinto en otros lenguajes, con su propia sintaxis y su propia curva de aprendizaje. Aquí son el mismo campo con distinto valor, y esa unificación —no la brevedad del código— es lo que hace que un programa Lua bien pensado envejezca tan bien.
- Construye la jerarquía
AnimalyPerrodel ejemplo y añade un tercer nivelCachorro. Comprueba quedescribirsigue funcionando desde el nieto. - Añade un método nuevo a
Animaldespués de haber creado instancias dePerroy verifica que aparece sin recrear nada. - Escribe un
__indexcomo función que registre en una lista cada clave ausente que alguien pida. Úsalo para descubrir erratas en un módulo real. - Provoca deliberadamente el error de cadena cíclica con dos tablas y lee el mensaje exacto. Repite con
__indexcomo función y compara los dos errores. - Mide con
os.clockun millón de llamadas a un método a través de tres niveles de cadena, y luego con la clase aplanada. Anota la diferencia.