Patrones en Dart 3: Desestructuración y Matching Unificado

Los patterns [disponible desde Dart 3.0] son una forma avanzada de inspeccionar la estructura de un valor para decidir qué hacer con él. En lugar de usar una serie de comprobaciones manuales con is y luego hacer un cast (lo que llamamos type casting), los patrones permiten hacer dos cosas simultáneamente: verificar si un valor tiene la forma esperada (matching) y extraer sus componentes internos en variables locales (destructuring).

Funciona de esta manera porque el motor de Dart ha unificado el concepto de “forma” para diferentes tipos de datos. Antes, para extraer datos de un objeto o una lista, tenías que acceder a sus propiedades o índices uno por uno. Ahora, un patrón actúa como un molde: si el valor “encaja” en el molde, el compilador automáticamente “descompone” el objeto y te entrega sus piezas.

Deberías usar patrones siempre que trabajes con estructuras compuestas como Records, Lists, Maps o instancias de clases propias, y cuando necesites lógica condicional basada en la forma de los datos (por ejemplo, “si es una lista de dos enteros, haz X”). También son ideales para limpiar el ruido visual de los if (x is Type) seguidos de var y = x as Type.

Si los usas de forma incorrecta, especialmente en un switch expression, lo más probable es que el compilador te detenga con un error de exhaustividad (exhaustiveness error). Un switch expression debe cubrir absolutamente todos los casos posibles del tipo de dato que estás evaluando. Si dejas un hueco, el código no compilará.

class Alarma {
  final String mensaje;
  final int nivel;
  Alarma(this.mensaje, this.nivel);
}

void main() {
  // Una colección heterogénea para demostrar la versatilidad
  final eventos = <Object?>[
    (1, 'Sonda_A', 35.5),    // Un Record
    [10, 20],                // Una List
    Alarma('Incendio', 5),   // Un objeto de clase personalizada
    "Error de sistema",      // Un String
    150,                    // Un Int (para probar patrones relacionales)
    42,                    // Un Int
    null,                   // Un valor nulo
  ];

  for (final e in eventos) {
    print(procesarEvento(e));
  }
}

String procesarEvento(Object? evento) {
  // El switch expression requiere exhaustividad total
  return switch (evento) {
    // 1. Record pattern + Guard clause (when)
    (int id, String nombre, double temp) when temp > 30.0 => 
        '¡Alerta en $nombre (ID: $id)! Calor extremo: $temp°C',
    
    // 2. Record pattern simple (matching de la forma)
    (int id, String nombre, double temp) => 
        'Sensor $nombre ($id) reporta $temp°C',

    // 3. List pattern (desestructuración de posición)
    [int primero, int segundo] => 'Coordenadas: $primero y $segundo',

    // 4. Object pattern (desestructurando propiedades de una clase)
    Alarma(mensaje: var msg, nivel: var n) when n > 3 => '¡CRÍTICO! $msg (Nivel $n)',
    Alarma(mensaje: var msg) => 'Aviso: $msg',

    // 5. Type pattern + Relational pattern (dentro de una guarda)
    int i when i > 100 => 'Valor numérico muy alto: $i',
    int i => 'Número entero: $i',

    // 6. String pattern (Type pattern)
    String s => 'Mensaje de texto: $s',

    // 7. Null-check pattern (disponible desde Dart 3.0)
    null => 'No se recibió ningún dato (nulo)',

    // 8. Wildcard pattern (el comodín que garantiza la exhaustividad)
    _ => 'Evento de tipo desconocido',
  };
}

Desglose del ejemplo

En el switch expression de procesarEvento, hemos aplicado varios conceptos clave:

  • Record Pattern: En la primera línea, (int id, String nombre, double temp) no solo verifica que evento sea un Record con tres elementos, sino que también extrae sus valores y los vincula a las variables id, nombre y temp. Si el primer elemento no fuera un int, el patrón fallaría y pasaría al siguiente caso.
  • Object Pattern: Cuando tratamos con la clase Alarma, usamos Alarma(mensaje: var msg, nivel: var n). Aquí, el patrón busca propiedades con esos nombres específicos y usa Variable Patterns (var msg) para asignar el contenido de la propiedad a una nueva variable local disponible en esa rama del switch.
  • List Pattern: La sintaxis [int primero, int segundo] es extremadamente potente. Solo coincidirá si la lista tiene exactamente dos elementos y ambos son enteros. Si la lista tuviera tres elementos, este patrón no coincidiría.
  • Relational & Guard Clauses: Aunque los patrones pueden ser relacionales (como > 100), en un switch sobre Object, primero debemos asegurar el tipo con un int i. La cláusula when (guard clause) permite añadir condiciones lógicas adicionales que el matching de forma por sí solo no puede expresar.
  • Wildcard (_): Este es el “comodín”. Como evento es de tipo Object?, el compilador nos obliga a considerar qué pasaría si llega algo que no cumple ninguna de las reglas anteriores. Sin el _, el código no compilaría.

El error frecuente

Un error clásico al empezar con patrones es confundir un Variable Pattern con un Constant Pattern dentro de un switch statement (no expression).

// ERROR SUBTIL
var x = 10;
switch (x) {
  case var y: // Esto es un Variable Pattern. Siempre coincide y vincula x a y.
    print('Esto siempre se ejecutará');
  case 10:   // Este caso es inalcanzable (unreachable code)
    print('Esto nunca se ejecutará');
}

En el código de arriba, case var y: no está preguntando “¿Es x igual a una variable llamada y?”. Está diciendo: “Cualquier valor que sea, asígnale ese valor a una nueva variable y“. Como cualquier valor siempre puede ser asignado a una variable, el primer caso actúa como un “atrapa-todo”, haciendo que los casos siguientes sean código muerto. Si tu intención es comparar contra un valor constante, usa el valor directamente o una constante definida.

85

Dejar un comentario

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

Scroll al inicio