【发布时间】:2021-09-18 07:29:42
【问题描述】:
在《Rust 编程语言》一书的结构一章中,我们被介绍给tuple structs。
在什么情况下我应该使用元组结构而不是普通元组(除了书中提到的例子)?
【问题讨论】:
在《Rust 编程语言》一书的结构一章中,我们被介绍给tuple structs。
在什么情况下我应该使用元组结构而不是普通元组(除了书中提到的例子)?
【问题讨论】:
在以下情况下使用元组结构:
您会发现这种情况并不常见。如果是这样,您通常只是试图将 一个 其他类型完全包装成一个新类型,以赋予它不同的行为。这是一种已知模式,称为'newtype' 模式。当您的结构中有多个字段时,您通常希望为它们命名。
快速提醒:
我们来看看[T]::split_at():
fn split_at(&self, mid: usize) -> (&[T], &[T])
这个函数的作者选择在这里返回一个元组。让我们看看,我们能从中得到什么……
SplitSlice, SliceParts, ...?我们可以给出的每个名称都是多余的,因为该函数已经被适当地命名了。left 和 right 会让你更清楚哪一部分是哪一面。但是这里我们假设程序员有正确的直觉。英语是从左到右书写的,在英语文化中,数组通常是从左到右绘制的 ([0 | 1 | 2 | 3 ]),所以这对大多数人来说是有意义的。⇒ 一个元组就可以了!
另一个例子:假设您正在编写一个以某种方式与文本文件(编译器、文本编辑器等)一起工作的应用程序。当您谈论文本文件中的区域(例如,搜索结果)时,您希望通过给出字节偏移量来指定该区域。让我们看看元组是否适合我们:
fn find_first_occurence(file: &TextFile, needle: &str) -> (usize, usize)
返回类型自己说话吗?而不是...即使您知道文件中的区域是由字节偏移量指定的,返回值仍然是模棱两可的:要么是(start, end),要么是(start, stuff),其中的东西可以是任何其他搜索指标(该函数不'不需要返回end,因为我们已经知道needle 的长度,因此可以计算它)。所以,我希望你同意,我们要命名返回类型。我们称它为Span——这是使用的名称in the Rust compiler。
下一个问题:结构体还是元组结构体?给字段命名有意义吗?同样,没有明确的答案,但我认为我们确实想要命名这些字段。哪个更容易阅读:span.1 - span.0 或 span.high - span.low?此外,我们可以为命名字段编写文档;例如,记录 high 是独占的。
⇒ 结构
现在假设您想向用户报告行号。尤其重要的是一个函数,它返回给定 span 的相应行号(为简单起见,我们假设这个 span 永远不会超过一行)。
fn get_line_number(file: &TextFile, span: Span) -> ???
那么我们返回什么?在这个函数的上下文中,一个简单的u32 可能就可以了!毫无疑问,u32 代表什么。虽然:行号计数是从 0 开始还是从 1 开始? 叹息
当然,我们可以在函数中记录这个属性......以及使用行号的所有其他函数。那么如何创建一个新类型并在那里记录呢?这也有助于函数采用多个数字,包括行号:
print_snippet(&file, 57, 63, 80);
等等,现在行号是多少?确切地说:不是采用u32s,而是采用LineNumbers——类型系统就是文档。
我们现在同意创建一种新类型。但是:结构还是元组结构?让我们试试结构:
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,我们还应该创建一个新类型来区分它与字符偏移量,例如!
【讨论】:
元组结构不太常见。当您只有几个成员并且很清楚哪些成员不需要名称时使用它们。
元组结构的一个常见用途是newtypes。这是一个只有一个成员的元组结构。这对于围绕现有类型制作简单的包装器很有用。
【讨论】:
元组和元组结构的主要区别在于后者引入了一个名称,而前者没有。
有时,数据片段被捆绑在一起只是因为。例如,想象一个状态机:每次转换都会返回一条数据以及状态机的下一个状态,这个叫什么名字?
impl StateA {
fn on_event_x(self) -> (String, StateB);
}
这不应该被滥用;毕竟,名称对于记录 API 很有用!然而,有时,只是没有合适的愚蠢名称 (StringAndStateB :x),而且元组也能很好地工作。
那么,什么时候使用元组结构呢?当您需要或希望命名类型时,您更喜欢使用元组结构而不是常规结构或枚举(这完全是另一个讨论!)。
【讨论】:
免责声明:我是 Rust 菜鸟。
对于私有方法,你应该知道你传递了什么,这样一个元组就可以了,如果你愿意,你可以随时更改它。
对于公共方法,避免歧义更为重要。例如,假设我们有元组 (u8, u8, u8, u8) 来表示颜色;是指RGBA、ARGB、BGRA、ABGR还是HSLA?
将其表示为元组结构Rgba(u8, u8, u8, u8) 会更清楚。
另外请注意,如果您从公共函数或公共特征公开元组,那么您将限制未来的扩展,因为引入的任何添加或更改都会破坏定义的 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!(); }
}
【讨论】: