Flutter 小技巧之 equatable 包解析以及宏编程解析

今天我们聊聊 equatable 包的实现,并通过 equatable 去理解 Dart 宏编程的作用和实现,对于 Flutter 开发者来说,Dart 宏编程可以说是「望眼欲穿」。

equatable

正如 equatable 这个包名所示,它的功能很简单,主要是用来帮助实现 class 级别基于值的「🟰」封装,如下代码所示,就算是一个三岁的程序都知道,正常情况下此时的 bob == Person("Bob") 结果会是 false ,因为它们是两个不同的 class 实例,hashcode 默认情况下就是不相等的。

class Person {const Person(this.name);final String name;
}final Person bob = Person("Bob");print(bob == Person("Bob")); // false

那么如果需要它们相等,如下代码所示,我们需要 override == 操作符去自定义所需的判断逻辑,这样看起来貌似也不麻烦,但是如果一个类参数很多,那么类似的重复性代码就会很多,这时候就需要 equatable 这个包来减轻工作量。

class Person {const Person(this.name);final String name;bool operator ==(Object other) =>identical(this, other) ||other is Person &&runtimeType == other.runtimeType &&name == other.name;int get hashCode => name.hashCode;
}

如下代码所示,通过 equatable 你只需要 extends Equatable ,然后 override props 参数即可实现对应的 == 自定义,这样从代码层级上看是不是更清晰简约了?

import 'package:equatable/equatable.dart';class Person extends Equatable {const Person(this.name);final String name;List<Object> get props => [name];
}class Person2 extends Equatable {const Person2(this.name, [this.age]);final String name;final int? age;List<Object?> get props => [name, age];
}

当然,对于 equatable 还是有一些限制,例如所有成员变量都必须是 final, 因为 Dart 官方在说明自定义 == 逻辑就表示过, 用可变值覆盖 hashCode 可能会破坏基于哈希的集合:

定义 == 时,还必须定义 hashCode,这两者都应该考虑对象的字段,如果字段发生更改,则意味着对象的哈希代码可以更改

大多数基于哈希的集合不会预料到这一点,它们假设对象的哈希代码将永远相同,如果不是这样,则可能会发生不可预测的行为。

那么回到 equatable 包的实现 ,核心逻辑就是处理 equals 来判断🟰 的逻辑,还有生成 mapPropsToHashCode 哈希来决定 hash 值。

	bool operator ==(Object other) {return identical(this, other) ||other is Equatable &&runtimeType == other.runtimeType &&equals(props, other.props);}int get hashCode => runtimeType.hashCode ^ mapPropsToHashCode(props);

首先自定义的 equals 判断其实就是对于两个 class 的 props 列表进行拆分判断,这里主要需要注意的是,由于类变量可以是任何对象,那么也就可以能是集合,例如 Map、Set 等,所以需要用到 Dart 的 DeepCollectionEquality 对象来处理,可以减轻很多判断的工作量。

DeepCollectionEquality 主要处理集合的深度相等的工具类,简单来说,它可以识别列表、集合、可迭代对象和映射,并深度较它们的元素,甚至可以按照有序或无序的工作模式来进行判断

const DeepCollectionEquality _equality = DeepCollectionEquality();/// Determines whether [list1] and [list2] are equal.
bool equals(List<Object?>? list1, List<Object?>? list2) {if (identical(list1, list2)) return true;if (list1 == null || list2 == null) return false;final length = list1.length;if (length != list2.length) return false;for (var i = 0; i < length; i++) {final unit1 = list1[i];final unit2 = list2[i];if (_isEquatable(unit1) && _isEquatable(unit2)) {if (unit1 != unit2) return false;} else if (unit1 is Iterable || unit1 is Map) {if (!_equality.equals(unit1, unit2)) return false;} else if (unit1?.runtimeType != unit2?.runtimeType) {return false;} else if (unit1 != unit2) {return false;}}return true;
}bool _isEquatable(Object? object) {return object is Equatable || object is EquatableMixin;
}

而对于生成哈希,equatable 用了 Jenkins 哈希算法,核心就是将任意长度的数值转换为固定长度的哈希值,算法的实现也相对简单,它只需要利用位移操作和迭代来生成哈希值,通过不断递归将所有参数进行 Jenkins 哈希计算,例如:

  • 将哈希值左移 10 位,与原哈希值相加,扩大了哈希值,增加了变化范围
  • 将哈希值右移 6 位,与原哈希值进行异或运算,减少了哈希值中的线性依赖
int mapPropsToHashCode(Iterable<Object?>? props) {return _finish(props == null ? 0 : props.fold(0, _combine));
}int _combine(int hash, Object? object) {if (object is Map) {object.keys.sorted((Object? a, Object? b) => a.hashCode - b.hashCode).forEach((Object? key) {hash = hash ^ _combine(hash, [key, (object! as Map)[key]]);});return hash;}if (object is Set) {object = object.sorted((Object? a, Object? b) => a.hashCode - b.hashCode);}if (object is Iterable) {for (final value in object) {hash = hash ^ _combine(hash, value);}return hash ^ object.length;}hash = 0x1fffffff & (hash + object.hashCode);hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));return hash ^ (hash >> 6);
}int _finish(int hash) {hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));hash = hash ^ (hash >> 11);return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}

到这里我们大概就解析了 equatable 的作用和实现,但是其实在使用上还不够优雅简介,因为需要手写 props 和显式继承的操作,还是让人觉得侵入性太强,那么这时候就该说宏编程的作用了。

Macro

事实上在这类应用场景上,宏编程对于 equatable 来说无疑是最适合的操作,equatable 包的作者在 3.0.0-dev.1 中就迫不及待发布了采用 macros 实现的 package ,而修改后的 equatable 代码如下所示:

()
class Person {const Person(this.name);final String name;
}

可以看到,他就是一个正常的 class ,你只需要添加 @Equatable() 注释,它就拥有了前面所说的 equatable class 的特性,这样看是不是优雅和简单了不少?

并且和之前旧的 build_runner 等不同,它不会在你项目里直接生产 .g.dart 的文件。

在引入带有宏编程的 equatable 包之后,只需要运行 flutter run --enable-experiment=macros ,就可以直接得到之前一样的结果:

并且 @Equatable() 和实验性的 @JsonCodabel 是可以同时使用,此时只需要两个注解,你就可以得到一个包含序列化能力的和类对比能力的 class 对象。

这里提一个题外话, 在 VSCode 其实你可以看到一个 Go to Augmentation 的区域,点击后可以跳转的 augment class ,也就是类似「宏生成效果」的文件预览里,通过 augment class, 你可以实时预览注解生成的代码:

这个增强能力属于宏生成带有 augment 的文件,与旧代码生成的实际区别在于,它是存在于内存中,而不是在 .g.dart 这样的形式出现在项目里。

Dart 的 augmentation 功能通过添加成员,或替换原始块之外的主体,来达到更改类或函数的能力,这个功能独立于宏之外。

当然,如果你是 Android Studio ,可能会看不到这样的支持,那么你可以通过引入一个 show_augmentation 的包来做到类似的功能预览,如下所示,通过运行 dart run show_augmentation --file=lib/person.dart 后,也可在命令行得到类似的输出:

回到 equatable,我们简单说下它的实现,常规上就是通过实现 ClassDeclarationsMacro ClassDefinitionMacro 来完成宏编程额基础操作,通过 buildDeclarationsForClass 去编辑需要的声明,然后通过 buildDefinitionForClass 去定义实现:

例如,一般在声明时,我们需要用到 Uri.parse('dart:core') ,因为我们需要用到 Dart 的能力支持,例如这里的 final boolean = await builder. codeFrom(_dartCore, 'bool'); ,当然这里的 codeFrom 实现 其实是 equatable 做了一些封装,我们可以通过下面一个简单的例子更好理解。

如下代码所示,首先我们通过 Uri.parse('dart:core') 得到了 Dart 的核心库,然后通过 MemberDeclarationBuilder 得到了 Dart 里的 print 方法:

所以这里是通过 dart:core 获取 print 方法,然后再生成的 hello 代码里输出所有参数,而 ClassDeclarationsMacro 会告诉编译器它可以应用于 class。

那为什么需要 dart:core 加载这一步,其实它其中一个作用就是可以自动生成前缀,还记得前面我们命令行到输出么?你会发现 dart:core 是用前缀导入的:

前缀是动态的,它能确保你的代码不会与任何核心内容(如 print)发生冲突,而且因为是动态,所以你也不知道它会是什么,所以你不能直接在代码里写 print(xxxxx) ,所以需要通过 parts 构建生成的代码。

回到 equatable ,同样的在实现对应的生成代码定义时,如果我们用到了自己的某些 function ,也需要通过 Uri.parse 引入,例如通过 final _equatable = Uri.parse('package:equatable/equatable.dart'); ,之后就可以使用对应的 equatable 能力:

而从最终实现效果看,equatable 借用宏完成了可以重复生成的部份,最终开发者只需要通过 @Equatable() 即可完成功能引入,类似 @JsonCodable() 即可引入 toJsonfromJson 一样。

其实我们还可以通过另外一种方式去查看宏的效果,那就是通过对 debug 编译后的 app.dill 文件进行分析,通过 dump_kernel.dart 可以转化出 app.dill.txt 文件,通过最终生成的 txt 文件我们可以直观感受宏的作用。

dart pkg/vm/bin/dump_kernel.dart xxxxxx/app.dill xxxxxx/app.dill.txt 

如下图所示,可以看到 "dart: core" 都加上了前缀,然后对应的方法都已经动态添加进入,并且标明了 marco 和 file 路径,同时如 deepEqualsjenkinsHash 也成功引入。

当然,上面命令的pkg/vm/bin/dump_kernel.dart 需要在官方 dart-lang/sdk 的全量 SDK 才能找到,但是如果你直接 clone dart-lang/sdk 项目,然后去执行 dump_kernel ,基本上会遇到下面这个问题:

因为如果想要使用 dump_kernel,你就需要 depot_tools 工具,depot_tools 是 Chromium 的源码管理工具,同时也需要对应的环境支持:

  • python3 环境
  • git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git ,并将路径配置 export PATH=/Users/xxxxx/workspace/depot_tools:$PATH
  • 创建一个目录,并执行 fetch dart ,会比较耗时,大概几个 G 的大小
  • 进入 sdk 目录,执行 git checkout xxxx ,切换到对应 dart 版本 tag ,因为一般情况下,你 debug 运行的 dart 版本和 sdk 的 dart 版本需要一致
  • 执行 gclient sync -D
  • 现在你就可以通过 dart pkg/vm/bin/dump_kernel.dart xxxxxx/app.dill xxxxxx/app.dill.txt 去 dump kernel ,这里的 pkg/vm/bin/dump_kernel.dart 路径就是前面 sdk 下的路径。

最后

总结一下,本篇主要通过 equatable 介绍了一些 Dart 的基础知识和技巧,同时利用 equatable 展开介绍了下宏的概念和作用,并且介绍了不同方式查看宏编程的产物,对于未来宏编程支持的正式发布,相信 Flutter 开发者们还是翘首以待的,那么你是否已经体验过 Dart 3.5 里的宏编程实验性支持了?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/455377.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

计算机毕业设计hadoop+spark知识图谱中药推荐系统 中药材推荐系统 中药可视化 中药数据分析 中药爬虫 机器学习 深度学习 人工智能 大数据

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 摘 要 本文所探讨的领域是…

【Linux】“echo $变量“ 命令打印变量值的底层原理

在 shell 中&#xff0c;echo $变量 命令的工作原理涉及几个关键步骤&#xff0c;主要是由 shell 解释器来处理变量的查找和替换。以下是详细的过程&#xff1a; 变量展开的过程顺序 变量引用&#xff1a; 在命令行中&#xff0c;变量通常以 $variable_name 或 ${variable_…

若依前后端分离超详情版

若依系统安装流程 1.安装Ubuntu系统 1.1 新建虚拟机 打开VMware Workstation&#xff0c;选择文件->新建虚拟机->典型&#xff08;推荐T&#xff09;->安装程序光盘映像文件->输入虚拟的名字->一直下一步即可 安装程序光盘映像文件 注意&#xff1a;选择ub…

专业第三方的控价价值

在当今竞争激烈的商业世界中&#xff0c;价格管控犹如一场没有硝烟的战争。品牌们为了维护自身的市场秩序和品牌价值&#xff0c;纷纷踏上控价的艰难征程。而在这个过程中&#xff0c;专业的第三方控价服务公司正以创新之姿&#xff0c;成为品牌们的得力助手。 曾经&#xff0c…

空间数据分析实验04:空间统计分析

实验概况 实验目的 了解空间统计分析的基本原理掌握空间统计分析的常用方法 实验内容 根据某村的土地利用数据和DEM数据&#xff0c;提取各村组耕地面积比例&#xff0c;并将其与村组平均坡度进行相关性分析&#xff0c;最后计算各村组单元的景观多样性指数。 实验原理与方…

【设计模式-原型】

**原型模式&#xff08;Prototype Pattern&#xff09;**是一种创建型设计模式&#xff0c;旨在通过复制现有对象的方式来创建新对象&#xff0c;而不是通过实例化类来创建对象。该模式允许对象通过克隆&#xff08;复制&#xff09;来创建新的实例&#xff0c;因此避免了重新创…

你不常用的 FileReader 能干什么?

前言 欢迎关注同名公众号《熊的猫》&#xff0c;文章会同步更新&#xff0c;也可快速加入前端交流群&#xff01; 本文灵感源于上周小伙伴遇到一个问题&#xff1a; “一个本该返回 Blob 类型的下载接口&#xff0c;却返回了 JSon 类型的内容&#xff01;&#xff01;&#xf…

HTML之表单设计

1、HTML表单 HTML表单是用于收集用户输入的信息&#xff0c;并将用户输入的内容信息传到后台服务器中。 表单是通过form标签实现。 特别注意&#xff1a;如果一些内容提交后&#xff0c;没有将内容提交给后台服务器&#xff0c;那么需要添加一个name属性&#xff0c;语法&am…

Stable Diffusion 3.5 震撼发布!最新开源 AI 图像生成模型,艺术创作必备神器!

❤️ 如果你也关注大模型与 AI 的发展现状&#xff0c;且对大模型应用开发非常感兴趣&#xff0c;我会快速跟你分享最新的感兴趣的 AI 应用和热点信息&#xff0c;也会不定期分享自己的想法和开源实例&#xff0c;欢迎关注我哦&#xff01; &#x1f966; 微信公众号&#xff…

【NOIP普及组】 装箱问题

【NOIP普及组】 装箱问题 &#x1f490;The Begin&#x1f490;点点关注&#xff0c;收藏不迷路&#x1f490; 有一个箱子容量为V&#xff08;正整数&#xff0c;0&#xff1c;&#xff1d;V&#xff1c;&#xff1d;20000&#xff09;&#xff0c;同时有n个物品&#xff08;0&…

KubeSphere 最佳实战:Kubernetes 部署集群模式 Nacos 实战指南

Nacos 是 Dynamic Naming and Configuration Service 的首字母简称&#xff0c;一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。 Nacos 是构建以服务为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。 在本文中&#xff0c;我将为您提供…

k8s备份恢复(velero)

velero简介 velero官网&#xff1a; https://velero.io/ velero-github&#xff1a; https://github.com/vmware-tanzu/velero velero的特性 备份可以按集群资源的子集&#xff0c;按命名空间、资源类型标签选择器进行过滤&#xff0c;从而为备份和恢复的内容提供高度的灵活…

怎么在线制作拼团活动

在这个快节奏的时代&#xff0c;我们总在寻找那份独特的购物乐趣与超值体验。传统购物模式已难以满足日益增长的个性化与性价比需求&#xff0c;而在线购物虽便捷&#xff0c;却常让人在琳琅满目的商品中迷失方向。正是在这样的背景下&#xff0c;一种全新的购物方式——“在线…

vue3处理货名的拼接

摘要&#xff1a; 货品的拼接规则是&#xff1a;【品牌】货名称/假如货品名称为空时&#xff0c;直接选择品牌为【品牌】赋值给货品&#xff0c;再选择品牌&#xff0c;会替换【品牌】&#xff1b;假如货名称为【品牌】名称&#xff0c;再选择品牌只会替换【品牌】&#xff0c;…

vue3项目页面实现echarts图表渐变色的动态配置

完整代码可点击vue3项目页面实现echarts图表渐变色的动态配置-星林社区 https://www.jl1mall.com/forum/PostDetail?postId202410151031000091552查看 一、背景 在开发可配置业务平台时&#xff0c;需要实现让用户对项目内echarts图表的动态配置&#xff0c;让用户脱离代码也…

2024下半年软考机考模拟系统已开放!小伙伴们速速练起来

千呼万唤使出来&#xff0c;软考机考的模拟练习系统已于10月23号正式开放&#xff01; 今年报名计算机技术与软件专业技术资格&#xff08;水平&#xff09;考试&#xff08;软考&#xff09;的小伙伴们千万不要忘记哦&#xff01; 01、开放时间 据中国计算机技术职业资格网发…

基于AI识别数据的Vue.js图像框选标注

在数字化时代&#xff0c;图像识别技术的应用越来越广泛&#xff0c;尤其是在车牌识别、人脸识别等领域。本文将介绍如何使用Vue.js框架和JavaScript创建一个交互式组件&#xff0c;该组件不仅允许用户在图片上绘制多个区域&#xff0c;加载文字&#xff0c;还提供了清空功能。…

外包干了2个月,技术明显退步

回望过去&#xff0c;我是一名普通的本科生&#xff0c;于2019年通过校招有幸加入了南京某知名软件公司。那时的我&#xff0c;满怀着对未来的憧憬和热情&#xff0c;投入到了功能测试的岗位中。日复一日&#xff0c;年复一年&#xff0c;转眼间&#xff0c;我已经在这个岗位上…

常用shell指令

这些指令通常在adb shell环境中使用&#xff0c;或者通过其他方式&#xff08;如SSH&#xff09;直接在设备的shell中使用。 文件操作命令 ls&#xff1a;列出目录的内容 ls /sdcard cd&#xff1a;改变目录 cd /sdcard/Download pwd&#xff1a;打印当前工作目录 pwd cat&…

CV2通过一组轮廓点扣取图片

代码如下&#xff1a; import cv2 import numpy as np# 读取原始图像 original_image cv2.imread(img.png)# 定义一组轮廓点&#xff08;这里只是示例&#xff0c;你需要根据实际情况替换&#xff09; points np.array([[50, 100], [100, 200], [200, 150], [200, 50], [160…