Un constructor de redirección es aquel que, en lugar de ejecutar un bloque de cuerpo para inicializar sus campos, delega la responsabilidad de la creación a otro constructor de la misma clase mediante el uso de this. Esto es fundamental para mantener el código seco (DRY), permitiéndote tener múltiples formas de instanciar un objeto sin repetir la lógica de asignación de parámetros.
Por otro lado, los constructores const [disponible desde Dart 2.0] permiten la creación de instancias inmutables en tiempo de compilación. Esto habilita una optimización crítica de la VM llamada canonicalización: cuando creas dos instancias const con los mismos valores, Dart no reserva dos espacios de memoria distintos, sino que ambas variables apuntan exactamente a la misma dirección de memoria. Esto reduce el uso de memoria y acelera la comparación de objetos.
Para que un constructor pueda ser const, la clase debe cumplir requisitos estrictos: todos sus campos deben ser final, el constructor no debe tener un cuerpo {} (solo puede usar una lista de inicialización) y todos los argumentos pasados deben ser expresiones constantes conocidas desde antes de que el programa se ejecute.
¿Cuándo aplicarlos? Usa constructores de redirección cuando tengas múltiples constructores nominales (con nombre) que compartan la misma lógica base. Usa const cuando definas objetos de configuración, constantes matemáticas o valores de dominio que sean inmutables. Si intentas usar un constructor const en una clase con campos que pueden cambiar, o si intentas pasar un valor dinámico (como el resultado de una función) a un constructor const, el compilador de Dart lanzará un error.
import 'dart:identical';
class Servidor {
final String host;
final int puerto;
final bool ssl;
// Constructor const principal: permite la canonicalización
const Servidor(this.host, this.puerto, {this.ssl = false});
// Constructor de redirección: delega la lógica al constructor principal.
// No tiene cuerpo {}, lo cual es obligatorio para la redirección.
Servidor.enPuertoHttp(String host) : this(host, 80, ssl: false);
// Constructor normal (no-const): permite lógica de validación en el cuerpo.
Servidor.conValidacion(String host, int puerto) : this(host, puerto) {
if (puerto < 1 || puerto > 65535) {
throw ArgumentError('El puerto $puerto no es válido');
}
}
}
void main() {
// 1. Uso de constructores const y canonicalización
const s1 = Servidor('127.0.0.1', 443, ssl: true);
const s2 = Servidor('127.0.0.1', 443, ssl: true);
// s1 y s2 son la misma instancia en memoria gracias a la canonicalización
print('¿s1 y s2 son idénticas?: ${identical(s1, s2)}'); // true
// 2. Uso de constructor de redirección
final s3 = Servidor.enPuertoHttp('localhost');
print('s3 (redirección): $s3');
// 3. Uso de constructor con lógica (no-const)
final s4 = Servidor.conValidacion('0.0.0.0', 8080);
print('s4 (con validación): $s4');
// 4. Diferencia entre const y final en la instancia
final s5 = Servidor('127.0.0.1', 80); // No es const, pero es inmutable por sus campos
final s6 = Servidor('127.0.0.1', 80);
print('¿s5 y s6 son idénticas?: ${identical(s5, s6)}'); // false
}
Desglose del ejemplo
En el código anterior, observamos cómo el constructor Servidor.enPuertoHttp(String host) no realiza ninguna asignación manual; simplemente usa : this(...) para enviar el trabajo al constructor principal. Esto garantiza que cualquier cambio futuro en la lógica de inicialización de Servidor solo deba hacerse en un lugar.
Al declarar const s1 y const s2, el compilador de Dart nota que los argumentos ('127.0.0.1', 443, true) son constantes. En lugar de construir dos objetos en el heap durante la ejecución, crea uno solo en la sección de constantes del binario. Por eso, identical(s1, s2) devuelve true.
El constructor Servidor.conValidacion es un constructor estándar. A diferencia de los constructores const, este tiene un cuerpo { ... } donde ejecutamos código imperativo (el throw). Por esta razón, no puede ser marcado como const.
Finalmente, fíjate en s5 y s6. Aunque la clase tiene campos final y son inmutables, las instanciamos usando final en lugar de const. Esto significa que se crean dos objetos distintos en tiempo de ejecución; por tanto, identical devuelve false.
El error frecuente
Un error común es intentar añadir un cuerpo a un constructor const para realizar tareas de inicialización adicionales, como imprimir un log o validar un dato.
// Esto fallará al compilar
class Config {
final int timeout;
const Config(this.timeout) {
print('Iniciando config...'); // Error: El cuerpo de un constructor const no es permitido.
}
}
Si necesitas lógica de validación, debes usar una lista de inicialización (antes del cuerpo) o un constructor no-const. Si intentas usar const con un campo que no sea final, el compilador te obligará a cambiar el constructor a no-const, ya que la mutabilidad rompe la garantía de la canonicalización.
N° 52