为什么str类型主要以借用形式存在?

10
这是如何使用str类型:
let hello = "Hello, world!";

// with an explicit type annotation
let hello: &'static str = "Hello, world!";

let hello: str = "Hello, world!";导致expected `str`, found `&str`

为什么文本的默认类型不像所有基本类型、向量和String一样是str而是引用呢?


2
&str 表示它是一个引用(也称为指针)。典型的编译器会将所有字符串常量(多个字节)放在二进制文件中,传递的只是指向这些数据的指针。这在 C 中也是一样的,你通常不会有一个 char[32],而是一个 char* 作为你的变量类型。 - Matthias Wimmer
1
我没有任何官方来源,但我认为这可能是因为str是唯一一个没有实现Copy的原始类型。所以对于整数类型、布尔类型等,让它们成为引用或具有生命周期根本没有意义,因为它们可以通过从可执行文件中复制来拥有上下文的所有权而不会出现运行时问题。然而,对于str来说情况并非如此,因为克隆它的成本很高,你几乎总是需要一个对它的引用或将其变成完整的字符串以便正确地修改它。 - msrd0
@MatthiasWimmer 谢谢,你能解释一下“在二进制中”的意思吗?难道所有的代码不都是在机器级别上的二进制吗? - QurakNerd
@msrd0 还有其他的原始类型没有实现 Copy,包括 切片可变引用,以及 数组元组,如果它们的元素没有实现它。 - Frxstrem
@Frxstrem 嗯,引用已经是引用了,但我同意这也适用于数组和元组,不仅仅是字符串。 - msrd0
显示剩余2条评论
2个回答

11

将字符串和切片仅通过引用进行访问的设计决策具有很多优点:

  1. 字符串可以有任意长度。因此,类型为str的变量在堆栈上难以管理,而&str在堆栈上只有指针的大小(变长数据存储在堆上)。请注意,所有其他原始类型都具有固定长度,每个引用都具有固定长度(不是它所指向的数据),以及每个结构(这是一种组合)。
  2. &str是一个不可变引用。如果您可以定义类型为str的变量,则必须对let mut s: str = "str";赋予语义。在堆栈上处理不可变字符串很困难,甚至更难处理可以追加的字符串。
  3. 拥有的str意味着每次移动都必须复制所有字符,这会影响性能。只需复制引用并保持引用的数据在堆上恒定就更便宜了。这实际上并不是零成本的抽象。
  4. str不是唯一仅作为引用&str出现的类型(对于切片也是如此,例如&[i8]),因此对字符串处理的更改会使其他行为变得奇怪(或必须相应地进行更改)。
假设一个函数可以管理类型为str的变量。现在你想从这个函数返回一个&str。这不可能,因为引用的生命周期最多只能和它指向的值一样长(试试任何原始类型)。由于str是一个局部创建的值,它不能超越函数的作用域。字符串字面值总是引用静态字符串的方便解决了这个问题。这意味着您需要编写额外的代码来将您拥有的str放入静态变量中,以便您可以返回&str。由于静态引用是我需要的默认行为,所以使用少量开销编写它非常方便。

首先,非常感谢您的回复。关于您提到的第一点,这与String有何不同?它具有您提到的属性,同时也是被拥有的。关于第三点,我很困惑为什么会这样?如果str通常是被拥有的,那么传递一个引用给它不会复制东西吗? - QurakNerd
  1. String确实具有大多数str的属性。但它并非零成本。如果您需要可变性或所有权,则可以使用它,但也可以使用&str而不需要付出代价。
  2. 移动引用(例如&str)意味着复制指针(有时编译器甚至可以省略此操作)。但是,如果您在堆栈上保留一个假设的str,则必须移动/复制该值(所有字节)。当然,您可以引用此值-但是那么您的类型就是&str。我只想再添加一点。
- CoronA
2
String 实际上更像 &str 而不是 strString 包含指向其字符串数据的指针,因此移动它很便宜。此外,它具有固定的大小,就像 &str 一样。String&str 之间唯一的区别在于 String 拥有其数据,并且您可以从 String 中添加/删除字符。 - Aloso
我不确定如何最好地将这个放在你的答案中,但是 &str 并不总是指向堆。它可能在文本段(字符串字面量)或堆栈上(例如,将 [u8; N] 数组转换为 str)。 - c-x-berger

4
我将尝试提供一个不同的视角。在Rust中,有一个通用约定:如果你有某种类型为T的变量,那么它意味着你拥有与T相关联的数据。如果你有一种类型为&T的变量,则你不拥有该数据。
现在让我们考虑一个堆分配的字符串。根据这个约定,应该有一种非引用类型表示对分配的所有权。确实存在这样一种类型:String
还有一种不同类型的字符串:&'static str。这些字符串没有任何所有者:恰好一个字符串实例被放置在编译后的二进制文件中,并且只传递指针。没有分配和释放,因此也没有所有权。在某种意义上,静态字符串是由编译器拥有的,而不是由程序员拥有的。这就是为什么String不能用于表示静态字符串的原因。
那么为什么不使用&String来表示静态字符串呢?想象一个这样的世界,在这个世界里,以下代码是有效的Rust代码:
let s: &'static String = "hello, world!";

这看起来很好,但实现起来并不是最优的:
  1. String 本身有一个指向实际数据的指针,所以 &String 基本上必须是指向指针的指针。这违反了零成本抽象原则:为什么我们要引入过度的间接层,当编译器静态地知道 "hello, world!" 的地址?
  2. 即使编译器聪明到决定这里不需要过度指针(这会导致一堆其他问题),String 本身仍然包含三个 8 字节字段:

    • 数据指针;
    • 数据长度;
    • 分配容量 - 让我们知道数据之后有多少自由空间。

    然而,当我们谈论静态字符串时,容量毫无意义:静态字符串是只读的。

因此,最终当编译器看到&'static String时,我们实际上希望它只存储数据指针和长度——否则,我们为永远不会使用的东西付出代价,这违反了零成本抽象原则。这看起来像是黑魔法,我们希望编译器做到的:变量类型是&String,但变量本身实际上不是对String的引用。

为了使这个工作起来,我们实际上需要一个不同的类型,而不是&String,它只保存数据指针和长度。这就是&str!与&String相比,它在许多方面都更好:

  1. 没有过度的间接层 - 只有一个指针;
  2. 不存储容量,在许多情况下是没有意义的;
  3. 没有黑魔法:我们将str定义为可变大小的类型(数据本身),因此&str只是对数据的引用。

现在你可能会想知道:为什么不引入str而不是&str?记住惯例:拥有str会意味着您拥有数据,而实际上您并没有。因此使用&str


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