Dart Record (Registro / Tupla)

I. ¿Qué es un Record?

El Record es un tipo agregado, inmutable y anónimo, sirve para agrupar datos de distintos tipos en un único objeto. Estas son sus diferencias fundamentales frente a List / Set / Map:

  • Tamaño fijo: una vez creado, no se pueden agregar ni quitar campos; almacenamiento heterogéneo:
  • Dentro de un mismo Record se pueden guardar tipos completamente distintos como int, String, bool
  • Restricción de tipado fuerte: el compilador reconoce el tipo de cada campo, no se pierde la seguridad de tipos
  • Solo lectura, no modificable: después de crearlo no se puede reasignar el valor de sus campos

El Record es una variable válida en su totalidad, admite todas las operaciones habituales: almacenarse en variables, pasar como parámetro / múltiples valores de retorno en funciones, anidarse, guardarse en List/Map/Set.

II. Sintaxis básica de Record

1. Literal de Record

Se envuelve entre paréntesis y separa por comas, se divide en dos categorías: campos posicionales y campos con nombre. Los campos posicionales van al principio, los campos con nombre se escriben después con la sintaxis clave:.

// Mezcla de campos posicionales y con nombre
var info = ('Jim', age: 20, flag: true, 'Student');Lenguaje del código: JavaScript (javascript)

2. Formas de uso de Record

1 Campos posicionales

// Declaración: dos campos posicionales, primero String, luego int
(String, int) user;

// Asignación coincidiendo con la estructura
user = ('李四', 22);
Lenguaje del código: JavaScript (javascript)

2 Campos con nombre

La declaración de tipo utiliza llaves {}

// Declarar dos campos con nombre a(int), b(bool)
({int a, bool b}) data;

data = (a: 100, b: false);Lenguaje del código: JavaScript (javascript)

3 Uso mixto

Declaración de tipo combinando campos posicionales y campos con nombre

// 1 campo posicional String, 2 campos con nombre score(int) y pass(bool)
(String, {int score, bool pass}) exam;
exam = ('Examen final', score: 90, pass: true);
Lenguaje del código: JavaScript (javascript)

III. Errores frecuentes

1: El nombre de los campos con nombre forma parte de la estructura de tipo; nombres distintos generan tipos totalmente diferentes
// Tipo: ({int a, int b})
({int a, int b}) pointAB = (a: 1, b: 2);
// Tipo: ({int x, int y})
({int x, int y}) pointXY = (x: 3, y: 4);

// Error de compilación! Los tipos no coinciden, no se pueden asignar mutuamente
// pointAB = pointXY;
Lenguaje del código: JavaScript (javascript)

En los campos con nombre, aunque ambos tengan dos valores int, los nombres ab y xy son distintos por lo que no se pueden asignar entre sí. Consulta el apartado siguiente para los campos posicionales.

2: Los alias en campos posicionales de la anotación de tipo son solo comentarios, no afectan la coincidencia de tipos

Los nombres asignados a los campos posicionales dentro de paréntesis son únicamente documentación para el desarrollador, el compilador los ignora. Si la estructura coincide, se permite la asignación:

(int a, int b) p1 = (10, 20);
(int x, int y) p2 = (30, 40);

// Válido, internamente son dos campos posicionales int, tipos idénticos
p1 = p2;Lenguaje del código: JavaScript (javascript)

Analogía con los parámetros de función: renombrar los parámetros no modifica la firma de la función, la lógica es equivalente.

IV. Leer los campos internos de un Record

El Record es inmutable, solo dispone de getters sin setters, no se pueden modificar los valores de sus campos.

  1. Campos con nombre: acceso directo mediante .nombreCampo
  2. Campos posicionales: acceso con $número; el número ignora todos los campos con nombre y solo cuenta el orden de los campos posicionales
var record = ('first', a: 2, b: true, 'last');

print(record.$1); // Primer campo posicional: first
print(record.a);  // Campo con nombre a: 2
print(record.b);  // Campo con nombre b: true
print(record.$2); // Segundo campo posicional: last
Lenguaje del código: PHP (php)

V. Reglas de identificación de tipo estructural del Record

El Record no requiere una definición de clase separada; el tipo se determina por su forma estructural:

Forma = número total de campos + tipo de cada campo + nombres de los campos con nombre

Si la forma es completamente idéntica, aunque provengan de archivos o librerías distintas, pertenecen al mismo tipo.

El compilador reconoce permanentemente el tipo estático de cada campo, no se pierde información de tipos:

// Posición 1 tipo num, posición 2 tipo Object
(num, Object) pair = (42, 'Un texto');

var firstVal = pair.$1; // Tipo estático num, en tiempo de ejecución int
var secondVal = pair.$2;// Tipo estático Object, en tiempo de ejecución StringLenguaje del código: JavaScript (javascript)

VI. Comparación de igualdad en Record ==

Se requieren dos condiciones para que dos Records sean iguales:

  1. Ambos Records tienen una forma estructural totalmente igual
  2. Todos los valores de los campos posicionales / con nombre correspondientes coinciden

El orden de escritura de los campos con nombre no influye en la comparación de igualdad; en cambio, nombres de campos diferentes suponen tipos distintos y por tanto siempre devuelven falso.

// Campos posicionales, misma forma y valores coincidentes → true
(int x, int y, int z) point = (1, 2, 3);
(int r, int g, int b) color = (1, 2, 3);
print(point == color); // true

// Nombres de campos con nombre distintos, tipos diferentes → false
({int x, int y, int z}) p = (x:1, y:2, z:3);
({int r, int g, int b}) c = (r:1, g:2, b:3);
print(p == c); // falseLenguaje del código: PHP (php)

El Record sobrescribe automáticamente el operador == y el método hashCode, no hace falta implementarlos manualmente.

VII. Múltiples valores de retorno en funciones

Las funciones Dart solo pueden devolver un único valor. Se usa Record para agrupar varios datos de tipos variados, junto con la desestructuración por patrones para separarlos rápidamente en variables independientes; es una alternativa perfecta a List/Map (sin pérdida de seguridad de tipos).

1 Desestructuración de campos posicionales

// Retorna: Nombre(String), Edad(int)
(String name, int age) getUserInfo(Map<String, dynamic> json) {
  return (json['name'] as String, json['age'] as int);
}

void main() {
  final userJson = {'name': 'Dash', 'age': 10, 'color': 'blue'};
  // Desestructuración directa, dos variables en una línea
  var (userName, userAge) = getUserInfo(userJson);
  print(userName); // Dash
  print(userAge);  // 10
}
Lenguaje del código: JavaScript (javascript)

2 Desestructuración de campos con nombre

Sintaxis :nombreCampo

({String name, int age}) getUserInfo(Map<String, dynamic> json) {
  return (name: json['name'] as String, age: json['age'] as int);
}

void main() {
  final userJson = {'name': 'Dash', 'age': 10};
  // Sintaxis de desestructuración nombrada
  final (:name, :age) = getUserInfo(userJson);
  print(name);
}
Lenguaje del código: JavaScript (javascript)

Desventajas de alternativas

En otros lenguajes como C++, C#, Java sin el uso de Record se requieren los siguientes pasos:

  1. Crear una clase nueva: código redundante, hay que escribir constructor y campos;
  2. Retornar mediante List: pérdida de seguridad de tipos, todo pasa a ser dynamic;
  3. Retornar mediante Map: riesgo de errores tipográficos en claves, sin comprobación estática de tipos.

VIII. Datos estructurados ligeros

Si solo necesitas almacenar datos sin métodos, usa directamente Record sin crear una clase nueva. Elimina el código redundante de definición de clase y es ideal para listas masivas de datos.

Ejemplo

Lista de configuración de botones de página (ejemplo de Flutter, se comprenderá tras aprender Flutter)

import 'package:flutter/material.dart';

void main() {
  final buttonList = [
    (
      label: "Subir archivo",
      icon: const Icon(Icons.upload_file),
      onPressed: () => print("Clic en botón de subir"),
    ),
    (
      label: "Ver detalles",
      icon: const Icon(Icons.info),
      onPressed: () => print("Clic en botón detalles"),
    )
  ];
}
Lenguaje del código: JavaScript (javascript)

IX. typedef: alias de tipo para Record

Simplificar tipos extensos

Las declaraciones de tipo de Record suelen ser muy largas. Usa typedef para definir un alias de tipo, facilita su reutilización y permite modificar uniformemente la estructura de campos en el futuro.

Uso básico
// Definir alias para Record de botón, onPressed admite valor null
typedef ButtonConfig = ({
  String label,
  Icon icon,
  void Function()? onPressed
});

// Declarar lista directamente con el alias
List<ButtonConfig> buttons = [
  (label: "Enviar", icon: const Icon(Icons.check), onPressed: () {}),
  (label: "Cancelar", icon: const Icon(Icons.close), onPressed: null),
];
Lenguaje del código: PHP (php)
Evolución posterior

Si en el futuro debes agregar métodos o encapsular lógica a los datos, no tienes que modificar el código de recorrido de negocio: solo reemplaza el tipo:

  1. Solución 1: sustituir por una clase normal
class ButtonConfig {
  final String label;
  final Icon icon;
  final void Function()? onPressed;
  ButtonConfig({required this.label, required this.icon, this.onPressed});
  // Agregar método personalizado
  bool get hasClickEvent => onPressed != null;
}
Lenguaje del código: PHP (php)
  1. Solución 2: envolver el Record original mediante Extension Type
extension type ButtonConfig._(({String label, Icon icon, void Function()? onPressed}) _) {
  String get label => _.label;
  Icon get icon => _.icon;
  void Function()? get onPressed => _.onPressed;

  ButtonConfig({required String label, required Icon icon, void Function()? onPressed})
      : this._((label: label, icon: icon, onPressed: onPressed));

  bool get hasClickEvent => _.onPressed != null;
}
Lenguaje del código: PHP (php)

Con ambos métodos de migración, el código de negocio que recorre y renderiza botones en la página permanece intacto.

X. Resumen

  1. Requisito de versión: Dart 3.0 o superior;
  2. Tres propiedades centrales: anónimo, inmutable, agregado; tamaño fijo, almacenamiento heterogéneo, tipado fuerte;
  3. Dos tipos de campos: campos posicionales $1/$2, campos con nombre .nombre;
  4. Regla de identificación de tipos: el tipo depende de la forma estructural, los nombres de campos con nombre participan en la forma; los alias de campos posicionales son solo comentarios;
  5. Comparación de igualdad: forma idéntica + todos los valores de campos coincidentes → true;
  6. Ventaja principal: múltiples valores de retorno en funciones, contenedor ligero de datos, seguridad completa de tipos;
  7. Ampliación posible: typedef para simplificar alias de tipo, migración transparente a class / extension type en el futuro.

Deja una respuesta

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