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 里的宏编程实验性支持了?

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

LeetCode 热题100之双指针

1.移动零 思路分析1&#xff08;纯模拟&#xff09; 定义指针j&#xff0c;用来收集不是0的数&#xff1b;收集完毕之后&#xff0c;再把剩下位置处置为0即可。 具体实现代码&#xff08;详解版&#xff09;&#xff1a; class Solution { public:void moveZeroes(vector<…

前端vue框架配置基础信息详解分析

前端vue2、vue3框架是我们最近常用的框架&#xff0c;今天我们分析一下配置基础信息、详解其中的功能含义。 1、vue.config.js 文件分析 这个 vue.config.js 文件是 Vue CLI 项目中用于配置项目构建行为和开发环境设置的文件。它能够让开发者定制打包、代理、路径、样式等方面…

国产单片机及其特点

国产单片机在近年来取得了显著的发展&#xff0c;不仅在技术上不断突破&#xff0c;还在市场上占据了越来越重要的位置。 主要国产单片机品牌及特点 兆易创新&#xff08;GD&#xff09; 主要系列&#xff1a;GD32系列&#xff0c;基于ARM Cortex-M内核。特点&#xff1a;高性能…

Android 中的串口开发

一&#xff1a;背景 本文着重讲安卓下的串口。 由于开源的Android在各种智能设备上的使用越来越多&#xff0c;如车载系统等。在我们的认识中&#xff0c;Android OS的物理接口一般只有usb host接口和耳机接口&#xff0c;但其实安卓支持各种各样的工业接口&#xff0c;如HDM…

ResNet18果蔬图像识别分类

1. 项目简介 本项目的目标是开发一个基于ResNet18深度学习模型的果蔬图像分类系统。随着现代农业与人工智能的结合&#xff0c;智能果蔬分类技术在供应链、生产和销售管理中扮演了越来越重要的角色。本项目的背景源于提升果蔬分类效率的需求&#xff0c;通过使用计算机视觉技术…

基于SSM+微信小程序的酒店管理系统1

&#x1f449;文末查看项目功能视频演示获取源码sql脚本视频导入教程视频 1、项目介绍 基于微信小程序开发的酒店管理系统管理员&#xff0c;酒店管理员以及用户。 1、管理员功能可以管理个人中心&#xff0c;用户信息管理&#xff0c;酒店管理员管理&#xff0c;房间类型管…

YOLO11改进 | 注意力机制 | 添加SE注意力机制

秋招面试专栏推荐 &#xff1a;深度学习算法工程师面试问题总结【百面算法工程师】——点击即可跳转 &#x1f4a1;&#x1f4a1;&#x1f4a1;本专栏所有程序均经过测试&#xff0c;可成功执行&#x1f4a1;&#x1f4a1;&#x1f4a1; 本文介绍了YOLOv11添加SE注意力机制&…

Redis中String类型数据扩容原理分析

大家好&#xff0c;我是 V 哥。在 Java 中&#xff0c;我们有动态数组ArrayList&#xff0c;当插入新元素空间不足时&#xff0c;会进行扩容&#xff0c;好奇 Redis 中的 String 类型&#xff0c;C 语言又是怎样的实现策略&#xff0c;带着疑问&#xff0c;咱们来了解一下。 最…

Python酷库之旅-第三方库Pandas(167)

目录 一、用法精讲 766、pandas.Interval.open_left属性 766-1、语法 766-2、参数 766-3、功能 766-4、返回值 766-5、说明 766-6、用法 766-6-1、数据准备 766-6-2、代码示例 766-6-3、结果输出 767、pandas.Interval.open_right属性 767-1、语法 767-2、参数 …

[LeetCode] 78. 子集

题目描述&#xff1b; 给你一个整数数组 nums &#xff0c;数组中的元素 互不相同 。返回该数组所有可能的子集&#xff08;幂集&#xff09;。 解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。 示例 1&#xff1a; 输入&#xff1a;nums [1,2,3] 输出&#xff1…

Windows通过netsh控制安全中心防火墙和网络保护策略

Windows通过netsh控制安全中心防火墙和网络保护策略 1. 工具简介 【1】. Windows安全中心 【2】. netsh工具 netsh(Network Shell) 是一个Windows系统本身提供的功能强大的网络配置命令行工具。 2. 开启/关闭防火墙策略 在设置端口&#xff08;禁用/启用&#xff09;前&am…

传输层协议UDP详解

目录 一. 知识准备 1.1 传输层 1.2 重识端口号 二. UDP协议 三. UDP协议特点 一. 知识准备 1.1 传输层 前面已经讲过&#xff0c;HTTP协议是应用层协议&#xff0c;在此之前&#xff0c;我们短暂的认为HTTP是直接通过应用层与外界通信的。但是我们要知道&…

DOTween动画插件超详解(保姆级巨细)

文章目录 一、前言二、DOTween简介与安装&#xff08;一&#xff09;什么是DOTween&#xff1f;&#xff08;二&#xff09;下载安装 三、DOTween 的使用&#xff08;基础&#xff09;&#xff08;一&#xff09;使用前注意事项1. 引入命名空间2. 进行初始化3. 清除遗留4. 设置…

基于Java的电商书城系统源码带本地搭建教程

技术框架&#xff1a;jQuery MySQL5.7 mybatis jsp shiro 运行环境&#xff1a;jdk8 IntelliJ IDEA maven3 宝塔面板 系统功能介绍 该系统分为前台展示和后台管理两大模块&#xff0c;前台主要是为消费者服务。该子系统实现了注册&#xff0c;登录&#xff0c; 以及…

asp.net core mvc发布时输出视图文件Views

var builder WebApplication.CreateBuilder(args); builder.Services.AddRazorPages();builder.Services.AddControllersWithViews(ops > {//全局异常过滤器&#xff0c;注册ops.Filters.Add<ExceptionFilter>(); })// Views视图文件输出到发布目录&#xff0c;视图文…

使用 VSCode 通过 Remote-SSH 连接远程服务器详细教程

使用 VSCode 通过 Remote-SSH 连接远程服务器详细教程 在日常开发中&#xff0c;许多开发者需要远程连接服务器进行代码编辑和调试。Visual Studio Code&#xff08;VSCode&#xff09;提供了一个非常强大的扩展——Remote-SSH&#xff0c;它允许我们通过 SSH 协议直接连接远程…

背包九讲——完全背包问题

目录 完全背包问题 问题定义 动态规划解法 状态转移方程 初始化 遍历顺序 三种解法&#xff1a; 朴素版——枚举k 进阶版——dp正推&#xff08;一维滚动数组&#xff09; 背包问题第三讲——完全背包问题 背包问题是一类经典的组合优化问题&#xff0c;通常涉及在限定…

kafka 的高可用机制是什么?

大家好&#xff0c;我是锋哥。今天分享关于【kafka 的高可用机制是什么&#xff1f;】面试题&#xff1f;希望对大家有帮助&#xff1b; kafka 的高可用机制是什么&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 Apache Kafka 是一个分布式消息系统&am…

《性能之巅:洞悉系统、企业与云计算》读书笔记-Part 1

本文是读书笔记第一部分&#xff0c;包括原书第一、二章。 绪论 性能是一门令人激动的&#xff0c;富于变化同时又充满挑战的学科。 系统性能 单台服务器上的通用系统软件栈 人员 系统性能是一项需要多类人员参与的工程。 事情 关于性能的理想执行顺序排列如下&#x…

8个方法教会你提高企业培训效率

培训成本是企业中的一个复杂问题。它完全取决于课程内容、培训方法以及成本效益。在计算培训费用时&#xff0c;公司会面临许多关于包括哪些内容、如何进行以及假设情景的问题。 企业员工培训的每个方面都会产生自己的成本。例如&#xff1a; 地点&#xff1a;我们专门找个培训…