Genéricos en Dart

Los genéricos te permiten no especificar un tipo concreto al escribir código, sino usar un parámetro de tipo como marcador de posición y determinar el tipo al momento de usarlo.

El valor fundamental de los genéricos esSeguridad de tiposyReutilización de código—Un mismo código se adapta a múltiples tipos y, al mismo tiempo, el compilador te ayuda a detectar errores de tipo.


¿Por qué se necesitan los genéricos?

Primero veamos un problema cuando no hay genéricos.

Ejemplo

Escritura sin genéricos:

// No hay genéricos: solo se puede almacenar Object; al extraerlo, se necesita una conversión manual
class IntBox {
  int value;
  IntBox(this.value);
}

class StringBox {
  String value;
  StringBox(this.value);
}

// Cada vez que se añade un nuevo tipo, hay que escribir una nueva clase Box; el código se repite mucho.

void main() {
  var intBox = IntBox(42);
  var stringBox = StringBox('EJEMPLO');

  print('Entero: ${intBox.value}');
  print('Cadena: ${stringBox.value}');
}

¿Hay alguna manera de escribir una clase Box que pueda contener tanto int como String y, además, garantizar la seguridad de tipos en tiempo de compilación?

Este es el problema que resuelven los genéricos.


Clases genéricas

En una clase genérica, se declara el parámetro de tipo después del nombre de la clase con corchetes angulares <> y se usa dicho parámetro de tipo dentro de la clase.

Ejemplo

// <T> es el parámetro de tipo; T es un marcador de posición que representa "algún tipo".
// Convención de nomenclatura: T (Type), E (Element), K (Key), V (Value)
class Box<T> {
  T value;

  Box(this.value);

  T getValue() {
    return value;
  }

  void setValue(T newValue) {
    value = newValue;
  }

  void printValue() {
    print('Valor en Box: $value (tipo: ${value.runtimeType})');
  }
}

void main() {
  // Al usar una clase genérica, se especifica el tipo concreto.
  var intBox = Box<int>(42);
  var stringBox = Box<String>('Tutorial de EJEMPLO Dart');
  var doubleBox = Box<double>(3.14);

  intBox.printValue();
  stringBox.printValue();
  doubleBox.printValue();

  // Seguridad de tipos: el siguiente código producirá un error en tiempo de compilación.
  // intBox.setValue('hello'); // Error: String no se puede asignar a int

  // Inferencia de tipos: Dart puede inferir el tipo a partir de los parámetros del constructor.
  var autoBox = Box('Inferido automáticamente como String');
  print('Tipo inferido: ${autoBox.value.runtimeType}');
}
Box 中的值: 42 (类型: int)
Box 中的值: EJEMPLO Dart 教程 (类型: String)
Box 中的值: 3.14 (类型: double)
推断类型: String

Múltiples parámetros de tipo

Las clases genéricas pueden tener múltiples parámetros de tipo.

Ejemplo

// K representa el tipo de la clave, V representa el tipo del valor.
class Pair<K, V> {
  K key;
  V value;

  Pair(this.key, this.value);

  @override
  String toString() => 'Pair($key: $value)';
}

void main() {
  var pair1 = Pair<String, int>('ejemplo', 10);
  var pair2 = Pair('score', 95.5);  // Inferencia de tipos

  print(pair1);
  print(pair2);
}
Pair(ejemplo: 10)
Pair(score: 95.5)

Métodos genéricos

Además de las clases genéricas, las funciones y los métodos también pueden usar genéricos.

Ejemplo

// Función genérica: el tipo de retorno coincide con el tipo del parámetro
// Declarar <T> antes del tipo de retorno
T firstElement<T>(List<T> list) {
  if (list.isEmpty) {
    throw ArgumentError('La lista no puede estar vacía');
  }
  return list[0];
}

// Función genérica: intercambiar dos valores
void swap<T>(List<T> list, int i, int j) {
  T temp = list[i];
  list[i] = list[j];
  list[j] = temp;
}

void main() {
  // Dart infiere el tipo automáticamente al llamar
  var names = ['ejemplo', 'Dart', 'Flutter'];
  print('Primer nombre: ${firstElement(names)}');

  var scores = [95, 88, 72];
  print('Primera puntuación: ${firstElement(scores)}');

  // Intercambiar elementos
  var items = ['A', 'B', 'C'];
  print('Antes del intercambio: $items');
  swap(items, 0, 2);
  print('Después del intercambio: $items');
}
第一个名字: ejemplo
第一个分数: 95
交换前: [A, B, C]
交换后: [C, B, A]

Restricciones de tipo: extends

A veces quieres que el parámetro de tipo solo pueda ser un subtipo de ciertos tipos.

Con la palabra clave extends se pueden imponer restricciones al parámetro genérico.

Ejemplo

// Definir una calculadora numérica
// <T extends num> restringe T a num o sus subclases (int, double)
class Calculator<T extends num> {
  T a;
  T b;

  Calculator(this.a, this.b);

  // Debido a la restricción T extends num, se pueden usar los métodos de num de forma segura
  double add() => (a + b).toDouble();
  double subtract() => (a - b).toDouble();
  double multiply() => (a * b).toDouble();
  double divide() => a / b;

  void printOperations() {
    print('Calculadora EJEMPLO: $a y $b');
    print(' Suma: ${add()}');
    print(' Resta: ${subtract()}');
    print(' Multiplicación: ${multiply()}');
    print(' División: ${divide().toStringAsFixed(2)}');
  }
}

void main() {
  var intCalc = Calculator<int>(10, 3);
  intCalc.printOperations();

  var doubleCalc = Calculator<double>(3.5, 1.5);
  doubleCalc.printOperations();

  // La siguiente línea generará un error: String no es una subclase de num
  // var strCalc = Calculator<String>('a', 'b'); // Error!
}
EJEMPLO 计算器: 10 和 3
  加: 13.0
  减: 7.0
  乘: 30.0
  除: 3.33
EJEMPLO 计算器: 3.5 和 1.5
  加: 5.0
  减: 2.0
  乘: 5.25
  除: 2.33

Las restricciones de tipo te permiten mantener la flexibilidad de los genéricos y, al mismo tiempo, usar los métodos del tipo restringido. Por ejemplo, con la restricción T extends num de arriba, puedes usar operadores como +, -, *, / dentro del cuerpo del método, porque el tipo num admite estas operaciones.


Uso de colecciones genéricas

Las clases de colección principales de Dart (List, Set, Map) son todas genéricas.

El uso de colecciones genéricas permite obtener comprobación de tipos en tiempo de compilación.

Ejemplo

void main() {
  // Especifica el tipo de elemento como String
  List<String> names = ['ejemplo', 'Dart', 'Flutter'];
  // names.add(42); // Error: no se puede agregar int a List<String>

  // Especifica el tipo de clave-valor
  Map<String, int> scores = {
    'ejemplo': 95,
    'Alice': 87,
  };
  // scores['Bob'] = 'excelente'; // Error: no se puede asignar String a int

  // Especifica el tipo de elemento del Set
  Set<double> prices = {9.99, 19.99, 29.99};

  // Operaciones funcionales con colecciones genéricas
  List<int> numbers = [1, 2, 3, 4, 5, 6];

  // Filtrado y mapeo con seguridad de tipos
  List<int> evenNumbers = numbers.where((n) => n % 2 == 0).toList();
  List<String> labels = numbers.map((n) => 'EJEMPLO-$n').toList();

  print('Números pares: $evenNumbers');
  print('Etiquetas: $labels');

  // Usa fold para la acumulación con seguridad de tipos
  int sum = numbers.fold<int>(0, (prev, n) => prev + n);
  print('Suma: $sum');
}
偶数: [2, 4, 6]
标签: [EJEMPLO-1, EJEMPLO-2, EJEMPLO-3, EJEMPLO-4, EJEMPLO-5, EJEMPLO-6]
总和: 21

Aplicación práctica: patrón de repositorio genérico

Una de las aplicaciones más clásicas de los genéricos en el desarrollo real es el "patrón de repositorio" (Repository Pattern).

Ejemplo

// Define una interfaz de repositorio genérica
abstract class Repository<T> {
  void add(T item);
  void remove(T item);
  T? findById(String id);
  List<T> getAll();
}

// Modelo de usuario
class User {
  String id;
  String name;

  User(this.id, this.name);

  @override
  String toString() => 'User($id, $name)';
}

// Modelo de producto
class Product {
  String id;
  String name;
  double price;

  Product(this.id, this.name, this.price);

  @override
  String toString() => 'Product($name, ¥$price)';
}

// Implementación concreta: repositorio de usuarios
class UserRepository implements Repository<User> {
  final List<User> _users = [];

  @override
  void add(User user) => _users.add(user);

  @override
  void remove(User user) => _users.remove(user);

  @override
  User? findById(String id) {
    try {
      return _users.firstWhere((u) => u.id == id);
    } catch (_) {
      return null;
    }
  }

  @override
  List<User> getAll() => List.unmodifiable(_users);
}

// Implementación concreta: repositorio de productos (la misma interfaz, diferentes tipos)
class ProductRepository implements Repository<Product> {
  final List<Product> _products = [];

  @override
  void add(Product product) => _products.add(product);

  @override
  void remove(Product product) => _products.remove(product);

  @override
  Product? findById(String id) {
    try {
      return _products.firstWhere((p) => p.id == id);
    } catch (_) {
      return null;
    }
  }

  @override
  List<Product> getAll() => List.unmodifiable(_products);
}

void main() {
  var userRepo = UserRepository();
  userRepo.add(User('u1', 'ejemplo'));
  userRepo.add(User('u2', 'Alice'));

  var productRepo = ProductRepository();
  productRepo.add(Product('p1', 'Tutorial de Dart', 29.99));
  productRepo.add(Product('p2', 'Tutorial de Flutter', 49.99));

  print(Lista de usuarios: ${userRepo.getAll()});
  print(Lista de productos: ${productRepo.getAll()});
  print(Buscar usuario u1: ${userRepo.findById('u1')}');
  print(Buscar producto p2: ${productRepo.findById('p2')}');
}
用户列表: [User(u1, ejemplo), User(u2, Alice)]
商品列表: [Product(Dart 教程, ¥29.99), Product(Flutter 教程, ¥49.99)]
查找用户 u1: User(u1, ejemplo)
查找商品 p2: Product(Flutter 教程, ¥49.99)

El patrón de repositorio genérico te permite manejar diferentes tipos de datos con una interfaz unificada, lo que garantiza la seguridad de tipos y evita código duplicado.

Otras extensiones