8.2-使用字符串存儲 UTF-8 編碼文本

使用字符串存儲 UTF-8 編碼文本

我們在第4章討論過字符串,但現在將更深入地探討它們。新手 Rustacean 常常因為三個原因而卡在字符串上:Rust 傾向于暴露可能的錯誤、字符串比許多程序員想象的要復雜得多,以及 UTF-8。這些因素結合起來,對于來自其他編程語言的人來說,可能顯得很難理解。

我們在集合的上下文中討論字符串,因為字符串是作為字節集合實現的,并附帶一些方法,當這些字節被解釋為文本時提供有用功能。在本節中,我們將談論 String 上每個集合類型都有的操作,如創建、更新和讀取。我們還會討論 String 與其他集合不同之處,即索引一個 String 時,由于人類和計算機對 String 數據解釋方式不同,這一過程變得復雜。

什么是字符串?

首先定義“字符串”這個術語。Rust 核心語言只有一種字符串類型,即通常以借用形式 &str 出現的字符串切片 str。在第4章,我們講過字符串切片,它們是對存儲在別處某些 UTF-8 編碼數據的引用。例如,字符串字面量存儲在程序二進制文件中,因此它們就是字符串切片。

String 類型由 Rust 標準庫提供,而非內置核心語言,是一種可增長、可變、有所有權且采用 UTF-8 編碼的字符串類型。當 Rustacean 提到 Rust 中“strings”時,他們可能指的是 String 或者 字符串切片 &str 兩種類型中的任意一種,而不僅僅是一種。雖然本節主要講解 String,但這兩種類型都廣泛用于標準庫,而且都是 UTF-8 編碼。

創建新的 String

許多 Vec 可用操作同樣適用于 String,因為實際上,String 是圍繞字節向量封裝的一層包裝器,并增加了一些額外保證、限制和能力。例如,用來創建實例的新函數 new,在 Vec 和 String 中工作方式相同,如清單 8-11 所示:

let mut s = String::new();

清單 8-11:創建一個新的空白 String
這一行代碼創建了一個名為 s 的新空串,我們可以往里加載數據。通常,我們會有一些初始數據想放入該串,為此可以使用 to_string 方法,該方法適用于任何實現了 Display trait 的類型,比如字符字面量。如清單 8-12 所示:

let data = "initial contents";
let s = data.to_string();
// 此方法也能直接作用于字面量:
let s = "initial contents".to_string();

清單 8-12:使用 to_string 方法從字符字面量創建一個 String
這段代碼生成包含初始內容的 string。

我們也可以使用函數 String::from 從字符字面量創建一個 String。如清單 8-13,其代碼等價于使用 to_string 的版本:

let s = String::from("initial contents");

清單 8-13:利用 String::from 函數從字符字面量構造 String
由于 strings 用途廣泛,可以通過很多通用 API 操作它們,給開發者提供大量選擇。有些看似冗余,但各有其用途!這里,String::fromto_string 功能相同,你選哪個取決于風格與可讀性。

記住,strings 是 UTF-8 編碼,所以你可以包含任何正確編碼的數據,如下例(見清單 8-14)所示:

let hello = String::from("?????? ?????");
let hello = String::from("Dobry den");
let hello = String::from("Hello");
let hello = String::from("????");
let hello = String::from("??????");
let hello = String::from("こんにちは");
let hello = String::from("?????");
let hello = String::from("你好");
let hello = String::from("Olá");let hello= String :: from ("Здравствуйте") ;lethello=Str ing :: from ("Hola") ;

清單 8-14 : 將不同語言問候語存入 strings
以上均為有效的 string 值。

更新一個 string

像 Vec 一樣,一個 string 可以增長并改變其內容,只要你往里面推送更多數據。此外,還可以方便地用 + 運算符或 format! 宏連接多個 string 值。

通過 push_str 和 push 向 string 添加內容

我們可以調用 push_str 方法追加一個 string 切片,從而擴展已有 string,如下(見清單8-15):

    let mut s = String::from("foo");s.push_str("bar");

清 單8-15 : 使用push_str方法把string slice添加到string后

執行完上述兩行后,s就成了foobar 。push_str 接受參數為string slice ,因為不一定需要取得參數所有權。例如,下面代碼(見 清 單8-16)希望追加完之后還能繼續訪問s2 :

   let mut s1 = String::from("foo");let s2 = "bar";s1.push_str(s2);println!("s2 is {s2}");

清 單8-16 : 在追加后仍然能訪問原來的slice

如果push_str取得了s2'所有權,那么最后一行打印s2’值就無法成功。但實際運行結果符合預期!

push 方法接受的是 char 類型參數,將該字符添加至末尾。如以下例子(見 清 單8-17 )把’l’加到了 ‘lo’:

   let mut s = String::from("lo");s.push('l');

清 單8-17 : 用push給string添加1個char

結果s == lol.

+運算符或format!宏進行拼接

經常需要合并兩個已存在 strings,一種做法是 + 運算符,例如(見 清 單8-18 ):

   let s1 = String::from("Hello, ");let s2 = String::from("world!");let s3 = s1 + &s2; // 注意:此處移動了's1',不能再用了。

清 單8-18 : 利用+運算符合并兩個Strings得到新值

變量s3== Hello, world!. 為什么`s1’失效?為什么傳遞的是&s2?這是因為 + 調用了 add 函數,其簽名大致如下:

fn add(self, s: &str) -> String {

標準庫里add定義較復雜,這里簡化說明。當調用add時,第2個參數必須是&str,不支持兩個完整 Strings 相加;但&s2 實際上是 &St ring ,為何編譯沒錯呢?

答案是在調用 add 時發生了解引用強制轉換(deref coercion),即把 &S tring 轉換成對應范圍內(&[…]) 的&st r . 我們將在第15章詳細介紹deref coercion 。此外,由于是引用傳參,沒有轉移所有權,所以$s2依舊有效。而 self 參數沒有 &, 表明擁有self所有權,也就是說 $S1 被移動進add 調用了。因此表達式看似復制兩次其實只移動一次,更高效無冗余拷貝.

當需拼接多個 strings 時,+ 會讓表達式變得難懂,例如:

   let s1 = String::from("tic");let s2 = String::from("tac");let s3 = String::from("toe");let s = s1 + "-" + &s2 + "-" + &s3;

此時$s == tic-tac-toe. 多重+號及雙引號使閱讀困難,可改用 format! 宏代替:

   let s1 = String::from("tic");let s2 = String::from("tac");let s3 = String::from("toe");let s = format!("{s1}-{s2}-{s3}");

同樣賦值$tic-tac-toe. format! 類似println!, 不過不是輸出屏幕,而返回含格式化內容的新 String 。且內部采用引用,不轉移參數所有權,使代碼更易讀、更安全.

字符串索引

在許多其他編程語言中,通過索引訪問字符串中的單個字符是一種有效且常見的操作。然而,如果你嘗試在 Rust 中使用索引語法訪問 String 的部分內容,會得到一個錯誤。請看清單 8-19 中的無效代碼。

let s1 = String::from("hi");
let h = s1[0];

清單 8-19:嘗試對 String 使用索引語法
這段代碼會產生如下錯誤:

$ cargo runCompiling collections v0.1.0 (file:///projects/collections)
error[E0277]: the type `str` cannot be indexed by `{integer}`--> src/main.rs:3:16|
3 |     let h = s1[0];|                ^ string indices are ranges of `usize`|= note: you can use `.chars().nth()` or `.bytes().nth()`for more information, see chapter 8 in The Book: <https://doc.rust-lang.org/book/ch08-02-strings.html#indexing-into-strings>= help: the trait `SliceIndex<str>` is not implemented for `{integer}`but trait `SliceIndex<[_]>` is implemented for `usize`= help: for that trait implementation, expected `[_]`, found `str`= note: required for `String` to implement `Index<{integer}>`For more information about this error, try `rustc --explain E0277`.
error: could not compile `collections` (bin "collections") due to 1 previous error

錯誤和提示說明了問題所在:Rust 字符串不支持索引。但為什么呢?要回答這個問題,我們需要討論 Rust 如何在內存中存儲字符串。

內部表示

String 是 Vec<u8> 的封裝。讓我們看看之前 UTF-8 編碼正確的示例字符串(清單 8-14)中的一些例子。首先是:

let hello = String::from("Hola");

此時,len 為4,意味著存儲字符串 “Hola” 的向量長度為4字節。每個字母用 UTF-8 編碼時占用一個字節。然而,下面這一行可能會讓你感到驚訝(注意該字符串以大寫西里爾字母 Ze 開頭,而不是數字3):

let hello = String::from("Здравствуйте");

如果被問及這個字符串有多長,你可能會說12。但實際上,Rust 給出的答案是24:這是“Здравствуйте”用 UTF-8 編碼所需的字節數,因為該字符串中每個 Unicode 標量值占用2個字節。因此,對字符串按字節進行索引并不總能對應到有效的 Unicode 標量值。例如,請看以下無效 Rust 示例代碼:

let hello = "Здравствуйте";
let answer = &hello[0];

你已經知道 answer 不會是第一個字符 З。當以 UTF-8 編碼時,З 的第一個字節是208,第二個是151,所以似乎 answer 應該返回208,但208本身不是有效字符。如果用戶請求獲取這個字符串的第一個字符,他們通常不會想要得到208這樣的原始字節;然而,這正是 Rust 在 byte index=0 時擁有的數據。如果&“hi”[0] 是合法代碼且返回的是原始字節,它將返回104而非’h’。

因此,為避免返回意外值并導致難以發現的 bug,Rust 干脆不允許編譯這類代碼,從開發初期就防止誤解發生。

字節、標量值與圖形簇!哎呀!

關于 UTF-8,還有一點需要注意的是,從 Rust 的角度來看,有三種相關方式來觀察字符串:作為字節、標量值以及圖形簇(最接近我們所謂“字符”的概念)。

例如印地語詞 “??????”,它使用天城文書寫,在計算機中被存儲為 u8 向量,如下所示:

[224,164,168,224,164,174,224,164,184,224,165,141,
224,164,164,
224,165 ,135]

共18個字節,這是計算機最終如何保存這些數據。如果把它們視作 Unicode 標量值,也就是 Rust 中 char 類型,這些 bytes 對應于:

[‘?’, ‘?’, ‘?’, ‘?’, ‘?’, ‘?’]

這里有6個 char 值,但第四和第六不是獨立意義上的“字符”:它們是不完整不能獨立存在的變音符號。最后,如果從圖形簇角度看,則相當于人們認知中的四個印地文字母組成詞匯:

[“?”, “?”, “??”, “??”]

Rust 提供不同方法解釋底層原始數據,使程序可以根據需求選擇合適的人類語言處理方式。

另一個原因是不允許通過索引直接獲取某一位置上的字符,是因為期望所有索引操作都能保證常數時間復雜度(O(1))。但對于 String 來說無法保證這一點,因為必須從開頭遍歷直到目標位置才能確定有多少有效字符。

切片 Strings

對 string 索引用途往往不好界定——到底應該返回什么類型?是單一 byte 值、char 字符、圖形簇還是 string slice?如果確實需要基于范圍創建切片,那么 Rust 要求更明確指定范圍,而非簡單數字下標,例如:

let hello ="Здравствуйте";let s =&hello[0..4];

此處 s 是包含前四個 bytes 的 &str 切片。如前所述,每兩個 bytes 表示一個 character,因此s 包含 “Зд”。

若嘗試只截取部分 character 所占 bytes,比如 &hello[0…1] ,則運行時會 panic,就像 vector 越界一樣報錯:

$ cargo run Compiling collections v0.1.0 (file:///projects/collections) Finished dev profile [unoptimized + debuginfo] target(s) in 0.43s Running target/debug/collections thread ‘main’ panicked at src/main.rs:4:19:
byte index 1 is not a char boundary; it is inside ‘З’ (bytes 0..2) of ‘Здравствуйте’
note : run with RUST_BACKTRACE=1 environment variable to display a backtrace 

因此,在使用范圍創建 string slices 時務必小心,否則程序可能崩潰。

迭代 Strings 方法

處理 strings 最好明確自己想要的是 characters 或者 bytes 。針對單獨 Unicode 標量,可以調用 chars 方法。“Зд”.chars() 會分離出兩個 char 類型元素,可迭代訪問:

for c in "Зд".chars() {println!("{c}");
}

輸出結果為:

З
д

另外也可調用 bytes 返回每個位元組(byte),適用于特定場景:

for b in "Зд".bytes() {println!("{b}");
}

輸出四個位元組(bytes):

208
151
208
180

但請記住,有效 Unicode 標量可能由多個 byte 構成,不可簡單按 byte 操作替代 char 。

由于提取如天城文等復雜腳本中的 grapheme clusters 較困難,此功能未納入標準庫。如需此功能,可查找 crates.io 上相關第三方庫實現。

Strings 并非那么簡單
總結來說,string 很復雜,不同語言對此做出了不同設計權衡。Rust 默認要求正確處理 String 數據,這使得程序員必須提前認真考慮如何處理 UTF-8 數據。這雖然暴露了更多細節,卻避免了后續因非 ASCII 字符帶來的潛在錯誤風險。

好消息是標準庫提供大量基于 String 和 &str 類型的方法幫助正確應對這些復雜情況,比如 contains 用于搜索,以及 replace 用于替換子串等,非常實用,請務必查看官方文檔了解詳情。

接下來,讓我們轉向稍微簡單一點的話題:哈希映射(hash maps)!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/91440.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/91440.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/91440.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

以AI大模型重構教育新生態,打造“教-學-練-輔-評”一體化智能平臺

在《中國教育現代化2035》與“教育新基建”政策驅動下&#xff0c;教育數字化轉型已進入深水區。如何將AI技術深度融合于教學全流程&#xff0c;實現從“標準化”到“個性化”的跨越&#xff1f;文淵智閣推出的 AI教學大模型建設方案 &#xff0c;以“數據驅動AI潛能&#xff0…

AI在法律合同內容比對的應用實例

在商業世界的復雜交易中&#xff0c;合同是至關重要的法律保障。然而&#xff0c;隨著業務的擴展&#xff0c;合同數量呈指數級增長&#xff0c;合同條款也日趨復雜。對于法務和商務團隊來說&#xff0c;如何高效、準確地進行合同比對&#xff0c;成為一個亟待解決的難題。傳統…

【Maven】Maven多模塊拆分與依賴隔離 的終極深度解析,從 原子級配置 到 企業級架構設計,涵蓋 8大核心維度

Maven多模塊拆分與依賴隔離 的終極深度解析&#xff0c;從 原子級配置 到 企業級架構設計&#xff0c;涵蓋 8大核心維度一、模塊化工程結構設計&#xff08;黃金法則&#xff09;1. 分層架構模板2. 依賴流向控制矩陣二、依賴隔離的原子級配置1. 嚴格依賴管理&#xff08;父POM&…

大模型流式長鏈接場景下 k8s 優雅退出 JAVA

一、 java bootstrap.yml bootstrap.yml 啟動文件增加timeout-per-shutdown-phase spring:lifecycle:timeout-per-shutdown-phase: 30m# 這個值是故意設置這么大的&#xff0c;因為現在推理服務支持深度思考# 為了保證用戶側的連接不被斷開&#xff0c;因此我們需要設置超大 g…

uni-app用css編寫族譜樹家譜樹

需求背景&#xff1a;公司接到一個項目&#xff0c;是需要做一個族譜微信小程序&#xff0c;需要有族譜樹&#xff0c;且可以添加家族人員。 靈感來源&#xff1a;在插件市場中下載了作者 羊羊不想寫代碼 的插件tree-list族譜&#xff0c;樹形列表&#xff0c;可縮放滑動 - DC…

思途JSP學習 0731

繼0730&#xff0c;我們對項目做最后的升級一、刪除功能1、新增復選框輔助刪除條目的選擇修改我們的list.jsp和list.js在列表的第一列增加一列選擇框2、給復選框添加全選與行點擊選擇功能在行選擇功能中&#xff0c;因為此時的選擇框還未生成&#xff0c;所以我們將事件委托給他…

某訊視頻風控參數逆向分析

文章目錄1. 寫在前面2. 接口分析3. 加密分析4. 扣JS代碼【&#x1f3e0;作者主頁】&#xff1a;吳秋霖 【&#x1f4bc;作者介紹】&#xff1a;擅長爬蟲與JS加密逆向分析&#xff01;Python領域優質創作者、CSDN博客專家、阿里云博客專家、華為云享專家。一路走來長期堅守并致力…

[Broken IOS] 配置CLI | 終端用戶界面TUI

鏈接&#xff1a;https://palera.in/ docs&#xff1a;palera1n palera1n 是一款專為 Jailbroken蘋果設備 設計的強大工具&#xff0c;支持運行 iOS/iPadOS/tvOS 15.0 及更新系統 的 iPhone、iPad 和 Apple TV。 該工具通過 DFU 模式 下的底層 USB 通信引導設備&#xff0c;…

論文閱讀|ArxiV 2024|Mamba進一步研究|VSSD

論文地址&#xff1a;pdf 代碼地址&#xff1a;code 文章目錄1.研究背景與動機2. 核心方法2.1 預備知識:mamba-ssm2.2 非因果狀態空間對偶性2.3 視覺狀態空間對偶性模型3. 實驗結果3.1 圖像分類任務3.2 目標檢測任務3.3 語義分割任務3.4 消融實驗4.局限性與結論4.1 局限性4.2 結…

Flutter中 Provider 的基礎用法超詳細講解(二)之ChangeNotifierProvider

目錄 前言 一、什么是ChangeNotifierProvider? 二、ChangeNotifier的簡單用法 1.定義狀態類 2.使用ChangeNotifierProvider提供狀態 3.獲取狀態并監聽更新 1.Consumer 2.通過API方式獲取 1.Provider.of (context) 2.context.watch () 3.context.read () 4.各種獲…

2025電商CPS分銷與推客系統小程序開發:趨勢、架構與實戰解析

一、行業趨勢&#xff1a;CPS模式與社交電商的深度融合1.1 電商行業新趨勢根據《2025年電子商務行業發展趨勢預測報告》&#xff0c;社交電商與內容營銷已成為核心增長點。消費者行為呈現三大特征&#xff1a;消費習慣轉變&#xff1a;線上購物占比超70%&#xff0c;Z世代用戶更…

Conda環境下配置的基本命令

功能命令創建環境conda create -n myenv python3.11激活環境conda activate myenv刪除環境conda env remove -n myenv復制環境conda create -n newenv --clone myenv列出所有環境conda env list列出環境所有包conda list徹底清除某個 Conda 環境中的所有已安裝包&#xff08;但…

Ps2025

快捷鍵CShs保存CSw存儲為S選取疊加選取,A選取減去選區C回車保存路徑內容識別 SF5 ADel填充前景色CDel填充背景色A上下 上下行間距A左右 左右字間距C左鍵絲滑放大縮小CASE蓋印圖層C}上移一格CG新建組sF6羽化像素鋼筆工具打上抹點&#xff0c;按住shift水平拉調增弧度左右兩個手柄…

ceph sc 設置文件系統格式化參數

前言 默認的 sc 文件系統 inode 太少,對于小文件場景,往往會出現容量沒滿,inode 已經用盡的情況,本文說明如何設置 inode。 說明 本文使用的是 rook-ceph 部署的 ceph 作為存儲后端。 xfs 文件系統 sc 創建帶格式化參數的 xfs 文件系統的 sc allowVolumeExpansion: t…

【LY88】ubuntu下的常用操作

vscode 下載安裝包 在安裝包所處文件夾空白區域右鍵調出終端 輸入下行命令安裝 c后接tab自動補全安裝包名稱&#xff08;前提是該文件夾中僅這一個c開頭文件&#xff0c;否則得再輸點字母&#xff0c;保證其可唯一索引到&#xff09; sudo dpkg -i ctab安裝完畢后輸入code&…

web應用從服務器主動推動數據到客戶端的方式

html5 websocket 全雙工交互 全雙工通信&#xff1a;建立持久連接&#xff0c;服務端和客戶端可隨時互相發送消息 低延遲&#xff1a;適合實時應用&#xff08;聊天、游戲、股票行情等&#xff09; socket協議是與HTTP協議平級的&#xff0c;websocket協議是建立在TCP協議之上的…

基于Spring Boot實現中醫醫學處方管理實踐

基于Spring Boot實現中醫醫學處方管理 以下是基于Spring Boot實現中醫醫學處方管理的相關示例和資源整理,涵蓋基礎架構、功能模塊及實際應用案例: 基礎項目結構 Spring Boot中醫處方系統通常采用MVC分層設計: 實體類:定義處方、藥材、患者等JPA實體 @Entity public clas…

從“人工核驗”到“智能鑒防”:護照鑒偽設備的科技革命

“一本偽造護照的查獲成本從72小時降至3秒&#xff0c;背后是光學傳感、量子加密與多模態AI的十年協同進化。”2025年全球邊檢口岸查獲偽假護照近500份&#xff0c;其中芯片偽造占比首超40%。當造假技術逼近分子級仿制&#xff0c;傳統肉眼鑒別徹底失效&#xff0c;多光譜成像、…

無人機飛控系統3D (C++)實踐

大疆無人機飛控系統3D模型開發 大疆無人機飛控系統3D模型開發(C++) 核心架構設計 大疆無人機的飛控系統通常采用分層架構,分為硬件抽象層(HAL)、中間件層和應用層。HAL負責與傳感器/執行器直接交互,中間件處理數據融合和通信協議,應用層實現核心控制算法。 典型代碼結…

ES6中import與export的用法詳解

目錄 一、ES6模塊化的核心概念 1. 模塊化的基本規則 二、export的用法 1. 命名導出&#xff08;Named Export&#xff09; 示例&#xff1a; 2. 默認導出&#xff08;Default Export&#xff09; 示例&#xff1a; 默認導出函數或類&#xff1a; 3. 導出語句的統一聲明…