En muchos lenguajes, el error más común es intentar acceder a algo que no existe, lanzando un temido NullPointerException. Dart soluciona esto con el Sound Null Safety [disponible desde Dart 2.12]. Cuando decimos que el sistema de tipos es “sound” (robusto), nos referimos a que si declaras una variable como String, el compilador te garantiza que, en tiempo de ejecución, esa variable nunca contendrá un null. No es una suposición; es una certeza matemática que permite al compilador optimizar el código.
Para lograr esto, Dart divide el mundo de los tipos en dos: los tipos non-nullable (no nulos), que deben tener siempre un valor, y los tipos nullable (nulos), que se declaran añadiendo un signo de interrogación ? al final (por ejemplo, String?). Si intentas asignar null a una variable String, el código ni siquiera compilará. Sin embargo, si intentas usar un valor de tipo String? (que podría ser nulo) como si fuera un String normal, el compilador te detendrá. La única forma de saltarse esta protección es usando el operador de aserción forzada ! (el “bang operator”), el cual le dice al compilador: “confía en mí, sé que no es nulo”. Pero ojo: si te equivocas y usas ! en un valor que sí es null, el programa fallará en ejecución.
Este mecanismo funciona gracias al flow analysis (análisis de flujo). El compilador no solo mira el tipo de la variable, sino que “sigue” el camino que recorre tu código. Si en una línea haces un chequeo de if (variable != null), el compilador es lo suficientemente inteligente como para saber que, dentro de ese bloque, la variable ya no es String?, sino que puede ser tratada como un String seguro.
En la jerarquía de tipos, tenemos a Object? como el gran padre de todos, ya que es el supertipo de cualquier valor, incluyendo null. Por otro lado, Object es el padre de todos los valores que no pueden ser nulos. Finalmente, existe el tipo Never, que representa un valor que nunca se devuelve (como una función que siempre lanza una excepción o un bucle infinito), situándose en la base de la jerarquía.
void main() {
// 1. Variables no nulas (Non-nullable)
// No puedes asignar null aquí; el compilador te lo impide.
String nombre = "Elena";
// 2. Variables que aceptan nulos (Nullable)
// Usamos '?' para indicar que 'id' puede ser un String o ser null.
String? idUsuario;
print("--- Perfil de usuario ---");
imprimirPerfil(nombre, idUsuario);
print("\n--- Perfil con ID ---");
// Asignamos un valor al nullable
idUsuario = "USR-9921";
imprimirPerfil(nombre, idUsuario);
}
void imprimirPerfil(String nombre, String? id) {
// Aquí 'nombre' es String (garantizado)
print("Nombre: $nombre");
// El 'flow analysis' permite la promoción de tipos.
// Al hacer este chequeo, Dart sabe que dentro del 'if', 'id' no es null.
if (id != null) {
// Aquí 'id' se trata como String, no como String?
print("ID de usuario: ${id.length} caracteres");
print("ID: $id");
} else {
print("ID: No asignado");
}
}
Análisis del ejemplo
En el código anterior, observa cómo nombre es de tipo String. Esto significa que el compilador ha reservado espacio y reglas para asegurar que siempre habrá una cadena de texto allí.
Cuando llamamos a imprimirPerfil(nombre, idUsuario), la función recibe dos tipos distintos. nombre es String, mientras que id es String?. Dentro de la función imprimirPerfil, el compilador aplica el flow analysis. En la línea if (id != null), el motor de Dart analiza el flujo lógico: si la condición se cumple, el valor de id ya no puede ser null. Por eso, en la siguiente línea, puedes llamar a id.length sin errores; el compilador ha “promovido” el tipo de String? a String temporalmente para ese bloque.
Si idUsuario fuera de tipo Object? en lugar de String?, no podrías acceder a .length directamente, porque Object? es demasiado genérico y no garantiza que el objeto tenga una propiedad llamada length.
El error frecuente
El error más peligroso ocurre cuando intentas “engañar” al sistema de tipos usando el operador !. Es una práctica común cuando el programador está seguro de la lógica, pero el compilador no puede probarla mediante análisis de flujo.
void errorDeSeguridad() {
String? nombreOpcional = null;
// El compilador confía en ti por usar '!', pero te mientes a ti mismo.
// Esto lanzará un error en tiempo de ejecución (runtime error).
print(nombreOpcional!.length);
}
En este caso, el código compila perfectamente, pero al ejecutarse, la máquina virtual de Dart lanzará una excepción porque intentaste forzar la existencia de un valor donde hay un null.
N° 10