Saltar al contenido principal

Patrones

Resumen de los patrones en Dart.

Los patrones son una categoría sintáctica en el lenguaje Dart, al igual que las sentencias y expresiones. Un patrón representa la forma de un conjunto de valores con los que podría coincidir frente a valores reales.

Esta página describe:

  • Qué hacen los patrones.
  • Dónde se permiten los patrones en el código Dart.
  • Cuáles son los casos de uso comunes para los patrones.

Para conocer los diferentes tipos de patrones, visita la página de tipos de patrones (pattern types).

Qué hacen los patrones

#

En general, un patrón puede coincidir (match) con un valor, desestructurar (destructure) un valor, o ambos, según el contexto y la forma del patrón.

Primero, la coincidencia de patrones (pattern matching) te permite comprobar si un valor determinado:

  • Tiene una forma determinada.
  • Es una constante determinada.
  • Es igual a otra cosa.
  • Tiene un tipo determinado.

Luego, la desestructuración de patrones (pattern destructuring) te proporciona una sintaxis declarativa conveniente para descomponer ese valor en sus partes constituyentes. El mismo patrón también puede permitirte vincular variables a algunas o a todas esas partes en el proceso.

Coincidencia

#

Un patrón siempre realiza una comprobación con un valor para determinar si el valor tiene la forma esperada. En otras palabras, compruebas si el valor coincide (matches) con el patrón.

Lo que constituye una coincidencia depende de qué tipo de patrón estés usando. Por ejemplo, un patrón constante coincide si el valor es igual a la constante del patrón:

dart
switch (number) {
  // Constant pattern matches if 1 == number.
  case 1:
    print('one');
}

Muchos patrones utilizan subpatrones, a veces denominados patrones externos e internos, respectivamente. Los patrones coinciden recursivamente en sus subpatrones. Por ejemplo, los campos individuales de cualquier patrón de tipo colección (collection-type) podrían ser patrones de variable o patrones constantes:

dart
const a = 'a';
const b = 'b';
switch (obj) {
  // List pattern [a, b] matches obj first if obj is a list with two fields,
  // then if its fields match the constant subpatterns 'a' and 'b'.
  case [a, b]:
    print('$a, $b');
}

Para ignorar partes de un valor coincidente, puedes usar un patrón comodín (wildcard pattern) como marcador de posición. En el caso de los patrones de lista, puedes usar un elemento restante (rest element).

Desestructuración (Destructuring)

#

Cuando un objeto y un patrón coinciden, el patrón puede acceder a los datos del objeto y extraerlos por partes. En otras palabras, el patrón desestructura (destructures) el objeto:

dart
var numList = [1, 2, 3];
// List pattern [a, b, c] destructures the three elements from numList...
var [a, b, c] = numList;
// ...and assigns them to new variables.
print(a + b + c);

Puedes anidar cualquier tipo de patrón dentro de un patrón de desestructuración. Por ejemplo, este patrón de caso coincide y desestructura una lista de dos elementos cuyo primer elemento es 'a' o 'b':

dart
switch (list) {
  case ['a' || 'b', var c]:
    print(c);
}

Lugares donde pueden aparecer los patrones

#

Puedes usar patrones en varios lugares en el lenguaje Dart:

Esta sección describe casos de uso comunes para la coincidencia y la desestructuración con patrones.

Declaración de variables

#

Puedes usar una declaración de variable de patrón (pattern variable declaration) en cualquier lugar donde Dart permita la declaración de variables locales. El patrón coincide con el valor a la derecha de la declaración. Una vez que coincide, desestructura el valor y lo vincula a nuevas variables locales:

dart
// Declares new variables a, b, and c.
var (a, [b, c]) = ('str', [1, 2]);

Una declaración de variable de patrón debe comenzar con var o final, seguida por un patrón.

Asignación de variables

#

Un patrón de asignación de variable (variable assignment pattern) se sitúa en el lado izquierdo de una asignación. Primero, desestructura el objeto coincidente. Luego asigna los valores a variables existentes, en lugar de vincular otras nuevas.

Usa un patrón de asignación de variable para intercambiar los valores de dos variables sin declarar una tercera variable temporal:

dart
var (a, b) = ('left', 'right');
(b, a) = (a, b); // Swap.
print('$a $b'); // Prints "right left".

Sentencias y expresiones switch (Switch statements and expressions)

#

Cada cláusula case contiene un patrón. Esto se aplica tanto a las sentencias switch y expresiones switch, como a las sentencias if-case. Puedes usar cualquier tipo de patrón en un case.

Los patrones de caso (case patterns) son refutablesPatrón refutableUn patrón que se puede probar contra un valor. Más información. Permiten que el flujo de control haga una de las siguientes opciones:

  • Coincidir y desestructurar el objeto sobre el que se realiza el switch.
  • Continuar la ejecución si el objeto no coincide.

Los valores que un patrón desestructura en un case se convierten en variables locales. Su alcance es solo dentro del cuerpo de ese case.

dart
switch (obj) {
  // Matches if 1 == obj.
  case 1:
    print('one');

  // Matches if the value of obj is between the
  // constant values of 'first' and 'last'.
  case >= first && <= last:
    print('in range');

  // Matches if obj is a record with two fields,
  // then assigns the fields to 'a' and 'b'.
  case (var a, var b):
    print('a = $a, b = $b');

  default:
}

Los patrones OR lógicos (logical-or) son útiles para que múltiples casos compartan un cuerpo en expresiones o sentencias switch:

dart
var isPrimary = switch (color) {
  Color.red || Color.yellow || Color.blue => true,
  _ => false,
};

Las sentencias switch pueden tener múltiples casos que compartan un cuerpo sin usar patrones OR lógicos, pero estos siguen siendo especialmente útiles para permitir que múltiples casos compartan una guarda (guard):

dart
switch (shape) {
  case Square(size: var s) || Circle(size: var s) when s > 0:
    print('Non-empty symmetric shape');
}

Las cláusulas de guarda (guard clauses) evalúan una condición arbitraria como parte de un case, sin salir del switch si la condición es falsa (como causaría usar una sentencia if en el cuerpo del case).

dart
switch (pair) {
  case (int a, int b):
    if (a > b) print('First element greater');
  // If false, prints nothing and exits the switch.
  case (int a, int b) when a > b:
    // If false, prints nothing but proceeds to next case.
    print('First element greater');
  case (int a, int b):
    print('First element not greater');
}

Bucles for y for-in

#

Puedes usar patrones en bucles for y for-in para iterar y desestructurar valores en una colección.

Este ejemplo utiliza la desestructuración de objetos en un bucle for-in para desestructurar los objetos MapEntry que devuelve una llamada a <Map>.entries:

dart
Map<String, int> hist = {'a': 23, 'b': 100};

for (var MapEntry(key: key, value: count) in hist.entries) {
  print('$key occurred $count times');
}

El patrón de objeto comprueba que hist.entries tenga el tipo con nombre MapEntry, y luego se introduce recursivamente en los subpatrones de campo con nombre key y value. Llama al getter key y al getter value en el MapEntry en cada iteración, y vincula los resultados a las variables locales key y count, respectivamente.

Vincular el resultado de una llamada a un getter a una variable del mismo nombre es un caso de uso común, por lo que los patrones de objeto también pueden inferir el nombre del getter a partir del subpatrón de variable. Esto te permite simplificar el patrón de variable de algo redundante como key: key a simplemente :key:

dart
for (var MapEntry(:key, value: count) in hist.entries) {
  print('$key occurred $count times');
}

Casos de uso para patrones

#

La sección anterior describe cómo encajan los patrones en otros constructos de código de Dart. Viste algunos casos de uso interesantes como ejemplos, como intercambiar los valores de dos variables, o desestructurar pares clave-valor en un mapa. Esta sección describe aún más casos de uso, respondiendo a:

  • Cuándo y por qué podrías querer usar patrones.
  • Qué tipo de problemas resuelven.
  • A qué modismos se adaptan mejor.

Desestructurar múltiples retornos (Destructuring multiple returns)

#

Los registros (records) permiten agregar y retornar múltiples valores desde una sola llamada a función. Los patrones añaden la capacidad de desestructurar los campos de un registro directamente en variables locales, en línea con la llamada a la función.

En lugar de declarar individualmente nuevas variables locales para cada campo del registro, así:

dart
var info = userInfo(json);
var name = info.$1;
var age = info.$2;

Puedes desestructurar los campos de un registro que devuelve una función en variables locales usando una declaración de variable o patrón de asignación, y un patrón de registro (record pattern) como su subpatrón:

dart
var (name, age) = userInfo(json);

Para desestructurar un registro con campos nombrados usando un patrón:

dart
final (:name, :age) =
    getData(); // For example, return (name: 'doug', age: 25);

Desestructurar instancias de clases

#

Los patrones de objeto (object patterns) coinciden con tipos de objetos con nombre, lo que te permite desestructurar sus datos utilizando los getters que la clase del objeto ya expone.

Para desestructurar una instancia de una clase, usa el tipo con nombre, seguido de las propiedades a desestructurar entre paréntesis:

dart
final Foo myFoo = Foo(one: 'one', two: 2);
var Foo(:one, :two) = myFoo;
print('one $one, two $two');

Tipos de datos algebraicos (Algebraic data types)

#

La desestructuración de objetos y los switch cases conducen a escribir código al estilo de un tipo de datos algebraico (algebraic data type). Usa este método cuando:

  • Tienes una familia de tipos relacionados.
  • Tienes una operación que necesita un comportamiento específico para cada tipo.
  • Desees agrupar ese comportamiento en un solo lugar en lugar de distribuirlo en todas las definiciones de tipo diferentes.

En lugar de implementar la operación como un método de instancia para cada tipo, mantén las variaciones de la operación en una sola función que realice un switch sobre los subtipos:

dart
sealed class Shape {}

class Square implements Shape {
  final double length;
  Square(this.length);
}

class Circle implements Shape {
  final double radius;
  Circle(this.radius);
}

double calculateArea(Shape shape) => switch (shape) {
  Square(length: var l) => l * l,
  Circle(radius: var r) => math.pi * r * r,
};

Validar JSON entrante

#

Los patrones Map y list funcionan bien para desestructurar pares clave-valor en datos deserializados, como datos analizados desde JSON:

dart
var data = {
  'user': ['Lily', 13],
};
var {'user': [name, age]} = data;

Si sabes que los datos JSON tienen la estructura esperada, el ejemplo anterior es realista. Pero los datos normalmente provienen de una fuente externa, como a través de la red. Primero debes validarlos para confirmar su estructura.

Sin patrones, la validación es muy verbosa:

dart
if (data is Map<String, Object?> &&
    data.length == 1 &&
    data.containsKey('user')) {
  var user = data['user'];
  if (user is List<Object> &&
      user.length == 2 &&
      user[0] is String &&
      user[1] is int) {
    var name = user[0] as String;
    var age = user[1] as int;
    print('User $name is $age years old.');
  }
}

Un solo patrón de caso (case pattern) puede lograr la misma validación. Los casos únicos funcionan mejor como sentencias if-case. Los patrones proporcionan un método más declarativo y mucho menos verboso para validar JSON:

dart
if (data case {'user': [String name, int age]}) {
  print('User $name is $age years old.');
}

Este patrón de caso valida simultáneamente que:

  • json is a map, because it must first match the outer patrón de mapa (map pattern) to proceed.
    • Y, dado que es un mapa, también confirma que json no es nulo.
  • json contiene una clave user.
  • La clave user se empareja con una lista de dos valores.
  • Los tipos de los valores de la lista son String y int.
  • Las nuevas variables locales para almacenar los valores son name y age.