Genéricos en Dart

Si revisas la documentación API del tipo de matriz básico List, descubrirás que su tipo real es List<E>. Los signos de ángulo <...> indican un tipo genérico (tipo parametrizado), es decir, un tipo con parámetros de tipo formales. Por convención, casi todas las variables de tipo se nombran con una sola letra, como E, T, S, K, V.

¿Por qué usar genéricos?

  • La función principal de los genéricos es garantizar la seguridad de tipos
  • Especificar correctamente los genéricos genera un código de ejecución más optimizado;
  • El uso de genéricos reduce drásticamente la repetición de código.

Si quieres que una lista solo almacene cadenas, declárala como List<String>: se trata de una lista de cadenas.

Una vez definida como lista exclusiva de cadenas, insertar valores de otro tipo generará un error.

// Error de comprobación estática de tipos
var names = <String>[];
names.addAll(['Seth', 'Kathy', 'Lars']);
names.add(42); // Error de compilación: 42 no es una cadenaLenguaje del código: PHP (php)
D:\dartdemo\firstdart>dart run
Building package executable...
Failed to build firstdart:firstdart:
bin/firstdart.dart:5:12: Error: The argument type 'int' can't be assigned to the parameter type 'String'.
        names.add(42); // Error de compilación: 42 no es una cadenaLenguaje del código: PHP (php)

Reducir redundancia de código

Supongamos que necesitas una interfaz abstracta para un caché de objetos:

abstract class ObjectCache {
  Object getByKey(String key);
  void setByKey(String key, Object value);
}
Lenguaje del código: JavaScript (javascript)

Más adelante, si requieres un caché que solo guarde cadenas, debes crear una interfaz nueva:

abstract class StringCache {
  String getByKey(String key);
  void setByKey(String key, String value);
}
Lenguaje del código: Dart (dart)

Luego necesitarás cachés exclusivos para números… tendrás que escribir repetidamente código casi idéntico.

Por ejemplo, terminarías escribiendo algo así:

abstract class IntCache {
  int getByKey(String key);
  void setByKey(String key, int value);
}Lenguaje del código: Dart (dart)

Los genéricos solucionan este problema de una sola vez: solo debes definir una interfaz universal con parámetros de tipo:

abstract class Cache<T> {
  T getByKey(String key);
  void setByKey(String key, T value);
}Lenguaje del código: Dart (dart)

Aquí T es un tipo marcador de posición, representa cualquier tipo concreto que el desarrollador pase posteriormente.

Ejemplo práctico

// Clase base abstracta de caché genérico
abstract class Cache<T> {
  T? getByKey(String key);
  void setByKey(String key, T value);
  void remove(String key);
}

// Implementación de caché en memoria (basada en Map)
class MemoryCache<T> implements Cache<T> {
  final Map<String, T> _storage = {};

  @override
  T? getByKey(String key) {
    return _storage[key];
  }

  @Override
  void setByKey(String key, T value) {
    _storage[key] = value;
  }

  @Override
  void remove(String key) {
    _storage.remove(key);
  }
}

void main() {
  // 1. Ejemplo de caché de cadenas
  final Cache<String> strCache = MemoryCache<String>();
  strCache.setByKey("name", "Tom");
  print(strCache.getByKey("name")); // Tom

  // 2. Caché de enteros int
  final Cache<int> intCache = MemoryCache<int>();
  intCache.setByKey("age", 26);
  print(intCache.getByKey("age")); // 26

  // 3. Caché de objetos personalizados
  final Cache<User> userCache = MemoryCache<User>();
  userCache.setByKey("u1", User("Jack", 18));
  final User? user = userCache.getByKey("u1");
  print(user?.name); // Jack

  // Prueba de eliminación
  userCache.remove("u1");
  print(userCache.getByKey("u1")); // null
}

// Clase de entidad personalizada
class User {
  final String name;
  final int age;
  User(this.name, this.age);
}Lenguaje del código: Dart (dart)

Usar literales de colección genéricos

Los literales List, Set y Map admiten genéricos parametrizados:

  • List / Set: escribe <Tipo> antes del corchete abierto
  • Map: escribe <TipoClave, TipoValor> antes de la llave abierta

Ejemplos:

// Lista de cadenas
var names = <String>['Seth', 'Kathy', 'Lars'];

// Conjunto de cadenas (elimina duplicados automáticamente)
var uniqueNames = <String>{'Seth', 'Kathy', 'Lars'};

// Diccionario con claves y valores de cadena
var pages = <String, String>{
  'index.html': 'Página principal',
  'robots.txt': 'Archivo de instrucciones para rastreadores',
  'humans.txt': 'Archivo explicativo para desarrolladores humanos',
};
Lenguaje del código: PHP (php)

Ejemplo en ejecución

void main() {
  // 1. List<String> Lista de cadenas: ordenada, permite duplicados, acceso por índice
  var names = <String>['Seth', 'Kathy', 'Lars', 'Seth'];
  print("=== List Lista ===");
  print(names); // [Seth, Kathy, Lars, Seth] se permiten duplicados
  print(names[0]); // Obtener primer elemento por índice: Seth
  names.add("Mike"); // Añadir elemento al final
  print("Después de añadir: $names");

  // 2. Set<String> Conjunto de cadenas: desordenado, elimina duplicados, sin índices
  var uniqueNames = <String>{'Seth', 'Kathy', 'Lars', 'Seth'};
  print("\n=== Set Conjunto (eliminación automática de duplicados) ===");
  print(uniqueNames); // {Seth, Kathy, Lars} el duplicado Seth se borra solo
  print(uniqueNames.contains("Kathy")); // Comprobar existencia: true
  uniqueNames.remove("Lars");
  print("Después de borrar Lars: $uniqueNames");

  // 3. Map<String, String> Diccionario clave-valor: claves únicas, acceso por clave
  var pages = <String, String>{
    'index.html': 'Página principal',
    'robots.txt': 'Archivo de instrucciones para rastreadores',
    'humans.txt': 'Archivo explicativo para desarrolladores humanos',
  };
  print("\n=== Map Diccionario ===");
  print(pages);
  print(pages['index.html']); // Obtener valor por clave: Página principal
  pages["about.html"] = "Página acerca de"; // Añadir par clave-valor
  print("Después de añadir about.html: $pages");

  // Ejemplos comunes de recorrido
  print("\n=== Recorrer List ===");
  for (var name in names) {
    print("- $name");
  }

  print("\n=== Recorrer todas las entradas de Map ===");
  for (var entry in pages.entries) {
    print("Archivo: ${entry.key} → Descripción: ${entry.value}");
  }
}Lenguaje del código: Dart (dart)

Usar parámetros genéricos en constructores

Al invocar un constructor, especifica el tipo genérico entre ángulos después del nombre de la clase:

var nameSet = Set<String>.of(names);Lenguaje del código: JavaScript (javascript)

El siguiente código crea un diccionario con claves de tipo int y valores de tipo View:

var views = SplayTreeMap<int, View>();
Lenguaje del código: HTML, XML (xml)
import 'dart:collection';

void main() {
  var names = <String>['Seth', 'Kathy', 'Lars', 'Seth'];
  var nameSet = Set<String>.of(names);
  print("Lista original: $names");
  print("Set sin duplicados: $nameSet");

  // SplayTreeMap requiere importar dart:collection
  var views = SplayTreeMap<int, String>();
  views[5] = "Página de configuración";
  views[2] = "Página principal";
  views[9] = "Mi página";
  views[1] = "Pantalla de inicio";

  print("\nTreeMap ordenado:");
  for (var e in views.entries) {
    print("${e.key} : ${e.value}");
  }
}Lenguaje del código: PHP (php)

Tipos en tiempo de ejecución de colecciones genéricas

Reificación de tipos (reified)

Los genéricos de Dart son reificados: la información completa del tipo genérico se conserva en tiempo de ejecución.

Puedes comprobar directamente el tipo genérico concreto de una colección durante la ejecución:

void main() {
    var names = <String>[];
	names.addAll(['Seth', 'Kathy', 'Lars']);
	print(names is List<String>); // Muestra true
}Lenguaje del código: PHP (php)

Comparación complementaria: Los genéricos de Java usan borrado de tipos, eliminando los parámetros genéricos en tiempo de ejecución. En Java solo se puede comprobar si un objeto es una List, sin diferenciar entre List<String> y List<int>.

* Restringir tipos de parámetros genéricos

Límite (bound). Si aún no has estudiado clases, basta con entender el concepto general de esta sección.

Al definir un genérico, puedes limitar que los tipos pasados sean subclases de una superclase determinada. Esta restricción se llama límite de tipo y se implementa con la palabra clave extends.

1 Restringir a tipos no anulables

Forzar que el genérico solo acepte tipos no anulables (el límite superior por defecto es el tipo anulable Object?):

class Foo<T extends Object> {
  // El tipo T pasado no puede ser anulable
}
Lenguaje del código: JavaScript (javascript)

2 Limitar a subclases de una superclase específica

Usa extends para indicar la clase base; así podrás llamar directamente a todos los métodos miembro de la clase base:

class Foo<T extends SomeBaseClass> {
  String toString() => "Instancia Foo<$T>";
}

class Extender extends SomeBaseClass {}
Lenguaje del código: JavaScript (javascript)

Argumentos de tipo válidos: la superclase misma, cualquiera de sus subclases

var someBaseClassFoo = Foo<SomeBaseClass>();
var extenderFoo = Foo<Extender>();
Lenguaje del código: HTML, XML (xml)

Si omites el parámetro genérico, se usa el tipo límite por defecto:

var foo = Foo();
print(foo); // Muestra: Instancia Foo<SomeBaseClass>
Lenguaje del código: PHP (php)

Pasar un tipo sin relación genera un error de compilación estático:

var foo = Foo<Object>(); // Error de compilación: Object no es subclase de SomeBaseClass
Lenguaje del código: JavaScript (javascript)

* Límites de tipo autorreferentes (genéricos con límite F)

Al restringir genéricos, el límite puede hacer referencia a su propio parámetro de tipo para crear una autorrestricción, conocida como límite F (F-bound).

Ejemplo estándar: interfaz comparable Comparable<T>

abstract interface class Comparable<T> {
  int compareTo(T o);
}

// T debe implementar Comparable<T>, solo se puede comparar con objetos del mismo tipo
int compareAndOffset<T extends Comparable<T>>(T t1, T t2) =>
    t1.compareTo(t2) + 1;

class A implements Comparable<A> {
  @override
  int compareTo(A other) {
    // Implementar lógica de comparación
    return 0;
  }
}

int res = compareAndOffset(A(), A());
Lenguaje del código: PHP (php)

La restricción T extends Comparable<T> significa: T solo se puede comparar con instancias del mismo tipo que él mismo.

* Métodos genéricos

Las funciones normales y los métodos de clase también admiten parámetros de tipo genéricos:

T first<T>(List<T> ts) {
  // Lógica de comprobación previa
  T tmp = ts[0];
  // Lógica de procesamiento posterior
  return tmp;
}
Lenguaje del código: PHP (php)

Ejemplo completo

T first<T>(List<T> ts) {
  if (ts.isEmpty) {
    throw ArgumentError("La lista no puede estar vacía");
  }
  T tmp = ts[0];
  return tmp;
}

void main() {
  List<String> strList = ["apple", "banana", "orange"];
  String firstStr = first(strList);
  print(firstStr); //apple

  List<int> numList = [10, 20, 30];
  int firstNum = first(numList);
  print(firstNum);//10
}Lenguaje del código: Dart (dart)

El parámetro genérico <T> actúa en tres lugares:

  1. Tipo de retorno de la función T
  2. Tipo del parámetro de entrada List<T>
  3. Tipo de variable local T tmp

Deja una respuesta

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