【Rust】从同质形式的 variants 中获取同类型的数据
本文内容整理自:https://users.rust-lang.org/t/generic-referencing-enum-inner-data/66342
同质形式的 variants
如果你定义一个这样的枚举体:
#[derive(Debug)]
enum Foo {Bar(u32),Bink(u32),
}
这是一种 data-carrying 的枚举体,而且它特殊在:
- variants 中携带你所关心的
u32
类型的数据; - 这个类型位于 tuple variant 在第一个位置上。
从而它们 (variants) 形式上同质:相同的类型位于相同的位置。
那么,为了获取到 u32
数据,通常我们采用模式匹配的方式取出数据,而且对于枚举体,最常用 match
匹配:
// src: https://users.rust-lang.org/t/generic-referencing-enum-inner-data/66342/2
impl Foo {fn get_value(&self) -> u32 {match self {Bar(value) => *value,Bink(value) => *value,}}
}
而 @CKalt 提出一个很有启发性的观点:
It would be nice, if, when all the variants of an enum share an identical type, that there were a way to refer to that data by it’s position instead of having to pull it would with some long match statement, etc…
当所有 variants 都有相同的类型时,是否可能有一种方式,按照位置引用/访问数据,而不是只能写很长的匹配语句。
习惯上把 variant 类比 struct,因为它们都有:常规型、元组型、unit 型。
而元组结构体可以直接通过 .number_of_position
语法访问数据,那么在同质形式的 variants 中也使用这种语法,是不是更方便呢:
let bink = Foo::Bink(20);
println!("bink num = {}", bink.0);
当然,这种同质形式的 variants 很少见,因为很多时候,一个枚举体里的各个 variant 形态各异:可能携带数据也可能不携带数据,而且即便携带数据,其数量、类型、位置都不一定相同。
可是这个问题并不算无趣,将思维拓展之后,会发现这是一个值得学习的案例(当然不仅仅是学习 variant)。
思路一:重新设计数据结构/数据类型
回到那个例子:你最终需要 u32
类型,那么你可以考虑从一开始就把这个数据从枚举体中分离出来。
// src: https://users.rust-lang.org/t/generic-referencing-enum-inner-data/66342/2
struct Foo {value: u32,kind: FooKind,
}enum FooKind {Bar,Bink,
}
虽然你依然需要使用 match
匹配每种情况,但是你的数据和流程逻辑都放在结构体类型上——枚举体仅仅是作为一个标签而已(FooKind
不携带数据,被称作 fieldless enum)。这样做的缺点可能在于:因为结构体对齐方式,其内存布局会不太紧凑。
思路二:善用模式匹配
在 Rust 1.53 版本中,模式匹配有了一种新模式叫:or-patterns,其一般阐述见 reference#or-patterns 。
- 当所有 variants 都是元组型,而且第一个位置都是相同类型,那么你可以这样写:
// src: https://users.rust-lang.org/t/generic-referencing-enum-inner-data/66342/6
enum Foo {Bar(u32, u32, f64),Bink(u32, f64),
}fn demo(x: Foo) {use Foo::*;let (Bar(num, ..) | Bink(num, _)) = x;println!("{}", num);
}fn main() {demo(Foo::Bink(0, 1.));
}
- 类似地,当所有 variants 都是常规结构体型,而且相同类型的字段名一致,那么你可以这样写:
enum Foo {Bar {x: u32, y: u32, z: f64},Bink {x: u32, y: f64},
}fn demo(foo: Foo) {use Foo::*;// 这个 `()` 叫分组模式 (Grouped Pattern) 用于显式控制复合模式的优先级// 在多种模式表达不清的时候,用它来明确优先级let (Bar {x, ..} | Bink {x, ..}) = foo; println!("{}", x);
}fn main() {demo(Foo::Bink {x: 0, y: 1.});
}
即便相同类型的字段名不一致,你依然可以利用强大的模式匹配:
enum Foo {Bar {xx: u32, y: u32, z: f64},Bink{x: u32, y: f64},
}fn demo(foo: Foo) {use Foo::*;let (Bar {xx: x, z, ..} | Bink {x, y: z, ..}) = foo;println!("{} {}", x, z);
}fn main() {demo(Foo::Bink {x: 0, y: 1.2});
}
思路三:用宏来简化代码
当你以同一种方式重复编写代码时,可以考虑使用宏来简化。编写宏需要很多技巧,所以更需要通过经常学习和训练来提高。(“宏”有时候是声明宏和过程宏的统称,有时候专指声明宏,这里专指声明宏)
从 @steffahn 的回答中,一起学习这些技巧吧。
技巧 1:$name:pat
在宏的 13 种分类片段符 (Fragment Specifiers) 里,专门有一种来匹配模式:pat 。在思路二中,我们一直在谈论模式匹配,所以这是引导我们考虑编写宏的起点。
从最简单的例子出发:
#[derive(Debug)]
enum Foo {Bar(u32),Bink(u32),
}
把 (Bar(n) | Bink(n))
部分用宏进行替换:
//! src: https://users.rust-lang.org/t/generic-referencing-enum-inner-data/66342/9
//! author: https://users.rust-lang.org/u/steffahn
macro_rules! Foo {($p:pat) => {Foo::Bar($p) | Foo::Bink($p)}
}fn demonstration1() {let x = Foo::Bar(42);let Foo!(n) = x; // 即 let (Bar(n) | Bink(n)) = x;println!("{}", n);
}
由思路二可以知道,当 variants 是元组长度不一时或者常规结构体型时,我们都可以使用模式匹配语法轻松处理掉。
//! src: https://users.rust-lang.org/t/generic-referencing-enum-inner-data/66342/9
//! author: https://users.rust-lang.org/u/steffahn
#[derive(Debug)]
enum Baz {Variant1 {x: u32,y: String,z: bool,},Variant2 {x: u32,z: bool,qux: fn(),}
}macro_rules! Baz {($($field:ident $(: $p:pat)?,)* ..) => {Baz::Variant1{ $($field $(: $p)?,)* .. } | Baz::Variant2{ $($field $(: $p)?,)* .. }}
}fn demonstration2() {let baz = Baz::Variant1 { x: 42, y: "hello".into(), z: false };let Baz!{ x, z, .. } = &baz;println!("{:?} with `x` being {:?} and `z` being {:?}", baz, x, z);
}
这里有一些细节:
- 所有 item 的声明顺序与使用顺序无关;但宏不是 item,宏必须在使用之前声明。
Foo!
和Baz!
可适用于以下两种模式:- 解构 (destructuring):把值分解成其组成部分,所以
let Foo!(n) = x;
让n
为u32
类型。 - 以
ref
方式绑定:以非引用模式匹配引用时,绑定方式 (binding mode) 会自动变为ref
或ref mut
(详细说明见 RFC: match ergonomics)。所以let Baz!{ x, z, .. } = &baz;
中的x
和z
分别是&u32
和&bool
类型。
- 解构 (destructuring):把值分解成其组成部分,所以
- 正如 Rust By Example 的 macros 一章提到的宏的优点:减少重复;自定义语法;可变长度的参数。虽然无法拥有
Foo::Bink(20).0
或者Baz::Variant1 { x: 42, y: "hello".into(), z: false }.x
直接访问的语法,但我们依然可以通过宏和模式进行创造。
技巧 2:用宏来生成宏
当有很多类似 Baz
的枚举体时,我们可以用宏来生成同名宏 Baz!
。
//! src: https://users.rust-lang.org/t/generic-referencing-enum-inner-data/66342/9
//! author: https://users.rust-lang.org/u/steffahn
#![allow(unused)]macro_rules! define_enum_macro {($Type:ident, $($variant:ident),+ $(,)?) => {define_enum_macro!{#internal, [$], $Type, $($variant),+}};(#internal, [$dollar:tt], $Type:ident, $($variant:ident),+) => {macro_rules! $Type {($dollar($field:ident $dollar(: $p:pat)?,)* ..) => {$($Type::$variant { $dollar($field $dollar(: $p)?,)* .. } )|+}}};
}// -------------------------------------------------------------------------- //#[derive(Debug)]
enum Baz {Variant1 {x: u32,y: String,z: bool,},Variant2 {x: u32,z: bool,qux: fn(),}
}define_enum_macro!(Baz, Variant1, Variant2);fn demonstration2() {let baz = Baz::Variant1 { x: 42, y: "hello".into(), z: false };let Baz!{ z, .. } = &baz;println!("{:?} with `z` being {:?}", baz, z);
}fn main() {demonstration2();
}
一些理解/解释:
- 这里编写了两条解析规则:第一次解析的目的是匹配用户输入的语法,取出所需的 fragment specifiers;第二次解析后真正生成宏。
- 这里编写了一条内用规则 (internal rules),主要原因是:生成宏需要
$
符号,而它无法直接在 transcriber(指=>
之后的部分)表示出来,因此通过递归,把$
符号作为 tt 分类符(用于匹配 tokens tree) 传给下一次解析。而#internal
被视为内用规则的名称,你可以取任何名字,也可以使用其他前缀符号(比如常见的@internal_name
)。
结语:
- 通过本篇文章,你可以感受到 Rust 的模式 (patterns) 无处不在,见识了它与宏完美结合的典范。
- 如果你觉得这篇文章有用,请给 steffahn 的 回答 一个点赞以示鼓励。我只是对一则 Rust User Forum 的帖子进行了梳理、记录和补充,没有 steffahn 的回答,也就没有这篇文章。
- Rust User Forum 是学习 Rust 和体验 Rust 圈氛围的好地方,每次逛都可以学到许多额外的技巧与收获。一些高质量的回答淹没在茫茫帖子中,这是可惜的,或许以文章/笔记的方式,让更多人看见,这会是一种不错的尝试。
【Rust】从同质形式的 variants 中获取同类型的数据相关推荐
- 【Qt】Scene中获取指定类型的自定义图元
通过阅读 Qt 官方文档中的"Elastic Nodes Example"例子,学习到如何在场景(Scene)中获取某个类型的自定义图元. 官方Demo运行效果: 该文档中,作者自 ...
- 在NSUserDefaults中存储自定义类型的数据
将自定义的类的数据以数组的形式直接存储到NSUserDefaults中会报错,需要进行转换,且需要将该类实现NSCoding协议. e.g. 存储过程 NSMutableArray *archiveA ...
- 使用SQL语句在表中插入date类型的数据
如果想使用SQL语句在数据库的表中插入一个date类型的数据,可以使用 insert into user values (null, '小红', 1234, 24, '男', 20200808); 注 ...
- H.264裸流文件中获取每一帧数据
测试解码器性能时,最常用的无非是向解码器中推送码流. 之前封装了一个avc的解码器,想做一个测试,读取H.264裸流文件将码流定期定时推送到解码器. 测试其实很简单: 1.了解H.264裸流文件的构成 ...
- php接收表单图片,如何在PHP中获取表单图片数据
这是我的HTML,用于发布带图片的广告. {% for count in 1..10 %} {% endfor %} 这是我的轮廓仪功能 public function insertProduct( ...
- hive中存Array类型的数据的案例,将字符串的数组格式转成数组的字符串,自定义函数方式处理‘[12,23,23,34]‘字符串格式的数据为array<int>格式的数据。
1.创建表带有Array的表: create table t_afan_test ( info1 array<int>, info2 array<string> ) ROW F ...
- android中ListView控件onItemClick事件中获取listView传递的数据
http://blog.csdn.net/aben_2005/article/details/6592205 本文转载自:android中ListView控件&&onItemClick ...
- 用springboot编写RestController之——详解RestController中获取请求的各种数据
参考资料:老葛课堂 https://study.163.com/course/courseLearn.htm?courseId=1005213034#/learn/video?lessonId=105 ...
- 【SQL】利用sql语句在mysql的表中插入date类型的数据,
文中可能有错,请谨慎实施 一. 创建一个数据库 create database test 二. 在数据库中创建表 create TABLE employees (emp_no int(4) not n ...
最新文章
- R语言distHaversine函数计算大圆距离实战
- BZOJ-1045 糖果传递 数学+递推
- C++中的string 类型占几个字节
- SQLSERVER数据库经常置疑的原因
- 信息检索与数据挖掘的常用加权技术。
- Spring MVC 和 Spring 总结
- 序列化反序列化api(入门级)
- MySQL sql99语法—左(右)外连接
- 在 Azure Functions 上使用不同的路由前缀
- 【LeetCode】1. 盛最多水的容器:C#三种解法
- 【OpenCV 例程200篇】96. 谐波平均滤波器
- Android 内存泄漏检测工具
- mysql连表删除语句_MySQL中联表更新与删除的语法介绍
- 论文写作——latex三线表tabular*文本居中与正文两端对齐、标题加黑
- Charles mac版本进行https抓包的配置方法
- 狗年拜年php源码,创意拜年祝福语狗年
- c++实现高速缓存Cache
- 咨询行业细分——管理咨询、战略咨询、IT咨询
- C++ STL函数库 vector(henu.hjy)
- 湘潭大学计算机考研复试题,湘潭大学信息工程学院2019年考研复试程序设计练习题...
热门文章
- 什么是互联网舆情监测分析系统,TOOM舆情监测云服务有哪些内容?
- CSS关于子元素溢出
- Linux安装BCM4331驱动包下载
- FFmpeg 开启QSV硬解加速
- matlab作图背景黑色,【matlab】版本2014a 修改背景为酷炫暗黑色
- SystemUI StatusBar流程梳理
- Python+Django毕业设计智能仓储设备管理系统(程序+LW+部署)
- 2021年安全员-C证考试试卷及安全员-C证考试技巧
- DDR200T TFT - LCD 显示屏 显示图片 NucleiStudio 蜂鸟E203 详细教程 RISC-V
- 360手机:360Q5plus Twrp、Root、Magisk教程