Análisis de flujo y promoción de tipos en Dart

El análisis de flujo (flow analysis) [disponible desde Dart 2.12] es el mecanismo interno que utiliza el compilador para rastrear la nulidad de una variable a lo largo de los distintos caminos de ejecución de tu código. En lugar de obligarte a usar el operador de aserción ! de forma constante, el compilador observa las decisiones que toma tu programa y, si detecta que una variable no puede ser null en un punto específico, realiza lo que conocemos como promoción de tipos (type promotion). Es decir, “estrecha” el tipo de una variable de un tipo nullable (ej. String?) a su versión non-nullable (ej. String).

Este proceso ocurre porque el compilador es capaz de entender la lógica de control: si has verificado que un objeto no es nulo mediante un if, o si has comprobado su tipo mediante is, el compilador garantiza que, en esa rama de ejecución, el valor es seguro. Debes usar esto siempre que quieras escribir código limpio y seguro, evitando la verbosidad de los checks repetitivos. Sin embargo, si intentas forzar la promoción en situaciones donde el compilador no puede garantizar la inmutabilidad de la variable (como ocurre con los campos de una clase), el programa simplemente no compilará por seguridad, o podrías introducir errores lógicos si intentas saltarte las advertencias con el operador !.

class ProcesadorDeTexto {
  // Un campo de instancia nullable
  String? _contenido;

  void configurar(String? nuevoContenido) {
    _contenido = nuevoContenido;
  }

  void ejecutar() {
    print('--- Iniciando proceso ---');

    // 1. Caso de éxito: Promoción por comprobación de nulidad
    if (_contenido != null) {
      // Fíjate que no podemos usar _contenido.length directamente aquí 
      // porque _contenido es un campo de instancia y podría cambiar.
      // Para solucionarlo, usamos una variable local (snapshot).
      final contenidoLocal = _contenido; 
      if (contenidoLocal != null) {
        // 'contenidoLocal' ha sido promovido a 'String' (non-nullable)
        print('Contenido local length: ${contenidoLocal.length}');
      }
    }

    // 2. Caso de éxito: Promoción por tipo (is)
    Object entrada = "Datos de prueba";
    if (entrada is String) {
      // 'entrada' ha sido promovido de 'Object' a 'String'
      print('La entrada es un string: ${entrada.toUpperCase()}');
    }

    // 3. Caso de éxito: Early return
    String? nombre = _obtenerNombre();
    if (nombre == null) {
      print('No hay nombre, abortando...');
      return; // Al salir de la función, el flujo se corta
    }
    // A partir de aquí, 'nombre' ha sido promovido a 'String' por el return previo
    print('Nombre procesado: ${nombre.length}');

    // 4. Uso de la aserción explícita (!)
    // Le decimos al compilador: "Confía en mí, sé que esto no es null"
    // Si llegamos aquí y es null, lanzará una RuntimeError.
    print('Longitud forzada: ${_contenido?.length ?? 0}');
  }

  String? _obtenerNombre() => "Dart Developer";
}

void main() {
  final procesador = ProcesadorDeTexto();
  procesador.configurar("Hola Mundo");
  procesador.ejecutar();
}

En el ejemplo anterior, observa cómo el análisis de flujo actúa de forma distinta según el ámbito de la variable. En la clase ProcesadorDeTexto, el campo _contenido es un miembro de la clase. Aunque hagamos un if (_contenido != null), el compilador de Dart es conservador y no lo promociona automáticamente a un tipo no-nulo. Esto se debe a que, en un entorno complejo, una llamada a un método o incluso una ejecución asíncrona podría modificar el valor de _contenido justo después del check pero antes de usarlo.

Para mitigar esto, realizamos una técnica común: asignamos _contenido a una variable local llamada contenidoLocal. Al ser una variable local, el compilador tiene la certeza de que nada externo puede cambiar su valor entre la comprobación y el uso, permitiendo la promoción de tipos dentro del bloque if. Por otro lado, en el caso de la variable entrada, el operador is no solo verifica el tipo, sino que reescribe el tipo en el flujo de control, permitiéndonos llamar a métodos como toUpperCase() sin necesidad de castear o usar !. Finalmente, el early return es una de las formas más elegantes de realizar promociones: al invalidar la rama donde la variable es nula, el resto de la función queda protegida por el sistema de tipos.

El error frecuente

class Servidor {
  String? host;

  void conectar() {
    if (host != null) {
      // ERROR: El compilador no promociona 'host' porque es un campo de clase.
      // Un método externo o un hilo podría cambiar 'host' a null justo aquí.
      // print(host.length); 
      
      // SOLUCIÓN:
      final hostLocal = host;
      if (hostLocal != null) {
        print(hostLocal.length); // Ahora sí es seguro.
      }
    }
  }
}

Este error ocurre porque el compilador de Dart prioriza la seguridad de tipos sobre la conveniencia. En aplicaciones con mucha concurrencia o lógica compleja de subclases, confiar en que un campo de instancia no cambiará entre un if y su uso es un riesgo de ejecución.

29

Dejar un comentario

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

Scroll al inicio