Saltar al contenido principal

Registros

Resumen de la estructura de datos record (registro) en Dart.

Los records son un tipo agregado, inmutable y anónimo. Al igual que otros tipos de colección, te permiten agrupar múltiples objetos en un solo objeto. A diferencia de otros tipos de colección, los records tienen un tamaño fijo, son heterogéneos y tienen tipo.

Los records son valores reales; puedes almacenarlos en variables, anidarlos, pasarlos hacia y desde funciones, y almacenarlos en estructuras de datos como listas, mapas y sets.

Sintaxis de records (registros)

#

Las expresiones de records (records expressions) son listas delimitadas por comas de campos nombrados o posicionales, entre paréntesis:

dart
var record = ('first', a: 2, b: true, 'last');

Las anotaciones de tipo record (record type annotations) son listas de tipos delimitadas por comas entre paréntesis. Puedes usar anotaciones de tipo record para definir tipos de retorno y tipos de parámetros. Por ejemplo, las siguientes declaraciones (int, int) son anotaciones de tipo record:

dart
(int, int) swap((int, int) record) {
  var (a, b) = record;
  return (b, a);
}

Los campos en las expresiones de record y las anotaciones de tipo reflejan cómo funcionan los parámetros y argumentos en las funciones. Los campos posicionales van directamente dentro de los paréntesis:

dart
// Record type annotation in a variable declaration:
(String, int) record;

// Initialize it with a record expression:
record = ('A string', 123);

En una anotación de tipo record, los campos nombrados van dentro de una sección delimitada por llaves de pares tipo-y-nombre, después de todos los campos posicionales. En una expresión de record, los nombres van antes de cada valor de campo con dos puntos después:

dart
// Record type annotation in a variable declaration:
({int a, bool b}) record;

// Initialize it with a record expression:
record = (a: 123, b: true);

Los nombres de los campos nombrados en un tipo record son parte de la definición de tipo del record, o su forma (shape). Dos records con campos nombrados con diferentes nombres tienen tipos diferentes:

dart
({int a, int b}) recordAB = (a: 1, b: 2);
({int x, int y}) recordXY = (x: 3, y: 4);

// Compile error! These records don't have the same type.
// recordAB = recordXY;

En una anotación de tipo record, también puedes nombrar los campos posicionales, pero estos nombres son puramente para documentación y no afectan al tipo del record:

dart
(int a, int b) recordAB = (1, 2);
(int x, int y) recordXY = (3, 4);

recordAB = recordXY; // OK.

Esto es similar a cómo los parámetros posicionales en una declaración de función o typedef de función pueden tener nombres, pero esos nombres no afectan a la firma de la función.

Para obtener más información y ejemplos, consulta Tipos de record e Igualdad de records.

Campos del record

#

Los campos del record son accesibles a través de getters integrados. Los records son inmutables, por lo que los campos no tienen setters.

Los campos nombrados exponen getters del mismo nombre. Los campos posicionales exponen getters con el nombre $<position>, omitiendo los campos nombrados:

dart
var record = ('first', a: 2, b: true, 'last');

print(record.$1); // Prints 'first'
print(record.a); // Prints 2
print(record.b); // Prints true
print(record.$2); // Prints 'last'

Para simplificar aún más el acceso a los campos del record, consulta la página sobre Patrones (Patterns).

Tipos de record

#

No hay declaración de tipo para tipos de record individuales. Los records tienen tipos estructurales basados en los tipos de sus campos. La forma (shape) de un record (el conjunto de sus campos, los tipos de los campos y sus nombres, si los hay) determina de manera única el tipo de un record.

Cada campo en un record tiene su propio tipo. Los tipos de campo pueden diferir dentro del mismo record. El sistema de tipos conoce el tipo de cada campo dondequiera que se acceda a él desde el record:

dart
(num, Object) pair = (42, 'a');

var first = pair.$1; // Static type `num`, runtime type `int`.
var second = pair.$2; // Static type `Object`, runtime type `String`.

Considera dos bibliotecas no relacionadas que crean records con el mismo conjunto de campos. El sistema de tipos entiende que esos records son del mismo tipo, aunque las bibliotecas no estén acopladas entre sí.

Igualdad de records

#

Dos records son iguales si tienen la misma forma (shape) (conjunto de campos), y sus campos correspondientes tienen los mismos valores. Dado que el orden de los campos nombrados no es parte de la forma de un record, el orden de los campos nombrados no afecta a la igualdad.

Por ejemplo:

dart
(int x, int y, int z) point = (1, 2, 3);
(int r, int g, int b) color = (1, 2, 3);

print(point == color); // Prints 'true'.
dart
({int x, int y, int z}) point = (x: 1, y: 2, z: 3);
({int r, int g, int b}) color = (r: 1, g: 2, b: 3);

print(point == color); // Prints 'false'. Lint: Equals on unrelated types.

Los records definen automáticamente los métodos hashCode y == basados en la estructura de sus campos.

Retornos múltiples

#

Los records permiten que las funciones devuelvan múltiples valores agrupados. Para recuperar los valores del record de un retorno, desestructura (destructure) los valores en variables locales usando coincidencia de patrones (pattern matching).

dart
// Returns multiple values in a record:
(String name, int age) userInfo(Map<String, dynamic> json) {
  return (json['name'] as String, json['age'] as int);
}

final json = <String, dynamic>{'name': 'Dash', 'age': 10, 'color': 'blue'};

// Destructures using a record pattern with positional fields:
var (name, age) = userInfo(json);

/* Equivalent to:
  var info = userInfo(json);
  var name = info.$1;
  var age  = info.$2;
*/

También puedes desestructurar un record usando sus campos nombrados, usando la sintaxis de dos puntos :, sobre la cual puedes leer más en la página de Tipos de patrones (Pattern types):

dart
({String name, int age}) userInfo(Map<String, dynamic> json)
// ···
// Destructures using a record pattern with named fields:
final (:name, :age) = userInfo(json);

Puedes devolver múltiples valores desde una función sin usar records, pero otros métodos tienen desventajas. Por ejemplo, crear una clase es mucho más verboso y usar otros tipos de colección como List o Map pierde la seguridad de tipos (type safety).

Records como estructuras de datos simples

#

Los records solo contienen datos. Cuando eso es todo lo que necesitas, están disponibles de inmediato y son fáciles de usar sin necesidad de declarar nuevas clases. Para una lista simple de tuplas de datos que tienen todas la misma forma, una lista de records es la representación más directa.

Toma esta lista de "definiciones de botones", por ejemplo:

dart
final buttons = [
  (
    label: "Button I",
    icon: const Icon(Icons.upload_file),
    onPressed: () => print("Action -> Button I"),
  ),
  (
    label: "Button II",
    icon: const Icon(Icons.info),
    onPressed: () => print("Action -> Button II"),
  )
];

Este código se puede escribir directamente sin necesidad de declaraciones adicionales.

Records y typedefs

#

Puedes optar por usar typedefs para darle un nombre al propio tipo record, y usarlo en lugar de escribir el tipo record completo. Este método te permite indicar que algunos campos pueden ser nulos (?), incluso si ninguna de las entradas actuales en la lista tiene un valor nulo.

dart
typedef ButtonItem = ({String label, Icon icon, void Function()? onPressed});
final List<ButtonItem> buttons = [
  // ...
];

Debido a que los tipos de record son tipos estructurales, asignar un nombre como ButtonItem solo introduce un alias que facilita hacer referencia al tipo estructural: ({String label, Icon icon, void Function()? onPressed}).

Hacer que todo tu código haga referencia a un tipo de record por su alias facilita cambiar más adelante la implementación del record sin necesidad de actualizar cada referencia.

El código puede funcionar con las definiciones de botones dadas de la misma manera que lo haría con instancias de clases simples:

dart
List<Container> widget = [
  for (var button in buttons)
    Container(
      margin: const EdgeInsets.all(4.0),
      child: OutlinedButton.icon(
        onPressed: button.onPressed,
        icon: button.icon,
        label: Text(button.label),
      ),
    ),
];

Incluso podrías decidir cambiar más adelante el tipo record a un tipo clase para añadir métodos:

dart
class ButtonItem {
  final String label;
  final Icon icon;
  final void Function()? onPressed;
  ButtonItem({required this.label, required this.icon, this.onPressed});
  bool get hasOnPressed => onPressed != null;
}

O a un tipo de extensión (extension type):

dart
extension type ButtonItem._(({String label, Icon icon, void Function()? onPressed}) _) {
  String get label => _.label;
  Icon get icon => _.icon;
  void Function()? get onPressed => _.onPressed;
  ButtonItem({required String label, required Icon icon, void Function()? onPressed})
      : this._((label: label, icon: icon, onPressed: onPressed));
  bool get hasOnPressed => _.onPressed != null;
}

Y luego crear la lista de definiciones de botones usando los constructores de ese tipo:

dart
final List<ButtonItem> buttons =  [
  ButtonItem(
    label: "Button I",
    icon: const Icon(Icons.upload_file),
    onPressed: () => print("Action -> Button I"),
  ),
  ButtonItem(
    label: "Button II",
    icon: const Icon(Icons.info),
    onPressed: () => print("Action -> Button II"),
  )
];

De nuevo, todo ello sin necesidad de cambiar el código que utiliza esa lista.

Cambiar cualquier tipo sí requiere que el código que lo usa sea muy cuidadoso al no hacer suposiciones. Un alias de tipo no ofrece ninguna protección ni garantía, para el código que lo usa como referencia, de que el valor alias sea un record. Los tipos de extensión (extension types) también ofrecen poca protección. Solo una clase puede proporcionar una abstracción y encapsulación completas.