Structs
Qué es un struct
Un struct es un registro de datos ligero: agrupa varios valores bajo nombres de campo, igual que las propiedades de una clase, pero con el conjunto de campos fijado en la declaración. A cambio de esa restricción, internamente los campos se almacenan por índice en un bloque compacto de memoria, sin tablas hash, lo que hace que un struct ocupe varias veces menos memoria que una instancia de clase y se cree mucho más rápido.
Los structs no sustituyen a las clases: las complementan. JARU ofrece tres formas de agrupar datos, cada una con su propósito:
| Contenedor | Campos | Uso ideal |
|---|---|---|
map | Dinámicos, cualquier clave en tiempo de ejecución | Datos cuya estructura no se conoce de antemano (JSON, configuraciones) |
class | Dinámicos, con métodos, herencia y encapsulación | Objetos con comportamiento: datos + lógica |
struct | Fijos, solo datos | Registros puros: puntos, entidades de juego, lecturas de sensores |
Si necesitas métodos, herencia o añadir propiedades sobre la marcha, usa una clase. Si solo necesitas agrupar datos —sobre todo si vas a crear muchos objetos en un ESP32— usa un struct.
Declaración
Un struct se declara con la palabra clave struct, seguida del nombre y de las declaraciones var de sus campos:
struct Point
var x = 0
var y = 0
end
Se pueden declarar varios campos en una misma línea, y un campo sin valor inicial vale 0 (igual que cualquier var en JARU):
struct Enemy
var x = 0, y = 0 // varios campos por linea
var hp = 100
var name = "orc"
var alive = true
var target // sin valor inicial: vale 0
end
El orden de declaración de los campos es parte del contrato del struct: determina el orden de los argumentos al crear instancias. Reordenar los campos de un struct cambia el significado de las llamadas existentes.
Valores por defecto: solo literales constantes
Los valores por defecto de los campos deben ser literales constantes: números (con signo opcional), cadenas, true/false o nil. Cualquier expresión produce un error de compilación:
struct Config
var retries = 3 // ok: numero
var speed = -1.5 // ok: numero con signo
var mode = "auto" // ok: cadena
var debug = false // ok: booleano
var owner = nil // ok: nil
var pos = Point() // ERROR: no es un literal
var items = [] // ERROR: no es un literal
var seed = clock() // ERROR: no es un literal
end
Esta restricción tiene dos motivos. El primero es de rendimiento: al ser constantes, crear una instancia es una simple copia de memoria. El segundo es de seguridad: si un campo pudiera valer [] por defecto, todas las instancias compartirían la misma lista — una fuente clásica de errores difíciles de encontrar en otros lenguajes.
Cuando un campo necesita contener una lista, un mapa u otro struct, el patrón idiomático es asignarlo después de crear la instancia, normalmente en una función fábrica:
func newEnemy(x, y)
var e = Enemy()
e.x = x
e.y = y
e.target = Point(x, y) // el campo ESTA declarado; se rellena aqui
return e
end
Crear instancias
Un struct se instancia llamándolo como si fuera una función, igual que una clase. Los argumentos son posicionales: rellenan los campos en su orden de declaración, y los que falten toman su valor por defecto:
var a = Point() // {x: 0, y: 0} todo a defaults
var b = Point(3, 4) // {x: 3, y: 4} posicional
var c = Point(7) // {x: 7, y: 0} parcial: el resto a defaults
var d = Point(1, 2, 3) // EXCEPCION: Point solo tiene 2 campos
Acceso a los campos y sellado
Los campos se leen y escriben con la notación de punto, como las propiedades de una clase:
var p = Point(1, 2)
p.x = 10
p.y = p.x + 5
println(p.x, " ", p.y) // 10 15
La gran diferencia con las clases es que un struct está sellado: el conjunto de campos es fijo. Acceder a un campo que no existe lanza una excepción, también al escribir:
var e = Enemy()
e.helth = 50 // typo de 'hp': EXCEPCION al instante
// Struct 'Enemy' has no field 'helth'
// (fields: x, y, hp, name, alive, target).
En una clase, e.helth = 50 crea silenciosamente una propiedad nueva llamada helth, y el error no se manifiesta hasta mucho más tarde y en otro lugar del programa. En un struct, el typo se detecta en la línea exacta donde ocurre, y el mensaje incluye la lista de campos válidos.
Semántica de referencia, copy y clone
Como el resto de objetos de JARU, asignar un struct copia la referencia, no el contenido:
var a = Point(1, 2)
var b = a
b.x = 99
println(a.x) // 99: a y b son el mismo objeto
Para duplicar un struct están los métodos copy() (copia superficial: los contenedores anidados se comparten) y clone() (copia profunda: también se duplican listas, mapas y structs anidados), con el mismo contrato que en listas, mapas e instancias:
var original = newEnemy(5, 5)
var copia = original.copy() // campos duplicados, target compartido
var clon = original.clone() // todo duplicado, tambien target
La igualdad == compara referencias: dos structs distintos con los mismos valores no son iguales, igual que ocurre con [1, 2] == [1, 2].
Structs anidados y en contenedores
Un campo puede contener cualquier valor: otro struct, una lista, un mapa... Solo los valores por defecto están restringidos a literales; el contenido en tiempo de ejecución es libre:
struct Segment
var a = nil
var b = nil
end
var s = Segment(Point(1, 2), Point(3, 4))
println(s.a.x) // 1: acceso encadenado
var enemigos = [newEnemy(0, 0), newEnemy(10, 5)]
println(enemigos[1].x) // 10: structs dentro de listas
Al imprimir un struct se muestran sus campos con sus nombres:
println(Point(3, 4)) // Point{x: 3, y: 4}
Funciones en campos (callbacks)
Un struct no tiene métodos propios, pero como un campo admite cualquier valor, puede contener una función. Si guardas una función en un campo, puedes llamarla con la notación de punto, igual que harías con una propiedad de instancia:
func alDisparar(x, y)
println("disparo en ", x, ", ", y)
end
struct Boton
var etiqueta = ""
var onClick = nil // aqui guardaremos una funcion
end
var b = Boton()
b.etiqueta = "Fuego"
b.onClick = alDisparar // asignar la funcion al campo
b.onClick(100, 50) // llamarla: imprime "disparo en 100, 50"
La función guardada en el campo se invoca tal cual: no recibe el struct como this, porque no es un método del struct sino una función normal a la que da la casualidad de que se llega a través de un campo. Es exactamente el mismo mecanismo que ya existe para las propiedades de una instancia de clase.
Esto convierte a los structs en una forma cómoda y ligera de montar tablas de despacho o manejadores de eventos: cada entidad de un juego puede llevar su propia función de comportamiento en un campo, sin el coste de una clase.
Si el campo no contiene un valor invocable (por ejemplo un número), llamarlo lanza una excepción, igual que intentar llamar a cualquier otro valor no invocable. Y si el nombre no corresponde a ningún campo (ni a copy/clone), la llamada lanza has no field or method.
Memoria y rendimiento
La razón de ser de los structs es la eficiencia, especialmente en el ESP32, donde la memoria RAM es el recurso más escaso. Medido con memUse() creando 10.000 objetos:
| Objeto | Como clase | Como struct | Diferencia |
|---|---|---|---|
| Punto (2 campos) | 184 bytes | 36 bytes | 5,1x menos |
| Entidad de juego (8 campos) | 440 bytes | 84 bytes | 5,2x menos |
Para un juego con 500 entidades de 8 campos, eso significa pasar de ~215 KB a ~41 KB: en una placa con ~230 KB libres, es la diferencia entre caber o no caber.
La creación de instancias también es más rápida (en torno a 1,4x frente a una clase con constructor), porque no hay que ejecutar ningún método init ni construir tablas hash. La velocidad de acceso a los campos, en cambio, es prácticamente la misma que en una clase: la ventaja de los structs está en la memoria y en la creación, no en la lectura.
Lo que un struct no tiene
Deliberadamente, un struct no admite:
- Métodos ni herencia — un struct con comportamiento es una clase: usa
class. (Sí puedes guardar una función en un campo y llamarla; ver Funciones en campos.) - Campos privados — todos los campos son públicos.
- Añadir o quitar campos en tiempo de ejecución — para claves dinámicas, usa un
map. - Recorrido con
foreach— un struct no es iterable.
Ejemplo completo
struct Bullet
var x = 0, y = 0
var vx = 0, vy = -4
var active = true
end
func updateBullets(bullets)
foreach (var b in bullets)
if (b.active) then
b.x = b.x + b.vx
b.y = b.y + b.vy
if (b.y < 0) then
b.active = false
end
end
end
end
var bullets = []
for (var i = 0; i < 100; i++)
bullets.append(Bullet(i * 3, 240))
end
updateBullets(bullets)
println(bullets[0]) // Bullet{x: 0, y: 236, vx: 0, vy: -4, active: true}
Cien balas como structs ocupan una fracción de lo que ocuparían como instancias de clase, se crean casi al instante, y un typo en cualquier campo se detecta en el momento.