Capítulo 2: Tablas - Arrays y Secuencias
Capítulo 2: Tablas - Arrays y Secuencias
“Haz una cosa y hazla bien.” — Filosofía Unix
En la mayoría de lenguajes de programación, tienes múltiples estructuras de datos: arrays, listas enlazadas, hash maps, sets, etc. En Lua, tienes una sola estructura de datos: la tabla.
Antes de que pienses “eso suena limitante”, considera esto: las tablas de Lua son tan versátiles y eficientes que pueden simular todas esas estructuras. Y como solo hay una estructura, dominarla te hace proficient en Lua.
En este capítulo, cubriremos las tablas como arrays (secuencias ordenadas de valores). En el siguiente, las veremos como diccionarios (hash maps).
Lo Básico: Crear y Usar Arrays
En Lua, crear un array es tan simple como esto:
-- >>> fruits = {'manzana', 'banana', 'cereza'}
-- >>> print(fruits[1])
-- manzana
-- >>> print(fruits[2])
-- banana
-- >>> print(fruits[3])
-- cereza
Índices Desde 1 (No Desde 0)
La característica más comentada (y a veces odiada) de Lua: los índices empiezan en 1, no en 0.
-- >>> numbers = {10, 20, 30, 40}
-- >>> print(numbers[1]) -- Primer elemento
-- 10
-- >>> print(numbers[0]) -- Esto es nil
-- nil
-- >>> print(numbers[5]) -- Esto también es nil
-- nil
¿Por qué? Roberto Ierusalimschy (creador de Lua) diseñó Lua pensando en no-programadores. En matemáticas y en el lenguaje natural, contamos desde 1, no desde 0.
SOAPBOX: Índices desde 1
Sí, la mayoría de lenguajes usan índices desde 0. Pero Lua fue diseñado para ser embebido en aplicaciones, frecuentemente usado por diseñadores de juegos, artistas, y científicos que no son programadores profesionales. Para ellos, índices desde 1 es más natural.
Si vienes de Python, JavaScript, o C, te adaptarás en un día. Dale una oportunidad.
El Operador #: Longitud de un Array
El operador # devuelve la longitud de un array:
-- >>> fruits = {'manzana', 'banana', 'cereza'}
-- >>> print(#fruits)
-- 3
Pero hay una trampa importante: # solo funciona correctamente con secuencias.
Secuencias vs Tablas Sparse
Una secuencia es una tabla cuyos índices son enteros consecutivos desde 1 hasta N, sin huecos (nil).
-- Esto es una secuencia
-- >>> seq = {10, 20, 30, 40}
-- >>> print(#seq)
-- 4
-- Esto NO es una secuencia (tiene un hueco)
-- >>> sparse = {10, 20, nil, 40}
-- >>> print(#sparse)
-- 2 -- ¡Se detiene en el primer nil!
Si una tabla tiene huecos, el comportamiento de # es indefinido. Puede devolver cualquier valor válido como “longitud”.
-- >>> weird = {10, 20, nil, 40, 50}
-- >>> print(#weird)
-- 2 -- Puede ser 2, o 5, o cualquier cosa
ADVERTENCIA: Huecos en Arrays
Evita crear arrays con
nilen el medio. Si necesitas “vacío”, usa un valor centinela comofalseo una string vacía"".
Agregar Elementos: table.insert
Para agregar elementos al final de un array, usa table.insert:
-- >>> fruits = {'manzana', 'banana'}
-- >>> table.insert(fruits, 'cereza')
-- >>> print(#fruits)
-- 3
-- >>> print(fruits[3])
-- cereza
También puedes insertar en una posición específica:
-- >>> table.insert(fruits, 2, 'arándano') -- Insertar en posición 2
-- >>> for i, fruit in ipairs(fruits) do
-- >>> print(i, fruit)
-- >>> end
-- 1 manzana
-- 2 arándano
-- 3 banana
-- 4 cereza
Eliminar Elementos: table.remove
Para eliminar el último elemento:
-- >>> fruits = {'manzana', 'banana', 'cereza'}
-- >>> local last = table.remove(fruits)
-- >>> print(last)
-- cereza
-- >>> print(#fruits)
-- 2
O eliminar de una posición específica:
-- >>> table.remove(fruits, 1) -- Eliminar primer elemento
-- >>> print(fruits[1])
-- banana
NOTA: Performance de
table.insertytable.removeInsertar/eliminar al final es O(1). Insertar/eliminar en el medio es O(n) porque requiere desplazar elementos.
Iteración: ipairs vs pairs
Hay dos formas de iterar sobre una tabla en Lua: ipairs y pairs.
ipairs: Para Secuencias
ipairs itera sobre una secuencia en orden, desde 1 hasta el primer nil:
-- >>> fruits = {'manzana', 'banana', 'cereza'}
-- >>> for i, fruit in ipairs(fruits) do
-- >>> print(i, fruit)
-- >>> end
-- 1 manzana
-- 2 banana
-- 3 cereza
Si hay un hueco, ipairs se detiene:
-- >>> sparse = {10, 20, nil, 40}
-- >>> for i, v in ipairs(sparse) do
-- >>> print(i, v)
-- >>> end
-- 1 10
-- 2 20
-- (se detiene en el nil)
pairs: Para Todas las Claves
pairs itera sobre todas las claves de una tabla, sin importar si es secuencia o no:
-- >>> mixed = {10, 20, 30, foo = 'bar', baz = 42}
-- >>> for k, v in pairs(mixed) do
-- >>> print(k, v)
-- >>> end
-- 1 10
-- 2 20
-- 3 30
-- foo bar
-- baz 42
El orden de pairs es indefinido. No confíes en él.
REGLA DE ORO:
- Usa
ipairscuando trabajes con secuencias puras.- Usa
pairscuando trabajes con tablas que pueden tener claves no-numéricas.
Operaciones Comunes con Arrays
Concatenar: table.concat
Para unir elementos de un array en una string:
-- >>> words = {'Lua', 'is', 'awesome'}
-- >>> print(table.concat(words, ' '))
-- Lua is awesome
-- >>> numbers = {1, 2, 3, 4, 5}
-- >>> print(table.concat(numbers, ', '))
-- 1, 2, 3, 4, 5
Puedes especificar un rango:
-- >>> print(table.concat(numbers, ', ', 2, 4)) -- Desde índice 2 al 4
-- 2, 3, 4
PERFORMANCE TIP:
Usar
table.concates mucho más eficiente que concatenar strings con..en un loop:-- ❌ LENTO local result = "" for _, word in ipairs(words) do result = result .. word .. " " end -- ✅ RÁPIDO local result = table.concat(words, " ")¿Por qué? Las strings en Lua son inmutables. Cada concatenación crea una nueva string.
Ordenar: table.sort
Para ordenar un array in-place:
-- >>> numbers = {5, 2, 8, 1, 9}
-- >>> table.sort(numbers)
-- >>> for _, n in ipairs(numbers) do print(n) end
-- 1
-- 2
-- 5
-- 8
-- 9
Con un comparador custom:
-- >>> table.sort(numbers, function(a, b) return a > b end) -- Orden descendente
-- >>> for _, n in ipairs(numbers) do print(n) end
-- 9
-- 8
-- 5
-- 2
-- 1
Para strings:
-- >>> fruits = {'cereza', 'manzana', 'banana'}
-- >>> table.sort(fruits)
-- >>> for _, fruit in ipairs(fruits) do print(fruit) end
-- banana
-- cereza
-- manzana
Buscar Elementos
Lua no tiene un método built-in para buscar, pero es fácil de implementar:
local function find(tbl, value)
for i, v in ipairs(tbl) do
if v == value then
return i
end
end
return nil
end
-- >>> numbers = {10, 20, 30, 40}
-- >>> print(find(numbers, 30))
-- 3
-- >>> print(find(numbers, 99))
-- nil
Tablas Multidimensionales
Lua no tiene arrays multidimensionales nativos, pero puedes simularlos con tablas de tablas:
-- Matriz 3x3
-- >>> matrix = {
-- >>> {1, 2, 3},
-- >>> {4, 5, 6},
-- >>> {7, 8, 9}
-- >>> }
-- >>> print(matrix[2][3]) -- Fila 2, columna 3
-- 6
Iterar sobre una matriz:
-- >>> for i, row in ipairs(matrix) do
-- >>> for j, value in ipairs(row) do
-- >>> print(string.format('matrix[%d][%d] = %d', i, j, value))
-- >>> end
-- >>> end
-- matrix[1][1] = 1
-- matrix[1][2] = 2
-- matrix[1][3] = 3
-- matrix[2][1] = 4
-- ...
Alternativa: Tablas de 1 Dimensión
Para mejor performance, usa una tabla 1D con índices calculados:
local function create_matrix(rows, cols, initial_value)
local m = {}
for i = 1, rows * cols do
m[i] = initial_value or 0
end
return {
data = m,
rows = rows,
cols = cols,
get = function(self, i, j)
return self.data[(i - 1) * self.cols + j]
end,
set = function(self, i, j, value)
self.data[(i - 1) * self.cols + j] = value
end
}
end
-- >>> m = create_matrix(3, 3, 0)
-- >>> m:set(2, 3, 42)
-- >>> print(m:get(2, 3))
-- 42
Esta técnica es más eficiente porque tiene mejor localidad de cache.
Caso Práctico: Implementar una Cola Circular
Una cola circular (circular queue) es una estructura de datos eficiente para implementar una cola (FIFO) con espacio fijo.
local CircularQueue = {}
CircularQueue.__index = CircularQueue
function CircularQueue.new(capacity)
local self = setmetatable({}, CircularQueue)
self.capacity = capacity
self.data = {}
self.head = 1
self.tail = 1
self.size = 0
return self
end
function CircularQueue:enqueue(value)
if self.size == self.capacity then
error("Queue is full")
end
self.data[self.tail] = value
self.tail = (self.tail % self.capacity) + 1
self.size = self.size + 1
end
function CircularQueue:dequeue()
if self.size == 0 then
error("Queue is empty")
end
local value = self.data[self.head]
self.data[self.head] = nil -- Ayudar al GC
self.head = (self.head % self.capacity) + 1
self.size = self.size - 1
return value
end
function CircularQueue:peek()
if self.size == 0 then
return nil
end
return self.data[self.head]
end
function CircularQueue:is_empty()
return self.size == 0
end
function CircularQueue:is_full()
return self.size == self.capacity
end
function CircularQueue:__len()
return self.size
end
function CircularQueue:__tostring()
local elements = {}
local index = self.head
for i = 1, self.size do
table.insert(elements, tostring(self.data[index]))
index = (index % self.capacity) + 1
end
return "CircularQueue[" .. table.concat(elements, ", ") .. "]"
end
Uso:
-- >>> q = CircularQueue.new(5)
-- >>> q:enqueue(10)
-- >>> q:enqueue(20)
-- >>> q:enqueue(30)
-- >>> print(q)
-- CircularQueue[10, 20, 30]
-- >>> print(q:dequeue())
-- 10
-- >>> print(q:peek())
-- 20
-- >>> print(#q)
-- 2
DEEP DIVE: Cómo Funcionan las Tablas Internamente
Las tablas de Lua tienen una implementación híbrida: parte array, parte hash table.
La Parte Array
Cuando Lua detecta que estás usando índices consecutivos desde 1, optimiza la tabla para usar un array real en memoria:
-- >>> t = {10, 20, 30, 40, 50} -- Optimizado como array
Esto hace que el acceso sea O(1) y muy eficiente en cache.
La Parte Hash
Cuando usas claves no-enteras o índices no-consecutivos, Lua usa una hash table:
-- >>> t = {foo = 'bar', baz = 42} -- Hash table
Híbrido
La magia ocurre cuando mezclas ambos:
-- >>> mixed = {10, 20, 30, foo = 'bar'}
-- Los índices 1, 2, 3 van al array
-- La clave 'foo' va al hash
Lua decide dinámicamente cuánto espacio asignar a cada parte basándose en tu uso.
Implicaciones para Performance
-
Secuencias puras son las más rápidas: Usa índices consecutivos desde 1 siempre que puedas.
-
Evita huecos: Un solo
nilen el medio puede hacer que Lua cambie a hash table. -
Prealoca si conoces el tamaño: Aunque Lua crece dinámicamente, preallocating es más eficiente:
-- En lugar de:
local t = {}
for i = 1, 1000 do
t[i] = i * 2
end
-- Mejor (solo en LuaJIT con tabla grande):
local t = table.new(1000, 0) -- LuaJIT: 1000 array slots, 0 hash slots
for i = 1, 1000 do
t[i] = i * 2
end
Cuándo NO Usar Arrays
Arrays no son la mejor estructura para todo:
❌ No uses arrays para:
- Sets (mejor: tabla con keys = true)
- Prioridades (mejor: binary heap)
- Búsqueda eficiente (mejor: tabla como hash map)
✅ Usa arrays para:
- Secuencias ordenadas
- Stacks (push/pop al final)
- Colas (con índices circulares)
- Datos tabulares (matrices)
En el próximo capítulo, veremos cómo usar tablas como diccionarios y sets.
Resumen del Capítulo
Las tablas de Lua son increíblemente versátiles. Como arrays:
- Índices desde 1, no 0
- Operador
#para obtener longitud (solo funciona con secuencias) - Secuencias = índices consecutivos desde 1, sin
nil ipairspara iterar secuencias,pairspara todotable.insert,table.remove,table.sort,table.concatson tus amigos
Internamente, las tablas son híbridas: parte array (rápido), parte hash (flexible).
Próximo: Capítulo 3: Diccionarios y Conjuntos
Ejercicios
- Array Reverso: Escribe una función que revierta un array in-place.
function reverse(arr)
-- Tu código aquí
end
-- >>> nums = {1, 2, 3, 4, 5}
-- >>> reverse(nums)
-- >>> print(table.concat(nums, ', '))
-- 5, 4, 3, 2, 1
- Flatten: Escribe una función que aplane un array anidado.
function flatten(arr)
-- Tu código aquí
end
-- >>> nested = {1, {2, 3}, {4, {5, 6}}}
-- >>> flat = flatten(nested)
-- >>> print(table.concat(flat, ', '))
-- 1, 2, 3, 4, 5, 6
- Implementa un Stack: Usa una tabla para implementar un stack (push, pop, peek, is_empty).
Lecturas Adicionales
- Programming in Lua, 4th edition - Capítulo 11: Data Structures
- Lua 5.4 Reference Manual - Section 3.4.7: Table Constructors
- Lua Performance Tips - Chapter 11: Lua Performance Tips