Dart 泛型

如果你查看基础数组类型 List 的 API 文档,会发现它的真正的类型其实是 List<E><...> 这种尖括号标记代表这是一个泛型类型(参数化类型), 带有形式类型参数的类型。 按照惯例,绝大多数类型变量都使用单个字母命名,例如 ETSKV

为什么要使用泛型?

  • 泛型最核心的作用是保障类型安全
  • 正确指定泛型可以生成更优的运行代码;
  • 使用泛型能大幅减少代码重复。

如果你希望一个列表只存放字符串,就可以声明为 List<String>,这就是字符串列表

一旦定义了这个是字符串的列表,如果往它里放入非字符串类型属于错误操作。

// 静态类型检查报错
var names = <String>[];
names.addAll(['Seth', 'Kathy', 'Lars']);
names.add(42); // 编译错误:42 不是字符串Code language: PHP (php)
D:\dartdemo\firstdart>dart run
Building package executable...
Failed to build firstdart:firstdart:
bin/firstdart.dart:5:12: Error: The argument type 'int' can't be assigned to the parameter type 'String'.
        names.add(42); // 编译错误:42 不是字符串Code language: PHP (php)

减少代码冗余

假设你需要一个缓存对象的抽象接口:

abstract class ObjectCache {
  Object getByKey(String key);
  void setByKey(String key, Object value);
}
Code language: JavaScript (javascript)

后续你又需要仅存储字符串的缓存接口,只能再新建一套:

abstract class StringCache {
  String getByKey(String key);
  void setByKey(String key, String value);
}
Code language: Dart (dart)

之后还会需要数字专用缓存…… 不断重复编写几乎一样的代码。

例如你可能会这样:

abstract class IntCache {
  int getByKey(String key);
  void setByKey(String key, int value);
}Code language: Dart (dart)

泛型可以一次性解决这个问题,只需要定义一个带类型参数的通用接口:

abstract class Cache<T> {
  T getByKey(String key);
  void setByKey(String key, T value);
}Code language: Dart (dart)

这里的 T 是占位类型,代表开发者后续传入的任意实际类型。

具体例子

// 泛型抽象缓存基类
abstract class Cache<T> {
  T? getByKey(String key);
  void setByKey(String key, T value);
  void remove(String key);
}

// 内存缓存实现(基于Map)
class MemoryCache<T> implements Cache<T> {
  final Map<String, T> _storage = {};

  @override
  T? getByKey(String key) {
    return _storage[key];
  }

  @override
  void setByKey(String key, T value) {
    _storage[key] = value;
  }

  @override
  void remove(String key) {
    _storage.remove(key);
  }
}

void main() {
  // 1. 字符串缓存示例
  final Cache<String> strCache = MemoryCache<String>();
  strCache.setByKey("name", "Tom");
  print(strCache.getByKey("name")); // Tom

  // 2. 数字int缓存
  final Cache<int> intCache = MemoryCache<int>();
  intCache.setByKey("age", 26);
  print(intCache.getByKey("age")); // 26

  // 3. 自定义对象缓存
  final Cache<User> userCache = MemoryCache<User>();
  userCache.setByKey("u1", User("Jack", 18));
  final User? user = userCache.getByKey("u1");
  print(user?.name); // Jack

  // 删除测试
  userCache.remove("u1");
  print(userCache.getByKey("u1")); // null
}

// 自定义实体类
class User {
  final String name;
  final int age;
  User(this.name, this.age);
}Code language: Dart (dart)

使用泛型集合字面量

List、Set、Map 字面量都支持参数化泛型:

  • List / Set:在左方括号前写 <类型>
  • Map:在左大括号前写 <键类型, 值类型>

示例:

// 字符串列表
var names = <String>['Seth', 'Kathy', 'Lars'];

// 字符串集合(去重)
var uniqueNames = <String>{'Seth', 'Kathy', 'Lars'};

// 字符串键、字符串值的字典
var pages = <String, String>{
  'index.html': 'Homepage',
  'robots.txt': '爬虫提示文件',
  'humans.txt': '面向人类开发者的说明文件',
};
Code language: PHP (php)

运行例子

void main() {
  // 1. List<String> 字符串列表:有序、允许重复、可按下标取值
  var names = <String>['Seth', 'Kathy', 'Lars', 'Seth'];
  print("=== List 列表 ===");
  print(names); // [Seth, Kathy, Lars, Seth] 允许重复
  print(names[0]); // 按下标取第一个:Seth
  names.add("Mike"); // 末尾新增元素
  print("新增后:$names");

  // 2. Set<String> 字符串集合:无序、自动去重、无下标
  var uniqueNames = <String>{'Seth', 'Kathy', 'Lars', 'Seth'};
  print("\n=== Set 集合(自动去重)===");
  print(uniqueNames); // {Seth, Kathy, Lars} 重复的Seth自动删掉
  print(uniqueNames.contains("Kathy")); // 判断是否存在:true
  uniqueNames.remove("Lars");
  print("删除Lars后:$uniqueNames");

  // 3. Map<String, String> 键值对字典:键唯一,通过key取值
  var pages = <String, String>{
    'index.html': 'Homepage',
    'robots.txt': '爬虫提示文件',
    'humans.txt': '面向人类开发者的说明文件',
  };
  print("\n=== Map 字典 ===");
  print(pages);
  print(pages['index.html']); // 通过key取值:Homepage
  pages["about.html"] = "关于页面"; // 新增键值
  print("新增about.html后:$pages");

  // 常用遍历示例
  print("\n=== 遍历List ===");
  for (var name in names) {
    print("- $name");
  }

  print("\n=== 遍历Map所有键值 ===");
  for (var entry in pages.entries) {
    print("文件:${entry.key} → 说明:${entry.value}");
  }
}Code language: Dart (dart)

构造函数使用泛型参数

调用构造器时,在类名后方尖括号内指定泛型类型:

var nameSet = Set<String>.of(names);Code language: JavaScript (javascript)

下面代码创建一个键为 int、值为 View 类型的字典:

var views = SplayTreeMap<int, View>();
Code language: HTML, XML (xml)
import 'dart:collection';

void main() {
  var names = <String>['Seth', 'Kathy', 'Lars', 'Seth'];
  var nameSet = Set<String>.of(names);
  print("原始列表:$names");
  print("去重Set:$nameSet");

  // SplayTreeMap 需要 dart:collection
  var views = SplayTreeMap<int, String>();
  views[5] = "设置页面";
  views[2] = "首页";
  views[9] = "我的页面";
  views[1] = "启动页";

  print("\n有序TreeMap:");
  for (var e in views.entries) {
    print("${e.key} : ${e.value}");
  }
}Code language: PHP (php)

泛型集合的运行时类型

类型实化 reified

Dart 的泛型是 实化(reified)的:运行时会完整保留泛型类型信息。

你可以在运行时直接判断集合的具体泛型类型:

void main() {
    var names = <String>[];
	names.addAll(['Seth', 'Kathy', 'Lars']);
	print(names is List<String>); // 输出 true
}Code language: PHP (php)

补充对比:Java 的泛型采用类型擦除,运行时会抹除泛型参数。Java 中只能判断对象是否是 List,无法区分 List<String>List<int>

* 限制泛型参数类型

边界 bound,由于读者还没学到class,所以本部分内容了解就可以。

定义泛型时,可以限定传入的类型必须是某个父类的子类,这种约束称为类型边界,使用 extends 关键字实现。

1 限制非空类型

让泛型必须是非空类型(默认上限是可空 Object?):

class Foo<T extends Object> {
  // 传入 T 的类型不能为可空类型
}
Code language: JavaScript (javascript)

2 限定为指定父类的子类

使用 extends 指定基类,这样你可以直接调用基类的所有成员方法:

class Foo<T extends SomeBaseClass> {
  String toString() => "实例 Foo<$T>";
}

class Extender extends SomeBaseClass {}
Code language: JavaScript (javascript)

合法传参:父类本身、任意子类

var someBaseClassFoo = Foo<SomeBaseClass>();
var extenderFoo = Foo<Extender>();
Code language: HTML, XML (xml)

不传泛型参数时,默认使用边界类型:

var foo = Foo();
print(foo); // 输出:实例 Foo<SomeBaseClass>
Code language: PHP (php)

传入不相关类型会直接静态报错:

var foo = Foo<Object>(); // 编译报错:Object 不是 SomeBaseClass 子类
Code language: JavaScript (javascript)

* 自引用类型边界(F-bound 有界泛型)

约束泛型时,可以让边界引用自身类型参数,形成自约束,称为 F-bound。

标准示例:Comparable<T> 可比较接口

abstract interface class Comparable<T> {
  int compareTo(T o);
}

// T 必须实现 Comparable<T>,只能和同类型对象比较
int compareAndOffset<T extends Comparable<T>>(T t1, T t2) =>
    t1.compareTo(t2) + 1;

class A implements Comparable<A> {
  @override
  int compareTo(A other) {
    // 实现比较逻辑
    return 0;
  }
}

int res = compareAndOffset(A(), A());
Code language: PHP (php)

约束 T extends Comparable<T> 的含义:T 只能和自身同类型实例做比较。

* 泛型方法

普通函数、方法同样支持泛型类型参数:

T first<T>(List<T> ts) {
  // 前置校验逻辑
  T tmp = ts[0];
  // 后续处理逻辑
  return tmp;
}
Code language: PHP (php)

看具体例子

T first<T>(List<T> ts) {
  if (ts.isEmpty) {
    throw ArgumentError("List cannot be empty");
  }
  T tmp = ts[0];
  return tmp;
}

void main() {
  List<String> strList = ["apple", "banana", "orange"];
  String firstStr = first(strList);
  print(firstStr); //apple

  List<int> numList = [10, 20, 30];
  int firstNum = first(numList);
  print(firstNum);//10
}Code language: Dart (dart)

泛型参数 <T> 作用在三处:

  1. 函数返回值类型 T
  2. 入参类型 List<T>
  3. 局部变量类型 T tmp

Previous:

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注