Dart Generics
Generics let you avoid specifying concrete types when writing code, instead using type parameters as placeholders, and determining the actual types at the point of use.
The core value of generics isType SafetyandCode Reuse—One piece of code adapts to multiple types, while the compiler checks type errors for you.
Why Generics Are Needed
First, let's look at a pain point without generics.
Example
Writing without generics:
class IntBox {
int value;
IntBox(this.value);
}
class StringBox {
String value;
StringBox(this.value);
}
// Every time a new type is added, a new Box class must be written, resulting in a lot of duplicate code
void main() {
var intBox = IntBox(42);
var stringBox = StringBox('EXAMPLE');
print('Integer: ${intBox.value}');
print('String: ${stringBox.value}');
}
Is there a way to write a Box class that can hold both int and String, while still guaranteeing type safety at compile time?
This is the problem generics aim to solve.
Generic class
A generic class declares type parameters after the class name using angle brackets <>, and uses that type parameter inside the class.
Example
// Conventional naming: 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('Value in Box: $value (Type: ${value.runtimeType})');
}
}
void main() {
// Specify a concrete type when using a generic class
var intBox = Box<int>(42);
var stringBox = Box<String>('EXAMPLE Dart Tutorial');
var doubleBox = Box<double>(3.14);
intBox.printValue();
stringBox.printValue();
doubleBox.printValue();
// Type safety: the following code will cause a compile-time error
// intBox.setValue('hello'); // Error: String cannot be assigned to int
// Type inference: Dart can infer types from constructor arguments
var autoBox = Box('Automatically inferred as String');
print('Inferred type: ${autoBox.value.runtimeType}');
}
Box 中的值: 42 (类型: int) Box 中的值: EXAMPLE Dart 教程 (类型: String) Box 中的值: 3.14 (类型: double) 推断类型: String
Multiple Type Parameters
A generic class can have multiple type parameters.
Example
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>('example', 10);
var pair2 = Pair('score', 95.5); // Type inference
print(pair1);
print(pair2);
}
Pair(example: 10) Pair(score: 95.5)
Generic methods
In addition to generic classes, functions and methods can also use generics.
Example
// Declare <T> before the return type
T firstElement<T>(List<T> list) {
if (list.isEmpty) {
throw ArgumentError('The list cannot be empty');
}
return list[0];
}
// Generic function: swap two values
void swap<T>(List<T> list, int i, int j) {
T temp = list[i];
list[i] = list[j];
list[j] = temp;
}
void main() {
// Dart automatically infers the type when called
var names = ['example', 'Dart', 'Flutter'];
print('First name: ${firstElement(names)}');
var scores = [95, 88, 72];
print('First score: ${firstElement(scores)}');
// Swap elements
var items = ['A', 'B', 'C'];
print('Before swap: $items');
swap(items, 0, 2);
print('After swap: $items');
}
第一个名字: example 第一个分数: 95 交换前: [A, B, C] 交换后: [C, B, A]
Type constraints extends
Sometimes you want a type parameter to be restricted to subtypes of certain types.
Using the extends keyword, you can impose constraints on generic parameters.
Example
// <T extends num> constrains T to be num or its subclasses (int, double)
class Calculator<T extends num> {
T a;
T b;
Calculator(this.a, this.b);
// Because T is constrained to extend num, num's methods can be used safely.
double add() => (a + b).toDouble();
double subtract() => (a - b).toDouble();
double multiply() => (a * b).toDouble();
double divide() => a / b;
void printOperations() {
print('EXAMPLE calculator: $a and $b');
print(' Add: ${add()}');
print(' Subtract: ${subtract()}');
print(' Multiply: ${multiply()}');
print(' Divide: ${divide().toStringAsFixed(2)}');
}
}
void main() {
var intCalc = Calculator<int>(10, 3);
intCalc.printOperations();
var doubleCalc = Calculator<double>(3.5, 1.5);
doubleCalc.printOperations();
// The following line will report an error: String is not a subclass of num.
// var strCalc = Calculator<String>('a', 'b'); // Error!
}
EXAMPLE 计算器: 10 和 3 加: 13.0 减: 7.0 乘: 30.0 除: 3.33 EXAMPLE 计算器: 3.5 和 1.5 加: 5.0 减: 2.0 乘: 5.25 除: 2.33
Type constraints let you keep the flexibility of generics while also using methods of the constrained type. For example, with the constraint T extends num above, you can use operators such as +, -, *, / in the method body, because the num type supports these operations.
Using Generic Collections
Dart's core collection classes (List, Set, Map) are all generic.
Using generic collections provides compile-time type checking.
Example
// Specify the element type as String
List<String> names = ['example', 'Dart', 'Flutter'];
// names.add(42); // Error: int cannot be added to List<String>
// Specify the key-value types
Map<String, int> scores = {
'example': 95,
'Alice': 87,
};
// scores['Bob'] = 'Excellent'; // Error: String cannot be assigned to int
// Specify the Set element type
Set<double> prices = {9.99, 19.99, 29.99};
// Functional operations on generic collections
List<int> numbers = [1, 2, 3, 4, 5, 6];
// Type-safe filtering and mapping
List<int> evenNumbers = numbers.where((n) => n % 2 == 0).toList();
List<String> labels = numbers.map((n) => 'EXAMPLE-$n').toList();
print(Even numbers: $evenNumbers);
print(Labels: $labels);
// Use fold for type-safe accumulation
int sum = numbers.fold<int>(0, (prev, n) => prev + n);
print(Sum: $sum);
}
偶数: [2, 4, 6] 标签: [EXAMPLE-1, EXAMPLE-2, EXAMPLE-3, EXAMPLE-4, EXAMPLE-5, EXAMPLE-6] 总和: 21
Practical application: Generic Repository Pattern
One of the most classic applications of generics in real-world development is the "Repository Pattern."
Example
abstract class Repository<T> {
void add(T item);
void remove(T item);
T? findById(String id);
List<T> getAll();
}
// User model
class User {
String id;
String name;
User(this.id, this.name);
@override
String toString() => 'User($id, $name)';
}
// Product model
class Product {
String id;
String name;
double price;
Product(this.id, this.name, this.price);
@override
String toString() => 'Product($name, ¥$price)';
}
// Concrete implementation: User repository
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);
}
// Concrete implementation: Product repository (same interface, different types)
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', 'example'));
userRepo.add(User('u2', 'Alice'));
var productRepo = ProductRepository();
productRepo.add(Product('p1', Dart Tutorial, 29.99));
productRepo.add(Product('p2', Flutter Tutorial, 49.99));
print(User list: ${userRepo.getAll()});
print(Product list: ${productRepo.getAll()});
print(Find user u1: ${userRepo.findById(u1')}');
print(Find product p2: ${productRepo.findById(p2')}');
}
用户列表: [User(u1, example), User(u2, Alice)] 商品列表: [Product(Dart 教程, ¥29.99), Product(Flutter 教程, ¥49.99)] 查找用户 u1: User(u1, example) 查找商品 p2: Product(Flutter 教程, ¥49.99)
The generic repository pattern lets you handle different types of data with a unified interface, ensuring type safety while avoiding duplicate code.
other extensions