2024 Flutter 重大更新,Dart 宏(Macros)编程开始支持,JSON 序列化有救
说起宏编程可能大家并不陌生,但是这对于 Flutter 和 Dart 开发者来说它一直是一个「遗憾」,这个「遗憾」体现在编辑过程的代码修改支持上,其中最典型的莫过于 Dart 的 JSON 序列化。
举个例子,目前 Dart 语言的 JSON 序列化高度依赖 build_runner
去生成 Dart 代码,例如在实际使用中我们需要:
- 依赖
json_serializable
,通过注解声明一个Event
对象 - 运行
flutter packages pub run build_runner build
生成文件 - 得到
Event.g.dart
文件,在项目中使用它去实现 JSON 的序列化和反序列化
![]() | ![]() |
---|
这里最大的问题在于,我们需要通过命令行去生成一个项目文件,并且这个文件我们还可以随意手动修改,从开发角度来说,这并不优雅也不方便。
而宏声明是用户定义的 Dart 类,它可以实现一个或多个新的内置宏接口,Dart 中的宏是用正常的命令式 Dart 代码来开发,不存在单独的“宏语言”。
大多数宏并不是简单地从头开始生成新代码,而是根据程序的现有属性去添加代码,例如向 Class 添加 JSON 序列化的宏,可能会查看 Class 声明的字段,并从中合成一个
toJson()
,将这些字段序列化为 JSON 对象。
我们首先看一段官方的 Demo , 如下代码所示,可以看到 :
MyState
添加了一个自定义的@AutoDispose()
注解,这是一个开发者自己实现的宏声明,并且继承了State
对象,带有dispose
方法。- 在
MyState
里有多个a
、a2
、b
和c
三个对象,其中a
、a2
、b
都实现了Disposable
接口,都有dispose
方法 - 虽然
a
、a2
、b
和MyState
的dispose();
方法来自不同基类实现,但是基于@AutoDispose()
的实现,在代码调用state.dispose();
时,a
、a2
、b
变量的dispose
方法也会被同步调用
import 'package:macro_proposal/auto_dispose.dart';void main() {var state = MyState(a: ADisposable(), b: BDisposable(), c: 'hello world');state.dispose();
}()
class MyState extends State {final ADisposable a;final ADisposable? a2;final BDisposable b;final String c;MyState({required this.a, this.a2, required this.b, required this.c});String toString() => 'MyState!';
}class State {void dispose() {print('disposing of $this');}
}class ADisposable implements Disposable {void dispose() {print('disposing of ADisposable');}
}class BDisposable implements Disposable {void dispose() {print('disposing of BDisposable');}
}
如下图所示,可以看到,尽管 MyState
没用主动调用 a
、a2
、b
变量的 dispose
方法,并且它们和 MyState
的 dispose
也来自不同基类,但是最终执行所有 dispose
方法都被成功调用,这就是@AutoDispose()
的宏声明实现在编译时对代码进行了调整。
如下图所示是 @AutoDispose()
的宏编程实现,其中 macro
就是一个标志性的宏关键字,剩下的代码可以看到基本就是 dart 脚本的实现, macro
里主要是实现 ClassDeclarationsMacro
和buildDeclarationsForClass
方法,如下代码可以很直观看到关于 super.dispose();
和 disposeCalls
的相关实现。
import 'package:_fe_analyzer_shared/src/macros/api.dart';// Interface for disposable things.
abstract class Disposable {void dispose();
}macro class AutoDispose implements ClassDeclarationsMacro, ClassDefinitionMacro {const AutoDispose();void buildDeclarationsForClass(ClassDeclaration clazz, MemberDeclarationBuilder builder) async {var methods = await builder.methodsOf(clazz);if (methods.any((d) => d.identifier.name == 'dispose')) {// Don't need to add the dispose method, it already exists.return;}builder.declareInType(DeclarationCode.fromParts([// TODO: Remove external once the CFE supports it.'external void dispose();',]));}Future<void> buildDefinitionForClass(ClassDeclaration clazz, TypeDefinitionBuilder builder) async {var disposableIdentifier =// ignore: deprecated_member_useawait builder.resolveIdentifier(Uri.parse('package:macro_proposal/auto_dispose.dart'),'Disposable');var disposableType = await builder.resolve(NamedTypeAnnotationCode(name: disposableIdentifier));var disposeCalls = <Code>[];var fields = await builder.fieldsOf(clazz);for (var field in fields) {var type = await builder.resolve(field.type.code);if (!await type.isSubtypeOf(disposableType)) continue;disposeCalls.add(RawCode.fromParts(['\n',field.identifier,if (field.type.isNullable) '?','.dispose();',]));}// Augment the dispose method by injecting all the new dispose calls after// either a call to `augmented()` or `super.dispose()`, depending on if// there already is an existing body to call.//// If there was an existing body, it is responsible for calling// `super.dispose()`.var disposeMethod = (await builder.methodsOf(clazz)).firstWhere((method) => method.identifier.name == 'dispose');var disposeBuilder = await builder.buildMethod(disposeMethod.identifier);disposeBuilder.augment(FunctionBodyCode.fromParts(['{\n',if (disposeMethod.hasExternal || !disposeMethod.hasBody)'super.dispose();'else'augmented();',...disposeCalls,'}',]));}
}
到这里大家应该可以直观感受到宏编程的魅力,上述 Demo 来自 dart-language 的 macros/example/auto_dispose_main ,其中 bin/
目录下的代码是运行的脚本示例,lib/
目录下的代码是宏编程实现的示例:
https://github.com/dart-lang/language/tree/main/working/macros/example
当然,因为现在是实验性阶段,API 和稳定性还有待商榷,所以想运行这些 Demo 还需要一些额外的处理,比如版本强关联,例如上述的 auto_dispose_main
例子:
-
需要 dart sdk 3.4.0-97.0.dev ,目前你可以通过 master 分支下载这个 dark-sdk https://storage.googleapis.com/dart-archive/channels/main/raw/latest/sdk/dartsdk-macos-arm64-release.zip
-
将 sdk 配置到环境变量,或者进入到 dart sdk 的 bin 目录执行 ./dart --version 检查版本
-
进入上诉的 example 下执行 dart pub get,过程可能会有点长
-
最后,执行
dart --enable-experiment=macros bin/auto_dispose_main.dart
,记得这个 dart 是你指定版本的 dart 。
另外,还有一个第三方例子是来自 millsteed 的 macros ,这是一个简单的 JSON 序列化实现 Demo ,并且可以直接不用额外下载 dark-sdk,通过某个 flutter 内置 dart-sdk 版本就可以满足条件:3.19.0-12.0.pre
:
在本地 Flutter 目录下,切换到
git checkout 3.19.0-12.0.pre
,然后执行 flutter doctor 初始化 dark sdk 即可。
代码的实现很简单,首先看 bin 下的示例,通过 @Model()
将 GetUsersResponse
和 User
声明为 JSON 对象,然后在运行时,宏编程会自动添加 fromJson
和 toJson
方式。
import 'dart:convert';import 'package:macros/model.dart';()
class User {User({required this.username,required this.password,});final String username;final String password;
}()
class GetUsersResponse {GetUsersResponse({required this.users,required this.pageNumber,required this.pageSize,});final List<User> users;final int pageNumber;final int pageSize;
}void main() {const body = '''{"users": [{"username": "ramon","password": "12345678"}],"pageNumber": 1,"pageSize": 30}''';final json = jsonDecode(body) as Map<String, dynamic>;final response = GetUsersResponse.fromJson(json);final ramon = response.users.first;final millsteed = ramon.copyWith(username: 'millsteed', password: '87654321');final newResponse = response.copyWith(users: [...response.users, millsteed]);print(const JsonEncoder.withIndent(' ').convert(newResponse));
}
而 Model
的宏实现就相对复杂一些,但是实际上就是将类似 freezed
/ json_serializable
是实现调整到宏实现了,而最终效果就是,开发者使用起来更加优雅了。
// ignore_for_file: depend_on_referenced_packages, implementation_importsimport 'dart:async';import 'package:_fe_analyzer_shared/src/macros/api.dart';macro class Model implements ClassDeclarationsMacro {const Model();static const _baseTypes = ['bool', 'double', 'int', 'num', 'String'];static const _collectionTypes = ['List'];Future<void> buildDeclarationsForClass(ClassDeclaration classDeclaration,MemberDeclarationBuilder builder,) async {final className = classDeclaration.identifier.name;final fields = await builder.fieldsOf(classDeclaration);final fieldNames = <String>[];final fieldTypes = <String, String>{};final fieldGenerics = <String, List<String>>{};for (final field in fields) {final fieldName = field.identifier.name;fieldNames.add(fieldName);final fieldType = (field.type.code as NamedTypeAnnotationCode).name.name;fieldTypes[fieldName] = fieldType;if (_collectionTypes.contains(fieldType)) {final generics = (field.type.code as NamedTypeAnnotationCode).typeArguments.map((e) => (e as NamedTypeAnnotationCode).name.name).toList();fieldGenerics[fieldName] = generics;}}final fieldTypesWithGenerics = fieldTypes.map((name, type) {final generics = fieldGenerics[name];return MapEntry(name,generics == null ? type : '$type<${generics.join(', ')}>',);},);_buildFromJson(builder, className, fieldNames, fieldTypes, fieldGenerics);_buildToJson(builder, fieldNames, fieldTypes);_buildCopyWith(builder, className, fieldNames, fieldTypesWithGenerics);_buildToString(builder, className, fieldNames);_buildEquals(builder, className, fieldNames);_buildHashCode(builder, fieldNames);}void _buildFromJson(MemberDeclarationBuilder builder,String className,List<String> fieldNames,Map<String, String> fieldTypes,Map<String, List<String>> fieldGenerics,) {final code = ['factory $className.fromJson(Map<String, dynamic> json) {'.indent(2),'return $className('.indent(4),for (final fieldName in fieldNames) ...[if (_baseTypes.contains(fieldTypes[fieldName])) ...["$fieldName: json['$fieldName'] as ${fieldTypes[fieldName]},".indent(6),] else if (_collectionTypes.contains(fieldTypes[fieldName])) ...["$fieldName: (json['$fieldName'] as List<dynamic>)".indent(6),'.whereType<Map<String, dynamic>>()'.indent(10),'.map(${fieldGenerics[fieldName]?.first}.fromJson)'.indent(10),'.toList(),'.indent(10),] else ...['$fieldName: ${fieldTypes[fieldName]}'".fromJson(json['$fieldName'] "'as Map<String, dynamic>),'.indent(6),],],');'.indent(4),'}'.indent(2),].join('\n');builder.declareInType(DeclarationCode.fromString(code));}void _buildToJson(MemberDeclarationBuilder builder,List<String> fieldNames,Map<String, String> fieldTypes,) {final code = ['Map<String, dynamic> toJson() {'.indent(2),'return {'.indent(4),for (final fieldName in fieldNames) ...[if (_baseTypes.contains(fieldTypes[fieldName])) ...["'$fieldName': $fieldName,".indent(6),] else if (_collectionTypes.contains(fieldTypes[fieldName])) ...["'$fieldName': $fieldName.map((e) => e.toJson()).toList(),".indent(6),] else ...["'$fieldName': $fieldName.toJson(),".indent(6),],],'};'.indent(4),'}'.indent(2),].join('\n');builder.declareInType(DeclarationCode.fromString(code));}void _buildCopyWith(MemberDeclarationBuilder builder,String className,List<String> fieldNames,Map<String, String> fieldTypes,) {final code = ['$className copyWith({'.indent(2),for (final fieldName in fieldNames) ...['${fieldTypes[fieldName]}? $fieldName,'.indent(4),],'}) {'.indent(2),'return $className('.indent(4),for (final fieldName in fieldNames) ...['$fieldName: $fieldName ?? this.$fieldName,'.indent(6),],');'.indent(4),'}'.indent(2),].join('\n');builder.declareInType(DeclarationCode.fromString(code));}void _buildToString(MemberDeclarationBuilder builder,String className,List<String> fieldNames,) {final code = ['@override'.indent(2),'String toString() {'.indent(2),"return '$className('".indent(4),for (final fieldName in fieldNames) ...[if (fieldName != fieldNames.last) ...["'$fieldName: \$$fieldName, '".indent(8),] else ...["'$fieldName: \$$fieldName'".indent(8),],],"')';".indent(8),'}'.indent(2),].join('\n');builder.declareInType(DeclarationCode.fromString(code));}void _buildEquals(MemberDeclarationBuilder builder,String className,List<String> fieldNames,) {final code = ['@override'.indent(2),'bool operator ==(Object other) {'.indent(2),'return other is $className &&'.indent(4),'runtimeType == other.runtimeType &&'.indent(8),for (final fieldName in fieldNames) ...[if (fieldName != fieldNames.last) ...['$fieldName == other.$fieldName &&'.indent(8),] else ...['$fieldName == other.$fieldName;'.indent(8),],],'}'.indent(2),].join('\n');builder.declareInType(DeclarationCode.fromString(code));}void _buildHashCode(MemberDeclarationBuilder builder,List<String> fieldNames,) {final code = ['@override'.indent(2),'int get hashCode {'.indent(2),'return Object.hash('.indent(4),'runtimeType,'.indent(6),for (final fieldName in fieldNames) ...['$fieldName,'.indent(6),],');'.indent(4),'}'.indent(2),].join('\n');builder.declareInType(DeclarationCode.fromString(code));}
}extension on String {String indent(int length) {final space = StringBuffer();for (var i = 0; i < length; i++) {space.write(' ');}return '$space$this';}
}
目前宏还处于试验性质的阶段,所以 API 还在调整,这也是为什么上面的例子需要指定 dart 版本的原因,另外宏目前规划里还有一些要求,例如
- 所有宏构造函数都必须标记为
const
- 所有宏必须至少实现其中一个
Macro
接口 - 宏不能是抽象对象
- 宏 class 不能由其他宏生成
- 宏 class 不能包含泛型类型参数
- 每个宏接口都需要声明宏类必须实现的方法,例如,在声明阶段应用的
ClassDeclarationsMacro
及其buildDeclarationsForClass
方法。
未来规划里,宏 API 可能会作为 Pub 包提供,通过库 dart:_macros
来提供支持 ,具体还要等正式发布时 dart 团队的决策。
总的来说,这对于 dart 和 flutter 是一个重大的厉害消息,虽然宏编程并不是什么新鲜概念,该是 dart 终于可以优雅地实现 JSON 序列化,并且还是用 dart 来实现,这对于 flutter 开发者来说,无疑是最好的新年礼物。
所以,新年快乐~我们节后再见~
相关文章:

2024 Flutter 重大更新,Dart 宏(Macros)编程开始支持,JSON 序列化有救
说起宏编程可能大家并不陌生,但是这对于 Flutter 和 Dart 开发者来说它一直是一个「遗憾」,这个「遗憾」体现在编辑过程的代码修改支持上,其中最典型的莫过于 Dart 的 JSON 序列化。 举个例子,目前 Dart 语言的 JSON 序列化高度依…...

云计算概述(云计算类型、技术驱动力、关键技术、特征、特点、通用点、架构层次)(二)
云计算概述(二) (云计算类型、技术驱动力、关键技术、特征、特点、通用点、架构层次) 目录 零、00时光宝盒 一、云计算类型(以服务的内容或形态来分) 二、云计算的12种技术驱动力 三、云计算的关键技术 四、云计…...

物流平台架构设计与实践
随着电商行业的迅猛发展,物流行业也得到了极大的发展。从最初的传统物流到现在的智慧物流,物流技术和模式也在不断的更新与升级。物流平台作为连接电商和物流的重要媒介,其架构设计和实践显得尤为重要。 一、物流平台架构设计 1. 前端架构设…...

RedHat8.4安装邮件服务器
一、配置发件服务器 1.1 根据现场IP,配置主机名 vim /etc/hosts 192.168.8.120 mail.test.com 将主机名更改为邮件服务器域名mail.test.com 1.2 关闭防火墙,禁止开机启动 systemctl stop firewalld systemctl disable firewalld 1.3 关闭selinux v…...

Linux Shell系列--dirname 去除基本文件名
一、目的 上一篇中我们介绍了basename命令的使用,本篇我们介绍dirname命令,dirname 命令与 basename 互补,它负责删除路径中的基本文件名部分(包括扩展名),只保留目录部分。 二、介绍 dirname首先去除字符…...

池化技术的总结
文章目录 1.什么是池化技术2.池化技术的应用一、连接池二、线程池三、内存池 3.池化技术的总结 1.什么是池化技术 池化技术指的是提前准备一些资源,在需要时可以重复使用这些预先准备的资源。 在系统开发过程中,我们经常会用到池化技术。通俗的讲&am…...

H5简约星空旋转引导页源码
H5简约星空旋转引导页源码 源码介绍:一款带有星空旋转背景特效的源码,带有四个按钮 下载地址: https://www.changyouzuhao.cn/11655.html...
前端学习之路(4) vue2和vue3的区别
一. 根节点不同 vue2中必须要有根标签vue3中可以没有根标签,会默认将多个根标签包裹在一个fragement虚拟标签中,有利于减少内存。 二. 组合式API和选项式API 在vue2中采用选项式API,将数据和函数集中起来处理,将功能点切割了当…...

网络原理-TCP/IP(5)
TCP协议 延迟应答 它也是基于滑动窗口,提高效率的一种机制,结合滑动窗口以及流量控制,能够以延迟应答ACK的方式,把反馈的窗口,搞大.核心在于允许范围内,让窗口尽可能大. 如果接收数据的主机立刻返回ACK应答,这时候返回的窗口可能比较小. 1.假设接收端缓冲区为1M.一次收到了5…...
Docker 常用命令详细介绍
Docker 是一个开源的应用容器引擎,它允许开发者打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口。Docker 使用概率最高的命令…...

10G PON演进到50G PON
10G-PON是万兆无源光网络,光纤链路传输速率能够达到10Gbps。根据ZTE的报告称,截至2023年6月,全球10G PON出货量已超过3000万个PON端口,其中中国市场份额约占80%。 10G PON在中国市场的广泛部署,显着推进了10G PON产业链…...

智能指针——浅析
智能指针 本人不才,只能将智能指针介绍一下,无法结合线程进行深入探索 介绍及作用 在异常产生进行跳转时,通过栈帧回收进行内存释放,防止内存泄漏 基于RAII思想可以创建出只能指针 RAII(Resource Acquisition Is Initializatio…...

JAVA后端上传图片至企微临时素材
1.使用场景 在使用企业微信API接口中,往往开发者需要使用自定义的资源,比如发送本地图片消息,设置通讯录自定义头像等。 为了实现同一资源文件,一次上传可以多次使用,这里提供了素材管理接口:以media_id来…...

MySQL-----初识
一 SQL的基本概述 基本概述 ▶SQL全称: Structured Query Language,是结构化查询语言,用于访问和处理数据库的标准的计算机语言。SQL语言1974年由Boyce和Chamberlin提出,并首先在IBM公司研制的关系数据库系统SystemR上实现。 ▶美国国家标…...

[基础IO]文件描述符{重定向/perror/磁盘结构/inode/软硬链接}
文章目录 1. 再识重定向2.浅谈perror()3.初始文件系统4.软硬链接 1. 再识重定向 图解./sf > file.txt 2>&1 1中内容拷贝给2 使得2指向file 再学一个 把file的内容传给cat cat拿到后再给file2 2.浅谈perror() open()接口调用失败返回-1,并且错误码errno被适当的设置,…...

NAS系统折腾记 – Emby搭建家庭多媒体服务器
Emby简介 Emby是一款优秀的媒体服务器软件,致力于为用户提供丰富的多媒体体验。通过Emby,您可以方便地在家庭内的各种设备上观看您喜爱的电影、电视剧和其他视频内容。而且,Emby还具备强大的媒体管理功能,让您的影视资源井然有序…...

#从零开始# 在深度学习环境中,如何用 pycharm配置使用 pipenv 虚拟环境
为Python项目创建虚拟环境 在深度学习环境和一般python环境中安装pipenv基本一致,只需要确认好pipenv指定的python版本即可,安装pipenv前,可以通过python --version来确认安装版本 快捷键:crtl alt S 查看interpreter,查看所有…...

Cmake编译Opencv3.3.1遇到有些文件无法下载的错误解决:
前言: 对于,opencv有些配置文件错误并未致命,所以,有错误也不影响后续的编译:但是,后引用如果要用,在回过头来还是要解决的。 问题表述: 比如,有些文件下载的错误&am…...

Python基础知识:Python序列以及序列的索引、切片、相乘和相加
索引 索引就是序列中的每个元素所在的位置,可以通过从左往右的正数索引,也可以通过从右往左的负数索引。 从左往右的正数索引:在python序列中,第一个元素的索引值为0,第二个元素的索引值为1,以此类推&…...

回归预测 | Matlab实现CPO-GRU【24年新算法】冠豪猪优化门控循环单元多变量回归预测
回归预测 | Matlab实现CPO-GRU【24年新算法】冠豪猪优化门控循环单元多变量回归预测 目录 回归预测 | Matlab实现CPO-GRU【24年新算法】冠豪猪优化门控循环单元多变量回归预测效果一览基本介绍程序设计参考资料 效果一览 基本介绍 1.Matlab实现CPO-GRU【24年新算法】冠豪猪优化…...
神经网络基础:从单个神经元到多层网络(superior哥AI系列第3期)
🧠 神经网络基础:从单个神经元到多层网络(superior哥AI系列第3期) 哈喽!各位AI探索者们!👋 上期我们把数学"怪兽"给驯服了,是不是感觉还挺轻松的?今天我们要进…...

飞算 JavaAI 赋能老项目重构:破旧立新的高效利器
许多企业的 Java 老项目面临着代码陈旧、架构落后、维护困难等问题。老项目重构势在必行,却又因庞大的代码量、复杂的业务逻辑让开发团队望而却步。 老项目重构困境重重 传统的 Java 老项目往往在长期的迭代和维护中积累了诸多问题。一方面,代码质量堪…...

NLP学习路线图(十八):Word2Vec (CBOW Skip-gram)
自然语言处理(NLP)的核心挑战在于让机器“理解”人类语言。传统方法依赖独热编码(One-hot Encoding) 表示单词,但它存在严重缺陷:每个单词被视为孤立的符号,无法捕捉词义关联(如“国…...
[特殊字符] Unity 性能优化终极指南 — Text / TextMeshPro 组件篇
UGUI Text组件的不当使用及其性能瓶颈与优化 在Unity UGUI系统中,Text 组件(或其升级版 TextMeshPro)是显示文本信息的核心元素。然而,如果不当使用,它极易成为UI性能瓶颈的罪魁祸首,尤其是在预制体、属性…...
Vue-Cropper:全面掌握图片裁剪组件
Vue-Cropper 完全学习指南:Vue图片裁剪组件 🎯 什么是 Vue-Cropper? Vue-Cropper 是一个简单易用的Vue图片裁剪组件,支持Vue2和Vue3。它提供了丰富的配置选项和回调方法,可以满足各种图片裁剪需求。 🌟 …...

从零到一:我的技术博客导航(持续更新)
作者:冰茶 最后更新:2025年6月3日 本文收录了我的C#编程学习心得与技术探索,将持续更新 前言 作为一名.NET开发者,C#语言的学习与探索一直是我技术成长的核心路径。本文集整理了我在C#学习过程中的思考与实践,希望能够…...

【手搓一个原生全局loading组件解决页面闪烁问题】
页面闪烁效果1 页面闪烁效果2 封装一个全局loading组件 class GlobalLoading extends HTMLElement {constructor() {super();this.attachShadow({ mode: open });}connectedCallback() {this.render();this.init();}render() {this.shadowRoot.innerHTML <style>.load…...

极简以太彩光网络解决方案4.0正式发布,“彩光”重构园区网络极简之道
5月28日下午,锐捷网络在京举办以“光,本该如此‘简单’”为主题的发布会,正式发布极简以太彩光网络解决方案4.0。作为“彩光”方案的全新进化版本,极简以太彩光4.0从用户需求出发,聚焦场景洞察,开启了一场从底层基因出发的极简革命,通过架构、部署、运维等多维度的创新升级,以强…...
循环神经网络(RNN):为什么它能处理时序数据?它真的能减轻过拟合吗?
循环神经网络(RNN):为什么它能处理时序数据?它真的能减轻过拟合吗? 在深度学习领域,循环神经网络(RNN, Recurrent Neural Network)是一种非常重要的神经网络结构,尤其适…...

C++ 类模板三参数深度解析:从链表迭代器看类型推导与实例化(为什么迭代器类模版使用三参数?实例化又会是怎样?)
本篇主要续上一篇的list模拟实现遇到的问题详细讲解:<传送门> 一、引言:模板参数的 "三角锁钥" 在 C 双向链表实现中,__list_iterator类模板的三个参数(T、Ref、Ptr)如同精密仪器的调节旋钮&#x…...