Constantes y canonicalización en Dart

Constantes y canonicalización en Dart

Cuando escribes const en Dart, no solo estás declarando una variable inmutable; estás instruyendo al compilador para que evalúe esa expresión durante la fase de compilación y no durante la ejecución. Una expresión constante es aquella cuyo valor es conocido por completo antes de que el programa arranque. Esto es posible gracias a los constructores const [disponible desde Dart 1.0], que permiten que el compilador instancie objetos en tiempo de compilación, siempre que todos sus campos sean final.

La verdadera potencia de const reside en la canonicalización. Si el compilador encuentra dos expresiones constantes que resultan en el mismo valor, no creará dos objetos distintos en memoria; en su lugar, hará que ambas referencias apunten a la misma instancia única. Esto ahorra memoria y permite optimizaciones críticas en el runtime. Por el contrario, si usas final, el objeto se crea en tiempo de ejecución. Aunque dos objetos final tengan los mismos valores, vivirán en direcciones de memoria distintas. Por tanto, si intentas usar const con valores que dependen de una lógica de tiempo de ejecución (como una llamada a una API o la hora actual), obtendrás un error de compilación. Si usas const pero luego intentas modificar su contenido (como añadir un elemento a una lista const), el programa lanzará un error en tiempo de ejecución.

class AppConfig {
  final String environment;
  final int timeoutSeconds;

  // El constructor debe ser 'const' para que los objetos resultantes
  // puedan ser tratados como constantes en tiempo de compilación.
  const AppConfig(this.environment, this.timeoutSeconds);
}

void main() {
  // 1. Diferencia entre 'final' y 'const' (Identidad de memoria)
  final configFinalA = AppConfig('production', 30);
  final configFinalB = AppConfig('production', 30);

  const configConstA = AppConfig('production', 30);
  const configConstB = AppConfig('production', 30);

  // 'final' crea instancias distintas en memoria, aunque los valores sean iguales.
  print('Finales son el mismo objeto? ${identical(configFinalA, configFinalB)}'); // false

  // 'const' utiliza la canonicalización: ambas apuntan a la misma instancia única.
  print('Constantes son el mismo objeto? ${identical(configConstA, configConstB)}'); // true

  // 2. Colecciones constantes y canonicalización
  const listaA = [1, 2, 3];
  const listaB = [1, 2, 3];
  
  // Incluso las colecciones literales se canonicalizan.
  print('Listas const son el mismo objeto? ${identical(listaA, listaB)}'); // true

  // 3. Operaciones aritméticas en tiempo de compilación
  const base = 100;
  const incremento = 50;
  const total = base + incremento * 2; // Evaluado totalmente en compilación

  print('Total calculado en compilación: $total');
}

En el ejemplo anterior, observa cómo AppConfig define sus campos como final. Esto es obligatorio: un objeto const debe ser profundamente inmutable para que el compilador pueda garantizar su integridad.

Cuando definimos configFinalA y configFinalB, el motor de Dart asigna espacio en memoria para cada una de forma independiente en el heap durante la ejecución. Sin embargo, al usar const para configConstA y configConstB, el compilador nota que ambas expresiones son idénticas (AppConfig('production', 30)). En lugar de duplicar el objeto, simplemente hace que ambas variables compartan la misma dirección de memoria. Esto es lo que identical() detecta como true.

Lo mismo ocurre con las listas. listaA y listaB no son solo iguales en contenido; son exactamente el mismo objeto en la memoria del proceso. Esto es especialmente útil cuando manejas configuraciones globales o listas de elementos estáticos que se consultan en múltiples partes del código, ya que eliminas la sobrecarga de instanciación y reduces la presión sobre el Garbage Collector. Finalmente, la variable total no es una operación matemática que el CPU ejecute cada vez que se lee; es un valor literal ya resuelto en el binario.

El error frecuente

Un error común es confundir la inmutabilidad de los campos con la inmutabilidad de la estructura de la colección. Aunque una lista const es inmutable, el error no suele ser de compilación si intentas acceder a ella, sino de ejecución si intentas alterarla.

void main() {
  const listaInmutable = [1, 2, 3];
  
  // Esto lanza una UnsupportedError en tiempo de ejecución.
  // El compilador permite la sintaxis, pero la VM bloquea la mutación.
  listaInmutable.add(4); 
}

Esta distinción es vital: const garantiza que el objeto y su contenido no cambien, mientras que final solo garantiza que la variable no pueda reasignarse a otra instancia.

106

Dejar un comentario

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

Scroll al inicio