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:
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
// 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
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
// 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
// <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
// 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
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