Saltar al contenido principal

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:

ContenedorCamposUso ideal
mapDinámicos, cualquier clave en tiempo de ejecuciónDatos cuya estructura no se conoce de antemano (JSON, configuraciones)
classDinámicos, con métodos, herencia y encapsulaciónObjetos con comportamiento: datos + lógica
structFijos, solo datosRegistros puros: puntos, entidades de juego, lecturas de sensores
¿Clase o struct?

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).
Protección contra typos

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.

nota

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:

ObjetoComo claseComo structDiferencia
Punto (2 campos)184 bytes36 bytes5,1x menos
Entidad de juego (8 campos)440 bytes84 bytes5,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.