Rust: String vs &str

當你開始第一次學習Rust的時候,不知不覺中就會開始對string類型感到困惑,并與編譯器斗智斗勇:),通常你會認為那應該是一個string吧,然后編譯器就說: Shut the fu*k up。 (努力保持微笑??
為了幫讀者弄清楚Rust中String, &String, str 和 &str的區(qū)別和聯(lián)系,花了一點時間幫你們翻譯了一篇文章并努力讓它看起來不那么無聊 :)。(不用謝我??,覺得有用的話點個贊叭,謝謝啦~
首先,我們來看一個炒雞簡單的函數(shù):向老鐵問好!
fn main() {
let friend_name = "laotie";
greet(friend_name);
}
fn greet(name: String) {
println!("{}!, what's up", name);
}
如果你嘗試編譯這段代碼,編譯器就會教你做人(大霧
來看看錯誤信息叭
error[E0308]: mismatched types
--> src/main.rs:3:9
|
3 | greet(friend_name);
| ^^^^^^^^^^^
| |
| expected struct `std::string::String`, found `&str`
| help: try using a conversion method: `friend_name.to_string()`
error: aborting due to previous error
For more information about this error, try `rustc --explain E0308`.
你可以在Rust-playground中運行這段代碼,點"Run"就可以啦。
這里的錯誤信息還是很容易看懂的,greet函數(shù)本來想要一個std::string::String類型,但是你卻給了它一個&str類型,所以出錯啦,并且編譯器還給出了可能的修正方法。所以按照編譯器說的,把第三行改為let friend_name = "laotie".to_string()就可以了。
同時,它也引出了下面幾個問題:
- 這段代碼的背后發(fā)生了什么?
- 什么是
&str? - 為什么使用函數(shù)
to_string()來進行顯式轉換?
理解String類型
要想回答這些問題,最好還是要理解Rust是如何將數(shù)據存儲在內存中的,可以先去看看官方出品的Rust-Book。
如果你已經安裝了Rust,可以在終端或者Powershell中輸入:
rustup doc --book,然后瀏覽器就會自動打開那本書了,俗稱Rust中的"圣經"。
繼續(xù)沿用前面的例子,我們來研究一下friend_name在內存中的布局,假設我們接受了編譯器的建議:用to_string()將類型轉換成了String。
緩沖區(qū)(buffer)
/ 容量(capacity)
/ / 長度(length)
/ / /
+–––+–––+–––+
棧 │ ? │ 8 │ 6 │ <- friend_name: String
+–│–+–––+–––+
│
[–│–––––––––– 容量 ––––––––––––––]
│
+–V–+–––+–––+–––+–––+–––+–––+–––+
堆 │ l │ a │ o │ t │ i │ e │ │ │
+–––+–––+–––+–––+–––+–––+–––+–––+
[–––––––– 長度 –––––––––]
Rust會將friend_name這個String對象存儲在棧上,這個棧由一個指向緩沖區(qū)的堆分配指針,緩沖區(qū)的容量和數(shù)據的長度組成。有了這些玩意兒,這個String對象的大小(size)就總是保持確定并且為3個字長。
看到這里你可能會有疑惑,String中的容量和長度有什么不一樣的嗎?答案是區(qū)別很大,容量是指緩沖區(qū)的大小,而長度指的緩沖區(qū)里存放著的數(shù)據的長度。但更值得注意的是,當我們要改變這個String對象里所存儲的內容時,它會重新申請緩沖區(qū)大小。比如,我們可以用push_str()方法在后面加一些內容(注意要在friend_name前加mut使其可變)。
let mut friend_name = "laotie"
friend_name.push_str(" shuang ji 666");
事實上,如果你已經非常了解Rust的Vec<T>類型,你早就知道String是啥了,當然如果這樣你也不會在看這篇文章了hhh..
總結一下:String就是三個玩意組成的:指向緩沖區(qū)的堆分配指針,容量,長度。就這么簡單~
理解字符串切片(str)
字符串切片(str)是我們引用別人擁有的字符串文本或者字符串字面量。
如果我們只對名字最后的“雙擊666”感興趣,我們可以用如下方法得到部分字符串:
let mut friend_name = "laotie".to_string();
my_name.push_str( " shuang ji 666");
let last_text = &my_name[7..];
last_text現(xiàn)在是一個引用了friend_name文本的字符串切片(注意,不是字符串切片str, 而是字符串切片的引用),它在內存中的布局如下:
friend_name: String last_text: &str
[––––––––––––] [–––––––]
+–––+––––+––––+–––+–––+–––+
stack frame │ ? │ 32 │ 20 │ │ ? │13 │
+–│–+––––+––––+–––+–│–+–––+
│ │
│ +–––––––––+
│ │
│ │
│ [–│––––––––––––––––––––– str –––––––––––––––––––––––]
+–V–+–––+–––+–––+–––+–––+–––+–V–+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+
heap │ l │ a │ o │ t │ i │ e │ │ s │ h │ u │ a │ n │ g │ │ j │ i │ │ 6 │ 6 │ 6 │
+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+
注意到last_text沒有在棧上存儲容量信息。這是因為它只是另一個會自己管理容量的String對象的一個引用。重要的地方來了,字符串切片(str),是unsized的,即大小不確定的。好了,奇怪的事情又出現(xiàn)了,怎么會是不確定的呢?你一個個數(shù)也能知道它是13個呀。這是因為str是在堆上存儲的,不能直接通過堆獲取它的大小信息,因為堆是動態(tài)分配的(隨時準備重新申請緩沖區(qū)大小)。但是&str是fixed sized的,為什么?因為,它其實就是一個地址啊,引用本身就是我們常說的指針啊,它就是地址,比如0x8342e93ef..之類的。同樣,在實際中,字符串切片永遠是引用所以它們的類型是&str或者str。所以,如果我們以后談到字符串切片,我們指的是&str而不是str,切記。
那么,&String又是個啥呢?,很好理解了呀,它是一個String對象的引用,就是一個地址: &String -> String(buffer, capcity, length) -> heap(buffer)
我想,這大概就解釋清楚了String, &String, str和&str之間的區(qū)別。
理解字符串字面量
看完上面那些,我想你大概已經有個感覺了,現(xiàn)在,我們要回答最核心的問題,即"laotie shuang ji 666"這段字符串字面量在Rust中到底是指什么?
回顧上面所講的,如果我們要使用字符串切片&str,我們要么引用“別人”的字符串,要么自己創(chuàng)建一個字符串字面量。它就是指被一對雙引號括起來的玩意:
let text = "I love Rust" //這是&str,不是String
接下來的問題是,如果說&str是別人的字符串的切片引用,那么字符串字面量是誰的切片引用呢?即這個字符串字面量在當前空間里屬于誰呢?
結論是字符串字面量有一點特殊,它們是“預分配文本(preallocated text)”的字符串切片的引用,該文本作為可執(zhí)行文件的一部分存儲在只讀(read-only)內存中。換句話說,它是我們程序中附帶的“內存”,不依賴堆分配的緩沖區(qū)。
這就是說,在執(zhí)行程序時,堆棧上仍然有一項指向該預分配的內存(preallocated memory):
my_name: &str
[–––––––––––]
+–––+–––+
stack frame │ ? │ 6 │
+–│–+–––+
│
+––+
│
preallocated +–V–+–––+–––+–––+–––+–––+
read-only │ l │ a │ o │ t │ i │ e │
memory +–––+–––+–––+–––+–––+–––+
用白話解釋就是,要是它不屬于任何人,那我就直接把它放在內存里,然后引用它就完事了,我不關心你到底是誰的,我只知道我能讀取你的內容就行了。
讀完以上內容,我還希望你注意到一點,&str所指向的字符串切片是不可修改的,因為它是只讀的。
用哪個?
顯然,這取決于許多因素,但是總的來說,可以肯定的是,如果我們所寫的API不依賴于擁有或者改變這個在使用的字符串,它應該是&str的而不是String。于是,可以寫出一個改進版本的問好函數(shù):
fn greet(name: &str) {
println!("Hello, {}!", name);
}
但是,等一下!如果這個API的調用者真的只有String類型且因為不明原因不能將其轉為&str類型,咋辦?
對Rust來說,完全不是問題,因為有一個超級強大的特性:強制解引用(deref coercing),允許你使用引用運算符&來轉換任何傳遞的String引用,所以,在API被執(zhí)行之前,&String轉換為&str
fn main() {
let name1 = "lao wang";
let name2 = "zhang san".to_string();
greet(name1);
greet(&name2); // `name2`被通過引用傳遞
}
fn greet(name: &str) {
println!("Hello, {}!", name);
}
翻譯完啦,其實這篇博客省去很多細節(jié)沒講,不過,該講的重點,它們之間的區(qū)別,倒是講清楚了,更多細節(jié)我會親自寫一篇博文專門介紹Rust中的字符串,有緣會再見,祝好!