本文内容整理自:https://users.rust-lang.org/t/generic-referencing-enum-inner-data/66342

同质形式的 variants

如果你定义一个这样的枚举体:

#[derive(Debug)]
enum Foo {Bar(u32),Bink(u32),
}

这是一种 data-carrying 的枚举体,而且它特殊在:

  1. variants 中携带你所关心的 u32 类型的数据;
  2. 这个类型位于 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 。

  1. 当所有 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.));
}
  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);
}

这里有一些细节:

  1. 所有 item 的声明顺序与使用顺序无关;但宏不是 item,宏必须在使用之前声明。
  2. Foo!Baz! 可适用于以下两种模式:
    1. 解构 (destructuring):把值分解成其组成部分,所以 let Foo!(n) = x;nu32 类型。
    2. ref 方式绑定:以非引用模式匹配引用时,绑定方式 (binding mode) 会自动变为 refref mut(详细说明见 RFC: match ergonomics)。所以 let Baz!{ x, z, .. } = &baz; 中的 xz 分别是 &u32&bool 类型。
  3. 正如 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();
}

一些理解/解释:

  1. 这里编写了两条解析规则:第一次解析的目的是匹配用户输入的语法,取出所需的 fragment specifiers;第二次解析后真正生成宏。
  2. 这里编写了一条内用规则 (internal rules),主要原因是:生成宏需要 $ 符号,而它无法直接在 transcriber(指 => 之后的部分)表示出来,因此通过递归,把 $ 符号作为 tt 分类符(用于匹配 tokens tree) 传给下一次解析。而 #internal 被视为内用规则的名称,你可以取任何名字,也可以使用其他前缀符号(比如常见的 @internal_name)。

结语:

  1. 通过本篇文章,你可以感受到 Rust 的模式 (patterns) 无处不在,见识了它与宏完美结合的典范。
  2. 如果你觉得这篇文章有用,请给 steffahn 的 回答 一个点赞以示鼓励。我只是对一则 Rust User Forum 的帖子进行了梳理、记录和补充,没有 steffahn 的回答,也就没有这篇文章。
  3. Rust User Forum 是学习 Rust 和体验 Rust 圈氛围的好地方,每次逛都可以学到许多额外的技巧与收获。一些高质量的回答淹没在茫茫帖子中,这是可惜的,或许以文章/笔记的方式,让更多人看见,这会是一种不错的尝试。

【Rust】从同质形式的 variants 中获取同类型的数据相关推荐

  1. 【Qt】Scene中获取指定类型的自定义图元

    通过阅读 Qt 官方文档中的"Elastic Nodes Example"例子,学习到如何在场景(Scene)中获取某个类型的自定义图元. 官方Demo运行效果: 该文档中,作者自 ...

  2. 在NSUserDefaults中存储自定义类型的数据

    将自定义的类的数据以数组的形式直接存储到NSUserDefaults中会报错,需要进行转换,且需要将该类实现NSCoding协议. e.g. 存储过程 NSMutableArray *archiveA ...

  3. 使用SQL语句在表中插入date类型的数据

    如果想使用SQL语句在数据库的表中插入一个date类型的数据,可以使用 insert into user values (null, '小红', 1234, 24, '男', 20200808); 注 ...

  4. H.264裸流文件中获取每一帧数据

    测试解码器性能时,最常用的无非是向解码器中推送码流. 之前封装了一个avc的解码器,想做一个测试,读取H.264裸流文件将码流定期定时推送到解码器. 测试其实很简单: 1.了解H.264裸流文件的构成 ...

  5. php接收表单图片,如何在PHP中获取表单图片数据

    这是我的HTML,用于发布带图片的广告. {% for count in 1..10 %} {% endfor %} 这是我的轮廓仪功能 public function insertProduct( ...

  6. hive中存Array类型的数据的案例,将字符串的数组格式转成数组的字符串,自定义函数方式处理‘[12,23,23,34]‘字符串格式的数据为array<int>格式的数据。

    1.创建表带有Array的表: create table t_afan_test ( info1 array<int>, info2 array<string> ) ROW F ...

  7. android中ListView控件onItemClick事件中获取listView传递的数据

    http://blog.csdn.net/aben_2005/article/details/6592205 本文转载自:android中ListView控件&&onItemClick ...

  8. 用springboot编写RestController之——详解RestController中获取请求的各种数据

    参考资料:老葛课堂 https://study.163.com/course/courseLearn.htm?courseId=1005213034#/learn/video?lessonId=105 ...

  9. 【SQL】利用sql语句在mysql的表中插入date类型的数据,

    文中可能有错,请谨慎实施 一. 创建一个数据库 create database test 二. 在数据库中创建表 create TABLE employees (emp_no int(4) not n ...

最新文章

  1. R语言distHaversine函数计算大圆距离实战
  2. BZOJ-1045 糖果传递 数学+递推
  3. C++中的string 类型占几个字节
  4. SQLSERVER数据库经常置疑的原因
  5. 信息检索与数据挖掘的常用加权技术。
  6. Spring MVC 和 Spring 总结
  7. 序列化反序列化api(入门级)
  8. MySQL sql99语法—左(右)外连接
  9. 在 Azure Functions 上使用不同的路由前缀
  10. 【LeetCode】1. 盛最多水的容器:C#三种解法
  11. 【OpenCV 例程200篇】96. 谐波平均滤波器
  12. Android 内存泄漏检测工具
  13. mysql连表删除语句_MySQL中联表更新与删除的语法介绍
  14. 论文写作——latex三线表tabular*文本居中与正文两端对齐、标题加黑
  15. Charles mac版本进行https抓包的配置方法
  16. 狗年拜年php源码,创意拜年祝福语狗年
  17. c++实现高速缓存Cache
  18. 咨询行业细分——管理咨询、战略咨询、IT咨询
  19. C++ STL函数库 vector(henu.hjy)
  20. 湘潭大学计算机考研复试题,湘潭大学信息工程学院2019年考研复试程序设计练习题...

热门文章

  1. 什么是互联网舆情监测分析系统,TOOM舆情监测云服务有哪些内容?
  2. CSS关于子元素溢出
  3. Linux安装BCM4331驱动包下载
  4. FFmpeg 开启QSV硬解加速
  5. matlab作图背景黑色,【matlab】版本2014a 修改背景为酷炫暗黑色
  6. SystemUI StatusBar流程梳理
  7. Python+Django毕业设计智能仓储设备管理系统(程序+LW+部署)
  8. 2021年安全员-C证考试试卷及安全员-C证考试技巧
  9. DDR200T TFT - LCD 显示屏 显示图片 NucleiStudio 蜂鸟E203 详细教程 RISC-V
  10. 360手机:360Q5plus Twrp、Root、Magisk教程