Records: Tipos de datos estructurados y anónimos

Los records [disponible desde Dart 3.0] son tipos de producto anónimos que permiten agrupar múltiples valores en una sola entidad sin necesidad de declarar una clase formal. A diferencia de las clases, que se basan en la identidad de su nombre (tipo nominal), los records se definen por su contenido y su estructura (tipo estructural). Esto significa que si dos records tienen los mismos tipos de datos en el mismo orden, el compilador los trata como el mismo tipo.

Esta característica los hace extremadamente eficientes para retornar múltiples valores de una función o para transportar datos temporales entre capas de lógica sin la verbosidad de crear archivos de soporte para clases que solo sirven como contenedores de datos. Sin embargo, su diseño impone una restricción crítica: son inmutables. Una vez creado un record, no puedes modificar sus campos, lo que garantiza la integridad de los datos al pasar de un proceso a otro.

Debes usar records cuando necesites una estructura de datos rápida, efímera y puramente descriptiva. Si la estructura requiere lógica de negocio (métodos), herencia o una identidad única persistente en el tiempo, una clase sigue siendo la opción correcta. Si intentas usar un record para modelar entidades principales de tu dominio y luego intentas modificar sus valores, te encontrarás con un error de compilación, ya que el runtime de Dart no permite la mutación de sus campos.

void main() {
  // 1. Creación de un record mixto: posicional, nombrado y posicional
  // El tipo es: (int, {String status, int id})
  final result = fetchUserStatus(101);

  // 2. Destructuring usando Patterns para extraer valores
  // Esto extrae los valores directamente a variables locales
  final (code, status, userId) = result;

  print('Respuesta: $code | Estado: $status | Usuario: $userId');

  // 3. Acceso directo a campos posicionales y nombrados
  print('Acceso posicional ($1): ${result.$1}');
  print('Acceso nombrado (status): ${result.status}');

  // 4. Comparación estructural (Structural Equality)
  // Dos records son iguales si sus tipos y valores coinciden exactamente
  final duplicate = (200, status: 'Active', 101);
  final isSame = result == duplicate;
  print('¿Es el duplicado igual al original? $isSame');

  // 5. Uso de records en colecciones para mapeo rápido
  final userPoints = [
    (10, 20), // Record posicional simple (int, int)
    (50, 100),
    (0, 0),
  ];

  for (final (x, y) in userPoints) {
    print('Coordenada: X=$x, Y=$y');
  }
}

/// Una función que retorna un record mixto
/// El tipo de retorno es un record con un campo posicional (int) 
/// y un campo nombrado ({String status, int id})
(int, {String status, int id}) fetchUserStatus(int userId) {
  // Simulamos una respuesta de servidor
  // El compilador garantiza que el orden y los tipos coincidan con la firma
  return (200, status: 'Active', id: userId);
}

Análisis del flujo

En el ejemplo anterior, fetchUserStatus no devuelve un objeto de una clase, sino una estructura de datos pura. El tipo de retorno es (int, {String status, int id}). Al llamar a la función, el valor se almacena en result.

Cuando aplicamos el destructuring con la sintaxis final (code, status, userId) = result;, el runtime de Dart no está creando nuevas instancias de objetos, sino simplemente asignando las referencias de los campos del record a nuevas variables locales. Esto es extremadamente eficiente a nivel de memoria.

La comparación result == duplicate es posible gracias a que los records implementan de forma nativa la igualdad estructural. El compilador de Dart (AOT/JIT) no compara las direcciones de memoria como lo haría con dos instancias de una clase normal; en su lugar, recorre cada campo y compara su valor. Como tanto result como duplicate tienen la misma estructura de tipos y los mismos valores, el resultado es true.

En el bucle for, aprovechamos que los records son tipos ligeros para iterar sobre userPoints. La expresión (x, y) dentro del for-in es un patrón que descompone cada elemento de la lista instantáneamente, permitiendo un código mucho más limpio que usar point.item1.

El error frecuente

Un error común al transicionar de clases a records es intentar la mutación de datos. En una clase con campos var, puedes cambiar un valor; en un record, no.

void main() {
  final position = (x: 10, y: 20);
  
  // Error de compilación: "The setter 'x' isn't defined for the type '(x: int, y: int)'. 
  // A record's fields are immutable."
  position.x = 30; 
}

Si necesitas “cambiar” un valor, la única forma válida es crear un nuevo record utilizando la sintación de expansión o simplemente reasignando la variable si esta fue declarada como var, pero la instancia original del record nunca cambiará.

89

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio