dart学习第 12 节: 泛型 —— 写出 “通用” 的代码​

109 阅读6分钟

今天我们要学习的泛型(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
}

这种写法的问题很明显:逻辑完全相同,却要为每种类型重复实现一次。如果需要支持更多类型(如 boolList),代码会变得臃肿不堪。

更严重的是,如果用 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 内置类型(如 intString)和自定义类型。



四、泛型的高级用法:多类型参数

泛型支持同时声明多个类型参数,用逗号分隔,适用于需要关联多种类型的场景(如键值对)。

// 多类型参数: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> 表示键为字符串、值为整数的映射。



五、泛型的实际应用场景

泛型在实际开发中非常常见,尤其是在以下场景:

  1. 容器类:如 List<T>Set<T>Map<K, V> 等集合类,通过泛型实现对任意类型元素的存储和操作。
  2. 网络请求工具:封装通用的网络请求函数,指定请求返回的数据类型:
Future<T> fetchData<T>(String url) async {
  // 通用网络请求逻辑
  // ...
  return parsedData as T;
}
// 使用:指定返回类型为 User
fetchData<User>("/api/user").then((user) {
  print(user.name);
});
  1. 状态管理:在 Flutter 状态管理中,用泛型定义通用的状态容器:
class StateHolder<T> {
  T state;
  StateHolder(this.state);
  void update(T newState) {
    state = newState;
    // 通知监听者
  }
}