概述
在Rust中,字符串是一種非常重要的數據類型,用於處理文本數據。Rust的字符串是以UTF-8編碼的字節序列,主要有兩種類型:&str和String。其中,&str是一個對字符數據的不可變引用,更像是對現有字符串數據的“視圖”,而String則是一個獨立、可變更的字符串實體。
&str和String
&str和String是Rust中兩種主要的字符串類型,它們在以下6個方面存在比較明顯的區別。
所有權和可變性
&str:是Rust核心語言中唯一的字符串類型,它是一個不可變的字符串切片,是對字符串數據的引用,並不擁有數據的所有權。&str可以安全地使用,但它的內容是不可變的,也就是説,不能改變它指向的字符串的內容。&str可以指向String的內容,也可以指向靜態字符串字面量。
String:這是一個在堆上分配的、可變的字符串類型。String類型由Rust標準庫提供,而不是編入核心語言。它擁有其內容的所有權,這意味着String可以被修改。String本質上是一個封裝了動態大小數組(Vec<u8>)的結構體,該數組存儲了UTF-8編碼的字節。
生命週期
&str:生命週期取決於它的來源。如果是字符串字面量,則生命週期為'static。如果來自某個作用域內的String或其他類型,則其生命週期與該作用域相同。
String:沒有明確的生命週期限制,只要String實例存在,它就可以被使用。
存儲位置
&str:可能是指向靜態內存中的字符串字面量(&'static str),比如:編譯時確定的常量字符串。也可能是指向堆上分配的String的一部分,或者任何其他類型的UTF-8編碼數據的區域。
String:始終在堆上動態分配。
性能
&str:由於它只是一個引用,沒有額外的內存分配成本,因此在某些情況下可能更高效。
String:由於它在堆上分配,因此會有額外的內存分配和複製成本,尤其是在字符串拼接時。
使用場景
&str:當只需要讀取字符串內容,或者想要避免額外的內存分配時,使用&str。此外,在函數參數中,使用&str可以允許函數接受不同類型的字符串參數,包括:String和靜態字符串字面量。
String:當需要一個可變的字符串,或者不關心字符串的具體來源時,使用String。
與C/C++語言的比較
&str:類似於C語言中的const char *,它只是一個指向字符串數據的指針,並不擁有數據。在Rust中,&str比C語言中的裸指針更安全,因為它有一個生命週期參數來確保引用的有效性。
String:類似於C++中的std::string,是一個字符的容器,並且擁有其內容。
字符串的創建
在Rust中,創建字符串有多種方法。根據具體需求,我們可以選擇不同的方法。如果需要一個可變的字符串並且打算在程序運行時修改它,那麼String類型是最佳選擇。如果只是需要一個對靜態文本的引用,那麼&str就足夠了。
使用字符串字面量創建&str
字符串字面量是在代碼中直接寫入的文本,它們被存儲在程序的只讀數據段中,並且是不可變的。字符串字面量隱式地具有&str類型。在下面的示例代碼中,text是一個指向字符串字面量的引用,其類型為&str。
let text: &str = "Hello, World";
使用String::new創建空的String
如果我們想要一個可變的、可以增長的字符串,應該使用String類型。在下面的示例代碼中,empty_str是一個空的String變量,我們可以向其中添加內容。
fn main() {
let mut empty_str = String::new();
empty_str.push_str("Hello");
println!("{}", empty_str);
}
使用字符串字面量初始化String
可以直接將字符串字面量轉換為String,這是通過調用to_string方法或to_owned方法來實現的。
fn main() {
let text1 = "Hello, World".to_string();
let str_slice: &str = "Hello, Rust";
let text2 = str_slice.to_owned();
println!("{}", text1);
println!("{}", text2);
}
使用format!宏創建String
format!宏是Rust中創建格式化字符串的強大工具,它可以根據提供的格式字符串和參數生成一個 String。
fn main() {
let name: &str = "World";
let info = format!("Hello, {}", name);
println!("{}", info);
}
使用String::from創建String
String::from是一個便利的方法,用於從實現了Into<String>特徵的任何類型創建String。因為字符串字面量隱式地實現了這個特徵,故可以直接使用。
let text = String::from("Hello, World");
字符串的拼接
Rust提供了強大的字符串拼接功能,可以讓字符串操作變得更加靈活和高效。
使用+運算符或+=運算符
如果想要將兩個String類型進行拼接,可以使用+運算法。
fn main() {
let str1 = String::from("Hello");
let str2 = String::from(" World");
// 不能直接使用str1 + str2
let str = str1 + &str2;
println!("{}", str);
// 編譯錯誤:value borrowed here after move
println!("{}", str1);
}
在上面的示例代碼中,我們將str1和str2進行了拼接,並得到了str。拼接時,我們使用了&str2,而沒有直接使用str2。拼接完成後,str1不再有效。之所以會這樣,與使用+運算符時調用的函數簽名有關。Rust的+運算符使用了add函數,其簽名與下面的函數聲明類似。
fn add(self, s: &str) -> String
首先,str2使用了&,意味着我們使用第二個字符串的引用與第一個字符串相加。這是因為add函數只能將&str和String相加,而不能將兩個String值相加。在Rust中,可以通過Deref強制轉換將&String強轉成&str,相當於自動把&str2變成了&str2[..]。其次,add函數直接獲取了self的所有權,因為self沒有使用&。這意味着,str1的所有權被移動到add函數後,str1將不再有效。
若要對可變的String進行拼接操作,還可以使用+=操作符。但實際上,這並不是簡單的連接,而是創建了一個新的String實例,並丟棄了原String分配的內存。
fn main() {
let mut str1 = String::from("Hello");
let str2 = " World";
str1 += str2;
println!("{}", str1);
}
注意:使用+=運算符,或者連續使用+運算符進行多次拼接,會導致多次內存分配,效率較低,尤其是在處理大量數據時。如果需要高效地拼接多個字符串,建議使用下面的format!宏。
使用format!宏
format!宏是一種更靈活且高效的字符串拼接方法,尤其適用於包含變量和格式化文本的情況。format!宏可以處理各種複雜的格式化需求,並且它的性能通常優於簡單的+拼接。
fn main() {
let name: &str = "World";
let info = format!("Hello, {}", name);
println!("{}", info);
}
使用push_str方法或push方法
如果已經有了一個String變量,並且想要將另一個字符串或字符追加到它後面,可以使用push_str方法或push方法。注意:push系列方法不會創建新的String實例,而是直接在原有的String緩衝區上追加內容,這通常比使用+運算符更高效。
fn main() {
let mut text = String::from("Hello ");
text.push_str("Rust");
println!("{}", text);
text.push(' ');
text.push('C');
text.push('S');
text.push('D');
text.push('N');
println!("{}", text);
}
字符串的搜索與替換
在Rust中,我們可以使用find、rfind、contains、replace等方法來進行字符串的搜索與替換。在下面的示例代碼中,我們首先調用find方法查找子串"World",並返回一個Option類型的值。接下來,我們調用contains方法來檢查text字符串是否包含了子串"Hello",若包含,返回true,否則返回false。最後,我們調用replace方法來替換字符串中的子串。
replace方法接收兩個參數:第一個參數是要被替換的子串,第二個參數是替換後的新子串。該方法會返回一個新的字符串,其中所有與給定模式匹配的子串都被替換為指定的替換字符串。注意:第一個參數中的原始字符串不會被修改。
fn main() {
let text = "Hello World";
// 搜索子串
let index = text.find("World");
if let Some(value) = index {
println!("found: {}", value);
} else {
println!("not found");
}
// 包含子串
let contain_hello = text.contains("Hello");
println!("contain hello: {}", contain_hello);
// 替換子串
let replaced: String = text.replace("World", "GitHub");
println!("{}", replaced);
}
字符串的長度
在Rust中,獲取字符串的長度是一個常見的操作。Rust的String類型提供了一個len方法,可以用來獲取字符串中字節的數量。需要特別注意的是:這個長度是以字節為單位的,對於ASCII字符串來説,每個字符佔用一個字節;但是,對於包含多字節字符(比如:UTF-8編碼的Unicode字符)的字符串,len方法返回的是字節的總數,而不是字符的總數。
如果想要獲取字符串中Unicode字符的數量,我們應該使用chars方法,然後計算迭代器中元素的數量。chars方法會返回一個迭代器,該迭代器逐個產生字符串中的Unicode字符。
fn main() {
let text = "Hello 霸都";
// 獲取字節長度
let byte_len = text.len();
// 輸出:12
println!("{}", byte_len);
// 獲取字符長度
let char_len = text.chars().count();
// 輸出:8
println!("{}", char_len);
}
另外,Rust字符串不支持直接通過索引來訪問單個字符。這是因為,UTF-8編碼格式下,單個字符可能佔用1到4個字節,索引操作會帶來潛在的非確定性和不一致性問題。如果確實需要通過索引訪問字符,可以使用chars()方法。它會返回一個迭代器,產生字符串中的每個Unicode字符。然後,我們可以使用nth方法或者其他集合方法來獲取特定位置的字符。
fn main() {
let text = "Hello 霸都";
// 注意:索引從0開始計數
let index = 6;
let cur_char = text.chars().nth(index);
// 輸出:index 6: 霸
match cur_char {
Some(c) => println!("index {}: {}", index, c),
None => println!("index out of bounds"),
}
}
字符串與字節的轉換
Rust中的字符串和字節之間可以方便地進行轉換,這在處理二進制數據和編解碼時非常有用。
fn main() {
let text = "Hello World";
// 字符串轉字節
let bytes = text.as_bytes();
// 輸出:[72, 101, 108, 108, 111, 32, 67, 83, 68, 78]
println!("{:?}", bytes);
// 字節轉字符串
let bytes2 = [72, 101, 108, 108, 111];
let text2 = std::str::from_utf8(&bytes2).unwrap();
// 輸出:Hello
println!("{}", text2);
}
總結
由於Rust強調安全性與內存管理,它的字符串設計也體現出了這一點:不可變的&str確保了引用安全,而String則通過所有權系統保證了內存的有效管理,避免了懸垂引用和其他常見的內存錯誤。
💡 如果想閲讀最新的文章,或者有技術問題需要交流和溝通,可搜索並關注微信公眾號“希望睿智”。