Restricciones de tipo y polimorfismo F-bounded en Dart

Cuando defines un parámetro genérico T, el compilador de Dart, por defecto, trata a T como un Object?. Esto significa que, aunque sepas que T será un entero o un string, dentro del cuerpo de la clase solo puedes acceder a los métodos básicos de la clase base (como hashCode o toString). Las restricciones de tipo (o type bounds), implementadas mediante la palabra clave extends, cambian esta regla al limitar los tipos que pueden ser asignados a T, garantizando que cumplan con una interfaz específica.

Esto es fundamental para la seguridad de tipos (type safety) en tiempo de compilación. Si intentas llamar a un método como compareTo en un parámetro genérico sin la restricción adecuada, el compilador fallará porque no hay garantía de que el tipo T posea ese método. El uso de extends permite que el compilador “conozca” los miembros disponibles de T sin conocer el tipo exacto de antemano. Esto es especialmente útil cuando necesitas implementar estructuras de datos comparables o algoritmos que operan sobre tipos con comportamientos compartidos.

Debes usar estas restricciones cuando tu lógica dependa de una capacidad específica del objeto (como ser comparable, serializable o tener un identificador) o cuando necesites asegurar que un tipo no sea nullable. Si intentas usar un tipo que no satisface la restricción, obtendrás un error de compilación; si fallas al definir la jerarquía en patrones complejos como el polimorfismo F-bounded, el problema no será un error de sintaxis, sino que perderás la información del tipo específico en los retornos, rompiendo la fluidez de la API (el famoso method chaining).

// Ejemplo de F-bounded polymorphism para mantener el tipo específico en retornos
abstract class Entidad<E extends Entidad<E>> {
  String get id;
  E clonar();
}

class Usuario implements Entidad<Usuario> {
  final String id;
  Usuario(this.id);

  @override
  String get id => id;

  @override
  Usuario clonar() => Usuario(id);
}

// Uso de bounds para restringir a tipos comparables
class BufferOrdenado<T extends Comparable<T>> {
  final List<T> _elementos = [];

  void insertar(T nuevo) {
    // El compilador permite .compareTo porque T está restringido a Comparable
    int indice = 0;
    for (var i = 0; i < _elementos.length; i++) {
      if (_elementos[i].compareTo(nuevo) > 0) {
        indice = i;
        break;
      }
      indice = i + 1;
    }
    _elementos.insert(indice, nuevo);
  }

  List<T> get elementos => List.unmodifiable(_elementos);
}

// Restricción de no nulidad mediante el bound Object
class ContenedorSeguro<T extends Object> {
  final T valor;
  ContenedorSeguro(this.valor);
}

void main() {
  // 1. Prueba de F-bounded polymorphism
  final user = Usuario("u-123");
  final clonedUser = user.clonar(); // clonadoUser es de tipo Usuario, no Entidad
  print('Usuario original: ${user.id}, Clonado: ${clonedUser.id}');

  // 2. Prueba de Sorted Buffer con tipos primitivos que implementan Comparable
  final buffer = BufferOrdenado<int>();
  buffer.insertar(42);
  buffer.insertar(10);
  buffer.insertar(25);
  print('Elementos ordenados: ${buffer.elementos}');

  // 3. Prueba de bounds de nulidad
  final seguro = ContenedorSeguro<int>(100);
  print('Valor seguro: ${seguro.valor}');
  
  // Nota: ContenedorSeguro<int?> no compilaría porque int? no extiende Object
}

En el código anterior, observa cómo BufferOrdenado<T extends Comparable<T>> permite llamar a _elementos[i].compareTo(nuevo). Aquí, T no “contiene” un Comparable, sino que el compilador garantiza que cualquier tipo que se pase como T (como int o double) implementa la interfaz Comparable.

El patrón Entidad<E extends Entidad<E>> es una implementación de polimorfismo F-bounded. Fíjate que en la clase Usuario, implementamos Entidad<Usuario>. Esto es crucial: le estamos diciendo al sistema de tipos que E (el tipo que define el comportamiento de clonación) es exactamente Usuario. Si clonar() devolviera simplemente Entidad, al llamar a user.clonar(), perderíamos acceso a las propiedades específicas de Usuario. Gracias al bound, el motor de Dart puede resolver el tipo exacto en tiempo de compilación.

En ContenedorSeguro<T extends Object>, estamos utilizando el sistema de null safety a nuestro favor. En Dart, Object es la raíz de todos los tipos no-nulos, mientras que Object? es la raíz de todos los tipos, incluyendo los nulos. Al restringir T extends Object, bloqueamos cualquier intento de instanciar la clase con un tipo nullable (como int?), forzando la integridad de los datos en el campo valor.

Si necesitaras que un tipo tuviera múltiples restricciones (por ejemplo, que T fuera Comparable Y Serializable), Dart no permite la sintaxis T extends A & B. En esos casos, debes definir una interfaz que combine ambas o utilizar un mixin para compondirlas en una única clase abstracta.

El error frecuente

Un error clásico al trabajar con polimorfismo F-bounded ocurre cuando la subclase no pasa su propio tipo como argumento genérico al padre.

// ERROR: Olvidar pasar el tipo específico en la implementación
class Animal {
  Animal copia();
}

class Perro extends Animal {
  @override
  Perro copia() => Perro();
}

// El problema: Aunque Perro implemente el método, 
// la firma de la clase base dice que devuelve 'Animal'.
// Esto rompe la consistencia de tipos si se usa a través de la referencia de la clase base.

Si intentas usar Animal como base para una API de clonación, animal.copia() te devolverá un tipo Animal, obligándote a realizar un cast manual a Perro para acceder a sus métodos específicos, lo cual anula las ventajas del sistema de tipos fuerte de Dart.

54

Dejar un comentario

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

Scroll al inicio