dynamic vs Object: dominando la flexibilidad de tipos en Dart

dynamic, Object y Never: Navegando la flexibilidad y el rigor en Dart

El tipo dynamic es, en esencia, un marcador de posición que le dice al compilador: “no verifiques esto ahora, yo me encargo en ejecución”. Esto es vital para la flexibilidad que requiere la interoperabilidad con lenguajes dinámicos como JavaScript o la manipulación de estructuras JSON arbitrarias antes de ser mapeadas a clases de dominio. Sin embargo, esta libertad tiene un costo: al desactivar la verificación estática, delegas la responsabilidad al runtime; si intentas acceder a una propiedad inexistente en un objeto dynamic, el programa lanzará un NoSuchMethodError en lugar de un error de compilación. En cambio, si necesitas representar cualquier objeto pero quieres mantener la seguridad, Object es tu aliado, ya que es la raíz de toda la jerarquía no-nula, garantizando únicamente métodos básicos como toString(), hashCode y ==. Si lo usas mal, intentando acceder a propiedades específicas de un tipo concreto sin realizar un casting previo, el compilador te detendrá, obligándote a ser explícito.

Para entender la jerarquía, debemos diferenciar var de dynamic. Cuando declaras var nombre = 'Dart', el compilador infiere que nombre es de tipo String y no te permitirá asignarle un entero después. Con dynamic, la variable puede cambiar de tipo en cualquier momento. Por último, si te encuentras con una función que nunca termina, ya sea porque lanza una excepción o porque entra en un bucle infinito, debes usar Never, el tipo que representa la ausencia total de retorno, diferenciándose de void, que simplemente indica que el valor devuelto no es relevante para el llamador.

// Ejemplo de jerarquía de tipos y flujo de control

// Una función que nunca retorna porque siempre lanza un error
Never reportError(String mensaje) {
  throw Exception('Error crítico: $mensaje');
}

// Una función que devuelve un tipo desconocido (típico en JSON crudo)
dynamic getRawData() {
  return {'id': 101, 'status': 'active'};
}

// Una función que devuelve cualquier objeto no nulo
Object getStandardResponse() {
  return "Operación exitosa";
}

// Una función que no devuelve nada útil (void)
void logAction(String action) {
  print('Log: $action');
}

void main() {
  // 1. Inferencia con 'var' vs flexibilidad con 'dynamic'
  var edad = 25; // El tipo es int (estático)
  // edad = 'veinticinco'; // Error de compilación: un 'int' no puede ser 'String'

  dynamic info = 25; // El tipo puede cambiar
  info = 'veinticinco'; // Permitido

  // 2. Uso de Object (Seguridad estricta)
  Object respuesta = getStandardResponse();
  print(respuesta.toString()); // Seguro: toString() está en la raíz de Object
  // print(respuesta.length); // Error de compilación: Object no tiene 'length'

  // 3. Uso de Object? (La raíz absoluta)
  // Object? permite cualquier cosa, incluyendo nulos
  Object? posibleNulo = null;
  print(posibleNulo.runtimeType);

  // 4. Procesando datos 'dynamic' (El peligro del runtime)
  final data = getRawData();
  print(data['id']); // Funciona en runtime porque 'data' es dynamic

  // 5. Flujo con 'Never' y 'void'
  try {
    logAction('Iniciando proceso...');
    if (data['id'] == 101) {
      print('ID encontrado');
    } else {
      reportError('ID no reconocido');
    }
  } catch (e) {
    print('Capturado: $e');
  }
}

En el código anterior, observamos cómo la jerarquía dicta qué podemos hacer. Cuando trabajamos con respuesta (de tipo Object), el compilador solo nos permite llamar a métodos definidos en la clase base de la VM de Dart, como toString(). Intentar acceder a .length falla en el análisis estático porque Object no garantiza esa propiedad, protegiéndote de errores en tiempo de ejecución.

Sin embargo, al usar data (de tipo dynamic), el compilador “se apaga”. Al ejecutar data['id'], el runtime busca el operador de indexación en el objeto real. Si data resultara ser un entero en lugar de un Map, el programa colapsaría con un NoSuchMethodError. Esta es la diferencia fundamental: Object te obliga a realizar un cast explícito (as Map) para recuperar la seguridad, mientras que dynamic te permite saltarte esa validación.

Con Never, como en reportError, el sistema de tipos sabe que esa línea de código nunca permitirá que la ejecución continúe hacia la siguiente instrucción en ese hilo de control. En cambio, void en logAction indica que, aunque la función termine, el valor que devuelve no tiene un tipo asignable que valga la pena usar; es una señal semántica para el programador.

El error frecuente
El error más peligroso ocurre al abusar de dynamic para evitar el casting.

void procesar(dynamic entrada) {
  // Error: Si 'entrada' es un String en lugar de un Map, 
  // esto fallará en runtime con NoSuchMethodError
  print(entrada['usuario']); 
}

void main() {
  procesar("No soy un mapa"); // El compilador no te avisa.
}

Si utilizas Object en su lugar, el compilador te habría obligado a verificar si entrada es un Map antes de intentar acceder a sus llaves, transformando un posible crash en producción en un error de compilación manejable.

15

Dejar un comentario

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

Scroll al inicio