Miembros estáticos: Gestión de clase y constantes
Los miembros estáticos en Dart pertenecen a la definición de la clase misma y no a las instancias individuales que se crean a partir de ella. Cuando marcas un campo o un método con la palabra clave static, estás indicando que su ciclo de vida y su ámbito están ligados al tipo, no al objeto. Esto implica que no necesitas crear una instancia con new (o simplemente el constructor) para acceder a ellos; puedes llamarlos directamente usando el nombre de la clase.
Esta distinción es vital por cómo la Dart VM gestiona la memoria. Los campos static se inicializan de forma perezosa (lazy), es decir, solo cuando se accede a ellos por primera vez, lo que optimiza el uso de recursos. Sin embargo, a diferencia de los métodos de instancia, los métodos estáticos no tienen acceso al puntero this, ya que el concepto de “esta instancia” no tiene sentido si la llamada se hace desde la clase. Por lo tanto, un método estático solo puede interactuar con otros miembros estáticos de la misma clase.
Si intentas acceder a una variable de instancia desde un método estático, obtendrás un error de compilación. Asimismo, aunque las clases pueden heredar propiedades, los miembros estáticos no se heredan en el sentido de la polimorfismo: no puedes “sobrescribir” un método estático en una subclase para que cambie el comportamiento cuando se llame desde la clase padre.
Para gestionar constantes, debemos distinguir entre static const y static final. Un static const es una constante de tiempo de compilación; su valor debe ser conocido antes de que el programa se ejecute. Un static final es una constante de tiempo de ejecución: se calcula una vez cuando se accede a ella y su valor puede depender de lógica que solo ocurre al arrancar el programa.
class SistemaConfig {
// Constante de compilación: conocida antes de ejecutar el código.
static const String version = '3.4.0';
// Constante de ejecución: se inicializa la primera vez que se accede.
// Se asigna el valor actual del reloj al arrancar.
static final DateTime fechaLanzamiento = DateTime.now();
// Patrón Singleton usando 'late' para inicialización perezosa.
// Garantiza que solo exista una instancia de la configuración.
static late final SistemaConfig instance = _SistemaConfigInterno();
// Constructor privado para evitar instanciación externa (Singleton).
SistemaConfig._();
// Método estático: utilidad que no requiere estado de instancia.
static void imprimirVersion() {
print('Versión del sistema: $version');
print('Iniciado en: $fechaLanzamiento');
}
// Un método de instancia para comparar.
void log(String mensaje) {
print('[$version] LOG: $mensaje');
}
// Simulamos una clase interna para el Singleton.
static _SistemaConfigInterno _SistemaConfigInterno() => _SistemaConfigInterno();
}
class _SistemaConfigInterno {
void ejecutar() => print('Singleton en ejecución.');
}
void main() {
// 1. Acceso mediante la clase, no mediante una instancia.
SistemaConfig.imprimirVersion();
// 2. Acceso al Singleton.
final config = SistemaConfig.instance;
config.log('El sistema está operando correctamente.');
// 3. Uso de la constante de compilación directamente.
print('Verificando versión: ${SistemaConfig.version}');
}
En el código anterior, observa cómo SistemaConfig.version es una static const; esto permite que el compilador optimice el valor directamente en el código máquina. En cambio, fechaLanzamiento es static final, lo que permite que DateTime.now() se ejecute realmente cuando la aplicación arranca, algo que no sería posible con const.
El patrón Singleton se implementa de forma elegante con static late final instance. El modificador late le dice al compilador que no inicialice la variable inmediatamente, sino hasta que alguien la use por primera vez, y final asegura que una vez asignada la instancia única, no pueda ser modificada.
En imprimirVersion(), vemos un método estático puro. Este método no tiene acceso a ninguna variable que no sea static. Si intentáramos usar this dentro de imprimirVersion(), el compilador fallaría porque this apunta a una instancia, y los métodos estáticos pueden existir sin que exista ninguna instancia en memoria.
Finalmente, el acceso a los miembros es siempre a través del nombre de la clase (SistemaConfig.imprimirVersion()). Aunque podrías intentar acceder a ellos desde una instancia en algunos lenguajes, en Dart la semántica es clara: si es static, pertenece a la clase.
El error frecuente
Un error clásico ocurre al intentar acceder a propiedades de instancia desde un contexto estático, algo que suele pasar al intentar crear “clases de utilidad” que accidentalmente dependen de datos que solo existen en objetos vivos.
class Usuario {
String nombre = 'Anonimo';
// ERROR: Este método fallará al compilar.
static void saludar() {
// print(nombre); // Error: 'nombre' no es un miembro estático.
print('Hola, $nombre');
}
}
void main() {
// El programador espera que 'saludar' use el nombre del usuario actual,
// pero el método estático no tiene noción de qué es un "usuario actual".
Usuario.saludar();
}
Si necesitas que un método dependa de datos de una instancia, no debe ser static. Si necesitas un método que sea una utilidad global pero que use datos de una instancia, debes pasarle la instancia como argumento: static void saludar(Usuario usuario) => print(usuario.nombre);.
N° 48