Capítulo 4: Strings - Inmutables y Compartidos
Capítulo 4: Strings - Inmutables y Compartidos
“Todo lo complicado es innecesario, y todo lo necesario es simple.” — Filosofía Lua
Las strings en Lua son uno de los tipos de datos más usados. Son inmutables, internadas (interned), y tienen un sistema de pattern matching único que es más simple que regex pero sorprendentemente poderoso.
Inmutabilidad: Strings Nunca Cambian
En Lua, una vez que creas una string, nunca cambia:
-- >>> s = "hello"
-- >>> s = s .. " world" -- Esto NO modifica la string original
-- >>> print(s)
-- hello world
Lo que realmente sucede:
- Se crea la string
"hello" - Se crea una NUEVA string
"hello world" - La variable
sahora apunta a la nueva string - La string original
"hello"queda para el garbage collector (a menos que otra variable la referencie)
Implicaciones
-- ❌ MALO: Concatenación en loop
local result = ""
for i = 1, 10000 do
result = result .. "x" -- Crea 10,000 strings nuevas!
end
-- ✅ BUENO: Usar table.concat
local parts = {}
for i = 1, 10000 do
table.insert(parts, "x")
end
local result = table.concat(parts) -- Una sola concatenación
PERFORMANCE TIP:
table.concates órdenes de magnitud más rápido que concatenación repetida:
- Concatenación en loop: O(n²)
table.concat: O(n)
String Interning: Todas las Strings Son Únicas
Lua mantiene una tabla interna de todas las strings. Si dos strings tienen el mismo contenido, son la misma string en memoria:
-- >>> s1 = "hello"
-- >>> s2 = "hello"
-- >>> print(s1 == s2) -- Comparación de contenido
-- true
-- >>> print(s1 == s2) -- ¡También son la MISMA string en memoria!
-- true
Esto significa que:
- Comparar strings es rápido: Solo compara punteros, no contenido
- Strings ocupan menos memoria: Cada string única se almacena solo una vez
- Crear strings “duplicadas” es gratis: Si ya existe, se reutiliza
Deep Dive: Cómo Funciona
-- Internamente, Lua mantiene algo así:
string_table = {
["hello"] = <puntero a memoria>,
["world"] = <puntero a memoria>,
["foo"] = <puntero a memoria>
}
-- Cuando haces:
local s = "hello"
-- Lua busca en string_table. Si existe, devuelve el puntero. Si no, crea la string.
NOTA: No hay strings mutables
A diferencia de Python (que tiene
strybytearray) o JavaScript (que tiene strings y arrays de chars), Lua solo tiene strings inmutables.
Operaciones Básicas
Concatenación: Operador ..
-- >>> s1 = "Hello"
-- >>> s2 = "World"
-- >>> s3 = s1 .. " " .. s2
-- >>> print(s3)
-- Hello World
Convertir números automáticamente:
-- >>> age = 30
-- >>> msg = "You are " .. age .. " years old"
-- >>> print(msg)
-- You are 30 years old
Longitud: Operador #
-- >>> s = "hello"
-- >>> print(#s)
-- 5
Cuidado con UTF-8:
-- >>> s = "señor" -- 5 caracteres
-- >>> print(#s)
-- 6 -- ¡Cuenta bytes, no caracteres!
Para contar caracteres UTF-8:
-- >>> s = "señor"
-- >>> print(utf8.len(s))
-- 5
Acceso a Caracteres: string.sub
-- >>> s = "hello"
-- >>> print(string.sub(s, 1, 1)) -- Primer carácter
-- h
-- >>> print(string.sub(s, 2, 4)) -- Del 2 al 4
-- ell
-- >>> print(string.sub(s, -1)) -- Último carácter
-- o
Lua NO tiene s[i] para acceder a caracteres individuales.
Comparación
Strings se comparan lexicográficamente:
-- >>> print("apple" < "banana")
-- true
-- >>> print("Z" < "a") -- Mayúsculas vienen antes
-- true
La Biblioteca string
Lua tiene una rica biblioteca para manipular strings.
Mayúsculas/Minúsculas
-- >>> s = "Hello World"
-- >>> print(string.upper(s))
-- HELLO WORLD
-- >>> print(string.lower(s))
-- hello world
Repetición
-- >>> print(string.rep("ha", 3))
-- hahaha
-- >>> print(string.rep("-", 20))
-- --------------------
Reverso
-- >>> print(string.reverse("hello"))
-- olleh
Buscar y Reemplazar
-- Buscar
-- >>> s = "hello world"
-- >>> start, finish = string.find(s, "world")
-- >>> print(start, finish)
-- 7 11
-- Reemplazar
-- >>> s = "hello world"
-- >>> result = string.gsub(s, "world", "Lua")
-- >>> print(result)
-- hello Lua
-- Reemplazar N veces
-- >>> s = "foo foo foo"
-- >>> result = string.gsub(s, "foo", "bar", 2)
-- >>> print(result)
-- bar bar foo
Formato: string.format
Similar a printf de C:
-- >>> name = "Alice"
-- >>> age = 30
-- >>> print(string.format("My name is %s and I'm %d years old", name, age))
-- My name is Alice and I'm 30 years old
-- >>> pi = 3.14159265
-- >>> print(string.format("Pi: %.2f", pi))
-- Pi: 3.14
-- >>> num = 42
-- >>> print(string.format("Hex: 0x%x, Oct: %o", num, num))
-- Hex: 0x2a, Oct: 52
Especificadores comunes:
%s: string%d: entero%f: float%.Nf: float con N decimales%x: hexadecimal%o: octal
Pattern Matching: NO es Regex
Lua tiene su propio sistema de patrones. NO es compatible con regex, pero es más simple y suficientemente poderoso.
Diferencias con Regex
| Regex | Lua Pattern |
|---|---|
. | . (cualquier carácter) |
\d | %d (dígito) |
\w | %w (alfanumérico) |
\s | %s (espacio) |
* | * (0 o más) |
+ | + (1 o más) |
? | - (0 o más, no greedy) |
[abc] | [abc] (clase de caracteres) |
(...) | (...) (captura) |
Clases de Caracteres
.: cualquier carácter%a: letras%d: dígitos%w: alfanuméricos%s: espacios%p: puntuación%l: letras minúsculas%u: letras mayúsculas
Versión negada (mayúscula):
%A: NO letras%D: NO dígitos%S: NO espacios
Ejemplos
-- Encontrar un número
-- >>> s = "Age: 30"
-- >>> num = string.match(s, "%d+")
-- >>> print(num)
-- 30
-- Encontrar una palabra
-- >>> s = "Hello World"
-- >>> word = string.match(s, "%a+")
-- >>> print(word)
-- Hello
-- Capturar grupos
-- >>> s = "Name: Alice, Age: 30"
-- >>> name, age = string.match(s, "Name: (%a+), Age: (%d+)")
-- >>> print(name, age)
-- Alice 30
-- Encontrar todos los matches
-- >>> s = "foo bar baz"
-- >>> for word in string.gmatch(s, "%a+") do
-- >>> print(word)
-- >>> end
-- foo
-- bar
-- baz
Reemplazar con Patterns
-- Reemplazar todos los números con 'X'
-- >>> s = "Room 101, Floor 5"
-- >>> result = string.gsub(s, "%d+", "X")
-- >>> print(result)
-- Room X, Floor X
-- Usar capturas en el reemplazo
-- >>> s = "John Doe"
-- >>> result = string.gsub(s, "(%a+) (%a+)", "%2, %1")
-- >>> print(result)
-- Doe, John
Anclajes
^: inicio de string$: fin de string
-- Verificar que empiece con "Hello"
-- >>> s = "Hello World"
-- >>> if string.match(s, "^Hello") then
-- >>> print("Starts with Hello")
-- >>> end
-- Starts with Hello
-- Verificar que termine con ".txt"
-- >>> filename = "document.txt"
-- >>> if string.match(filename, "%.txt$") then
-- >>> print("Es un archivo de texto")
-- >>> end
-- Es un archivo de texto
UTF-8 en Lua 5.3+
Desde Lua 5.3, hay soporte built-in para UTF-8.
Biblioteca utf8
-- Longitud en caracteres (no bytes)
-- >>> s = "señor"
-- >>> print(utf8.len(s))
-- 5
-- Iterar sobre caracteres UTF-8
-- >>> s = "héllo"
-- >>> for pos, code in utf8.codes(s) do
-- >>> print(pos, utf8.char(code))
-- >>> end
-- 1 h
-- 2 é
-- 4 l
-- 5 l
-- 6 o
-- Convertir code point a carácter
-- >>> print(utf8.char(0x1F600)) -- Emoji 😀
-- 😀
Validar UTF-8
-- >>> s = "válido"
-- >>> if utf8.len(s) then
-- >>> print("UTF-8 válido")
-- >>> end
-- UTF-8 válido
Caso Práctico: Parser de Markdown Simple
Implementemos un parser que convierta markdown básico a HTML:
local MarkdownParser = {}
function MarkdownParser.parse(markdown)
local html = markdown
-- Headers
html = string.gsub(html, "^### (.+)$", "<h3>%1</h3>", 1)
html = string.gsub(html, "^## (.+)$", "<h2>%1</h2>", 1)
html = string.gsub(html, "^# (.+)$", "<h1>%1</h1>", 1)
-- Bold
html = string.gsub(html, "%*%*(.-)%*%*", "<strong>%1</strong>")
-- Italic
html = string.gsub(html, "%*(.-)%*", "<em>%1</em>")
-- Code inline
html = string.gsub(html, "`(.-)`", "<code>%1</code>")
-- Links [text](url)
html = string.gsub(html, "%[(.-)%]%((.-)%)", '<a href="%2">%1</a>')
return html
end
function MarkdownParser.parse_multiline(markdown)
local lines = {}
for line in markdown:gmatch("[^\r\n]+") do
table.insert(lines, MarkdownParser.parse(line))
end
return table.concat(lines, "\n")
end
Uso:
-- >>> md = [[
-- >>> # Welcome to Lua
-- >>> This is **bold** and this is *italic*.
-- >>> Check out [Lua](https://lua.org) for more info.
-- >>> Use `print()` to output text.
-- >>> ]]
-- >>> html = MarkdownParser.parse_multiline(md)
-- >>> print(html)
-- <h1>Welcome to Lua</h1>
-- This is <strong>bold</strong> and this is <em>italic</em>.
-- Check out <a href="https://lua.org">Lua</a> for more info.
-- Use <code>print()</code> to output text.
Mejora: Bloques de Código
function MarkdownParser.parse_code_blocks(markdown)
local in_code_block = false
local lines = {}
local code_lines = {}
for line in markdown:gmatch("[^\r\n]+") do
if line:match("^```") then
if in_code_block then
-- Cerrar bloque de código
table.insert(lines, "<pre><code>")
table.insert(lines, table.concat(code_lines, "\n"))
table.insert(lines, "</code></pre>")
code_lines = {}
in_code_block = false
else
-- Abrir bloque de código
in_code_block = true
end
else
if in_code_block then
table.insert(code_lines, line)
else
table.insert(lines, MarkdownParser.parse(line))
end
end
end
return table.concat(lines, "\n")
end
Builder de Strings Eficiente
Cuando construyes strings dinámicamente, usa este patrón:
local StringBuilder = {}
StringBuilder.__index = StringBuilder
function StringBuilder.new()
return setmetatable({_parts = {}}, StringBuilder)
end
function StringBuilder:append(str)
table.insert(self._parts, str)
return self -- Para chainear
end
function StringBuilder:append_line(str)
return self:append(str):append("\n")
end
function StringBuilder:to_string()
return table.concat(self._parts)
end
function StringBuilder:__tostring()
return self:to_string()
end
Uso:
-- >>> sb = StringBuilder.new()
-- >>> sb:append("Hello ")
-- >>> :append("World")
-- >>> :append_line("!")
-- >>> :append("Lua is awesome")
-- >>> print(sb)
-- Hello World!
-- Lua is awesome
Caso Práctico: Template Engine
local Template = {}
function Template.render(template, data)
-- Reemplazar {{variable}} con valores de data
return (string.gsub(template, "{{%s*(.-)%s*}}", function(key)
return tostring(data[key] or "")
end))
end
Uso:
-- >>> template = "Hello {{name}}, you are {{age}} years old!"
-- >>> data = {name = "Alice", age = 30}
-- >>> print(Template.render(template, data))
-- Hello Alice, you are 30 years old!
Versión avanzada con expresiones:
function Template.render_advanced(template, data)
-- Permitir expresiones simples: {{user.name}}
return (string.gsub(template, "{{%s*(.-)%s*}}", function(expr)
-- Evaluar la expresión en el contexto de data
local func, err = load("return " .. expr, "template", "t", data)
if not func then
return "{{" .. expr .. "}}" -- Si falla, dejar como está
end
local ok, result = pcall(func)
return ok and tostring(result) or ""
end))
end
Resumen del Capítulo
Strings en Lua:
- Inmutables: Nunca cambian, cada modificación crea una nueva string
- Internadas: Strings iguales son la misma en memoria
table.concates más eficiente que concatenación repetida- Pattern matching: Similar a regex pero más simple
- UTF-8: Soporte nativo desde Lua 5.3 con la librería
utf8
Próximo: Capítulo 5: Múltiples Valores y Destructuring
Ejercicios
- Contar Palabras: Escribe una función que cuente palabras en una string.
function word_count(s)
-- Tu código aquí
end
-- >>> print(word_count("Hello world from Lua"))
-- 4
- Slug Generator: Convierte una string a formato slug (URL-friendly).
function slugify(s)
-- Tu código aquí
end
-- >>> print(slugify("Hello World! ¿Cómo estás?"))
-- hello-world-como-estas
- CSV Parser: Parse una línea CSV considerando comillas.
function parse_csv_line(line)
-- Tu código aquí
end
-- >>> print(table.concat(parse_csv_line('a,b,"c,d",e'), " | "))
-- a | b | c,d | e