Clase 099 — Registros, structs y clases

Parte 6 — Datos y estructuras · ⏱️ Duración estimada: 90 min · Nivel: IntermedioClase construida — 10 implementaciones del núcleo verificadas contra casos.json.


🎯 Objetivo

Comprender el registro (struct, clase, record, según el lenguaje) como la estructura que agrupa campos heterogéneos accedidos por nombre, y no por posición. Es el complemento exacto de la tupla de la clase 091: donde la tupla dice «el elemento 0 y el elemento 1», el registro dice «persona.nombre y persona.edad». Ese cambio de índice a nombre no es cosmético: convierte una posición anónima —fácil de confundir— en un campo con significado que el compilador y el lector reconocen. Bajo la sintaxis late una decisión de la que depende casi todo el comportamiento del programa: si el tipo tiene semántica de valor (se copia al asignarlo o pasarlo, como un struct de C, Go o Rust) o semántica de referencia (se comparte, como una clase de Java, C# o Python). En memoria, los campos de un registro viven contiguos, uno tras otro, lo que lo hace amable con la caché igual que el arreglo de la clase 089. En esta clase modelas la misma Persona en diez lenguajes para ver cómo cada uno resuelve las dos preguntas que definen un registro: cómo se nombra y cómo se copia.

📚 Resultados de aprendizaje

Al finalizar, podrás:

  1. Definir un tipo con campos nombrados.
  2. Crear una instancia y acceder a sus campos.
  3. Distinguir struct de clase donde aplique.

🗺️ Temas

# Tema Por qué importa
1 Registro/struct Campos nombrados juntos
2 Instancia Un valor del tipo
3 Acceso a campos .nombre, .edad

📖 Definiciones y características

🧩 Situación

Imagina una función que recibe nombre, edad, email y saldo como cuatro parámetros sueltos. Tarde o temprano alguien invierte dos de ellos, o actualiza edad en un sitio y olvida el otro, y los datos que deberían viajar juntos se desincronizan. El registro cura ese mal de raíz: junta los campos relacionados en un solo tipo con nombre —Persona— que se pasa como una sola cosa, se valida como una sola cosa y no puede separarse por accidente. Ganamos también legibilidad (p.edad dice mucho más que args[1]) y evolución: añadir un campo no obliga a reescribir cada firma de función. El problema de hoy es deliberadamente pequeño —leer un nombre y una edad, construir una Persona e imprimirla— para que la atención caiga sobre lo que de verdad importa: cómo cada lenguaje declara el tipo, nombra los campos y decide si la instancia se copia o se comparte.

🧮 Modelo

Especificación y verificación en casos.json:

stdin esperado
Ada 36 Persona(nombre=Ada, edad=36)
Bo 5 Persona(nombre=Bo, edad=5)
Cy 99 Persona(nombre=Cy, edad=99)

📐 Algoritmo (pseudocódigo neutral)

LEER nombre, edad ; crear Persona ; ESCRIBIR formateado

🌐 Implementaciones idiomáticas — el código a la vista

Mismo algoritmo, forma idiomática en cada lenguaje. Todas producen la salida de casos.json. Cada bloque es el archivo real de implementaciones/: el enlace de cada lenguaje abre su fuente, y el comando de al lado lo ejecuta.

Python · python/main.py · python main.py

import sys
from dataclasses import dataclass


@dataclass
class Persona:
    nombre: str
    edad: int


t = sys.stdin.readline().split()
p = Persona(t[0], int(t[1]))
print(f"Persona(nombre={p.nombre}, edad={p.edad})")

🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R

JavaScript · javascript/main.mjs · node main.mjs

import { readFileSync } from "node:fs";

const t = readFileSync(0, "utf8").trim().split(/\s+/);
const persona = { nombre: t[0], edad: parseInt(t[1], 10) };
console.log(`Persona(nombre=${persona.nombre}, edad=${persona.edad})`);

🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript

TypeScript · typescript/main.ts · pnpm exec tsx main.ts

import { readFileSync } from "node:fs";

interface Persona {
  nombre: string;
  edad: number;
}

const t: string[] = readFileSync(0, "utf8").trim().split(/\s+/);
const p: Persona = { nombre: t[0], edad: parseInt(t[1], 10) };
console.log(`Persona(nombre=${p.nombre}, edad=${p.edad})`);

🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript

Java · java/Main.java · java Main.java

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {
    record Persona(String nombre, int edad) {}

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String[] t = br.readLine().trim().split("\\s+");
        Persona p = new Persona(t[0], Integer.parseInt(t[1]));
        System.out.println("Persona(nombre=" + p.nombre() + ", edad=" + p.edad() + ")");
    }
}

🧬 El mismo programa en la familia JVM: Kotlin · Scala · Groovy · Clojure

C# · csharp/Program.cs · dotnet run

using System;

string[] t = Console.In.ReadToEnd()
    .Split(new[] { ' ', '\t', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
var p = new Persona(t[0], int.Parse(t[1]));
Console.WriteLine($"Persona(nombre={p.Nombre}, edad={p.Edad})");

record Persona(string Nombre, int Edad);

🧬 El mismo programa en la familia .NET: F# · VB.NET

Go · go/main.go · go run main.go

package main

import (
    "bufio"
    "fmt"
    "os"
    "strconv"
    "strings"
)

type Persona struct {
    Nombre string
    Edad   int
}

func main() {
    line, _ := bufio.NewReader(os.Stdin).ReadString('\n')
    t := strings.Fields(line)
    edad, _ := strconv.Atoi(t[1])
    p := Persona{Nombre: t[0], Edad: edad}
    fmt.Printf("Persona(nombre=%s, edad=%d)\n", p.Nombre, p.Edad)
}

🧬 El mismo programa en la familia Sistemas: Zig · Nim · D

Rust · rust/main.rs · rustc main.rs -o main && ./main

use std::io::Read;

struct Persona {
    nombre: String,
    edad: i64,
}

fn main() {
    let mut s = String::new();
    std::io::stdin().read_to_string(&mut s).unwrap();
    let t: Vec<&str> = s.split_whitespace().collect();
    let p = Persona {
        nombre: t[0].to_string(),
        edad: t[1].parse().unwrap(),
    };
    println!("Persona(nombre={}, edad={})", p.nombre, p.edad);
}

🧬 El mismo programa en la familia Sistemas: Zig · Nim · D

C · c/main.c · cc main.c -o main && ./main

#include <stdio.h>

struct Persona {
    char nombre[64];
    long edad;
};

int main(void) {
    struct Persona p;
    if (scanf("%63s %ld", p.nombre, &p.edad) != 2) return 1;
    printf("Persona(nombre=%s, edad=%ld)\n", p.nombre, p.edad);
    return 0;
}

🧬 El mismo programa en la familia C / llaves: C++ · Objective-C

SQL · sql/main.sql · sqlite3 :memory: < main.sql

-- SQL: una fila de una tabla es un registro.
WITH personas(nombre, edad) AS (VALUES ('Ada', 36))
SELECT printf('Persona(nombre=%s, edad=%d)', nombre, edad) AS resultado FROM personas;

🧬 El mismo programa en la familia Lógica y declarativa: Prolog · Datalog

PHP · php/main.php · php main.php

<?php
class Persona {
    public function __construct(public string $nombre, public int $edad) {}
}

$t = preg_split('/\s+/', trim(fgets(STDIN)));
$p = new Persona($t[0], (int) $t[1]);
echo "Persona(nombre={$p->nombre}, edad={$p->edad})\n";

🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R

SQL es declarativo: no lee de stdin como los demás; su implementación muestra la misma idea sobre una tabla de casos, y el verificador la marca como ilustrativa.

🧪 Laboratorio guiado: del código a la salida

Sigamos el caso Ada 36, que debe producir Persona(nombre=Ada, edad=36). Los diez programas leen dos palabras, construyen un registro y lo formatean; conviene mirar tres que revelan modelos distintos de declaración y de memoria.

En Python, el decorador @dataclass sobre class Persona con las anotaciones nombre: str y edad: int genera automáticamente el __init__, de modo que Persona(t[0], int(t[1])) guarda "Ada" y 36 como atributos. Aquí p es una referencia a un objeto en el heap: si hicieras q = p, ambos nombres apuntarían al mismo objeto. El f-string lee p.nombre y p.edad por nombre y arma la línea. Ramalho (Fluent Python, cap. sobre data class builders) muestra que @dataclass es azúcar que evita escribir el constructor y el __repr__ a mano.

En Go, type Persona struct { Nombre string; Edad int } declara un tipo con semántica de valor: p := Persona{Nombre: t[0], Edad: edad} crea el registro en la pila y, si lo asignaras a otra variable, se copiaría campo a campo. Los nombres de campo van en mayúscula por la regla de exportación de Go, no por capricho. fmt.Printf los formatea por nombre para producir exactamente Persona(nombre=Ada, edad=36).

En Java, record Persona(String nombre, int edad) {} es la forma moderna (Java 16+): en una línea el compilador genera el constructor, los accesores nombre() y edad(), y equals/hashCode/toString coherentes. new Persona(t[0], ...) crea el objeto en el heap —semántica de referencia, como toda clase de Java—; los accesores devuelven los campos que la salida ensambla. Bloch (Effective Java) recomienda los records precisamente para estos «portadores de datos inmutables».

Los tres imprimen Persona(nombre=Ada, edad=36); el verificador comprueba que las diez implementaciones coinciden carácter a carácter con lo que dicta casos.json.

🔬 Comparación

Clase de diferencia Observación entre lenguajes
Sintáctica class/@dataclass (Python), record (Java), struct (Go/Rust/C), objeto (JS).
Semántica Struct suele ser por valor; clase por referencia (Java/C#).
Paradigmática SQL: una fila de una tabla es un registro.

La diferencia más honda entre los diez no es sintáctica sino de semántica de copia. En C, Go, Rust y los struct de C#, un registro es un valor: asignarlo o pasarlo a una función lo copia entero, y las dos variables quedan independientes. En Java, Python, JavaScript y las class de C#, la instancia vive en el heap y la variable es una referencia: asignarla comparte el mismo objeto. Esto explica por qué C# ofrece ambas herramientas —record class (referencia) y record struct (valor)— y obliga al programador a elegir. Hay además un eje de cuánto trabajo te ahorra el lenguaje: el record de Java, la @dataclass de Python y el record de C# generan constructor, comparación e impresión gratis; en C debes declarar cada campo del struct y formatearlo a mano, sin accesores ni toString. Y un eje de disposición en memoria: en C, Go y Rust los campos ocupan bytes contiguos con posible relleno de alineamiento, un detalle que importa para el rendimiento y la interoperabilidad; en Python un objeto es un diccionario de atributos, mucho más flexible pero más pesado.

🧬 El concepto en la familia

El registro es tan fundamental que cada familia le da su propia forma, y los nombres engañan. En Kotlin, data class Persona(val nombre: String, val edad: Int) genera igualdad y copia como el record de Java. En C++, struct y class son casi idénticos —solo cambia la visibilidad por defecto— y ambos tienen semántica de valor, al revés que en C#, donde struct es valor y class es referencia. En Swift, la distinción struct (valor) vs. class (referencia) es el eje central del lenguaje. En Rust, un struct es valor y solo se copia implícitamente si implementa Copy; si no, se mueve. Reconocer, en cada lenguaje que toques, si tu registro se copia o se comparte es la mitad de entender su comportamiento: de ahí salen tanto los bugs de aliasing (clase 102) como las sorpresas de identidad (clase 101).

✅ Prueba común

Los mismos casos para todas las implementaciones: casos.json. Verifica la equivalencia:

python scripts/verificar_equivalencia.py 099

🧪 Reto de transferencia

Detalle en reto.md.

⚠️ Errores comunes

❓ Preguntas frecuentes

🔗 Referencias

Libros de la parte:

Libros de los lenguajes del núcleo:


⏮️ Clase 098 · 📂 Parte · 📚 Índice · 🌐 Atlas · Clase 100 ⏭️