Rust中何时应使用 String 还是 &str?

Rust 有两种主要的字符串类型:String和&str。有时,人们认为这两种类型使得 Rust 代码难以编写,因为你必须考虑在特定情况下应该使用哪一种。我编写 Rust 的经验是,我并没有真正考虑过这个问题,这篇文章是关于一些经验法则,你可以使用这些法则来像我一样。

第一级:根本不要考虑
您可以做的第一件事就是遵循最简单的规则:

总是使用String,永远不要使用&str。

看起来像这样:

struct Person {
    name: String,
}

fn first_word(words: String) -> String {
    words
        .split_whitespace()
        .next()
        .expect("words should not be empty")
        .to_string()
}

这种风格意味着你有时可能需要添加.to_string()或.clone() 才能使事情正常工作:

fn main() {
    let sentence = "Hello, world!";

    println!("{}", first_word(sentence.to_string()));

    let owned = String::from("A string");

    // if we don't clone here, we can't use owned the second time
    println!("{}", first_word(owned.clone()));
    println!("{}", first_word(owned));
}

但没关系,当您需要时,编译器会通知您:

error[E0382]: use of moved value: owned
  --> src/main.rs:21:31
   |
18 |     let owned = String::from("A string");
   |         ----- move occurs because owned has type String, which does not implement the Copy trait
19 |
20 |     println!("{}", first_word(owned));
   |                               ----- value moved here
21 |     println!("{}", first_word(owned));
   |                               ^^^^^ value used here after move
   |
note: consider changing this parameter type in function first_word to borrow instead if owning the value isn't necessary
  --> src/main.rs:5:22
   |
5  | fn first_word(words: String) -> String {
   |    ----------        ^^^^^^ this parameter takes ownership of the value
   |    |
   |    in this function
help: consider cloning the value if the performance cost is acceptable
   |
20 |     println!("{}", first_word(owned.clone()));
   |                                    ++++++++

嘿,编译器,这是个好主意。让我们进入第 2 级。

级别 2:优先&str使用函数参数
一个更好的规则是这样的:

  • 始终String在结构体中使用,对于函数,用于&str参数和String返回值的类型。

这就是第 1 级编译器错误建议我们做的事情.clone。

其结果代码如下:

struct Person {
    name: String,
}

fn first_word(words: &str) -> String {
    words
        .split_whitespace()
        .next()
        .expect("words should not be empty")
        .to_string()
}

fn main() {
    let sentence = "Hello, world!";

    println!("{}", first_word(sentence));

    let owned = String::from("A string");

    println!("{}", first_word(&owned));
    println!("{}", first_word(&owned));
}

现在我们做的复制少了很多。我们确实需要在我们希望传递给的值&上添加一个,但这并不是太糟糕,当我们忘记时,编译器会帮助我们:
Stringfirst_word

error[E0308]: mismatched types
  --> src/main.rs:20:31
   |
20 |     println!("{}", first_word(owned));
   |                    ---------- ^^^^^ expected &str, found String
   |                    |
   |                    arguments to this function are incorrect
   |
note: function defined here
  --> src/main.rs:5:4
   |
5  | fn first_word(words: &str) -> String {
   |    ^^^^^^^^^^ -----------
help: consider borrowing here
   |
20 |     println!("{}", first_word(&owned));
   |                            
  +

遵循这条规则将帮助你成功度过 95% 的情况。是的,这个数字是通过非常科学的过程得出的:“这是我编造的,但在编写 Rust 十二年后,感觉它是正确的。”

对于最后 5% 中的 4%,我们可以进入第 3 级:

第 3 级:有时返回&str
以下是针对特定情况的稍微高级一点的规则:

  • 在结构体中始终使用 String,在函数中使用 &str 表示参数。
  • 如果函数的返回类型是从参数派生的,并且没有被主体改变,则返回 &str。
  • 如果遇到任何问题,请返回 String。

看起来是这样的:

struct Person {
    name: String,
}

// we're returning a substring of words, so &str is appropriate
fn first_word(words: &str) -> &str {
    words
        .split_whitespace()
        .next()
        .expect("words should not be empty")
}

fn main() {
    let sentence = "Hello, world!";

    println!("{}", first_word(sentence));

    let owned = String::from("A string");

    println!("{}", first_word(&owned));
    println!("{}", first_word(&owned));
}

.to_string这样我们就可以删除一个副本,在的主体中 我们不再有一个first_word。

但有时我们不能这样做:

// 因为我们要将第一个单词大写,所以我们的返回类型不能再是
// &str,因为我们实际上不是返回一个子串:我们是
// 创建我们自己的新字符串。
fn first_word_uppercase(words: &str) -> String {
    words
        .split_whitespace()
        .next()
        .expect("words should not be empty")
        .to_uppercase()
}

fn main() {
    let sentence = "Hello, world!";

    println!("{}", first_word_uppercase(sentence));

    let owned = String::from("A string");

    println!("{}", first_word_uppercase(&owned));
    println!("{}", first_word_uppercase(&owned));
}

您如何知道情况确实如此?好吧,在这个特定情况下, to_uppercase已经返回了 a String。所以这是一个很好的提示。如果我们尝试返回 a &str,我们会收到错误:

// this can't work
fn first_word_uppercase(words: &str) -> &str {
    &words
        .split_whitespace()
        .next()
        .expect("words should not be empty")
        .to_uppercase()
}

会给我们

error[E0515]: cannot return reference to temporary value
  --> src/main.rs:7:5
   |
7  |        &words
   |  __^-
   | | __|
8  | ||         .split_whitespace()
9  | ||         .next()
10 | ||         .expect("words should not be empty")
11 | ||         .to_uppercase()
   | ||                       ^
   | ||_____|
   |  |
_____returns a reference to data owned by the current function
   |                          temporary value created here

就是这样。遵循这条规则几乎可以帮你应对所有需要思考String和&str思考的场景。通过一些练习,你会内化这些规则,当你对某个级别感到满意时,你可以进入下一个级别。

那么最后的 1% 怎么办呢?嗯,还有下一个级别……

第 4 级:何时在结构体中使用&str
以下是第 4 级的规则:

  • 是否应该在结构体中使用 &str?
  • 如果你想问这个问题,那就使用 String。
  • 当你需要在结构体中使用 &str 时,你就会知道了。

在结构体中存储引用当然很有用,Rust 支持这一点也很好。 但你只有在相当特殊的情况下才会用到它,如果你觉得自己在担心 String vs &str 的问题,那么你现在还不适合担心在结构体中存储 &str 的复杂性。

事实上,有些人对这一规则深信不疑,以至于他们正在开发一种甚至不可能在结构体中存储引用的语言,我最近发现这种语言非常有趣:Hylo.。

在Hylot语言  中,你可以把所有东西都看作是值,而不是引用。
他们认为,使用这种模型可以编写出有意义的程序。 

对于很多有用的 Rust 程序来说,不在结构体中存储 &strs 确实是可以做到的。默认使用“值”。

因此,在你确定必须这么做之前,这并不值得花费心思。 当你对程序进行了剖析,并确定将字符串复制到结构体或从结构体复制出字符串是一个大问题,足以让你耗费毕生精力时,情况就会是这样。