dart:mirrors es la librería que permite la reflexión en Dart, permitiendo que un programa inspeccione su propia estructura (clases, métodos, campos) y manipule objetos en tiempo de ejecución. A través del MirrorSystem, podemos obtener InstanceMirror para interactuar con objetos, ClassMirror para analizar tipos y LibraryMirror para inspeccionar módulos.
Aunque esto otorga una flexibilidad extrema, tiene un costo arquitectónico masivo. El motor de Dart (Dart VM) necesita mantener metadatos exhaustivos de todo el código para que la reflexión funcione. Esto hace que dart:mirrors [disponible desde Dart 1.0] sea incompatible con la optimización de tree shaking y la compilación AOT (Ahead-of-Time) agresiva. Si el compilador no puede saber estáticamente qué métodos se van a llamar (porque el nombre del método reside en un Symbol dinámico), no puede eliminar el código que parezca “no utilizado”, lo que dispara el tamaño del binario y degrada el rendimiento. Por esta razón, en el Dart moderno, la reflexión se ha desplazado hacia la generación de código mediante build_runner.
Debes usar dart:mirrors únicamente cuando estés trabajando en herramientas de desarrollo para la VM, scripts de servidor donde el tamaño del binario no sea crítico, o manteniendo sistemas de inyección de dependencias/serialización legacy que no utilicen generación de código. Si intentas usarlo en Flutter o en cualquier entorno compilado para producción con optimizaciones de tamaño, el proceso de compilación fallará o el código será eliminado. Si te equivocas al usarlo, lo más probable es que pierdas la seguridad de tipos en tiempo de ejecución, convirtiendo errores de tipado en excepciones NoSuchMethodError que solo verás cuando el programa ya esté corriendo.
import 'dart:mirrors';
class PerfilUsuario {
final String nombre;
final String rol;
PerfilUsuario(this.nombre, this.rol);
void ejecutarAccion(String comando) {
print('Ejecutando comando "$comando" para el usuario $nombre...');
}
}
void main() {
// 1. Obtención de metadatos de la clase
// reflectClass nos devuelve un ClassMirror para inspeccionar la estructura
final classMirror = reflectClass(PerfilUsuario);
print('--- Inspección de Clase ---');
print('Superclase: ${classMirror.declaredSuperclass}');
print('Declaraciones detectadas: ${classMirror.declarations.keys.length} elementos');
// 2. Instanciación dinámica
// Necesitamos un Symbol que represente el nombre del constructor
final constructorSymbol = Symbol('PerfilUsuario');
// newInstance requiere una lista de InstanceMirror.value para envolver los argumentos
final instanceMirror = classMirror.newInstance(
constructorSymbol,
[
const InstanceMirror.value('Elena'),
const InstanceMirror.value('Administradora'),
],
);
// 3. Invocación dinámica de métodos
// Usamos Symbol para referirnos al método 'ejecutarAccion'
// Si el Symbol no coincide exactamente con un método real, lanzará un error en runtime
instanceMirror.invoke(Symbol('ejecutarAccion'), [const InstanceMirror.value('REINICIAR_SISTEMA')]);
// 4. Iteración sobre declaraciones de la clase
print('\n--- Listado de métodos y propiedades ---');
classMirror.declarations.forEach((symbol, mirror) {
if (mirror is MethodMirror) {
print('Método encontrado: ${symbol.toString()}');
} else if (mirror is VariableMirror) {
print('Campo encontrado: ${symbol.toString()}');
}
});
}
Desglose técnico
En el ejemplo, la magia ocurre mediante la transición de la tipificación estática a la reflexión dinámica. Primero, reflectClass(PerfilUsuario) le pide a la VM que nos devuelva el ClassMirror de la clase. Este objeto no es la clase en sí, sino una representación estructural que contiene el mapa de declarations.
Cuando ejecutamos classMirror.newInstance, no estamos llamando a un constructor convencional. Estamos operando a nivel de metadatos: el compilador necesita saber que el Symbol('PerfilUsuario') corresponde a un constructor válido. El uso de InstanceMirror.value es crucial aquí; dado que newInstance y invoke trabajan con el sistema de mirrors, no podemos pasar un String o un int directamente. Debemos envolverlos en un InstanceMirror para que la VM pueda gestionar la transferencia de datos en el runtime.
La llamada instanceMirror.invoke es el punto más crítico para la performance. A diferencia de una llamada normal, donde el compilador sabe exactamente a qué dirección de memoria saltar, invoke obliga a la VM a buscar en la tabla de símbolos del objeto, verificar la compatibilidad de los argumentos y luego realizar el salto. Esto anula cualquier posibilidad de inlining por parte del compilador AOT.
El error frecuente
El error más común al trabajar con dart:mirrors es la falta de coincidencia exacta entre el Symbol y el identificador real, lo que resulta en un NoSuchMethodError que el compilador no detecta.
// ERROR COMÚN
// El desarrollador comete un error tipográfico en el String del Symbol
instanceMirror.invoke(Symbol('ejecutar_accion'), [const InstanceMirror.value('test')]);
// Error en runtime: NoSuchMethodError: InstanceMirror with no method named 'ejecutar_accion'
Otro error crítico es intentar usar dart:mirrors en proyectos que se compilan con --compile=aot o en entornos de Flutter. Al ejecutar un comando de compilación para producción, el optimizador eliminará todo aquello que parezca código muerto. Como el compilador no puede rastrear qué métodos se llamarán mediante Symbol, eliminará los métodos de tus clases, dejando la aplicación sin los métodos que intentas invocar dinámicamente.
N° 93