什么时候应该使用元组结构体而不是普通元组?

10
在《Rust编程语言》这本书的结构体章节中,我们介绍了元组结构体。除了书中提到的例子之外,在哪些情况下应该使用元组结构体而不是普通元组呢?
4个回答

26

摘要

使用 元组结构体,当:

  • 类型 名称携带语义信息
  • 字段 命名不增加任何语义信息

你会发现这种情况并不经常出现。如果是的话,通常只是尝试将一个完全相同的其他类型包装成一个新类型来提供不同的行为。这是一种已知的模式,称为'newtype' pattern。当你在 struct 中有多个字段时,通常需要为它们命名。


示例

快速提醒:

  • tuples 是带有匿名字段的匿名类型
  • tuple structs 是带有匿名字段的命名类型
  • structs 是带有命名字段的命名类型

让我们来看看[T]::split_at()

fn split_at(&self, mid: usize) -> (&[T], &[T])

这个函数的作者选择在此处返回一个元组。让我们看看,我们能从中获得什么好处...
  • 命名类型: 实际上并没有,对吧?我们会称之为什么?SplitSlice, SliceParts, ...? 无论我们给出的名称是什么,都是多余的,因为函数已经被适当地命名了。
  • 命名字段: 值得怀疑。 leftright 会使每个部分更加清晰明确。但是这里我们假设程序员有正确的直觉。英语是从左到右写的,在英语文化中,数组通常从左到右绘制([0 | 1 | 2 | 3 ]),所以对大多数人来说,这是有意义的。

⇒ 只返回元组就可以了!


另一个例子:想象一下,你正在编写一个与文本文件相关的应用程序(编译器、文本编辑器等)。当你谈论文本文件中的某个区域时(例如搜索结果),你希望通过给出字节偏移量来指定该区域。让我们看看元组是否适合我们:
fn find_first_occurence(file: &TextFile, needle: &str) -> (usize, usize)

返回结果:

返回类型能够清晰地表达吗?并不是...即使你知道文件中的区域是由字节偏移量指定的,返回值仍然是模棱两可的:它可以是(start, end),也可以是(start, stuff),其中stuff可以是任何其他搜索度量(函数不需要返回end,因为我们已经知道needle的长度,因此可以计算出它)。因此,我希望您同意,我们要给返回类型命名。让我们称之为Span——这是 Rust编译器中使用的名称。

下一个问题:结构体或元组结构体?是否有意义为字段命名?同样,没有明确的答案,但我认为我们应该为字段命名。哪个更容易阅读:span.1 - span.0还是span.high - span.low?此外,我们可以为命名字段编写文档;例如,记录high是排除性的。

⇒ 结构体


现在想象一下,您想向用户报告行号。特别重要的是,有一个函数可以返回给定范围的相应行号(为简单起见,我们假设此范围永远不会跨越多行)。
fn get_line_number(file: &TextFile, span: Span) -> ???

在这个函数的上下文中,我们需要返回什么?一个简单的u32可能就足够了!这个u32代表的含义毫无疑问。不过:行号是从0还是1开始计数呢?叹气 当然,我们可以在函数上记录此属性...以及与行号相关的每个其他函数。那么,在创建一个新类型并在其中记录它的方式如何?这也将有助于使用多个数字(包括行号)的函数:
print_snippet(&file, 57, 63, 80);

等等,现在什么是行号?确切地说:它将使用LineNumber而不是u32。类型系统就是文档。

我们现在同意创建一个新类型。但是:结构体还是元组结构体?让我们尝试结构体:

struct LineNumber {
    line_number: u32,    // uhm...
}

好的,如何称呼该字段?唯一适合的名称分为两类:

  • 与结构体名称相似:line_number, number, line,...
  • 没有语义信息:inner, value, data,...

给该字段命名实际上并没有什么好处。因此,让我们不要命名并使用一个...

⇒元组结构体

将其作为自己的类型的决定有一些好处:我们可以在源代码中使用从0开始的数字,但永远不需要担心打印不正确:

impl fmt::Display for LineNumber {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        (self.0 + 1).fmt(f)
    }
}

我们可以在一个地方将基于0的数字调整为基于1的数字(针对那些该死的人类!)。
此外,请注意,我们上面谈论的是字节偏移量。我们应该创建一个新类型来区分它与字符偏移量,而不是使用usize

5
转念一想,对于这段文字来说,写一个博客文章可能比一个 Stack Overflow 回答更为合适。 - Lukas Kalbertodt
2
哦,我的天啊!感谢您抽出时间写下如此全面的答案 :) - Chakravarthy Raghunandan

2

元组结构不太常见。当您只有少量成员,而且很清楚哪些成员是什么,不需要名称时,请使用它们。

元组结构的一个常见用途是新类型。这是一个只有一个成员的元组结构。这对于在现有类型周围创建简单的包装器非常有用。


2

元组和元组结构体的主要区别在于后者引入了一个名称,而前者没有。

有时,数据被捆绑在一起只是因为需要。例如,想象一个状态机:每个转换都返回一个数据片段以及状态机的下一个状态,这叫什么名字?

impl StateA {
    fn on_event_x(self) -> (String, StateB);
}

这种情况不应该被滥用;毕竟,命名在文档 API 中很有用!然而,有时候就是找不到一个合适的愚蠢名称(StringAndStateB :x),这时使用元组结构体是比较好的选择。

那么,什么时候使用元组结构体呢?当您需要或希望为类型命名,并且您更喜欢使用元组结构体而不是常规结构体或枚举(这是另一个完全不同的讨论!)时。


0

免责声明:我是 Rust 新手。

对于私有方法,你应该知道传递的内容,所以元组是可以的,如果需要的话,你总是可以随后更改它。

避免歧义

对于公共方法,避免歧义更为重要。例如,假设我们有元组 (u8, u8, u8, u8) 来表示颜色;它是 RGBA、ARGB、BGRA、ABGR 还是 HSLA?

将其表示为元组结构体 Rgba(u8, u8, u8, u8) 会更清晰。

可扩展性

此外,请注意,如果你从公共函数或公共 trait 中公开一个元组,则在未来限制了扩展性,因为引入任何添加或更改都会破坏已定义的 API。

使用结构体或元组结构体,你可以将方法添加到结构体中。

例如:

struct Rgba(u8, u8, u8, u8);

impl Rgba {
    pub fm as_hsl() -> Hsla(u8, u8, u8, u8) { todo!(); }
    pub fm lighten(&mut self, u8) { todo!(); }
    pub fm darken(&mut self, u8) { todo!(); }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接