今天我们要学习的泛型(Generics) ,就是解决 “通用逻辑复用” 与 “类型安全” 的核心技术。
一、为什么需要泛型?—— 从 “重复代码” 到 “通用逻辑”
假设我们需要实现一个 “盒子” 类,用于存储不同类型的数据(如数字、字符串)。如果不使用泛型,可能会写出这样的代码:
// 存储整数的盒子
class IntBox {
int value;
IntBox(this.value);
int getValue() => value;
}
// 存储字符串的盒子
class StringBox {
String value;
StringBox(this.value);
String getValue() => value;
}
void main() {
IntBox intBox = IntBox(10);
StringBox stringBox = StringBox("Dart");
print(intBox.getValue()); // 输出:10
print(stringBox.getValue()); // 输出:Dart
}
这种写法的问题很明显:逻辑完全相同,却要为每种类型重复实现一次。如果需要支持更多类型(如 bool、List),代码会变得臃肿不堪。
更严重的是,如果用 dynamic 类型实现 “通用” 盒子,会丢失类型安全:
// 用 dynamic 实现的“通用”盒子(丢失类型安全)
class DynamicBox {
dynamic value;
DynamicBox(this.value);
dynamic getValue() => value;
}
void main() {
DynamicBox box = DynamicBox(10);
// 编译时不报错,但运行时会崩溃(字符串没有 length 属性)
print(box.getValue().length);
}
泛型的出现就是为了同时解决这两个问题:在保证类型安全的前提下,实现代码复用。
二、泛型基础:泛型类与类型参数
泛型的核心思想是:在定义类、函数时不指定具体类型,而是使用一个 “占位符” 表示类型,在使用时再指定具体类型。这个 “占位符” 称为类型参数。
1. 泛型类(Generic Class)
以 “盒子” 为例,用泛型类实现一次,即可支持所有类型:
// 泛型类:用 <T> 声明类型参数 T(T 是占位符,可自定义名称)
class Box<T> {
T value; // 用 T 作为 value 的类型
Box(this.value); // 构造函数参数类型为 T
T getValue() => value; // 返回值类型为 T
}
void main() {
// 使用时指定具体类型:int
Box<int> intBox = Box<int>(10);
int num = intBox.getValue(); // 类型安全:返回值一定是 int
print(num); // 输出:10
// 使用时指定具体类型:String
Box<String> stringBox = Box<String>("Dart");
String str = stringBox.getValue(); // 类型安全:返回值一定是 String
print(str); // 输出:Dart
// 类型错误:编译时直接报错(不能将 String 存入 int 类型的盒子)
// Box<int> errorBox = Box<int>("error"); // 编译错误
}
泛型类的优势:
- 代码复用:一份逻辑支持所有类型
- 类型安全:编译时检查类型匹配,避免运行时错误
- 自动类型推断:Dart 会根据传入的值自动推断类型参数,可简化写法:
Box<int> intBox = Box(10); // 省略 <int>,编译器自动推断
Box<String> stringBox = Box("Dart"); // 省略 <String>
2. 泛型函数(Generic Function)
不仅类可以是泛型的,函数和方法也可以通过泛型实现通用逻辑。
// 泛型函数:用 <T> 声明类型参数,参数和返回值都使用 T
T getFirstElement<T>(List<T> list) {
if (list.isEmpty) {
throw Exception("List is empty");
}
return list[0]; // 返回列表的第一个元素,类型为 T
}
void main() {
List<int> numbers = [1, 2, 3];
int firstNum = getFirstElement(numbers); // 自动推断 T 为 int
print(firstNum); // 输出:1
List<String> fruits = ["apple", "banana"];
String firstFruit = getFirstElement(fruits); // 自动推断 T 为 String
print(firstFruit); // 输出:apple
}
泛型函数让我们可以为不同类型的输入编写通用算法(如排序、过滤、转换等),同时保证类型安全。
三、泛型约束:限制类型参数的范围
默认情况下,泛型的类型参数可以是任何类型。但有时我们需要限制类型参数只能是特定类型或其子类型,这就是泛型约束。
使用 extends 关键字指定泛型约束:
1. 约束为具体类型
// 定义一个有 name 属性的类
class HasName {
String name;
HasName(this.name);
}
// 泛型类约束:T 必须是 HasName 或其子类
class NameBox<T extends HasName> {
T value;
NameBox(this.value);
// 可以安全调用 T 的 name 属性(因为 T 一定是 HasName 的子类)
String getValueName() => value.name;
}
// 子类也符合约束
class Person extends HasName {
Person(String name) : super(name);
}
void main() {
// 正确:T 是 HasName
NameBox<HasName> box1 = NameBox<HasName>(HasName("Generic"));
print(box1.getValueName()); // 输出:Generic
// 正确:T 是 Person(HasName 的子类)
NameBox<Person> box2 = NameBox<Person>(Person("Alice"));
print(box2.getValueName()); // 输出:Alice
// 错误:T 是 String(不符合 HasName 约束)
// NameBox<String> errorBox = NameBox<String>("error"); // 编译错误
}
通过约束,我们可以在泛型类 / 函数中安全地调用约束类型的方法和属性,避免因类型不确定导致的错误。
2. 约束为接口(使用 Dart 内置的 Comparable)
对于需要比较大小的场景,我们可以利用 Dart 内置的 Comparable 接口作为约束,确保类型参数实现了比较方法:
// 导入 Dart 核心库(包含 Comparable 接口)
import 'dart:core';
// 泛型函数:约束 T 必须实现内置的 Comparable 接口
T findMax<T extends Comparable>(List<T> list) {
T max = list[0];
for (T item in list) {
// 安全调用 compareTo 方法(因 T 已约束为 Comparable)
if (item.compareTo(max) > 0) {
max = item;
}
}
return max;
}
// 自定义类实现 Comparable 接口
class Student implements Comparable<Student> {
int score;
Student(this.score);
@override
int compareTo(Student other) {
return score - other.score;
}
}
void main() {
// 应用于 int(int 已实现 Comparable 接口)
List<int> numbers = [3, 1, 4, 2];
print(findMax(numbers)); // 输出:4
// 应用于自定义 Student 类
List<Student> students = [Student(80), Student(95), Student(70)];
print(findMax(students).score); // 输出:95
}
这个例子中,findMax 函数通过约束 T extends Comparable,确保传入的列表元素都能调用 compareTo 方法,从而实现通用的 “找最大值” 逻辑。同时兼容了 Dart 内置类型(如 int、String)和自定义类型。
四、泛型的高级用法:多类型参数
泛型支持同时声明多个类型参数,用逗号分隔,适用于需要关联多种类型的场景(如键值对)。
// 多类型参数:K 表示键类型,V 表示值类型
class Pair<K, V> {
K key;
V value;
Pair(this.key, this.value);
@override
String toString() => "$key: $value";
}
void main() {
// 键为 String,值为 int
Pair<String, int> agePair = Pair("age", 20);
print(agePair); // 输出:age: 20
// 键为 int,值为 String
Pair<int, String> idPair = Pair(1001, "Alice");
print(idPair); // 输出:1001: Alice
}
Dart 中的很多内置类(如 Map<K, V>)都使用了多类型参数,例如 Map<String, int> 表示键为字符串、值为整数的映射。
五、泛型的实际应用场景
泛型在实际开发中非常常见,尤其是在以下场景:
- 容器类:如
List<T>、Set<T>、Map<K, V>等集合类,通过泛型实现对任意类型元素的存储和操作。 - 网络请求工具:封装通用的网络请求函数,指定请求返回的数据类型:
Future<T> fetchData<T>(String url) async {
// 通用网络请求逻辑
// ...
return parsedData as T;
}
// 使用:指定返回类型为 User
fetchData<User>("/api/user").then((user) {
print(user.name);
});
- 状态管理:在 Flutter 状态管理中,用泛型定义通用的状态容器:
class StateHolder<T> {
T state;
StateHolder(this.state);
void update(T newState) {
state = newState;
// 通知监听者
}
}