news 2026/9/11 4:49:13

Dart之泛型

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Dart之泛型

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>()的结果是intfirst<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,它是一个待确定的类型占位符。
  • 类型实参:使用泛型时传入的Stringint等具体类型。
  • 类型约束:限制T必须满足某个上界,例如<T extends num>
finalstringBox=Box<String>('Dart');finalintBox=Box<int>(100);

这里T是类型参数,Stringint是类型实参。

四、泛型方法

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必须是numnum的子类型,例如intdouble

注意:Dart 的num运算结果类型可能需要根据实际需求转换或统一为num。约束负责限制输入范围,不会替你决定所有运算结果的精确类型。

更常见也更稳妥的写法是直接返回num

num addNumbers<Textendsnum>(Tleft,Tright){returnleft+right;}

6.2 约束带来的好处

TmaxValue<TextendsComparable<T>>(Tleft,Tright){returnleft.compareTo(right)>=0?left:right;}

这里的约束保证TcompareTo()方法,所以函数可以安全比较,而不需要使用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,也不是越复杂越好,要围绕真实的类型关系使用。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/2 0:37:05

ST25R与STSAFE-V:车规NFC数字钥匙的完整技术链路解析

刚参加完一场ST25R与STSAFE-V的专题研讨会&#xff0c;信息密度比预想高不少。做汽车NFC这几年&#xff0c;大家私下讨论最多的就两件事&#xff1a;一是读卡器前端怎么选型、天线怎么调&#xff0c;二是安全芯片怎么和读卡器形成闭环。这次研讨会的案例从手机数字钥匙讲到充电…

作者头像 李华
网站建设 2026/9/2 3:16:35

多用户接入架构全解析:从并发连接到分布式会话管理

简介&#xff1a;本资源是面向5G通信系统研究者与无线通信方向研究生的MUSA&#xff08;Multi-User Shared Access&#xff09;技术入门实践材料&#xff0c;聚焦非正交多址接入在提升频谱效率、降低时延及支撑海量物联网连接等核心问题上的实现路径。压缩包仅含1个MATLAB脚本文…

作者头像 李华
网站建设 2026/9/5 13:39:33

杜邦Pyralux ML高导热层压板:兼顾散热与耐温的功率电子基材解析

1. 这块层压板到底解决了什么问题做电子材料这行的人&#xff0c;看到“高频”“高导热”“极端环境”这几个词凑在一起&#xff0c;第一反应往往不是兴奋&#xff0c;而是头疼。因为这些需求在传统FR-4板上几乎是互相打架的&#xff1a;导热好的材料介电性能往往拉胯&#xff…

作者头像 李华
网站建设 2026/9/6 0:10:48

HN Hiring:把Hacker News招聘帖变成可搜索的结构化数据库

《Show HN: HN Hiring》是最近出现在 Hacker News 上的一个社区项目&#xff0c;名字本身已经把功能说得很清楚&#xff1a;围绕 HN 每月固定的 “Who Is Hiring” 招聘帖&#xff0c;做搜索和筛选。用过 HN 的人应该都有同感&#xff0c;这个系列招聘贴在海外技术圈信息量很大…

作者头像 李华
网站建设 2026/9/3 0:56:37

Claude开放真实数据:从黑盒到白盒的变革与开发者实践

过去这段时间&#xff0c;只要是关注 Claude 生态的开发者&#xff0c;大概率都被同一类问题刷过屏&#xff1a;Claude Code 装不上&#xff0c;命令行报 “claude 不是内部或外部命令”&#xff0c;调用 API 时提示 “unable to connect to anthropic services”&#xff0c;或…

作者头像 李华
网站建设 2026/9/5 23:19:19

基于 PaddleOCR-VL 与 PP-OCRv6 的指定颜色发票验证码识别服务实践记录

一、为什么要做这个服务 在发票查验、票据流转、自动化录入等场景里&#xff0c;验证码并不总是简单的“看图输入字符”。有一类验证码会要求用户只识别某一种颜色的文字&#xff0c;例如提示识别红色、蓝色或黑色字符&#xff0c;而图片里还会混有干扰字符、背景纹理、噪声和…

作者头像 李华