一、泛型的核心本质与基础回顾

很多人刚接触泛型时,会觉得它是个“高大上”的概念,其实完全不是。泛型本质就是给代码加个“类型模板”——比如你做一个装东西的盒子,普通盒子只能装指定类型的物品(比如只能装苹果),而泛型盒子可以装任意类型,还能保证装进去的和拿出来的是同一种类型,不会出现“装苹果拿梨”的乱序问题。

先给大家看个最基础的泛型示例,所有示例统一使用Dart技术栈,代码开头会标注:

// 技术栈:Dart 3.0+
// 基础泛型类:一个可以装任意类型的盒子
class Box<T> {
  // 盒子里存放的物品,类型由外部指定(T就是类型模板)
  T? item;

  // 放物品:只能放和T同类型的东西
  void put(T newItem) {
    item = newItem;
  }

  // 拿物品:拿出来的类型和放进去的完全一致
  T? take() {
    return item;
  }
}

这个示例里,T就是泛型参数,相当于一个“类型占位符”。你用的时候可以指定Box<int>(装整数的盒子)、Box<String>(装字符串的盒子),编译器会自动帮你检查类型,不会出现把字符串放进整数盒子的错误。

基础泛型的作用大家都懂,但如果泛型只用到这一步,那Dart的泛型系统就太浪费了。接下来我们讲高级用法。

二、Dart泛型的高级用法

2.1 泛型的类型约束:给模板加“限制条件”

有时候我们不希望泛型能支持所有类型,比如做一个“能放动物的盒子”,总不能放进去汽车吧?这时候就需要给泛型加类型约束——限定T必须是某个类型的子类。

Dart里用extends关键字给泛型加约束,比如:

// 技术栈:Dart 3.0+
// 先定义一个父类:动物
abstract class Animal {
  // 动物都会叫
  void makeSound();
}

// 定义狗类,继承动物
class Dog extends Animal {
  @override
  void makeSound() {
    print('汪汪汪');
  }
}

// 定义汽车类,不继承动物
class Car {
  void drive() {
    print('呜呜呜');
  }
}

// 带类型约束的泛型盒子:只能放动物
class AnimalBox<T extends Animal> {
  T? animal;

  void put(T newAnimal) {
    animal = newAnimal;
  }

  // 因为T继承了Animal,所以可以直接调用Animal的方法
  void makeAnimalSound() {
    animal?.makeSound();
  }
}

// 使用示例
void main() {
  // 正确:狗是动物,符合约束
  AnimalBox<Dog> dogBox = AnimalBox();
  dogBox.put(Dog());
  dogBox.makeAnimalSound(); // 输出:汪汪汪

  // 错误:汽车不是动物,不符合约束,编译器会报错
  // AnimalBox<Car> carBox = AnimalBox();
}

这个例子里,T extends Animal就给泛型加了限制:只有继承Animal的类型才能用这个盒子。编译器会在编译阶段就检查类型是否符合要求,避免运行时出现错误。

2.2 泛型函数:方法级别的类型模板

除了类可以用泛型,Dart的函数也可以用泛型,适合那些“方法逻辑通用,但类型需要动态指定”的场景。比如写一个“从列表里随机取一个元素”的方法,这个方法可以处理整数列表、字符串列表,逻辑完全一样,只是类型不同,用泛型函数就很合适。

示例:

// 技术栈:Dart 3.0+
import 'dart:math';

// 泛型函数:从任意类型的列表中随机取一个元素
// <T>是函数的泛型参数,List<T>是输入参数的类型,T是返回值的类型
T? getRandomElement<T>(List<T> list) {
  // 列表为空时返回null
  if (list.isEmpty) return null;
  // 生成0到列表长度-1的随机索引
  int randomIndex = Random().nextInt(list.length);
  // 返回随机位置的元素,类型是T,和输入列表的元素类型一致
  return list[randomIndex];
}

// 使用示例
void main() {
  // 随机取整数
  List<int> numbers = [1, 2, 3, 4, 5];
  int? randomNumber = getRandomElement(numbers);
  print('随机整数:$randomNumber');

  // 随机取字符串
  List<String> fruits = ['苹果', '香蕉', '橙子'];
  String? randomFruit = getRandomElement(fruits);
  print('随机水果:$randomFruit');
}

这个泛型函数的好处是:你不用为每种类型写一个单独的方法,一套逻辑适配所有类型,还能保证类型安全——比如你给getRandomElement传一个整数列表,返回值肯定是整数,不会是字符串。

2.3 泛型的协变与逆变:解决类型转换的坑

很多人在写泛型代码时会遇到一个奇怪的问题:Dog是Animal的子类,那Box<Dog>是不是Box<Animal>的子类?答案是:在Dart里,默认情况下是不允许的。

比如下面的代码会报错:

// 技术栈:Dart 3.0+
class Animal {}
class Dog extends Animal {}

void main() {
  Box<Dog> dogBox = Box();
  // 错误:不能把Box<Dog>赋值给Box<Animal>
  // Box<Animal> animalBox = dogBox;
}

这是因为Dart的泛型默认是“不变”的,也就是Box<T>和Box<S>(T是S的子类)没有继承关系。那什么时候需要让它们有继承关系呢?比如你写一个方法,需要接收一个“能装动物的盒子”,这时候传一个“装狗的盒子”应该是可以的,这时候就需要用到协变。

Dart里用out关键字标记协变的泛型参数,用in关键字标记逆变的泛型参数:

  • 协变(out):表示这个泛型参数只会被“输出”(比如作为方法的返回值),不会被“输入”(比如作为方法的参数)。这时候子类的泛型类可以赋值给父类的泛型类。
  • 逆变(in):表示这个泛型参数只会被“输入”(比如作为方法的参数),不会被“输出”(比如作为方法的返回值)。这时候父类的泛型类可以赋值给子类的泛型类。

示例:

// 技术栈:Dart 3.0+
class Animal {}
class Dog extends Animal {}

// 协变的泛型类:T只会被输出(take方法的返回值)
class CovariantBox<out T> {
  T? item;

  void put(T newItem) {
    item = newItem;
  }

  T? take() {
    return item;
  }
}

// 逆变的泛型类:T只会被输入(put方法的参数)
class ContravariantBox<in T> {
  T? item;

  void put(T newItem) {
    item = newItem;
  }

  T? take() {
    return item;
  }
}

// 使用示例
void main() {
  // 协变测试:Box<Dog>是Box<Animal>的子类
  CovariantBox<Dog> dogBox = CovariantBox();
  CovariantBox<Animal> animalBox = dogBox; // 合法

  // 逆变测试:Box<Animal>是Box<Dog>的子类
  ContravariantBox<Animal> animalBox2 = ContravariantBox();
  ContravariantBox<Dog> dogBox2 = animalBox2; // 合法
}

协变和逆变看起来复杂,其实记住一个原则就行:“只出不进用协变,只进不出用逆变”。

三、跨模块类型约束实践

很多项目会拆分成多个模块,比如一个电商项目,会有商品模块、用户模块、订单模块,不同模块之间需要互相调用,这时候如果没有类型约束,很容易出现“模块A传的类型,模块B不识别”的问题。

比如订单模块需要一个“可支付的商品”,这个商品需要有价格、名称两个属性,同时需要实现payable方法。如果没有类型约束,你随便传一个没有这些属性的对象进去,订单模块就会出错。

我们可以用泛型的类型约束来解决这个问题,让跨模块的调用更安全。

3.1 定义公共的约束接口

首先,我们在公共模块里定义一个约束接口,用来规定“可支付商品”必须具备的属性和方法:

// 技术栈:Dart 3.0+
// 公共模块的代码:定义可支付的约束接口
abstract class PayableItem {
  // 商品价格(必须有)
  double get price;
  // 商品名称(必须有)
  String get name;
  // 支付方法(必须实现)
  void pay();
}

3.2 商品模块实现约束接口

然后,商品模块的商品类需要实现这个PayableItem接口:

// 技术栈:Dart 3.0+
// 商品模块的代码:商品类实现PayableItem接口
class Product implements PayableItem {
  final String productId;
  @override
  final String name;
  @override
  final double price;

  Product({required this.productId, required this.name, required this.price});

  @override
  void pay() {
    print('支付商品:$name,价格:$price 元');
  }
}

3.3 订单模块用泛型约束接收参数

最后,订单模块的订单类用泛型约束,限定传入的商品必须是PayableItem的实现类:

// 技术栈:Dart 3.0+
// 订单模块的代码:订单类用泛型约束接收商品
class Order<T extends PayableItem> {
  final List<T> items = [];

  // 添加商品:只能添加符合PayableItem约束的商品
  void addItem(T item) {
    items.add(item);
  }

  // 计算总金额:因为T有price属性,所以可以直接调用
  double getTotalPrice() {
    return items.fold(0.0, (sum, item) => sum + item.price);
  }

  // 支付所有商品:因为T有pay方法,所以可以直接调用
  void payAll() {
    for (var item in items) {
      item.pay();
    }
    print('总支付金额:${getTotalPrice()} 元');
  }
}

// 跨模块使用示例
void main() {
  // 商品模块的商品
  Product apple = Product(productId: 'p001', name: '苹果', price: 5.0);
  Product banana = Product(productId: 'p002', name: '香蕉', price: 3.0);

  // 订单模块的订单,指定商品类型是Product(符合PayableItem约束)
  Order<Product> order = Order();
  order.addItem(apple);
  order.addItem(banana);

  order.payAll();
  // 输出:
  // 支付商品:苹果,价格:5.0 元
  // 支付商品:香蕉,价格:3.0 元
  // 总支付金额:8.0 元

  // 错误:如果传一个不符合约束的对象,编译器会报错
  // class NonPayable {}
  // Order<NonPayable> badOrder = Order();
}

这个实践的好处是:不同模块之间的依赖是明确的,订单模块不需要知道商品模块的具体实现,只需要知道商品必须符合PayableItem的约束;商品模块只要实现了这个约束,就可以被订单模块使用,模块之间的耦合度大大降低,同时类型安全也得到了保证。

四、应用场景、技术优缺点与注意事项

4.1 应用场景

泛型的高级用法和跨模块约束适合很多场景,比如:

  1. 通用工具类:比如列表处理、缓存、网络请求类,一套逻辑适配所有类型;
  2. 跨模块调用:比如大型项目的模块拆分,用泛型约束保证模块之间的类型安全;
  3. 框架开发:比如UI框架、状态管理框架,需要支持多种类型的组件或状态;
  4. 数据处理:比如序列化、反序列化,用泛型指定数据的类型,避免类型转换错误。

4.2 技术优缺点

优点

  1. 类型安全:编译器在编译阶段就能检查类型错误,减少运行时的bug;
  2. 代码复用:一套逻辑适配多种类型,不用写重复代码;
  3. 降低耦合:跨模块用约束接口解耦,模块之间只依赖接口,不依赖具体实现;
  4. 可读性强:泛型参数明确了类型的要求,代码逻辑更清晰。

缺点

  1. 学习成本:协变、逆变、跨模块约束等概念对新手来说有一定难度;
  2. 代码复杂度:如果泛型用得太多,代码会变得复杂,维护起来比较麻烦;
  3. 编译时间:泛型会增加编译器的工作量,可能会稍微增加编译时间。

4.3 注意事项

  1. 不要过度使用泛型:如果某个逻辑只用到一种类型,就不要用泛型,避免代码不必要的复杂;
  2. 泛型约束要合理:约束不能太严(限制了太多类型,导致代码复用性降低),也不能太松(起不到约束的作用);
  3. 协变和逆变要谨慎:只有在需要类型转换的时候才用,否则容易出现类型错误;
  4. 跨模块约束要统一:公共的约束接口要保持稳定,不要随便修改,否则会影响所有依赖它的模块。

五、文章总结

Dart的泛型系统不只是简单的类型模板,它的高级用法(类型约束、泛型函数、协变逆变)可以让代码更灵活、更安全;跨模块的类型约束实践则是大型项目中保证模块协作安全、降低耦合的重要手段。

泛型的核心是“类型安全”和“代码复用”,在实际开发中,我们需要根据场景合理使用,不要为了用泛型而用泛型。掌握好泛型的高级用法和跨模块约束,能让你的Dart代码更健壮、更易维护。