Dart之泛型
一、前言
什么是泛型呢?
第一次看到List<T>、Future<T>这样的写法,可能会觉得尖括号里藏着一个神秘字母。其实它更像快递分拣线上的“包裹类型”:同一条流水线可以处理不同包裹,但每个包裹的类型要先说清楚。
如果没有泛型,一条通用代码要么只能复制很多份,要么只能把所有值都当成dynamic,最后把类型错误留到运行时。泛型做的事情,就是让代码保持复用,同时把类型信息保留下来。
一句话理解:泛型是把“类型”也参数化,让同一份代码安全地服务于多种数据类型。
二、为什么需要泛型
2.1 没有泛型,代码容易重复
假设我们要从列表中取出第一个元素。如果只为整数写一个函数,再为字符串写一个函数,代码很快就会重复:
intfirstInt(List<int>values){returnvalues.first;}StringfirstString(List<String>values){returnvalues.first;}两个函数的逻辑完全一样,只有类型不同。泛型可以把变化的类型提取出来:
Tfirst<T>(List<T>values){returnvalues.first;}调用时,T可以是int,也可以是String,函数逻辑只保留一份。
2.2 没有泛型,类型检查会变弱
如果把参数写成dynamic,代码看起来也能复用:
dynamicfirstValue(List<dynamic>values){returnvalues.first;}但调用者拿到的是dynamic,编译器无法尽早提醒错误:
finalvalue=firstValue([1,2,3]);print(value.toUpperCase());// 运行时才发现 int 没有这个方法泛型则会保留类型:first<int>()的结果是int,first<String>()的结果是String。错误可以更早暴露。
2.3 泛型解决什么问题
| 问题 | 没有泛型 | 使用泛型 |
|---|---|---|
| 代码复用 | 复制多份类型不同的代码 | 一份代码服务多种类型 |
| 类型检查 | 依赖运行时发现错误 | 编译阶段尽早发现 |
| 代码阅读 | dynamic难以判断真实类型 | T表示明确的类型关系 |
三、泛型是什么
3.1 从快递分拣线理解
把一个通用容器想成快递分拣线。
分拣线本身不关心包裹来自哪家店,但它需要知道当前处理的是“书籍包裹”还是“食品包裹”。类型参数T就像贴在分拣线入口的标签:今天这条线处理String,明天也可以处理int。
classBox<T>{finalTvalue;Box(this.value);TgetValue()=>value;}Box<String>表示装字符串的盒子,Box<int>表示装整数的盒子。盒子的结构相同,但每个盒子里的值类型清楚可见。
3.2 类型参数、类型实参和类型约束
泛型中经常出现三个词:
- 类型参数:定义泛型时写的
T,它是一个待确定的类型占位符。 - 类型实参:使用泛型时传入的
String、int等具体类型。 - 类型约束:限制
T必须满足某个上界,例如<T extends num>。
finalstringBox=Box<String>('Dart');finalintBox=Box<int>(100);这里T是类型参数,String和int是类型实参。
四、泛型方法
4.1 定义泛型方法
泛型方法把类型参数放在方法名之前:
TgetFirst<T>(List<T>values){returnvalues.first;}调用时可以显式写出类型:
finalnumber=getFirst<int>([1,2,3]);finalname=getFirst<String>(['Dart','Flutter']);4.2 Dart 可以推断类型
很多时候不需要手动写<int>或<String>,Dart 会根据参数推断T:
finalnumber=getFirst([1,2,3]);finalname=getFirst(['Dart','Flutter']);推断并不等于没有类型。相反,类型仍然存在,只是由编译器帮我们补出来了。
4.3 泛型方法的边界
泛型只抽象类型变化,不会自动解决业务逻辑差异。如果不同类型的处理规则完全不同,强行写成一个泛型方法,反而会让代码难懂。
好的泛型方法应该满足一个特点:算法对类型本身没有特殊偏好,只依赖类型提供的共同能力。
五、泛型类和泛型接口
5.1 泛型类
缓存、容器、结果包装器都适合使用泛型类:
classCache<T>{T?_value;voidsave(Tvalue){_value=value;}T?read(){return_value;}}使用时,缓存的读写类型保持一致:
finalcache=Cache<String>();cache.save('Dart');finalvalue=cache.read();// String?如果误把整数放入Cache<String>,分析器会在编译阶段提示错误。
5.2 泛型接口
数据仓库经常需要对不同实体提供相同的读取约定:
abstractclassRepository<T>{Future<T?>findById(Stringid);Future<List<T>>findAll();}用户仓库和订单仓库可以复用接口结构,同时保留各自的实体类型:
classUserRepositoryimplementsRepository<User>{@overrideFuture<User?>findById(Stringid)async=>null;@overrideFuture<List<User>>findAll()async=>[];}classUser{finalStringname;User(this.name);}Repository<User>表明这个仓库只负责用户数据,调用方不需要再猜返回值是什么。
六、泛型类型约束
6.1 使用 extends 限制能力
有时我们不希望T可以是任意类型,而是要求它具备某些能力。例如求和函数只接受数字:
Tadd<Textendsnum>(Tleft,Tright){return(left+right)asT;}T extends num的意思是:T必须是num或num的子类型,例如int、double。
注意:Dart 的
num运算结果类型可能需要根据实际需求转换或统一为num。约束负责限制输入范围,不会替你决定所有运算结果的精确类型。
更常见也更稳妥的写法是直接返回num:
num addNumbers<Textendsnum>(Tleft,Tright){returnleft+right;}6.2 约束带来的好处
TmaxValue<TextendsComparable<T>>(Tleft,Tright){returnleft.compareTo(right)>=0?left:right;}这里的约束保证T有compareTo()方法,所以函数可以安全比较,而不需要使用dynamic。
6.3 约束不是类型转换
T extends num不是把字符串转换成数字,也不是把所有类型都变成num。它只是告诉编译器:调用这个方法时,T必须属于允许的类型范围。
七、泛型与集合
7.1 List 让元素类型明确
finalList<int>scores=[90,88,95];finalList<String>names=['Ada','Linus'];List<int>只能放整数,List<String>只能放字符串。集合的泛型参数会影响添加元素、读取元素和遍历时的类型推断。
7.2 Map<K, V> 有两个类型参数
finalMap<String,int>scores={'Dart':100,'Flutter':95,};K表示 key 类型,V表示 value 类型。Map<String, int>读起来就是“字符串键、整数值的映射”。
7.3 Future 和 Stream
泛型不只用于集合:
Future<String>loadName()async=>'Dart';Stream<int>count()async*{yield1;yield2;}Future<String>表示未来产生一个字符串,Stream<int>表示持续产生整数。前面学过的异步类型,本身就是泛型类型。
7.4 List 和 List 不应混为一谈
Listvalues=[1,'Dart'];List<dynamic>dynamicValues=[1,'Dart'];List<Object>objectValues=[1,'Dart'];它们都能装不同值,但表达的意图不同。裸List会丢失更多静态类型信息;List<dynamic>明确表示元素按动态类型处理;List<Object>仍然保留“这是对象”的静态边界。
八、常见误区
8.1 泛型不是模板复制
泛型不是 C++ 模板那种简单的文本复制,也不是把代码机械生成很多份。它是一种类型系统机制,用来表达代码与类型之间的关系。
8.2 泛型不等于 Object
Object表示一个值至少是对象,但泛型T表示“调用者传入的具体类型关系”。
Tidentity<T>(Tvalue)=>value;调用identity(1)时,返回值仍然是int;如果把参数和返回值都写成Object,调用方通常还需要额外判断或转换。
8.3 泛型不等于 dynamic
dynamic会关闭一部分静态检查,适合确实需要动态调用的边界场景。泛型则尽量保留类型信息,让编译器继续帮助你。
可以这样记:dynamic是“暂时不检查”,泛型是“换一种类型继续检查”。
8.4 不要为了泛型而泛型
如果一个类只服务于一种类型,而且没有复用需求,直接写具体类型通常更清楚。泛型的目标是表达真实的类型变化,不是让每个类都出现一个T。
九、总结
这一篇我们系统学习了 Dart 中的泛型。
泛型把类型变成参数,让同一份方法、类或接口可以复用,同时保留静态类型检查。List<T>、Map<K, V>、Future<T>和Stream<T>都是泛型在 Dart 中的实际应用。
使用<T extends num>或<T extends Comparable<T>>可以限制类型范围,让泛型代码安全地使用某些共同能力。
最后记住三句话:
- 泛型让代码复用和类型安全同时存在。
T是类型参数,调用时可以由 Dart 自动推断。- 泛型不是
dynamic,也不是越复杂越好,要围绕真实的类型关系使用。