现在位置: 首页 > Dart 教程 > 正文

Dart 泛型

泛型(Generics)让你在编写代码时不指定具体类型,而是用类型参数占位,在使用时再确定类型。

泛型的核心价值是类型安全代码复用——一份代码适配多种类型,同时编译器帮你检查类型错误。


为什么需要泛型

先看一个没有泛型时的痛点。

实例

没有泛型的写法:

// 没有泛型:只能存放 Object,取出时需要手动转型
class IntBox {
  int value;
  IntBox(this.value);
}

class StringBox {
  String value;
  StringBox(this.value);
}

// 每新增一种类型就要写一个新的 Box 类,代码大量重复

void main() {
  var intBox = IntBox(42);
  var stringBox = StringBox('RUNOOB');

  print('整数: ${intBox.value}');
  print('字符串: ${stringBox.value}');
}

有没有办法写一个 Box 类,既能装 int、又能装 String,还能在编译时保证类型安全?

这就是泛型要解决的问题。


泛型类

泛型类在类名后用尖括号 <> 声明类型参数,类内部使用这个类型参数。

实例

// <T> 是类型参数,T 是一个占位符,代表"某种类型"
// 约定俗成的命名: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('Box 中的值: $value (类型: ${value.runtimeType})');
  }
}

void main() {
  // 使用泛型类时指定具体类型
  var intBox = Box<int>(42);
  var stringBox = Box<String>('RUNOOB Dart 教程');
  var doubleBox = Box<double>(3.14);

  intBox.printValue();
  stringBox.printValue();
  doubleBox.printValue();

  // 类型安全:下面的代码会在编译时报错
  // intBox.setValue('hello');  // 错误:String 不能赋值给 int

  // 类型推断:Dart 可以根据构造函数参数推断类型
  var autoBox = Box('自动推断为 String');
  print('推断类型: ${autoBox.value.runtimeType}');
}
Box 中的值: 42 (类型: int)
Box 中的值: RUNOOB Dart 教程 (类型: String)
Box 中的值: 3.14 (类型: double)
推断类型: String

多个类型参数

泛型类可以有多个类型参数。

实例

// K 表示键的类型,V 表示值的类型
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>('runoob', 10);
  var pair2 = Pair('score', 95.5);  // 类型推断

  print(pair1);
  print(pair2);
}
Pair(runoob: 10)
Pair(score: 95.5)

泛型方法

除了泛型类,函数和方法也可以使用泛型。

实例

// 泛型函数:返回类型和参数类型一致
// 在返回值类型前声明 <T>
T firstElement<T>(List<T> list) {
  if (list.isEmpty) {
    throw ArgumentError('列表不能为空');
  }
  return list[0];
}

// 泛型函数:交换两个值
void swap<T>(List<T> list, int i, int j) {
  T temp = list[i];
  list[i] = list[j];
  list[j] = temp;
}

void main() {
  // 调用时 Dart 会自动推断类型
  var names = ['runoob', 'Dart', 'Flutter'];
  print('第一个名字: ${firstElement(names)}');

  var scores = [95, 88, 72];
  print('第一个分数: ${firstElement(scores)}');

  // 交换元素
  var items = ['A', 'B', 'C'];
  print('交换前: $items');
  swap(items, 0, 2);
  print('交换后: $items');
}
第一个名字: runoob
第一个分数: 95
交换前: [A, B, C]
交换后: [C, B, A]

类型约束 extends

有时候你希望类型参数只能是某些类型的子类型。

使用 extends 关键字可以对泛型参数施加约束。

实例

// 定义一个数值计算器
// <T extends num> 约束 T 必须是 num 或其子类(int、double)
class Calculator<T extends num> {
  T a;
  T b;

  Calculator(this.a, this.b);

  // 因为约束了 T extends num,所以可以安全地使用 num 的方法
  double add() => (a + b).toDouble();
  double subtract() => (a - b).toDouble();
  double multiply() => (a * b).toDouble();
  double divide() => a / b;

  void printOperations() {
    print('RUNOOB 计算器: $a 和 $b');
    print('  加: ${add()}');
    print('  减: ${subtract()}');
    print('  乘: ${multiply()}');
    print('  除: ${divide().toStringAsFixed(2)}');
  }
}

void main() {
  var intCalc = Calculator<int>(10, 3);
  intCalc.printOperations();

  var doubleCalc = Calculator<double>(3.5, 1.5);
  doubleCalc.printOperations();

  // 下面这行会报错:String 不是 num 的子类
  // var strCalc = Calculator<String>('a', 'b');  // 错误!
}
RUNOOB 计算器: 10 和 3
  加: 13.0
  减: 7.0
  乘: 30.0
  除: 3.33
RUNOOB 计算器: 3.5 和 1.5
  加: 5.0
  减: 2.0
  乘: 5.25
  除: 2.33

类型约束让你既能保持泛型的灵活性,又能使用约束类型的方法。比如上面约束 T extends num,就可以在方法体内使用 +、-、*、/ 等运算符,因为 num 类型支持这些操作。


泛型集合的使用

Dart 的核心集合类(List、Set、Map)都是泛型的。

使用泛型集合可以获得编译期的类型检查。

实例

void main() {
  // 指定元素类型为 String
  List<String> names = ['runoob', 'Dart', 'Flutter'];
  // names.add(42);  // 错误:int 不能添加到 List<String>

  // 指定键值类型
  Map<String, int> scores = {
    'runoob': 95,
    'Alice': 87,
  };
  // scores['Bob'] = '优秀';  // 错误:String 不能赋值给 int

  // 指定 Set 元素类型
  Set<double> prices = {9.99, 19.99, 29.99};

  // 泛型集合的函数式操作
  List<int> numbers = [1, 2, 3, 4, 5, 6];

  // 类型安全的过滤和映射
  List<int> evenNumbers = numbers.where((n) => n % 2 == 0).toList();
  List<String> labels = numbers.map((n) => 'RUNOOB-$n').toList();

  print('偶数: $evenNumbers');
  print('标签: $labels');

  // 使用 fold 进行类型安全的累积计算
  int sum = numbers.fold<int>(0, (prev, n) => prev + n);
  print('总和: $sum');
}
偶数: [2, 4, 6]
标签: [RUNOOB-1, RUNOOB-2, RUNOOB-3, RUNOOB-4, RUNOOB-5, RUNOOB-6]
总和: 21

实际应用:泛型仓库模式

泛型在实际开发中最经典的应用之一是"仓库模式"(Repository Pattern)。

实例

// 定义一个通用仓库接口
abstract class Repository<T> {
  void add(T item);
  void remove(T item);
  T? findById(String id);
  List<T> getAll();
}

// 用户模型
class User {
  String id;
  String name;

  User(this.id, this.name);

  @override
  String toString() => 'User($id, $name)';
}

// 商品模型
class Product {
  String id;
  String name;
  double price;

  Product(this.id, this.name, this.price);

  @override
  String toString() => 'Product($name, ¥$price)';
}

// 具体实现:用户仓库
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);
}

// 具体实现:商品仓库(同样的接口,不同的类型)
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', 'runoob'));
  userRepo.add(User('u2', 'Alice'));

  var productRepo = ProductRepository();
  productRepo.add(Product('p1', 'Dart 教程', 29.99));
  productRepo.add(Product('p2', 'Flutter 教程', 49.99));

  print('用户列表: ${userRepo.getAll()}');
  print('商品列表: ${productRepo.getAll()}');
  print('查找用户 u1: ${userRepo.findById('u1')}');
  print('查找商品 p2: ${productRepo.findById('p2')}');
}
用户列表: [User(u1, runoob), User(u2, Alice)]
商品列表: [Product(Dart 教程, ¥29.99), Product(Flutter 教程, ¥49.99)]
查找用户 u1: User(u1, runoob)
查找商品 p2: Product(Flutter 教程, ¥49.99)

泛型仓库模式让你用统一的接口处理不同类型的数据,既保证了类型安全,又避免了重复代码。