Un Symbol es un identificador opaco que representa un nombre único dentro de tu programa. A diferencia de un String, que es una secuencia de caracteres que puede ser manipulada o alterada, un Symbol es un valor internado [disponible desde Dart 1.0]: esto significa que el runtime garantiza que dos símbolos creados con el mismo identificador apuntarán exactamente a la misma instancia en memoria. Si utilizas identical(#metodo, #metodo), el resultado será true.
El propósito de los Symbol no es almacenar texto para mostrar al usuario, sino servir como una referencia semántica estable. Cuando trabajas con metaprogramación o librerías que necesitan inspeccionar código (como ORMs o serializadores), utilizas Symbol para referenciar métodos, campos o getters/setters. La razón de su existencia es la ofuscación y la minificación. En un proceso de compilación optimizado (como en AOT), el compilador puede renombrar una función llamada procesarPago() a simplemente a() para reducir el tamaño del binario. Sin embargo, el Symbol mantiene su identidad lógica, permitiendo que la lógica de reflexión encuentre el miembro correcto aunque su nombre visual haya cambiado.
Debes usar Symbol cuando estés construyendo herramientas que operen sobre la estructura del código o cuando necesites llaves en un Map que representen propiedades de una clase de forma inequívoca. Si intentas usar String para identificar propiedades y luego compilas tu aplicación con optimizaciones de minificación, tu código fallará estrepitosamente porque el nombre del campo en la cadena de texto ya no coincidirá con el nombre del campo en el código compilado.
El error crítico ocurre si confías en la representación textual de un símbolo (usando symbol.toString() o Symbol.name) para ejecutar lógica de negocio. Aunque el símbolo sea único, su “nombre” como String puede ser impredecible en entornos de producción optimizados.
import 'dart:mirrors';
class Empleado {
final String nombre;
final int salario;
Empleado(this.nombre, this.salario);
void presentarse() => print('Hola, soy $nombre.');
bool get esVIP => salario > 5000;
set nombre(String nuevoNombre) { /* lógica de validación */ }
// Ejemplo de cómo se representan operadores
int operator +(int extra) => salario + extra;
}
void main() {
// 1. Identidad y el concepto de "Interning"
const s1 = #presentarse;
const s2 = #presentarse;
// Aunque parezcan objetos distintos, son la misma instancia en memoria
print('¿Son idénticos? ${identical(s1, s2)}');
// 2. Sintaxis de literales para diferentes contextos
const sGetter = #esVIP; // Representa un getter
const sSetter = #nombre; // Representa un setter
const sOperator = #operator+; // Representa un operador
// 3. Uso de Symbols como llaves semánticas en un Map
// Esto es común en motores de serialización para mapear propiedades a etiquetas
final metadatos = <Symbol, String>{
#nombre: 'Nombre Completo del Empleado',
#salario: 'Sueldo Base Mensual',
#esVIP: 'Estatus de Fidelidad',
};
final empleado = Empleado('Ana García', 6000);
print('Metadato de #nombre: ${metadatos[#nombre]}');
// 4. Reflexión con dart:mirrors (Solo disponible en la VM de Dart)
// MirrorSystem.getName permite obtener el nombre del símbolo como String.
// ¡Cuidado! En código AOT/Minificado, esto podría devolver nombres ofuscados.
print('Nombre recuperado vía MirrorSystem: ${MirrorSystem.getName(sGetter)}');
print('El nombre del empleado es: ${empleado.nombre}');
}
En el ejemplo anterior, observa cómo s1 y s2 son idénticos gracias al interning; esto permite que las comparaciones sean extremadamente rápidas, ya que el runtime solo compara punteros de memoria en lugar de comparar caracteres de una cadena. Al usar el mapa metadatos, estamos asociando la identidad semántica de la propiedad #nombre con una descripción legible, una técnica estándar cuando se crean sistemas de introspección que deben ser inmunes a la minificación.
Es vital notar que el literal #nombre se comporta de forma distinta según el contexto: si lo usas en un Map, es una clave; si lo usas para referenciar un setter, el motor de reflexión lo interpreta como la operación de asignación. Finalmente, fíjate en MirrorSystem.getName(sGetter). Aunque aquí vemos "esVIP", en un entorno de producción donde el código haya sido ofuscado para proteger la propiedad intelectual, esa función podría devolver algo como "a". Por eso, nunca uses el resultado de esa función para comparar lógica; usa siempre el Symbol directamente.
El error frecuente
Si intentas implementar una lógica de serialización basada en String en lugar de Symbol, como en este caso:
// MAL: Esto fallará en producción (AOT/Minificado)
void serializar(dynamic objeto) {
if (objeto.toString().contains('salario')) {
// ... logic
}
}
El compilador transformará el campo salario en algo como v para optimizar el binario. El String "salario" dejará de existir en el código compilado, mientras que el Symbol(#salario) permanecerá vinculado semánticamente al miembro, permitiendo que las herramientas de reflexión lo localicen sin errores.
N° 94