Rust中何時應使用 String 還是 &str?

banq發表於2024-10-17

Rust 有兩種主要的字串型別:String和&str。有時,人們認為這兩種型別使得 Rust 程式碼難以編寫,因為你必須考慮在特定情況下應該使用哪一種。我編寫 Rust 的經驗是,我並沒有真正考慮過這個問題,這篇文章是關於一些經驗法則,你可以使用這些法則來像我一樣。

第一級:根本不要考慮
您可以做的第一件事就是遵循最簡單的規則:

總是使用String,永遠不要使用&str。

看起來像這樣:

struct Person {
    name: String,
}

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

這種風格意味著你有時可能需要新增.to_string()或.clone() 才能使事情正常工作:

fn main() {
    let sentence = <font>"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<i>
    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(<font>"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(<font>"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!(<font>"{}", 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,
}

<font>// we're returning a substring of words, so &str is appropriate<i>
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。

但有時我們不能這樣做:

<font>// 因為我們要將第一個單詞大寫,所以我們的返回型別不能再是<i>
// &str,因為我們實際上不是返回一個子串:我們是<i>
// 建立我們自己的新字串。<i>
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,我們會收到錯誤:

<font>// this can't work<i>
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(<font>"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 確實是可以做到的。預設使用“值”。

因此,在你確定必須這麼做之前,這並不值得花費心思。 當你對程式進行了剖析,並確定將字串複製到結構體或從結構體複製出字串是一個大問題,足以讓你耗費畢生精力時,情況就會是這樣。

 

相關文章