:用結(jié)構(gòu)體 Struct 組織相關(guān)聯(lián)的數(shù)據(jù))
教程文檔【免費(fèi)下載鏈接】bookThe Rust Programming Language項(xiàng)目地址https://gitcode.com/gh_mirrors/bo/book點(diǎn)擊查看免費(fèi)下載結(jié)構(gòu)體struct是 Rust 中用于把多個(gè)相關(guān)聯(lián)的值打包成一個(gè)有意義整體、并給每個(gè)部分命名的自定義數(shù)據(jù)類型。本文基于《The Rust Programming Language》本項(xiàng)目倉庫中的 book 開源書籍第 5 章內(nèi)容系統(tǒng)講解結(jié)構(gòu)體的定義與實(shí)例化、字段初始化簡寫、結(jié)構(gòu)體更新語法、元組結(jié)構(gòu)體、類單元結(jié)構(gòu)體、派生 traitDebug、方法method與關(guān)聯(lián)函數(shù)associated function并輔以倉庫中的可運(yùn)行代碼清單listings/ch05-using-structs-to-structure-related-data/作為實(shí)操佐證。讀完本文你將掌握如何在 Rust 中創(chuàng)建自定義類型、通過impl塊為其綁定行為并能夠理解結(jié)構(gòu)體在所有權(quán)與借用體系下的正確用法。結(jié)構(gòu)體與元組的對(duì)比為什么需要結(jié)構(gòu)體結(jié)構(gòu)體與元組tuple類似都可以容納多個(gè)不同類型的相關(guān)值。但二者的關(guān)鍵區(qū)別在于元組的元素沒有名字只能依賴位置索引如dimensions.0、dimensions.1來訪問而結(jié)構(gòu)體為每一塊數(shù)據(jù)都命名稱為字段 field訪問時(shí)無需關(guān)心聲明順序代碼語義一目了然。也就是說結(jié)構(gòu)體定義相當(dāng)于類型的一份通用模板而實(shí)例instance則是用具體數(shù)據(jù)填充該模板后得到的類型值。定義與實(shí)例化結(jié)構(gòu)體定義結(jié)構(gòu)體使用struct關(guān)鍵字結(jié)構(gòu)體名稱應(yīng)能概括被組合數(shù)據(jù)的含義隨后在大括號(hào)內(nèi)聲明各字段的名稱與類型。以倉庫中 listing-05-01/src/main.rs 的User結(jié)構(gòu)體為例struct User { active: bool, username: String, email: String, sign_in_count: u64, }創(chuàng)建實(shí)例創(chuàng)建實(shí)例時(shí)寫出結(jié)構(gòu)體名后跟包含key: value鍵值對(duì)的大括號(hào)。字段無需按聲明順序書寫fn main() { let user1 User { active: true, username: String::from(someusername123), email: String::from(someoneexample.com), sign_in_count: 1, }; }訪問與修改字段使用點(diǎn)號(hào)dot notation訪問字段例如user1.email。若實(shí)例可變let mut還可以用點(diǎn)號(hào)配合賦值修改某個(gè)字段fn main() { let mut user1 User { active: true, username: String::from(someusername123), email: String::from(someoneexample.com), sign_in_count: 1, }; user1.email String::from(anotheremailexample.com); }注意整個(gè)實(shí)例必須可變Rust 不允許只把個(gè)別字段標(biāo)記為可變。另外與任何表達(dá)式一樣可以把新建實(shí)例作為函數(shù)體的最后一個(gè)表達(dá)式來隱式返回該實(shí)例。字段初始化簡寫Field Init Shorthand當(dāng)函數(shù)參數(shù)名與結(jié)構(gòu)體字段名完全一致時(shí)可以直接寫字段名而不必重復(fù)字段: 參數(shù)的形式fn build_user(email: String, username: String) - User { User { active: true, username, email, sign_in_count: 1, } }這里username與email參數(shù)名和字段名相同因此只需寫username、email即可行為與username: username、email: email完全一致。結(jié)構(gòu)體更新語法Struct Update Syntax當(dāng)需要基于某個(gè)已有實(shí)例創(chuàng)建新實(shí)例、僅改動(dòng)部分字段時(shí)可以用..語法把其余字段從給定實(shí)例中補(bǔ)齊。..user1必須放在最后但其余字段可以任意順序、任意數(shù)量地顯式指定fn main() { // --snip-- let user2 User { email: String::from(anotherexample.com), ..user1 }; }值得特別強(qiáng)調(diào)的是結(jié)構(gòu)體更新語法使用類似賦值本質(zhì)上是*移動(dòng)move*數(shù)據(jù)。在上例中user1的username字段一個(gè)String被移動(dòng)進(jìn)了user2因此創(chuàng)建user2之后不能再使用user1的該字段。如果user2的email和username都使用全新的String只從user1借用active與sign_in_count二者均實(shí)現(xiàn)了Copytrait那么user1在創(chuàng)建user2后依然有效此例中user1.email也仍可使用因?yàn)樗闹禌]有被移走。元組結(jié)構(gòu)體Tuple Structs元組結(jié)構(gòu)體擁有結(jié)構(gòu)體名稱帶來的類型含義但字段沒有名字只有類型。當(dāng)不想為每個(gè)字段命名、又要讓整個(gè)元組成為獨(dú)立類型時(shí)非常有用struct Color(i32, i32, i32); struct Point(i32, i32, i32); fn main() { let black Color(0, 0, 0); let origin Point(0, 0, 0); }關(guān)鍵點(diǎn)black與origin是不同類型——每個(gè)struct定義都是獨(dú)立類型即使內(nèi)部字段類型完全相同比如都是三個(gè)i32接受Color的函數(shù)也不能接收Point。與元組類似元組結(jié)構(gòu)體可以解構(gòu)為各個(gè)部分也可以用.加索引訪問單個(gè)值但與元組不同的是解構(gòu)時(shí)必須寫出結(jié)構(gòu)體類型名例如let Point(x, y, z) origin;。類單元結(jié)構(gòu)體Unit-Like Structs不含任何字段的結(jié)構(gòu)體稱為類單元結(jié)構(gòu)體行為類似()單元類型。當(dāng)需要在某個(gè)類型上實(shí)現(xiàn) trait 而又無需存儲(chǔ)數(shù)據(jù)時(shí)例如測(cè)試中需要永遠(yuǎn)相等的已知結(jié)果類型可以用它struct AlwaysEqual; fn main() { let subject AlwaysEqual; }定義時(shí)只需struct關(guān)鍵字、名稱和分號(hào)實(shí)例化同樣不需要大括號(hào)或圓括號(hào)。倉庫中的 no-listing-04-unit-like-structs/src/main.rs 可直接運(yùn)行驗(yàn)證。結(jié)構(gòu)體的數(shù)據(jù)所有權(quán)為什么用 String 而不是 str在User定義中使用擁有所有權(quán)的String而非字符串切片str是有意為之我們希望每個(gè)實(shí)例擁有自己的全部數(shù)據(jù)并且數(shù)據(jù)在結(jié)構(gòu)體存續(xù)期間始終有效。如果試圖在結(jié)構(gòu)體中存放引用而不標(biāo)注生命周期編譯會(huì)失敗。倉庫中的 no-listing-02-reference-in-struct/src/main.rs 展示了這段無法編譯的代碼struct User { active: bool, username: str, email: str, sign_in_count: u64, }對(duì)應(yīng)的編譯錯(cuò)誤記錄在 no-listing-02-reference-in-struct/output.txt 中error[E0106]: missing lifetime specifier -- src/main.rs:3:15 | 3 | username: str, | ^ expected named lifetime parameter編譯器會(huì)提示引入具名生命周期參數(shù)例如struct Usera配合username: a str。生命周期確保被引用的數(shù)據(jù)與結(jié)構(gòu)體本身存活時(shí)間一致相關(guān)內(nèi)容將在本書第 10 章深入講解在此之前遇到此類報(bào)錯(cuò)時(shí)用String等擁有類型替代str引用即可。實(shí)戰(zhàn)案例用結(jié)構(gòu)體重構(gòu)矩形面積程序?yàn)榱死斫夂螘r(shí)該用結(jié)構(gòu)體我們以計(jì)算矩形面積為例從裸變量逐步重構(gòu)到結(jié)構(gòu)體與方法的完整形態(tài)。先用 Cargo 新建名為rectangles的二進(jìn)制項(xiàng)目并在src/main.rs中編寫代碼。第一階段兩個(gè)獨(dú)立變量fn main() { let width1 30; let height1 50; println!( The area of the rectangle is {} square pixels., area(width1, height1) ); } fn area(width: u32, height: u32) - u32 { width * height }運(yùn)行cargo run輸出The area of the rectangle is 1500 square pixels.。問題在于area函數(shù)有兩個(gè)參數(shù)程序中沒有任何地方表明這兩個(gè)參數(shù)是關(guān)聯(lián)的可讀性差。第二階段用元組重構(gòu)fn main() { let rect1 (30, 50); println!( The area of the rectangle is {} square pixels., area(rect1) ); } fn area(dimensions: (u32, u32)) - u32 { dimensions.0 * dimensions.1 }元組讓參數(shù)個(gè)數(shù)變成一個(gè)但元素沒有名字必須記住width是索引0、height是索引1。對(duì)面積計(jì)算影響不大但若要在屏幕上繪制矩形混用寬高就很容易出錯(cuò)——代碼沒有傳達(dá)數(shù)據(jù)含義。第三階段用結(jié)構(gòu)體重構(gòu)struct Rectangle { width: u32, height: u32, } fn main() { let rect1 Rectangle { width: 30, height: 50, }; println!( The area of the rectangle is {} square pixels., area(rect1) ); } fn area(rectangle: Rectangle) - u32 { rectangle.width * rectangle.height }這里area接收的是Rectangle——對(duì)結(jié)構(gòu)體的不可變借用借用的原理詳見第 4 章這樣main保留所有權(quán)之后仍可使用rect1。訪問被借用結(jié)構(gòu)體的字段并不會(huì)移動(dòng)字段值這也是借用結(jié)構(gòu)體在代碼中常見的原因。函數(shù)簽名現(xiàn)在精確表達(dá)了語義計(jì)算一個(gè)Rectangle的面積。完整的可運(yùn)行代碼見 listing-05-10/src/main.rs。用派生 trait 添加功能Debug 與 dbg!調(diào)試時(shí)往往希望打印出結(jié)構(gòu)體實(shí)例及其所有字段值但直接println!(rect1 is {rect1})會(huì)編譯失敗error[E0277]: Rectangle doesnt implement std::fmt::Displayprintln!的{}占位符默認(rèn)使用Display格式面向終端用戶的輸出?;绢愋椭挥幸环N直觀展示方式故默認(rèn)實(shí)現(xiàn)Display而結(jié)構(gòu)體的展示方式存在多種可能是否帶逗號(hào)、是否帶大括號(hào)、展示哪些字段Rust 不做猜測(cè)因此結(jié)構(gòu)體沒有內(nèi)置Display實(shí)現(xiàn)。編譯器會(huì)給出提示可以改用{:?}或{:#?}。把打印語句改為println!(rect1 is {rect1:?})后又會(huì)出現(xiàn)error[E0277]: Rectangle doesnt implement Debug help: the trait Debug is not implemented for Rectangle note: add #[derive(Debug)] to Rectangle or manually impl Debug for RectangleDebug格式面向開發(fā)者調(diào)試。Rust 內(nèi)置了打印調(diào)試信息的能力但需要顯式開啟——在結(jié)構(gòu)體定義前添加外部屬性#[derive(Debug)]#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let rect1 Rectangle { width: 30, height: 50, }; println!(rect1 is {rect1:?}); }運(yùn)行后輸出rect1 is Rectangle { width: 30, height: 50 }當(dāng)結(jié)構(gòu)體字段較多時(shí)用{:#?}pretty-print可獲得更易讀的多行輸出rect1 is Rectangle { width: 30, height: 50, }使用 dbg! 宏另一種以Debug格式打印的方式是dbg!宏。與println!接收引用、輸出到 stdout不同dbg!會(huì)取得表達(dá)式所有權(quán)打印宏調(diào)用所在的文件與行號(hào)及表達(dá)式的值然后把所有權(quán)交還且輸出到標(biāo)準(zhǔn)錯(cuò)誤流stderrstderr與stdout的區(qū)別在本書第 12 章講解。倉庫中的 no-listing-05-dbg-macro/src/main.rs 是完整示例#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let scale 2; let rect1 Rectangle { width: dbg!(30 * scale), height: 50, }; dbg!(rect1); }運(yùn)行輸出[src/main.rs:10:16] 30 * scale 60 [src/main.rs:14:5] rect1 Rectangle { width: 60, height: 50, }因?yàn)閐bg!會(huì)返回表達(dá)式值width字段正常獲得60對(duì)rect1則傳入引用避免宏拿走所有權(quán)。除Debug外derive屬性還支持若干其他 trait完整列表見附錄 C自定義實(shí)現(xiàn)與自定義 trait 的寫法在第 10 章介紹。方法Method方法method與函數(shù)類似用fn聲明、有參數(shù)與返回值、包含可執(zhí)行代碼。區(qū)別在于方法定義在結(jié)構(gòu)體或枚舉、trait 對(duì)象的上下文中且第一個(gè)參數(shù)永遠(yuǎn)是self代表調(diào)用該方法的結(jié)構(gòu)體實(shí)例。方法語法與 impl 塊將area函數(shù)改造成Rectangle的方法需要使用implimplementation塊#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn area(self) - u32 { self.width * self.height } } fn main() { let rect1 Rectangle { width: 30, height: 50, }; println!( The area of the rectangle is {} square pixels., rect1.area() ); }在impl Rectangle塊內(nèi)的所有內(nèi)容都與Rectangle類型相關(guān)聯(lián)。調(diào)用時(shí)使用方法語法實(shí)例后加點(diǎn)號(hào)、方法名、圓括號(hào)與參數(shù)rect1.area()。self是self: Self的簡寫——在impl塊中Self是當(dāng)前類型的別名。方法第一個(gè)參數(shù)必須是類型為Self、名為self的參數(shù)Rust 允許只用self簡寫前面的表示借用方式。方法可以取得self所有權(quán)、不可變借用self本例如是或可變借用mut self。若方法需要改寫實(shí)例本身就用mut self直接以self取得所有權(quán)較為少見通常用于把self變換成別的類型、并阻止調(diào)用方繼續(xù)使用原實(shí)例的場景。完整代碼見 listing-05-13/src/main.rs。使用方法而非函數(shù)的主要理由是組織性把某個(gè)類型實(shí)例能做的所有事情集中在同一impl塊內(nèi)使用者無需在代碼各處尋找該類型的能力。方法名與字段同名getter方法可以刻意與字段同名。Rust 根據(jù)是否帶圓括號(hào)區(qū)分二者rect1.width()調(diào)用方法rect1.width訪問字段。倉庫中的 no-listing-06-method-field-interaction/src/main.rs 展示了這種寫法impl Rectangle { fn width(self) - bool { self.width 0 } } fn main() { let rect1 Rectangle { width: 30, height: 50, }; if rect1.width() { println!(The rectangle has a nonzero width; it is {}, rect1.width); } }這種只返回字段值、不做其他事情的方法稱為getter。Rust 不像某些語言那樣為結(jié)構(gòu)體字段自動(dòng)生成 getter手寫 getter 的價(jià)值在于可以把字段設(shè)為私有、方法設(shè)為公有從而在類型公共 API 中提供對(duì)該字段的只讀訪問公有/私有的控制見第 7 章。為什么沒有 - 運(yùn)算符C/C 中調(diào)用對(duì)象方法與指針方法分別使用.與-后者相當(dāng)于先解引用(*object).something()。Rust 沒有-運(yùn)算符取而代之的是自動(dòng)引用與解引用automatic referencing and dereferencing調(diào)用object.something()時(shí)Rust 會(huì)自動(dòng)補(bǔ)上、mut或*使object匹配方法的簽名。例如下面兩種寫法等價(jià)p1.distance(p2); (p1).distance(p2);之所以可行是因?yàn)榉椒ㄓ忻鞔_的接收者——self的類型。根據(jù)接收者與方法名Rust 能確定方法是讀取self、修改mut self還是消費(fèi)self。方法接收者隱式借用是讓所有權(quán)在實(shí)際使用中保持舒適ergonomic的重要因素。帶多個(gè)參數(shù)的方法can_hold方法在self之后可以帶多個(gè)參數(shù)用法與函數(shù)參數(shù)一致。下面實(shí)現(xiàn)can_hold判斷一個(gè)Rectangle能否完全容納另一個(gè)impl Rectangle { fn area(self) - u32 { self.width * self.height } fn can_hold(self, other: Rectangle) - bool { self.width other.width self.height other.height } } fn main() { let rect1 Rectangle { width: 30, height: 50, }; let rect2 Rectangle { width: 10, height: 40, }; let rect3 Rectangle { width: 60, height: 45, }; println!(Can rect1 hold rect2? {}, rect1.can_hold(rect2)); println!(Can rect1 hold rect3? {}, rect1.can_hold(rect3)); }參數(shù)rect2是對(duì)Rectangle的不可變借用方法只需讀取rect2且main在調(diào)用后仍要使用它。輸出為Can rect1 hold rect2? true Can rect1 hold rect3? false該示例在倉庫中的完整可運(yùn)行版本見 listing-05-15/src/main.rs。關(guān)聯(lián)函數(shù)Associated Functionsimpl塊內(nèi)定義的所有函數(shù)都稱為關(guān)聯(lián)函數(shù)因?yàn)樗鼈兣cimpl后面的類型相關(guān)聯(lián)。不以self作為第一個(gè)參數(shù)的關(guān)聯(lián)函數(shù)不是方法它們不需要類型實(shí)例即可調(diào)用——前面用過的String::from就是典型例子。不以self開頭的關(guān)聯(lián)函數(shù)常用于構(gòu)造函數(shù)返回該結(jié)構(gòu)體的新實(shí)例。這類函數(shù)習(xí)慣命名為new但new并非語言內(nèi)置的特殊名稱impl Rectangle { fn square(size: u32) - Self { Self { width: size, height: size, } } }返回類型與函數(shù)體中的Self都是impl關(guān)鍵字后類型的別名此處即Rectangle。調(diào)用時(shí)使用::語法并冠以結(jié)構(gòu)體名let sq Rectangle::square(3);。::語法既用于關(guān)聯(lián)函數(shù)也用于模塊命名空間見第 7 章。倉庫中的 no-listing-03-associated-functions/src/main.rs 提供了可運(yùn)行的完整版本。多個(gè) impl 塊每個(gè)結(jié)構(gòu)體允許擁有多個(gè)impl塊。例如把a(bǔ)rea與can_hold分別放進(jìn)兩個(gè)impl Rectangle塊是完全合法的impl Rectangle { fn area(self) - u32 { self.width * self.height } } impl Rectangle { fn can_hold(self, other: Rectangle) - bool { self.width other.width self.height other.height } }當(dāng)前示例沒有拆分的必要但這是有效語法第 10 章討論泛型與 trait 時(shí)會(huì)看到多impl塊的實(shí)用場景。本章配套源碼與延伸閱讀本章全部可編譯/可復(fù)現(xiàn)的代碼清單位于倉庫的listings/ch05-using-structs-to-structure-related-data/目錄覆蓋定義listing-05-01、實(shí)例化listing-05-02、字段修改listing-05-03、簡寫listing-05-05、更新語法listing-05-07、元組結(jié)構(gòu)體no-listing-01、類單元結(jié)構(gòu)體no-listing-04、生命周期報(bào)錯(cuò)no-listing-02、派生 trait 調(diào)試listing-05-12、no-listing-05-dbg-macro、output-only-01-debug、output-only-02-pretty-debug以及方法與關(guān)聯(lián)函數(shù)listing-05-13 至 listing-05-16。本倉庫的src/目錄保留了本書主版本對(duì)應(yīng)章節(jié)源文件ch05-01-defining-structs.md、ch05-02-example-structs.md 與 ch05-03-method-syntax.md可對(duì)照閱讀。直接使用println!({:?}, x)遇到編譯錯(cuò)誤時(shí)運(yùn)行rustc --explain E0277可獲取Debug/Display的詳細(xì)解釋。小結(jié)結(jié)構(gòu)體讓你能創(chuàng)建對(duì)業(yè)務(wù)領(lǐng)域有意義的自定義類型把相關(guān)聯(lián)的數(shù)據(jù)聚合在一起并為每塊數(shù)據(jù)命名使代碼清晰可讀。在impl塊中可以定義與類型關(guān)聯(lián)的函數(shù)其中方法第一個(gè)參數(shù)為self規(guī)定了結(jié)構(gòu)體實(shí)例的行為。結(jié)構(gòu)體并非創(chuàng)建自定義類型的唯一途徑——接下來本書將進(jìn)入第 6 章介紹枚舉enum這一同為自定義類型基石的語言特性配合模式匹配發(fā)揮更強(qiáng)大的類型檢查能力。贊分享教程文檔【免費(fèi)下載鏈接】bookThe Rust Programming Language項(xiàng)目地址https://gitcode.com/gh_mirrors/bo/book點(diǎn)擊查看免費(fèi)下載相關(guān)推薦如何為老舊Intel Mac安裝最新macOSOpenCore Legacy Patcher終極升級(jí)指南如何為老舊Intel Mac安裝最新macOSOpenCore Legacy Patcher終極升級(jí)指南 OpenCore Legacy Patcher是一款操作系統(tǒng)固件驅(qū)動(dòng)開發(fā)The Rust Programming Language如何編寫高效Rust函數(shù)的7個(gè)技巧The Rust Programming Language如何編寫高效Rust函數(shù)的7個(gè)技巧 Rust作為一門現(xiàn)代系統(tǒng)編程語言其函數(shù)設(shè)計(jì)哲學(xué)強(qiáng)調(diào)安全、性能和教程文檔終極指南如何簡單快速解決Cursor試用限制問題終極指南如何簡單快速解決Cursor試用限制問題 你是否曾經(jīng)遇到過這樣的情況剛剛開始享受Cursor AI代碼編輯器的流暢體驗(yàn)突然彈出一條令人沮喪的提示開發(fā)工具CLI上一篇Diffusers 實(shí)戰(zhàn)指南Stable Diffusion 推理提速、顯存優(yōu)化與畫質(zhì)提升下一篇PaddleOCR 手寫 OCR 數(shù)據(jù)集實(shí)戰(zhàn)指南CASIA-HWDB 與 NIST19 的獲取、格式適配與訓(xùn)練方案創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考