Claves y valores
Qué puede ser clave de una tabla en Lua y qué no: la prohibición de nil y NaN, la normalización de los flotantes con parte decimal nula, la identidad de referencia cuando la clave es una tabla o una función, y la asimetría real entre claves numéricas y claves de cadena.
La regla oficial cabe en una línea: cualquier valor de Lua puede ser clave de una tabla salvo nil y salvo el no-número. Esa frase, aparentemente liberal, esconde tres precisiones con consecuencias diarias: los flotantes cuyo valor es entero se normalizan antes de indexar, las tablas y funciones se comparan por identidad y nunca por contenido, y asignar el valor ausente equivale a borrar la entrada. Quien no interioriza estas tres cosas escribe cachés que nunca aciertan y bucles que ven claves fantasma.
- Enumerar con precisión qué valores pueden ser clave y por qué los dos excluidos lo están.
- Entender la normalización de claves numéricas y la trampa de los flotantes.
- Usar tablas y funciones como claves sabiendo que la comparación es por identidad.
- Distinguir el papel de un valor ausente en una clave y en un valor almacenado.
Qué puede ser una clave
Los seis tipos con valores utilizables como clave son booleano, número, cadena, función, tabla y dato de usuario. Los dos excluidos lo están por razones distintas y ambas instructivas:
local t = {}
t[true] = "booleano"
t[42] = "entero"
t["texto"] = "cadena"
t[print] = "funcion"
t[{}] = "tabla anonima e inalcanzable"
t[nil] = 1 --> error: table index is nil
t[0/0] = 1 --> error: table index is NaN
nil no puede ser clave porque en Lua ausencia de valor y ausencia de entrada son la misma cosa: si t[nil] pudiera almacenarse, no habría forma de distinguir una clave presente de una consulta a una clave inexistente, y el recorrido con next perdería su condición de parada. El no-número queda fuera por un motivo más profundo: el estándar de coma flotante establece que no es igual a sí mismo, de modo que jamás podrías recuperar lo que guardaras bajo él. Una clave irrecuperable no es una clave; es una fuga de memoria con sintaxis de índice.
flowchart TD V[Un valor cualquiera] --> P[Ausente o no numero] P --> E[Prohibido como clave] V --> Q[Flotante con parte decimal nula] Q --> N[Se normaliza al entero equivalente] V --> D[Tabla funcion o dato de usuario] D --> I[Se compara por identidad de referencia] V --> C[Booleano entero o cadena] C --> W[Se compara por valor]
La normalización de las claves numéricas
Desde la versión 5.3 Lua distingue enteros de flotantes, pero al indexar una tabla aplica una regla de normalización: si la clave es un flotante cuyo valor es exactamente entero, se convierte al entero correspondiente antes de buscar. Es la única conversión implícita que sobrevive en el operador de indexación:
local t = {}
t[1] = "uno"
print(t[1.0]) --> uno (1.0 se normaliza a 1)
t[2.5] = "dos y medio"
print(t[2.5]) --> dos y medio (no es entero: sigue siendo flotante)
La normalización opera únicamente sobre la clave, nunca sobre el valor almacenado, y no tiene contrapartida en el sentido inverso: el entero y el flotante siguen siendo valores de subtipos distintos si los guardas, y el resultado de math.type lo delata. Solo al indexar se unifican.
local t = {}
t[3] = "entero"
for k in pairs(t) do print(math.type(k)) end --> integer, no float
Esta normalización es la que permite que un array construido con literales enteros siga funcionando cuando el índice viene de una división. Pero deja intacta la trampa clásica: un valor con parte decimal no nula es una clave distinta de cualquier entero, y no hay redondeo alguno. Tampoco hay conversión entre números y cadenas: t[1] y t["1"] son dos entradas independientes que pueden coexistir con valores distintos, algo que sorprende a quien llega de lenguajes donde los índices se convierten a texto.
Si una tabla se llena leyendo texto de un archivo, todas sus claves numéricas serán cadenas salvo que las conviertas explícitamente con tonumber. El síntoma es siempre el mismo: la tabla parece vacía para el operador de longitud, la iteración con ipairs no produce nada y solo pairs muestra el contenido. Normaliza el tipo de la clave en el punto de entrada de los datos, nunca en el punto de consulta.
La identidad como clave
Cuando la clave es una tabla, una función o un dato de usuario, Lua compara por referencia: dos valores son la misma clave si y solo si son el mismo objeto en memoria. Dos tablas con idéntico contenido son claves distintas, para siempre.
local a, b = {}, {}
local reg = {}
reg[a] = "primera"
reg[b] = "segunda"
print(reg[a], reg[b]) --> primera segunda (dos claves distintas)
local mismo = a
print(reg[mismo]) --> primera (misma identidad)
Las cadenas son la excepción interesante a esta regla. Aunque son valores compuestos por dentro, Lua las interna: dos cadenas con el mismo contenido son literalmente el mismo objeto en memoria, de modo que compararlas cuesta lo mismo que comparar dos punteros y usarlas como clave es tan barato como usar un entero. Esa decisión es la que hace viable que todo el acceso a campos con nombre del lenguaje —cada t.campo, cada llamada a método, cada consulta al entorno global— pase por la parte dispersa de una tabla sin que el coste se note.
local x = "hola"
local y = "ho" .. "la"
local reg2 = {}
reg2[x] = 1
print(reg2[y]) --> 1 (misma cadena internada, misma clave)
Objeto como etiqueta
Usar una tabla como clave permite anotar metadatos sobre un objeto sin tocarlo. El objeto no sabe que está siendo anotado y su estructura no se contamina con campos auxiliares.
Clave privada infalsificable
Una tabla vacía creada en un ámbito cerrado es una clave que nadie de fuera puede construir por casualidad. Es el idioma habitual para campos privados dentro de objetos compartidos.
Registro de funciones
Como las funciones son valores de primera clase, pueden ser claves. Eso habilita tablas que asocian una función con su nombre, su coste medido o su documentación.
Tensión con el recolector
Una tabla usada como clave mantiene viva a la clave mientras la tabla contenedora viva. Las tablas débiles existen precisamente para romper esa retención cuando no se desea.
El valor ausente borra
La asimetría más importante del modelo es que un valor ausente no puede ser clave pero sí puede ser valor, y cuando lo es su efecto no es almacenar nada: es eliminar la entrada.
local t = { a = 1, b = 2 }
t.a = nil -- no guarda nil: borra la clave "a"
print(t.a) --> nil (indistinguible de una clave que nunca existio)
Cuando el dominio del problema necesita distinguir no informado de informado como vacío, la solución idiomática es fabricar un centinela: una tabla vacía única, creada una sola vez, que signifique vacío deliberado y que se compare por identidad.
local VACIO = setmetatable({}, { __tostring = function() return "VACIO" end })
local registro = { nombre = "ana", telefono = VACIO }
if registro.telefono == VACIO then print("declarado sin telefono") end
if registro.correo == nil then print("campo nunca informado") end
De ahí se sigue que en Lua no hay forma de preguntar si una clave existe con valor ausente, porque tal estado no es representable. Y de ahí se sigue también la prohibición de modificar una tabla mientras se recorre con pairs: añadir claves durante la iteración tiene comportamiento indefinido, mientras que asignar el valor ausente a una clave ya visitada está expresamente permitido, justo porque borrar no reorganiza la estructura interna.
Detrás de las dos únicas prohibiciones del modelo hay una tesis coherente sobre la identidad y la existencia que conviene enunciar sin rodeos. Lua decide que una tabla no es un contenedor de pares sino una función parcial total: una función matemática que asigna un valor a todos los valores posibles del universo, y que devuelve el valor ausente en todos los puntos donde no se ha dicho otra cosa. Con esa definición, no existe la operación de borrar ni la de crear una clave; solo existe la de redefinir la función en un punto, y el valor ausente es simplemente el resultado por defecto. Todo lo demás se deduce. No puede haber clave ausente porque no tiene sentido preguntar por el valor de la función en un punto que no pertenece al universo. No puede haber clave no-numérica porque una función necesita poder localizar el punto que se le pide, y ese valor no es igual ni a sí mismo. La longitud de una tabla resulta ser una noción emergente y no un atributo almacenado, y por eso es frágil en presencia de agujeros. La distinción entre presente-con-ausencia y ausente desaparece del lenguaje, y con ella desaparece toda una clase de errores en la que otros lenguajes viven instalados. El coste, y hay que decirlo, es que Lua no puede expresar el registro cuyo campo vale explícitamente nada, y quien lo necesita debe fabricarse un centinela: una tabla vacía única que signifique vacío deliberado. Es un intercambio pequeño y consciente, y es el mejor ejemplo del método de diseño de Lua: elegir un modelo teórico limpio, aceptar su única incomodidad y dejar que todas las demás reglas del lenguaje se sigan de él sin excepciones.
- Comprueba que
t[1],t[1.0]yt["1"]no son todas la misma clave, y explica cuáles dos coinciden y por qué. - Crea dos tablas de contenido idéntico, úsalas como claves y verifica que la comparación es por identidad y no por estructura.
- Anota metadatos sobre un objeto usando el objeto como clave de una tabla externa, sin añadirle ningún campo.
- Asigna el valor ausente a una clave existente y demuestra con
nextque la entrada ha desaparecido de verdad y no solo del acceso directo.