From a2eb91103ea1fc5b61909bbd3483b9dddf9db9bf Mon Sep 17 00:00:00 2001 From: weihanglo Date: Tue, 6 Aug 2024 03:51:14 +0000 Subject: [PATCH] deploy: f0edaf9fdf48db7668fcb5626cd93e59347cf120 --- index.html | 2 +- print.html | 2 +- searchindex.js | 2 +- searchindex.json | 2 +- title-page.html | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/index.html b/index.html index a15eab67..4c81098e 100644 --- a/index.html +++ b/index.html @@ -143,7 +143,7 @@

第一章的「安裝」段落來安裝或更新 Rust。

本書的 HTML 格式可以在線上閱讀:https://doc.rust-lang.org/stable/book/正體中文版)。而離線版則包含在 rustup 安裝的 Rust 中,輸入 rustup docs --book 就能開啟。

-

社群中也有提供本書的各種[譯本]。

+

社群中也有提供本書的各種譯本

本書也有由 No Starch Press 出版平裝與電子版格式

🚨 想要更有互動的學習體驗?來嘗試不同的 Rust Book,賣點有:隨堂測驗、重點提示、視覺化呈現,更多都在 https://rust-book.cs.brown.edu

diff --git a/print.html b/print.html index db5acf27..a32e7bf1 100644 --- a/print.html +++ b/print.html @@ -144,7 +144,7 @@

第一章的「安裝」段落來安裝或更新 Rust。

本書的 HTML 格式可以在線上閱讀:https://doc.rust-lang.org/stable/book/正體中文版)。而離線版則包含在 rustup 安裝的 Rust 中,輸入 rustup docs --book 就能開啟。

-

社群中也有提供本書的各種[譯本]。

+

社群中也有提供本書的各種譯本

本書也有由 No Starch Press 出版平裝與電子版格式

🚨 想要更有互動的學習體驗?來嘗試不同的 Rust Book,賣點有:隨堂測驗、重點提示、視覺化呈現,更多都在 https://rust-book.cs.brown.edu

diff --git a/searchindex.js b/searchindex.js index 0b15a793..4d528fa6 100644 --- a/searchindex.js +++ b/searchindex.js @@ -1 +1 @@ -Object.assign(window.search, {"doc_urls":["title-page.html#rust-程式設計語言","foreword.html#前言","ch00-00-introduction.html#介紹","ch00-00-introduction.html#rust-適用於誰","ch00-00-introduction.html#開發團隊","ch00-00-introduction.html#學生","ch00-00-introduction.html#公司","ch00-00-introduction.html#開源開發者","ch00-00-introduction.html#重視速度與穩定性的開發者","ch00-00-introduction.html#本書寫給誰看","ch00-00-introduction.html#如何閱讀本書","ch00-00-introduction.html#原始碼","ch01-00-getting-started.html#開始入門","ch01-01-installation.html#安裝教學","ch01-01-installation.html#命令列標記","ch01-01-installation.html#在-linux-或-macos-上安裝-rustup","ch01-01-installation.html#在-windows-上安裝-rustup","ch01-01-installation.html#疑難排除","ch01-01-installation.html#更新與解除安裝","ch01-01-installation.html#本地端技術文件","ch01-02-hello-world.html#hello-world","ch01-02-hello-world.html#建立專案目錄","ch01-02-hello-world.html#編寫並執行-rust-程式","ch01-02-hello-world.html#分析這支-rust-程式","ch01-02-hello-world.html#編譯和執行是不同的步驟","ch01-03-hello-cargo.html#hello-cargo","ch01-03-hello-cargo.html#使用-cargo-建立專案","ch01-03-hello-cargo.html#建構並執行-cargo-專案","ch01-03-hello-cargo.html#建構發佈版本release","ch01-03-hello-cargo.html#將-cargo-視為常規","ch01-03-hello-cargo.html#總結","ch02-00-guessing-game-tutorial.html#設計猜謎遊戲程式","ch02-00-guessing-game-tutorial.html#設置新專案","ch02-00-guessing-game-tutorial.html#處理猜測","ch02-00-guessing-game-tutorial.html#透過變數儲存數值","ch02-00-guessing-game-tutorial.html#取得使用者輸入","ch02-00-guessing-game-tutorial.html#使用-result-處理可能的錯誤","ch02-00-guessing-game-tutorial.html#透過-println-佔位符印出數值","ch02-00-guessing-game-tutorial.html#測試第一個部分","ch02-00-guessing-game-tutorial.html#產生祕密數字","ch02-00-guessing-game-tutorial.html#使用-crate-來取得更多功能","ch02-00-guessing-game-tutorial.html#產生隨機數字","ch02-00-guessing-game-tutorial.html#將猜測的數字與祕密數字做比較","ch02-00-guessing-game-tutorial.html#透過迴圈來允許多次猜測","ch02-00-guessing-game-tutorial.html#猜對後離開","ch02-00-guessing-game-tutorial.html#處理無效輸入","ch02-00-guessing-game-tutorial.html#總結","ch03-00-common-programming-concepts.html#常見程式設計概念","ch03-01-variables-and-mutability.html#變數與可變性","ch03-01-variables-and-mutability.html#常數","ch03-01-variables-and-mutability.html#遮蔽shadowing","ch03-02-data-types.html#資料型別","ch03-02-data-types.html#純量型別","ch03-02-data-types.html#複合型別","ch03-03-how-functions-work.html#函式","ch03-03-how-functions-work.html#參數","ch03-03-how-functions-work.html#陳述式與表達式","ch03-03-how-functions-work.html#函式回傳值","ch03-04-comments.html#註解","ch03-05-control-flow.html#控制流程","ch03-05-control-flow.html#if-表達式","ch03-05-control-flow.html#使用迴圈重複執行","ch03-05-control-flow.html#總結","ch04-00-understanding-ownership.html#理解所有權","ch04-01-what-is-ownership.html#什麼是所有權","ch04-01-what-is-ownership.html#堆疊stack與堆積heap","ch04-01-what-is-ownership.html#所有權規則","ch04-01-what-is-ownership.html#變數作用域","ch04-01-what-is-ownership.html#string-型別","ch04-01-what-is-ownership.html#記憶體與配置","ch04-01-what-is-ownership.html#所有權與函式","ch04-01-what-is-ownership.html#回傳值與作用域","ch04-02-references-and-borrowing.html#參考與借用","ch04-02-references-and-borrowing.html#可變參考","ch04-02-references-and-borrowing.html#迷途參考","ch04-02-references-and-borrowing.html#參考規則","ch04-03-slices.html#切片型別","ch04-03-slices.html#字串切片","ch04-03-slices.html#其他切片","ch04-03-slices.html#總結","ch05-00-structs.html#透過結構體組織相關資料","ch05-01-defining-structs.html#定義與實例化結構體","ch05-01-defining-structs.html#用欄位初始化簡寫語法","ch05-01-defining-structs.html#使用結構體更新語法從其他結構體建立實例","ch05-01-defining-structs.html#使用無名稱欄位的元組結構體來建立不同型別","ch05-01-defining-structs.html#無任何欄位的類單元結構體","ch05-01-defining-structs.html#結構體資料的所有權","ch05-02-example-structs.html#使用結構體的程式範例","ch05-02-example-structs.html#使用元組重構","ch05-02-example-structs.html#使用結構體重構賦予更多意義","ch05-02-example-structs.html#使用推導特徵實現更多功能","ch05-03-method-syntax.html#方法語法","ch05-03-method-syntax.html#定義方法","ch05-03-method-syntax.html#--運算子跑去哪了","ch05-03-method-syntax.html#擁有更多參數的方法","ch05-03-method-syntax.html#關聯函式","ch05-03-method-syntax.html#多重-impl-區塊","ch05-03-method-syntax.html#總結","ch06-00-enums.html#列舉與模式配對","ch06-01-defining-an-enum.html#定義列舉","ch06-01-defining-an-enum.html#列舉數值","ch06-01-defining-an-enum.html#option-列舉相對於空值的優勢","ch06-02-match.html#match-控制流建構子","ch06-02-match.html#綁定數值的模式","ch06-02-match.html#配對-option","ch06-02-match.html#配對必須是徹底的","ch06-02-match.html#catch-all-模式與-_-佔位符","ch06-03-if-let.html#透過-if-let-簡化控制流","ch06-03-if-let.html#總結","ch07-00-managing-growing-projects-with-packages-crates-and-modules.html#透過套件crate-與模組管理成長中的專案","ch07-01-packages-and-crates.html#套件與-crates","ch07-02-defining-modules-to-control-scope-and-privacy.html#定義模組來控制作用域與隱私權","ch07-02-defining-modules-to-control-scope-and-privacy.html#模組懶人包","ch07-02-defining-modules-to-control-scope-and-privacy.html#組織相關程式碼成模組","ch07-03-paths-for-referring-to-an-item-in-the-module-tree.html#參考模組項目的路徑","ch07-03-paths-for-referring-to-an-item-in-the-module-tree.html#使用-pub-關鍵字公開路徑","ch07-03-paths-for-referring-to-an-item-in-the-module-tree.html#使用-super-作為相對路徑的開頭","ch07-03-paths-for-referring-to-an-item-in-the-module-tree.html#公開結構體與列舉","ch07-04-bringing-paths-into-scope-with-the-use-keyword.html#透過-use-關鍵字引入路徑","ch07-04-bringing-paths-into-scope-with-the-use-keyword.html#建立慣用的-use-路徑","ch07-04-bringing-paths-into-scope-with-the-use-keyword.html#使用-as-關鍵字提供新名稱","ch07-04-bringing-paths-into-scope-with-the-use-keyword.html#使用-pub-use-重新匯出名稱","ch07-04-bringing-paths-into-scope-with-the-use-keyword.html#使用外部套件","ch07-04-bringing-paths-into-scope-with-the-use-keyword.html#使用巢狀路徑來清理大量的-use-行數","ch07-04-bringing-paths-into-scope-with-the-use-keyword.html#全域運算子","ch07-05-separating-modules-into-different-files.html#將模組拆成不同檔案","ch07-05-separating-modules-into-different-files.html#其他種的檔案路徑","ch07-05-separating-modules-into-different-files.html#總結","ch08-00-common-collections.html#常見集合","ch08-01-vectors.html#透過向量儲存列表","ch08-01-vectors.html#建立新的向量","ch08-01-vectors.html#更新向量","ch08-01-vectors.html#讀取向量元素","ch08-01-vectors.html#遍歷向量的元素","ch08-01-vectors.html#使用列舉來儲存多種型別","ch08-01-vectors.html#釋放向量的同時也會釋放其元素","ch08-02-strings.html#透過字串儲存-utf-8-編碼的文字","ch08-02-strings.html#什麼是字串","ch08-02-strings.html#建立新的字串","ch08-02-strings.html#更新字串","ch08-02-strings.html#索引字串","ch08-02-strings.html#字串切片","ch08-02-strings.html#遍歷字串的方法","ch08-02-strings.html#字串並不簡單","ch08-03-hash-maps.html#透過雜湊映射儲存鍵值配對","ch08-03-hash-maps.html#建立新的雜湊映射","ch08-03-hash-maps.html#取得雜湊映射的數值","ch08-03-hash-maps.html#雜湊映射與所有權","ch08-03-hash-maps.html#更新雜湊映射","ch08-03-hash-maps.html#雜湊函式","ch08-03-hash-maps.html#總結","ch09-00-error-handling.html#錯誤處理","ch09-01-unrecoverable-errors-with-panic.html#對無法復原的錯誤使用-panic","ch09-01-unrecoverable-errors-with-panic.html#恐慌時該解開堆疊還是直接終止","ch09-01-unrecoverable-errors-with-panic.html#使用-panic-backtrace","ch09-02-recoverable-errors-with-result.html#result-與可復原的錯誤","ch09-02-recoverable-errors-with-result.html#配對不同種的錯誤","ch09-02-recoverable-errors-with-result.html#除了使用-match-配對-result-以外的方式","ch09-02-recoverable-errors-with-result.html#錯誤發生時產生恐慌的捷徑unwrap-與-expect","ch09-02-recoverable-errors-with-result.html#傳播錯誤","ch09-03-to-panic-or-not-to-panic.html#要-panic-還是不要-panic","ch09-03-to-panic-or-not-to-panic.html#範例程式碼原型與測試","ch09-03-to-panic-or-not-to-panic.html#當你知道的比編譯器還多的時候","ch09-03-to-panic-or-not-to-panic.html#錯誤處理的指導原則","ch09-03-to-panic-or-not-to-panic.html#建立自訂型別來驗證","ch09-03-to-panic-or-not-to-panic.html#總結","ch10-00-generics.html#泛型型別特徵與生命週期","ch10-00-generics.html#提取函數來減少重複性","ch10-01-syntax.html#泛型資料型別","ch10-01-syntax.html#在函式中定義","ch10-01-syntax.html#在結構體中定義","ch10-01-syntax.html#在列舉中定義","ch10-01-syntax.html#在方法中定義","ch10-01-syntax.html#使用泛型的程式碼效能","ch10-02-traits.html#特徵定義共同行為","ch10-02-traits.html#定義特徵","ch10-02-traits.html#為型別實作特徵","ch10-02-traits.html#預設實作","ch10-02-traits.html#特徵作為參數","ch10-02-traits.html#回傳有實作特徵的型別","ch10-02-traits.html#透過特徵界限來選擇性實作方法","ch10-03-lifetime-syntax.html#透過生命週期驗證參考","ch10-03-lifetime-syntax.html#透過生命週期預防迷途參考","ch10-03-lifetime-syntax.html#借用檢查器","ch10-03-lifetime-syntax.html#函式中的泛型生命週期","ch10-03-lifetime-syntax.html#生命週期詮釋語法","ch10-03-lifetime-syntax.html#函式簽名中的生命週期詮釋","ch10-03-lifetime-syntax.html#深入理解生命週期","ch10-03-lifetime-syntax.html#結構體定義中的生命週期詮釋","ch10-03-lifetime-syntax.html#生命週期省略","ch10-03-lifetime-syntax.html#在方法定義中的生命週期詮釋","ch10-03-lifetime-syntax.html#靜態生命週期","ch10-03-lifetime-syntax.html#組合泛型型別參數特徵界限與生命週期","ch10-03-lifetime-syntax.html#總結","ch11-00-testing.html#編寫自動化測試","ch11-01-writing-tests.html#如何寫測試","ch11-01-writing-tests.html#測試函式剖析","ch11-01-writing-tests.html#透過-assert-巨集檢查結果","ch11-01-writing-tests.html#透過-assert_eq-與-assert_ne-macros-測試相等","ch11-01-writing-tests.html#加入自訂失敗訊息","ch11-01-writing-tests.html#透過-should_panic-檢查恐慌","ch11-01-writing-tests.html#在測試中使用-result","ch11-02-running-tests.html#控制程式如何執行","ch11-02-running-tests.html#平行或接續執行測試","ch11-02-running-tests.html#顯示函式輸出結果","ch11-02-running-tests.html#透過名稱來執行部分測試","ch11-02-running-tests.html#忽略某些測試除非特別指定","ch11-03-test-organization.html#測試組織架構","ch11-03-test-organization.html#單元測試","ch11-03-test-organization.html#整合測試","ch11-03-test-organization.html#總結","ch12-00-an-io-project.html#io-專案建立一個命令列程式","ch12-01-accepting-command-line-arguments.html#接受命令列引數","ch12-01-accepting-command-line-arguments.html#讀取引數數值","ch12-01-accepting-command-line-arguments.html#args-函式與無效的-unicode","ch12-01-accepting-command-line-arguments.html#將引數數值儲存至變數","ch12-02-reading-a-file.html#讀取檔案","ch12-03-improving-error-handling-and-modularity.html#透過重構來改善模組性與錯誤處理","ch12-03-improving-error-handling-and-modularity.html#分開執行檔專案的任務","ch12-03-improving-error-handling-and-modularity.html#使用-clone-的權衡取捨","ch12-03-improving-error-handling-and-modularity.html#修正錯誤處理","ch12-03-improving-error-handling-and-modularity.html#從-main-提取邏輯","ch12-03-improving-error-handling-and-modularity.html#將程式碼拆到函式庫-crate","ch12-04-testing-the-librarys-functionality.html#透過測試驅動開發完善函式庫功能","ch12-04-testing-the-librarys-functionality.html#編寫失敗的測試","ch12-04-testing-the-librarys-functionality.html#寫出讓測試成功的程式碼","ch12-05-working-with-environment-variables.html#處理環境變數","ch12-05-working-with-environment-variables.html#寫個不區分大小寫的-search-函式的失敗測試","ch12-05-working-with-environment-variables.html#實作-search_case_insensitive-函式","ch12-06-writing-to-stderr-instead-of-stdout.html#將錯誤訊息寫入標準錯誤而非標準輸出","ch12-06-writing-to-stderr-instead-of-stdout.html#檢查該在哪裡寫錯誤","ch12-06-writing-to-stderr-instead-of-stdout.html#將錯誤印出至標準錯誤","ch12-06-writing-to-stderr-instead-of-stdout.html#總結","ch13-00-functional-features.html#函式語言功能疊代器與閉包","ch13-01-closures.html#閉包獲取其環境的匿名函式","ch13-01-closures.html#透過閉包獲取環境","ch13-01-closures.html#閉包型別推導與詮釋","ch13-01-closures.html#獲取參考或移動所有權","ch13-01-closures.html#fn-特徵以及將獲取的數值移出閉包","ch13-02-iterators.html#使用疊代器來處理一系列的項目","ch13-02-iterators.html#iterator-特徵與-next-方法","ch13-02-iterators.html#消耗疊代器的方法","ch13-02-iterators.html#產生其他疊代器的方法","ch13-02-iterators.html#使用閉包獲取它們的環境","ch13-03-improving-our-io-project.html#改善我們的-io-專案","ch13-03-improving-our-io-project.html#使用疊代器移除-clone","ch13-03-improving-our-io-project.html#透過疊代配接器讓程式碼更清楚","ch13-03-improving-our-io-project.html#迴圈與疊代器之間的選擇","ch13-04-performance.html#比較效能迴圈-vs-疊代器","ch13-04-performance.html#總結","ch14-00-more-about-cargo.html#更多關於-cargo-與-cratesio-的內容","ch14-01-release-profiles.html#透過發佈設定檔自訂建構","ch14-02-publishing-to-crates-io.html#發佈-crate-到-cratesio","ch14-02-publishing-to-crates-io.html#寫上有幫助的技術文件註解","ch14-02-publishing-to-crates-io.html#透過-pub-use-匯出理想的公開-api","ch14-02-publishing-to-crates-io.html#設定-cratesio-帳號","ch14-02-publishing-to-crates-io.html#新增詮釋資料到新的-crate","ch14-02-publishing-to-crates-io.html#發佈至-cratesio","ch14-02-publishing-to-crates-io.html#對現有-crate-發佈新版本","ch14-02-publishing-to-crates-io.html#透過-cargo-yank-棄用-cratesio-的版本","ch14-03-cargo-workspaces.html#cargo-工作空間","ch14-03-cargo-workspaces.html#建立工作空間","ch14-03-cargo-workspaces.html#在工作空間中建立第二個套件","ch14-04-installing-binaries.html#透過-cargo-install-安裝執行檔","ch14-05-extending-cargo.html#透過自訂命令來擴展-cargo-的功能","ch14-05-extending-cargo.html#總結","ch15-00-smart-pointers.html#智慧指標","ch15-01-box.html#使用-box-指向堆積上的資料","ch15-01-box.html#使用-box-儲存資料到堆積上","ch15-01-box.html#透過-box-建立遞迴型別","ch15-02-deref.html#透過-deref-特徵將智慧指標視為一般參考","ch15-02-deref.html#追蹤指標的數值","ch15-02-deref.html#像參考般使用-box","ch15-02-deref.html#定義我們自己的智慧指標","ch15-02-deref.html#透過實作-deref-特徵來將一個型別能像參考般對待","ch15-02-deref.html#函式與方法的隱式強制解參考","ch15-02-deref.html#強制解參考如何處理可變性","ch15-03-drop.html#透過-drop-特徵執行清除程式碼","ch15-03-drop.html#透過-stdmemdrop-提早釋放數值","ch15-04-rc.html#rc-參考計數智慧指標","ch15-04-rc.html#使用-rc-來分享資料","ch15-04-rc.html#克隆-rc-實例會增加其參考計數","ch15-05-interior-mutability.html#refcell-與內部可變性模式","ch15-05-interior-mutability.html#透過-refcell-在執行時強制檢測借用規則","ch15-05-interior-mutability.html#內部可變性不可變數值的可變借用","ch15-05-interior-mutability.html#組合-rc-與-refcell-來擁有多個可變資料的擁有者","ch15-06-reference-cycles.html#參考循環會導致記憶體泄漏","ch15-06-reference-cycles.html#產生參考循環","ch15-06-reference-cycles.html#避免參考循環將-rc-轉換成-weak","ch15-06-reference-cycles.html#總結","ch16-00-concurrency.html#無懼並行","ch16-01-threads.html#使用執行緒同時執行程式碼","ch16-01-threads.html#透過-spawn-建立新的執行緒","ch16-01-threads.html#使用-join-等待所有執行緒完成","ch16-01-threads.html#透過執行緒使用-move-閉包","ch16-02-message-passing.html#使用訊息傳遞在執行緒間傳送資料","ch16-02-message-passing.html#通道與所有權轉移","ch16-02-message-passing.html#傳送多重數值並觀察接收者等待","ch16-02-message-passing.html#透過克隆發送者來建立多重生產者","ch16-03-shared-state.html#共享狀態並行","ch16-03-shared-state.html#使用互斥鎖在同時間只允許一條執行緒存取資料","ch16-03-shared-state.html#refcellrc-與-mutexarc-之間的相似度","ch16-04-extensible-concurrency-sync-and-send.html#可延展的並行與-sync-及-send-特徵","ch16-04-extensible-concurrency-sync-and-send.html#透過-send-來允許所有權能在執行緒間轉移","ch16-04-extensible-concurrency-sync-and-send.html#透過-sync-來允許多重執行緒存取","ch16-04-extensible-concurrency-sync-and-send.html#手動實作-send-與-sync-是不安全的","ch16-04-extensible-concurrency-sync-and-send.html#總結","ch17-00-oop.html#rust-的物件導向程式設計特色","ch17-01-what-is-oo.html#物件導向語言的特色","ch17-01-what-is-oo.html#物件包含資料與行為","ch17-01-what-is-oo.html#隱藏實作細節的封裝","ch17-01-what-is-oo.html#作為型別系統與程式碼共享來繼承","ch17-01-what-is-oo.html#多型","ch17-02-trait-objects.html#允許不同型別數值的特徵物件","ch17-02-trait-objects.html#定義共同行為的特徵","ch17-02-trait-objects.html#實作特徵","ch17-02-trait-objects.html#特徵物件執行動態調度","ch17-03-oo-design-patterns.html#實作物件導向設計模式","ch17-03-oo-design-patterns.html#定義-post-並在草稿階段建立新實例","ch17-03-oo-design-patterns.html#儲存文章內容的文字","ch17-03-oo-design-patterns.html#確保文章草稿的內容為空","ch17-03-oo-design-patterns.html#請求文章審核來變更它的狀態","ch17-03-oo-design-patterns.html#透過-approve-改變-content-的行為","ch17-03-oo-design-patterns.html#狀態模式的權衡取捨","ch17-03-oo-design-patterns.html#總結","ch18-00-patterns.html#模式與配對","ch18-01-all-the-places-for-patterns.html#所有能使用模式的地方","ch18-01-all-the-places-for-patterns.html#match-分支","ch18-01-all-the-places-for-patterns.html#if-let-條件表達式","ch18-01-all-the-places-for-patterns.html#while-let-條件迴圈","ch18-01-all-the-places-for-patterns.html#for-迴圈","ch18-01-all-the-places-for-patterns.html#let-陳述式","ch18-01-all-the-places-for-patterns.html#函式參數","ch18-02-refutability.html#可反駁性何時模式可能會配對失敗","ch18-03-pattern-syntax.html#模式語法","ch18-03-pattern-syntax.html#配對字面值","ch18-03-pattern-syntax.html#配對變數名稱","ch18-03-pattern-syntax.html#多重模式","ch18-03-pattern-syntax.html#透過--配對數值範圍","ch18-03-pattern-syntax.html#解構拆開數值","ch18-03-pattern-syntax.html#忽略模式中的數值","ch18-03-pattern-syntax.html#提供額外條件的配對守護","ch18-03-pattern-syntax.html#-綁定","ch18-03-pattern-syntax.html#總結","ch19-00-advanced-features.html#進階特色","ch19-01-unsafe-rust.html#不安全的-rust","ch19-01-unsafe-rust.html#不安全的超能力","ch19-01-unsafe-rust.html#對裸指標解參考","ch19-01-unsafe-rust.html#呼叫不安全函式或方法","ch19-01-unsafe-rust.html#存取或修改可變的靜態變數","ch19-01-unsafe-rust.html#實作不安全特徵","ch19-01-unsafe-rust.html#存取聯合體的欄位","ch19-01-unsafe-rust.html#何時該用不安全程式碼","ch19-02-advanced-traits.html#進階特徵","ch19-02-advanced-traits.html#利用關聯型別在特徵定義中指定佔位符型別","ch19-02-advanced-traits.html#預設泛型型別參數與運算子重載","ch19-02-advanced-traits.html#消除歧義的完全限定語法呼叫同名的方法","ch19-02-advanced-traits.html#使用超特徵要求在一個特徵內有另一特徵的功能","ch19-02-advanced-traits.html#使用新型別模式替外部型別實作外部特徵","ch19-03-advanced-types.html#進階型別","ch19-03-advanced-types.html#透過新型別模式達成型別安全與抽象","ch19-03-advanced-types.html#透過型別別名建立型別同義詞","ch19-03-advanced-types.html#永不回傳的永不型別","ch19-03-advanced-types.html#動態大小型別與-sized-特徵","ch19-04-advanced-functions-and-closures.html#進階函式與閉包","ch19-04-advanced-functions-and-closures.html#函式指標","ch19-04-advanced-functions-and-closures.html#回傳閉包","ch19-05-macros.html#巨集","ch19-05-macros.html#巨集與函式的差異","ch19-05-macros.html#使用-macro_rules-宣告式巨集做普通的超程式設計","ch19-05-macros.html#使用程序式巨集從屬性產生程式碼","ch19-05-macros.html#如何撰寫自訂的-derive-巨集","ch19-05-macros.html#類屬性巨集","ch19-05-macros.html#類函式巨集","ch19-05-macros.html#總結","ch20-00-final-project-a-web-server.html#最終專案建立多執行緒網頁伺服器","ch20-01-single-threaded.html#建立單一執行緒的網頁伺服器","ch20-01-single-threaded.html#監聽-tcp-連線","ch20-01-single-threaded.html#讀取請求","ch20-01-single-threaded.html#仔細觀察-http-請求","ch20-01-single-threaded.html#寫入回應","ch20-01-single-threaded.html#回傳真正的-html","ch20-01-single-threaded.html#驗證請求並選擇性地回應","ch20-01-single-threaded.html#再做一些重構","ch20-02-multithreaded.html#將單一執行緒伺服器轉換為多執行緒伺服器","ch20-02-multithreaded.html#對目前伺服器實作模擬緩慢的請求","ch20-02-multithreaded.html#透過執行緒池改善吞吐量","ch20-03-graceful-shutdown-and-cleanup.html#正常關機與清理","ch20-03-graceful-shutdown-and-cleanup.html#對-threadpool-實作-drop-特徵","ch20-03-graceful-shutdown-and-cleanup.html#對執行緒發送停止接收任務的信號","ch20-03-graceful-shutdown-and-cleanup.html#總結","appendix-00.html#附錄","appendix-01-keywords.html#附錄-a關鍵字","appendix-01-keywords.html#目前有在使用的關鍵字","appendix-01-keywords.html#未來可能會使用而保留的關鍵字","appendix-01-keywords.html#原始標識符","appendix-02-operators.html#附錄-b運算子與符號","appendix-02-operators.html#運算子","appendix-02-operators.html#非運算子符號","appendix-03-derivable-traits.html#附錄-c可推導的特徵","appendix-03-derivable-traits.html#用於開發時輸出的-debug","appendix-03-derivable-traits.html#用於比較相等的-partialeq-與-eq","appendix-03-derivable-traits.html#用於比較順序的-partialord-與-ord","appendix-03-derivable-traits.html#用於複製數值的-clone-與-copy","appendix-03-derivable-traits.html#用於映射數值至固定大小數值的-hash","appendix-03-derivable-traits.html#用於預設數值的-default","appendix-04-useful-development-tools.html#附錄-d---實用開發工具","appendix-04-useful-development-tools.html#透過-rustfmt-自動格式化","appendix-04-useful-development-tools.html#透過-rustfix-修正你的程式碼","appendix-04-useful-development-tools.html#透過-clippy-運用更多功能","appendix-04-useful-development-tools.html#使用-rust-analyzer-整合-ide","appendix-05-editions.html#附錄-e---版號","appendix-06-translation.html#附錄-f本書的翻譯本","appendix-07-nightly-rust.html#附錄-g---rust-的開發流程與每夜版-rust","appendix-07-nightly-rust.html#無停滯穩定","appendix-07-nightly-rust.html#嘟嘟火車出發發佈通道與時刻表","appendix-07-nightly-rust.html#不穩定功能","appendix-07-nightly-rust.html#rustup-與-rust-每夜版的職責","appendix-07-nightly-rust.html#rfc-流程與團隊","appendix-08-terminology.html#中英術語對照表","appendix-08-terminology.html#未翻譯"],"index":{"documentStore":{"docInfo":{"0":{"body":31,"breadcrumbs":2,"title":1},"1":{"body":18,"breadcrumbs":0,"title":0},"10":{"body":35,"breadcrumbs":0,"title":0},"100":{"body":226,"breadcrumbs":0,"title":0},"101":{"body":151,"breadcrumbs":1,"title":1},"102":{"body":70,"breadcrumbs":2,"title":1},"103":{"body":76,"breadcrumbs":1,"title":0},"104":{"body":114,"breadcrumbs":2,"title":1},"105":{"body":88,"breadcrumbs":1,"title":0},"106":{"body":78,"breadcrumbs":3,"title":2},"107":{"body":109,"breadcrumbs":0,"title":0},"108":{"body":6,"breadcrumbs":0,"title":0},"109":{"body":14,"breadcrumbs":2,"title":1},"11":{"body":1,"breadcrumbs":0,"title":0},"110":{"body":80,"breadcrumbs":3,"title":1},"111":{"body":4,"breadcrumbs":1,"title":0},"112":{"body":80,"breadcrumbs":1,"title":0},"113":{"body":74,"breadcrumbs":1,"title":0},"114":{"body":138,"breadcrumbs":1,"title":0},"115":{"body":177,"breadcrumbs":2,"title":1},"116":{"body":34,"breadcrumbs":2,"title":1},"117":{"body":91,"breadcrumbs":1,"title":0},"118":{"body":124,"breadcrumbs":3,"title":1},"119":{"body":84,"breadcrumbs":3,"title":1},"12":{"body":8,"breadcrumbs":0,"title":0},"120":{"body":32,"breadcrumbs":2,"title":0},"121":{"body":46,"breadcrumbs":4,"title":2},"122":{"body":53,"breadcrumbs":2,"title":0},"123":{"body":103,"breadcrumbs":3,"title":1},"124":{"body":6,"breadcrumbs":2,"title":0},"125":{"body":68,"breadcrumbs":1,"title":0},"126":{"body":20,"breadcrumbs":1,"title":0},"127":{"body":5,"breadcrumbs":1,"title":0},"128":{"body":8,"breadcrumbs":0,"title":0},"129":{"body":1,"breadcrumbs":0,"title":0},"13":{"body":11,"breadcrumbs":0,"title":0},"130":{"body":38,"breadcrumbs":0,"title":0},"131":{"body":19,"breadcrumbs":0,"title":0},"132":{"body":121,"breadcrumbs":0,"title":0},"133":{"body":34,"breadcrumbs":0,"title":0},"134":{"body":25,"breadcrumbs":0,"title":0},"135":{"body":15,"breadcrumbs":0,"title":0},"136":{"body":6,"breadcrumbs":4,"title":2},"137":{"body":21,"breadcrumbs":2,"title":0},"138":{"body":81,"breadcrumbs":2,"title":0},"139":{"body":166,"breadcrumbs":2,"title":0},"14":{"body":1,"breadcrumbs":0,"title":0},"140":{"body":194,"breadcrumbs":2,"title":0},"141":{"body":43,"breadcrumbs":2,"title":0},"142":{"body":19,"breadcrumbs":2,"title":0},"143":{"body":11,"breadcrumbs":2,"title":0},"144":{"body":16,"breadcrumbs":0,"title":0},"145":{"body":25,"breadcrumbs":0,"title":0},"146":{"body":42,"breadcrumbs":0,"title":0},"147":{"body":26,"breadcrumbs":0,"title":0},"148":{"body":94,"breadcrumbs":0,"title":0},"149":{"body":16,"breadcrumbs":0,"title":0},"15":{"body":32,"breadcrumbs":3,"title":3},"150":{"body":5,"breadcrumbs":0,"title":0},"151":{"body":11,"breadcrumbs":0,"title":0},"152":{"body":3,"breadcrumbs":2,"title":1},"153":{"body":52,"breadcrumbs":1,"title":0},"154":{"body":136,"breadcrumbs":3,"title":2},"155":{"body":125,"breadcrumbs":2,"title":1},"156":{"body":57,"breadcrumbs":1,"title":0},"157":{"body":26,"breadcrumbs":4,"title":3},"158":{"body":79,"breadcrumbs":3,"title":2},"159":{"body":408,"breadcrumbs":1,"title":0},"16":{"body":25,"breadcrumbs":2,"title":2},"160":{"body":7,"breadcrumbs":4,"title":2},"161":{"body":6,"breadcrumbs":2,"title":0},"162":{"body":33,"breadcrumbs":2,"title":0},"163":{"body":11,"breadcrumbs":2,"title":0},"164":{"body":140,"breadcrumbs":2,"title":0},"165":{"body":8,"breadcrumbs":2,"title":0},"166":{"body":11,"breadcrumbs":0,"title":0},"167":{"body":138,"breadcrumbs":0,"title":0},"168":{"body":0,"breadcrumbs":0,"title":0},"169":{"body":179,"breadcrumbs":0,"title":0},"17":{"body":25,"breadcrumbs":0,"title":0},"170":{"body":147,"breadcrumbs":0,"title":0},"171":{"body":33,"breadcrumbs":0,"title":0},"172":{"body":193,"breadcrumbs":0,"title":0},"173":{"body":35,"breadcrumbs":0,"title":0},"174":{"body":3,"breadcrumbs":0,"title":0},"175":{"body":30,"breadcrumbs":0,"title":0},"176":{"body":142,"breadcrumbs":0,"title":0},"177":{"body":210,"breadcrumbs":0,"title":0},"178":{"body":172,"breadcrumbs":0,"title":0},"179":{"body":192,"breadcrumbs":0,"title":0},"18":{"body":11,"breadcrumbs":0,"title":0},"180":{"body":70,"breadcrumbs":0,"title":0},"181":{"body":1,"breadcrumbs":0,"title":0},"182":{"body":84,"breadcrumbs":0,"title":0},"183":{"body":51,"breadcrumbs":0,"title":0},"184":{"body":119,"breadcrumbs":0,"title":0},"185":{"body":10,"breadcrumbs":0,"title":0},"186":{"body":175,"breadcrumbs":0,"title":0},"187":{"body":85,"breadcrumbs":0,"title":0},"188":{"body":34,"breadcrumbs":0,"title":0},"189":{"body":107,"breadcrumbs":0,"title":0},"19":{"body":3,"breadcrumbs":0,"title":0},"190":{"body":75,"breadcrumbs":0,"title":0},"191":{"body":10,"breadcrumbs":0,"title":0},"192":{"body":43,"breadcrumbs":0,"title":0},"193":{"body":3,"breadcrumbs":0,"title":0},"194":{"body":14,"breadcrumbs":0,"title":0},"195":{"body":5,"breadcrumbs":0,"title":0},"196":{"body":325,"breadcrumbs":0,"title":0},"197":{"body":414,"breadcrumbs":1,"title":1},"198":{"body":221,"breadcrumbs":3,"title":3},"199":{"body":189,"breadcrumbs":0,"title":0},"2":{"body":9,"breadcrumbs":0,"title":0},"20":{"body":13,"breadcrumbs":4,"title":2},"200":{"body":375,"breadcrumbs":1,"title":1},"201":{"body":39,"breadcrumbs":2,"title":2},"202":{"body":18,"breadcrumbs":0,"title":0},"203":{"body":8,"breadcrumbs":0,"title":0},"204":{"body":207,"breadcrumbs":0,"title":0},"205":{"body":198,"breadcrumbs":0,"title":0},"206":{"body":158,"breadcrumbs":0,"title":0},"207":{"body":2,"breadcrumbs":0,"title":0},"208":{"body":73,"breadcrumbs":0,"title":0},"209":{"body":372,"breadcrumbs":0,"title":0},"21":{"body":29,"breadcrumbs":2,"title":0},"210":{"body":3,"breadcrumbs":0,"title":0},"211":{"body":19,"breadcrumbs":2,"title":1},"212":{"body":25,"breadcrumbs":1,"title":0},"213":{"body":29,"breadcrumbs":1,"title":0},"214":{"body":59,"breadcrumbs":3,"title":2},"215":{"body":46,"breadcrumbs":1,"title":0},"216":{"body":106,"breadcrumbs":1,"title":0},"217":{"body":11,"breadcrumbs":1,"title":0},"218":{"body":137,"breadcrumbs":1,"title":0},"219":{"body":88,"breadcrumbs":2,"title":1},"22":{"body":38,"breadcrumbs":3,"title":1},"220":{"body":313,"breadcrumbs":1,"title":0},"221":{"body":302,"breadcrumbs":2,"title":1},"222":{"body":116,"breadcrumbs":2,"title":1},"223":{"body":7,"breadcrumbs":1,"title":0},"224":{"body":312,"breadcrumbs":1,"title":0},"225":{"body":513,"breadcrumbs":1,"title":0},"226":{"body":1,"breadcrumbs":1,"title":0},"227":{"body":128,"breadcrumbs":2,"title":1},"228":{"body":750,"breadcrumbs":2,"title":1},"229":{"body":8,"breadcrumbs":1,"title":0},"23":{"body":29,"breadcrumbs":3,"title":1},"230":{"body":8,"breadcrumbs":1,"title":0},"231":{"body":42,"breadcrumbs":1,"title":0},"232":{"body":4,"breadcrumbs":1,"title":0},"233":{"body":8,"breadcrumbs":0,"title":0},"234":{"body":1,"breadcrumbs":0,"title":0},"235":{"body":127,"breadcrumbs":0,"title":0},"236":{"body":145,"breadcrumbs":0,"title":0},"237":{"body":141,"breadcrumbs":0,"title":0},"238":{"body":301,"breadcrumbs":1,"title":1},"239":{"body":37,"breadcrumbs":0,"title":0},"24":{"body":61,"breadcrumbs":2,"title":0},"240":{"body":64,"breadcrumbs":2,"title":2},"241":{"body":38,"breadcrumbs":0,"title":0},"242":{"body":92,"breadcrumbs":0,"title":0},"243":{"body":80,"breadcrumbs":0,"title":0},"244":{"body":3,"breadcrumbs":2,"title":1},"245":{"body":569,"breadcrumbs":2,"title":1},"246":{"body":260,"breadcrumbs":1,"title":0},"247":{"body":5,"breadcrumbs":1,"title":0},"248":{"body":77,"breadcrumbs":2,"title":1},"249":{"body":5,"breadcrumbs":1,"title":0},"25":{"body":22,"breadcrumbs":4,"title":2},"250":{"body":5,"breadcrumbs":4,"title":2},"251":{"body":79,"breadcrumbs":2,"title":0},"252":{"body":4,"breadcrumbs":6,"title":2},"253":{"body":164,"breadcrumbs":4,"title":0},"254":{"body":270,"breadcrumbs":7,"title":3},"255":{"body":25,"breadcrumbs":5,"title":1},"256":{"body":119,"breadcrumbs":5,"title":1},"257":{"body":42,"breadcrumbs":5,"title":1},"258":{"body":5,"breadcrumbs":5,"title":1},"259":{"body":35,"breadcrumbs":7,"title":3},"26":{"body":99,"breadcrumbs":3,"title":1},"260":{"body":5,"breadcrumbs":4,"title":1},"261":{"body":59,"breadcrumbs":3,"title":0},"262":{"body":436,"breadcrumbs":3,"title":0},"263":{"body":66,"breadcrumbs":6,"title":2},"264":{"body":14,"breadcrumbs":4,"title":1},"265":{"body":6,"breadcrumbs":3,"title":0},"266":{"body":23,"breadcrumbs":0,"title":0},"267":{"body":7,"breadcrumbs":2,"title":1},"268":{"body":28,"breadcrumbs":2,"title":1},"269":{"body":294,"breadcrumbs":2,"title":1},"27":{"body":118,"breadcrumbs":3,"title":1},"270":{"body":12,"breadcrumbs":2,"title":1},"271":{"body":99,"breadcrumbs":1,"title":0},"272":{"body":33,"breadcrumbs":2,"title":1},"273":{"body":96,"breadcrumbs":1,"title":0},"274":{"body":77,"breadcrumbs":2,"title":1},"275":{"body":146,"breadcrumbs":1,"title":0},"276":{"body":25,"breadcrumbs":1,"title":0},"277":{"body":90,"breadcrumbs":2,"title":1},"278":{"body":168,"breadcrumbs":2,"title":1},"279":{"body":8,"breadcrumbs":2,"title":1},"28":{"body":11,"breadcrumbs":3,"title":1},"280":{"body":172,"breadcrumbs":2,"title":1},"281":{"body":92,"breadcrumbs":2,"title":1},"282":{"body":8,"breadcrumbs":2,"title":1},"283":{"body":28,"breadcrumbs":2,"title":1},"284":{"body":674,"breadcrumbs":1,"title":0},"285":{"body":124,"breadcrumbs":3,"title":2},"286":{"body":13,"breadcrumbs":0,"title":0},"287":{"body":205,"breadcrumbs":0,"title":0},"288":{"body":394,"breadcrumbs":2,"title":2},"289":{"body":8,"breadcrumbs":0,"title":0},"29":{"body":14,"breadcrumbs":3,"title":1},"290":{"body":18,"breadcrumbs":0,"title":0},"291":{"body":10,"breadcrumbs":0,"title":0},"292":{"body":32,"breadcrumbs":1,"title":1},"293":{"body":83,"breadcrumbs":1,"title":1},"294":{"body":260,"breadcrumbs":1,"title":1},"295":{"body":93,"breadcrumbs":0,"title":0},"296":{"body":101,"breadcrumbs":0,"title":0},"297":{"body":41,"breadcrumbs":0,"title":0},"298":{"body":53,"breadcrumbs":0,"title":0},"299":{"body":1,"breadcrumbs":0,"title":0},"3":{"body":1,"breadcrumbs":1,"title":1},"30":{"body":10,"breadcrumbs":2,"title":0},"300":{"body":349,"breadcrumbs":0,"title":0},"301":{"body":19,"breadcrumbs":2,"title":2},"302":{"body":4,"breadcrumbs":4,"title":2},"303":{"body":23,"breadcrumbs":3,"title":1},"304":{"body":22,"breadcrumbs":3,"title":1},"305":{"body":9,"breadcrumbs":4,"title":2},"306":{"body":11,"breadcrumbs":2,"title":0},"307":{"body":12,"breadcrumbs":2,"title":1},"308":{"body":1,"breadcrumbs":1,"title":0},"309":{"body":23,"breadcrumbs":1,"title":0},"31":{"body":6,"breadcrumbs":0,"title":0},"310":{"body":107,"breadcrumbs":1,"title":0},"311":{"body":11,"breadcrumbs":1,"title":0},"312":{"body":8,"breadcrumbs":1,"title":0},"313":{"body":30,"breadcrumbs":1,"title":0},"314":{"body":116,"breadcrumbs":1,"title":0},"315":{"body":237,"breadcrumbs":1,"title":0},"316":{"body":7,"breadcrumbs":1,"title":0},"317":{"body":37,"breadcrumbs":1,"title":0},"318":{"body":62,"breadcrumbs":2,"title":1},"319":{"body":60,"breadcrumbs":1,"title":0},"32":{"body":57,"breadcrumbs":0,"title":0},"320":{"body":51,"breadcrumbs":1,"title":0},"321":{"body":131,"breadcrumbs":1,"title":0},"322":{"body":436,"breadcrumbs":3,"title":2},"323":{"body":247,"breadcrumbs":1,"title":0},"324":{"body":5,"breadcrumbs":1,"title":0},"325":{"body":9,"breadcrumbs":0,"title":0},"326":{"body":1,"breadcrumbs":0,"title":0},"327":{"body":30,"breadcrumbs":1,"title":1},"328":{"body":52,"breadcrumbs":0,"title":0},"329":{"body":25,"breadcrumbs":0,"title":0},"33":{"body":78,"breadcrumbs":0,"title":0},"330":{"body":47,"breadcrumbs":0,"title":0},"331":{"body":103,"breadcrumbs":0,"title":0},"332":{"body":42,"breadcrumbs":0,"title":0},"333":{"body":172,"breadcrumbs":0,"title":0},"334":{"body":0,"breadcrumbs":0,"title":0},"335":{"body":15,"breadcrumbs":0,"title":0},"336":{"body":71,"breadcrumbs":0,"title":0},"337":{"body":16,"breadcrumbs":0,"title":0},"338":{"body":41,"breadcrumbs":0,"title":0},"339":{"body":292,"breadcrumbs":0,"title":0},"34":{"body":50,"breadcrumbs":0,"title":0},"340":{"body":255,"breadcrumbs":0,"title":0},"341":{"body":139,"breadcrumbs":0,"title":0},"342":{"body":47,"breadcrumbs":0,"title":0},"343":{"body":4,"breadcrumbs":0,"title":0},"344":{"body":16,"breadcrumbs":0,"title":0},"345":{"body":12,"breadcrumbs":2,"title":1},"346":{"body":22,"breadcrumbs":1,"title":0},"347":{"body":78,"breadcrumbs":1,"title":0},"348":{"body":379,"breadcrumbs":1,"title":0},"349":{"body":54,"breadcrumbs":1,"title":0},"35":{"body":40,"breadcrumbs":0,"title":0},"350":{"body":32,"breadcrumbs":1,"title":0},"351":{"body":7,"breadcrumbs":1,"title":0},"352":{"body":4,"breadcrumbs":1,"title":0},"353":{"body":1,"breadcrumbs":0,"title":0},"354":{"body":91,"breadcrumbs":0,"title":0},"355":{"body":147,"breadcrumbs":0,"title":0},"356":{"body":406,"breadcrumbs":0,"title":0},"357":{"body":238,"breadcrumbs":0,"title":0},"358":{"body":69,"breadcrumbs":0,"title":0},"359":{"body":4,"breadcrumbs":0,"title":0},"36":{"body":82,"breadcrumbs":1,"title":1},"360":{"body":17,"breadcrumbs":0,"title":0},"361":{"body":229,"breadcrumbs":0,"title":0},"362":{"body":134,"breadcrumbs":0,"title":0},"363":{"body":66,"breadcrumbs":1,"title":1},"364":{"body":0,"breadcrumbs":0,"title":0},"365":{"body":107,"breadcrumbs":0,"title":0},"366":{"body":92,"breadcrumbs":0,"title":0},"367":{"body":7,"breadcrumbs":0,"title":0},"368":{"body":9,"breadcrumbs":0,"title":0},"369":{"body":76,"breadcrumbs":1,"title":1},"37":{"body":31,"breadcrumbs":1,"title":1},"370":{"body":25,"breadcrumbs":0,"title":0},"371":{"body":339,"breadcrumbs":1,"title":1},"372":{"body":23,"breadcrumbs":0,"title":0},"373":{"body":21,"breadcrumbs":0,"title":0},"374":{"body":1,"breadcrumbs":0,"title":0},"375":{"body":18,"breadcrumbs":0,"title":0},"376":{"body":17,"breadcrumbs":0,"title":0},"377":{"body":72,"breadcrumbs":1,"title":1},"378":{"body":137,"breadcrumbs":0,"title":0},"379":{"body":45,"breadcrumbs":1,"title":1},"38":{"body":18,"breadcrumbs":0,"title":0},"380":{"body":79,"breadcrumbs":0,"title":0},"381":{"body":100,"breadcrumbs":1,"title":1},"382":{"body":172,"breadcrumbs":0,"title":0},"383":{"body":61,"breadcrumbs":0,"title":0},"384":{"body":0,"breadcrumbs":0,"title":0},"385":{"body":83,"breadcrumbs":0,"title":0},"386":{"body":1490,"breadcrumbs":0,"title":0},"387":{"body":8,"breadcrumbs":0,"title":0},"388":{"body":632,"breadcrumbs":2,"title":2},"389":{"body":641,"breadcrumbs":0,"title":0},"39":{"body":6,"breadcrumbs":0,"title":0},"390":{"body":4,"breadcrumbs":0,"title":0},"391":{"body":1,"breadcrumbs":0,"title":0},"392":{"body":1,"breadcrumbs":0,"title":0},"393":{"body":42,"breadcrumbs":0,"title":0},"394":{"body":13,"breadcrumbs":0,"title":0},"395":{"body":57,"breadcrumbs":0,"title":0},"396":{"body":1,"breadcrumbs":2,"title":1},"397":{"body":152,"breadcrumbs":1,"title":0},"398":{"body":142,"breadcrumbs":1,"title":0},"399":{"body":12,"breadcrumbs":2,"title":1},"4":{"body":11,"breadcrumbs":0,"title":0},"40":{"body":210,"breadcrumbs":1,"title":1},"400":{"body":4,"breadcrumbs":2,"title":1},"401":{"body":16,"breadcrumbs":3,"title":2},"402":{"body":33,"breadcrumbs":3,"title":2},"403":{"body":32,"breadcrumbs":3,"title":2},"404":{"body":10,"breadcrumbs":2,"title":1},"405":{"body":17,"breadcrumbs":2,"title":1},"406":{"body":2,"breadcrumbs":2,"title":1},"407":{"body":20,"breadcrumbs":2,"title":1},"408":{"body":74,"breadcrumbs":2,"title":1},"409":{"body":71,"breadcrumbs":2,"title":1},"41":{"body":83,"breadcrumbs":0,"title":0},"410":{"body":21,"breadcrumbs":4,"title":3},"411":{"body":40,"breadcrumbs":2,"title":1},"412":{"body":18,"breadcrumbs":2,"title":1},"413":{"body":2,"breadcrumbs":6,"title":3},"414":{"body":5,"breadcrumbs":3,"title":0},"415":{"body":63,"breadcrumbs":3,"title":0},"416":{"body":3,"breadcrumbs":3,"title":0},"417":{"body":49,"breadcrumbs":5,"title":2},"418":{"body":15,"breadcrumbs":4,"title":1},"419":{"body":192,"breadcrumbs":1,"title":0},"42":{"body":257,"breadcrumbs":0,"title":0},"420":{"body":18,"breadcrumbs":1,"title":0},"43":{"body":75,"breadcrumbs":0,"title":0},"44":{"body":38,"breadcrumbs":0,"title":0},"45":{"body":140,"breadcrumbs":0,"title":0},"46":{"body":6,"breadcrumbs":0,"title":0},"47":{"body":9,"breadcrumbs":0,"title":0},"48":{"body":116,"breadcrumbs":0,"title":0},"49":{"body":18,"breadcrumbs":0,"title":0},"5":{"body":2,"breadcrumbs":0,"title":0},"50":{"body":101,"breadcrumbs":1,"title":1},"51":{"body":47,"breadcrumbs":0,"title":0},"52":{"body":178,"breadcrumbs":0,"title":0},"53":{"body":154,"breadcrumbs":0,"title":0},"54":{"body":44,"breadcrumbs":0,"title":0},"55":{"body":79,"breadcrumbs":0,"title":0},"56":{"body":160,"breadcrumbs":0,"title":0},"57":{"body":120,"breadcrumbs":0,"title":0},"58":{"body":17,"breadcrumbs":0,"title":0},"59":{"body":3,"breadcrumbs":0,"title":0},"6":{"body":3,"breadcrumbs":0,"title":0},"60":{"body":231,"breadcrumbs":0,"title":0},"61":{"body":245,"breadcrumbs":0,"title":0},"62":{"body":6,"breadcrumbs":0,"title":0},"63":{"body":6,"breadcrumbs":0,"title":0},"64":{"body":3,"breadcrumbs":0,"title":0},"65":{"body":14,"breadcrumbs":1,"title":1},"66":{"body":2,"breadcrumbs":0,"title":0},"67":{"body":23,"breadcrumbs":0,"title":0},"68":{"body":25,"breadcrumbs":1,"title":1},"69":{"body":262,"breadcrumbs":0,"title":0},"7":{"body":3,"breadcrumbs":0,"title":0},"70":{"body":44,"breadcrumbs":0,"title":0},"71":{"body":71,"breadcrumbs":0,"title":0},"72":{"body":136,"breadcrumbs":0,"title":0},"73":{"body":221,"breadcrumbs":0,"title":0},"74":{"body":108,"breadcrumbs":0,"title":0},"75":{"body":0,"breadcrumbs":0,"title":0},"76":{"body":143,"breadcrumbs":0,"title":0},"77":{"body":274,"breadcrumbs":0,"title":0},"78":{"body":15,"breadcrumbs":0,"title":0},"79":{"body":4,"breadcrumbs":0,"title":0},"8":{"body":9,"breadcrumbs":0,"title":0},"80":{"body":5,"breadcrumbs":0,"title":0},"81":{"body":131,"breadcrumbs":0,"title":0},"82":{"body":54,"breadcrumbs":0,"title":0},"83":{"body":109,"breadcrumbs":0,"title":0},"84":{"body":30,"breadcrumbs":0,"title":0},"85":{"body":15,"breadcrumbs":0,"title":0},"86":{"body":108,"breadcrumbs":0,"title":0},"87":{"body":64,"breadcrumbs":0,"title":0},"88":{"body":22,"breadcrumbs":0,"title":0},"89":{"body":50,"breadcrumbs":0,"title":0},"9":{"body":0,"breadcrumbs":0,"title":0},"90":{"body":230,"breadcrumbs":0,"title":0},"91":{"body":3,"breadcrumbs":0,"title":0},"92":{"body":122,"breadcrumbs":0,"title":0},"93":{"body":57,"breadcrumbs":0,"title":0},"94":{"body":128,"breadcrumbs":0,"title":0},"95":{"body":38,"breadcrumbs":0,"title":0},"96":{"body":63,"breadcrumbs":1,"title":1},"97":{"body":2,"breadcrumbs":0,"title":0},"98":{"body":3,"breadcrumbs":0,"title":0},"99":{"body":35,"breadcrumbs":0,"title":0}},"docs":{"0":{"body":"由 Steve Klabnik 與 Carol Nichols,以及 Rust 社群的貢獻撰寫而成 此版本假設你使用的是 Rust 1.65(於 2022-11-03 發布)或更高的版本,並在所有專案中的 Cargo.toml 都有 edition=\"2021\" 來使用 Rust 2021 版號。請查看 第一章的「安裝」段落 來安裝或更新 Rust。 本書的 HTML 格式可以在線上閱讀: https://doc.rust-lang.org/stable/book/ ( 正體中文版 )。而離線版則包含在 rustup 安裝的 Rust 中,輸入 rustup docs --book 就能開啟。 社群中也有提供本書的各種[譯本]。 本書也有由 No Starch Press 出版平裝與電子版格式 。 🚨 想要更有互動的學習體驗?來嘗試不同的 Rust Book,賣點有:隨堂測驗、重點提示、視覺化呈現,更多都在 https://rust-book.cs.brown.edu commit: 3f64052","breadcrumbs":"Rust 程式設計語言 » Rust 程式設計語言","id":"0","title":"Rust 程式設計語言"},"1":{"body":"雖然不是那麼明確,但 Rust 程式設計語言旨在 賦權(empowerment) :無論你現在寫的是何種程式碼,Rust 都能賦予你更多能力,在更廣泛的領域中帶有自信地向前邁進。 比方說,「系統層級」的工作會需要處理低階的記憶體管理、資料呈現與程序並行的細節。對以往來說,這塊程式領域被視為巫術般的存在,只有投入好幾年時間學習的選中之人才有辦法駕馭,也才能懂得如何避開其惡名昭彰的陷阱。而且就算是這個領域的實作者也謹慎行事,也生怕他們的程式碼會出現漏洞、崩潰或損壞。 Rust 破除了這些障礙,消除了以往的陷阱並提供友善全面的工具來協助你。想要「深入」底層控制的程式設計師可以使用 Rust,無需冒著常見的崩潰或安全漏洞風險,也無需學習得經常改變的工具鏈其中的細節。更好地是,這個語言本身就是設計成能引導你自然而然地寫出可靠且在速度與記憶體使用都十分高效的程式碼。 已經在處理底層程式碼的程式設計師可以使用 Rust 來擴展他們的野心。舉例來說,在 Rust 中進行平行化是相對低風險的動作,編譯器會幫你抓到典型的錯誤。而且你也能更有自信且更積極地進行最佳化,不必擔心會意外造成崩潰或漏洞。 但 Rust 並不只限於底層的系統程式設計。其表現力與易用易讀的程度能令人愉快地寫出 CLI 應用程式、網頁伺服器與許多其他種程式碼。你會在本書中看到這兩種簡單的範例。使用 Rust 能這讓你將一個領域所學到的技能延伸到另一個領域,你可以透過寫網頁應用程式來學習 Rust,然後應用同樣的技能到你的樹莓派(Raspberry Pi)上。 本書充分體現了 Rust 賦予其使用者更多能力的潛力。其內容平易近人,不止致力於協助你提升 Rust 的知識,還能提升讓你身為程式設計師的整體信心。所以讓我們準備開始學習吧,歡迎加入 Rust 的社群! — Nicholas Matsakis 與 Aaron Turon","breadcrumbs":"前言 » 前言","id":"1","title":"前言"},"10":{"body":"一般來說,本書預設你會從前往後依序閱讀。後面的章節建立在前面提到的概念上,並且前面的章節不會深入某特定主題,而於後面的章節再議。 你會發現本書有兩種類型的章節:概念章節與專案章節。在概念章節中,你會學到 Rust 的某些概念。在專案章節中,我們會一起應用當前所學來做小專案。第二、十二和二十章是專案章節,其餘是概念章節。 第一章會解釋如何安裝 Rust、如何寫支「Hello, world!」程式以及如何使用 Cargo--Rust 的套件管理與建構工具。第二章透過實作一款猜數字遊戲的程式來介紹 Rust。我們在此提及大概的觀念,並在之後的章節提供更詳細的介紹。如果你想馬上動手實作看看的話,第二章會很適合你。第三章會涵蓋 Rust 與其他程式設計語言類似的功能。第四章則會學習 Rust 的所有權系統。如果你是嚴謹派的讀者、傾向先學習所有細節再進入實作,你可能會想跳過第二章直接前往第三章。當你想要應用學到的細節時,再回到第二章練習。 第五章討論結構體與方法,而第六章涵蓋列舉、match 表達式與 if let 控制流程的語法。你會在 Rust 中用結構體與列舉來自訂型別。 在第七章中,你會學到 Rust 的模組系統與隱私規則,讓你可以組織程式碼以及其公開應用程式介面(Application Programming Interface, API)。第八章會討論標準函式庫提供的一些常見集合資料結構,像是向量、字串與雜湊映射。第九章會探討 Rust 的錯誤處理哲學與技巧。 第十章將深入探討泛型、特徵(traits)與生命週期,讓你能定義出能套用多種型別的程式碼。第十一章都在討論測試,就算有 Rust 的安全性保障,還是必須透過測試來確保你的程式邏輯正確。在第十二章中,我們會動手實作 grep 命令列工具的部分功能,可以搜尋檔案中的文字。我們將會應用前幾章討論過的許多概念。 第十三章會探索閉包與疊代器,這是 Rust 借鑒函式程式設計語言的功能。在第十四章中,我們要更深入研究 Cargo 並討論分享函式庫給其他人的最佳方式。第十五章會討論標準函式庫提供的智慧指標以及能啟用它們功能的特徵(traits)。 在第十六章中,我們會介紹各種不同的並行程式設計模型,並談論 Rust 如何幫助你無懼地開發多執行緒的程式。第十七章會拿 Rust 的慣用風格與你可能較熟悉的物件導向程式設計原則作比較。 第十八章涉及模式與模式配對,它們的強大力量讓你能用 Rust 表達更多概念。第十九章是進階主題的大雜燴,其中包含:不安全(unsafe)的 Rust、巨集、以及更多關於生命週期、特徵、型別、函式與閉包的介紹。 在第二十章中,我們會完整實作一個底層跑多執行緒的網頁伺服器! 最後,以參照的方式收錄本語言的一些實用資訊。附錄 A 涵蓋 Rust 的關鍵字、附錄 B 涵蓋 Rust 的運算子與符號、附錄 C 涵蓋標準函式庫提供的可推導的特徵(derivable traits)、附錄 D 涵蓋一些實用開發工具,然後附錄 E 會解釋 Rust 的版號。在附錄 F 中你可以找到本書籍的各種翻譯版本,然後在附錄 G 我們會講解 Rust 的開發流程以及什麼是每夜版(Nightly)Rust。 本書沒有錯誤的閱讀方式--如果你想要跳過一些章節,儘管跳過吧!後面也許會遇到不懂的地方而需要回頭去看。總之用最適合自己的方式閱讀。 學習 Rust 的過程中有個重要的部分--學習如何閱讀編譯器顯示的錯誤訊息,讓訊息引導你寫出正確的程式碼。因此,我們特意提供很多無法編譯的範例,以及編譯器對應顯示的錯誤訊息。如果你隨意挑一則範例執行的話,它可能無法編譯喔!請務必看看範例旁的文字來瞭解該範例是不是故意出錯。可愛的吉祥物 Ferris 也能幫助你分辨哪些程式碼本來就無法運作: Ferris 意思 此程式碼無法編譯! 此程式碼會恐慌! 此程式碼不會產生預期的行為。 在大多數的情況下,我們會引導你將無法編譯的程式碼寫成正確的版本。","breadcrumbs":"介紹 » 如何閱讀本書","id":"10","title":"如何閱讀本書"},"100":{"body":"我們可以像這樣建立兩個不同變體的 IpAddrKind 實例: # enum IpAddrKind {\n# V4,\n# V6,\n# }\n# # fn main() { let four = IpAddrKind::V4; let six = IpAddrKind::V6;\n# # route(IpAddrKind::V4);\n# route(IpAddrKind::V6);\n# }\n# # fn route(ip_kind: IpAddrKind) {} 注意變體會位於列舉命名空間底下,所以我們用兩個冒號來標示。這樣的好處在於 IpAddrKind::V4 和 IpAddrKind::V6 都是同型別 IpAddrKind。比方說,我們就可以定義一個接收任 IpAddrKind 的函式: # enum IpAddrKind {\n# V4,\n# V6,\n# }\n# # fn main() {\n# let four = IpAddrKind::V4;\n# let six = IpAddrKind::V6;\n# # route(IpAddrKind::V4);\n# route(IpAddrKind::V6);\n# }\n# fn route(ip_kind: IpAddrKind) {} 然後我們可以用任意變體呼叫此函式: # enum IpAddrKind {\n# V4,\n# V6,\n# }\n# # fn main() {\n# let four = IpAddrKind::V4;\n# let six = IpAddrKind::V6;\n# route(IpAddrKind::V4); route(IpAddrKind::V6);\n# }\n# # fn route(ip_kind: IpAddrKind) {} 使用列舉還有更多好處。我們再進一步想一下我們的 IP 位址型別還沒有辦法儲存實際的 IP 位址 資料 ,我們現在只知道它是哪種 類型 。考慮到你已經學會第五章的結構體,你應該會像範例 6-1 這樣嘗試用結構體解決問題。 # fn main() { enum IpAddrKind { V4, V6, } struct IpAddr { kind: IpAddrKind, address: String, } let home = IpAddr { kind: IpAddrKind::V4, address: String::from(\"127.0.0.1\"), }; let loopback = IpAddr { kind: IpAddrKind::V6, address: String::from(\"::1\"), };\n# } 範例 6-1:使用 struct 儲存 IP 位址的資料與 IpAddrKind 的變體 我們在這裡定義了一個有兩個欄位的結構體 IpAddr:欄位 kind 擁有 IpAddrKind(我們上面定義過的列舉)型別,address 欄位則是 String 型別。再來我們有兩個此結構體的實例。第一個 home 擁有 IpAddrKind::V4 作為 kind 的值,然後位址資料是 127.0.0.1。第二個實例 loopback 擁有 IpAddrKind 另一個變體 V6 作為 kind 的值,且有 ::1 作為位址資料。我們用結構體來組織 kind 和 address 的值在一起,讓變體可以與數值相關。 但是我們可以用另一種更簡潔的方式來定義列舉就好,而不必使用結構體加上列舉。列舉內的每個變體其實都能擁有數值。以下這樣新的定義方式讓 IpAddr 的 V4 與 V6 都能擁有與其相關的 String 數值: # fn main() { enum IpAddr { V4(String), V6(String), } let home = IpAddr::V4(String::from(\"127.0.0.1\")); let loopback = IpAddr::V6(String::from(\"::1\"));\n# } 我們將資料直接附加到列舉的每個變體上,這樣就不再用結構體。這裏我們還能看到另一項列舉的細節:我們定義的每一個列舉變體也會變成建構該列舉的函式。也就是說 IpAddr::V4() 是個函式,且接收 String 引數並回傳 IpAddr 的實例。我們在定義列舉時就會自動拿到這樣的建構函式。 改使用列舉而非結構體的話還有另一項好處:每個變體可以擁有不同型別與資料的數量。第四版的 IP 位址永遠只會有四個 0 到 255 的數字部分,如果我們想要讓 V4 儲存四個 u8,但 V6 位址仍保持 String 不變的話,我們在結構體是無法做到的。列舉可以輕鬆勝任: # fn main() { enum IpAddr { V4(u8, u8, u8, u8), V6(String), } let home = IpAddr::V4(127, 0, 0, 1); let loopback = IpAddr::V6(String::from(\"::1\"));\n# } 我們展示了許多種定義儲存第四版與第六版 IP 位址資料結構的方式,不過需要儲存 IP 位址並編碼成不同類型的案例實在太常見了,所以 標準函式庫已經幫我們定義好了! 讓我們看看標準函式庫是怎麼定義 IpAddr 的:它有和我們一模一樣的列舉變體,不過變體各自儲存的資料是另外兩個不同的結構體,兩個定義的內容均不相同: struct Ipv4Addr { // --省略--\n} struct Ipv6Addr { // --省略--\n} enum IpAddr { V4(Ipv4Addr), V6(Ipv6Addr),\n} 此程式碼展示了你可以將任何資料類型放入列舉的變體中:字串、數字型別、結構體都可以。你甚至可以再包含另一個列舉!另外標準函式庫內的型別常常沒有你想得那麼複雜。 請注意雖然標準函式庫已經有定義 IpAddr,但我們還是可以使用並建立我們自己定義的型別,而且不會產生衝突,因為我們還沒有將標準函式庫的定義匯入到我們的作用域中。我們會在第七章討論如何將型別匯入作用域內。 讓我們再看看範例 6-2 的另一個列舉範例,這次的變體有各式各樣的型別。 enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32),\n}\n# # fn main() {} 範例 6-2:Message 列舉的變體各自擁有不同的型別與數值數量 此列舉有四個不同型別的變體: Quit 沒有包含任何資料。 Move 包含了和結構體一樣的名稱欄位。 Write 包含了一個 String。 ChangeColor 包含了三個 i32。 如同範例 6-2 這樣定義列舉變體和定義不同類型的結構體很像,只不過列舉不使用 struct 關鍵字,而且所有的變體都會在 Message 型別底下。以下的結構體可以包含與上方列舉變體定義過的資料: struct QuitMessage; // 類單元結構體\nstruct MoveMessage { x: i32, y: i32,\n}\nstruct WriteMessage(String); // 元組結構體\nstruct ChangeColorMessage(i32, i32, i32); // 元組結構體\n# # fn main() {} 但是如果我們使用不同結構體且各自都有自己的型別的話,我們就無法像範例 6-2 那樣將 Message 視為單一型別,輕鬆在定義函式時接收訊息所有可能的類型。 列舉和結構體還有一個地方很像:如同我們可以對結構體使用 impl 定義方法,我們也可以對列舉定義方法。以下範例顯示我們可以對 Message 列舉定義一個 call 方法: # fn main() {\n# enum Message {\n# Quit,\n# Move { x: i32, y: i32 },\n# Write(String),\n# ChangeColor(i32, i32, i32),\n# }\n# impl Message { fn call(&self) { // 在此定義方法本體 } } let m = Message::Write(String::from(\"hello\")); m.call();\n# } 方法本體使用 self 來取得我們呼叫方法的值。在此例中,我們建立了一個變數 m 並取得 Message::Write(String::from(\"hello\")),而這就會是當我們執行 m.call() 時 call 方法內會用到的 self。 讓我們再看看另一個標準函式庫內非常常見且實用的列舉:Option。","breadcrumbs":"列舉與模式配對 » 定義列舉 » 列舉數值","id":"100","title":"列舉數值"},"101":{"body":"在此段落我們將來研究 Option,這是在標準函式庫中定義的另一種列舉。Option 廣泛運用在許多場合,它能表示一個數值可能有某個東西,或者什麼都沒有。 舉例來說,如果你向一串包含元素的列表索取第一個值,你會拿到數值,但如果你向空列表索取的話,你就什麼都拿不到。在型別系統中表達這樣的概念可以讓編譯器檢查我們是否都處理完我們該處理的情況了。這樣的功能可以防止其他程式語言中極度常見的程式錯誤。 程式語言設計通常要考慮哪些功能是你要的,但同時哪些功能是你不要的也很重要。Rust 沒有像其他許多語言都有空值。 空值 (Null)代表的是沒有任何數值。在有空值的語言,所有變數都有兩種可能:空值或非空值。 而其發明者 Tony Hoare 在他 2009 的演講「空參考:造成數十億損失的錯誤」(“Null References: The Billion Dollar Mistake”)中提到: 我稱它為我十億美元級的錯誤。當時我正在為一門物件導向語言設計第一個全方位的參考型別系統。我當時的目標是透過編譯器自動檢查來確保所有的參考都是安全的。但我無法抗拒去加入空參考的誘惑,因為實作的方式實在太簡單了。這導致了無數的錯誤、漏洞與系統崩潰,在接下來的四十年中造成了大概數十億美金的痛苦與傷害。 空值的問題在於,如果你想在非空值使用空值的話,你會得到某種錯誤。由於空值與非空值的特性無所不在,你會很容易犯下這類型的錯誤。 但有時候能夠表達「空(null)」的概念還是很有用的:空值代表目前的數值因為某些原因而無效或缺少。 所以問題不在於概念本身,而在於如何實作。所以 Rust 並沒有空值,但是它有一個列舉可以表達出這樣的概念,也就是一個值可能是存在或不存在的。此列舉就是 Option,它是在 標準函式庫中這樣定義的 : enum Option { None, Some(T),\n} Option 實在太實用了,所以它早已加進 prelude 中,你不需要特地匯入作用域中。它的變體一樣也被加進 prelude 中,你可以直接使用 Some 和 None 而不必加上 Option:: 的前綴。Option 仍然就只是個列舉, Some(T) 與 None 仍然是Option 型別的變體。 語法是我們還沒介紹到的 Rust 功能。它是個泛型型別參數,我們會在第十章正式介紹泛型(generics)。現在你只需要知道 指的是 Option 列舉中的 Some 變體可以是任意型別。而透過 Option 數值來持有數字型別和字串型別的話,它們最終會換掉 Option 中的 T,成為不同的型別。以下是使用 Option 來包含數字與字串型別的範例: # fn main() { let some_number = Some(5); let some_char = Some('e'); let absent_number: Option = None;\n# } some_number 的型別是 Option,而 some_char 的型別是 Option,兩者是不同的型別。Rust 可以推導出這些型別,因為我們已經在 Some 變體指定數值。至於 absent_number 的話,Rust 需要我們寫出完整的 Option 型別,因為編譯器無法從 None 推導出相對應的 Some 變體會持有哪種型別。我們在這裡告訴 Rust 我們 absent_number 所指的型別為 Option。 當我們有 Some 值時,我們會知道數值是存在的而且就位於 Some 內。當我們有 None 值時,在某種意義上它代表該值是空的,我們沒有有效的數值。所以為何 Option 會比用空值來得好呢? 簡單來說因為 Option 與 T(T 可以是任意型別)是不同的型別,編譯器不會允許我們像一般有效的值那樣來使用 Option。舉例來說,以下範例是無法編譯的,因為這是將 i8 與 Option 相加: # fn main() { let x: i8 = 5; let y: Option = Some(5); let sum = x + y;\n# } 如果我們執行此程式,我們會得到以下錯誤訊息: $ cargo run Compiling enums v0.1.0 (file:///projects/enums)\nerror[E0277]: cannot add `Option` to `i8` --> src/main.rs:5:17 |\n5 | let sum = x + y; | ^ no implementation for `i8 + Option` | = help: the trait `std::ops::Add>` is not implemented for `i8` = help: the following other types implement trait `Add`: <&'a f32 as Add> <&'a f64 as Add> <&'a i128 as Add> <&'a i16 as Add> <&'a i32 as Add> <&'a i64 as Add> <&'a i8 as Add> <&'a isize as Add> and 48 others For more information about this error, try `rustc --explain E0277`.\nerror: could not compile `enums` due to previous error 這樣其實很好!此錯誤訊息事實上指的是 Rust 不知道如何將 i8 與 Option 相加,因為它們是不同的型別。當我們在 Rust 中有個型別像是 i8,編譯器將會確保我們永遠會擁有有效數值。我們可以很放心地使用該值,而不必檢查是不是空的。我們只有在使用 Option (或者任何其他要使用的型別)時才需要去擔心會不會沒有值。然後編譯器會確保我們在使用該值前,有處理過該有的條件。 換句話說,你必須將 Option 轉換為 T 你才能對 T 做運算。這通常就能幫助我們抓到空值最常見的問題:認為某值不為空,但它其實就是空值。 消除掉非空值是否正確的風險,可以讓你對你寫的程式碼更有信心。要讓一個值變成可能為空的話,你必須顯式建立成對應型別的 Option。然後當你要使用該值時,你就得顯式處理數值是否為空的條件。只要一個數值的型別不是 Option,你就 可以 安全地認定該值不為空。這是 Rust 刻意考慮的設計決定,限制無所不在的空值,並增強 Rust 程式碼的安全性。 所以當我們有一個數值型別 Option,我們要怎麼從 Some 變體取出 T,好讓我們可以使用該值呢?Option 列舉有大量實用的方法可以在不同的場合下使用。你可以在 它的技術文件 查閱。更加熟悉 Option 的方法十分益於你接下來的 Rust 旅程。 整體來說,要使用 Option 數值的話,你要讓程式碼可以處理每個變體。你會希望有一些程式碼只會在當我們有 Some(T) 時執行,然後這些程式碼允許使用內部的 T。你會希望有另一部分的程式碼能在只有 None 時執行,且這些程式碼不會拿到有效的 T 數值。match 表達式正是處理此列舉行為的控制流結構:它會針對不同的列舉變體執行不同的程式碼,而且程式碼可以使用配對到的數值資料。","breadcrumbs":"列舉與模式配對 » 定義列舉 » Option 列舉相對於空值的優勢","id":"101","title":"Option 列舉相對於空值的優勢"},"102":{"body":"Rust 有個功能非常強大的控制流建構子叫做 match,你可以使用一系列模式來配對數值並依據配對到的模式來執行對應的程式。模式(Patterns)可以是字面數值、變數名稱、萬用字元(wildcards)和其他更多元件來組成。 第十八章 會涵蓋所有不同類型的模式,以及它們的用途。match 強大的地方在於模式表達的清楚程度以及編譯器會確保所有可能的情況都處理了。 你可以想像 match 表達式成一個硬幣分類機器:硬幣會滑到不同大小的軌道,然後每個硬幣會滑入第一個符合大小的軌道。同樣地,數值會依序遍歷 match 的每個模式,然後進入第一個「配對」到該數值的模式所在的程式碼區塊,並在執行過程中使用。 既然我們都提到硬幣了,就讓我們用它們來作為 match 的範例吧!我們可以寫一個接收未知美國硬幣的函式,以類似驗鈔機的方式,決定它是何種硬幣並以美分作為單位回傳其值。如範例 6-3 所示。 enum Coin { Penny, Nickel, Dime, Quarter,\n} fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, }\n}\n# # fn main() {} 範例 6-3:列舉以及用列舉變體作為模式的 match 表達式 讓我們一一介紹 value_in_cents 函式中 match 的每個部分。首先我們使用 match 並加上一個表達式,在此例的話就是指 coin。這和 if 中條件表達式的用法很像。不過差別在於 if 中的條件必須是布林值,而在此它可以回傳任何型別。在此範例中 coin 的型別是我們在第一行定義的列舉 Coin。 接下來是 match 的分支,每個分支有兩個部分:一個模式以及對應的程式碼。這邊第一個分支的模式是 Coin::Penny 然後 => 會將模式與要執行的程式碼分開來,而在此例的程式碼就只是個 1。每個分支之間由逗號區隔開來。 當 match 表達式執行時,他會將計算的數據結果依序與每個分支的模式做比較。如果有模式配對到該值的話,其對應的程式碼就會執行。如果該模式與數值不符的話,就繼續執行下一個分支,就像硬幣分類機器。 每個分支對應的程式碼都是表達式,然後在配對到的分支中表達式的數值結果就會是整個 match 表達式的回傳值。 如果配對分支的程式碼很短的話,我們通常就不會用到大括號,像是範例 6-3 每個分支就只回傳一個數值。如果你想要在配對分支執行多行程式碼的話,你就必須用大括號,然後你可以在括號後選擇性加上逗號。舉例來說,以下程式會在每次配對到 Coin::Penny 時印出「幸運幣!」再回傳程式碼區塊最後的數值 1: # enum Coin {\n# Penny,\n# Nickel,\n# Dime,\n# Quarter,\n# }\n# fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => { println!(\"幸運幣!\"); 1 } Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, }\n}\n# # fn main() {}","breadcrumbs":"列舉與模式配對 » match 控制流建構子 » match 控制流建構子","id":"102","title":"match 控制流建構子"},"103":{"body":"另一項配對分支的實用功能是它們可以綁定配對模式中部分的數值,這讓我們可以取出列舉變體中的數值。 舉例來說,讓我們改變我們其中一個列舉變體成擁有資料。從 1999 年到 2008 年,美國在鑄造 25 美分硬幣時,其中一側會有 50 個州不同的設計。不過其他的硬幣就沒有這樣的設計,只有 25 美分會有特殊值而已。我們可以改變我們的 enum 中的 Quarter 變體成儲存 UsState 數值,如範例 6-4 所示。 #[derive(Debug)] // 這讓我們可以顯示每個州\nenum UsState { Alabama, Alaska, // --省略--\n} enum Coin { Penny, Nickel, Dime, Quarter(UsState),\n}\n# # fn main() {} 範例 6-4:修改 Coin 列舉的 Quarter 變體來包含一個 UsState 數值 讓我們想像有一個朋友想要收集所有 50 州的 25 美分硬幣。當我們在排序零錢的同時,我們會在拿到 25 美分時喊出該硬幣對應的州,好讓我們的朋友知道,如果他沒有的話就可以納入收藏。 在此程式中的配對表達式中,我們在 Coin::Quarter 變體的配對模式中新增了一個變數 state。當 Coin::Quarter 配對符合時,變數 state 會綁定該 25 美分的數值,然後我們就可以在分支程式碼中使用 state,如以下所示: # #[derive(Debug)]\n# enum UsState {\n# Alabama,\n# Alaska,\n# // --省略--\n# }\n# # enum Coin {\n# Penny,\n# Nickel,\n# Dime,\n# Quarter(UsState),\n# }\n# fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter(state) => { println!(\"此 25 美分所屬的州為 {:?}!\", state); 25 } }\n}\n# # fn main() {\n# value_in_cents(Coin::Quarter(UsState::Alaska));\n# } 如果我們呼叫 value_in_cents(Coin::Quarter(UsState::Alaska)) 的話,coin 就會是 Coin::Quarter(UsState::Alaska)。當我們比較每個配對分支時,我們會到 Coin::Quarter(state) 的分支才配對成功。此時 state 綁定的數值就會是 UsState::Alaska。我們就可以在 println! 表達式中使用該綁定的值,以此取得 Coin 列舉中 Quarter 變體內的值。","breadcrumbs":"列舉與模式配對 » match 控制流建構子 » 綁定數值的模式","id":"103","title":"綁定數值的模式"},"104":{"body":"在上一個段落,我們想要在使用 Option 時取得 Some 內部的 T 值。如同列舉 Coin,我們一樣可以使用 match 來處理 Option!相對於比較硬幣,我們要比較的是 Option 的變體,不過 match 表達式運作的方式一模一樣。 假設我們要寫個接受 Option 的函式,而且如果內部有值的話就將其加上 1。如果內部沒有數值的話,該函式就回傳 None 且不再嘗試做任何動作。 拜 match 所賜,這樣的函式很容易寫出來,長得就像範例 6-5。 # fn main() { fn plus_one(x: Option) -> Option { match x { None => None, Some(i) => Some(i + 1), } } let five = Some(5); let six = plus_one(five); let none = plus_one(None);\n# } 範例 6-5:對 Option 使用 match 表達式的函式 讓我們來仔細分析 plus_one 第一次的執行結果。當我們呼叫 plus_one(five) 時,plus_one 本體中的變數 x 會擁有 Some(5)。我們接著就拿去和每個配對分支比較: # fn main() {\n# fn plus_one(x: Option) -> Option {\n# match x { None => None,\n# Some(i) => Some(i + 1),\n# }\n# }\n# # let five = Some(5);\n# let six = plus_one(five);\n# let none = plus_one(None);\n# } Some(5) 並不符合 None 這樣的模式,所以我們繼續進行下一個分支: # fn main() {\n# fn plus_one(x: Option) -> Option {\n# match x {\n# None => None, Some(i) => Some(i + 1),\n# }\n# }\n# # let five = Some(5);\n# let six = plus_one(five);\n# let none = plus_one(None);\n# } Some(5) 有符合 Some(i) 這樣的模式嗎?這是當然的囉!我們有相同的變體。i 會綁定 Some 中的值,所以 i 會取得 5。接下來配對分支中的程式碼就會執行,我們將 1 加入 i 並產生新的 Some 其內部的值就會是 6。 現在讓我們看看範例 6-5 第二次的 plus_one 呼叫,這次的 x 是 None。我們進入 match 然後比較第一個分支: # fn main() {\n# fn plus_one(x: Option) -> Option {\n# match x { None => None,\n# Some(i) => Some(i + 1),\n# }\n# }\n# # let five = Some(5);\n# let six = plus_one(five);\n# let none = plus_one(None);\n# } 配對成功!因為沒有任何數值可以相加,程式就停止並在 => 之後馬上回傳 None。因為第一個分支就配對成功了,沒有其他的分支需要再做比較。 用 match 與列舉組合起來在很多地方都很實用。你將會在許多 Rust 程式碼看到這樣的模式,使用 match 配對列舉,綁定內部的資料,然後執行對應的程式碼。一開始使用的確會有點陌生,但當你熟悉以後,你會希望所有語言都能提供這樣的功能。這一直是使用者最愛的功能之一。","breadcrumbs":"列舉與模式配對 » match 控制流建構子 » 配對 Option","id":"104","title":"配對 Option"},"105":{"body":"我們還有一個 match 的細節要討論:分支的模式必須涵蓋所有可能性。今天要是我們像這樣寫了一個有錯誤的 plus_one 函式版本,它會無法編譯: # fn main() { fn plus_one(x: Option) -> Option { match x { Some(i) => Some(i + 1), } }\n# # let five = Some(5);\n# let six = plus_one(five);\n# let none = plus_one(None);\n# } 我們沒有處理到 None 的情形,所以此程式碼會產生錯誤。幸運的是這是 Rust 能夠抓到的錯誤。如果我們嘗試編譯此程式的話,我們會得到以下錯誤: $ cargo run Compiling enums v0.1.0 (file:///projects/enums)\nerror[E0004]: non-exhaustive patterns: `None` not covered --> src/main.rs:3:15 |\n3 | match x { | ^ pattern `None` not covered |\nnote: `Option` defined here = note: the matched value is of type `Option`\nhelp: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown |\n4 ~ Some(i) => Some(i + 1),\n5 ~ None => todo!(), | For more information about this error, try `rustc --explain E0004`.\nerror: could not compile `enums` due to previous error Rust 發現我們沒有考慮到所有可能條件,而且還知道我們少了哪些模式!Rust 中的配對必須是 徹底 ( exhaustive )的:我們必須列舉出所有可能的情形,程式碼才能夠被視為有效。尤其是在 Option 的情況下,當 Rust 防止我們忘記處理 None 的情形時,它也使我們免於以為擁有一個有效實際上卻是空的值。因此要造成之前提過的十億美元級錯誤在這邊基本上是不可能的。","breadcrumbs":"列舉與模式配對 » match 控制流建構子 » 配對必須是徹底的","id":"105","title":"配對必須是徹底的"},"106":{"body":"使用列舉的話,我們可以針對特定數值作特別的動作,而對其他所有數值採取預設動作。想像一下我們正在做款骰子遊戲,如果你骰出 3 的話,你的角色就動不了,但是可以拿頂酷炫的帽子。如果你骰出 7 你的角色就損失那頂帽子。至於其他的數值,你的角色就按照那個數值在遊戲桌上移動步數。以下是用 match 實作出的邏輯,骰子的結果並非隨機數而是寫死的,且所有邏輯對應的函式本體都是空的,因為實際去實作並非本範例的重點: # fn main() { let dice_roll = 9; match dice_roll { 3 => add_fancy_hat(), 7 => remove_fancy_hat(), other => move_player(other), } fn add_fancy_hat() {} fn remove_fancy_hat() {} fn move_player(num_spaces: u8) {}\n# } 在前兩個分支中的模式分別為數值 3 和 7。至於最後一個涵蓋其他可能數值的分支,我們用變數 other 作為模式。在 other 分支執行的程式碼會將該變數傳入函式 move_player 中。 此程式碼就算我們沒有列完所有 u8 可能的數字也能編譯完成,因為最後的模式會配對所有尚未被列出來的數值。這樣的 catch-all 模式能滿足 match 必須要徹底的要求。注意到我們需要將 catch-all 分支放在最後面,因為模式是按照順序配對的。如果我們將 catch-all 放在其他分支前的話,這樣一來其他後面的分支就永遠配對不到了,所以要是我們在 catch-all 之後仍加上分支的話 Rust 會警告我們! 當我們想使用 catch-all 模式但不想 使用 其數值時,Rust 還有一種模式能讓我們使用:_ 這是個特殊模式,用來配對任意數值且不綁定該數值。這告訴 Rust 我們不會用到該數值,所以 Rust 不會警告我們沒使用到變數。 讓我們來改變一下遊戲規則:如果你骰到除了 3 與 7 以外的話,你必須要重新擲骰。我們不需要用到 catch-all 的數值,所以我們可以修改我們的程式碼來使用 _,而不必繼續用變數 other: # fn main() { let dice_roll = 9; match dice_roll { 3 => add_fancy_hat(), 7 => remove_fancy_hat(), _ => reroll(), } fn add_fancy_hat() {} fn remove_fancy_hat() {} fn reroll() {}\n# } 此範例一樣也滿足徹底的要求,因為我們在最後的分支顯式地忽略其他所有數值,我們沒有遺漏任何值。 我們再改最後一次遊戲規則,改成如果你骰到除了 3 與 7 以外,不會有任何事發生的話,我們可以用單元數值(我們在 元組型別 段落提到的空元組)作為 _ 的程式碼: # fn main() { let dice_roll = 9; match dice_roll { 3 => add_fancy_hat(), 7 => remove_fancy_hat(), _ => (), } fn add_fancy_hat() {} fn remove_fancy_hat() {}\n# } 這裡我們顯式地告訴 Rust 我們不會使用任何其他沒被先前分支配對到的數值,而且我們也不想在此執行任何程式碼。 我們會在 第十八章 更進一步探討模式與配對,現在我們要先去看看 if let 語法。當 match 表達式變得太囉嗦時,這語法就會變得很有用。","breadcrumbs":"列舉與模式配對 » match 控制流建構子 » Catch-all 模式與 _ 佔位符","id":"106","title":"Catch-all 模式與 _ 佔位符"},"107":{"body":"if let 語法讓你可以用 if 與 let 的組合來以比較不冗長的方式,來處理只在乎其中一種模式而忽略其餘的數值。現在考慮一支程式如範例 6-6 所示,我們在配對 config_max 中 Option 的值,但只想在數值為 Some 變體時執行程式。 # fn main() { let config_max = Some(3u8); match config_max { Some(max) => println!(\"最大值被設為 {}\", max), _ => (), }\n# } 範例 6-6:match 只在數值為 Some 時執行程式 如果數值為 Some,我們就在分支中綁定 max 變數,印出 Some 變體內的數值。我們不想對 None 作任何事情。為了滿足 match 表達式,我們必須在只處理一種變體的分支後面,再加上 _ => ()。這樣就加了不少樣板程式碼。 不過我們可以使用 if let 以更精簡的方式寫出來,以下程式碼的行為就與範例 6-6 的 match 一樣: # fn main() { let config_max = Some(3u8); if let Some(max) = config_max { println!(\"最大值被設為 {}\", max); }\n# } if let 接收一個模式與一個表達式,然後用等號區隔開來。它與 match 的運作方式相同,表達式的意義與 match 相同,然後前面的模式就是第一個分支。 在此例中的模式就是 Some(max),然後 max 會綁定 Some 內的數值。我們就和 match 分支中使用 max 一樣,在 if let 區塊的本體中使用 max。如果數值沒有配對到模式,if let 中的程式碼就不會執行。 使用 if let 可以少打些字、減少縮排以及不用寫多餘的樣板程式碼。不過你就少了 match 強制的徹底窮舉檢查。要何時選擇 match 還是 if let 得依據你在的場合是要做什麼事情,以及在精簡度與徹底檢查之間做取捨。 換句話說,你可以想像 if let 是 match 的語法糖(syntax sugar),它只會配對一種模式來執行程式碼並忽略其他數值。 我們也可以在 if let 之後加上 else,else 之後的程式碼區塊等同於 match 表達式中 _ 情形的程式碼區塊。這樣一來的 if let 和 else 組合就等同於 match 了。回想一下範例 6-4 的 Coin 列舉定義, Quarter 變體擁有數值 UsState。如果我們希望統計所有不是 25 美分的硬幣的同時,也能繼續回報 25 美分所屬的州的話,我們可以用 match 像這樣寫: # #[derive(Debug)]\n# enum UsState {\n# Alabama,\n# Alaska,\n# // --省略--\n# }\n# # enum Coin {\n# Penny,\n# Nickel,\n# Dime,\n# Quarter(UsState),\n# }\n# # fn main() {\n# let coin = Coin::Penny; let mut count = 0; match coin { Coin::Quarter(state) => println!(\"此 25 美分所屬的州為 {:?}!\", state), _ => count += 1, }\n# } 或是我們也可以用 if let 和 else 表達式這樣寫: # #[derive(Debug)]\n# enum UsState {\n# Alabama,\n# Alaska,\n# // --省略--\n# }\n# # enum Coin {\n# Penny,\n# Nickel,\n# Dime,\n# Quarter(UsState),\n# }\n# # fn main() {\n# let coin = Coin::Penny; let mut count = 0; if let Coin::Quarter(state) = coin { println!(\"此 25 美分所屬的州為 {:?}!\", state); } else { count += 1; }\n# } 如果你的程式碼邏輯遇到使用 match 表達會太囉唆的話,記得 if let 也在你的 Rust 工具箱中供你使用。","breadcrumbs":"列舉與模式配對 » 透過 if let 簡化控制流 » 透過 if let 簡化控制流","id":"107","title":"透過 if let 簡化控制流"},"108":{"body":"我們現在涵蓋了如何使用列舉來建立一系列列舉數值的自訂型別。我們展示了標準函式庫的 Option 型別如何用型別系統來預防錯誤。當列舉數值其內有資料時,你可以依照你想處理的情況數量,使用 match 或 if let 來取出並使用那些數值。 你的 Rust 程式碼現在能夠使用結構體與列舉來表達你所相關研究領域的概念了。在你的 API 建立自訂型別可以確保型別安全,編譯器會保證你的函式只會取得該函式預期的型別數值。 接下來為了提供組織完善且直觀的的 API 供你的使用者使用,並只表達出使用者確切所需要的內容,我們需要瞭解 Rust 的模組。","breadcrumbs":"列舉與模式配對 » 透過 if let 簡化控制流 » 總結","id":"108","title":"總結"},"109":{"body":"當你寫的程式規模更大時,組織你的程式碼就很重要。因為用你的腦袋要記住整個程式碼是幾乎不可能的。要是能組織相關功能的程式碼並將它們分成明確功能的話,你就能清楚地找到實作特定功能的程式碼,以及該在哪裏修改該功能的行為。 我們之前寫過的程式都只在一個檔案內的一個模組(module)中。隨著專案成長,我們應該要組織程式碼,拆成數個模組與數個檔案。一個套件(package)可以包含數個執行檔 crate 以及選擇性提供一個函式庫 crate。隨著套件增長,你可以取出不同的部分作為獨立的 crate,成為對外的依賴函式庫。此章節將會介紹這些所有概念。對於非常龐大的專案,需要一系列的關聯套件組合在一起的話,Cargo 有提供 工作空間 (workspaces),我們會在第十四章的 「Cargo 工作空間」 做介紹。 我們還會討論對實作細節進行封裝,讓你的程式碼在頂層更好使用。一旦你實作了某項功能,其他程式就可以用程式碼的公開介面呼叫該程式碼,而不必去知道它實作如何運作。你在寫程式碼時會去定義哪些部分是給其他程式碼公開使用的,以及哪些部分是私底下你可以任意修改的實作細節。這能再減少你的腦袋需要煩惱的細節數量。 還有一個概念需要再提一次,也就是作用域(scope):程式碼需要能被定義在「作用域內」並要能夠指明此作用域。當讀取寫入或編譯程式碼時,程式設計師與編譯器需要知道特定地點的名稱,才能知道其內的變數、函式、結構體、列舉、常數或其他任何有意義的項目。你可以建立作用域,並改變其在作用域內與作用域外的名稱。你無法在同個作用域內擁有兩個相同名稱的項目。我們可以使用一些工具來解決名稱衝突的問題。 Rust 有一系列的功能能讓你管理你的程式碼組織,包含哪些細節能對外提供、哪些細節是私有的,以及程式中每個作用域的名稱為何。這些功能有時會統一稱作 模組系統(module system) ,其中包含: 套件(Package): 讓你建構、測試並分享 crate 的 Cargo 功能 Crates: 產生函式庫或執行檔的模組集合 模組(Modules)與 use: 讓你控制組織、作用域與路徑的隱私權 路徑(Paths): 對一個項目的命名方式,像是一個結構體、函式或模組 在本章節中,我們會涵蓋所有這些功能,討論它們如何互動,並解釋如何使用它們來管理作用域。在讀完後,你應該就會對模組系統有紮實的認知,並能夠對作用域的使用駕輕就熟!","breadcrumbs":"透過套件、Crate 與模組管理成長中的專案 » 透過套件、Crate 與模組管理成長中的專案","id":"109","title":"透過套件、Crate 與模組管理成長中的專案"},"11":{"body":"產生本書的原始檔案可以在 GitHub 上找到。","breadcrumbs":"介紹 » 原始碼","id":"11","title":"原始碼"},"110":{"body":"首先我們要介紹的第一個模組系統部分為套件與 crates。 一個 crate 是 Rust 編譯器同個時間內視為程式碼的最小單位。就算你執行的是 rustc 而非 cargo,然後傳入單一源碼檔案(就像我們在第一章的「編寫並執行 Rust 程式」那樣),編譯器會將該檔案視為一個 crate。Crate 能包含模組,而模組可以在其他檔案中定義然後同時與 crate 一起編譯,我們會在接下來的段落看到。 一個 crate 可以有兩種形式:執行檔 crate 或函式庫 crate。 執行檔(Binary)crate 是種你能編譯成執行檔並執行的程式,像是命令列程式或伺服器。這種 crate 需要有一個函式 main 來定義執行檔執行時該做什麼事。目前我們建立的所有 crate 都是執行檔 crate。 函式庫(Library)crate 則不會有 main 函式,而且它們也不會編譯成執行檔。這種 crate 定義的功能用來分享給多重專案使用。舉例來說,我們在 第二章 用到的 rand crate 就提供了產生隨機數值的功能。當大多數的 Rustacean 講到「crate」時,他們其實指的是函式庫 crate,所以他們講到「crate」時相當於就是在講其他程式語言概念中的「函式庫」。 crate 的源頭 會是一個原始檔案,讓 Rust 的編譯器可以作為起始點並組織 crate 模組的地方(我們會在 「定義模組來控制作用域與隱私權」 的段落詳加解釋模組)。 套件 (package)則是提供一系列功能的一或數個 crate。一個套件會包含一個 Cargo.toml 檔案來解釋如何建構那些 crate。Cargo 本身其實就是個套件,包含了你已經用來建構程式碼的命令列工具。Cargo 套件還包含執行檔 crate 需要依賴的函式庫 crate。其他專案可以依賴 Cargo 函式庫來使用與 Cargo 命令列工具用到的相同邏輯功能。 一個套件能依照你的喜好擁有數個執行檔 crate,但最多只能有一個函式庫 crate。而一個套件至少要有一個 crate,無論是函式庫或執行檔 crate。 讓我們看看當我們建立一個套件時發生了什麼事。首先我們先輸入 cargo new 命令: $ cargo new my-project Created binary (application) `my-project` package\n$ ls my-project\nCargo.toml\nsrc\n$ ls my-project/src\nmain.rs 在我們執行 cargo new 之後,我們使用 ls 來查看 Cargo 建立了什麼。在專案的目錄中會有個 Cargo.toml 檔案,這是套件的設定檔。然後還會有個 src 目錄底下包含了 main.rs 。透過你的文字編輯器打開 Cargo.toml ,你會發現沒有提到 src/main.rs 。Cargo 遵循的常規是 src/main.rs 就是與套件同名的執行檔 crate 的 crate 源頭。同樣地,Cargo 也會知道如果套件目錄包含 src/lib.rs 的話,則該套件就會包含與套件同名的函式庫 crate。Cargo 會將 crate 源頭檔案傳遞給 rustc 來建構函式庫或執行檔。 我們在此的套件只有包含 src/main.rs 代表它只有一個同名的執行檔 crate 叫做 my-project。如果套件包含 src/main.rs 與 src/lib.rs 的話,它就有兩個 crate:一個執行檔與一個函式庫,兩者都與套件同名。一個套件可以有多個執行檔 crate,只要將檔案放在 src/bin 目錄底下就好,每個檔案會被視為獨立的執行檔 crate。","breadcrumbs":"透過套件、Crate 與模組管理成長中的專案 » 套件與 Crates » 套件與 Crates","id":"110","title":"套件與 Crates"},"111":{"body":"在此段落,我們將討論模組以及其他模組系統的部分,像是 路徑 (paths)允許你來命名項目,而 use 關鍵字可以將路徑引入作用域,再來 pub 關鍵字可以讓指定的項目對外公開。我們還會討論到 as 關鍵字、外部套件以及全域(glob)運算子。 首先,讓我們先介紹一些規則好讓你在之後組織程式碼時能更容易理解初步概念。然後我們會再詳細解釋每個規則。","breadcrumbs":"透過套件、Crate 與模組管理成長中的專案 » 定義模組來控制作用域與隱私權 » 定義模組來控制作用域與隱私權","id":"111","title":"定義模組來控制作用域與隱私權"},"112":{"body":"這裡我們先快速帶過模組、路徑、use 關鍵字以及 pub 關鍵字在編譯器中是怎麼運作的,以及多數開發者會怎麼組織他們的程式碼。我們會在此章節透過範例逐一介紹,不過這裡能讓你快速理解模組是怎麼運作的。 從 crate 源頭開始 :在編譯 crate 時,編譯器會先尋找 crate 源頭檔案(函式庫 crate 的話,通常就是 src/lib.rs ;執行檔 crate 的話,通常就是 src/main.rs )來編譯程式碼。 宣告模組 :在 crate 源頭檔案中,你可以宣告新的模組,比如說你宣告了一個「garden」模組 mod garden;。編譯器會在以下這幾處尋找模組的程式碼: 同檔案內用 mod garden 加上大括號,寫在括號內的程式碼 src/garden.rs 檔案中 src/garden/mod.rs 檔案中 宣告子模組 :除了 crate 源頭以外,其他檔案也可以宣告子模組。舉例來說,你可能會在 src/garden.rs 中宣告個 mod vegetables;。編譯器會與當前模組同名的目錄底下這幾處尋找子模組的程式碼: 同檔案內,直接用 mod vegetables 加上大括號,寫在括號內的程式碼 src/garden/vegetables.rs 檔案中 src/garden/vegetables/mod.rs 檔案中 模組的路徑 :一旦有個模組成為 crate 的一部分,只要隱私權規則允許,你可以在 crate 裡任何地方使用該模組的程式碼。舉例來說,「garden」模組底下的「vegetables」模組的 Asparagus 型別可以用 crate::garden::vegetables::Asparagus 來找到。 私有 vs 公開 :模組內的程式碼從上層模組來看預設是私有的。要公開的話,將它宣告為 pub mod 而非只是 mod。要讓公開模組內的項目也公開的話,在這些項目前面也加上 pub 即可。 use 關鍵字 :在一個作用域內,use 關鍵字可以建立項目的捷徑,來縮短冗長的路徑名稱。在任何能使用 crate::garden::vegetables::Asparagus 的作用域中,你可以透過 use crate::garden::vegetables::Asparagus; 來建立捷徑,接著你只需要寫 Asparagus 就能在作用域內使用該型別了。 這裡我們建立個執行檔 crate 叫做 backyard 來展示這些規則。Crate 的目錄也叫做 backyard,其中包含了這些檔案與目錄: backyard\n├── Cargo.lock\n├── Cargo.toml\n└── src ├── garden │ └── vegetables.rs ├── garden.rs └── main.rs 此例的 crate 源頭檔案就是 src/main.rs ,它包含了: 檔案名稱:src/main.rs use crate::garden::vegetables::Asparagus; pub mod garden; fn main() { let plant = Asparagus {}; println!(\"I'm growing {:?}!\", plant);\n} pub mod garden; 這行告訴編譯器要包含在 src/garden.rs 中的程式碼,也就是: 檔案名稱:src/garden.rs pub mod vegetables; 這裡的 pub mod vegetables; 代表 src/garden/vegetables.rs 的程式碼也包含在內。而這段程式碼就是: #[derive(Debug)]\npub struct Asparagus {} 現在讓我們詳細介紹這些規則並解釋如何運作的吧!","breadcrumbs":"透過套件、Crate 與模組管理成長中的專案 » 定義模組來控制作用域與隱私權 » 模組懶人包","id":"112","title":"模組懶人包"},"113":{"body":"模組 (Modules)能讓我們在 crate 內組織程式碼成數個群組以便使用且增加閱讀性。模組也能控制項目的 隱私權 ,因為模組內的程式碼預設是私有的。私有項目是內部的實作細節,並不打算讓外部能使用。我們能讓模組與其內的項目公開,讓外部程式碼能夠使用並依賴它們。 舉例來說,讓我們建立一個提供餐廳功能的函式庫 crate。我們定義一個函式簽名不過本體會是空的,好讓我們專注在程式組織,而非餐廳程式碼的實作。 在餐飲業中,餐廳有些地方會被稱作 前台(front of house) 而其他部分則是 後台(back of house) 。前台是消費者的所在區域,這裡是安排顧客座位、點餐並結帳、吧台調酒的地方。而後台則是主廚與廚師工作的廚房、洗碗工洗碗以及經理管理行政工作的地方。 要讓 crate 架構長這樣的話,我們可以組織函式進入模組中。要建立一個新的函式庫叫做 restaurant 的話,請執行 cargo new --lib restaurant。然後將範例 7-1 的程式碼放入 src/lib.rs 中,這定義了一些模組與函式簽名。以下是前台的段落: 檔案名稱:src/lib.rs mod front_of_house { mod hosting { fn add_to_waitlist() {} fn seat_at_table() {} } mod serving { fn take_order() {} fn serve_order() {} fn take_payment() {} }\n} 範例 7-1:front_of_house 模組包含了其他擁有函式的模組 我們用 mod 關鍵字加上模組的名稱(在此例為 front_of_house)來定義一個模組,並用大括號涵蓋模組的本體。在模組中,我們可以再包含其他模組,在此例中我們包含了 hosting 和 serving。模組還能包含其他項目,像是結構體、列舉、常數、特徵、以及像是範例 7-1 的函式。 使用模組的話,我們就能將相關的定義組合起來,並用名稱指出會與它們互相關聯。程式設計師在使用此程式碼時只要觀察依據組合起來的模組名稱就好,不必遍歷所有的定義。這樣就能快速找到他們想使用的定義。要對此程式碼增加新功能的開發者也能知道該將程式碼放在哪裡,以維持程式碼的組織。 稍早我們提到說 src/main.rs 和 src/lib.rs 屬於 crate 的源頭。之所以這樣命名的原因是因為這兩個文件的內容都會在 crate 源頭模組架構中組成一個模組叫做 crate,這樣的結構稱之為 模組樹(module tree) 。 範例 7-2 顯示了範例 7-1 的模組樹架構。 crate └── front_of_house ├── hosting │ ├── add_to_waitlist │ └── seat_at_table └── serving ├── take_order ├── serve_order └── take_payment 範例 7-2:範例 7-1 的模組樹 此樹顯示了有些模組是包含在其他模組內的,比方說 hosting 就在 front_of_house 底下。此樹也顯示了有些模組是其他模組的 同輩(siblings) ,代表它們是在同模組底下定義的,hosting 和 serving 都在 front_of_house 底下定義。如果模組 A 被包含在模組 B 中,我們會說模組 A 是模組 B 的 下一代(child) ,而模組 B 是模組 A 的 上一代(parent) 。注意到整個模組樹的根是一個隱性模組叫做 crate。 模組樹可能會讓你想到電腦中檔案系統的目錄樹,這是一個非常恰當的比喻!就像檔案系統中的目錄,你使用模組來組織你的程式碼。而且就像目錄中的檔案,我們需要有方法可以找到我們的模組。","breadcrumbs":"透過套件、Crate 與模組管理成長中的專案 » 定義模組來控制作用域與隱私權 » 組織相關程式碼成模組","id":"113","title":"組織相關程式碼成模組"},"114":{"body":"要展示 Rust 如何從模組樹中找到一個項目,我們要使用和查閱檔案系統時一樣的路徑方法。要呼叫函式的話,我們需要知道它的路徑: 路徑可以有兩種形式: 絕對路徑 (absolute path)是從 crate 的源頭起始的完整路徑。如果是外部 crate 的話,絕對路徑起始於該 crate 的名稱;如果是當前 crate 的話,則是 crate 作為起頭。 相對路徑 (relative path)則是從本身的模組開始,使用 self、super 或是當前模組的標識符(identifiers)。 無論是絕對或相對路徑其後都會接著一或多個標識符,並使用雙冒號(::)區隔開來。 回頭看看範例 7-1,假設我們想呼叫函式 add_to_waitlist。這就像在問函式 add_to_waitlist 的路徑在哪?範例 7-3 移除了一些範例 7-1 的模組與函式來精簡程式碼的呈現方式。 我們會展示兩種從 crate 源頭定義的 eat_at_restaurant 函式內呼叫 add_to_waitlist 的方法。這些路徑是正確的,不過目前還有其他問題會導致此範例無法編譯,我們會在稍後說明。 eat_at_restaurant 函式是我們函式庫 crate 公開 API 的一部分,所以我們會加上 pub 關鍵字。在 「使用 pub 關鍵字公開路徑」 的段落中,我們會提到更多 pub 的細節。 檔案名稱:src/lib.rs mod front_of_house { mod hosting { fn add_to_waitlist() {} }\n} pub fn eat_at_restaurant() { // 絕對路徑 crate::front_of_house::hosting::add_to_waitlist(); // 相對路徑 front_of_house::hosting::add_to_waitlist();\n} 範例 7-3:使用絕對與相對路徑呼叫 add_to_waitlist 函式 我們在 eat_at_restaurant 中第一次呼叫 add_to_waitlist 函式的方式是用絕對路徑。add_to_waitlist 函式和 eat_at_restaurant 都是在同一個 crate 底下,所以我們可以使用 crate 關鍵字來作為絕對路徑的開頭。我們接續加上對應的模組直到抵達 add_to_waitlist。你可以想像一個有相同架構的檔案系統,然後我們指定 /front_of_house/hosting/add_to_waitlist 這樣的路徑來執行 add_to_waitlist 程式。使用 crate 這樣的名稱作為 crate 源頭的開始,就像在你的 shell 使用 / 作為檔案系統的根一樣。 而我們第二次在 eat_at_restaurant 呼叫 add_to_waitlist 的方式是使用相對路徑。路徑的起頭是 front_of_house,因為它和 eat_at_restaurant 都被定義在模組樹的同一層中。這裡相對應的檔案系統路徑就是 front_of_house/hosting/add_to_waitlist。使用一個模組名稱作為開頭通常就是代表相對路徑。 何時該用相對或絕對路徑是你在你的專案中要做的選擇,依照你想將程式碼的定義連帶與使用它們的程式碼一起移動,或是分開移動到不同地方。舉例來說,如果我們同時將 front_of_house 模組和 eat_at_restaurant 函式移入另一個模組叫做 customer_experience 的話,就會需要修改 add_to_waitlist 的絕對路徑,但是相對路徑就可以原封不動。而如果我們只單獨將 eat_at_restaurant 函式移入一個叫做 dining 模組的話,add_to_waitlist 的絕對路徑就不用修改,但相對路徑就需要更新。我們通常會傾向於指定絕對路徑,因為分別移動程式碼定義與項目呼叫的位置通常是比較常見的。 讓我們嘗試編譯範例 7-3 並看看為何不能編譯吧!以下範例 7-4 是我們得到的錯誤資訊。 $ cargo build Compiling restaurant v0.1.0 (file:///projects/restaurant)\nerror[E0603]: module `hosting` is private --> src/lib.rs:9:28 |\n9 | crate::front_of_house::hosting::add_to_waitlist(); | ^^^^^^^ private module |\nnote: the module `hosting` is defined here --> src/lib.rs:2:5 |\n2 | mod hosting { | ^^^^^^^^^^^ error[E0603]: module `hosting` is private --> src/lib.rs:12:21 |\n12 | front_of_house::hosting::add_to_waitlist(); | ^^^^^^^ private module |\nnote: the module `hosting` is defined here --> src/lib.rs:2:5 |\n2 | mod hosting { | ^^^^^^^^^^^ For more information about this error, try `rustc --explain E0603`.\nerror: could not compile `restaurant` due to 2 previous errors 範例 7-4:範例 7-3 嘗試編譯程式碼出現的錯誤 錯誤訊息表示 hosting 模組是私有的。換句話說,我們指定 hosting 模組與 add_to_waitlist 函式的路徑是正確的,但是因為它沒有私有部分的存取權,所以 Rust 不讓我們使用。在 Rust 中所有項目(函式、方法、結構體、列舉、模組與常數)的隱私權都是私有的。如果你想要建立私有的函式或結構體,你可以將它們放入模組內。 上層模組的項目無法使用下層模組的私有項目,但下層模組能使用它們上方所有模組的項目。這麼做的原因是因為下層模組用來實現實作細節,而下層模組應該要能夠看到在自己所定義的地方的其他內容。讓我們繼續用餐廳做比喻的話,我們可以想像隱私權規則就像是餐廳的後台辦公室。對餐廳顧客來說裡面發生什麼事情都是未知的,但是辦公室經理可以知道經營餐廳時的所有事物。 Rust 選擇這樣的模組系統,讓內部實作細節預設都是隱藏起來的。這樣一來,你就能知道內部哪些程式碼需要修改,而不會破壞到外部的程式碼。不過 Rust 有提供 pub 關鍵字能讓項目公開,讓你可以將下層模組內部的一些程式碼公開給上層模組來使用。","breadcrumbs":"透過套件、Crate 與模組管理成長中的專案 » 參考模組項目的路徑 » 參考模組項目的路徑","id":"114","title":"參考模組項目的路徑"},"115":{"body":"讓我們再執行一次範例 7-4 的錯誤,它告訴我們 hosting 模組是私有的。我們希望上層模組中的 eat_at_restaurant 函式可以呼叫下層模組的 add_to_waitlist 函式,所以我們將 hosting 模組加上 pub 關鍵字,如範例 7-5 所示。 檔案名稱:src/lib.rs mod front_of_house { pub mod hosting { fn add_to_waitlist() {} }\n} pub fn eat_at_restaurant() { // 絕對路徑 crate::front_of_house::hosting::add_to_waitlist(); // 相對路徑 front_of_house::hosting::add_to_waitlist();\n} 範例 7-5:宣告 hosting 模組為 pub 好讓 eat_at_restaurant 可以使用 不幸的是範例 7-5 的程式碼仍然回傳了另一個錯誤,如範例 7-6 所示。 $ cargo build Compiling restaurant v0.1.0 (file:///projects/restaurant)\nerror[E0603]: function `add_to_waitlist` is private --> src/lib.rs:9:37 |\n9 | crate::front_of_house::hosting::add_to_waitlist(); | ^^^^^^^^^^^^^^^ private function |\nnote: the function `add_to_waitlist` is defined here --> src/lib.rs:3:9 |\n3 | fn add_to_waitlist() {} | ^^^^^^^^^^^^^^^^^^^^ error[E0603]: function `add_to_waitlist` is private --> src/lib.rs:12:30 |\n12 | front_of_house::hosting::add_to_waitlist(); | ^^^^^^^^^^^^^^^ private function |\nnote: the function `add_to_waitlist` is defined here --> src/lib.rs:3:9 |\n3 | fn add_to_waitlist() {} | ^^^^^^^^^^^^^^^^^^^^ For more information about this error, try `rustc --explain E0603`.\nerror: could not compile `restaurant` due to 2 previous errors 範例 7-6:編譯範例 7-5 時產生的錯誤 到底發生了什麼事?在 mod hosting 之前加上 pub 關鍵字確實公開了模組。有了這項修改後,我們的確可以在取得 front_of_house 後,繼續進入 hosting。但是 hosting 的所有 內容 仍然是私有的。模組中的 pub 關鍵字只會讓該模組公開讓上層模組使用而已,而不是存取它所有的內部程式碼。因為模組相當於一個容器,如果我們只公開模組的話,本身並不能做多少事情。我們需要再進一步選擇公開模組內一些項目才行。 範例 7-6 的錯誤訊息表示 add_to_waitlist 函式是私有的。隱私權規則如同模組一樣適用於結構體、列舉、函式與方法。 讓我們在 add_to_waitlist 的函式定義加上 pub 公開它吧,如範例 7-7 所示。 檔案名稱:src/lib.rs mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} }\n} pub fn eat_at_restaurant() { // 絕對路徑 crate::front_of_house::hosting::add_to_waitlist(); // 相對路徑 front_of_house::hosting::add_to_waitlist();\n} 範例 7-7:將 mod hosting 和 fn add_to_waitlist 都加上 pub 關鍵字,讓我們可以從 eat_at_restaurant 呼叫函式 現在程式碼就能成功編譯了!要理解為何加上 pub 關鍵字讓我們可以在 add_to_waitlist 取得這些路徑,同時遵守隱私權規則,讓我們來看看絕對路徑與相對路徑。 在絕對路徑中,我們始於 crate,這是 crate 模組樹的根。再來 front_of_house 模組被定義在 crate 源頭中,front_of_house 模組不是公開,但因為 eat_at_restaurant 函式被定義在與 front_of_house 同一層模組中(也就是 eat_at_restaurant 與 front_of_house 同輩(siblings)),我們可以從 eat_at_restaurant 參考 front_of_house。接下來是有 pub 標記的 hosting 模組,我們可以取得 hosting 的上層模組,所以我們可以取得 hosting。最後 add_to_waitlist 函式也有 pub 標記而我們可以取得它的上層模組,所以整個程式呼叫就能執行了! 而在相對路徑中,基本邏輯與絕對路徑一樣,不過第一步有點不同。我們不是從 crate 源頭開始,路徑是從 front_of_house 開始。front_of_house 與 eat_at_restaurant 被定義在同一層模組中,所以從 eat_at_restaurant 開始定義的相對路徑是有效的。再來因為 hosting 與 add_to_waitlist 都有 pub 標記,其餘的路徑也都是可以進入的,所以此函式呼叫也是有效的! 如果你計畫分享你的函式庫 crate 來讓其他專案能使用你的程式碼,你的公開 API 就是你對 crate 使用者的合約,這會決定他們能如何使用你的程式碼。這需要考量管理你的公開 API,好讓其他人能輕鬆依賴你的 crate。這類的考量不在本書的範疇,如果你對於此議題有興趣的話,請查看 Rust API Guidelines 。 執行檔與函式庫套件的最佳實踐 我們提到套件能同時包含 src/main.rs 作為執行檔 crate 源頭以及 src/lib.rs 作為函式庫 crate 源頭,兩者預設都是用套件的名稱。通常來說,一個函式庫與一個執行檔 crate 這樣的套件模式,在執行檔中只會留下必要的程式碼,其餘則呼叫函式庫的程式碼。這樣其他專案也能運用到套件提供的多數功能,因為函式庫 crate 的程式碼可以分享。 模組要定義在 src/lib.rs 。然後在執行檔 crate 中,任何公開項目都能用套件名稱作為開頭找到。執行檔 crate 應視為函式庫 crate 的使用者,就像外部 crate 那樣使用一樣,只能使用公開 API。這有助於你設計出良好的 API,你不僅是作者,同時還是自己的客戶! 在 第十二章 中,我們會透過寫個命令列程式來介紹這樣的組織練習,該程式會包含一個執行檔 crate 與一個函式庫 crate。","breadcrumbs":"透過套件、Crate 與模組管理成長中的專案 » 參考模組項目的路徑 » 使用 pub 關鍵字公開路徑","id":"115","title":"使用 pub 關鍵字公開路徑"},"116":{"body":"我們可以在路徑開頭使用 super 來建構從上層模組出發的相對路徑,而不用從 crate 源頭開始。這就像在檔案系統中使用 .. 作為路徑開頭一樣。使用 super 讓我們能參考確定位於上層模組的項目。當模組與上層模組有高度關聯,且上層模組可能以後會被移到模組樹的其他地方時,這能讓組織模組樹更加輕鬆。 請考慮範例 7-8 的程式碼,這模擬了一個主廚修正一個錯誤的訂單,並親自提供給顧客的場景。定義在 back_of_house 模組的函式 fix_incorrect_order 呼叫了定義在上層模組的函式 deliver_order,不過這次是使用 super 來指定 deliver_order 的路徑: 檔案名稱:src/lib.rs fn deliver_order() {} mod back_of_house { fn fix_incorrect_order() { cook_order(); super::deliver_order(); } fn cook_order() {}\n} 範例 7-8:使用 super 作為呼叫函式路徑的開頭 fix_incorrect_order 函式在 back_of_house 模組中,所以我們可以使用 super 前往 back_of_house 的上層模組,在此例的話就是源頭 crate。然後在此時我們就能找到 deliver_order。成功!我們認定 back_of_house 模組與 deliver_order 函式應該會維持這樣相同的關係,在我們要組織 crate 的模組樹時,它們理當一起被移動。因此我們使用 super 讓我們在未來程式碼被移動到不同模組時,我們不用更新太多程式路徑。","breadcrumbs":"透過套件、Crate 與模組管理成長中的專案 » 參考模組項目的路徑 » 使用 super 作為相對路徑的開頭","id":"116","title":"使用 super 作為相對路徑的開頭"},"117":{"body":"我們也可以使用 pub 來公開結構體與列舉,但是我們有些額外細節要考慮到。如果我們在結構體定義之前加上 pub 的話,我們的確能公開結構體,但是結構體內的欄位仍然會是私有的。我們可以視情況決定每個欄位要不要公開。在範例 7-9 我們定義了一個公開的結構體 back_of_house::Breakfast 並公開欄位 toast,不過將欄位 seasonal_fruit 維持是私有的。這次範例模擬的情境是,餐廳顧客可以選擇早餐要點什麼類型的麵包,但是由主廚視庫存與當季食材來決定提供何種水果。餐廳提供的水果種類隨季節變化很快,所以顧客無法選擇或預先知道他們會拿到何種水果。 檔案名稱:src/lib.rs mod back_of_house { pub struct Breakfast { pub toast: String, seasonal_fruit: String, } impl Breakfast { pub fn summer(toast: &str) -> Breakfast { Breakfast { toast: String::from(toast), seasonal_fruit: String::from(\"桃子\"), } } }\n} pub fn eat_at_restaurant() { // 點夏季早餐並選擇黑麥麵包 let mut meal = back_of_house::Breakfast::summer(\"黑麥\"); // 我們想改成全麥麵包 meal.toast = String::from(\"全麥\"); println!(\"我想要{}麵包,謝謝\", meal.toast); // 接下來這行取消註解的話,我們就無法編譯通過 // 我們無法擅自更改餐點搭配的季節水果 // meal.seasonal_fruit = String::from(\"藍莓\");\n} 範例 7-9:一個有些欄位公開而有些是私有欄位的結構體 因為 back_of_house::Breakfast 結構體中的 toast 欄位是公開的,在 eat_at_restaurant 中我們可以加上句點來對 toast 欄位進行讀寫。注意我們不能在 eat_at_restaurant 使用 seasonal_fruit 欄位,因為它是私有的。請嘗試解開修改 seasonal_fruit 欄位數值的那行程式註解,看看你會獲得什麼錯誤! 另外因為 back_of_house::Breakfast 擁有私有欄位,該結構體必須提供一個公開的關聯函式(associated function)才有辦法產生 Breakfast 的實例(我們在此例命名為 summer)。如果 Breakfast 沒有這樣的函式的話,我們就無法在 eat_at_restaurant 建立 Breakfast 的實例,因為我們無法在 eat_at_restaurant 設置私有欄位 seasonal_fruit 的數值。 接下來,如果我們公開列舉的話,那它所有的變體也都會公開。我們只需要在 enum 關鍵字之前加上 pub 就好,如範例 7-10 所示。 檔案名稱:src/lib.rs mod back_of_house { pub enum Appetizer { Soup, Salad, }\n} pub fn eat_at_restaurant() { let order1 = back_of_house::Appetizer::Soup; let order2 = back_of_house::Appetizer::Salad;\n} 範例 7-10:公開列舉會讓其所有變體也公開 因為我們公開了 Appetizer 列舉,我們可以在 eat_at_restaurant 使用 Soup 和 Salad。 列舉的變體沒有全部都公開的話,通常會讓列舉很不好用。要用 pub 標註所有的列舉變體都公開的話又很麻煩。所以公開列舉的話,預設就會公開其變體。相反地,結構體不讓它的欄位全部都公開的話,通常反而比較實用。因此結構體欄位的通用原則是預設為私有,除非有 pub 標註。 我們還有一個 pub 的使用情境還沒提到,也就是我們模組系統最後一項功能:use 關鍵字。我們接下來會先解釋 use,再來研究如何組合 pub 和 use。","breadcrumbs":"透過套件、Crate 與模組管理成長中的專案 » 參考模組項目的路徑 » 公開結構體與列舉","id":"117","title":"公開結構體與列舉"},"118":{"body":"要是每次都得寫出呼叫函式的路徑的話是很冗長、重複且不方便的。舉例來說範例 7-7 我們在考慮要使用絕對或相對路徑來呼叫 add_to_waitlist 函式時,每次想要呼叫 add_to_waitlist 我們都得指明 front_of_house 以及 hosting。幸運的是,我們有簡化過程的辦法:我們可以用 use 關鍵字建立路徑的捷徑,然後在作用域內透過更短的名稱來使用。 在範例 7-11 中,我們引入了 crate::front_of_house::hosting 模組進 eat_at_restaurant 函式的作用域中,所以我們要呼叫函式 add_to_waitlist 的話我們只需要指明 hosting::add_to_waitlist。 檔案名稱:src/lib.rs mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} }\n} use crate::front_of_house::hosting; pub fn eat_at_restaurant() { hosting::add_to_waitlist();\n} 範例 7-11:使用 use 將模組引入 使用 use 將路徑引入作用域就像是在檔案系統中產生符號連結一樣(symbolic link)。在 crate 源頭加上 use crate::front_of_house::hosting 後,hosting 在作用域內就是個有效的名稱了。使用 use 的路徑也會檢查隱私權,就像其他路徑一樣。 注意到 use 只會在它所位在的特定作用域內建立捷徑。範例 7-12 將 eat_at_restaurant 移入子模組 customer,這樣就會與 use 陳述式的作用域不同,所以其函式本體將無法編譯。 檔案名稱:src/lib.rs mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} }\n} use crate::front_of_house::hosting; mod customer { pub fn eat_at_restaurant() { hosting::add_to_waitlist(); }\n} 範例 7-12:use 陳述式只適用於所在的作用域 編譯器錯誤顯示了該捷徑無法用在 customer 模組內: $ cargo build Compiling restaurant v0.1.0 (file:///projects/restaurant)\nerror[E0433]: failed to resolve: use of undeclared crate or module `hosting` --> src/lib.rs:11:9 |\n11 | hosting::add_to_waitlist(); | ^^^^^^^ use of undeclared crate or module `hosting` warning: unused import: `crate::front_of_house::hosting` --> src/lib.rs:7:5 |\n7 | use crate::front_of_house::hosting; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: `#[warn(unused_imports)]` on by default For more information about this error, try `rustc --explain E0433`.\nwarning: `restaurant` (lib) generated 1 warning\nerror: could not compile `restaurant` due to previous error; 1 warning emitted 你會發現還有另外一個警告說明 use 在它的作用域中並沒有被用到!要解決此問題的話,我們可以將 use 也移動到 customer 模組內,或是在customer 子模組透過 super::hosting 參考上層模組的捷徑。","breadcrumbs":"透過套件、Crate 與模組管理成長中的專案 » 透過 use 關鍵字引入路徑 » 透過 use 關鍵字引入路徑","id":"118","title":"透過 use 關鍵字引入路徑"},"119":{"body":"在範例 7-11 你可能會好奇為何我們指明 use crate::front_of_house::hosting 然後在 eat_at_restaurant 呼叫,而不是直接用 use 指明 add_to_waitlist 函式的整個路徑就好。像範例 7-13 這樣寫。 檔案名稱:src/lib.rs mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} }\n} use crate::front_of_house::hosting::add_to_waitlist; pub fn eat_at_restaurant() { add_to_waitlist();\n} 範例 7-13:使用 use 將 add_to_waitlist 函式引入作用域,但這較不符合習慣 雖然範例 7-11 與範例 7-13 都能完成相同的任務,但是範例 7-11 的做法比較符合習慣用法。使用 use 將函式的上層模組引入作用域,讓我們必須在呼叫函式時得指明對應模組。在呼叫函式時指定上層模組能清楚地知道該函式並非本地定義的,同時一樣能簡化路徑。範例 7-13 的程式碼會不清楚 add_to_waitlist 是在哪定義的。 另一方面,如果是要使用 use 引入結構體、列舉或其他項目的話,直接指明完整路徑反而是符合習慣的方式。範例 7-14 顯示了將標準函式庫的 HashMap 引入執行檔 crate 作用域的習慣用法。 檔案名稱:src/main.rs use std::collections::HashMap; fn main() { let mut map = HashMap::new(); map.insert(1, 2);\n} 範例 7-14:引入 HashMap 進作用域的習慣用法 此習慣沒什麼強硬的理由:就只是大家已經習慣這樣的用法來讀寫 Rust 的程式碼。 這樣的習慣有個例外,那就是如果我們將兩個相同名稱的項目使用 use 陳述式引入作用域時,因為 Rust 不會允許。範例 7-15 展示了如何引入兩個同名但屬於不同模組的 Result 型別進作用域中並使用的方法。 檔案名稱:src/lib.rs use std::fmt;\nuse std::io; fn function1() -> fmt::Result { // --省略--\n# Ok(())\n} fn function2() -> io::Result<()> { // --省略--\n# Ok(())\n} 範例 7-15:要將兩個同名的型別引入相同作用域的話,必須使用它們所屬的模組 如同你所見使用對應的模組可以分辨出是在使用哪個 Result 型別。如果我們直接指明 use std::fmt::Result 和 use std::io::Result 的話,我們會在同一個作用域中擁有兩個 Result 型別,這樣一來 Rust 就無法知道我們想用的 Result 是哪一個。","breadcrumbs":"透過套件、Crate 與模組管理成長中的專案 » 透過 use 關鍵字引入路徑 » 建立慣用的 use 路徑","id":"119","title":"建立慣用的 use 路徑"},"12":{"body":"讓我們開始你的 Rust 旅途吧!千里之行始於足下,在此章節我們將討論: 在 Linux、macOS 和 Windows 上安裝 Rust 寫一支印出 Hello, world! 的程式 使用 Rust 的套件管理工具暨建構系統 cargo","breadcrumbs":"開始入門 » 開始入門","id":"12","title":"開始入門"},"120":{"body":"要在相同作用域中使用 use 引入兩個同名型別的話,還有另一個辦法。在路徑之後,我們可以用 as 指定一個該型別在本地的新名稱,或者說 別名 (alias)。範例 7-16 展示重寫了範例 7-15,將其中一個 Result 型別使用 as 重新命名。 檔案名稱:src/lib.rs use std::fmt::Result;\nuse std::io::Result as IoResult; fn function1() -> Result { // --省略--\n# Ok(())\n} fn function2() -> IoResult<()> { // --省略--\n# Ok(())\n} 範例 7-16:使用 as 將型別引入作用域的同時重新命名 在第二個 use 陳述式,我們選擇了將 std::io::Result 型別重新命名為 IoResult,這樣就不會和同樣引入作用域內 std::fmt 的 Result 有所衝突。範例 7-15 與 範例 7-16 都屬於習慣用法,你可以選擇你比較喜歡的方式!","breadcrumbs":"透過套件、Crate 與模組管理成長中的專案 » 透過 use 關鍵字引入路徑 » 使用 as 關鍵字提供新名稱","id":"120","title":"使用 as 關鍵字提供新名稱"},"121":{"body":"當我們使用 use 關鍵字將名稱引入作用域時,該有效名稱在新的作用域中是私有的。要是我們希望呼叫我們這段程式碼時,也可以使用這個名稱的話(就像該名稱是在此作用域內定義的),我們可以組合 pub 和 use。這樣的技巧稱之為 重新匯出(re-exporting) ,因為我們將項目引入作用域,並同時公開給其他作用域參考。 範例 7-17 將範例 7-11 在源頭模組中原本的 use 改成 pub use。 檔案名稱:src/lib.rs mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} }\n} pub use crate::front_of_house::hosting; pub fn eat_at_restaurant() { hosting::add_to_waitlist();\n} 範例 7-17:使用 pub use 使名稱公開給任何程式的作用域中參考 在此之前,外部程式碼會需要透過 restaurant::front_of_house::hosting::add_to_waitlist() 這樣的路徑才能呼叫 add_to_waitlist。現在 pub use 從源頭模組重新匯出了 hosting 模組,外部程式碼現在可以使用 restaurant::hosting::add_to_waitlist() 這樣的路徑就好。 當程式碼的內部結構與使用程式的開發者對於該領域所想像的結構不同時,重新匯出會很有用。我們再次用餐廳做比喻的話就像是,經營餐廳的人可能會想像餐廳是由「前台」與「後台」所組成,但光顧的顧客可能不會用這些術語來描繪餐廳的每個部分。使用 pub use 的話,我們可以用某種架構寫出程式碼,再以不同的架構對外公開。這樣讓我們的的函式庫可以完整的組織起來,且對開發函式庫的開發者與使用函式庫的開發者都提供友善的架構。我們會在第十四章的 「透過 pub use 匯出理想的公開 API」 段落再看看另一個 pub use 的範例並了解它會如何影響 crate 的技術文件。","breadcrumbs":"透過套件、Crate 與模組管理成長中的專案 » 透過 use 關鍵字引入路徑 » 使用 pub use 重新匯出名稱","id":"121","title":"使用 pub use 重新匯出名稱"},"122":{"body":"在第二章我們寫了一支猜謎遊戲專案時,有用到一個外部套件叫做 rand 來取得隨機數字。要在專案內使用 rand 的話,我們會在 Cargo.toml 加上此行: 檔案名稱:Cargo.toml rand = \"0.8.5\" 在 Cargo.toml 新增 rand 作為依賴函式庫會告訴 Cargo 要從 crates.io 下載 rand 以及其他相關的依賴,讓我們的專案可以使用 rand。 接下來要將 rand 的定義引入我們套件的作用域的話,我們加上一行 use 後面接著 crate 的名稱 rand 然後列出我們想要引入作用域的項目。回想一下在第二章 「產生隨機數字」 的段落,我們將 Rng 特徵引入作用域中,並呼叫函式 rand::thread_rng: # use std::io;\nuse rand::Rng; fn main() {\n# println!(\"請猜測一個數字!\");\n# let secret_number = rand::thread_rng().gen_range(1..=100);\n# # println!(\"祕密數字為:{secret_number}\");\n# # println!(\"請輸入你的猜測數字。\");\n# # let mut guess = String::new();\n# # io::stdin()\n# .read_line(&mut guess)\n# .expect(\"讀取該行失敗\");\n# # println!(\"你的猜測數字:{guess}\");\n} Rust 社群成員在 crates.io 發佈了不少套件可供使用,要將這些套件引入到你的套件的步驟是一樣的。在你的套件的 Cargo.toml 檔案列出它們,然後使用 use 將這些 crate 內的項目引入作用域中。 請注意到標準函式庫 std 對於我們的套件來說也是一個外部 crate。由於標準函式庫會跟著 Rust 語言發佈,所以我們不需要更改 Cargo.toml 來包含 std。但是我們仍然需使用 use 來將它的項目引入我們套件的作用域中。舉例來說,要使用 HashMap 我們可以這樣寫: use std::collections::HashMap; 這是個用標準函式庫的 crate 名稱 std 起頭的絕對路徑。","breadcrumbs":"透過套件、Crate 與模組管理成長中的專案 » 透過 use 關鍵字引入路徑 » 使用外部套件","id":"122","title":"使用外部套件"},"123":{"body":"如果我們要使用在相同 crate 或是相同模組內定義的數個項目,針對每個項目都單獨寫一行的話,會佔據我們檔案內很多空間。舉例來說,範例 2-4 中的猜謎遊戲我們用了這兩個 use 陳述式來引入作用域中: 檔案名稱:src/main.rs # use rand::Rng;\n// --省略--\nuse std::cmp::Ordering;\nuse std::io;\n// --省略--\n# # fn main() {\n# println!(\"請猜測一個數字!\");\n# # let secret_number = rand::thread_rng().gen_range(1..=100);\n# # println!(\"祕密數字為:{secret_number}\");\n# # println!(\"請輸入你的猜測數字。\");\n# # let mut guess = String::new();\n# # io::stdin()\n# .read_line(&mut guess)\n# .expect(\"讀取行數失敗\");\n# # println!(\"你的猜測數字:{guess}\");\n# # match guess.cmp(&secret_number) {\n# Ordering::Less => println!(\"太小了!\"),\n# Ordering::Greater => println!(\"太大了!\"),\n# Ordering::Equal => println!(\"獲勝!\"),\n# }\n# } 我們可以改使用巢狀路徑(nested paths)來只用一行就能將數個項目引入作用域中。我們先指明相同路徑的部分,加上雙冒號,然後在大括號內列出各自不同的路徑部分,如範例 7-18 所示。 檔案名稱:src/main.rs # use rand::Rng;\n// --省略--\nuse std::{cmp::Ordering, io};\n// --省略--\n# # fn main() {\n# println!(\"請猜測一個數字!\");\n# # let secret_number = rand::thread_rng().gen_range(1..=100);\n# # println!(\"祕密數字為:{secret_number}\");\n# # println!(\"請輸入你的猜測數字。\");\n# # let mut guess = String::new();\n# # io::stdin()\n# .read_line(&mut guess)\n# .expect(\"讀取行數失敗\");\n# # let guess: u32 = guess.trim().parse().expect(\"請輸入一個數字!\");\n# # println!(\"你的猜測數字:{guess}\");\n# # match guess.cmp(&secret_number) {\n# Ordering::Less => println!(\"太小了!\"),\n# Ordering::Greater => println!(\"太大了!\"),\n# Ordering::Equal => println!(\"獲勝!\"),\n# }\n# } 範例 7-18:使用巢狀路徑引入有部分相同前綴的數個路徑至作用域中 在較大的程式中,使用巢狀路徑將相同 crate 或相同模組中的許多項目引入作用域,可以大量減少 use 陳述式的數量! 我們可以在路徑中的任何部分使用巢狀路徑,這在組合兩個享有相同子路徑的 use 陳述式時非常有用。舉例來說,範例 7-19 顯示了兩個 use 陳述式:一個將 std::io 引入作用域,另一個將 std::io::Write 引入作用域。 檔案名稱:src/lib.rs use std::io;\nuse std::io::Write; 範例 7-19:兩個 use 陳述式且其中一個是另一個的子路徑 這兩個路徑的相同部分是 std::io,這也是整個第一個路徑。要將這兩個路徑合為一個 use 陳述式的話,我們可以在巢狀路徑使用 self,如範例 7-20 所示。 檔案名稱:src/lib.rs use std::io::{self, Write}; 範例 7-20:組合範例 7-19 的路徑為一個 use 陳述式 此行就會將 std::io 和 std::io::Write 引入作用域。","breadcrumbs":"透過套件、Crate 與模組管理成長中的專案 » 透過 use 關鍵字引入路徑 » 使用巢狀路徑來清理大量的 use 行數","id":"123","title":"使用巢狀路徑來清理大量的 use 行數"},"124":{"body":"如果我們想要將在一個路徑中所定義的 所有 公開項目引入作用域的話,我們可以在指明路徑之後加上全域(glob)運算子 *: use std::collections::*; 此 use 陳述式會將 std::collections 定義的所有公開項目都引入作用域中。不過請小心使用全域運算子!它容易讓我們無法分辨作用域內的名稱,以及程式中使用的名稱是從哪定義來的。 全域運算子很常用在 tests 模組下,將所有東西引入測試中。我們會在第十一章的 「如何寫測試」 段落來討論。全域運算子也常拿來用在 prelude 模式中,你可以查閱 標準函式庫的技術文件 來瞭解此模式的更多資訊。","breadcrumbs":"透過套件、Crate 與模組管理成長中的專案 » 透過 use 關鍵字引入路徑 » 全域運算子","id":"124","title":"全域運算子"},"125":{"body":"本章節目前所有的範例將數個模組定義在同一個檔案中。當模組增長時,你可能會想要將它們的定義拆開到別的檔案中,好讓程式碼容易瀏覽。 舉例來說,讓我們從範例 7-17 餐廳的多重模組開始。我們會將模組拆成數個檔案,而不只是將所有模組都放在 crate 源頭檔案。在此例中,源頭檔案為 src/lib.rs 不過這步驟在執行檔 crate 的 src/main.rs 一樣可行。 首先,我們將 front_of_house 模組移到獨立的檔案中。刪掉 front_of_house 模組大括號內的程式碼,只留下宣告 mod front_of_house;,讓 src/lib.rs 包含的程式碼如範例 7-21 所示。請注意在我們加上範例 7-22 的 src/front_of_house.rs 檔案前這會仍無法編譯。 檔案名稱:src/lib.rs mod front_of_house; pub use crate::front_of_house::hosting; pub fn eat_at_restaurant() { hosting::add_to_waitlist();\n} 範例 7-21:宣告 front_of_house 模組,其本體位於 src/front_of_house.rs 接著,將原本大括號內的程式碼寫到新的檔案 src/front_of_house.rs 中,如範例 7-22 所示。編譯器知道要查看這個檔案,因為 crate 源頭有宣告這個模組的名稱 front_of_house。 檔案名稱:src/front_of_house.rs pub mod hosting { pub fn add_to_waitlist() {}\n} 範例 7-22:front_of_house 模組的定義位於 src/front_of_house.rs 你只需要在模組樹中使用 mod 宣告一次來讀取檔案就好。一旦編譯器知道該檔案屬於專案的一部分(且知道其位在模組樹中的何處,因為你有宣告 mod 陳述式),專案中的其他檔案就能用宣告的路徑讀取檔案的程式碼,如同 「參考模組項目的路徑」 段落提到的一樣。換句話說,mod 和你在其他程式語言可能會看到的「include」動作並 不一樣 。 要開始移動 hosting 的話,我們先改變 src/front_of_house.rs ,讓它只包含 hosting 模組的宣告: 檔案名稱:src/front_of_house.rs pub mod hosting; 然後我們建立一個目錄 src/front_of_house 以及一個檔案 src/front_of_house/hosting.rs 來包含 hosting 模組的定義: 檔案名稱:src/front_of_house/hosting.rs pub fn add_to_waitlist() {} 如果我們將 hosting.rs 放在 src 目錄下,編譯器會將 hosting.rs 的程式碼視為是宣告在 crate 源頭底下的 hosting 模組。編譯器決定哪些檔案屬於哪些模組的規則讓目錄與檔案架構能更貼近模組樹的架構。","breadcrumbs":"透過套件、Crate 與模組管理成長中的專案 » 將模組拆成不同檔案 » 將模組拆成不同檔案","id":"125","title":"將模組拆成不同檔案"},"126":{"body":"目前我們涵蓋了 Rust 編譯器使用的最佳檔案路徑形式,但 Rust 仍然支援舊版的檔案路徑。當 crate 源頭宣告了一個模組 front_of_house 時,編譯器會在以下幾處尋找模組的程式碼: src/front_of_house.rs (我們介紹的) src/front_of_house/mod.rs (舊版風格,仍然支援的路徑形式) 當有個 front_of_house 的子模組 hosting 宣告時,編譯器會在以下幾處尋找模組的程式碼: src/front_of_house/hosting.rs (我們介紹的) src/front_of_house/hosting/mod.rs (舊版風格,仍然支援的路徑形式) 如果你對同個模組同時使用兩種風格的話,你會收到編譯器錯誤。在同個專案對不同模組使用不同風格則是允許的,但這有可能會讓瀏覽專案的人感到困惑。 使用 mod.rs 檔案名稱的風格最主要的缺點是你的專案可能最後會有很多檔案都叫做 mod.rs ,當你在編輯器同時開啟這些檔案時可能會被混淆。 我們將模組的程式碼搬到了不同的檔案,而模組樹仍維持完好如初。就算函式定義被移動不同檔案,eat_at_restaurant 內的函式呼叫不用任何修改仍能維持運作。這樣的方式讓你可以隨著模組成長時,移動到新的檔案中。 另外 src/lib.rs 內的 pub use crate::front_of_house::hosting 陳述式沒有改變,在檔案作為 crate 的一部分來編譯時,使用 use 的方式也沒有改變。mod 關鍵字能宣告模組,然後 Rust 會去同名的檔案尋找該模組的程式碼。","breadcrumbs":"透過套件、Crate 與模組管理成長中的專案 » 將模組拆成不同檔案 » 其他種的檔案路徑","id":"126","title":"其他種的檔案路徑"},"127":{"body":"Rust 讓你能夠將套件拆成數個 crate,然後 crate 能再分成數個模組,好讓你可以從一個模組內指定其他模組的項目。而你可以使用絕對或相對路徑來達成。這些路徑可以用 use 陳述式來引入作用域,讓你可以在該作用域用更短的路徑來多次呼叫該項目。模組程式碼預設為私有的,但你可以使用 pub 關鍵字公開它的定義內容。 在下個章節,我們將探討在標準函式庫中的一些資料結構集合,讓你可以利用它們寫出整潔有組織的程式碼。","breadcrumbs":"透過套件、Crate 與模組管理成長中的專案 » 將模組拆成不同檔案 » 總結","id":"127","title":"總結"},"128":{"body":"Rust 的標準函式庫提供一些非常實用的資料結構稱之為 集合(collections) 。多數其他資料型別只會呈現一個特定數值,但是集合可以包含數個數值。不像內建的陣列與元組型別,這些集合指向的資料位於堆積上,代表資料的數量不必在編譯期就知道,而且可以隨著程式執行增長或縮減。每種集合都有不同的能力以及消耗,依照你的情形選擇適當的集合,是一項你會隨著開發時間漸漸掌握的技能。在本章節我們會介紹三種在 Rust 程式中十分常用的集合: 向量 (Vector)允許你接二連三地儲存數量不定的數值。 字串 (String)是字元的集合。我們在之前就提過 String 型別,本章會正式深入介紹。 雜湊映射 (Hash map)允許你將值(value)與特定的鍵(key)相關聯。這是從一種更通用的資料結構 映射 (map)衍生出來的特定實作。 想瞭解更多標準函式庫提供的集合種類的話,歡迎查閱 技術文件 。 我們將討論如何建立與更新向量、字串與雜湊映射,以及它們的所長。","breadcrumbs":"常見集合 » 常見集合","id":"128","title":"常見集合"},"129":{"body":"我們第一個要來看的集合是 Vec 常稱為 向量 (vector)。向量允許你在一個資料結構儲存不止一個數值,而且該結構的記憶體會接連排列所有數值。它們很適合用來處理你手上的項目列表,像是一個檔案中每行的文字,或是購物車內每項物品。","breadcrumbs":"常見集合 » 透過向量儲存列表 » 透過向量儲存列表","id":"129","title":"透過向量儲存列表"},"13":{"body":"第一步是安裝 Rust,我們將會透過 rustup 安裝 Rust,這是個管理 Rust 版本及相關工具的命令列工具。你將會需要網路連線才能下載。 注意:如果你基於某些原因不想使用 rustup 的話,請前往 Rust 其他安裝方法的頁面 尋求其他選項。 以下步驟將會安裝最新的穩定版 Rust 編譯器。Rust 的穩定性能確保本書的所有範例在更新的 Rust 版本仍然能繼續編譯出來。輸出的結果可能會在不同版本間而有些微的差異,因為 Rust 時常會改善錯誤與警告訊息。換句話說,任何你所安裝的最新穩定版 Rust 都應該能夠正常運行本書的內容。","breadcrumbs":"開始入門 » 安裝教學 » 安裝教學","id":"13","title":"安裝教學"},"130":{"body":"要建立一個新的空向量的話,我們呼叫 Vec::new 函式,如範例 8-1 所示。 # fn main() { let v: Vec = Vec::new();\n# } 範例 8-1 建立一個儲存數值型別為 i32 的空向量 注意到我們在此加了型別詮釋。因為我們沒有對此向量插入任何數值,Rust 不知道我們想儲存什麼類型的元素。這是一項重點,向量是用泛型(generics)實作,我們會在第十章說明如何為你自己的型別使用泛型。現在我們只需要知道標準函式庫提供的 Vec 型別可以持有任意型別,然後當特定向量要持有特定型別時,我們可以在尖括號內指定該型別。在範例 8-1,我們告訴 Rust 在 v 中的 Vec 會持有 i32 型別的元素。 不過通常你在建立 Vec 時只需要給予初始數值,Rust 就能推導出你想儲存的數值型別,所以你不太常會需要指明型別詮釋。Rust 還提供了 vec! 巨集讓我們能方便地建立一個新的向量並取得你提供的數值。在範例 8-2 中,我們建立了一個新的 Vec 並擁有數值 1、2 和 3。整數型別為 i32 是因為這是預設整數型別,如同我們在第三章的 「資料型別」 段落提到的一樣。 # fn main() { let v = vec![1, 2, 3];\n# } 範例 8-2:建立一個擁有數值的新向量 因為我們給予了初始的 i32 數值,Rust 可以推導出 v 的型別為 Vec,所以型別詮釋就不是必要的了。接下來,讓我們看看如何修改向量。","breadcrumbs":"常見集合 » 透過向量儲存列表 » 建立新的向量","id":"130","title":"建立新的向量"},"131":{"body":"要在建立向量之後新增元素的話,我們可以使用 push 方法,如範例 8-3 所示。 # fn main() { let mut v = Vec::new(); v.push(5); v.push(6); v.push(7); v.push(8);\n# } 範例 8-3:使用 push 方法來新增數值到向量 與其他變數一樣,如果我們想要變更其數值的話,我們需要使用 mut 關鍵字使它成為可變的,如同第三章提到的一樣。我們插入的數值所屬型別均為 i32,然後 Rust 可以從資料推導,所以我們不必指明 Vec。","breadcrumbs":"常見集合 » 透過向量儲存列表 » 更新向量","id":"131","title":"更新向量"},"132":{"body":"要參考向量儲存的數值有兩種方式。為了更加清楚說明此範例,我們詮釋了函式回傳值的型別。 範例 8-4 顯示了取得向量中數值的方法,可以使用索引語法與 get 方法。 # fn main() { let v = vec![1, 2, 3, 4, 5]; let third: &i32 = &v[2]; println!(\"第三個元素是 {third}\"); let third: Option<&i32> = v.get(2); match third { Some(third) => println!(\"第三個元素是 {third}\"), None => println!(\"第三個元素並不存在。\"), }\n# } 範例 8-4:使用索引語法或 get 方法來取得向量項目 這邊我們要注意一些地方。我們使用了索引數值 2 來獲取第三個元素:向量可以用數字來索引,從零開始計算。使用 & 和 [] 會給我們索引數值的元素參考,而使用 get 方法加上一個索引作為引數,則會給我們 Option<&T>,我們可以用 match 來配對。 Rust 提供兩種取得元素參考方式,所以當你嘗試使用索引數值取得向量範圍外的元素時,你可以決定程式的行為。讓我們看看一個範例,我們有一個向量擁有五個元素,但我們嘗試用索引 100 來取得對應數值,如範例 8-5 所示。 # fn main() { let v = vec![1, 2, 3, 4, 5]; let does_not_exist = &v[100]; let does_not_exist = v.get(100);\n# } 範例 8-5:嘗試對只有五個元素的向量取得索引 100 的值 當我們執行程式時,第一個 [] 方法會讓程式恐慌,因為它參考了不存在的元素。此方法適用於當你希望一有無效索引時就讓程式崩潰的狀況。 當你使用 get 方法來索取向量不存在的索引時,它會回傳 None 而不會恐慌。如果正常情況下偶而會不小心存取超出向量範圍索引的話,你就會想要只用此方法。你的程式碼就會有個邏輯專門處理 Some(&element) 或 None,如同第六章所述。舉例來說,可能會有由使用者輸入的索引。如果他不小心輸入太大的數字的話,程式可以回傳 None,你可以告訴使用者目前向量有多少項目,並讓他們可以再輸入一次。這會比直接讓程式崩潰還來的友善,他們可能只是不小心打錯而已! 當程式有個有效參考時,借用檢查器(borrow checker)會貫徹所有權以及借用規則(如第四章所述)來確保此參考及其他對向量內容的參考都是有效的。回想一下有個規則是我們不能在同個作用域同時擁有可變與不可變參考。這個規則一樣適用於範例 8-6,在此我們有一個向量第一個元素的不可變參考,然後我們嘗試在向量後方新增元素。如果我們嘗試在此動作後繼續使用第一個參考的話,程式會無法執行: # fn main() { let mut v = vec![1, 2, 3, 4, 5]; let first = &v[0]; v.push(6); println!(\"第一個元素是:{first}\");\n# } 範例 8-6:在持有一個項目的參考時,還嘗試對向量新增元素 編譯此程式會得到以下錯誤: $ cargo run Compiling collections v0.1.0 (file:///projects/collections)\nerror[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable --> src/main.rs:6:5 |\n4 | let first = &v[0]; | - immutable borrow occurs here\n5 |\n6 | v.push(6); | ^^^^^^^^^ mutable borrow occurs here\n7 |\n8 | println!(\"第一個元素為 {first}\"); | ----- immutable borrow later used here For more information about this error, try `rustc --explain E0502`.\nerror: could not compile `collections` due to previous error 範例 8-6 的程式碼看起來好像能執行。為何第一個元素的參考要在意向量的最後端發生了什麼事呢?此錯誤其實跟向量運作的方式有關:由於向量會將元素放在前一位的記憶體位置後方,在向量後方新增元素時,如果當前向量的空間不夠再塞入另一個值的話,可能會需要配置新的記憶體並複製舊的元素到新的空間中。這樣一來,第一個元素的索引可能就會指向已經被釋放的記憶體,借用規則會防止程式遇到這樣的情形。 注意:關於 Vec 型別更多的實作細節,歡迎查閱 「The Rustonomicon」 。","breadcrumbs":"常見集合 » 透過向量儲存列表 » 讀取向量元素","id":"132","title":"讀取向量元素"},"133":{"body":"想要依序存取向量中每個元素的話,我們可以遍歷所有元素而不必用索引一個一個取得。範例 8-7 闡釋了如何使用 for 迴圈來取得一個 i32 向量中每個元素的不可變參考並印出他們。 # fn main() { let v = vec![100, 32, 57]; for i in &v { println!(\"{i}\"); }\n# } 範例 8-7:使用 for 迴圈遍歷向量中每個元素 我們還可以遍歷可變向量中的每個元素取得可變參考來改變每個元素。像是範例 8-8 就使用 for 迴圈來為每個元素加上 50。 # fn main() { let mut v = vec![100, 32, 57]; for i in &mut v { *i += 50; }\n# } 範例 8-8:遍歷向量中的元素取得可變參考 要改變可變參考指向的數值,在使用 += 運算子之前,我們需要使用 * 解參考運算子來取得 i 的數值。我們會在第十五章的 「追蹤指標的數值」 段落來講解更多解參考運算子的細節。 當遍歷向量時,無論是不可變或可變地都是安全,因為借用檢查器的規則能確保如此。如果我們嘗試在範例 8-7 或範例 8-8 的 for 迴圈本體插入或刪除項目,我們就會獲得和範例 8-6 程式碼類似的編譯錯誤。for 迴圈持有的向量參考能避免同時修改整個向量。","breadcrumbs":"常見集合 » 透過向量儲存列表 » 遍歷向量的元素","id":"133","title":"遍歷向量的元素"},"134":{"body":"向量只能儲存同型別的數值,這在某些情況會很不方便,一定會有場合是要儲存不同型別到一個列表中。幸運的是,列舉的變體是定義在相同的列舉型別,所以當我們需要在向量儲存不同型別的元素時,我們可以用列舉來定義! 舉例來說,假設我們想從表格中的一行取得數值,但是有些行內的列會包含整數、浮點數以及一些字串。我們可以定義一個列舉,其變體會持有不同的數值型別,然後所有的列舉變體都會被視為相同型別:就是它們的列舉。接著我們就可以建立一個擁有此列舉型別的向量,最終達成持有不同型別。如範例 8-9 所示。 # fn main() { enum SpreadsheetCell { Int(i32), Float(f64), Text(String), } let row = vec![ SpreadsheetCell::Int(3), SpreadsheetCell::Text(String::from(\"藍色\")), SpreadsheetCell::Float(10.12), ];\n# } 範例 8-9:用 enum 定義儲存不同型別的列舉並作為向量的型別 Rust 需要在編譯時期知道向量的型別以及要在堆積上用到多少記憶體才能儲存每個元素。我們必須明確知道哪些型別可以放入向量中。如果 Rust 允許向量一次持有任意型別的話,在對向量中每個元素進行處理時,可能就會有一或多種型別會產生錯誤。使用列舉和 match 表達式讓 Rust 可以在編譯期間確保每個可能的情形都已經處理完善了,如同第六章提到的一樣。 如果你無法確切知道執行時程式所處理的所有型別的話,列舉就不管用了。這時使用特徵物件會比較好,我們會在第十七章再來解釋。 現在我們已經講了一些向量常見的用法,有時間的話記得到 向量的 API 技術文件 瞭解標準函式庫中 Vec 所有實用的方法。舉例來說,除了 push 方法以外,還有個 pop 方法可以移除並回傳最後一個元素。","breadcrumbs":"常見集合 » 透過向量儲存列表 » 使用列舉來儲存多種型別","id":"134","title":"使用列舉來儲存多種型別"},"135":{"body":"就像其它 struct 一樣,向量會在作用域結束時被釋放,如範例 8-10 所示。 # fn main() { { let v = vec![1, 2, 3, 4]; // 使用 v 做些事情 } // <- v 在此離開作用域並釋放\n# } 範例 8-10:顯示向量及其元素在哪裡被釋放 當向量被釋放時,其所有內容也都會被釋放,代表它持有的那些整數都會被清除。這雖然聽起來很直觀,但是當我們開始參考向量中的元素時可能就會變得有點複雜。讓我們看看怎麼處理這種情形吧! 接下來讓我們看看下一個集合型別:String!","breadcrumbs":"常見集合 » 透過向量儲存列表 » 釋放向量的同時也會釋放其元素","id":"135","title":"釋放向量的同時也會釋放其元素"},"136":{"body":"我們已經在第四章提到字串(String),但現在我們要更加深入探討。Rustaceans 初學者常常會卡在三個環節:Rust 傾向於回報可能的錯誤、字串的資料結構比開發者所熟悉的還要複雜,以及 UTF-8。這些要素讓來自其他程式語言背景的開發者會遇到一些困難。 我們會在集合章節討論字串的原因是,字串本身就是位元組的集合,且位元組作為文字呈現時,它會提供一些實用的方法。在此段落我們將和其他集合型別一樣討論 String 的操作,像是建立、更新與讀取。我們還會討論到 String 與其他集合不一樣的地方,像是 String 的索引就比其他集合還複雜,因為它會依據人們對於 String 資料型別的理解而有所不同。","breadcrumbs":"常見集合 » 透過字串儲存 UTF-8 編碼的文字 » 透過字串儲存 UTF-8 編碼的文字","id":"136","title":"透過字串儲存 UTF-8 編碼的文字"},"137":{"body":"首先我們要好好定義 字串(String) 這個術語。Rust 在核心語言中只有一個字串型別,那就是字串切片 str,它通常是以借用的形式存在 &str。在第四章中我們提到 字串切片 是一個針對存在某處的 UTF-8 編碼資料的參考。舉例來說,字串字面值(String literals)就儲存在程式的執行檔中,因此就是字串切片。 String 型別是 Rust 標準函式庫所提供的型別,並不是核心語言內建的型別,它是可增長的、可變的、可擁有所有權的 UTF-8 編碼字串型別。當 Rustaceans 提及 Rust 中的「字串」時,他們通常指的是 String 以及字串切片 &str 型別,而不只是其中一種型別。雖然此段落大部分都在討論 String,這兩個型別都時常用在 Rust 的標準函式庫中,且 String 與字串切片都是 UTF-8 編碼的。","breadcrumbs":"常見集合 » 透過字串儲存 UTF-8 編碼的文字 » 什麼是字串?","id":"137","title":"什麼是字串?"},"138":{"body":"許多 Vec 可使用的方法在 String 也都能用,因為 String 其實就是一種位元組向量的封裝再加上一些額外的保障、限制與能力。其中一個 Vec 與 String 都有且用途相同的函式就是 new,這用來產生新的實例,如範例 8-11 所示。 # fn main() { let mut s = String::new();\n# } 範例 8-11:建立新的空 String 此行會建立新的字串叫做 s,我們之後可以再寫入資料。不過通常我們會希望建立字串的同時能夠初始化資料。為此我們可以使用 to_string 方法,任何有實作 Display 特徵的型別都可以使用此方法,就像字串字面值的使用方式一樣。範例 8-12 就展示了兩種例子。 # fn main() { let data = \"初始內容\"; let s = data.to_string(); // 此方法也能直接用於字面值上 let s = \"初始內容\".to_string();\n# } 範例 8-12:從字串字面值使用 to_string 方法來建立 String 此程式碼建立了一個字串內容為 初始內容。 我們也可以用函式 String::from 從字串字面值建立 String。範例 8-13 的程式碼和使用 to_string 的範例 8-12 效果一樣。 # fn main() { let s = String::from(\"初始內容\");\n# } 範例 8-13:使用函式 String::from 從字串字面值建立 String 因為字串用在許多地方,我們可以使用許多不同的通用字串 API 供我們選擇。有些看起來似乎是多餘的,但是它們都有一席之地的!在上面的範例中 String::from 和 to_string 都在做相同的事,所以你的選擇跟喜好風格與閱讀性比較有關。 另外記得字串是 UTF-8 編碼的,所以我們可以包含任何正確編碼的資料,如範例 8-14 所示。 # fn main() { let hello = String::from(\"السلام عليكم\"); let hello = String::from(\"Dobrý 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(\"Здравствуйте\"); let hello = String::from(\"Hola\");\n# } 範例 8-14:用字串儲存各種語言打招呼的文字 以上全是合理的 String 數值。","breadcrumbs":"常見集合 » 透過字串儲存 UTF-8 編碼的文字 » 建立新的字串","id":"138","title":"建立新的字串"},"139":{"body":"就和 Vec 一樣,如果你插入更多資料的話,String 可以增長大小並變更其內容。除此之外你也可以使用 + 運算子或 format! 巨集來串接 String 數值。 使用 push_str 和 push 追加字串 我們可以使用 push_str 方法來追加一個字串切片使字串增長,如範例 8-15 所示。 # fn main() { let mut s = String::from(\"foo\"); s.push_str(\"bar\");\n# } 範例 8-15:使用 push_str 方法向 String 追加字串切片 在這兩行之後,s 會包含 foobar。push_str 方法取得的是字串切片因為我們並不需要取得參數的所有權。舉例來說,在範例 8-16 我們想在 s2 追加其內容給 s1 之後仍能使用。 # fn main() { let mut s1 = String::from(\"foo\"); let s2 = \"bar\"; s1.push_str(s2); println!(\"s2 is {s2}\");\n# } 範例 8-16:在內容追加給 String 後繼續使用字串切片 如果 push_str 方法會取得 s2 的所有權,我們就無法在最後一行印出其數值了。幸好這段程式碼是可以執行的! 而 push 方法會取得一個字元作為參數並加到 String 上。範例 8-17 顯示了一個使用 push 方法將字母 \"l\" 加到 String 的程式碼。 # fn main() { let mut s = String::from(\"lo\"); s.push('l');\n# } 範例 8-17:使用 push 將一個字元加到 String 結果就是 s 會包含 lol。 使用 + 運算子或 format! 巨集串接字串 你通常會想要組合兩個字串在一起,其中一種方式是用 + 運算子。如範例 8-18 所示。 # fn main() { let s1 = String::from(\"Hello, \"); let s2 = String::from(\"world!\"); let s3 = s1 + &s2; // 注意到 s1 被移動因此無法再被使用\n# } 範例 8-18:使用 + 運算子組合兩個 String 數值成一個新的 String 數值 程式碼最後的字串 s3 就會獲得 Hello, world!。s1 之所以在相加後不再有效,以及 s2 是使用參考的原因,都和我們使用 + 運算子時呼叫的方法簽名有關。+ 運算子使用的是 add 方法,其簽名會長得像這樣: fn add(self, s: &str) -> String { 在標準函式庫中 add 是用泛型(generics)與關聯型別(associated types)定義。我們在此使用實際型別代替的 add 簽名。我們會在第十章討論到泛型。此簽名給了一些我們需要瞭解 + 運算子的一些線索。 首先 s2 有 & 代表我們是將第二個字串的 參考 與第一個字串相加,因為函式 add 中的參數 s 說明我們只能將 &str 與 String 相加,我們無法將兩個 String 數值相加。但等等 &s2 是 &String 才對,並非 add 第二個參數所指定的 &str。為何範例 8-18 可以編譯呢? 我們可以在 add 的呼叫中使用 &s2 的原因是因為編譯器可以 強制(coerce) &String 引數轉換成 &str。當我們我們呼叫 add 方法時,Rust 強制解參考 (deref coercion)讓 &s2 變成 &s2[..]。我們會在第十五章深入探討強制解參考。因為 add 不會取得 s 參數的所有權,s2 在此運算後仍然是個有效的 String。 再來,我們可以看到 add 的簽名會取得 self 的所有權,因為 self 沒有 &。這代表範例 8-18 的 s1 會移動到 add 的呼叫內,在之後就不再有效。所以雖然 let s3 = s1 + &s2; 看起來像是它拷貝了兩個字串的值並產生了一個新的,但此陳述式實際上是取得 s1 的所有權、追加一份 s2 的複製內容、然後回傳最終結果的所有權。換句話說,雖然它看起來像是產生了很多拷貝,但實際上並不是。此實作反而比較有效率。 如果我們需要串接數個字串的話,+ 運算子的行為看起來就顯得有點笨重了: # fn main() { let s1 = String::from(\"tic\"); let s2 = String::from(\"tac\"); let s3 = String::from(\"toe\"); let s = s1 + \"-\" + &s2 + \"-\" + &s3;\n# } 此時 s 會是 tic-tac-toe。有這麼多的 + 和 \" 字元,我們很難看清楚發生什麼事。如果要完成更複雜的字串組合的話,我們可以改使用 format! 巨集: # fn main() { let s1 = String::from(\"tic\"); let s2 = String::from(\"tac\"); let s3 = String::from(\"toe\"); let s = format!(\"{s1}-{s2}-{s3}\");\n# } 此程式碼一樣能設置 s 為 tic-tac-toe。format! 巨集運作的方式和 println! 類似,但不會將輸出結果顯示在螢幕上,它做的是回傳內容的 String。使用 format! 的程式碼版本看起來比較好讀懂,而且 format! 產生的程式碼使用的是參考,所以此呼叫不會取走任何參數的所有權。","breadcrumbs":"常見集合 » 透過字串儲存 UTF-8 編碼的文字 » 更新字串","id":"139","title":"更新字串"},"14":{"body":"在本章節到整本書為止,我們將會顯示一些終端機會用到的命令。任一你會用到的命令都會始於 $。但你不需要去輸入 $,因為這通常代表每一命令列的起始位置。而沒有出現 $ 的行數,通常則代表前一行命列輸出的結果。除此之外,針對 PowerShell 的範例則將會使用 > 而不是 $。","breadcrumbs":"開始入門 » 安裝教學 » 命令列標記","id":"14","title":"命令列標記"},"140":{"body":"在其他許多程式語言中,使用索引參考字串來取得獨立字元是有效且常見的操作。然而在 Rust 中如果你嘗試對 String 使用索引語法的話,你會得到錯誤。請看看範例 8-19 這段無效的程式碼。 # fn main() { let s1 = String::from(\"hello\"); let h = s1[0];\n# } 範例 8-19:嘗試在字串使用索引語法 此程式會有以下錯誤結果: $ cargo run Compiling collections v0.1.0 (file:///projects/collections)\nerror[E0277]: the type `String` cannot be indexed by `{integer}` --> src/main.rs:3:13 |\n3 | let h = s1[0]; | ^^^^^ `String` cannot be indexed by `{integer}` | = help: the trait `Index<{integer}>` is not implemented for `String` = help: the following other types implement trait `Index`: >> > >> >> >> >> > For more information about this error, try `rustc --explain E0277`.\nerror: could not compile `collections` due to previous error 錯誤訊息與提示告訴了我們 Rust 字串並不支援索引。但為何不支援呢?要回答此問題,我們需要先討論 Rust 如何儲存字串進記憶體的。 內部呈現 String 基本上就是 Vec 的封裝。讓我們看看範例 8-14 中一些正確編碼為 UTF-8 字串的例子,像是這一個: # fn main() {\n# let hello = String::from(\"السلام عليكم\");\n# let hello = String::from(\"Dobrý den\");\n# let hello = String::from(\"Hello\");\n# let hello = String::from(\"שָׁלוֹם\");\n# let hello = String::from(\"नमस्ते\");\n# let hello = String::from(\"こんにちは\");\n# let hello = String::from(\"안녕하세요\");\n# let hello = String::from(\"你好\");\n# let hello = String::from(\"Olá\");\n# let hello = String::from(\"Здравствуйте\"); let hello = String::from(\"Hola\");\n# } 在此例中 len 會是 4,也就是向量儲存的字串「Hola」長度為 4 個位元組。每個字母在用 UTF-8 編碼時長度均為 1 個位元組。但接下來這行可能就會讓你感到驚訝了(請注意字串的開頭是西里爾字母 Ze 的大寫,而不是阿拉伯數字 3)。 # fn main() {\n# let hello = String::from(\"السلام عليكم\");\n# let hello = String::from(\"Dobrý den\");\n# let hello = String::from(\"Hello\");\n# let hello = String::from(\"שָׁלוֹם\");\n# let hello = String::from(\"नमस्ते\");\n# let hello = String::from(\"こんにちは\");\n# let hello = String::from(\"안녕하세요\");\n# let hello = String::from(\"你好\");\n# let hello = String::from(\"Olá\"); let hello = String::from(\"Здравствуйте\");\n# let hello = String::from(\"Hola\");\n# } 你可能會以為這字串的長度為 12,事實上 Rust 給的答案卻是 24。這是將「Здравствуйте」用 UTF-8 編碼後的位元組長度,因為該字串的每個 Unicode 純量都佔據兩個位元組。因此字串位元組的索引不會永遠都能對應到有效的 Unicode 純量數值。我們用以下無效的 Rust 程式碼進一步說明: let hello = \"Здравствуйте\";\nlet answer = &hello[0]; 你已經知道第一個字母 answer 不會是 З。當經過 UTF-8 編碼時,З 的第一個位元組會是 208 然後第二個是 151。所以 answer 實際上會拿到 208,但 208 本身又不是個有效字元。回傳 208 可能不會是使用者想要的,他們希望的應該是此字串的第一個字母,但這是 Rust 在位元組索引 0 唯一能回傳的資料。就算字串都只包含拉丁字母,使用者通常也不會希望看到位元組數值作為回傳值。如果 &\"hello\"[0] 是有效程式碼且會回傳位元組數值的話,它會回傳的是 104 並非 h。 為了預防回傳意外數值進而導致無法立刻察覺的錯誤,Rust 不會成功編譯這段程式碼,並在開發過程前期就杜絕誤會發生。 位元組、純量數值與形素群集!我的天啊! UTF-8 還有一個重點是在 Rust 中我們實際上可以有三種觀點來理解字串:位元組、純量數值(scalar values)以及形素群集(grapheme clusters,最接近人們常說的「 字母 」)。 如果我們觀察用天成體寫的印度語「नमस्ते」,它存在向量中的 u8 數值就會長這樣: [224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,\n224, 165, 135] 這 18 個位元組是電腦最終儲存的資料?如果我們用 Unicode 純量數值觀察的話,也就是 Rust 的 char 型別,這些位元組會組成像這樣: ['न', 'म', 'स', '्', 'त', 'े'] 這邊有六個 char 數值,但第四個和第六個卻不是字母,它們是單獨存在不具任何意義的變音符號。最後如果我們以形素群集的角度來看的話,我們就會得到一般人所說的構成此印度語的四個字母: [\"न\", \"म\", \"स्\", \"ते\"] Rust 提供多種不同的方式來解釋電腦中儲存的原始字串資料,讓每個程式無論是何種人類語言的資料,都可以選擇它們需要的呈現方式。 Rust 還有一個不允許索引 String 來取得字元的原因是因為,索引運算必須永遠預期是花費常數時間(O(1))。但在 String 上無法提供這樣的效能保證,因為 Rust 會需要從索引的開頭遍歷每個內容才能決定多少有效字元存在。","breadcrumbs":"常見集合 » 透過字串儲存 UTF-8 編碼的文字 » 索引字串","id":"140","title":"索引字串"},"141":{"body":"索引字串通常不是個好點子,因為字串索引要回傳的型別是不明確的,是要一個位元組數值、一個字元、一個形素群集還是一個字串切片呢。因此如果你真的想要使用索引建立字串切片的話,Rust 會要你更明確些。要明確指定你的索引與你想要的字串切片。 與其在 [] 只使用一個數字來索引,你可以在 [] 指定一個範圍來建立包含特定位元組的字串切片: let hello = \"Здравствуйте\"; let s = &hello[0..4]; s 在此會是 &str 並包含字串前 4 個位元組。稍早我們提過這些字元各佔 2 個位元組,所以這裡的 s 就是 Зд。 如果我們嘗試只用 &hello[0..1] 來取得字元部分的位元組的話,Rust 會和在向量中取得無效索引一樣在執行時恐慌: $ cargo run Compiling collections v0.1.0 (file:///projects/collections) Finished dev [unoptimized + debuginfo] target(s) in 0.43s Running `target/debug/collections`\nthread 'main' panicked at 'byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `Здравствуйте`', src/main.rs:4:14\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace 你在使用範圍來建立字串切片時要格外小心,因為這樣做有可能會使你的程式崩潰。","breadcrumbs":"常見集合 » 透過字串儲存 UTF-8 編碼的文字 » 字串切片","id":"141","title":"字串切片"},"142":{"body":"要對字串的部分進行操作最好的方式是明確表達你想要的是字元還是位元組。對獨立的 Unicode 純量型別來說的話,就是使用 chars 方法。對「Зд」呼叫 chars 會將兩個擁有 char 型別的數值拆開並回傳,這樣一來你就可以遍歷每個元素: for c in \"Зд\".chars() { println!(\"{c}\");\n} 此程式碼會顯示以下輸出: З\nд 而 bytes 方法會回傳每個原始位元組,可能會在某些場合適合你: for b in \"Зд\".bytes() { println!(\"{b}\");\n} 此程式碼會印出此字串的四個位元組: 208\n151\n208\n180 請確定你已經瞭解有效的 Unicode 純量數值可能不止佔 1 個位元組。 而要從天成體組成的字串取得形素群集的話就非常複雜了,所以標準函式庫並未提供這項功能。如果你需要的話, crates.io 上會有提供這項功能的 crate。","breadcrumbs":"常見集合 » 透過字串儲存 UTF-8 編碼的文字 » 遍歷字串的方法","id":"142","title":"遍歷字串的方法"},"143":{"body":"總結來說,字串是很複雜的。不同的程式語言會選擇不同的決定來呈現給程式設計師。Rust 選擇正確處理 String 的方式作為所有 Rust 程式的預設行為,這也代表開發者在處理 UTF-8 資料時需要多加考量。這樣的取捨的確對比其他程式語言來說,增加了不少字串的複雜程度,但是這能讓你在開發週期免於處理非 ASCII 字元相關的錯誤。 好消息是標準函式庫針對 String 與 &str 型別提供了許多功能,來幫助正確處理這些複雜的情況。別忘了翻翻技術文件來學習這些實用的方法,像是 contains 能搜尋字串,而 replace 能替換部份字串成另一個字串。 讓我們接下去看一個較簡單地集合吧:雜湊映射(hash maps)!","breadcrumbs":"常見集合 » 透過字串儲存 UTF-8 編碼的文字 » 字串並不簡單","id":"143","title":"字串並不簡單"},"144":{"body":"我們最後一個常見的集合是 雜湊映射(hash map) ,HashMap 型別會儲存一個鍵(key)型別 K 對應到一個數值(value)型別 V。它透過 雜湊函式 (hashing function)來決定要將這些鍵與值放在記憶體何處。許多程式語言都有支援這種類型的資料結構,不過通常它們會提供不同的名稱,像是 hash、map、object、hash table、dictionary 或 associative array 等等。 雜湊映射適合用於當你不想像向量那樣用索引搜尋資料,而是透過一個可以為任意型別的鍵來搜尋的情況。舉例來說,在比賽中我們可以使用雜湊映射來儲存每隊的分數,每個鍵代表隊伍名稱,而每個值代表隊伍分數。給予一個隊伍名稱,你就能取得該隊伍分數。 我們會在此段落介紹雜湊映射的基本 API,但還有很多實用的函式定義在標準函式庫的 HashMap 中,所以別忘了查閱標準函式庫的技術文件來瞭解更多資訊。","breadcrumbs":"常見集合 » 透過雜湊映射儲存鍵值配對 » 透過雜湊映射儲存鍵值配對","id":"144","title":"透過雜湊映射儲存鍵值配對"},"145":{"body":"其中一種建立空的雜湊映射的方式是使用 new 並透過 insert 加入新元素。在範例 8-20 我們追蹤兩支隊伍的分數,分別為藍隊與黃隊。藍隊初始分數有 10 分,黃隊則有 50 分。 # fn main() { use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from(\"藍隊\"), 10); scores.insert(String::from(\"黃隊\"), 50);\n# } 範例 8-20:建立新的雜湊映射並插入一些鍵值 注意到我們需要先使用 use 將標準函式庫的 HashMap 集合引入。在我們介紹的三個常見集合中,此集合是最少被用到的,所以它並沒有包含在 prelude 內讓我們能自動參考。雜湊映射也沒有像前者那麼多標準函式庫提供的支援,像是內建建構它們的巨集。 和向量一樣,雜湊映射會將它們的資料儲存在堆積上。此 HashMap 的鍵是 String 型別而值是 i32 型別。和向量一樣,雜湊函式宣告後就都得是同類的,所有的鍵都必須是同型別,且所有的值也都必須是同型別。","breadcrumbs":"常見集合 » 透過雜湊映射儲存鍵值配對 » 建立新的雜湊映射","id":"145","title":"建立新的雜湊映射"},"146":{"body":"我們可以透過 get 方法並提供鍵來取得其在雜湊映射對應的值,如範例 8-21 所示。 # fn main() { use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from(\"藍隊\"), 10); scores.insert(String::from(\"黃隊\"), 50); let team_name = String::from(\"藍隊\"); let score = scores.get(&team_name).copied().unwrap_or(0);\n# } 範例 8-21:取得雜湊映射中藍隊的分數 score 在此將會是對應藍隊的分數,而且結果會是 10。結果是使用 Some 的原因是因為 get 回傳的是 Option<&V>。如果雜湊映射中該鍵沒有對應值的話,get 就會回傳 None。所以程式會需要透過我們在第六章談到的方式處理 Option。 我們也可以使用 for 迴圈用類似的方式來遍歷雜湊映射中每個鍵值配對: # fn main() { use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from(\"藍隊\"), 10); scores.insert(String::from(\"黃隊\"), 50); for (key, value) in &scores { println!(\"{key}: {value}\"); }\n# } 此程式會以任意順序印出每個配對: 黃隊: 50\n藍隊: 10","breadcrumbs":"常見集合 » 透過雜湊映射儲存鍵值配對 » 取得雜湊映射的數值","id":"146","title":"取得雜湊映射的數值"},"147":{"body":"像是 i32 這種有實作 Copy 特徵的型別其數值可以被拷貝進雜湊映射之中。但對於像是 String 這種擁有所有權的數值則會被移動到雜湊映射,並成為該數值新的擁有者,如範例 8-22 所示。 # fn main() { use std::collections::HashMap; let field_name = String::from(\"Favorite color\"); let field_value = String::from(\"藍隊\"); let mut map = HashMap::new(); map.insert(field_name, field_value); // field_name 和 field_value 在這之後就不能使用了,你可以試著使用它們並看看編譯器回傳什麼錯誤\n# } 範例 8-22:展示當鍵值插入雜湊映射後就會擁有它們 我們之後就無法使用變數 field_name 和 field_value,因為它們的值已經透過呼叫 insert 被移入雜湊映射之中。 如果我們插入雜湊映射的數值是參考的話,該值就不會被移動到雜湊映射之中。不過該值的參考就必須一直有效,至少直到該雜湊映射離開作用域為止。我們會在第十章的 「透過生命週期驗證參考」 段落討落更多細節。","breadcrumbs":"常見集合 » 透過雜湊映射儲存鍵值配對 » 雜湊映射與所有權","id":"147","title":"雜湊映射與所有權"},"148":{"body":"雖然鍵值配對的數量可以增加,但每個鍵同一時間就只能有一個對應的值而已。(反之並不成立:比如藍隊黃隊可以同時都在 scores 雜湊映射內儲存 10 分) 當你想要改變雜湊映射的資料的話,你必須決定如何處理當一個鍵已經有一個值的情況。你可以不管舊的值,直接用新值取代。你也可以保留舊值、忽略新值,只有在該鍵 尚未 擁有對應數值時才賦值給它。或者你也可以將舊值與新值組合起來。讓我們看看分別怎麼處理吧! 覆蓋數值 如果我們在雜湊映射插入一個鍵值配對,然後又在相同鍵插入不同的數值的話,該鍵相對應的數值就會被取代。如範例 8-23 雖然我們呼叫了兩次 insert,但是雜湊映射只會保留一個鍵值配對,因為我們向藍隊的鍵插入了兩次數值。 # fn main() { use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from(\"藍隊\"), 10); scores.insert(String::from(\"藍隊\"), 25); println!(\"{:?}\", scores);\n# } 範例 8-23:替換某個特定鍵對應的數值 此程式碼會印出 {\"藍隊\": 25},原本的數值 10 會被覆蓋。 只在鍵不存在的情況下插入鍵值 通常檢查雜湊映射有沒有存在某個特定的鍵值是很常見的。我們接下來的動作通常就是檢查如果鍵存在於雜湊映射的話,就不改變其值。但如果鍵不存在的話,就插入數值給它。 雜湊映射提供了一個特別的 API 叫做 entry 讓你可以用想要檢查的鍵作為參數。entry 方法的回傳值是一個列舉叫做 Entry,它代表了一個可能存在或不存在的數值。假設我們想要檢查黃隊的鍵有沒有對應的數值。如果沒有的話,我們想插入 50。而對藍隊也一樣。使用 entry API 的話,程式碼會長得像範例 8-24。 # fn main() { use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from(\"藍隊\"), 10); scores.entry(String::from(\"黃隊\")).or_insert(50); scores.entry(String::from(\"藍隊\")).or_insert(50); println!(\"{:?}\", scores);\n# } 範例 8-24:使用 entry 方法在只有該鍵尚無任何數值時插入數值 Entry 中的 or_insert 方法定義了如果 Entry 的鍵有對應的數值的話,就回傳該值的可變參考;如果沒有的話,那就插入參數作為新數值,並回傳此值的可變參考。這樣的技巧比我們親自寫邏輯還來的清楚,而且更有利於借用檢查器的檢查。 執行範例 8-25 的程式碼會印出 {\"黃隊\": 50, \"藍隊\": 10}。第一次 entry 的呼叫會對黃隊插入數值 50,因為黃隊尚未有任何數值。第二次 entry 的呼叫則不會改變雜湊映射,因為藍隊已經有數值 10。 依據舊值更新數值 雜湊映射還有另一種常見的用法是,依照鍵的舊數值來更新它。舉例來說,範例 8-25 展示了一支如何計算一些文字內每個單字各出現多少次的程式碼。我們使用雜湊映射,鍵為單字然後值為我們每次追蹤計算對應單字出現多少次的次數。如果我們是第一次看到該單字的話,我們插入數值 0。 # fn main() { use std::collections::HashMap; let text = \"hello world wonderful world\"; let mut map = HashMap::new(); for word in text.split_whitespace() { let count = map.entry(word).or_insert(0); *count += 1; } println!(\"{:?}\", map);\n# } 範例 8-25:使用雜湊映射儲存單字與次數來計算每個字出現的次數 此程式碼會印出 {\"world\": 2, \"hello\": 1, \"wonderful\": 1}。你可能會看到鍵值配對的順序不太一樣,回想一下 「取得雜湊映射的數值」 段落中提過遍歷雜湊映射的順序是任意的。 split_whitespace 方法會遍歷 text 中被空格分開來的切片。or_insert 方法會回傳該鍵對應數值的可變參考(&mut V)。在此我們將可變參考儲存在 count 變數中,所以要賦值的話,我們必須先使用 * 來解參考(dereference)count。可變參考會在 for 結束時離開作用域,所以所有的改變都是安全的且符合借用規則。","breadcrumbs":"常見集合 » 透過雜湊映射儲存鍵值配對 » 更新雜湊映射","id":"148","title":"更新雜湊映射"},"149":{"body":"HashMap 預設是使用一種叫做 SipHash 的雜湊函式(hashing function),這可以透過 [1] 雜湊表(hash table)抵禦阻斷服務(Denial of Service, DoS)的攻擊。這並不是最快的雜湊演算法,但為了提升安全性而犧牲一點效能是值得的。如果你做評測時覺得預設的雜湊函式太慢無法滿足你的需求的話,你可以指定不同的 hasher 來切換成其他雜湊函式。Hasher 是一個有實作 BuildHasher 特徵的型別。我們會在第十章討論到特徵以及如何實作它們。你不必從頭自己實作一個 hasher, crates.io 上有其他 Rust 使用者分享的函式庫,其中就有不少提供許多常見雜湊演算法的 hasher 實作。 https://en.wikipedia.org/wiki/SipHash","breadcrumbs":"常見集合 » 透過雜湊映射儲存鍵值配對 » 雜湊函式","id":"149","title":"雜湊函式"},"15":{"body":"如果你使用的是 Linux 或 macOS,請開啟終端機然後輸入以下命令: $ curl --proto '=https' --tlsv1.3 https://sh.rustup.rs -sSf | sh 這道命令會下載一支腳本然後開始安裝 rustup 工具,接著安裝最新的穩定版 Rust。下載過程中可能會要求你輸入你的密碼。如果下載成功的話,將會出現以下內容: Rust is installed now. Great! 你還會需要一個 連結器(linker) 來讓 Rust 將編譯好的輸出資料整理到一個檔案內。通常你很可能已經有安裝了,但如果你遇到連結器相關的錯誤時,這代表你需要安裝一個 C 編譯器,因爲它通常都會帶有一個的連結器。有個 C 編譯器通常也很實用,因爲一些常見的 Rust 套件也會依賴於 C 而需要一個 C 編譯器。 在 macOS 上,你可以輸入以下命令來安裝 C 編譯器: $ xcode-select --install Linux 使用者的話則需要依據他們的發行版文件來安裝 GCC 或 Clang。舉例來說,如果你使用 Ubuntu 的話,你可以安裝 build-essential 套件。","breadcrumbs":"開始入門 » 安裝教學 » 在 Linux 或 macOS 上安裝 rustup","id":"15","title":"在 Linux 或 macOS 上安裝 rustup"},"150":{"body":"當你的程式需要儲存、取得、修改資料時,向量、字串與雜湊映射可以提供大量的功能。以下是一些你應該能夠解決的練習題: 給予一個整數列表,請使用向量並回傳中位數(排序列表後正中間的值)以及眾數(出現最多次的值,雜湊映射在此應該會很有用)。 將字串轉換成 pig latin。每個單字的第一個字母為子音的話,就將該字母移到單字後方,並加上「ay」,所以「first」會變成「irst-fay」。而單字第一個字母為母音的話,就在單字後方加上「hay」,所以「apple」會變成「apple-hay」。請注意要考慮到 UTF-8 編碼! 使用雜湊映射與向量來建立文字介面,讓使用者能新增員工名字到公司內的一個部門。舉來來說「將莎莉加入工程部門」或「將阿米爾加入業務部門」。然後讓使用者可以索取一個部門所有的員工列表,或是依據部門用字典順序排序,取得公司內所有的員工。 標準函式庫的 API 技術文件有詳細介紹向量、字串與雜湊映射的所有方法,這對於這些練習題應該會很有幫助! 我們現在已經開始遇到有可能會運作失敗的複雜程式了,所以接下來正是來討論錯誤處理的時候!","breadcrumbs":"常見集合 » 透過雜湊映射儲存鍵值配對 » 總結","id":"150","title":"總結"},"151":{"body":"錯誤是軟體開發中不可避免的一環,所以 Rust 有一些特色能夠處理發生錯誤的情形。在許多情況下,Rust 要求你要能知道可能出錯的地方,並在編譯前採取行動。這樣的要求能讓你的程式更穩定,確保你能發現錯誤並在程式碼發佈到生產環境前妥善處理它們! Rust 將錯誤分成兩大類: 可復原的 (recoverable)和 不可復原的 (unrecoverable)錯誤。像是 找不到檔案 這種可復原的錯誤,我們通常很可能只想回報問題給使用者並重試。而不可復原的錯誤就會是程式錯誤的跡象,像是嘗試取得陣列結尾之後的位置。 許多語言不會區分這兩種錯誤,並以相同的方式處理,使用像是例外(exceptions)這樣統一的機制處理。Rust 沒有例外處理機制,取而代之的是它對可復原的錯誤提供 Result 型別,對不可復原的錯誤使用 panic! 將程式停止執行。本章節會先介紹 panic! 再來討論 Result 數值的回傳。除此之外,我們也將探討何時該從錯誤中復原,何時該選擇停止程式。","breadcrumbs":"錯誤處理 » 錯誤處理","id":"151","title":"錯誤處理"},"152":{"body":"有時候壞事就是會發生在你的程式中,這本來就是你沒辦法全部避免的。在這種情況,Rust 有提供 panic! 巨集。在實際情況下我們有兩種方式可以造成恐慌:做出確定會讓程式碼恐慌的動作(像是存取陣列範圍外的元素),或是直接呼叫 panic! 巨集。在這兩種狀況下,我們都對程式造成了恐慌。這些恐慌預設會印出程式出錯的訊息,展開並清理堆疊,然後離開程式。再加上環境變數的話,你還可以讓 Rust 顯示恐慌時呼叫的堆疊,讓你能更簡單地追蹤恐慌的源頭。","breadcrumbs":"錯誤處理 » panic! 與無法復原的錯誤 » 對無法復原的錯誤使用 panic!","id":"152","title":"對無法復原的錯誤使用 panic!"},"153":{"body":"當恐慌(panic)發生時,程式預設會開始做 解開 (unwind)堆疊的動作,這代表 Rust 會回溯整個堆疊,並清理每個它遇到的函式資料。但是這樣回溯並清理的動作很花力氣。另一種方式是直接 終止 (abort)程式而不清理,程式使用的記憶體會需要由作業系統來清理。 如果你需要你的專案產生的執行檔越小越好,你可以從解開切換成終止,只要在 Cargo.toml 檔案中的 [profile] 段落加上 panic = 'abort' 就好。舉例來說,如果你希望在發佈模式(release mode)恐慌時直接終止,那就加上: [profile.release]\npanic = 'abort' 讓我們先在小程式內試試呼叫 panic!: 檔案名稱:src/main.rs fn main() { panic!(\"◢▆▅▄▃ 崩╰(〒皿〒)╯潰▃▄▅▆◣\");\n} 當你執行程式時,你會看到像這樣的結果: $ cargo run Compiling panic v0.1.0 (file:///projects/panic) Finished dev [unoptimized + debuginfo] target(s) in 0.25s Running `target/debug/panic`\nthread 'main' panicked at '◢▆▅▄▃ 崩╰(〒皿〒)╯潰▃▄▅▆◣', src/main.rs:2:5\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace panic! 的呼叫導致印出了最後兩行的錯誤訊息。第一行顯示了我們的恐慌訊息以及該恐慌是在原始碼何處發生的: src/main.rs:2:5 指的是它發生在我們的 src/main.rs 檔案第二行第五個字元。 在此例中,該行指的就是我們寫的程式碼。如果我們查看該行,我們會看到 panic! 巨集的呼叫。在其他情形,panic! 的呼叫可能會發生在我們呼叫的其他程式碼內,所以錯誤訊息回報的檔案名稱與行數可能就會是其他人呼叫 panic! 巨集的程式碼,而不是因為我們的程式碼才導致 panic! 的呼叫。我們可以在呼叫 panic! 程式碼的地方使用 backtrace 來找出出現問題的地方。接下來我們就會深入瞭解 backtrace。","breadcrumbs":"錯誤處理 » panic! 與無法復原的錯誤 » 恐慌時該解開堆疊還是直接終止","id":"153","title":"恐慌時該解開堆疊還是直接終止"},"154":{"body":"讓我們看看另一個例子,這是函式庫發生錯誤而呼叫 panic!,而不是來自於我們在程式碼自己呼叫的巨集。範例 9-1 是個嘗試從向量有效範圍外取得索引的例子。 檔案名稱:src/main.rs fn main() { let v = vec![1, 2, 3]; v[99];\n} 範例 9-1:嘗試取得超出向量長度的元素,進而導致 panic! 被呼叫 我們在這邊嘗試取得向量中第 100 個元素(不過因為索引從零開始,所以是索引 99),但是該向量只有 3 個元素。在此情況下,Rust 就會恐慌。使用 [] 會回傳元素,但是如果你傳遞了無效的索引,Rust 就回傳不了正確的元素。 在 C 中,嘗試讀取資料結構結束之後的元素屬於未定義行為。你可能會得到該記憶體位置對應其資料結構的元素,即使該記憶體完全不屬於該資料結構。這就稱做 緩衝區過讀 (buffer overread)而且會導致安全漏洞。攻擊者可能故意操縱該索引來取得在資料結構後面他們原本不應該讀寫的值。 為了保護你的程式免於這樣的漏洞,如果你嘗試用一個不存在的索引讀取元素的話,Rust 會停止執行並拒絕繼續運作下去。讓我們嘗試執行並看看會如何: $ cargo run Compiling panic v0.1.0 (file:///projects/panic) Finished dev [unoptimized + debuginfo] target(s) in 0.27s Running `target/debug/panic`\nthread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace 此錯誤指向 main.rs 的第四行,也就是我們嘗試存取索引 99 的地方。下一行提示告訴我們可以設置 RUST_BACKTRACE 環境變數來取得 backtrace 以知道錯誤發生時到底發生什麼事。 backtrace 是一個函式列表,指出得到此錯誤時到底依序呼叫了哪些函式。Rust 的 backtraces 運作方式和其他語言一樣:讀取 backtrace 關鍵是從最一開始讀取直到你看到你寫的檔案。那就會是問題發生的源頭。那行以上的行數就是你所呼叫的程式,而以下則是其他呼叫你的程式碼的程式。這些行數可能還會包含 Rust 核心程式碼、標準函式庫程式碼,或是你所使用的 crate。我們設置 RUST_BACKTRACE 環境變數的值不為 0,來嘗試取得 backtrace 吧。你應該會看到和範例 9-2 類似的結果。 $ RUST_BACKTRACE=1 cargo run\nthread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5\nstack backtrace: 0: rust_begin_unwind at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/std/src/panicking.rs:584:5 1: core::panicking::panic_fmt at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:142:14 2: core::panicking::panic_bounds_check at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:84:5 3: >::index at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/slice/index.rs:242:10 4: core::slice::index:: for [T]>::index at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/slice/index.rs:18:9 5: as core::ops::index::Index>::index at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/alloc/src/vec/mod.rs:2591:9 6: panic::main at ./src/main.rs:4:5 7: core::ops::function::FnOnce::call_once at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/ops/function.rs:248:5\nnote: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace. 範例 9-2:當 RUST_BACKTRACE 設置時,透過呼叫 panic! 產生的 backtrace 輸出結果有點多啊!你看到的實際輸出可能會因你的作業系統與 Rust 版本而有所不同。要取得這些資訊的 backtrace,除錯符號(debug symbols)必須啟用。當我們在使用 cargo build 或 cargo run 且沒有加上 --release 時,除錯符號預設是啟用的。 在範例 9-2 的輸出結果中,第 6 行的 backtrace 指向了我們專案中產生問題的地方: src/main.rs 中的第四行。如果我們不想讓程式恐慌,我們就要來調查我們所寫的程式中第一個被錯誤訊息指向的位置。在範例 9-1 中,我們故意寫出會恐慌的程式碼。要修正的方法就是不要索取超出向量索引範圍的元素。當在未來你的程式碼恐慌時,你會需要知道是程式碼中的什麼動作造成的、什麼數值導致恐慌以及正確的程式碼該怎麼處理。 我們會在本章節 「要 panic! 還是不要 panic!」 的段落中再回來看 panic! 並研究何時該與不該使用 panic! 來處理錯誤條件。接下來,我們要看如何使用 Result來處理可回復的錯誤。","breadcrumbs":"錯誤處理 » panic! 與無法復原的錯誤 » 使用 panic! Backtrace","id":"154","title":"使用 panic! Backtrace"},"155":{"body":"大多數的錯誤沒有嚴重到需要讓整個程式停止執行。有時候當函式失敗時,你是可以輕易理解並作出反應的。舉例來說,如果你嘗試開啟一個檔案,但該動作卻因為沒有該檔案而失敗的話,你可能會想要建立檔案,而不是終止程序。 回憶一下第二章的 「使用 Result 型別可能的錯誤」 提到 Result 列舉的定義有兩個變體 Ok 和 Err,如以下所示: enum Result { Ok(T), Err(E),\n} T 和 E 是泛型型別參數,我們會在第十章深入討論泛型。你現在需要知道的是 T 代表我們在成功時會在 Ok 變體回傳的型別,而 E 則代表失敗時在 Err 變體會回傳的錯誤型別。因為 Result 有這些泛型型別參數,我們可以將 Result 型別和它的函式用在許多不同場合,讓成功與失敗時回傳的型別不相同。 讓我們呼叫一個可能會失敗的函式並回傳 Result 型別。在範例 9-3 我們嘗試開啟一個檔案。 檔案名稱:src/main.rs use std::fs::File; fn main() { let greeting_file_result = File::open(\"hello.txt\");\n} 範例 9-3:嘗試開啟一個檔案 File::open 的回傳型別為 Result。泛型參數 T 在此已經被 File::open 指明成功時會用到的型別 std::fs::File,也就是檔案的控制代碼(handle)。用於錯誤時的 E 型別則是 std::io::Error。這樣的回傳型別代表 File::open 的呼叫在成功時會回傳我們可以讀寫的檔案控制代碼,但該函式呼叫也可能失敗。舉例來說,該檔案可能會不存在,或者我們沒有檔案的存取權限。File::open 需要有某種方式能告訴我們它的結果是成功或失敗,並回傳檔案控制代碼或是錯誤資訊。這樣的資訊正是 Result 列舉想表達的。 如果 File::open 成功的話,變數 greeting_file_result 的數值就會獲得包含檔案控制代碼的 Ok 實例。如果失敗的話,greeting_file_result 的值就會是包含為何產生該錯誤的資訊的 Err 實例。 我們需要讓範例 9-3 的程式碼依據 File::open 回傳不同的結果採取不同的動作。範例 9-4 展示了其中一種處理 Result 的方式,我們使用第六章提到的 match 表達式。 檔案名稱:src/main.rs use std::fs::File; fn main() { let greeting_file_result = File::open(\"hello.txt\"); let greeting_file = match greeting_file_result { Ok(file) => file, Err(error) => panic!(\"開啟檔案時發生問題:{:?}\", error), };\n} 範例 9-4:使用 match 表達式來處理回傳的 Result 變體 和 Option 列舉一樣,Result 列舉與其變體都會透過 prelude 引入作用域,所以我們不需要指明 Result::,可以直接在 match 的分支中使用 Ok 和 Err 變體。 當結果是 Ok 時,這裡的程式碼就會回傳 Ok 變體中內部的 file,然後我們就可以將檔案控制代碼賦值給變數 greeting_file。在 match 之後,我們就可以使用檔案控制代碼來讀寫。 match 的另一個分支則負責處理我們從 File::open 中取得的 Err 數值。在此範例中,我們選擇呼叫 panic! 巨集。如果檔案 hello.txt 不存在我們當前的目錄的話,我們就會執行此程式碼,接著就會看到來自 panic! 巨集的輸出結果: $ cargo run Compiling error-handling v0.1.0 (file:///projects/error-handling) Finished dev [unoptimized + debuginfo] target(s) in 0.73s Running `target/debug/error-handling`\nthread 'main' panicked at '開啟檔案時發生問題:Os { code: 2, kind: NotFound, message: \"No such file or directory\" }', src/main.rs:8:23\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace 如往常一樣,此輸出告訴我們哪裡出錯了。","breadcrumbs":"錯誤處理 » Result 與可復原的錯誤 » Result 與可復原的錯誤","id":"155","title":"Result 與可復原的錯誤"},"156":{"body":"範例 9-4 的程式碼不管 File::open 為何失敗都會呼叫 panic!。不過我們想要依據不同的錯誤原因採取不同的動作,如果 File::open 是因為檔案不存在的話,我們想要建立檔案並回傳新檔案的控制代碼。如果 File::open 是因為其他原因失敗的話,像是我們沒有開啟檔案的權限,我們仍然要像範例 9-4 這樣呼叫 panic!。對此我們可以加上 match 表達式,如範例 9-5 所示。 檔案名稱:src/main.rs use std::fs::File;\nuse std::io::ErrorKind; fn main() { let greeting_file_result = File::open(\"hello.txt\"); let greeting_file = match greeting_file_result { Ok(file) => file, Err(error) => match error.kind() { ErrorKind::NotFound => match File::create(\"hello.txt\") { Ok(fc) => fc, Err(e) => panic!(\"建立檔案時發生問題:{:?}\", e), }, other_error => { panic!(\"開啟檔案時發生問題:{:?}\", other_error); } }, };\n} 範例 9-5:針對不同種類的錯誤採取不同動作 File::open 在 Err 變體的回傳型別為 io::Error,這是標準函式庫提供的結構體。此結構體有個 kind 方法讓我們可以取得 io::ErrorKind 數值。標準函式庫提供的列舉 io::ErrorKind 有從 io 運算可能發生的各種錯誤。我們想處理的變體是 ErrorKind::NotFound,這指的是我們嘗試開啟的檔案還不存在。所以我們對 greeting_file_result 配對並在用 error.kind() 繼續配對下去。 我們從內部配對檢查 error.kind() 的回傳值是否是 ErrorKind 列舉中的 NotFound 變體。如果是的話,我們就嘗試使用 File::create 建立檔案。不過 File::create 也可能會失敗,所以我們需要第二個內部 match 表達式來處理。如果檔案無法建立的話,我們就會印出不同的錯誤訊息。第二個分支的外部 match 分支保持不變,如果程式遇到其他錯誤的話就會恐慌。","breadcrumbs":"錯誤處理 » Result 與可復原的錯誤 » 配對不同種的錯誤","id":"156","title":"配對不同種的錯誤"},"157":{"body":"我們用的 match 的確有點多!match 表達式雖然很實用,不過它的行為非常基本。在第十三章你會學到閉包(closure),Result 型別有很多接收閉包的方法。使用這些方法可以讓你的程式碼更簡潔。 舉例來說,以下是另一種能寫出與範例 9-5 邏輯相同的程式碼,這次則是使用到閉包與 unwrap_or_else 方法: use std::fs::File;\nuse std::io::ErrorKind; fn main() { let greeting_file = File::open(\"hello.txt\").unwrap_or_else(|error| { if error.kind() == ErrorKind::NotFound { File::create(\"hello.txt\").unwrap_or_else(|error| { panic!(\"建立檔案時發生問題:{:?}\", error); }) } else { panic!(\"開啟檔案時發生問題:{:?}\", error); } });\n} 雖然此程式碼的行為和範例 9-5 一樣,但他沒有包含任何 match 表達式而且更易閱讀。當你讀完第十三章後,別忘了回來看看此範例,並查閱標準函式庫中的 unwrap_or_else 方法。除此方法以外,還有更多方法可以來解決處理錯誤時龐大的 match 表達式。","breadcrumbs":"錯誤處理 » Result 與可復原的錯誤 » 除了使用 match 配對 Result 以外的方式","id":"157","title":"除了使用 match 配對 Result 以外的方式"},"158":{"body":"雖然 match 已經足以勝任指派的任務了,但它還是有點冗長,而且可能無法正確傳遞錯誤的嚴重性。Result 型別有非常多的輔助方法來執行不同的特定任務。unwrap 就和我們在範例 9-4 所寫的 match 表達式一樣,擁有類似效果的捷徑方法。如果 Result 的值是 Ok 變體,unwrap會回傳 Ok 裡面的值;如果 Result 是 Err 變體的話,unwrap 會呼叫 panic! 巨集。以下是使用 unwrap 的方式: 檔案名稱:src/main.rs use std::fs::File; fn main() { let greeting_file = File::open(\"hello.txt\").unwrap();\n} 如果我們沒有 hello.txt 這個檔案並執行此程式碼的話,我們會看到從 unwrap 方法所呼叫的 panic! 回傳訊息: thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os {\ncode: 2, kind: NotFound, message: \"No such file or directory\" }',\nsrc/main.rs:4:49 還有另一個方法 expect 和 unwrap 類似,不過能讓我們選擇 panic! 回傳的錯誤訊息。使用 expect 而非 unwrap 並提供完善的錯誤訊息可以表明你的意圖,讓追蹤恐慌的源頭更容易。expect 的語法看起來就像這樣: 檔案名稱:src/main.rs use std::fs::File; fn main() { let greeting_file = File::open(\"hello.txt\") .expect(\"hello.txt 應該要存在此專案中\");\n} 我們使用 expect 的方式和 unwrap 一樣,不是回傳檔案控制代碼就是呼叫 panic! 巨集。使用 expect 呼叫 panic! 時的錯誤訊息會是我們傳遞給 expect 的參數,而不是像 unwrap 使用 panic! 預設的訊息。訊息看起來就會像這樣: thread 'main' panicked at 'hello.txt 應該要存在此專案中: Os {\ncode: 2, kind: NotFound, message: \"No such file or directory\" }',\nsrc/main.rs:5:10 在正式環境等級的程式碼,大多數 Rustaceans 會選擇 expect 而不是 unwrap,這樣能在出錯時提供更多資訊,告訴我們為何預期該動作永遠成功。這樣一來就算你的假設證明錯誤,你都能夠在除錯時有足夠的資訊來理解。","breadcrumbs":"錯誤處理 » Result 與可復原的錯誤 » 錯誤發生時產生恐慌的捷徑:unwrap 與 expect","id":"158","title":"錯誤發生時產生恐慌的捷徑:unwrap 與 expect"},"159":{"body":"當函式實作呼叫的程式碼可能會失敗時,與其直接在該函式本身處理錯誤,你可以回傳錯誤給呼叫此程式的程式碼,由它們決定如何處理。這稱之為 傳播 (propagating)錯誤並讓呼叫者可以有更多的控制權,因為比起你程式碼當下的內容,回傳的錯誤可能提供更多資訊與邏輯以利處理。 舉例來說,範例 9-6 展示了一個從檔案讀取使用者名稱的函式。如果檔案不存在或無法讀取的話,此函式會回傳該錯誤給呼叫該函式的程式碼。 檔案名稱:src/main.rs use std::fs::File;\nuse std::io::{self, Read}; fn read_username_from_file() -> Result { let username_file_result = File::open(\"hello.txt\"); let mut username_file = match username_file_result { Ok(file) => file, Err(e) => return Err(e), }; let mut username = String::new(); match username_file.read_to_string(&mut username) { Ok(_) => Ok(username), Err(e) => Err(e), }\n} 範例 9-6:使用 match 回傳錯誤給呼叫者的函式 此函式還能再更簡化,但我們要先繼續手動處理來進一步探討錯誤處理,最後我們會展示最精簡的方式。讓我們先看看此函式的回傳型別 Result。這代表此函式回傳的型別為 Result,其中泛型型別 T 已經指明為實際型別 String,而泛型型別 E 則指明為實際型別 io::Error。 如果函式正確無誤的話,程式碼會呼叫此函式並收到擁有 String 的 Ok 數值。如果程式遇到任何問題的話,呼叫此函式的程式碼就會獲得擁有包含相關問題發生資訊的 io::Error 實例的 Err 數值。我們選擇 io::Error 作為函式的回傳值是因為它正是 File::open 函式和 read_to_string 方法失敗時的回傳的錯誤型別。 函式本體從呼叫 File::open 開始,然後我們使用 match 回傳 Result 數值,就和範例 9-4 的 match 類似。如果 File::open 成功的話,變數 file 中的檔案控制代碼賦值給可變變數 username_file 並讓函式繼續執行下去。但在 Err 的情形時,與其呼叫 panic!,我們使用 return 關鍵字來讓函式提早回傳,並將 File::open 的錯誤值,也就是模式中的變數 e,作為此函式的錯誤值回傳給呼叫的程式碼。 所以如果我們在 username_file 有拿到檔案控制代碼的話,接著函式就會在變數 username 建立新的 String 並對檔案控制代碼 username_file 呼叫 read_to_string 方法來讀取檔案內容至 username。read_to_string 也會回傳 Result 因為它也可能失敗,就算 File::open 是執行成功的。所以我們需要另一個 match 來處理該 Result,如果 read_to_string 成功的話,我們的函式就是成功的,然後在 Ok 回傳 username 中該檔案的使用者名稱。如果 read_to_string 失敗的話,我們就像處理 File::open 的 match 一樣回傳錯誤值。不過我們不需要顯式寫出 return,因為這是函式中的最後一個表達式。 呼叫此程式碼的程式就會需要處理包含使用者名稱的 Ok 數值以及包含 io::Error 的 Err 數值。這交給呼叫的程式碼來決定如何處理這些數值。舉例來說,如果呼叫此程式碼而獲得錯誤的話,它可能選擇呼叫 panic! 讓程式崩潰,或者使用預設的使用者名稱從檔案以外的地方尋找該使用者。所以我們傳播所有成功或錯誤的資訊給呼叫者,讓它們能妥善處理。 這樣傳播錯誤的模式是非常常見的,所以 Rust 提供了 ? 來簡化流程。 傳播錯誤的捷徑:? 運算子 範例 9-7 是另一個 read_username_from_file的實作,擁有和範例 9-6 一樣的效果,不過這次使用了 ? 運算子。 檔案名稱:src/main.rs use std::fs::File;\nuse std::io::{self, Read}; fn read_username_from_file() -> Result { let mut username_file = File::open(\"hello.txt\")?; let mut username = String::new(); username_file.read_to_string(&mut username)?; Ok(username)\n} 範例 9-7:使用 ? 運算子回傳錯誤給呼叫者的函式 定義在 Result 數值後的 ? 運作方式幾乎與範例 9-6 的 match 表達式處理 Result 的方式一樣。如果 Result 的數值是 Ok 的話,Ok 內的數值就會從此表達式回傳,然後程式就會繼續執行。如果數值是 Err 的話,Err 就會使用 return 關鍵字作為整個函式的回傳值回傳,讓錯誤數值可以傳遞給呼叫者的程式碼。 不過範例 9-6 的 match 表達式做的事和 ? 運算子做的事還是有不同的地方:? 運算子呼叫所使用的錯誤數值會傳遞到 from 函式中,這是定義在標準函式庫的 From 特徵中,用來將數值從一種型別轉換另一種型別。當 ? 運算子呼叫 from 函式時,接收到的錯誤型別會轉換成目前函式回傳值的錯誤型別。這在當函式要回傳一個錯誤型別來代表所有函式可能的失敗是很有用的,即使可能會失敗的原因有很多種。 舉例來說,我們可以將範例 9-7 的函式 read_username_from_file 改成回傳一個我們自訂的錯誤型別叫做 OurError。如果我們有定義 impl From for OurError 能從 io::Error 建立一個 OurError 實例的話,那麼 read_username_from_file 本體中的 ? 運算就會呼叫 from 然後轉換錯誤型別,不必在函式那多加任何程式碼。 在範例 9-7 中,在 File::open 的結尾中 ? 回傳 Ok 中的數值給變數 username_file。如果有錯誤發生時,? 運算子會提早回傳整個函式並將 Err 的數值傳給呼叫的程式碼。同理也適用在呼叫 read_to_string 結尾的 ?。 ? 運算子可以消除大量樣板程式碼並讓函式實作更簡單。我們還可以再進一步將方法直接串接到 ? 後來簡化程式碼,如範例 9-8 所示。 檔案名稱:src/main.rs use std::fs::File;\nuse std::io::{self, Read}; fn read_username_from_file() -> Result { let mut username = String::new(); File::open(\"hello.txt\")?.read_to_string(&mut username)?; Ok(username)\n} 範例 9-8:在 ? 運算子後方串接方法呼叫 我們將建立新 String 的變數 username 移到函式的開頭,這部分沒有任何改變。再來與建立變數 username_file 的地方不同的是,我們直接將 read_to_string 串接到 File::open(\"hello.txt\")? 的結果後方。我們在 read_to_string 呼叫的結尾還是有 ?,然後我們還是在 File::open 和 read_to_string 成功沒有失敗時,回傳包含 username 的 Ok 數值。函式達成的效果仍然與範例 9-6 與 9-7 相同。這只是一個比較不同但慣用的寫法。 說到此函式不同的寫法,範例 9-9 展示了使用 fs::read_to_string 更短的寫法。 檔案名稱:src/main.rs use std::fs;\nuse std::io; fn read_username_from_file() -> Result { fs::read_to_string(\"hello.txt\")\n} 範例 9-9:使用 fs::read_to_string 而不是開啟檔案後才讀取 讀取檔案至字串中算是個常見動作,所以標準函式庫提供了一個方便的函式 fs::read_to_string 來開啟檔案、建立新的 String、讀取檔案內容、將內容放入該 String 並回傳它。不過使用 fs::read_to_string 就沒有機會讓我們來解釋所有的錯誤處理,所以我們一開始才用比較長的寫法。 ? 運算子可以用在哪裡? ? 運算子只能用在有函式的回傳值相容於 ? 使用的值才行。這是因為 ? 運算子會在函式中提早回傳數值,就像我們在範例 9-6 那樣用 match 表達式提早回傳一樣。在範例 9-6 中,match 使用的是 Result 數值,函式的回傳值必須是 Result 才能相容於此 return。 讓我們看看在範例 9-10 的 main 函式中的回傳值要是不相容於我們用在 ? 的型別,如果我們使用 ? 運算子會發生什麼事: use std::fs::File; fn main() { let greeting_file = File::open(\"hello.txt\")?;\n} 檔案名稱:src/main.rs 範例 9-10:嘗試在回傳 () 的 main 函式中使用 ? 會無法編譯 此程式碼會開啟檔案,所以可能會失敗。? 運算子會拿到 File::open 回傳的 Result 數值,但是此 main 函式的回傳值為() 而非 Result。當我們編譯此程式碼時,我們會得到以下錯誤訊息: $ cargo run Compiling error-handling v0.1.0 (file:///projects/error-handling)\nerror[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`) --> src/main.rs:4:48 |\n3 | fn main() { | --------- this function should return `Result` or `Option` to accept `?`\n4 | let greeting_file = File::open(\"hello.txt\")?; | ^ cannot use the `?` operator in a function that returns `()` | = help: the trait `FromResidual>` is not implemented for `()` For more information about this error, try `rustc --explain E0277`.\nerror: could not compile `error-handling` due to previous error 此錯誤告訴我們只能在回傳型別為 Result 或 Option 或其他有實作 FromResidual 的型別的函式才能使用 ? 運算子。 要修正此錯誤的話,你有兩種選擇。其中一種是如果你沒有任何限制的話,你可以將函式回傳值變更成與 ? 運算子相容的型別。另一種則是依照可能的情境使用 match 或 Result 其中一種方法來處理 Result。 錯誤訊息還提到了 ? 也能用在 Option 的數值。就像 ? 能用在 Result一樣,你只能在有回傳 Option 的函式中,對 Option 的值使用 ?。在 Option 呼叫 ? 的行為與在 Result 上呼叫類似:如果數值為 None,None 就會在函式該處被提早回傳;如果數值為 Some,Some 中的值就會是表達式的結果數值,且程式會繼續執行下去。以下範例 9-11 的函式會尋找給予文字的第一行中最後一個字元: fn last_char_of_first_line(text: &str) -> Option { text.lines().next()?.chars().last()\n}\n# # fn main() {\n# assert_eq!(\n# last_char_of_first_line(\"Hello, world\\nHow are you today?\"),\n# Some('d')\n# );\n# # assert_eq!(last_char_of_first_line(\"\"), None);\n# assert_eq!(last_char_of_first_line(\"\\nhi\"), None);\n# } 範例 9-11:Option 的數值上使用在 ? 運算子 此函式會回傳 Option,因為它可能會在此真的找到字元,或者可能根本沒有半個字存在。此程式碼接受引數 text 字串切片,並呼叫它的 lines 方法,這會回傳一個遍歷字串每一行的疊代器。因為此函式想要的是第一行,它對疊代器只呼叫 next 來取得疊代器的第一個數值。如果 text 是空字串的話,這裡 next 的呼叫就會回傳 None。我們這裡就可以使用 ? 來中斷 last_char_of_first_line 並回傳 None。如果 text 不是空字串的話,next 會用 Some 數值來回傳 text 的第一行字串切片。 ? 會取出字串切片,然後我們可以對字串切片呼叫 chars 來取得它的疊代器。我們在意的是第一行的最後一個字元,所以我們呼叫 last 來取得疊代器的最後一個值。這也是個 Option 因為第一行可能是個空字串。如果 text 開頭就換行,但在下一行有字元的話,它可能就會是 \"\\nhi\"。不過如果第一行真的有最後一個字元的話,它就會回傳 Some 變體。在這過程中的 ? 運算子讓我們能簡潔地表達此邏輯,並讓我們能只用一行就能實作出來。如果我們對 Option 無法使用 ? 運算子的話,我們使用更多方法呼叫或 match 表達式才能實作此邏輯。 請注意你可以在有回傳 Result 的函式對 Result 的值使用 ? 運算子,你可以在有回傳 Option 的函式對 Option 的值使用 ? 運算子,但你無法混合使用。? 運算子無法自動轉換 Result 與 Option 之間的值。在這種狀況下會需要顯式表達,Result 的話有提供 ok 方法,Option 的話有提供 ok_or 方法。 目前為止,所有我們使用過的 main 函式都是回傳 ()。main 是個特別的函式,因為它是可執行程式的進入點與出口點,而要讓程式可預期執行的話,它的回傳型別就得要有些限制。 幸運的是 main 也可以回傳 Result<(), E>。範例 9-12 取自範例 9-10,不過我們更改 main 的回傳型別為Result<(), Box>,並在結尾的回傳數值加上 Ok(())。這樣的程式碼是能編譯的: use std::error::Error;\nuse std::fs::File; fn main() -> Result<(), Box> { let greeting_file = File::open(\"hello.txt\")?; Ok(())\n} 範例 9-12:將 main 改成回傳 Result<(), E> 就能允許在 Result 數值上使用 ? 運算子 Box 型別使用了 特徵物件 (trait object)我們會在第十七章的 「允許不同型別數值的特徵物件」 討論到。現在你可以將 Box 視為它是「任何種類的錯誤」。在有 Box 錯誤型別的 main 函式中的 Result 使用 ? 是允許的,因為現在 Err 數值可以被提早回傳。盡管此 main 函式本來只會回傳錯誤型別 std::io::Error,但有了 Box 的話,此簽名就能允許其他錯誤型別加入 main 本體中。 當 main 函式回傳 Result<(), E> 時,如果 main 回傳 Ok(()) 的話,執行檔就會用 0 退出;如果 main 回傳 Err 數值的話,就會用非零數值退出。用 C 語言寫的執行檔在退出時會回傳整數:程式成功退出的話會回傳整數 0,而程式退出錯誤的話則會回傳不是 0 的其他整數。而 Rust 執行檔也遵循相容這項規則。 main 函式可以回傳任何有實作 std::process::Termination 特徵的型別,該特徵包含了一個函式 report 來回傳 ExitCode。你可以查閱標準函式庫技術文件來了解如何對你的型別實作 Termination 特徵。 現在我們已經討論了呼叫 panic! 與回傳 Result 的細節。現在讓我們回到何時該使用何種辦法的主題上吧。","breadcrumbs":"錯誤處理 » Result 與可復原的錯誤 » 傳播錯誤","id":"159","title":"傳播錯誤"},"16":{"body":"在 Windows 上請前往 下載頁面 並依照指示安裝 Rust。在安裝的某個過程中,你將會看到一個訊息要求說你還需要 C++ build tools for Visual Studio 2013 或更新的版本。 要取得 build tools 的話,你需要安裝 Visual Studio 2022 。當你被問到要安裝哪些時,請記得包含: “Desktop Development with C++” The Windows 10 or 11 SDK The English language pack component(以及其他你想選擇的語言包) 本書接下來使用的命令都相容於 cmd.exe 和 PowerShell。如果有特別不同的地方,我們會解釋該怎麼使用。","breadcrumbs":"開始入門 » 安裝教學 » 在 Windows 上安裝 rustup","id":"16","title":"在 Windows 上安裝 rustup"},"160":{"body":"所以你該如何決定何時要呼叫 panic! 還是要回傳 Result 呢?當程式碼恐慌時,就沒有任何恢復的方式。你可以在任何錯誤場合呼叫 panic!,無論是可能或不可能復原的情況。不過這樣你就等於替呼叫你的程式碼的呼叫者做出決定,讓情況變成無法復原的錯誤了。當你選擇回傳 Result 數值,你將決定權交給呼叫者的程式碼。呼叫者可能會選擇符合當下場合的方式嘗試復原錯誤,或者它可以選擇 Err 內的數值是不可恢復的,所以它就呼叫 panic! 讓你原本可恢復的錯誤轉成不可恢復。因此,當你定義可能失敗的函式時預設回傳 Result 是不錯的選擇。 在像是範例、草稿與測試的情況下,程式碼恐慌會比回傳 Result 來得恰當。讓我們來探討為何比較好。然後我們再來討論編譯器無法辨別出不可能失敗,但身為人類的你卻可以的情況。本章節會總結一些通用指導原則來決定何時在函式庫程式碼中恐慌。","breadcrumbs":"錯誤處理 » 要 panic! 還是不要 panic! » 要 panic! 還是不要 panic!","id":"160","title":"要 panic! 還是不要 panic!"},"161":{"body":"當你在寫解釋一些概念的範例時,寫出完善錯誤處理的範例,反而會讓範例變得較不清楚。在範例中,使用像是 unwrap 這樣會恐慌的方法可以被視為是一種要求使用者自行決定如何處理錯誤的表現,因為他們可以依據程式碼執行的方式來修改此方法。 同樣地 unwrap 與 expect 方法也很適用在試做原型,你可以在決定準備開始處理錯誤前使用它們。它們會留下清楚的痕跡,當你準備好要讓程式碼更穩固時,你就能回來修改。 如果有方法在測試內失敗時,你會希望整個測試都失敗,就算該方法不是要測試的功能。因為 panic! 會將測試標記為失敗,所以在此呼叫 unwrap 或 expect 是很正確的。","breadcrumbs":"錯誤處理 » 要 panic! 還是不要 panic! » 範例、程式碼原型與測試","id":"161","title":"範例、程式碼原型與測試"},"162":{"body":"如果你知道一些編譯器不知道的邏輯的話,直接在 Result 呼叫 unwrap 或 expect 來直接取得 Ok 的數值是很有用的。你還是會有個 Result 數值需要做處理,你呼叫的程式碼還是有機會失敗的,就算在你的特定場合中邏輯上是不可能的。如果你能保證在親自審閱程式碼後,你絕對不可能會有 Err 變體的話,那麼呼叫 unwrap 是完全可以接受的。而更好的話,用 expect 說明為何你一定不會遇到 Err 變體。以下範例就是如此: # fn main() { use std::net::IpAddr; let home: IpAddr = \"127.0.0.1\" .parse() .expect(\"寫死的 IP 位址應該要有效\");\n# } 我們傳遞寫死的字串來建立 IpAddr 的實例。我們可以看出 127.0.0.1 是完全合理的 IP 位址,所以這邊我們可以直接 expect。不過使用寫死的合理字串並不會改變 parse 方法的回傳型別,我們還是會取得 Result 數值,編譯器仍然會要我們處理 Result 並認為 Err 變體是有可能發生的。因為編譯器並沒有聰明到可以看出此字串是個有效的 IP 位址。如果 IP 位址的字串是來自使用者輸入而非我們寫死進程式的話,它 的確 有可能會失敗,這時我們就得要認真處理 Result 了。註明該 IP 位址是寫死的能在未來我們想拿掉 expect 並改善錯誤處理時,幫助我們理解需要如何從其他來源處理 IP 位址。","breadcrumbs":"錯誤處理 » 要 panic! 還是不要 panic! » 當你知道的比編譯器還多的時候","id":"162","title":"當你知道的比編譯器還多的時候"},"163":{"body":"當你的程式碼可能會導致嚴重狀態的話,就建議讓你的程式恐慌。這裡的嚴重狀態是指一些假設、保證、協議或不可變性被打破時的狀態,像是當你的程式碼有無效的數值、互相矛盾的數值或缺少數值。另外還加上以下情形: 該嚴重狀態並非預期會發生的,而不是像使用者輸入了錯誤格式這種偶而可能會發生的。 你的程式在此時需要避免這種嚴重狀態,而不是在每一步都處理此問題。 你所使用的型別沒有適合的方式能夠處理此嚴重狀態。我們會在第十七章的 「定義狀態與行為成型別」 段落用範例解釋我們指的是什麼。 如果有人呼叫了你的程式碼卻傳遞了不合理的數值,如果可以的話最好的辦法是回傳個錯誤,這樣函式庫的使用者可以決定在該情況下該如何處理。不過要是繼續執行下去可能會造成危險或不安全的話,最好的辦法是呼叫 panic! 並警告使用函式庫的人他們程式碼錯誤發生的位置,好讓他們在開發時就能修正。同樣地,panic! 也適合用於如果你呼叫了你無法掌控的外部程式碼,然後它回傳了你無法修正的無效狀態。 不過如果失敗是可預期的,回傳 Result 就會比呼叫 panic! 來得好。類似的例子有,語法分析器(parser)收到格式錯誤的資訊,或是 HTTP 請求回傳了一個狀態,告訴你已經達到請求上限了。在這樣的案例,回傳 Result 代表失敗是預期有時會發生的,而且呼叫者必須決定如何處理。 當你的程式碼可能會因為進行運算時輸入無效數值,而造成使用者安危的話,你的程式需要先驗證該數值,如果數值無效的話就要恐慌。這是基於安全原則,嘗試對無效資料做運算的話可能會導致你的程式碼產生漏洞。這也是標準函式庫在你嘗試取得超出界限的記憶體存取會呼叫 panic! 的主要原因。嘗試取得不屬於當前資料結構的記憶體是常見的安全問題。函式通常都會訂下一些 合約(contracts) ,它們的行為只有在輸入資料符合特定要求時才帶有保障。當違反合約時恐慌是十分合理的,因為違反合約就代表這是呼叫者的錯誤,這不是你的程式碼該主動處理的錯誤。事實上,呼叫者也沒有任何合理的理由來復原這樣的錯誤。函式的合約應該要寫在函式的技術文件中解釋,尤其是違反時會恐慌的情況。 然而要在你的函式寫一大堆錯誤檢查有時是很冗長且麻煩的。幸運的是你可以利用 Rust 的型別系統(以及編譯器的型別檢查)來幫你完成檢驗。如果你的函式用特定型別作為參數的話,你就可以認定你的程式邏輯是編輯器已經幫你確保你拿到的數值是有效的。舉例來說,如果你有一個型別而非 Option 的話,你的程式就會預期取得 某個值 而不是 沒拿到值 。你的程式就不必處理 Some 和 None 這兩個變體情形,它只會有一種情況並絕對會拿到數值。要是有人沒有傳遞任何值給你的函式會根本無法編譯,所以你的函式就不需要在執行時做檢查。另一個例子是使用非帶號整數像是 u32 來確保參數不會是負數。","breadcrumbs":"錯誤處理 » 要 panic! 還是不要 panic! » 錯誤處理的指導原則","id":"163","title":"錯誤處理的指導原則"},"164":{"body":"讓我們來試著使用 Rust 的型別系統來進一步確保我們擁有有效數值,並建立自訂型別來驗證。回想一下第二章的猜謎遊戲,我們的程式碼要使用者從 1 到 100 之間猜一個數字。在開始與祕密數字做比較之前,我們從未驗證使用者輸入的值,我們只驗證了它是否為正的。在這種情況帶來的後果還不算嚴重:我們還是會顯示「太大」或「太小」。但是我們可以改善這段來引導使用者輸入有效數值,並在使用者輸入時猜了超出範圍的數字或字母時呈現不同行為。 我們可以將輸入的猜測分析改成 i32 而非 u32 來允許負數,並檢查數字是否在範圍內,如以下所示: # use rand::Rng;\n# use std::cmp::Ordering;\n# use std::io;\n# # fn main() {\n# println!(\"請猜測一個數字!\");\n# # let secret_number = rand::thread_rng().gen_range(1..=100);\n# loop { // --省略-- # println!(\"請輸入你的猜測數字。\");\n# # let mut guess = String::new();\n# # io::stdin()\n# .read_line(&mut guess)\n# .expect(\"讀取行數失敗\");\n# let guess: i32 = match guess.trim().parse() { Ok(num) => num, Err(_) => continue, }; if guess < 1 || guess > 100 { println!(\"祕密數字介於 1 到 100 之間。\"); continue; } match guess.cmp(&secret_number) { // --省略--\n# Ordering::Less => println!(\"太小了!\"),\n# Ordering::Greater => println!(\"太大了!\"),\n# Ordering::Equal => {\n# println!(\"獲勝!\");\n# break;\n# }\n# } }\n# } if 表達式檢查我們的數值是否超出範圍,如果是的話就告訴使用者問題原因,並呼叫 continue 來進行下一次的猜測循環,要求再猜一次。在 if 表達式之後我們就能用已經知道範圍是在 1 到 100 的 guess 與祕密數字做比較。 不過這並非理想解決方案:如果程式必定要求數值一定要是 1 到 100,而且我們有很多函式都有此需求的話,在每個函式都檢查就太囉唆了(而且可能會影響效能)。 對此我們可以建立一個新的型別,並且建立一個驗證產生實例的函式,這樣我們就不必在每個地方都做驗證。這樣一來函式就可以安全地以這個新型別作為簽名,並放心地使用收到的數值。範例 9-13 顯示了定義 Guess 型別的例子,它的 new 函式只會在接收值為 1 到 100 時才會建立 Guess 實例。 pub struct Guess { value: i32,\n} impl Guess { pub fn new(value: i32) -> Guess { if value < 1 || value > 100 { panic!(\"猜測數字必須介於 1 到 100 之間,你輸入的是 {}。\", value); } Guess { value } } pub fn value(&self) -> i32 { self.value }\n} 範例 9-13:只會擁有 1 到 100 的 Guess 型別 首先我們定義了一個結構體叫做 Guess,其欄位叫做 value 並會持有 i32。這就是數字會被儲存的地方。 接著我們實作一個 Guess 的關聯函式叫做 new 來建立 Guess 的值。new 函式定義的參數叫做 value 並擁有型別 i32,且最後會回傳 Guess。函式 new 本體中的程式碼會驗證 value 確保它位於 1 到 100 之間。如果 value 沒有通過驗證,我們呼叫 panic! 來警告呼叫此程式碼的開發者,他們可能有需要修正的程式錯誤,因為使用超出範圍的 value 來建立 Guess 違反了 Guess::new 的合約。Guess::new 會恐慌的情況需要在公開的 API 技術文件中提及。我們會在第十四章討論如何寫出技術文件並在 API 技術文件中指出可能發生 panic! 的情形。如果 value 通過驗證的話,我們就建立一個新的 Guess 並將參數 value 賦值給 value 欄位,最後回傳 Guess。 接著我們實作了個方法叫做 value,它會借用 self 且沒有任何參數,並會回傳 i32。這種方法有時會被稱為 getter ,因為它的目的是從它的欄位中取得一些資料並回傳它。此公開方法是必要的,因為 Guess 結構體中的 value 欄位是私有的。將 Guess 結構體的 value 欄位設為私有是很重要的,這樣就無法直接設置 value ,模組外的程式碼 必須 使用 Guess::new 函式來建立 Guess 的實例,因而確保 Guess 不可能會有沒有經過 Guess::new 函式驗證的 value。 這樣當函式的參數或回傳值只能是數字 1 到 100 的話,它的簽名就能使用或回傳 Guess 而不是 i32,因此就不必在它的本體內做任何額外檢查。","breadcrumbs":"錯誤處理 » 要 panic! 還是不要 panic! » 建立自訂型別來驗證","id":"164","title":"建立自訂型別來驗證"},"165":{"body":"Rust 的錯誤檢查功能的設計旨在協助你寫出可靠的程式碼。panic! 巨集告訴你的程式遇到了它無法處理的狀態,並讓你告訴程序停止,而不是繼續嘗試使用無效或不正確的數值。Result 列舉使用 Rust 的型別系統來指出可能會失敗的運算,並讓你的程式碼有辦法恢復。你可以使用 Result 來告訴使用你的程式碼的呼叫者,他們需要處理可能成功與失敗的情形。在適當的場合使用 panic! 與 Result 能讓你的程式碼在不可避免的問題中更加可靠。 現在你已經看過標準函式庫中 Option 與 Result 使用泛型的優勢了,就讓我們來討論泛型如何運作的,以及你如何在程式碼中使用它們。","breadcrumbs":"錯誤處理 » 要 panic! 還是不要 panic! » 總結","id":"165","title":"總結"},"166":{"body":"每個程式語言都有能夠高效處理概念複製的工具。在 Rust 此工具就是 泛型(generics) :實際型別或其他屬性的抽象替代。我們可以表達泛型的行為,或是它們與其他泛型有何關聯,而不必在編譯與執行程式時知道它們實際上是什麼。 函式也可以接受一些泛型型別參數,而不是實際型別像是 i32 或 String,就像函式有辦法能接收多種未知數值作為參數來執行相同程式碼。事實上我們已經在第六章的 Option、第八章的 Vec 和 HashMap 以及第九章的 Result 使用過泛型了。在本章節,你將會探索如何用泛型定義你自己的型別、函式與方法! 首先我們會先檢視如何提取參數來減少重複的程式碼。接著我們會以相同的技巧使用泛型將兩個只有參數型別不同的函式轉變成泛型函式。我們還會解釋如何在結構體和列舉使用泛型型別。 再來你會學會如何使用 特徵(traits) 來定義共同行為。你可以組合特徵與泛型型別來限制泛型型別只適用在有特定行為的型別,而不是任意型別。 最後我們會來介紹 生命週期(lifetimes) :一種能讓編譯器知道參考如何互相關聯的泛型。生命週期讓我們能提供給編譯器更多關於借用數值的資訊,好讓它在更多情況下可以確保參考是有效的。","breadcrumbs":"泛型型別、特徵與生命週期 » 泛型型別、特徵與生命週期","id":"166","title":"泛型型別、特徵與生命週期"},"167":{"body":"泛型讓我們可以用佔位符(placeholder)替代特定型別,來表示多重型別並減少程式碼的重複性。在我們深入泛型語法之前,讓我們先來看如何不用泛型型別的情況下,用提取函式的方式減少重複的程式碼。之後我們就會用相同的方式來提取泛型函式!和你透過找出重複的程式碼來提取程式一樣,你也將找出重複的函式來轉成泛型。 我們先從範例 10-1 中一支尋找列表中最大數字的小程式開始。 檔案名稱:src/main.rs fn main() { let number_list = vec![34, 50, 25, 100, 65]; let mut largest = &number_list[0]; for number in &number_list { if number > largest { largest = number; } } println!(\"最大數字為 {}\", largest);\n# assert_eq!(*largest, 100);\n} 範例 10-1:在數字列表中尋找最大數字的程式碼 我們儲存整數列表到變數 number_list 並將列表第一個數字的參考放入變數 largest。接著我們遍歷列表中的所有元素,如果目前數字比 largest 內儲存的數字還大的話,就會替代成該變數的參考。不過如果目前數值小於或等於最大值的話,變數就不會被改變,程式會接續檢查列表中的下一個數字。在考慮完列表中的所有數字後,largest 就應該會指向最大數字,在此例就是 100。 現在我們要從兩個不同的數字列表中找到最大值,我們可以重複範例 10-1 的程式碼,然後在程式中兩個不同的地方使用相同的邏輯,如範例 10-2 所示。 檔案名稱:src/main.rs fn main() { let number_list = vec![34, 50, 25, 100, 65]; let mut largest = &number_list[0]; for number in &number_list { if number > largest { largest = number; } } println!(\"最大數字為 {}\", largest); let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8]; let mut largest = &number_list[0]; for number in &number_list { if number > largest { largest = number; } } println!(\"最大數字為 {}\", largest);\n} 範例 10-2:在 兩個 數字列表中尋找最大值 雖然這樣的程式碼能執行,寫出重複的程式碼很囉唆而且容易出錯。我們還得記住每次更新時就得一起更新各個地方。 要去除重複的部分,我們可以建立一層抽象,定義一個可以處理任意整數列表作為參數的函式。這樣的解決辦法讓我們的程式更清晰,而且讓我們能抽象表達出從列表中尋找最大值這樣的概念。 在範例 10-3 我們提取了尋找最大值的程式碼成一個函式叫做 largest。然後我們呼叫函式來尋找範例 10-2 兩個列表中最大的數字。我們還可以在未來對其他任何 i32 的列表使用此函式。 檔案名稱:src/main.rs fn largest(list: &[i32]) -> &i32 { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest\n} fn main() { let number_list = vec![34, 50, 25, 100, 65]; let result = largest(&number_list); println!(\"最大數字為 {}\", result);\n# assert_eq!(*result, 100); let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8]; let result = largest(&number_list); println!(\"最大數字為 {}\", result);\n# assert_eq!(*result, 6000);\n} 範例 10-3:抽象出尋找最大值的概念並用在兩個不同的列表 largest 函式有個參數 list 可以代表我們傳遞給函式的 i32 型別切片。所以當我們呼叫此函式時,程式可以依據我們傳入的特定數值執行。 總結來說,以下是我們將範例 10-2 的程式碼轉換成範例 10-3 的步驟: 找出重複的程式碼。 將重複的程式碼提取置函式本體內,並指定函式簽名輸入與回傳數值。 更新重複使用程式碼的實例,改呼叫我們定義的函式。 接著我們將以相同的步驟使用泛型來減少重複的程式碼。就像函式本體可以抽象出 list 而不用特定數值,泛型允許程式碼執行抽象型別。 舉例來說,假設我們有兩個函式:一個會找出 i32 型別切片中的最大值而另一個會找出 char 型別切片的最大值。我們要如何刪除重複的部分呢?讓我們拭目以待!","breadcrumbs":"泛型型別、特徵與生命週期 » 提取函數來減少重複性","id":"167","title":"提取函數來減少重複性"},"168":{"body":"我們使用泛型(generics)來建立項目的定義,像是函式簽名或結構體,讓我們之後可以使用在不同的實際資料型別。讓我們先看看如何使用泛型定義函式、列舉與方法。然後我們會再來看泛型對程式碼的效能影響如何。","breadcrumbs":"泛型型別、特徵與生命週期 » 泛型資料型別 » 泛型資料型別","id":"168","title":"泛型資料型別"},"169":{"body":"當要使用泛型定義函數時,我們通常會將泛型置於函式簽名中指定參數與回傳值資料型別的位置。這樣做能讓我們的程式碼更具彈性並向呼叫者提供更多功能,同時還能防止重複程式碼。 接續我們 largest 函式的例子,範例 10-4 展示了兩個都在切片上尋找最大值的函式。我們要使用泛型將它們融合成一個函式。 檔案名稱:src/main.rs fn largest_i32(list: &[i32]) -> &i32 { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest\n} fn largest_char(list: &[char]) -> &char { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest\n} fn main() { let number_list = vec![34, 50, 25, 100, 65]; let result = largest_i32(&number_list); println!(\"最大數字為 {}\", result);\n# assert_eq!(*result, 100); let char_list = vec!['y', 'm', 'a', 'q']; let result = largest_char(&char_list); println!(\"最大字元為 {}\", result);\n# assert_eq!(*result, 'y');\n} 範例 10-4:兩個名稱與其簽名中的型別都不同的函式 largest_i32 函式和我們在範例 10-3 提取的函式一樣都是尋找切片中最大的 i32。而 largest_char 函式則尋找切片中最大的 char。函式本體都擁有相同的程式碼,讓我們來開始用泛型型別參數來消除重複的部分,轉變成只有一個函式吧。 要在新定義的函式中參數化型別的話,我們需要為參數型別命名,就和我們在函式中的參數數值所做的一樣。你可以用任何標識符來命名型別參數名稱。但我們習慣上會用 T,因為 Rust 的型別參數名稱都盡量很短,常常只會有一個字母,而且 Rust 對於型別命名的慣用規則是駝峰式大小寫(CamelCase)。所以 T 作為「type」的簡稱是大多數 Rust 程式設計師的選擇。 當我們在函式本體使用參數時,我們必須在簽名中宣告參數名稱,編譯器才能知道該名稱代表什麼。同樣地,當我們要在函式簽名中使用型別參數名稱,我們必須在使用前宣告該型別參數名稱。要定義泛型 largest 函式的話,我們在函式名稱與參數列表之間加上尖括號,其內就是型別名稱的宣告,如以下所示: fn largest(list: &[T]) -> &T { 我們可以這樣理解定義:函式 largest 有泛型型別 T,此函式有一個參數叫做 list,它的型別為數值 T 的切片。largest 函式會回傳與型別 T 相同型別的參考數值。 範例 10-5 顯示了使用泛型資料型別於函式簽名組合出的 largest 函式。此範例還展示了我們如何依序用 i32 和 char 的切片呼叫函式。注意此程式碼尚未能編譯,不過我們會在本章之後修改它。 檔案名稱:src/main.rs fn largest(list: &[T]) -> &T { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest\n} fn main() { let number_list = vec![34, 50, 25, 100, 65]; let result = largest(&number_list); println!(\"最大數字為 {}\", result); let char_list = vec!['y', 'm', 'a', 'q']; let result = largest(&char_list); println!(\"最大字元為 {}\", result);\n} 範例 10-5:使用泛型型別參數的 largest 函式,但現在還不能編譯 如果我們現在就編譯程式碼的話,我們會得到此錯誤: $ cargo run Compiling chapter10 v0.1.0 (file:///projects/chapter10)\nerror[E0369]: binary operation `>` cannot be applied to type `&T` --> src/main.rs:5:17 |\n5 | if item > largest { | ---- ^ ------- &T | | | &T |\nhelp: consider restricting type parameter `T` |\n1 | fn largest(list: &[T]) -> &T { | ++++++++++++++++++++++ For more information about this error, try `rustc --explain E0369`.\nerror: could not compile `chapter10` due to previous error 提示文字中提到了 std::cmp::PartialOrd 這個 特徵(trait) 。我們會在下個段落來討論特徵。現在只需要知道 largest 本體無法適用於所有可能的 T 型別,因為我們想要在本體中比較型別 T 的數值,我們只能在能夠排序的型別中做比較。要能夠比較的話,標準函式庫有提供 std::cmp::PartialOrd 特徵讓你可以針對你的型別來實作(請查閱附錄 C 來瞭解更多此特徵的細節)。照著提示文字的建議,我們限制 T 只對有實作 PartialOrd 的型別有效。這樣此範例就能編譯,因為標準函式庫有對 i32 與 char 實作 PartialOrd。","breadcrumbs":"泛型型別、特徵與生命週期 » 泛型資料型別 » 在函式中定義","id":"169","title":"在函式中定義"},"17":{"body":"想簡單確認你是否有正確安裝 Rust 的話,請開啟 shell 然後輸入此命令: $ rustc --version 你應該會看到已發佈的最新穩定版本號、提交雜湊(hash)以及提交日期如以下格式所示: rustc x.y.z (abcabcabc yyyy-mm-dd) 如果你看到這則訊息代表你成功安裝 Rust 了!如果你沒有看到的話,請如下檢查 Rust 是否在你的 %PATH% 系統變數裡。 在 Windows CMD 中請使用: > echo %PATH% 在 PowerShell 中請使用: > echo $env:Path 在 Linux 和 macOS 的話請使用: $ echo $PATH 如果以上步驟皆正確無誤,但還是無法執行 Rust 的話,你可以前往一些地方尋求協助。例如您可以前往 社群頁面 聯絡其他 Rustaceans(這是我們常用稱呼自己取的暱稱)交談並取得協助。","breadcrumbs":"開始入門 » 安裝教學 » 疑難排除","id":"17","title":"疑難排除"},"170":{"body":"我們一樣能以 <> 語法來對結構體中一或多個欄位使用泛型型別參數。範例 10-6 展示了定義 Point 結構體並讓 x 與 y 可以是任意型別數值。 檔案名稱:src/main.rs struct Point { x: T, y: T,\n} fn main() { let integer = Point { x: 5, y: 10 }; let float = Point { x: 1.0, y: 4.0 };\n} 範例 10-6:Point 結構體的 x 與 y 會有型別 T 的數值 在結構體定義使用泛型的語法與函式定義類似。首先,我們在結構體名稱後方加上尖括號,並在其內宣告型別參數名稱。接著我們能在原本指定實際資料型別的地方,使用泛型型別來定義結構體。 注意到我們使用了一個泛型型別來定義 Point,此定義代表 Point 是某型別 T 下之通用的,而且欄位 x 與 y 擁有 相同 型別,無論最終是何種型別。如果我們用不同的型別數值來建立 Point 實例,我們的程式碼會無法編譯,如範例 10-7 所示。 檔案名稱:src/main.rs struct Point { x: T, y: T,\n} fn main() { let wont_work = Point { x: 5, y: 4.0 };\n} 範例 10-7:欄位 x 與 y 必須是相同型別,因為它們擁有相同的泛型資料型別 T 在此例中,當我們賦值 5 給 x 時,我們讓編譯器知道 Point 實例中的泛型型別 T 會是整數。然後我們將 4.0 賦值給 y,這應該要和 x 有相同型別,所以我們會獲得以下錯誤: $ cargo run Compiling chapter10 v0.1.0 (file:///projects/chapter10)\nerror[E0308]: mismatched types --> src/main.rs:7:38 |\n7 | let wont_work = Point { x: 5, y: 4.0 }; | ^^^ expected integer, found floating-point number For more information about this error, try `rustc --explain E0308`.\nerror: could not compile `chapter10` due to previous error 要將結構體 Point 的 x 與 y 定義成擁有不同型別卻仍然是泛型的話,我們可以使用多個泛型型別參數。舉例來說,在範例 10-8 我們改變了 Point 的定義為擁有兩個泛型型別 T 與 U,x 擁有型別 T 而 y 擁有型別 U。 檔案名稱:src/main.rs struct Point { x: T, y: U,\n} fn main() { let both_integer = Point { x: 5, y: 10 }; let both_float = Point { x: 1.0, y: 4.0 }; let integer_and_float = Point { x: 5, y: 4.0 };\n} 範例 10-8:Point 擁有兩個泛型型別,所以 x 和 y 可以有不同的型別數值 現在這些所有的 Point 實例都是允許的了!你要在定義中使用多少泛型型別參數都沒問題,但用太多的話會讓你的程式碼難以閱讀。如果你發現你的程式碼需要使用大量泛型的話,這通常代表你的程式碼需要重新組織成更小的元件。","breadcrumbs":"泛型型別、特徵與生命週期 » 泛型資料型別 » 在結構體中定義","id":"170","title":"在結構體中定義"},"171":{"body":"如同結構體一樣,我們可以定義列舉讓它們的變體擁有泛型資料型別。讓我們看看我們在第六章標準函式庫提供的 Option 列舉: enum Option { Some(T), None,\n} 此定義現在對你來說應該就說得通了。如同你所看到的 Option 列舉有個泛型型別參數 T 以及兩個變體:Some 擁有型別 T 的數值;而 None 則是不具任何數值的變體。使用 Option 列舉我們可以表達出一個可能擁有的數值這樣的抽象概念。而且因為 Option 是泛型,不管可能的數值型別為何,我們都能使用此抽象。 列舉也能有數個泛型型別。我們在第九章所使用列舉 Result 的定義就是個例子: enum Result { Ok(T), Err(E),\n} Result 列舉有兩個泛型型別 T 和 E 且有兩個變體:Ok 擁有型別 T 的數值;而 Err 擁有型別 E 的數值。這樣的定義讓我們很方便能表達 Result 列舉可能擁有一個成功的數值(回傳型別 T 的數值)或失敗的數值(回傳型別為 E 的錯誤值)。事實上這就是我們在範例 9-3 開啟檔案的方式,當我們成功開啟檔案時的 T 就會是型別 std::fs::File,然後當開啟檔案會發生問題時 E 就會是型別 std::io::Error。 當你發現你的程式碼有許多結構體或列舉都只有儲存的值有所不同時,你可以使用泛型型別來避免重複。","breadcrumbs":"泛型型別、特徵與生命週期 » 泛型資料型別 » 在列舉中定義","id":"171","title":"在列舉中定義"},"172":{"body":"我們可以對結構體或列舉定義方法(如第五章所述)並也可以使用泛型型別來定義。範例 10-9 展示了我們在範例 10-6 定義的結構體 Point 並實作了一個叫做 x 的方法。 檔案名稱:src/main.rs struct Point { x: T, y: T,\n} impl Point { fn x(&self) -> &T { &self.x }\n} fn main() { let p = Point { x: 5, y: 10 }; println!(\"p.x = {}\", p.x());\n} 範例 10-9:在 Point 結構體實作一個方法叫做 x,其會回傳 x 欄位中型別為 T 的參考 我們在這 Point 定義了一個方法叫做 x 並回傳欄位 x 的資料參考。 注意到我們需要在 impl 宣告 T,才有 T 可以用來標明我們在替型別 Point 實作其方法。在 impl 之後宣告泛型型別 T,Rust 可以識別出 Point 尖括號內的型別為泛型型別而非實際型別。我們其實可以選用不同的泛型參數名稱,而不用和結構體定義的泛型參數一樣,不過通常使用相同名稱還是比較常見。無論該泛型型別最終會是何種實際型別,任何方法在有宣告泛型型別的 impl 內,都會被定義成適用於各種型別實例。 當我們在定義方法時,我們也可以對泛型型別加上些限制。舉例來說,我們可以只針對 Point 的實例來實作方法,而非適用於任何泛型型別的 Point 實例。在範例 10-10 我們使用了實例型別 f32 而沒有在 impl 宣告任何型別。 檔案名稱:src/main.rs # struct Point {\n# x: T,\n# y: T,\n# }\n# # impl Point {\n# fn x(&self) -> &T {\n# &self.x\n# }\n# }\n# impl Point { fn distance_from_origin(&self) -> f32 { (self.x.powi(2) + self.y.powi(2)).sqrt() }\n}\n# # fn main() {\n# let p = Point { x: 5, y: 10 };\n# # println!(\"p.x = {}\", p.x());\n# } 範例 10-10:一個只適用於擁有泛型 T 結構體其中的特定實際型別的 impl 區塊 此程式碼代表 Point 會有個方法 distance_from_origin,其他 Point 只要 T 不是型別 f32 的實例都不會定義此方法。此方法測量我們的點距離座標 (0.0, 0.0) 有多遠並使用只有浮點數型別能使用的數學運算。 在結構體定義中的泛型型別參數不會總是和結構體方法簽名中的相同。舉例來說,範例 10-11 在 Point 結構體中使用泛型型別 X1 和 Y1,但在 mixup 方法中就使用 X2 Y2 以便清楚辨別。該方法用 self Point 的 x 值(型別為 X1)與參數傳進來的 Point 的 y 值(型別為 Y2)來建立新的 Point 實例。 檔案名稱:src/main.rs struct Point { x: X1, y: Y1,\n} impl Point { fn mixup(self, other: Point) -> Point { Point { x: self.x, y: other.y, } }\n} fn main() { let p1 = Point { x: 5, y: 10.4 }; let p2 = Point { x: \"Hello\", y: 'c' }; let p3 = p1.mixup(p2); println!(\"p3.x = {}, p3.y = {}\", p3.x, p3.y);\n} 範例 10-11:結構體定義中使用不同的泛型型別的方法 在 main 中,我們定義了一個 Point,其 x 型別為 i32(數值為 5),y 型別為 f64(數值為 10.4)。變數 p2 是個 Point 結構體,x 為字串切片(數值為 \"Hello\"),y 為 char(數值為 c)。在 p1 呼叫 mixup 並加上引數 p2 的話會給我們 p3,它的 x 會有型別 i32,因為 x 來自 p1。而且變數 p3 還會有型別為 char 的 y,因為 y 來自 p2。println! 巨集的呼叫就會顯示 p3.x = 5, p3.y = c。 此例是是為了展示一些泛型參數是透過 impl 宣告而有些則是透過方法定義來取得。泛型參數 X1 和 Y1 是宣告在 impl 之後,因為它們與結構體定義有關聯。泛型參數 X2 和 Y2 則是宣告在 fn mixup 之後,因為它們只與方法定義有關聯。","breadcrumbs":"泛型型別、特徵與生命週期 » 泛型資料型別 » 在方法中定義","id":"172","title":"在方法中定義"},"173":{"body":"你可能會好奇當你使用泛型型別參數會不會有執行時的消耗。好消息是使用泛型型別不會比使用實際型別還來的慢。 Rust 在編譯時對使用泛型的程式碼進行單型化(monomorphization)。 單型化 是個讓泛型程式碼轉換成特定程式碼的過程,在編譯時填入實際的型別。在此過程中,編譯器會做與我們在範例 10-5 建立泛型函式相反的事:編譯器檢查所有泛型程式碼被呼叫的地方,並依據泛型程式碼被呼叫的情況產生實際型別的程式碼。 讓我們看看這在標準函式庫的泛型列舉 Option 中是怎麼做到的: let integer = Some(5);\nlet float = Some(5.0); 當 Rust 編譯此程式碼時中,他會進行單型化。在此過程中,會讀取 Option 實例中使用的數值並識別出兩種 Option:一種是 i32 而另一種是 f64。接著它就會將 Option 的泛型定義展開為兩種定義 i32 與 f64,以此替換函式定義為特定型別。 單型化的版本看起來會像這樣(編譯器實際使用的名稱會和我們這邊示範的不同): 檔案名稱:src/main.rs enum Option_i32 { Some(i32), None,\n} enum Option_f64 { Some(f64), None,\n} fn main() { let integer = Option_i32::Some(5); let float = Option_f64::Some(5.0);\n} 泛型 Option 會被替換成編譯器定義的特定定義。因為 Rust 會編譯泛型程式碼成個別實例的特定型別,我們使用泛型就不會造成任何執行時消耗。當程式執行時,它就會和我們親自寫重複定義的版本一樣。單型化的過程讓 Rust 的泛型在執行時十分有效率。","breadcrumbs":"泛型型別、特徵與生命週期 » 泛型資料型別 » 使用泛型的程式碼效能","id":"173","title":"使用泛型的程式碼效能"},"174":{"body":"特徵 (trait)會定義特定型別與其他型別共享的功能。我們可以使用特徵定義來抽象出共同行為。我們可以使用 特徵界限 (trait bounds)來指定泛型型別為擁有特定行為的任意型別。 注意:特徵類似於其他語言常稱作 介面 (interfaces)的功能,但還是有些差異。","breadcrumbs":"泛型型別、特徵與生命週期 » 特徵:定義共同行為 » 特徵:定義共同行為","id":"174","title":"特徵:定義共同行為"},"175":{"body":"一個型別的行為包含我們對該型別可以呼叫的方法。如果我們可以對不同型別呼叫相同的方法,這些型別就能定義共同行為了。特徵定義是一個將方法簽名統整起來,來達成一些目的而定義一系列行為的方法。 舉例來說,如果我們有數個結構體各自擁有不同種類與不同數量的文字:結構體 NewsArticle 儲存特定地點的新聞故事,然後 Tweet 則有最多 280 字元的內容,且有個欄位來判斷是全新的推文、轉推或其他推文的回覆。 我們想要建立一個多媒體聚集器函式庫 crate 叫 aggregator 來顯示可能存在 NewsArticle 或 Tweet 實例的資料總結。要達成此目的的話,我們需要每個型別的總結,且我們會呼叫該實例的 summarize 方法來索取總結。範例 10-12 顯示了表達此行為的 Summary 特徵定義。 檔案名稱:src/lib.rs pub trait Summary { fn summarize(&self) -> String;\n} 範例 10-12:Summary 特徵包含 summarize 方法所定義的行為 我們在此使用 trait 關鍵字定義一個特徵,其名稱為 Summary。我們也將特徵宣告成 pub 所以其他會依賴此函式庫的 crate 也能用到此特徵,我們之後會再看到其他範例。在大括號中,我們宣告方法簽名來描述有實作此特徵的型別行為,在此例就是 fn summarize(&self) -> String。 在方法簽名之後,我們並沒有加上大括號提供實作細節,而是使用分號。每個有實作此特徵的型別必須提供其自訂行為的方法本體。編譯器會強制要求任何有 Summary 特徵的型別都要有定義相同簽名的 summarize 方法。 特徵本體中可以有多個方法,每行會有一個方法簽名並都以分號做結尾。","breadcrumbs":"泛型型別、特徵與生命週期 » 特徵:定義共同行為 » 定義特徵","id":"175","title":"定義特徵"},"176":{"body":"現在我們已經用 Summary 特徵定義了所需的方法簽名。我們可以在我們多媒體聚集器的型別中實作它。範例 10-13 顯示了 NewsArticle 結構體實作 Summary 特徵的方式,其使用頭條、作者、位置來建立 summarize 的回傳值。至於結構體 Tweet,我們使用使用者名稱加上整個推文的文字來定義 summarize,因為推文的內容長度已經被限制在 280 個字元以內了。 檔案名稱:src/lib.rs # pub trait Summary {\n# fn summarize(&self) -> String;\n# }\n# pub struct NewsArticle { pub headline: String, pub location: String, pub author: String, pub content: String,\n} impl Summary for NewsArticle { fn summarize(&self) -> String { format!(\"{} {} 著 ({})\", self.headline, self.author, self.location) }\n} pub struct Tweet { pub username: String, pub content: String, pub reply: bool, pub retweet: bool,\n} impl Summary for Tweet { fn summarize(&self) -> String { format!(\"{}: {}\", self.username, self.content) }\n} 範例 10-13:在型別 NewsArticle 與 Tweet 實作 Summary 特徵 為一個型別實作一個特徵類似於實作一般的方法。不同的地方在於在 impl 之後我們加上的是想要實作的特徵,然後在用 for 關鍵字加上我們想要實作特徵的型別名稱。在 impl 的區塊內我們置入該特徵所定義的方法簽名,我們使用大括號並填入方法本體來為對特定型別實作出特徵方法的指定行為。 現在,我們就能像呼叫正常方法一樣,來呼叫 NewsArticle 和 Tweet 實例的方法,如以下所示: 現在函式庫已經對 NewsArticle 和 Tweet 實作 Summary 特徵了,crate 的使用者能像我們平常呼叫方法那樣,對 NewsArticle 和 Tweet 的實例呼叫特徵方法。唯一的不同是使用者必須將特徵也加入作用域中。以下的範例展示執行檔 crate 如何使用我們的 aggregator 函式庫 crate: use aggregator::{self, Summary, Tweet}; fn main() { let tweet = Tweet { username: String::from(\"horse_ebooks\"), content: String::from( \"of course, as you probably already know, people\", ), reply: false, retweet: false, }; println!(\"1 則新推文:{}\", tweet.summarize());\n} 此程式碼會印出「1 則新推文:horse_ebooks: of course, as you probably already know, people」。 其他依賴 aggregator 函式庫的 crate 也能將 Summary 特徵引入作用域並對他們自己的型別實作 Summary 特徵。不過實作特徵時有一個限制,那就是我們只能在該特徵或該型別位於我們的 crate 時,才能對型別實作特徵。舉例來說,我們可以對自訂型別像是 Tweet 來實作標準函式庫的 Display 特徵來為我們 crate aggregator 增加更多功能。因為 Tweet 位於我們的 aggregator crate 裡面。我們也可以在我們的 crate aggregator 內對 Vec 實作 Summary。因為特徵 Summary 也位於我們的 aggregator crate 裡面。 但是我們無法對外部型別實作外部特徵。舉例來說我們無法在我們的 aggregator crate 裡面對 Vec 實作 Display 特徵。因為 Display 與 Vec 都定義在標準函式庫中,並沒有在我們 aggregator crate 裡面。此限制叫做「連貫性(coherence)」是程式屬性的一部分。更具體來說我們會稱作「孤兒原則(orphan rule)」,因為上一代(parent)型別不存在。此原則能確保其他人的程式碼不會破壞你的程式碼,反之亦然。沒有此原則的話,兩個 crate 可以都對相同型別實作相同特徵,然後 Rust 就會不知道該用哪個實作。","breadcrumbs":"泛型型別、特徵與生命週期 » 特徵:定義共同行為 » 為型別實作特徵","id":"176","title":"為型別實作特徵"},"177":{"body":"有時候對特徵內的一些或所有方法定義預設行為是很實用的,而不必要求每個型別都實作所有方法。然後當我們對特定型別實作特徵時,我們可以保留或覆蓋每個方法的預設行為。 在範例 10-14 我們在 Summary 特徵內指定 summarize 方法的預設字串,而不必像範例 10-12 只定義了方法簽名。 檔案名稱:src/lib.rs pub trait Summary { fn summarize(&self) -> String { String::from(\"(閱讀更多...)\") }\n}\n# # pub struct NewsArticle {\n# pub headline: String,\n# pub location: String,\n# pub author: String,\n# pub content: String,\n# }\n# # impl Summary for NewsArticle {}\n# # pub struct Tweet {\n# pub username: String,\n# pub content: String,\n# pub reply: bool,\n# pub retweet: bool,\n# }\n# # impl Summary for Tweet {\n# fn summarize(&self) -> String {\n# format!(\"{}: {}\", self.username, self.content)\n# }\n# } 範例 10-14:Summary 特徵定義了 summarize 方法的預設實作 要使用預設實作來總結 NewsArticle 的話,我們可以指定一個空的 impl 區塊,像是 impl Summary for NewsArticle {}。 我們沒有直接對 NewsArticle 定義 summarize 方法,因為我們使用的是預設實作並聲明對 NewsArticle 實作 Summary 特徵。所以最後我們仍然能在 NewsArticle 實例中呼叫 summarize,如以下所示: # use aggregator::{self, NewsArticle, Summary};\n# # fn main() { let article = NewsArticle { headline: String::from(\"Penguins win the Stanley Cup Championship!\"), location: String::from(\"Pittsburgh, PA, USA\"), author: String::from(\"Iceburgh\"), content: String::from( \"The Pittsburgh Penguins once again are the best \\ hockey team in the NHL.\", ), }; println!(\"有新文章發佈!{}\", article.summarize());\n# } 此程式碼會印出 有新文章發佈!(閱讀更多...)。 建立預設實作不會影響範例 10-13 中 Tweet 實作的 Summary。因為要取代預設實作的語法,與當沒有預設實作時實作特徵方法的語法是一樣的。 預設實作也能呼叫同特徵中的其他方法,就算那些方法沒有預設實作。這樣一來,特徵就可以提供一堆實用的功能,並要求實作者只需處理一小部分就好。舉例來說,我們可以定義 Summary 特徵,使其擁有一個必須要實作的summarize_author 方法,以及另一個擁有預設實作會呼叫 summarize_author 的方法: pub trait Summary { fn summarize_author(&self) -> String; fn summarize(&self) -> String { format!(\"(從 {} 閱讀更多...)\", self.summarize_author()) }\n}\n# # pub struct Tweet {\n# pub username: String,\n# pub content: String,\n# pub reply: bool,\n# pub retweet: bool,\n# }\n# # impl Summary for Tweet {\n# fn summarize_author(&self) -> String {\n# format!(\"@{}\", self.username)\n# }\n# } 要使用這個版本的 Summary,我們只需要在對型別實作特徵時定義 summarize_author 就好: # pub trait Summary {\n# fn summarize_author(&self) -> String;\n# # fn summarize(&self) -> String {\n# format!(\"(從 {} 閱讀更多...)\", self.summarize_author())\n# }\n# }\n# # pub struct Tweet {\n# pub username: String,\n# pub content: String,\n# pub reply: bool,\n# pub retweet: bool,\n# }\n# impl Summary for Tweet { fn summarize_author(&self) -> String { format!(\"@{}\", self.username) }\n} 在我們定義 summarize_author 之後,我們可以在結構體 Tweet 的實例呼叫 summarize,然後 summarize 的預設實作會呼叫我們提供的 summarize_author。因為我們已經定義了summarize_author,且 Summary 特徵有提供 summarize 方法的預設實作,所以我們不必再寫任何程式碼。 # use aggregator::{self, Summary, Tweet};\n# # fn main() { let tweet = Tweet { username: String::from(\"horse_ebooks\"), content: String::from( \"of course, as you probably already know, people\", ), reply: false, retweet: false, }; println!(\"1 則新推文:{}\", tweet.summarize());\n# } 此程式碼會印出 1 則新推文:(從 @horse_ebooks 閱讀更多...)。 注意要是對相同方法覆寫實作的話,就無法呼叫預設實作。","breadcrumbs":"泛型型別、特徵與生命週期 » 特徵:定義共同行為 » 預設實作","id":"177","title":"預設實作"},"178":{"body":"現在你知道如何定義與實作特徵,我們可以來探討如何使用特徵來定義函式來接受多種不同的型別。我們會使用範例 10-13 中 NewsArticle 與 Tweet 實作的 Summary 特徵,來定義一個函式 notify 使用它自己的參數 item 來呼叫 summarize 方法,所以此參數的型別預期有實作 Summary 特徵。 為此我們可以使用 impl Trait 語法,如以下所示: # pub trait Summary {\n# fn summarize(&self) -> String;\n# }\n# # pub struct NewsArticle {\n# pub headline: String,\n# pub location: String,\n# pub author: String,\n# pub content: String,\n# }\n# # impl Summary for NewsArticle {\n# fn summarize(&self) -> String {\n# format!(\"{} {} 著 ({})\", self.headline, self.author, self.location)\n# }\n# }\n# # pub struct Tweet {\n# pub username: String,\n# pub content: String,\n# pub reply: bool,\n# pub retweet: bool,\n# }\n# # impl Summary for Tweet {\n# fn summarize(&self) -> String {\n# format!(\"{}: {}\", self.username, self.content)\n# }\n# }\n# pub fn notify(item: &impl Summary) { println!(\"頭條新聞!{}\", item.summarize());\n} 與其在 item 參數指定實際型別,我們用的是 impl 關鍵字並加上特徵名稱。這樣此參數就會接受任何有實作指定特徵的型別。在 notify 本體中我們就可以用 item 呼叫 Summary 特徵的任何方法,像是 summarize。我們可以呼叫 notify 並傳遞任何 NewsArticle 或 Tweet 的實例。但如果用其他型別像是 String 或 i32 來呼叫此程式碼的話會無法編譯,因為那些型別沒有實作 Summary。 特徵界限語法 impl Trait 語法看起來很直觀,不過它其實是一個更長格式的語法糖,這個格式稱之為「特徵界限(trait bound)」,它長得會像這樣: pub fn notify(item: &T) { println!(\"頭條新聞!{}\", item.summarize());\n} 此格式等同於之前段落的範例,只是比較長一點。我們將特徵界限置於泛型型別參數的宣告中,在尖括號內接在冒號之後。 impl Trait 語法比較方便,且在簡單的案例中可以讓程式碼比較簡潔;而特徵界限語法則適合用於其他比較複雜的案例。舉例來說我們可以有兩個有實作 Summary 的參數,使用 impl Trait 語法看起來會像這樣: pub fn notify(item1: &impl Summary, item2: &impl Summary) { 如果我們想要此函式允許 item1 和 item2 是不同型別的話,使用 impl Trait 的確是正確的(只要它們都有實作 Summary)。不過如果我們希望兩個參數都是同一型別的話,我們就得使用特徵界限來表達,如以下所示: pub fn notify(item1: &T, item2: &T) { 泛型型別 T 作為 item1 和 item2 的參數會限制函式,讓傳遞給 item1 和 item2 參數的數值型別必須相同。 透過 + 來指定多個特徵界限 我們也可以指定不只一個特徵界限。假設我們還想要 notify 中的 item 不只能夠呼叫 summarize 方法,還能顯示格式化訊息的話,我們可以在 notify 定義中指定 item 必須同時要有 Display 和 Summary。這可以使用 + 語法來達成: pub fn notify(item: &(impl Summary + Display)) { + 也能用在泛型型別的特徵界限中: pub fn notify(item: &T) { 有了這兩個特徵界限,notify 本體就能呼叫 summarize 以及使用 {} 來格式化 item。 透過 where 來使特徵界限更清楚 使用太多特徵界限也會帶來壞處。每個泛型都有自己的特徵界限,所以有數個泛型型別的函式可以在函式名稱與參數列表之間包含大量的特徵界限資訊,讓函式簽名難以閱讀。因此 Rust 有提供另一個在函式簽名之後指定特徵界限的語法 where。所以與其這樣寫: fn some_function(t: &T, u: &U) -> i32 { 我們可以這樣寫 where 的語法,如以下所示: fn some_function(t: &T, u: &U) -> i32\nwhere T: Display + Clone, U: Clone + Debug,\n{\n# unimplemented!()\n# } 此函式簽名就沒有這麼複雜了,函式名稱、參數列表與回傳型別能靠得比較近,就像沒有一堆特徵界限的函式一樣。","breadcrumbs":"泛型型別、特徵與生命週期 » 特徵:定義共同行為 » 特徵作為參數","id":"178","title":"特徵作為參數"},"179":{"body":"我們也能在回傳的位置使用 impl Trait 語法來回傳某個有實作特徵的型別數值,如以下所示: # pub trait Summary {\n# fn summarize(&self) -> String;\n# }\n# # pub struct NewsArticle {\n# pub headline: String,\n# pub location: String,\n# pub author: String,\n# pub content: String,\n# }\n# # impl Summary for NewsArticle {\n# fn summarize(&self) -> String {\n# format!(\"{} {} 著 ({})\", self.headline, self.author, self.location)\n# }\n# }\n# # pub struct Tweet {\n# pub username: String,\n# pub content: String,\n# pub reply: bool,\n# pub retweet: bool,\n# }\n# # impl Summary for Tweet {\n# fn summarize(&self) -> String {\n# format!(\"{}: {}\", self.username, self.content)\n# }\n# }\n# fn returns_summarizable() -> impl Summary { Tweet { username: String::from(\"horse_ebooks\"), content: String::from( \"of course, as you probably already know, people\", ), reply: false, retweet: false, }\n} 將 impl Summary 作為回傳型別的同時,我們在函式 returns_summarizable 指定回傳有實作 Summary 特徵的型別而不必指出實際型別。在此例中,returns_summarizable 回傳 Tweet,但呼叫此函式的程式碼不需要知道。 回傳一個只有指定所需實作特徵的型別在閉包(closures)與疊代器(iterators)中非常有用,我們會在第十三章介紹它們。閉包與疊代器能建立只有編譯器知道的型別,或是太長而難以指定的型別。impl Trait 語法允許你不用寫出很長的型別,而是只要指定函數會回傳有實作 Iterator 特徵的型別就好。 然而如果你使用 impl Trait 的話,你就只能回傳單一型別。舉例來說此程式碼指定回傳型別為 impl Summary ,但是寫說可能會回傳 NewsArticle 或 Tweet 的話就會無法執行: # pub trait Summary {\n# fn summarize(&self) -> String;\n# }\n# # pub struct NewsArticle {\n# pub headline: String,\n# pub location: String,\n# pub author: String,\n# pub content: String,\n# }\n# # impl Summary for NewsArticle {\n# fn summarize(&self) -> String {\n# format!(\"{} {} 著 ({})\", self.headline, self.author, self.location)\n# }\n# }\n# # pub struct Tweet {\n# pub username: String,\n# pub content: String,\n# pub reply: bool,\n# pub retweet: bool,\n# }\n# # impl Summary for Tweet {\n# fn summarize(&self) -> String {\n# format!(\"{}: {}\", self.username, self.content)\n# }\n# }\n# fn returns_summarizable(switch: bool) -> impl Summary { if switch { NewsArticle { headline: String::from( \"Penguins win the Stanley Cup Championship!\", ), location: String::from(\"Pittsburgh, PA, USA\"), author: String::from(\"Iceburgh\"), content: String::from( \"The Pittsburgh Penguins once again are the best \\ hockey team in the NHL.\", ), } } else { Tweet { username: String::from(\"horse_ebooks\"), content: String::from( \"of course, as you probably already know, people\", ), reply: false, retweet: false, } }\n} 寫說可能回傳 NewsArticle 或 Tweet 的話是不被允許的,因為 impl Trait 語法會限制在編譯器中最終決定的型別。我們會在第十七章的 「允許不同型別數值的特徵物件」 來討論如何寫出這種行為的函式。","breadcrumbs":"泛型型別、特徵與生命週期 » 特徵:定義共同行為 » 回傳有實作特徵的型別","id":"179","title":"回傳有實作特徵的型別"},"18":{"body":"當你透過 rustup 安裝完 Rust 後,要更新到最新版本的方法非常簡單。在你的 shell 中執行以下更新腳本即可: $ rustup update 要解除安裝 Rust 與 rustup 的話,則在 shell 輸入以下解除安裝腳本: $ rustup self uninstall","breadcrumbs":"開始入門 » 安裝教學 » 更新與解除安裝","id":"18","title":"更新與解除安裝"},"180":{"body":"在有使用泛型型別參數 impl 區塊中使用特徵界限,我們可以選擇性地對有實作特定特徵的型別來實作方法。舉例來說,範例 10-15 的 Pair 對所有 T 實作了 new 函式來回傳新的 Pair 實例(回想一下第五章的 「定義方法」 段落,Self 是 impl 區塊內的型別別名,在此例就是 Pair)。但在下一個 impl 區塊中,只有在其內部型別 T 有實作能夠做比較的 PartialOrd 特徵 以及 能夠顯示在螢幕的 Display 特徵的話,才會實作 cmp_display 方法。 檔案名稱:src/lib.rs use std::fmt::Display; struct Pair { x: T, y: T,\n} impl Pair { fn new(x: T, y: T) -> Self { Self { x, y } }\n} impl Pair { fn cmp_display(&self) { if self.x >= self.y { println!(\"最大的是 x = {}\", self.x); } else { println!(\"最大的是 y = {}\", self.y); } }\n} 範例 10-15:依據特徵界限來選擇性地在泛型型別實作方法 我們還可以對有實作其他特徵的型別選擇性地來實作特徵。對滿足特徵界限的型別實作特徵會稱之為 全面實作(blanket implementations) ,這被廣泛地用在 Rust 標準函式庫中。舉例來說,標準函式庫會對任何有實作 Display 特徵的型別實作 ToString。標準函式庫中的 impl 區塊會有類似這樣的程式碼: impl ToString for T { // --省略--\n} 因為標準函式庫有此全面實作,我們可以在任何有實作 Display 特徵的型別呼叫 ToString 特徵的 to_string 方法。舉例來說,我們可以像這樣將整數轉變成對應的 String 數值,因為整數有實作 Display: let s = 3.to_string(); 全面實作在特徵技術文件的「Implementors」段落有做說明。 特徵與特徵界限讓我們能使用泛型型別參數來減少重複的程式碼的同時,告訴編譯器該泛型型別該擁有何種行為。編譯器可以利用特徵界限資訊來檢查程式碼提供的實際型別有沒有符合特定行為。在動態語言中,我們要是呼叫一個該型別沒有的方法的話,我們會在執行時才發生錯誤。但是 Rust 將此錯誤移到編譯期間,讓我們必須在程式能夠執行之前確保有修正此問題。除此之外,我們還不用寫在執行時檢查此行為的程式碼,因為我們已經在編譯時就檢查了。這麼做我們可以在不失去泛型彈性的情況下,提升效能。","breadcrumbs":"泛型型別、特徵與生命週期 » 特徵:定義共同行為 » 透過特徵界限來選擇性實作方法","id":"180","title":"透過特徵界限來選擇性實作方法"},"181":{"body":"生命週期(lifetime)是另一種我們已經使用過的泛型。不同於確保一個型別有沒有我們要的行為,生命週期確保我們在需要參考的時候,它們都是有效的。 我們在第四章的 「參考與借用」 段落沒談到的是,Rust 中的每個參考都有個 生命週期 ,這是決定該參考是否有效的作用域。大多情況下生命週期是隱式且可推導出來的,就像大多情況下型別是可推導出來的。當多種型別都有可能時,我們就得詮釋型別。同樣地,當生命週期的參考能以不同方式關聯的話,我們就得詮釋生命週期。Rust 要求我們用泛型生命週期參數來詮釋參考之間的關係,以確保實際在執行時的參考絕對是有效的。 詮釋生命週期在大多數的程式語言中都沒有這個概念,所以這段可能會有點讓你覺得陌生。雖然我們不會在此章涵蓋所有生命週期的內容,但是我們會講些你可能遇到生命週期的常見場景,好讓你更加熟悉這個概念。","breadcrumbs":"泛型型別、特徵與生命週期 » 透過生命週期驗證參考 » 透過生命週期驗證參考","id":"181","title":"透過生命週期驗證參考"},"182":{"body":"生命週期最主要的目的就是要預防 迷途參考 (dangling references),其會導致程式參考到其他資料,而非它原本想要的參考。請看一下範例 10-16 的程式,它有一個外部作用域與內部作用域。 fn main() { let r; { let x = 5; r = &x; } println!(\"r: {}\", r);\n} 範例 10-16:嘗試使用其值已經離開作用域的參考 注意:範例 10-16、10-17 與 10-23 宣告變數時都沒有給予初始數值,所以變數名稱可以存在於外部作用域。乍看之下這似乎違反 Rust 不存在空值的原則。但是如果我們嘗試在賦值前使用變數的話,我們就會獲得編譯期錯誤,這證明 Rust 的確不允許空值。 外部作用域宣告了一個沒有初始值的變數 r,然後內部作用域宣告了一個初始值為 5 的變數 x。在內部作用域中,我們嘗試將 x 的參考賦值給 r。然後內部作用域結束後,我們嘗試印出 r。此程式碼不會編譯成功,因為數值 r 指向的數值在我們嘗試使用它時已經離開作用域。以下是錯誤訊息。 $ cargo run Compiling chapter10 v0.1.0 (file:///projects/chapter10)\nerror[E0597]: `x` does not live long enough --> src/main.rs:6:13 |\n6 | r = &x; | ^^ borrowed value does not live long enough\n7 | } | - `x` dropped here while still borrowed\n8 |\n9 | println!(\"r: {}\", r); | - borrow later used here For more information about this error, try `rustc --explain E0597`.\nerror: could not compile `chapter10` due to previous error 變數 x 「存在的不夠久」。原因是因為當內部作用域在第 7 行結束時,x 會離開作用域。但是 r 卻還在外部作用域中有效,我們會說的「活得比較久」。如果 Rust 允許此程式碼可以執行的話,r 就會參考到 x 離開作用域後被釋放的記憶體位置,然後我們嘗試對 r 做的事情都不會是正確的了。所以 Rust 如何決定此程式碼無效呢?它使用了借用檢查器。","breadcrumbs":"泛型型別、特徵與生命週期 » 透過生命週期驗證參考 » 透過生命週期預防迷途參考","id":"182","title":"透過生命週期預防迷途參考"},"183":{"body":"Rust 編譯器有個 借用檢查器 (borrow checker)會比較作用域來檢測所有的借用是否有效。範例 10-17 顯示了範例 10-16 的程式碼,但加上了變數生命週期的詮釋。 fn main() { let r; // ---------+-- 'a // | { // | let x = 5; // -+-- 'b | r = &x; // | | } // -+ | // | println!(\"r: {}\", r); // |\n} // ---------+ 範例 10-17:變數 r 與 x 的生命週期詮釋,分別以 'a 和 'b 作為表示 我們在此定義 r 的生命週期詮釋為 'a 而 x 的生命週期為 'b。如同你所見,內部的 'b 區塊比外部的 'a 生命週期區塊還小。在編譯期間,Rust 會比較兩個生命週期的大小,並看出 r 有生命週期 'a 但它參考的記憶體有生命週期 'b。程式被回絕的原因是因為 'b 比 'a 還短:被參考的對象比參考者存在的時間還短。 範例 10-18 修正了此程式碼讓它不會存在迷途參考,並能夠正確編譯。 fn main() { let x = 5; // ----------+-- 'b // | let r = &x; // --+-- 'a | // | | println!(\"r: {}\", r); // | | // --+ |\n} // ----------+ 範例 10-18:一個有效參考,因為資料比參考的生命週期還長 x 在此有生命週期 'b,此時它比 'a 還長。這代表 r 可以參考 x,因為 Rust 知道 r 的參考在 x 是有效的時候永遠是有效的。 現在你知道參考的生命週期,以及 Rust 如何分析生命週期以確保參考永遠有效了。讓我們來探索函式中參數與回傳值的泛型生命週期。","breadcrumbs":"泛型型別、特徵與生命週期 » 透過生命週期驗證參考 » 借用檢查器","id":"183","title":"借用檢查器"},"184":{"body":"讓我們寫個回傳兩個字串切片中較長者的函式。此函式會接收兩個字串切片並回傳一個字串切片。在我們實作 longest 函式後,範例 10-19 的程式碼應該要印出 最長的字串為 abcd。 檔案名稱:src/main.rs fn main() { let string1 = String::from(\"abcd\"); let string2 = \"xyz\"; let result = longest(string1.as_str(), string2); println!(\"最長的字串為 {}\", result);\n} 範例 10-19:main 函式呼叫 longest 函式來找出兩個字串切片中較長的 注意我們想要函式接收的是字串切片的參考,而不是字串,因為我們不希望 longest 函式會取得它參數的所有權。第四章的 「字串切片作為參數」 段落有提到為何範例 10-19 的參數正是我們所想要使用的參數。 如果我們嘗試實作 longest 函式時,如範例 10-20 所示,它不會編譯過。 檔案名稱:src/main.rs # fn main() {\n# let string1 = String::from(\"abcd\");\n# let string2 = \"xyz\";\n# # let result = longest(string1.as_str(), string2);\n# println!(\"最長的字串為 {}\", result);\n# }\n# fn longest(x: &str, y: &str) -> &str { if x.len() > y.len() { x } else { y }\n} 範例 10-20:回傳兩個字串中較長者的 longest 函式實作,不過無法編譯成功 我們會看到以下關於生命週期的錯誤: $ cargo run Compiling chapter10 v0.1.0 (file:///projects/chapter10)\nerror[E0106]: missing lifetime specifier --> src/main.rs:9:33 |\n9 | fn longest(x: &str, y: &str) -> &str { | ---- ---- ^ expected named lifetime parameter | = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`\nhelp: consider introducing a named lifetime parameter |\n9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { | ++++ ++ ++ ++ For more information about this error, try `rustc --explain E0106`.\nerror: could not compile `chapter10` due to previous error 提示文字表示回傳型別需要有一個泛型生命週期參數,因為 Rust 無法辨別出回傳的參考指的是 x 還是 y。事實上,我們也不知道,因為函式本體中的 if 區塊會回傳 x 的參考而 else 區塊會回傳 y 的參考! 當我們定義函式時,我們不知道傳遞進此函式的實際數值會是什麼,所以我們不知道到底是 if 或 else 的區塊會被執行。我們也不知道傳遞進來的參考實際的生命週期為何,所以我們無法像範例 10-17 和 10-18 那樣觀察作用域,來判定我們回傳的參考會永遠有效。要修正此錯誤,我們要加上泛型生命週期參數來定義參考之間的關係,讓借用檢查器能夠進行分析。","breadcrumbs":"泛型型別、特徵與生命週期 » 透過生命週期驗證參考 » 函式中的泛型生命週期","id":"184","title":"函式中的泛型生命週期"},"185":{"body":"生命週期詮釋(Lifetime Annotation)不會改變參考能存活多久,它們僅描述了數個參考的生命週期之間互相的關係,而不會影響其生命週期。就像當函式簽名指定了一個泛型型別參數時,函式便能夠接受任意型別一樣。函式可以指定一個泛型生命週期參數,這樣函式就能接受任何生命週期。 生命週期詮釋的語法有一點不一樣:生命週期參數的名稱必須以撇號(')作為開頭,通常全是小寫且很短,就像泛型型別一樣。大多數的人會使用名稱 'a 作為第一個生命週期詮釋。我們將生命週期參數置於參考的 & 之後,並使用空格區隔詮釋與參考的型別。 以下是一些例子:沒有生命週期參數的 i32 參考、有生命週期 'a 的 i32 參考以及有生命週期 'a 的 i32 可變參考。 &i32 // 一個參考\n&'a i32 // 一個有顯式生命週期的參考\n&'a mut i32 // 一個有顯式生命週期的可變參考 只有自己一個生命週期本身沒有多少意義,因為該詮釋是為了告訴 Rust 數個參考的泛型生命週期參數之間互相的關係。讓我們來研究生命週期詮釋如何在 longest 函式中相互關聯吧。","breadcrumbs":"泛型型別、特徵與生命週期 » 透過生命週期驗證參考 » 生命週期詮釋語法","id":"185","title":"生命週期詮釋語法"},"186":{"body":"要在函式簽名使用生命週期詮釋的話,我們需要在函式名稱與參數列表之間的尖括號內宣告泛型 生命週期 參數,就像泛型 型別 參數那樣。 我們想在此簽名表達這樣的限制:只要所有參數都要是有效的,那麼回傳的參考才也會是有效的。也就是參數的生命週期與回傳參考的生命週期是相關的。我們會將生命週期命名為 'a 然後將它加到每個參考,如範例 10-21 所示。 檔案名稱:src/main.rs # fn main() {\n# let string1 = String::from(\"abcd\");\n# let string2 = \"xyz\";\n# # let result = longest(string1.as_str(), string2);\n# println!(\"最長的字串為 {}\", result);\n# }\n# fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y }\n} 範例 10-21:longest 函式定義指定所有簽名中的參考必須有相同的生命週期 'a 此程式碼能夠編譯成功並產生我們希望在範例 10-19 的 main 函式中得到的結果。 此函式簽名告訴 Rust 它有個生命週期 'a,函式的兩個參數都是字串切片,並且會有生命週期'a。此函式簽名還告訴了 Rust 從函式回傳的字串切片也會和生命週期 'a 存活的一樣久。實際上它代表 longest 函式回傳參考的生命週期與函式引數傳入時生命週期較短的參考的生命週期一樣。這樣的關係正是我們想讓 Rust 知道以便分析這段程式碼。 注意當我們在此函式簽名指定生命週期參數時,我們不會變更任何傳入或傳出數值的生命週期。我們只是告訴借用檢查器應該要拒絕任何沒有服從這些約束的數值。注意到 longest 函式不需要知道 x 和 y 實際上會活多久,只需要知道有某個作用域會用 'a 取代來滿足此簽名。 當要在函式詮釋生命週期時,詮釋會位於函式簽名中,而不是函式本體。就像型別會寫在簽名中一樣,生命週期詮釋會成為函式的一部份。在函式簽名加上生命週期能讓 Rust 編譯器的分析工作變得更輕鬆。如果當函式的詮釋或呼叫的方式出問題時,編譯器錯誤就能限縮到我們的程式碼中指出來。如果都改讓 Rust 編譯器去推導可能的生命週期關係的話,編譯器可能會指到程式碼真正出錯之後的好幾步之後。 當我們向 longest 傳入實際參考時,'a 實際替代的生命週期為 x 作用域與 y 作用域重疊的部分。換句話說,泛型生命週期 'a 取得的生命週期會等於 x 與 y 的生命週期中較短的。因為我們將回傳的參考詮釋了相同的生命週期參數 'a,回傳參考的生命週期也會保證在 x 和 y 的生命週期較短的結束前有效。 讓我們來看看如何透過傳入不同實際生命週期的參考來使生命週期詮釋能約束 longest 函式,如範例 10-22 所示。 檔案名稱:src/main.rs fn main() { let string1 = String::from(\"很長的長字串\"); { let string2 = String::from(\"xyz\"); let result = longest(string1.as_str(), string2.as_str()); println!(\"最長的字串為 {}\", result); }\n}\n# # fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {\n# if x.len() > y.len() {\n# x\n# } else {\n# y\n# }\n# } 範例 10-22 使用 longest 函式並傳入 String 數值的參考,但兩個參數的實際生命週期均不相同 在此例中 string1 在外部作用域結束前都有效,而 string2 在內部作用域結束前都有效,然後 result 會取得某個有效參考直到內部作用域結束為止。執行此程式的話,你會看到借用檢查器認可此程式碼,它會編譯成功然後印出 最長的字串為 很長的長字串。 接下來,讓我們寫一個範例能要求 result 生命週期的參考必須是兩個引數中較短的才行。我們會移動變數 result 的宣告到外部作用域,但保留變數 result 的賦值與 string2 一樣在內部作用域。然後我們也將使用到 result 的 println! 移到外部作用域,緊接在內部作用域結束之後。如範例 10-23 所示,此程式碼會編譯不過。 檔案名稱:src/main.rs fn main() { let string1 = String::from(\"很長的長字串\"); let result; { let string2 = String::from(\"xyz\"); result = longest(string1.as_str(), string2.as_str()); } println!(\"最長的字串為 {}\", result);\n}\n# # fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {\n# if x.len() > y.len() {\n# x\n# } else {\n# y\n# }\n# } 範例 10-23:嘗試在 string2 離開作用域後使用 result 當我們嘗試編譯此程式碼,我們會看到以下錯誤: $ cargo run Compiling chapter10 v0.1.0 (file:///projects/chapter10)\nerror[E0597]: `string2` does not live long enough --> src/main.rs:6:44 |\n6 | result = longest(string1.as_str(), string2.as_str()); | ^^^^^^^^^^^^^^^^ borrowed value does not live long enough\n7 | } | - `string2` dropped here while still borrowed\n8 | println!(\"最長的字串為 {}\", result); | ------ borrow later used here For more information about this error, try `rustc --explain E0597`.\nerror: could not compile `chapter10` due to previous error 錯誤訊息表示要讓 result 在 println! 陳述式有效的話,string2 必須在外部作用域結束前都是有效的。Rust 會知道是因為我們在函式的參數與回傳值使用相同的生命週期 'a 來詮釋。 身為人類我們能看出此程式碼的 string1 字串長度的確比 string2 長,因此 result 會包含 string1 的參考。因為 string1 尚未離開作用域,所以 string1 的參考在 println! 陳述式中仍然是有效的才對。然而編譯器在此情形會無法看出參考是有效的。所以我們才告訴 Rust longest 函式回傳參考的生命週期等同於傳入參考中較短的生命週期。這樣一來借用檢查器就會否決範例 10-23 的程式碼,因為它可能會有無效的參考。 歡迎嘗試設計更多採用不同數值與不同生命週期的參考作為 longest 函式參數與回傳值的實驗,並在編譯前假設你的實驗會不會通過借用檢查器,然後看看你的理解是不是正確的!","breadcrumbs":"泛型型別、特徵與生命週期 » 透過生命週期驗證參考 » 函式簽名中的生命週期詮釋","id":"186","title":"函式簽名中的生命週期詮釋"},"187":{"body":"你要指定生命週期參數的方式取決於函式的行為。舉例來說如果我們改變函式 longest 的實作為永遠只回傳第一個參數而不是最長的字串切片,我們就不需要在參數 y 指定生命週期。以下的程式碼就能編譯: 檔案名稱:src/main.rs # fn main() {\n# let string1 = String::from(\"abcd\");\n# let string2 = \"efghijklmnopqrstuvwxyz\";\n# # let result = longest(string1.as_str(), string2);\n# println!(\"最長的字串為 {}\", result);\n# }\n# fn longest<'a>(x: &'a str, y: &str) -> &'a str { x\n} 我們指定生命週期參數 'a 給參數 x 與回傳型別,但參數 y 則沒有,因為 y 的生命週期與 x 和回傳型別的生命週期之間沒有任何關係。 當函式回傳參考時,回傳型別的生命週期參數必須符合其中一個參數的生命週期參數。如果回傳參考 沒有 和任何參數有關聯的話,代表它參考的是函式本體中的數值。但這會是迷途參考,因為該數值會在函式結尾離開作用域。請看看以下嘗試在函式 longest 的實作做法,它並不會編譯成功: 檔案名稱:src/main.rs # fn main() {\n# let string1 = String::from(\"abcd\");\n# let string2 = \"xyz\";\n# # let result = longest(string1.as_str(), string2);\n# println!(\"最長的字串為 {}\", result);\n# }\n# fn longest<'a>(x: &str, y: &str) -> &'a str { let result = String::from(\"超長的字串\"); result.as_str()\n} 我們在這邊雖然有對回傳型別指定生命週期參數 'a,但此實作還是會失敗,因為回傳值的生命週期與參數的生命週期完全無關。以下是我們獲得的錯誤訊息: $ cargo run Compiling chapter10 v0.1.0 (file:///projects/chapter10)\nerror[E0515]: cannot return reference to local variable `result` --> src/main.rs:11:5 |\n11 | result.as_str() | ^^^^^^^^^^^^^^^ returns a reference to data owned by the current function For more information about this error, try `rustc --explain E0515`.\nerror: could not compile `chapter10` due to previous error 問題在於 result 會離開作用域並在 longest 函式結尾被清除。我們卻嘗試從函式中回傳 result 的參考。我們無法指定生命週期參數來改變迷途參考,而且 Rust 不會允許我們將建立迷途參考。在此例中,最好的解決辦法是回傳有所有權的資料型別而非參考,並讓呼叫的函式自行決定如何清理數值。 總結來說,生命週期語法是用來連接函式中不同參數與回傳值的生命週期。一旦連結起來,Rust 就可以獲得足夠的資訊來確保記憶體安全的運算並防止會產生迷途指標或違反記憶體安全的操作。","breadcrumbs":"泛型型別、特徵與生命週期 » 透過生命週期驗證參考 » 深入理解生命週期","id":"187","title":"深入理解生命週期"},"188":{"body":"目前為止,我們定義過的結構體都持有型別的所有權。結構體其實也能持有參考,不過我們會需要在結構體定義中每個參考加上生命週期詮釋。範例 10-24 有個持有字串切片的結構體 ImportantExcerpt。 檔案名稱:src/main.rs struct ImportantExcerpt<'a> { part: &'a str,\n} fn main() { let novel = String::from(\"Call me Ishmael. Some years ago...\"); let first_sentence = novel.split('.').next().expect(\"無法找到 '.'\"); let i = ImportantExcerpt { part: first_sentence, };\n} 範例 10-24:擁有參考的結構體需要加上生命週期詮釋 此結構體有個欄位 part 並擁有字串切片參考。如同泛型資料型別,我們在結構體名稱之後的尖括號內宣告泛型生命週期參數,所以我們就可以在結構體定義的本體中使用生命週期參數。此詮釋代表 ImportantExcerpt 的實例不能比它持有的欄位 part 活得還久。 main 函式在此產生一個結構體 ImportantExcerpt 的實例並持有一個參考,其為變數 novel 所擁有的 String 中的第一個句子的參考。novel 的資料是在 ImportantExcerpt 實例之前建立的。除此之外,novel 在 ImportantExcerpt 離開作用域之前不會離開作用域,所以 ImportantExcerpt 實例中的參考是有效的。","breadcrumbs":"泛型型別、特徵與生命週期 » 透過生命週期驗證參考 » 結構體定義中的生命週期詮釋","id":"188","title":"結構體定義中的生命週期詮釋"},"189":{"body":"你已經學到了每個參考都有個生命週期,而且你需要在有使用參考的函式與結構體中指定生命週期參數。然而在第四章的範例 4-9 我們有函式可以不詮釋生命週期並照樣編譯成功,我們在範例 10-25 再展示一次。 檔案名稱:src/lib.rs fn first_word(s: &str) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..]\n}\n# # fn main() {\n# let my_string = String::from(\"hello world\");\n# # // first_word 能用在`String` 的切片\n# let word = first_word(&my_string[..]);\n# # let my_string_literal = \"hello world\";\n# # // first_word 能用在字串字面值\n# let word = first_word(&my_string_literal[..]);\n# # // 因為字串字面值已經是字串切片了\n# // 所以也可以不用加上字串語法!\n# let word = first_word(my_string_literal);\n# } 範例 10-25:在範例 4-9 定義過的函式,雖然其參數與回傳值均為參考,卻仍可編譯成功 此函式可以不用生命週期詮釋仍照樣編譯過是有歷史因素的:在早期版本的 Rust(1.0 之前),此程式碼是無法編譯的,因為每個參考都得有顯式生命週期。在當時的情況下,此函式簽名會長得像這樣: fn first_word<'a>(s: &'a str) -> &'a str { 在寫了大量的 Rust 程式碼後,Rust 團隊發現 Rust 開發者會在特定情況反覆輸入同樣的生命週期詮釋。這些情形都是可預期的,而且可以遵循一些明確的模式。開發者將這些模式加入編譯器的程式碼中,所以借用檢查器可以依據這些情況自行推導生命週期,而讓我們不必顯式詮釋。 這樣的歷史值得提起的原因是因為很可能會有更多明確的模式被找出來並加到編譯器中,意味著未來對於生命週期詮釋的要求會更少。 被寫進 Rust 參考分析的模式被稱作 生命週期省略規則(lifetime elision rules) 。這些不是程式設計師要遵守的規則,而是一系列編譯器能去考慮的情形。而如果你的程式碼符合這些情形時,你就不必顯式寫出生命週期。 省略規則無法提供完整的推導。如果 Rust 能明確套用規則,但在這之後還是有參考存在模棱兩可的生命週期,編譯器就無法猜出剩餘參考的生命週期。編譯器不會亂猜,它會回傳錯誤給你,說明你需要指定生命週期詮釋。 在函式或方法參數上的生命週期稱為 輸入生命週期(input lifetimes) ,而在回傳值的生命週期則稱為 輸出生命週期(output lifetimes) 。 當參考沒有顯式詮釋生命週期時,編譯器會用三項規則來推導它們。第一個規則適用於輸入生命週期,而第二與第三個規則適用於輸出生命週期。如果編譯器處理完這三個規則,卻仍有參考無法推斷出生命週期時,編譯器就會停止並回傳錯誤。這些適用於 fn 定義的規則一樣適用於 impl 區塊。 第一個規則是編譯器會給予每個參考參數一個生命週期參數。換句話說,一個函式只有一個參數的話,就只會有一個生命週期:fn foo<'a>(x: &'a i32);一個函式有兩個參數的話,就會有分別兩個生命週期參數:fn foo<'a, 'b>(x: &'a i32, y: &'b i32),以此類推。 第二個規則是如果剛好只有一個輸入生命週期參數,該參數就會賦值給所有輸出生命週期參數:fn foo<'a>(x: &'a i32) -> &'a i32。 第三個規則是如果有多個輸入生命週期參數,但其中一個是 &self 或 &mut self,由於這是方法,self 的生命週期會賦值給所有輸出生命週期參數。此規則讓方法更容易讀寫,因為不用寫更多符號出來。 讓我們假裝我們是編譯器。我們會檢查這些規則並找出範例 10-25 中函式 first_word 簽名中參考的生命週期。簽名的參考一開始沒有任何生命週期: fn first_word(s: &str) -> &str { 接著編譯器檢查第一個規則,指明每個參數都有自己的生命週期。我們如往常一樣指定 'a,所以簽名就會變成: fn first_word<'a>(s: &'a str) -> &str { 然後第二個規則也適用因為這裡剛好就一個輸入生命週期而已。第二個規則指明只有一個輸入生命週期的話,就會賦值給所有其他輸出生命週期。所以簽名現在變成這樣: fn first_word<'a>(s: &'a str) -> &'a str { 現在此函式所有的參考都有生命週期了,而且編譯器可以繼續分析,不必要求程式設計師在此詮釋函式簽名的生命週期。 讓我們再看看一個例子,這次是範例 10-20 一開始沒有任何生命週期參數的 longest 函式: fn longest(x: &str, y: &str) -> &str { 讓我們先檢查第一項規則:每個參數都有自己的生命週期。這次我們有兩個參數,所以我們有兩個生命週期: fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str { 你可以看出來第二個規則並不適用,因為我們有不止一個輸入生命週期。而第三個也不適用,因為 longest 是函式而非方法,其參數不會有 self 。遍歷這三個規則下來,我們仍然無法推斷出回傳型別的生命週期。這就是為何我們嘗試編譯範例 10-20 的程式碼會出錯的原因:編譯器遍歷生命週期省略規則,但仍然無法推導出簽名中所有參考的生命週期。 因為第三個規則僅適用於方法簽名,我們接下來就會看看這種情況時的生命週期,看看為何第三個規則讓我們不必常常在方法簽名詮釋生命週期。","breadcrumbs":"泛型型別、特徵與生命週期 » 透過生命週期驗證參考 » 生命週期省略","id":"189","title":"生命週期省略"},"19":{"body":"安裝 Rust 的同時也會包含一份本地的技術文件副本,讓你可以離線閱讀。執行 rustup doc 就可以用你的瀏覽器開啟本地文件。 每當有任何型別或函式出現而你卻不清楚如何使用時,你就可以閱讀應用程式介面(API)技術文件來理解!","breadcrumbs":"開始入門 » 安裝教學 » 本地端技術文件","id":"19","title":"本地端技術文件"},"190":{"body":"當我們在有生命週期的結構體上實作方法時,其語法類似於我們在範例 10-11 中泛型型別參數的語法。宣告並使用生命週期參數的地方會依據它們是否與結構體欄位或方法參數與回傳值相關。 結構體欄位的生命週期永遠需要宣告在 impl 關鍵字後方以及結構體名稱後方,因為這些生命週期是結構體型別的一部分。 在 impl 區塊中方法簽名的參考可能會與結構體欄位的參考生命週期綁定,或者它們可能是互相獨立的。除此之外,生命週期省略規則常常可以省略方法簽名中的生命週期詮釋。讓我們看看範例 10-24 定義過的 ImportantExcerpt 來作為範例。 首先我們使用一個方法叫做 level 其參數只有 self 的參考而回傳值是 i32,這不是任何參考: # struct ImportantExcerpt<'a> {\n# part: &'a str,\n# }\n# impl<'a> ImportantExcerpt<'a> { fn level(&self) -> i32 { 3 }\n}\n# # impl<'a> ImportantExcerpt<'a> {\n# fn announce_and_return_part(&self, announcement: &str) -> &str {\n# println!(\"請注意:{}\", announcement);\n# self.part\n# }\n# }\n# # fn main() {\n# let novel = String::from(\"叫我以實瑪利。多年以前...\");\n# let first_sentence = novel.split('.').next().expect(\"找不到'.'\");\n# let i = ImportantExcerpt {\n# part: first_sentence,\n# };\n# } 我們必須在 impl 之後宣告生命週期參數,並在型別名稱後使用該生命週期。但是我們不必在 self 的參考加上生命週期詮釋,因為其適用於第一個省略規則。 以下是第三個生命週期省略規則適用的地方: # struct ImportantExcerpt<'a> {\n# part: &'a str,\n# }\n# # impl<'a> ImportantExcerpt<'a> {\n# fn level(&self) -> i32 {\n# 3\n# }\n# }\n# impl<'a> ImportantExcerpt<'a> { fn announce_and_return_part(&self, announcement: &str) -> &str { println!(\"請注意:{}\", announcement); self.part }\n}\n# # fn main() {\n# let novel = String::from(\"叫我以實瑪利。多年以前...\");\n# let first_sentence = novel.split('.').next().expect(\"找不到'.'\");\n# let i = ImportantExcerpt {\n# part: first_sentence,\n# };\n# } 這裡有兩個輸入生命週期,所以 Rust 用第一個生命週期省略規則給予 &self 和 announcement 它們自己的生命週期。然後因為其中一個參數是 &self,回傳型別會取得 &self 的生命週期,如此一來所有的生命週期都推導出來了。","breadcrumbs":"泛型型別、特徵與生命週期 » 透過生命週期驗證參考 » 在方法定義中的生命週期詮釋","id":"190","title":"在方法定義中的生命週期詮釋"},"191":{"body":"其中有個特殊的生命週期 'static 我們需要進一步討論,這是指該參考 可以 存活在整個程式期間。所有的字串字面值都有 'static 生命週期,我們可以這樣詮釋: let s: &'static str = \"我有靜態生命週期。\"; 此字串的文字會直接儲存在程式的執行檔中,所以永遠有效。因此所有的字串字面值的生命週期都是 'static。 你有時可能會看到錯誤訊息建議使用 'static 生命週期。但在你對參考指明 'static 生命週期前,最好想一下該參考的生命週期是否真的會存在於整個程式期間,以及是否真的該活得這麼久。大多數錯誤訊息會建議 'static 生命週期的情況都來自於嘗試建立迷途參考或可用的生命週期不符。這樣的情況下,應該是要實際嘗試解決問題,而不是指明 'static 生命週期。","breadcrumbs":"泛型型別、特徵與生命週期 » 透過生命週期驗證參考 » 靜態生命週期","id":"191","title":"靜態生命週期"},"192":{"body":"讓我們用一個函式來總結泛型型別參數、特徵界限與生命週期的語法! # fn main() {\n# let string1 = String::from(\"abcd\");\n# let string2 = \"xyz\";\n# # let result = longest_with_an_announcement(\n# string1.as_str(),\n# string2,\n# \"Today is someone's birthday!\",\n# );\n# println!(\"最長的字串為 {}\", result);\n# }\n# use std::fmt::Display; fn longest_with_an_announcement<'a, T>( x: &'a str, y: &'a str, ann: T,\n) -> &'a str\nwhere T: Display,\n{ println!(\"公告!{}\", ann); if x.len() > y.len() { x } else { y }\n} 這是範例 10-21 會回傳兩個字串切片較長者的 longest 函式。不過現在它有個額外的參數 ann,使用的是泛型型別 T,它可以是任何在 where 中所指定有實作 Display 特徵的型別。此額外參數會在 {} 的地方印出來,這正是為何 Display 的特徵界限是必須的。因為生命週期也是一種泛型,生命週期參數 'a 與泛型型別參數 T 都宣告在函式名稱後的尖括號內。","breadcrumbs":"泛型型別、特徵與生命週期 » 透過生命週期驗證參考 » 組合泛型型別參數、特徵界限與生命週期","id":"192","title":"組合泛型型別參數、特徵界限與生命週期"},"193":{"body":"我們在此章節涵蓋了許多內容!現在你已經知道泛型型別參數、特徵與特徵界限以及泛型生命週期參數,你已經準備好能寫出適用於許多不同情況且不重複的程式碼了。泛型型別參數讓你可以讓程式碼適用於不同型別;特徵與特徵界限確保就算型別為泛型,它們都會有相同的行為。你還學到了使用生命週期詮釋確保此如此彈性的程式碼不會造成迷途參考。而且這些分析都發生在編譯期間,完全不影響執行的效能! 不管你信不信,本章節還有很多延伸主題可以導論,像是第十七章就會討論特徵物件(trait objects),這是另一個使用特徵的方法。另外還有一些更複雜的場合會涉及到更進階的生命週期詮釋。對此你可能就會想閱讀 Rust Reference 。接下來,你將學習如何在 Rust 寫測試,讓你可以確保程式碼能如期執行。","breadcrumbs":"泛型型別、特徵與生命週期 » 透過生命週期驗證參考 » 總結","id":"193","title":"總結"},"194":{"body":"Edsger W. Dijkstra 曾在 1972 年的演講「謙遜的程式設計師」中提到:「程式測試是個證明程式錯誤存在非常有效的方法,但要證明它不存在卻反而顯得十分無力。」這不代表我們不應該盡可能地做測試! 程式碼的正確性意謂著我們的程式碼可以如我們的預期執行。Rust 就被設計為特別注重程式的正確性,但正確性是很複雜且難以證明的。Rust 的型別系統就承擔了很大一部分的負擔,但是型別系統還是沒辦法抓到全部。所以 Rust 在語言內提供了編寫自動化程式測試的支援。 假設我們要寫個程式叫做 add_two,其會將傳入任意數字加上 2。此函式簽名接受整數作為參數並回傳一個整數作為結果。當我們實作並編譯函式時,Rust 會做所有你已經學過的型別檢查與借用檢查,來確保像是我們不會中傳入 String 數值或任意無效參考至此函式。但 Rust 無法 檢查其是否能執行我們預期此函式會完成的任務,也就是回傳加上 2 的參數。說不定它會將參數加上 10 或減 50!這就是我們要做測試的地方。 舉例來說,我們可以寫測試來判定當我們傳入 3 給函式 add_two 時,回傳值是不是 5。我們可以再變更我們的程式碼時來執行這些測試,以確保原本就正確的行為不會被改變。 測試是個複雜的技能,雖然我們無法在一個章節就涵蓋如何寫出好測試的細節,但我們還是會討論 Rust 測試功能機制。我們會介紹當你寫測試時可以用的詮釋與巨集、執行測試時的預設行為與選項以及如何組織測試成單元測試與整合測試。","breadcrumbs":"編寫自動化測試 » 編寫自動化測試","id":"194","title":"編寫自動化測試"},"195":{"body":"測試是一種 Rust 函式來驗證非測試程式碼是否以預期的方式執行。測試函式的本體通常會做三件動作: 設置任何所需要的資料或狀態。 執行你希望測試的程式碼 判定結果是否與你預期的相符。 讓我們看看 Rust 特地提供給測試的功能:包含 test 屬性(attribute)、一些巨集以及 should_panic 屬性。","breadcrumbs":"編寫自動化測試 » 如何寫測試 » 如何寫測試","id":"195","title":"如何寫測試"},"196":{"body":"最簡單的形式來看,測試在 Rust 中就是附有 test 屬性的函式。屬性是一種關於某段 Rust 程式碼的詮釋資料(metadata),其中一個例子是我們在第五章使用的 derive 屬性。要將一個函式轉換成測試函式,在 fn 前一行加上 #[test] 即可。當你用 cargo test 命令來執行你的測試時,Rust 會建構一個測試執行檔並執行被標注的函式,並回報每個測試函式是否通過或失敗。 當我們用 Cargo 建立新的函式庫專案時,同時會自動建立一個擁有測試函式的測試模組。此模組能協助我們開始寫測試,讓你不必在每次建立新專案時,尋找特定結構體與測試函式的語法。你可以新增多少測試函式與多少測試模組都沒問題! 在實際測試任何程式碼之前,我們將會透過實驗測試產生的樣板,來探索測試如何運作的每個環節。然後我們會寫些現實世界會寫的測試,呼叫我們寫的程式碼並判定其行為是否正確。 讓我們建立個會相加兩個數字的函式庫專案 adder: $ cargo new adder --lib Created library `adder` project\n$ cd adder 函式庫專案 adder 中的 src/lib.rs 檔案內容會長得像範例 11-1 所示。 檔案名稱:src/lib.rs #[cfg(test)]\nmod tests { #[test] fn it_works() { let result = 2 + 2; assert_eq!(result, 4); }\n} 範例 11-1:透過 cargo new 自動產生的測試模組與函式 現在我們先忽略開頭前兩行並專注在函式。先注意到 #[test] 詮釋:此屬性指出這是測試函式,所以測試者會知道此函式是用來測試的。我們也可以在 tests 模組中加入非測試函式來協助設置常見場景或是執行常見運算,所以我們需要標注哪些是想要測試的函式。 範例函式本體使用 assert_eq! 巨集來判定該 result,也就是 2 + 2 的結果是否等於 4。此判定是作為典型測試的範例格式。讓我們執行它來看看此測試是否會通過。 cargo test 命令會執行專案中的所有測試,如範例 11-2 所示。 $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.57s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 1 test\ntest tests::it_works ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 範例 11-2:執行自動產生的測試的輸出結果 Cargo 會編譯並執行測試。在 running 1 test 這行之後會顯示自動產生的測試函式 it_works 以及測試執行的結果 ok。再來可以看到整體總結,test result: ok. 代表所有測試都有通過,然後 1 passed; 0 failed 指出所有測試成功或失敗的數量。 我們可以選擇忽略測試,讓它在特定情形不會執行,我們會在本章的 「忽略某些測試除非特別指定」 段落再做說明。因為我們尚未有任何會忽略的程式碼,所以總結會顯示 0 ignored。我們也可以在 cargo test 傳入引數,只執行名稱符合字串的測試。這叫做 過濾 (filtering),我們會在 「透過名稱來執行部分測試」 段落做說明。我們也沒有過濾會執行的測試,所以總結最後顯示 0 filtered out。 0 measured 的統計數值是指評測效能的效能測試。效能測試(Benchmark tests)在本書撰寫時,仍然僅在 nightly Rust 可用。請查閱 效能測試的技術文件 來瞭解詳情。 測試輸出結果的下一部分,也就是 Doc-tests adder,是指任何技術文件測試的結果。我們還沒有任何技術文件測試,但是 Rust 可以編譯在 API 技術文件中的任何程式碼範例。此功能能幫助我們將技術文件與程式碼保持同步!我們會在第十四章的 「將技術文件註解作為測試」 段落討論如何寫技術文件測試。現在我們會先忽略 Doc-tests 的輸出結果。 讓我們變更程式碼的名稱來看看測試輸出會變成什麼。將 it_works 函式變更名稱,像是以下改成 exploration 這樣: 檔案名稱:src/lib.rs #[cfg(test)]\nmod tests { #[test] fn exploration() { assert_eq!(2 + 2, 4); }\n} 然後再執行一次 cargo test,輸出會顯示 exploration 而非 it_works: $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.59s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 1 test\ntest tests::exploration ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 讓我們再加上另一個測試,不過這次我們要讓測試失敗!測試會在測試函式恐慌時失敗,每個測試會跑在新的執行緒(thread)上,然後當主執行緒看到測試執行緒死亡時,就會將該測試標記為失敗的。我們有在第九章提及引發恐慌最簡單的辦法,那就是呼叫 panic! 巨集。將它寫入新的測試 another 中,所以你在 src/lib.rs 的檔案中會看到向範例 11-3 這樣。 檔案名稱:src/lib.rs #[cfg(test)]\nmod tests { #[test] fn exploration() { assert_eq!(2 + 2, 4); } #[test] fn another() { panic!(\"此測試會失敗\"); }\n} 範例 11-3:新增第二個會失敗的測試,因為我們會呼叫 panic! 巨集 使用 cargo test 再執行一次測試,輸出結果應該會像範例 11-4 這樣,顯示出我們的 exploration 測試通過但 another 失敗。 $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.72s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 2 tests\ntest tests::another ... FAILED\ntest tests::exploration ... ok failures: ---- tests::another stdout ----\nthread 'main' panicked at '此測試會失敗', src/lib.rs:10:9\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::another test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass `--lib` 範例 11-4:其中一個測試通過,而另一個失敗的輸出結果 test tests::another 這行會顯示 FAILED 而非 ok。在獨立結果與總結之間出現了兩個新的段落,第一個段落會顯示每個測試失敗的原因細節。在此例中,我們會收到 another 失敗的緣由,因為 src/lib.rs 檔案中第十行的恐慌 panicked at '此測試會失敗'。下一個段落則是會列出所有失敗的測試,要是測試很多且失敗測試輸出結果很長的話,此資訊就很實用。我們可以使用失敗測試的名稱來只執行這個測試以便除錯。我們會在 「控制程式如何執行」 段落討論更多執行測試的方法。 總結會顯示在最後一行,在此例中它表示我們有一個測試結果是 FAILED。也就是我們有一個測試通過,一個測試失敗。 現在你知道測試結果在不同場合看起來的樣子,讓我們來看看除了 panic! 以外對測試也很有幫助的巨集吧。","breadcrumbs":"編寫自動化測試 » 如何寫測試 » 測試函式剖析","id":"196","title":"測試函式剖析"},"197":{"body":"標準函式庫提供的 assert! 巨集可以在你要確保測試中的一些條件評估為 true 時使用。我們給予 assert! 巨集一個引數來計算出布林值。如果數值為 true,assert! 不會做任何動作然後測試就會通過。如果數值為 false,assert! 巨集會呼叫 panic! 巨集導致測試失敗。使用 assert! 巨集能幫助我們檢查我們的程式碼是否以我們預期的方式運作。 在第五章的範例 5-15,我們有結構體 Rectangle 與方法 can_hold,我們在範例 11-5 再看一次。讓我們將此程式碼寫入 src/lib.rs 檔案中,並寫些對它使用 assert! 巨集的測試。 檔案名稱:src/lib.rs #[derive(Debug)]\nstruct Rectangle { width: u32, height: u32,\n} impl Rectangle { fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height }\n} 範例 11-5:第五章中的結構體 Rectangle 與其方法 can_hold can_hold 方法會回傳布林值,這代表它是 assert! 巨集的絕佳展示機會。在範例 11-6 中,我們寫了個測試來練習 can_hold 方法,我們建立了一個寬度為 8 長度為 7 的 Rectangle 實例,並判定它可以包含另一個寬度為 5 長度為 1 的 Rectangle 實例。 檔案名稱:src/lib.rs # #[derive(Debug)]\n# struct Rectangle {\n# width: u32,\n# height: u32,\n# }\n# # impl Rectangle {\n# fn can_hold(&self, other: &Rectangle) -> bool {\n# self.width > other.width && self.height > other.height\n# }\n# }\n# #[cfg(test)]\nmod tests { use super::*; #[test] fn larger_can_hold_smaller() { let larger = Rectangle { width: 8, height: 7, }; let smaller = Rectangle { width: 5, height: 1, }; assert!(larger.can_hold(&smaller)); }\n} 範例 11-6:一支檢查一個大長方形是否能包含一個小長方形的 can_hold 測試 注意到我們已經在 tests 模組中加了一行 use super::*;。tests 和一般的模組一樣都遵循我們在第七章 「參考模組項目的路徑」 提及的常見能見度規則。因為 tests 模組是內部模組,我們需要將外部模組的程式碼引入內部模組的作用域中。我們使用全域運算子(glob)讓外部模組定義的所有程式碼在此 tests 模組都可以使用。 我們將我們的測試命名為 larger_can_hold_smaller,然後我們建立兩個我們需要用到的 Rectangle 實例。然後我們呼叫 assert! 巨集並將 larger.can_hold(&smaller) 的結果傳給它。此表達式應該要回傳 true,所以我們的程式應該會通過。讓我們看看結果吧! $ cargo test Compiling rectangle v0.1.0 (file:///projects/rectangle) Finished test [unoptimized + debuginfo] target(s) in 0.66s Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e) running 1 test\ntest tests::larger_can_hold_smaller ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests rectangle running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 它通過了!讓我們再加另一個測試,這是是判定小長方形無法包含大長方形: 檔案名稱:src/lib.rs # #[derive(Debug)]\n# struct Rectangle {\n# width: u32,\n# height: u32,\n# }\n# # impl Rectangle {\n# fn can_hold(&self, other: &Rectangle) -> bool {\n# self.width > other.width && self.height > other.height\n# }\n# }\n# #[cfg(test)]\nmod tests { use super::*; #[test] fn larger_can_hold_smaller() { // --省略--\n# let larger = Rectangle {\n# width: 8,\n# height: 7,\n# };\n# let smaller = Rectangle {\n# width: 5,\n# height: 1,\n# };\n# # assert!(larger.can_hold(&smaller)); } #[test] fn smaller_cannot_hold_larger() { let larger = Rectangle { width: 8, height: 7, }; let smaller = Rectangle { width: 5, height: 1, }; assert!(!smaller.can_hold(&larger)); }\n} 因為函式 can_hold 的正確結果在此例為 false,我們需要將該結果反轉後才能傳給 assert! 巨集。因此我們的測試在 can_hold 回傳 false 時才會通過: $ cargo test Compiling rectangle v0.1.0 (file:///projects/rectangle) Finished test [unoptimized + debuginfo] target(s) in 0.66s Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e) running 2 tests\ntest tests::larger_can_hold_smaller ... ok\ntest tests::smaller_cannot_hold_larger ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests rectangle running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 兩個測試都過了!現在讓我們看看當我們在程式碼中引入程式錯誤的話,測試結果會為何。讓我們來改變 can_hold 方法的實作將比較時的大於符號改成小於符號: # #[derive(Debug)]\n# struct Rectangle {\n# width: u32,\n# height: u32,\n# }\n# // --省略--\nimpl Rectangle { fn can_hold(&self, other: &Rectangle) -> bool { self.width < other.width && self.height > other.height }\n}\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# fn larger_can_hold_smaller() {\n# let larger = Rectangle {\n# width: 8,\n# height: 7,\n# };\n# let smaller = Rectangle {\n# width: 5,\n# height: 1,\n# };\n# # assert!(larger.can_hold(&smaller));\n# }\n# # #[test]\n# fn smaller_cannot_hold_larger() {\n# let larger = Rectangle {\n# width: 8,\n# height: 7,\n# };\n# let smaller = Rectangle {\n# width: 5,\n# height: 1,\n# };\n# # assert!(!smaller.can_hold(&larger));\n# }\n# } 執行測試的話現在就會顯示以下結果: $ cargo test Compiling rectangle v0.1.0 (file:///projects/rectangle) Finished test [unoptimized + debuginfo] target(s) in 0.66s Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e) running 2 tests\ntest tests::larger_can_hold_smaller ... FAILED\ntest tests::smaller_cannot_hold_larger ... ok failures: ---- tests::larger_can_hold_smaller stdout ----\nthread 'main' panicked at 'assertion failed: larger.can_hold(&smaller)', src/lib.rs:28:9\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::larger_can_hold_smaller test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass `--lib` 我們的測試抓到了錯誤!因為 larger.width 是 8 而 smaller.width 是 5,can_hold 比較寬度時現在會回傳 false,因為 8 沒有比 5 小。","breadcrumbs":"編寫自動化測試 » 如何寫測試 » 透過 assert! 巨集檢查結果","id":"197","title":"透過 assert! 巨集檢查結果"},"198":{"body":"有一種常見驗證程式的方式是將程式碼的結果與你預期程式碼會回傳的數值做測試,檢查它們是否相等。你可以使用 assert! 巨集並傳入使用 == 運算子的表達式來辦到。不過這種測試方法是很常見的,所以標準函式庫提供了一對巨集 assert_eq! 與 assert_ne! 來讓你能更方便地測試。這兩個巨集分別比較兩個引數是否相等或不相等。如果判定失敗的話,它們還會印出兩個數值,讓我們能清楚看到 為何 測試失敗。相對地,assert! 巨集只會說明它在 == 表達式中取得 false 值,而不會告訴你導致 false 的那兩個值。 在範例 11-7 中,我們寫了個函式叫做 add_two 並對參數加上 2,然後我們使用 assert_eq! 巨集來測試此函式。 檔案名稱:src/lib.rs pub fn add_two(a: i32) -> i32 { a + 2\n} #[cfg(test)]\nmod tests { use super::*; #[test] fn it_adds_two() { assert_eq!(4, add_two(2)); }\n} 範例 11-7:使用 assert_eq! 巨集測試函式 add_two 讓我們檢查後它的確通過了! $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.58s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 1 test\ntest tests::it_adds_two ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 我們傳入 assert_eq! 巨集的引數 4 與呼叫 add_two(2) 的結果相等。測試的結果為 test tests::it_adds_two ... ok 而 ok 就代表我們的測試通過了! 讓我們在我們的程式碼引入個錯誤,看看使使用 assert_eq! 的測試失敗時看起來為何。變更函式 add_two 的實作改成加 3: pub fn add_two(a: i32) -> i32 { a + 3\n}\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# fn it_adds_two() {\n# assert_eq!(4, add_two(2));\n# }\n# } 再執行一次測試: $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.61s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 1 test\ntest tests::it_adds_two ... FAILED failures: ---- tests::it_adds_two stdout ----\nthread 'main' panicked at 'assertion failed: `(left == right)` left: `4`, right: `5`', src/lib.rs:11:9\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::it_adds_two test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass `--lib` 我們的測試抓到了錯誤!it_adds_two 測試失敗了,然後訊息會告訴我們失敗的判斷來自於 assertion failed: `(left == right)`,以及 left 與 right 的數值為何。此訊息能協助我們開始除錯:left 的引數是 4 但是擁有 add_two(2) 的引數 right 卻是 5。你應該能想像這會在有一大堆測試時是非常有幫助的。 注意到在有些語言或測試框架中,判定相等的函式的參數會稱作 expected 和 actual,然後它們會因為指定的引數順序而有差。但在 Rust 中它們被稱為 left 和 right,且我們預期的值與測試中程式碼產生的值之間的順序沒有任何影響。我們可以在此測試這樣寫判定 assert_eq!(add_two(2), 4),而錯誤訊息一樣會顯示 assertion failed: `(left == right)`。 assert_ne! 巨集會在我們給予的兩個值不相等時通過,相等時失敗。此巨集適用於當我們不確定一個數值 會是 什麼樣子,但是我們確定該數值 不該 是某種樣子。舉例來說,如果我們要測試一個保證會以某種形式更改其輸入的函式,但輸入變更的方式是依照我們執行程式時的當天是星期幾來決定,此時最好的判定方式就是檢查函式的輸出不等於輸入。 assert_eq! 和 assert_ne! 巨集底下分別使用了 == 和 != 運算子。當判定失敗時,巨集會透過除錯格式化資訊來顯示它們的引數,代表要比較的數值必須要實作 PartialEq 和 Debug 特徵。所有的基本型別與大多數標準函式庫中提供的型別都有實作這些特徵。對於你自己定義的結構體與列舉,你需要實作 PartialEq,這樣該型別的數值才能判定相等或不相等。你需要實作 Debug 來顯示判定失敗時的數值。因為這兩個特徵都是可推導的特徵,就像第五章的範例 5-12 所寫的那樣,我們通常只要在你定義的結構體或列舉前加上 #[derive(PartialEq, Debug)] 的詮釋就好。你可以查閱附錄 C 「可推導的特徵」 來發現更多可推導的特徵。","breadcrumbs":"編寫自動化測試 » 如何寫測試 » 透過 assert_eq! 與 assert_ne! Macros 測試相等","id":"198","title":"透過 assert_eq! 與 assert_ne! Macros 測試相等"},"199":{"body":"你可以寫一個與失敗訊息一同顯示的自訂訊息,作為 assert!、assert_eq! 與 assert_ne! 巨集的選擇性引數。任何指定在必要引數後方的任何引數都會傳給 format! 巨集(我們在第八章 「使用 + 運算子或 format! 巨集串接字串」 的段落討論過),所以你可以傳入一個包含 {} 佔位符(placeholder)的格式化字串以及其對應的數值。自訂訊息可以用來紀錄判定的意義,當測試失敗時,你可以更清楚知道程式碼的問題。 舉例來說,假設我們有個函式會以收到的名字向人們打招呼,而且我們希望測試我們傳入的名字有出現在輸出: 檔案名稱:src/lib.rs pub fn greeting(name: &str) -> String { format!(\"哈囉{}!\", name)\n} #[cfg(test)]\nmod tests { use super::*; #[test] fn greeting_contains_name() { let result = greeting(\"卡爾\"); assert!(result.contains(\"卡爾\")); }\n} 此函式的要求還沒完全確定,而我們招呼開頭的文字 哈囉 很可能會在之後改變。我們決定當需求改變時,我們不想要得同時更新測試。所以我們不打算檢查 greeting 函式回傳的整個數值,我們只需要判定輸出有沒有包含輸入參數。 現在讓我們將錯誤引進程式中吧,將 greeting 改成不包含 name 然後看看預設的測試失敗會如何呈現: pub fn greeting(name: &str) -> String { String::from(\"哈囉!\")\n}\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# fn greeting_contains_name() {\n# let result = greeting(\"卡爾\");\n# assert!(result.contains(\"卡爾\"));\n# }\n# } 執行此程式會產生以下錯誤: $ cargo test Compiling greeter v0.1.0 (file:///projects/greeter) Finished test [unoptimized + debuginfo] target(s) in 0.91s Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a) running 1 test\ntest tests::greeting_contains_name ... FAILED failures: ---- tests::greeting_contains_name stdout ----\nthread 'main' panicked at 'assertion failed: result.contains(\\\"卡爾\\\")', src/lib.rs:12:9\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::greeting_contains_name test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass `--lib` 此結果指出判定失敗以及發生的位置。要是錯誤訊息可以提供我們從 greeting 函式取得的數值會更好。讓我們來在測試函式中加入自訂訊息,該訊息會是個格式化字串,並有個佔位符(placeholder)來填入我們從 greeting 函式取得的確切數值: # pub fn greeting(name: &str) -> String {\n# String::from(\"哈囉!\")\n# }\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# #[test] fn greeting_contains_name() { let result = greeting(\"卡爾\"); assert!( result.contains(\"卡爾\"), \"打招呼時並沒有喊出名稱,其數值為 `{}`\", result ); }\n# } 現在當我們執行測試,我們能從錯誤訊息得到更多資訊: $ cargo test Compiling greeter v0.1.0 (file:///projects/greeter) Finished test [unoptimized + debuginfo] target(s) in 0.93s Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a) running 1 test\ntest tests::greeting_contains_name ... FAILED failures: ---- tests::greeting_contains_name stdout ----\nthread 'main' panicked at '打招呼時並沒有喊出名稱,其數值為 `哈囉!`', src/lib.rs:12:9\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::greeting_contains_name test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass `--lib` 我們可以看到我們實際從測試輸出拿到的數值,這能幫助我們除錯找到實際發生什麼,而不只是預期會是什麼。","breadcrumbs":"編寫自動化測試 » 如何寫測試 » 加入自訂失敗訊息","id":"199","title":"加入自訂失敗訊息"},"2":{"body":"注意:本書的(英文)版本與出版的 The Rust Programming Language 以及電子書版本的 No Starch Press 一致。 歡迎閱讀 Rust 程式設計語言 ,這是一本 Rust 的入門書籍。Rust 程式設計語言能幫助你寫出更快更可靠的軟體。在設計程式語言時,「上層的易讀易用性」與「底層的掌控性」經常難以取捨。Rust 直接挑戰這個矛盾。Rust 在強大的技術能力與良好的開發者體驗之間取得平衡,讓你能控制底層的實作細節(比如記憶體使用),但免於以往這樣的控制所帶來的相關麻煩。","breadcrumbs":"介紹 » 介紹","id":"2","title":"介紹"},"20":{"body":"現在你已經安裝好 Rust,是時候開始寫你的第一支 Rust 程式。當我們學習一門新的語言時,有一個習慣是寫一支印出「Hello, world!」到螢幕上的小程式,此章節將教你做一樣的事! 注意:本書將假設你已經知道命令列最基本的使用方法。Rust 對於你的編輯器、工具以及程式碼位於何處沒有特殊的要求,所以如果你更傾向於使用整合開發環境(IDE)的話,請儘管使用你最愛的 IDE。許多 IDE 都已經針對 Rust 提供某種程度的支援,請查看你所使用的 IDE 技術文件以瞭解詳情。Rust 團隊正透過 rust-analyzer 積極提升 IDE 的支援,請查 附錄 D 來了解更多細節。","breadcrumbs":"開始入門 » Hello, World! » Hello, World!","id":"20","title":"Hello, World!"},"200":{"body":"除了檢查我們的程式碼有沒有回傳我們預期的正確數值,檢查我們的程式碼有沒有如我們預期處理錯誤條件也是很重要的。舉例來說,考慮我們在第九章範例 9-13 建立的 Guess 型別。其他使用 Guess 的程式碼保證會拿到數值為 1 到 100 的 Guess 實例。我們可以寫個會恐慌的程式,嘗試用範圍之外的數字建立 Guess 實例。 為此我們可以加上屬性 should_panic 到我們的測試函式。此屬性讓函式的程式碼恐慌時才會通過測試,反之如果函式的程式碼沒有恐慌的話測試就會失敗。 範例 11-8 展示一支檢查 Guess::new 是否以我們預期的錯誤條件出錯的測試。 檔案名稱:src/lib.rs pub struct Guess { value: i32,\n} impl Guess { pub fn new(value: i32) -> Guess { if value < 1 || value > 100 { panic!(\"猜測數字必須介於 1 到 100 之間,你輸入的是 {}。\", value); } Guess { value } }\n} #[cfg(test)]\nmod tests { use super::*; #[test] #[should_panic] fn greater_than_100() { Guess::new(200); }\n} 範例 11-8:測試造成 panic! 的條件 我們將 #[should_panic] 屬性置於 #[test] 屬性之後與測試函式之前。讓我們看看測試通過的結果: $ cargo test Compiling guessing_game v0.1.0 (file:///projects/guessing_game) Finished test [unoptimized + debuginfo] target(s) in 0.58s Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d) running 1 test\ntest tests::greater_than_100 - should panic ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests guessing_game running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 看起來不錯!現在讓我們將錯誤引入程式碼中,移除會讓 new 函式在數值大於 100 會恐慌的程式碼: # pub struct Guess {\n# value: i32,\n# }\n# // --省略--\nimpl Guess { pub fn new(value: i32) -> Guess { if value < 1 { panic!(\"猜測數字必須介於 1 到 100 之間,你輸入的是 {}。\", value); } Guess { value } }\n}\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# #[should_panic]\n# fn greater_than_100() {\n# Guess::new(200);\n# }\n# } 當我們執行範例 11-8 的測試,它就會失敗: $ cargo test Compiling guessing_game v0.1.0 (file:///projects/guessing_game) Finished test [unoptimized + debuginfo] target(s) in 0.62s Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d) running 1 test\ntest tests::greater_than_100 - should panic ... FAILED failures: ---- tests::greater_than_100 stdout ----\nnote: test did not panic as expected failures: tests::greater_than_100 test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass `--lib` 我們在此情況得到的訊息並不是很有用,但是當我們查看測試函式,我們會看到它詮釋了 #[should_panic]。這個測試失敗代表測試函式內的程式碼沒有造成恐慌。 使用 should_panic 的測試可能會有點模棱兩可。should_panic 測試只要是有恐慌都會通過,就算是不同於我們預期發生的恐慌而造成的也一樣。要讓測試 should_panic 更精準的話,我們可以加上選擇性的 expected 參數到 should_panic 中。這樣測試就會確保錯誤訊息會包含我們所寫的文字。舉例來說,範例 11-9 更改了 Guess 讓 new 函式會依據數值太大或大小而有不同的錯誤訊息。 檔案名稱:src/lib.rs # pub struct Guess {\n# value: i32,\n# }\n# // --省略-- impl Guess { pub fn new(value: i32) -> Guess { if value < 1 { panic!( \"猜測數字必須大於等於 1,取得的數值是 {}。\", value ); } else if value > 100 { panic!( \"猜測數字必須小於等於 100,取得的數值是 {}。\", value ); } Guess { value } }\n} #[cfg(test)]\nmod tests { use super::*; #[test] #[should_panic(expected = \"小於等於 100\")] fn greater_than_100() { Guess::new(200); }\n} 範例 11-9:panic! 的錯誤訊息包含特定子字串才會通過的測試 此測試會通過是因為我們在 should_panic 屬性加上的 expected 就是 Guess::new 函式恐慌時的子字串。我們也可以指定整個恐慌訊息,在此例的話就是 猜測數字必須小於等於 100,取得的數值是 200。。你所指定的預期參數取決於該恐慌訊息是獨特或動態的,以及你希望你的測試要多精準。在此例中,恐慌訊息的子訊息就足以確認測試函式中的程式碼會執行 else if value > 100 的分支。 為了觀察擁有 expected 訊息的 should_panic 失敗時會發生什麼事。讓我同樣再次將錯誤引入程式中,將 if value < 1 與 else if value > 100 的區塊本體對調: # pub struct Guess {\n# value: i32,\n# }\n# # impl Guess {\n# pub fn new(value: i32) -> Guess { if value < 1 { panic!( \"猜測數字必須小於等於 100,取得的數值是 {}。\", value ); } else if value > 100 { panic!( \"猜測數字必須大於等於 1,取得的數值是 {}。\", value ); }\n# # Guess { value }\n# }\n# }\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# #[should_panic(expected = \"小於等於 100\")]\n# fn greater_than_100() {\n# Guess::new(200);\n# }\n# } 這次當我們執行 should_panic 測試,它就會失敗: $ cargo test Compiling guessing_game v0.1.0 (file:///projects/guessing_game) Finished test [unoptimized + debuginfo] target(s) in 0.66s Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d) running 1 test\ntest tests::greater_than_100 - should panic ... FAILED failures: ---- tests::greater_than_100 stdout ----\nthread 'main' panicked at '猜測數字必須大於等於 1,取得的數值是 200。', src/lib.rs:13:13\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace\nnote: panic did not contain expected string panic message: `\"猜測數字必須大於等於 1,取得的數值是 200。\"`, expected substring: `\"小於等於 100\"` failures: tests::greater_than_100 test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass `--lib` 錯誤訊息表示此程式碼的確有如我們預期地恐慌,但是恐慌訊息並沒有包含預期的字串 '猜測數字必須小於等於 100'。在此例我們的會得到的恐慌訊息為 猜測數字必須大於等於 1,取得的數值是 200。這樣我們就能尋找錯誤在哪了!","breadcrumbs":"編寫自動化測試 » 如何寫測試 » 透過 should_panic 檢查恐慌","id":"200","title":"透過 should_panic 檢查恐慌"},"201":{"body":"我們目前為止的測試在失敗時都會恐慌。我們也可以寫出使用 Result 的測試!以下是範例 11-1 的測試,不過重寫成 Result 的版本並回傳 Err 而非恐慌: #[cfg(test)]\nmod tests { #[test] fn it_works() -> Result<(), String> { if 2 + 2 == 4 { Ok(()) } else { Err(String::from(\"二加二不等於四\")) } }\n} it_works 函式現在有個回傳型別 Result<(), String>。在函式本體中,我們不再呼叫 assert_eq! 巨集,而是當測試成功時回傳 Ok(()),當程式失敗時回傳存有 String 的 Err。 測試中回傳 Result 讓你可以在測試本體中使用問號運算子,這樣能方便地寫出任何運算回傳 Err 時該失敗的測試。 不過你就不能將 #[should_panic] 詮釋用在使用 Result 的測試。要判斷一個操作是否回傳 Err 的話,不要在 Result 數值後加上 ?,而是改用 assert!(value.is_err())。 現在你知道了各種寫測試的方法,讓我們看看執行程式時發生了什麼事,並探索我們可以對 cargo test 使用的選項。","breadcrumbs":"編寫自動化測試 » 如何寫測試 » 在測試中使用 Result","id":"201","title":"在測試中使用 Result"},"202":{"body":"就像 cargo run 會編譯你的程式碼並執行產生的執行檔,cargo test 會在測試模式編譯你的程式碼並執行產生的測試執行檔。cargo test 產生的執行檔預設行為會平行執行所有測試,並獲取測試執行時的輸出,讓測試各自的輸出結果不會顯示出來,以更容易讀取相關測試的結果。然而你可以指定命令列選項來改變預設行為。 有些命令列選項用於 cargo test 而有些則用於產生的測試執行檔。要分開這兩種引數,你可以先列出要用於 cargo test 的引數然後加上 -- 分隔線來區隔要用於測試執行檔的引數。執行 cargo test --help 可以顯示你能用在 cargo test 的選項,而執行 cargo test -- --help 則會顯示你在 -- 之後能用的選項。","breadcrumbs":"編寫自動化測試 » 控制程式如何執行 » 控制程式如何執行","id":"202","title":"控制程式如何執行"},"203":{"body":"當你執行數個測試時,它們預設會使用執行緒(thread)來平行執行。這樣測試可以更快完成,讓你可以從你或其他人的程式碼更快獲得回饋。因為測試是同時一起執行的,請確保你的測試並不依賴其他測試或是共享的狀態。這包含共享環境,像是目前的工作目錄或是環境變數。 舉例來說,假設你的每個測試都會執行一些程式碼,用以在硬碟上產生一個檔案叫做 test-output.txt ,並將一些資料寫入檔案中。然後每個測試讀取檔案中的資料,並判定該檔案有沒有包含特定的值,而這個值在每個測試都不相同。因為測試同時執行,其中的測試可能覆蓋其他測試寫入與讀取的內容。這樣其他測試就會失敗,並不是因為程式碼不正確,而是因為平行執行時該測試會被其他測試所影響。其中一個解決辦法是確保每個測試都寫入不同的檔案,或者也可以選擇一次只執行一個測試。 如果你不想平行執行測試,或者你想要能更加掌控使用的執行緒數量,你可以傳遞 --test-threads 的選項以及你希望在測試執行檔使用的執行緒數量。請看一下以下範例: $ cargo test -- --test-threads=1 我們將測試執行緒設為 1,告訴程式不要做任何平行化。使用一條執行緒執行測試會比平行執行它們還來的久,但是如果測試有共享狀態的話,它們就會不互相影響到對方了。","breadcrumbs":"編寫自動化測試 » 控制程式如何執行 » 平行或接續執行測試","id":"203","title":"平行或接續執行測試"},"204":{"body":"如果測試通過的話,Rust 的測試函式庫預設會獲取所有印出的標準輸出。舉例來說,如果我們在測試中呼叫 println! 然後測試通過的話,我們不會在終端機看到 println! 的輸出,我們只會看到一行表達測試通過的訊息。如果測試失敗,我們才會看到所有印出的標準輸出與失敗訊息。 舉例來說,範例 11-10 有個蠢蠢的函式只會印出它的參數並回傳 10,以及一個會通過的測試與一個會失敗的測試。 檔案名稱:src/lib.rs fn prints_and_returns_10(a: i32) -> i32 { println!(\"我得到的數值為 {}\", a); 10\n} #[cfg(test)]\nmod tests { use super::*; #[test] fn this_test_will_pass() { let value = prints_and_returns_10(4); assert_eq!(10, value); } #[test] fn this_test_will_fail() { let value = prints_and_returns_10(8); assert_eq!(5, value); }\n} 範例 11-10:測試會呼叫 println! 的函式 當我們使用 cargo test 執行這些程式時,我們會看到以下輸出結果: $ cargo test Compiling silly-function v0.1.0 (file:///projects/silly-function) Finished test [unoptimized + debuginfo] target(s) in 0.58s Running unittests src/lib.rs (target/debug/deps/silly_function-160869f38cff9166) running 2 tests\ntest tests::this_test_will_fail ... FAILED\ntest tests::this_test_will_pass ... ok failures: ---- tests::this_test_will_fail stdout ----\n我得到的數值為 8\nthread 'main' panicked at 'assertion failed: `(left == right)` left: `5`, right: `10`', src/lib.rs:19:9\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::this_test_will_fail test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass `--lib` 注意到此輸出結果我們看不到 我得到的數值為 4,這是當測試通過時印出的訊息。這個輸出被獲取走了。而測試會失敗的標準輸出 我得到的數值為 8 則會出現在測試總結輸出的段落上,並同時顯示錯誤發生的原因。 如果我們希望在測試通過時也能看到印出的數值,我們可以用 --show-output 告訴 Rust 也在成功的測試顯示輸出結果。 $ cargo test -- --show-output 當我們使用 --show-output 再次執行範例 11-10 的話,我們就能看到以下輸出: $ cargo test -- --show-output Compiling silly-function v0.1.0 (file:///projects/silly-function) Finished test [unoptimized + debuginfo] target(s) in 0.60s Running unittests src/lib.rs (target/debug/deps/silly_function-160869f38cff9166) running 2 tests\ntest tests::this_test_will_fail ... FAILED\ntest tests::this_test_will_pass ... ok successes: ---- tests::this_test_will_pass stdout ----\n我得到的數值為 4 successes: tests::this_test_will_pass failures: ---- tests::this_test_will_fail stdout ----\n我得到的數值為 8\nthread 'main' panicked at 'assertion failed: `(left == right)` left: `5`, right: `10`', src/lib.rs:19:9\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::this_test_will_fail test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass `--lib`","breadcrumbs":"編寫自動化測試 » 控制程式如何執行 » 顯示函式輸出結果","id":"204","title":"顯示函式輸出結果"},"205":{"body":"有時執行完整所有的測試會很花時間。如果你正專注於程式碼的特定部分,你可能會想要只執行與該程式碼有關的測試。你可以向 cargo test 傳遞你想要執行的測試名稱作為引數。 為了解釋如何執行部分測試,我們將為 add_two 函式建立三個測試,如範例 11-11 所示,然後選擇其中一個執行。 檔案名稱:src/lib.rs pub fn add_two(a: i32) -> i32 { a + 2\n} #[cfg(test)]\nmod tests { use super::*; #[test] fn add_two_and_two() { assert_eq!(4, add_two(2)); } #[test] fn add_three_and_two() { assert_eq!(5, add_two(3)); } #[test] fn one_hundred() { assert_eq!(102, add_two(100)); }\n} 範例 11-11:三個名稱不同的測試 如果我們沒有傳遞任何引數來執行測試的話,如我們前面看過的一樣,所有測試會平行執行: $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.62s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 3 tests\ntest tests::add_three_and_two ... ok\ntest tests::add_two_and_two ... ok\ntest tests::one_hundred ... ok test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 執行單獨一個測試 我們可以傳遞任何測試函式的名稱給 cargo test 來只執行該測試: $ cargo test one_hundred Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.69s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 1 test\ntest tests::one_hundred ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s 只有名稱為 one_hundred 的測試會執行,其他兩個的名稱並不符合。測試輸出會在總結的最後顯示 2 filtered out 告訴我們除了命令列執行的測試以外,還有更多其他測試。 我們無法用此方式指定多個測試名稱,只有第一個傳給 cargo test 有用。但我們有其他方式能執行數個測試。 過濾執行數個測試 我們可以指定部分測試名稱,然後任何測試名稱中有相符的就會被執行。舉例來說,因為我們有兩個測試的名稱都包含 add,我們可以透過執行 cargo test add 來執行這兩個測試: $ cargo test add Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.61s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 2 tests\ntest tests::add_three_and_two ... ok\ntest tests::add_two_and_two ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s 此命令會執行所有名稱中包含 add 的測試,並過濾掉 one_hundred 的測試名稱。另外測試所在的模組也屬於測試名稱中,所以我們可以透過過濾模組名稱來執行該模組的所有測試。","breadcrumbs":"編寫自動化測試 » 控制程式如何執行 » 透過名稱來執行部分測試","id":"205","title":"透過名稱來執行部分測試"},"206":{"body":"有時候有些特定的測試執行會花非常多時間,所以你可能希望在執行 cargo test 時能排除它們。與其列出所有你想要的測試作為引數,你可以在花時間的測試前加上 ignore 屬性詮釋來排除它們,如以下所示: 檔案名稱:src/lib.rs #[test]\nfn it_works() { assert_eq!(2 + 2, 4);\n} #[test]\n#[ignore]\nfn expensive_test() { // 會執行一小時的程式碼\n} 對於想排除的測試,我們在 #[test] 之後我們加上 #[ignore]。現在當我們執行我們的測試時,it_works 會執行但 expensive_test 就不會: $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.60s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 2 tests\ntest expensive_test ... ignored\ntest it_works ... ok test result: ok. 1 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s expensive_test 函式會列在 ignored,如果我們希望只執行被忽略的測試,我們可以使用 cargo test -- --ignored: $ cargo test -- --ignored Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.61s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 1 test\ntest expensive_test ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 透過控制哪些測試能執行,你能夠確保快速執行 cargo test。當你有時間能夠執行 ignored 的測試時,你可以執行 cargo test -- --ignored 來等待結果。如果你想執行所有程式,無論他們是不是被忽略的話,你可以執行 cargo test -- --include-ignored。","breadcrumbs":"編寫自動化測試 » 控制程式如何執行 » 忽略某些測試除非特別指定","id":"206","title":"忽略某些測試除非特別指定"},"207":{"body":"如同本章開頭提到的,測試是個複雜的領域,不同的人可能使用不同的術語與組織架構。Rust 社群將測試分為兩大分類術語:單元測試和整合測試。 單元測試 (unit tests)比較小且較專注,傾向在隔離環境中一次只測試一個模組,且能夠測試私有介面。 整合測試 (integration tests)對於你的函式庫來說是個完全外部的程式碼,所以會如其他外部程式碼一樣使用你的程式碼,只能使用公開介面且每個測試可能會有數個模組。 這兩種測試都很重要,且能確保函式庫每個部分能在分別或一起執行的情況下,如你預期的方式運作。","breadcrumbs":"編寫自動化測試 » 測試組織架構 » 測試組織架構","id":"207","title":"測試組織架構"},"208":{"body":"單元測試的目的是要在隔離其他程式碼的狀況下測試每個程式碼單元,迅速查明程式碼有沒有如預期或非預期的方式運作。你會將單元測試放在 src 目錄中每個你要測試的程式同個檔案下。我們常見的做法是在每個檔案建立一個模組 tests 來包含測試函式,並用 cfg(test) 來詮釋模組。 測試模組與 #[cfg(test)] 測試模組上的 #[cfg(test)] 詮釋會告訴 Rust 當你執行 cargo test 才會編譯並執行測試程式碼。而不是當你執行 cargo build。當你想要建構函式庫時,這能節省編譯時間並降低編譯出的檔案所佔的空間,因為這些測試沒有被包含到。整合測試位於不同目錄,所以它們不需要 #[cfg(test)]。但是因為單元測試與程式碼位於相同的檔案下,你需要使用 #[cfg(test)] 來指明它們不應該被包含在編譯結果。 回想一下本章節第一個段落中我們建立了一個新專案 adder,並用 Cargo 為我們產生以下程式碼: 檔案名稱:src/lib.rs #[cfg(test)]\nmod tests { #[test] fn it_works() { let result = 2 + 2; assert_eq!(result, 4); }\n} 此程式碼是自動產生的測試模組。cfg 屬性代表的是 configuration 並告訴 Rust 以下項目只有在給予特定配置選項時才會被考慮。在此例中配置選項是 test,這是 Rust 提供用來編譯與執行測試的選項。使用 cfg 屬性的話,Cargo 只有在我們透過 cargo test 執行測試時才會編譯我們的測試程式碼。這包含此模組能可能需要的輔助函式,以及用 #[test] 詮釋的測試函式。 測試私有函式 在測試領域的社群中對於是否應該直接測試私有函式一直存在著爭議,而且有些其他語言會讓測試私有函式變得很困難,甚至不可能。不管你認為哪個論點比較理想,Rust 的隱私權規則還是能讓你測試私有函式。考慮以下範例 11-12 擁有私有函式 internal_adder 的程式碼。 檔案名稱:src/lib.rs pub fn add_two(a: i32) -> i32 { internal_adder(a, 2)\n} fn internal_adder(a: i32, b: i32) -> i32 { a + b\n} #[cfg(test)]\nmod tests { use super::*; #[test] fn internal() { assert_eq!(4, internal_adder(2, 2)); }\n} 範例 11-12:測試私有函式 注意到函式 internal_adder 沒有標記為 pub。測試也只是 Rust 的程式碼,且 tests 也只是另一個模組。如同我們在 參考模組項目的路徑 段落討論到的,下層模組的項目可以使用該項目以上的模組。在此測試中,我們透過 use super::* 引入 test 模組上層的所有項目,所以測試能呼叫 internal_adder。如果你不認為私有函式應該測試,Rust 也沒有什麼好阻止你的地方。","breadcrumbs":"編寫自動化測試 » 測試組織架構 » 單元測試","id":"208","title":"單元測試"},"209":{"body":"在 Rust 中,整合測試對你的函式庫來說是完全外部的程式。它們使用你的函式庫的方式與其他程式碼一樣,所以它們只能呼叫屬於函式庫中公開 API 的函式。它們的目的是要測試你的函式庫數個部分一起運作時有沒有正確無誤。單獨運作無誤的程式碼單元可能會在整合時出現問題,所以整合測試的程式碼的涵蓋率也很重要。要建立整合測試,你需要先有個 tests 目錄。 tests 目錄 我們在專案目錄最上層在 src 旁建立一個 tests 目錄。Cargo 知道要從此目錄來尋找整合測試。我們接著就可以建立多少個測試都沒問題,Cargo 會編譯每個檔案成獨立的 crate。 讓我們來建立一個整合測試,將範例 11-12 的程式碼保留在 src/lib.rs 檔案中,然後建立一個 tests 目錄、一個叫做 tests/integration_test.rs 的檔案。你的目錄架構應該要長的像這樣: adder\n├── Cargo.lock\n├── Cargo.toml\n├── src\n│ └── lib.rs\n└── tests └── integration_test.rs 請在 tests/integration_test.rs 輸入範例 11-13 的程式碼: 檔案名稱:tests/integration_test.rs use adder; #[test]\nfn it_adds_two() { assert_eq!(4, adder::add_two(2));\n} 範例 11-13:adder crate 中函式的整合測試 tests 目錄的每個檔案都是獨立的 crate,所以我們需要將函式庫引入每個測試 crate 的作用域中。因此我們在程式最上方加了 use adder,這在單元測試是不需要的。 我們不用對 tests/integration_test.rs 的任何程式碼詮釋 #[cfg(test)]。Cargo 會特別對待 tests 目錄並只在我們執行 cargo test 時,編譯此目錄的檔案。現在請執行 cargo test: $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 1.31s Running unittests src/lib.rs (target/debug/deps/adder-1082c4b063a8fbe6) running 1 test\ntest tests::internal ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running tests/integration_test.rs (target/debug/deps/integration_test-1082c4b063a8fbe6) running 1 test\ntest it_adds_two ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 輸出結果中有三個段落,包含單元測試、整合測試與技術文件測試。要是有個段落的任何一個測試失敗的話,接下來的段落就不會執行。舉例來說,如果單元測試失敗了,我們就不會看到整合測試與技術文件測試的輸出,因為它們只會在所有單元測試都通過之後才會執行。 第一個段落的單元測試與我們看過的相同:每行會是每個單元測試(在此例是我們在範例 11-12 寫的 internal)最後附上單元測試的總結。 整合測試段落從 Running tests/integration_test.rs 開始,接著每行會是每個整合測試的測試函式,最後在 Doc-tests adder 段落開始前的那一行則是整合測試的總結結果。 每個整合測試檔案會有自己的段落,如果我們在 tests 目錄加入更多檔案的話,就會出現更多整合測試段落。 我們一樣能用測試函式的名稱來作為 cargo test 的引數,來執行特定整合測試。要執行特定整合測試檔案內的所有測試,可以用 --test 作為 cargo test 的引數並加上檔案名稱: $ cargo test --test integration_test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.64s Running tests/integration_test.rs (target/debug/deps/integration_test-82e7799c1bc62298) running 1 test\ntest it_adds_two ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 此命令會只執行 tests/integration_test.rs 檔案內的測試。 整合測試的子模組 隨著你加入的整合測試越多,你可能會想要在 tests 目錄下產生更多檔案來協助組織它們。舉例來說,你可以用測試函式測試的功能來組織它們。如同稍早提到的, tests 目錄下的每個檔案都會編譯成自己獨立的 crate,這有助於建立不同的作用域,這就像是使用者使用你的 crate 的可能環境。然而這也代表 tests 目錄的檔案不會和 src 的檔案行為一樣,也就是你在第七章學到如何拆開程式碼成模組與檔案的部分。 當你希望擁有一些能協助數個整合測試檔案的輔助函式,並遵循第七章的 「將模組拆成不同檔案」 段落來提取它們到一個通用模組時,你就會發現 tests 目錄下的檔案行為是不同的。舉例來說,我們建立了 tests/common.rs 並寫了一個函式 setup,然後我們希望 setup 能被不同測試檔案的數個測試函式呼叫: 檔案名稱:tests/common.rs pub fn setup() { // 在此設置測試函式庫會用到的程式碼\n} 當我們再次執行程式時,我們會看到測試輸出多了一個 common.rs 檔案的段落,就算該檔案沒有包含任何測試函式,而且我們也還沒有在任何地方呼叫 setup 函式: $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.89s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 1 test\ntest tests::internal ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running tests/common.rs (target/debug/deps/common-92948b65e88960b4) running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running tests/integration_test.rs (target/debug/deps/integration_test-92948b65e88960b4) running 1 test\ntest it_adds_two ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 讓 common 出現在測試結果並顯示 running 0 tests 並不是我們想做的事。我們只是想要分享一些程式碼給其他整合測試檔案而已。 要防止 common 出現在測試輸出,我們不該建立 tests/common.rs ,而是要建立 tests/common/mod.rs 。專案目錄現在應該要長的像這樣: ├── Cargo.lock\n├── Cargo.toml\n├── src\n│ └── lib.rs\n└── tests ├── common │ └── mod.rs └── integration_test.rs 這是另一個 Rust 知道的舊版命名形式,我們在第七章的 「其他種的檔案路徑」 段落有提過。這樣命名檔案的話會告訴 Rust 不要將 common 模組視為整合測試檔案。當我們將 setup 函式程式碼移到 tests/common/mod.rs 並刪除 tests/common.rs 檔案時,原本的段落就不會再出現在測試輸出。 tests 目錄下子目錄的檔案不會被編譯成獨立 crate 或在測試輸出顯示段落。 在我們建立 tests/common/mod.rs 之後,我們可以將它以模組的形式用在任何整合測試檔案中。以下是在 tests/integration_test.rs 的 it_adds_two 測試中呼叫函式 setup 的範例: 檔案名稱:tests/integration_test.rs use adder; mod common; #[test]\nfn it_adds_two() { common::setup(); assert_eq!(4, adder::add_two(2));\n} 注意到 mod common; 的宣告與我們在範例 7-21 說明的模組宣告方式一樣。之後在測試函式中,我們就可以呼叫函式 common::setup()。 執行檔 Crate 的整合測試 如果我們的專案只包含 src/main.rs 檔案的執行檔 crate 而沒有 src/lib.rs 檔案的話,我們無法在 tests 目錄下建立整合測試,也無法將 src/main.rs 檔案中定義的函式透過 use 陳述式引入作用域。只有函式庫 crate 能公開函式給其他 crate 使用,執行檔 crate 只用於獨自執行。 這也是為何 Rust 專案為執行檔提供直白的 src/main.rs 檔案並允許呼叫 src/lib.rs 檔案中的邏輯程式碼。使用這樣子的架構的話,整合測試 可以 透過 use 來測試函式庫 crate,並讓重點功能可以公開使用。如果重點功能可以運作的話,那 src/main.rs 檔案中剩下的程式碼部分也能夠如期執行,而這一小部分就不必特地做測試。","breadcrumbs":"編寫自動化測試 » 測試組織架構 » 整合測試","id":"209","title":"整合測試"},"21":{"body":"你將先建立一個目錄來儲存你的 Rust 程式碼。程式碼位於何處並不重要,但為了能好好練習書中的範例和專案,我們建議你可以在你的 home 目錄建立一個 projects 目錄然後將你所有的專案保存在此。 請開啟終端機然後輸入以下命令來建立 projects 目錄和另一個在 projects 目錄底下的真正要寫「Hello, world!」專案的目錄。 對於 Linux、macOS 和 Windows 的 PowerShell,請輸入: $ mkdir ~/projects\n$ cd ~/projects\n$ mkdir hello_world\n$ cd hello_world 對於 Windows CMD,請輸入: > mkdir \"%USERPROFILE%\\projects\"\n> cd /d \"%USERPROFILE%\\projects\"\n> mkdir hello_world\n> cd hello_world","breadcrumbs":"開始入門 » Hello, World! » 建立專案目錄","id":"21","title":"建立專案目錄"},"210":{"body":"Rust 的測試功能提供了判定程式碼怎樣才算正常運作的方法,以確保它能以你預期的方式運作,就算當你做了改變時也是如此。單元測試分別測試函式庫中每個不同的部分,且能測試私有實作細節。整合測試檢查函式庫數個部分一起執行時是否正確無誤,且它們使用函式庫公開 API 來測試程式碼的行為與外部程式碼使用的方式一樣。雖然 Rust 型別系統與所有權規則能避免某些種類的程式錯誤,測試還是減少邏輯程式錯誤的重要辦法,讓你的程式碼能如預期行為運作。 讓我們統整此章節以及之前的章節所學到的知識來寫一支專案吧!","breadcrumbs":"編寫自動化測試 » 測試組織架構 » 總結","id":"210","title":"總結"},"211":{"body":"本章節用來回顧你目前學過的許多技能,並探索些標準函式庫中的更多功能。我們會來建立個命令列工作來處理檔案與命令列輸入/輸出,以此練習些你已經掌握的 Rust 概念。 Rust 的速度、安全、單一執行檔輸出與跨平台支援使其成為建立命令列工具的絕佳語言。所以在我們的專案中,我們要寫出我們自己的經典命令列工具 grep( g lobally search a r egular e xpression and p rint)。在最簡單的使用場合中,grep 會搜尋指定檔案中的指定字串。為此 grep 會接收一個檔案名稱與一個字串作為其引數。然後它會讀取檔案、在該檔案中找到包含字串引數的行數,並印出這些行數。 在過程中,我們會展示如何讓我們的命令列工具和其他許多命令列工具一樣使用終端機的功能。我們會讀取一個環境變數的數值來讓使用者可以配置此工具的行為。我們還會將錯誤訊息在控制台中的標準錯誤(stderr)顯示而非標準輸出(stdout)。所以舉例來說,使用者可以將成功的標準輸出重新導向至一個檔案,並仍能在螢幕上看到錯誤訊息。 其中一位 Rust 社群成員 Andrew Gallant 已經有建立個功能完善且十分迅速的 grep 版本,叫做 ripgrep。相比之下,我們的版本會相對簡單許多,但此章節能給你些背景知識,來幫你理解像是 ripgrep 等真實專案。 我們的 grep 專案會組合你所學過的各種概念: 組織程式碼(使用你在 第七章 所學的模組) 使用向量與字串( 第八章 的集合) 錯誤處理( 第九章 ) 合理的使用特徵與生命週期( 第十章 ) 測試( 第十一章 ) 我們還會簡單介紹閉包、疊代器與特徵物件,這些在 第十三章 與 第十七章 會做詳細介紹。","breadcrumbs":"I/O 專案:建立一個命令列程式 » I/O 專案:建立一個命令列程式","id":"211","title":"I/O 專案:建立一個命令列程式"},"212":{"body":"一如往常我們用 cargo new 建立新的專案,我們將我們的專案命名為 minigrep 來與很可能已經在你系統中的 grep 工具做區別。 $ cargo new minigrep Created binary (application) `minigrep` project\n$ cd minigrep 第一項任務是要讓 minigrep 能接收兩個命令列引數:檔案路徑與欲搜尋的字串。也就是說,我們想要能夠使用 cargo run 加上兩條連字號來指示接下來的引號用於我們的程式而不是 cargo,然後輸入欲搜尋的字串與要被搜尋的檔案路徑來執行程式,如以下所示: $ cargo run -- searchstring example-filename.txt 但現在由 cargo new 產生的程式還無法處理我們給予的引數。 crates.io 有些函式庫可以幫助程式接收命令列中的引數,但有鑑於你要學習此概念,讓我們親自來實作一個。","breadcrumbs":"I/O 專案:建立一個命令列程式 » 接受命令列引數 » 接受命令列引數","id":"212","title":"接受命令列引數"},"213":{"body":"要讓 minigrep 能夠讀取我們傳入的命令列引數數值,我們需要使用 Rust 標準函式庫中提供的 std::env::args 函式。此函式會回傳一個包含我們傳給 minigrep 的命令列引數的疊代器(iterator)。我們會在 第十三章 詳細解釋疊代器。現在你只需要知道疊代器的兩項重點:疊代器會產生一系列的數值,然後我們可以對疊代器呼叫 collect 方法來將其轉換成像是向量的集合,來包含疊代器產生的所有元素。 範例 21-1 的程式碼能讓你的 minigrep 程式能夠讀取任何傳入的命令列引數,然後收集數值成一個向量。 檔案名稱:src/main.rs use std::env; fn main() { let args: Vec = env::args().collect(); dbg!(args);\n} 範例 12-1:收集命令列引數至向量中並顯示它們 首先我們透過 use 陳述式將 std::env 模組引入作用域,讓我們可以使用它的 args 函式。注意到 std::env::args 函式位於兩層模組下。如同我們在 第七章 談過的,如果我們要用的函式模組路徑超過一層以上的話,我們選擇將上層模組引入作用域中,而不是函式本身。這樣的話,我們可以輕鬆使用 std::env 中的其他函式。而且這也比直接加上 use std::env::args 然後只使用 args 來呼叫函式還要明確些,因為 args 容易被誤認成是由目前模組定義的函式。","breadcrumbs":"I/O 專案:建立一個命令列程式 » 接受命令列引數 » 讀取引數數值","id":"213","title":"讀取引數數值"},"214":{"body":"值得注意的是如果任何引數包含無效 Unicode 的話,std::env::args 就會恐慌。如果你的程式想要接受包含無效 Unicode 引數的話,請改使用 std::env::args_os。該函式回傳會產生 OsString 數值的疊代器,而非 String 數值。我們出於簡單方便所以在此使用 std::env::args,因為 OsString 在不同平台中數值會有所差異,且會比 String 數值還要難處理。 我們在 main 中的第一行呼叫 env::args,然後馬上使用 collect 來將疊代器轉換成向量,這會包含疊代器產生的所有數值。我們可以使用 collect 函式來建立許多種集合,所以我們顯式詮釋 args 的型別來指定我們想要字串向量。雖然我們很少需要在 Rust 中詮釋型別,collect 是其中一個你常常需要詮釋的函式,因為 Rust 無法推斷出你想要何種集合。 最後,我們使用除錯巨集來顯示向量。讓我們先嘗試不用引數來執行程式碼,再用兩個引數來執行: $ cargo run Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.61s Running `target/debug/minigrep`\n[src/main.rs:5] args = [ \"target/debug/minigrep\",\n] $ cargo run -- needle haystack Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 1.57s Running `target/debug/minigrep needle haystack`\n[src/main.rs:5] args = [ \"target/debug/minigrep\", \"needle\", \"haystack\",\n] 值得注意的是向量中第一個數值為 \"target/debug/minigrep\",這是我們的執行檔名稱。這與 C 的引數列表行為相符,讓程式在執行時能使用它們被呼叫的名稱路徑。存取程式名稱通常是很實用的,像是你能將它顯示在訊息中,或是依據程式被呼叫的命令列別名來改變程式的行為。但考慮本章節的目的,我們會忽略它並只儲存我們想要的兩個引數。","breadcrumbs":"I/O 專案:建立一個命令列程式 » 接受命令列引數 » args 函式與無效的 Unicode","id":"214","title":"args 函式與無效的 Unicode"},"215":{"body":"目前程式能夠取得命令列引數指定的數值。現在我們想要將這兩個引數存入變數中,讓我們可以在接下來的程式中使用數值,如範例 12-2 所示。 檔案名稱:src/main.rs use std::env; fn main() { let args: Vec = env::args().collect(); let query = &args[1]; let file_path = &args[2]; println!(\"搜尋 {}\", query); println!(\"目標檔案為 {}\", file_path);\n} 範例 12-2:建立變數來儲存搜尋引數與檔案路徑引數 如我們印出向量時所看到的,向量的第一個數值 args[0] 會是程式名稱,所以我們從引數 1 開始。minigrep 接收的第一個引數會是我們要搜尋的字串,所以我們將第一個引數的參考賦值給變數 query。第二個引數會是檔案路徑,所以我們將第二個引數的參考賦值給 file_path。 我們暫時印出這些變數的數值來證明程式碼運作無誤。讓我們用引數 test 與 sample.txt 來再次執行程式: $ cargo run -- test sample.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep test sample.txt`\n搜尋 test\n目標檔案為 sample.txt 很好,程式能執行!我們想要的引數數值都有儲存至正確的變數中。之後我們會對特定潛在錯誤的情形來加上一些錯誤處理,像是當使用者沒有提供引數的情況。現在我們先忽略這樣的情況,並開始加上讀取檔案的功能。","breadcrumbs":"I/O 專案:建立一個命令列程式 » 接受命令列引數 » 將引數數值儲存至變數","id":"215","title":"將引數數值儲存至變數"},"216":{"body":"現在我們要加上能夠讀取 file_path 中命令列引數指定的檔案功能。首先我們需要有個檔案範本能讓我們測試,我們可以建立一個文字檔,其中由數行重複的單字組成少量文字。範例 12-3 Emily Dickinson 的詩就是不錯的選擇!在專案根目錄建立一個檔案叫做 poem.txt ,然後輸入此詩「I’m Nobody! Who are you?」 檔案名稱:poem.txt I'm nobody! Who are you?\nAre you nobody, too?\nThen there's a pair of us - don't tell!\nThey'd banish us, you know. How dreary to be somebody!\nHow public, like a frog\nTo tell your name the livelong day\nTo an admiring bog! 範例 12-3:以 Emily Dickinson 的詩作為絕佳測試範本 有了這些文字,接著修改 src/main.rs 來加上讀取檔案的程式碼,如範例 12-4 所示。 檔案名稱:src/main.rs use std::env;\nuse std::fs; fn main() { // --省略--\n# let args: Vec = env::args().collect();\n# # let query = &args[1];\n# let file_path = &args[2];\n# # println!(\"搜尋 {}\", query); println!(\"目標檔案為 {}\", file_path); let contents = fs::read_to_string(file_path) .expect(\"應該要能夠讀取檔案\"); println!(\"文字內容:\\n{contents}\");\n} 範例 12-4:讀取第二個引數指定的檔案內容 首先,我們加上另一個 use 陳述式來將標準函式庫中的另一個相關部分引入:我們需要 std::fs 來處理檔案。 在 main 中,我們加上新的陳述式:fs::read_to_string 會接收 file_path、開啟該檔案並回傳檔案內容的 Result。 在陳述式之後,我們再次加上暫時的 println! 陳述式來在讀取檔案之後,顯示 contents 的數值,讓我們能檢查程式目前運作無誤。 讓我們用任何字串作為第一個命令列引數(因為我們還沒實作搜尋的部分)並與 poem.txt 檔案作為第二個引數來執行此程式碼: $ cargo run -- the poem.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep the poem.txt`\n搜尋 the\n目標檔案為 poem.txt\n文字內容:\nI'm nobody! Who are you?\nAre you nobody, too?\nThen there's a pair of us - don't tell!\nThey'd banish us, you know. How dreary to be somebody!\nHow public, like a frog\nTo tell your name the livelong day\nTo an admiring bog! 很好!程式碼有讀取並印出檔案內容。但此程式碼有些缺陷。main 函式負責太多事情了,通常如果每個函式都只負責一件事的話,函式才能清楚直白且易於維護。另一個問題是我們盡可能地處理錯誤。由於程式還很小,此缺陷不算什麼大問題,但隨著程式增長時,這會越來越難清楚地修正。在開發程式時盡早重構是很好的做法,因為重構少量的程式碼會比較簡單。接下來就讓我們開始吧。","breadcrumbs":"I/O 專案:建立一個命令列程式 » 讀取檔案 » 讀取檔案","id":"216","title":"讀取檔案"},"217":{"body":"為了改善我們的程式,我們需要修正四個問題,這與程式架構與如何處理潛在錯誤有關。首先,我們的 main 函式會處理兩件任務:它得解析引數並讀取檔案。隨著我們的程式增長,main 函式中要處理的任務就會增加。要是一個函式有這麼多責任,它就會越來越難理解、越難測試並且難在不破壞其他部分的情況下做改變。我們最好能將不同功能拆開,讓每個函式只負責一項任務。 而這也和第二個問題有關:雖然 query 與 file_path 是我們程式的設置變數,而變數 contents 則用於程式邏輯。隨著 main 增長,我們會需要引入越多變數至作用域中。而作用域中有越多變數,我們就越難追蹤每個變數的用途。我們最好是將設置變數集結成一個結構體,讓它們的用途清楚明白。 第三個問題是當讀取檔案失敗時,我們使用 expect 來印出錯誤訊息,但是錯誤訊息只印出 應該要能夠讀取檔案。讀取檔案可以有好幾種失敗的方式:舉例來說,檔案可能不存在,或是我們可能沒有權限能開啟它。目前不管原因為何,我們都只印出相同的錯誤訊息,這並沒有給使用者足夠的資訊! 第四,我們重複使用 expect 來處理不同錯誤,而如果有使用者沒有指定足夠的引數來執行程式的話,他們會從 Rust 獲得 index out of bounds 的錯誤,這並沒有清楚解釋問題。最好是所有的錯誤處理程式碼都可以位於同個地方,讓未來的維護者只需要在此處來修改錯誤處理的程式碼。將所有錯誤處理的程式碼置於同處也能確保我們能提供對終端使用者有意義的訊息。 讓我們來重構專案以解決這四個問題吧。","breadcrumbs":"I/O 專案:建立一個命令列程式 » 透過重構來改善模組性與錯誤處理 » 透過重構來改善模組性與錯誤處理","id":"217","title":"透過重構來改善模組性與錯誤處理"},"218":{"body":"main 函式負責多數任務的組織分配問題在許多執行檔專案中都很常見。所以 Rust 社群開發出了一種流程,這在當 main 開始變大時,能作為分開執行檔程式中任務的指導原則。此流程有以下步驟: 將你的程式分成 main.rs 與 lib.rs 並將程式邏輯放到 lib.rs 。 只要你的命令列解析邏輯很小,它可以留在 main.rs 。 當命令行解析邏輯變得複雜時,就將其從 main.rs 移至 lib.rs 。 在此流程之後的 main 函式應該要只負責以下任務: 透過引數數值呼叫命令列解析邏輯 設置任何其他的配置 呼叫 lib.rs 中的 run 函式 如果 run 回傳錯誤的話,處理該錯誤 此模式用於分開不同任務: main.rs 處理程式的執行,然後 lib.rs 處理眼前的所有任務邏輯。因為你無法直接測試 main,此架構讓你能測試所有移至 lib.rs 的程式函式邏輯。留在 main.rs 的程式碼會非常小,所以容易直接用閱讀來驗證。讓我們用此流程來重構程式吧。 提取引數解析器 我們會提取解析引數的功能到一個 main 會呼叫的函式中,以將命令列解析邏輯妥善地移至 src/lib.rs 。範例 12-5 展示新的 main 會呼叫新的函式 parse_config,而此函式我們先暫時留在 src/main.rs 。 檔案名稱:src/main.rs # use std::env;\n# use std::fs;\n# fn main() { let args: Vec = env::args().collect(); let (query, file_path) = parse_config(&args); // --省略--\n# # println!(\"搜尋 {}\", query);\n# println!(\"目標檔案為 {}\", file_path);\n# # let contents = fs::read_to_string(file_path)\n# .expect(\"應該要能夠讀取檔案\");\n# # println!(\"文字內容:\\n{contents}\");\n} fn parse_config(args: &[String]) -> (&str, &str) { let query = &args[1]; let file_path = &args[2]; (query, file_path)\n} 範例 12-5:從 main 提取 parse_config 函式 我們仍然收集命令列引數至向量中,但不同於在 main 函式中將索引 1 的引數數值賦值給變數 query 且將索引 2 的引數數值賦值給變數 file_path,我們將整個向量傳至 parse_config 函式。parse_config 函式會擁有決定哪些引數要賦值給哪些變數的邏輯,並將數值回傳給 main。我們仍然在 main 中建立變數 query and file_path,但 main 不再負責決定命令列引數與變數之間的關係。 此重構可能對我們的小程式來說有點像是殺雞焉用牛刀,但是我們正一小步一小步地累積重構。做了這項改變後,請再次執行程式來驗證引數解析有沒有正常運作。經常檢查你的進展是很好的,這能幫助你找出問題發生的原因。 集結配置數值 我們可以再進一步改善 parse_config 函式。目前我們回傳的是元組,但是我們馬上又將元組拆成獨立部分。這是個我們還沒有建立正確抽象的信號。 另外一個告訴我們還有改善空間的地方是 parse_config 名稱中的 config,這指示我們回傳的兩個數值是相關的,且都是配置數值的一部分。我們現在沒有確實表達出這樣的資料結構,而只有將兩個數值組合成一個元組而已,我們可以將這兩個數值存入一個結構體,並對每個結構體欄位給予有意義的名稱。這樣做能讓未來的維護者可以清楚知道這些數值的不同與關聯,以及它們的用途。 範例 12-6 改善了 parse_config 函式。 檔案名稱:src/main.rs # use std::env;\n# use std::fs;\n# fn main() { let args: Vec = env::args().collect(); let config = parse_config(&args); println!(\"搜尋 {}\", config.query); println!(\"目標檔案為 {}\", config.file_path); let contents = fs::read_to_string(config.file_path) .expect(\"應該要能夠讀取檔案\"); // --省略--\n# # println!(\"文字內容:\\n{contents}\");\n} struct Config { query: String, file_path: String,\n} fn parse_config(args: &[String]) -> Config { let query = args[1].clone(); let file_path = args[2].clone(); Config { query, file_path }\n} 範例 12-6:重構 parse_config 來回傳 Config 結構體實例 我們定義了一個結構體 Config 其欄位有 query 與 file_path。parse_config 的簽名現在指明它會回傳一個 Config 數值。在 parse_config 的本體中,我們原先回傳 args 中 String 數值參考的字串切片,現在我們定義 Config 來包含具所有權的 String 數值。main 中的 args 變數是引數數值的擁有者,而且只是借用它們給 parse_config 函式,這意味著如果 Config 嘗試取得 args 中數值的所有權的話,我們會違反 Rust 的借用規則。 我們可以用許多不同的方式來管理 String 的資料,最簡單(卻較無效率)的方式是對數值呼叫 clone 方法。這會複製整個資料讓 Config 能夠擁有,這會比參考字串資料還要花時間與記憶體。然而克隆資料讓我們的程式碼比較直白,因為在此情況下我們就不需要管理參考的生命週期,犧牲一點效能以換取簡潔性是值得的。","breadcrumbs":"I/O 專案:建立一個命令列程式 » 透過重構來改善模組性與錯誤處理 » 分開執行檔專案的任務","id":"218","title":"分開執行檔專案的任務"},"219":{"body":"由於 clone 會有運行時消耗,所以許多 Rustaceans 傾向於避免使用它來修正所有權問題。在 第十三章 中,你會學到如何更有效率的處理這種情況。但現在我們可以先複製字串來繼續進行下去,因為你只複製了一次,而且檔案路徑與搜尋字串都算很小。先寫出較沒有效率但可執行的程式會比第一次就要過分優化還來的好。隨著你對 Rust 越熟練,你的確就可以從有效率的解決方案開始,但現在呼叫 clone 是完全可以接受的。 我們更新 main 來將 parse_config 回傳的 Config 實例儲存至 config 變數中,並更新之前分別使用變數 query 與 file_path 的程式碼段落來改使用 Config 結構體中的欄位。 現在我們的程式碼更能表達出 query 與 file_path 是相關的,而且它們的目的是配置程式的行為。任何使用這些數值的程式碼都會從 config 實例中的欄位名稱知道它們的用途。 建立 Config 的建構子 目前我們將負責解析命令列引數的邏輯從 main 移至 parse_config 函式。這樣做能幫助我們理解 query 與 file_path 數值是相關的,且此關係應該要能在我們的程式碼中表達出來。然後我們增加了結構體 Config 來描述 query 與 file_path 的相關性,並在 parse_config 函式中將數值名稱作為結構體欄位名稱來回傳。 所以現在 parse_config 函式的目的是要建立 Config 實例,我們可以將 parse_config 從普通的函式變成與 Config 結構體相關連的 new 函式。這樣做能讓程式碼更符合慣例。我們可以對像是 String 等標準函式庫中的型別呼叫 String::new 來建立實例。同樣地,透過將 parse_config 改為 Config 的關聯函式 new,我們可以透過呼叫 Config::new 來建立 Config 的實例。範例 12-7 正是我們要作出的改變。 檔案名稱:src/main.rs # use std::env;\n# use std::fs;\n# fn main() { let args: Vec = env::args().collect(); let config = Config::new(&args);\n# # println!(\"搜尋 {}\", config.query);\n# println!(\"目標檔案為 {}\", config.file_path);\n# # let contents = fs::read_to_string(config.file_path)\n# .expect(\"應該要能夠讀取檔案\");\n# # println!(\"文字內容:\\n{contents}\"); // --省略--\n} // --省略-- # struct Config {\n# query: String,\n# file_path: String,\n# }\n# impl Config { fn new(args: &[String]) -> Config { let query = args[1].clone(); let file_path = args[2].clone(); Config { query, file_path } }\n} 範例 12-7:變更 parse_config 成 Config::new 我們更新了 main 原先呼叫 parse_config 的地方來改呼叫 Config::new。我們變更了 parse_config 的名稱成 new 並移入 impl 區塊中,讓 new 成為 Config 的關聯函式。請嘗試再次編譯此程式碼來確保它能執行。","breadcrumbs":"I/O 專案:建立一個命令列程式 » 透過重構來改善模組性與錯誤處理 » 使用 clone 的權衡取捨","id":"219","title":"使用 clone 的權衡取捨"},"22":{"body":"接著,請產生一個全新原始碼檔案並命名為 main.rs 。Rust 的文件檔案都會以 .rs 副檔名稱作為結尾。如果你用到不只一個單字的話,慣例上是用底線區隔開來。比方說,請使用 hello_world.rs 而不是 helloworld.rs 。 現在請開啟 main.rs 檔案然而後輸入範例 1-1 中的程式碼。 檔案名稱:main.rs fn main() { println!(\"Hello, world!\");\n} 範例 1-1:印出「Hello, world!」的程式 儲存檔案然後回到你的專案目錄底下 ~/projects/hello_world 。在 Linux 或 macOS 上,請輸入以下命令來編譯並執行檔案: $ rustc main.rs\n$ ./main\nHello, world! 在 Windows 上則輸入 .\\main.exe 而非 ./main: > rustc main.rs\n> .\\main.exe\nHello, world! 不管你的作業系統為何,終端機上應該都會出現 Hello, world!。如果你沒有看到,可以回到安裝章節中的 「疑難排除」 尋求協助。 如果 Hello, world! 有印出來,那麼恭喜你!你正式寫了一支 Rust 程式,所以你也正式成為 Rust 開發者了——歡迎加入!","breadcrumbs":"開始入門 » Hello, World! » 編寫並執行 Rust 程式","id":"22","title":"編寫並執行 Rust 程式"},"220":{"body":"現在我們要來修正錯誤處理。回想一下要是 args向量中的項目太少的話,嘗試取得向量中索引 1 或索引 2 的數值的話可能就會導致程式恐慌。試著不用任何引數執行程式的話,它會產生以下結果: $ cargo run Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep`\nthread 'main' panicked at 'index out of bounds: the len is 1 but the index is 1', src/main.rs:27:21\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace index out of bounds: the len is 1 but the index is 1 這行是給程式設計師看的錯誤訊息。這無法協助我們的終端使用者理解他們該怎麼處理。讓我們來修正吧。 改善錯誤訊息 在範例 12-8 中,我們在 new 函式加上了一項檢查來驗證 slice 是否夠長,接著才會取得索引 1 和 2。如果 slice 不夠長的話,程式就會恐慌並顯示更清楚的錯誤訊息。 檔案名稱:src/main.rs # use std::env;\n# use std::fs;\n# # fn main() {\n# let args: Vec = env::args().collect();\n# # let config = Config::new(&args);\n# # println!(\"搜尋 {}\", config.query);\n# println!(\"目標檔案為 {}\", config.file_path);\n# # let contents = fs::read_to_string(config.file_path)\n# .expect(\"應該要能夠讀取檔案\");\n# # println!(\"文字內容:\\n{contents}\");\n# }\n# # struct Config {\n# query: String,\n# file_path: String,\n# }\n# # impl Config { // --省略-- fn new(args: &[String]) -> Config { if args.len() < 3 { panic!(\"引數不足\"); } // --省略--\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # Config { query, file_path }\n# }\n# } 範例 12-8:新增對引數數量的檢查 此程式碼類似於我們在 範例 9-13 寫的 Guess::new 函式 ,在那裡當 value 超出有效數值的範圍時,我們就呼叫 panic!。然而在此我們不是檢查數值的範圍,而是檢查 args 的長度是否至少為 3,然後函式剩餘的段落都能在假設此條件成立情況下正常執行。如果 args 的項目數量少於三的話,此條件會為真,然後我們就會立即呼叫 panic! 巨集來結束程式。 在 new 多了這些額外的程式碼之後,讓我們不用任何引數再次執行程式,來看看錯誤訊息為何: $ cargo run Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep`\nthread 'main' panicked at '引數不足', src/main.rs:26:13\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace 這樣的輸出就好多了,我們現在有個合理的錯誤訊息。然而我們還是顯示了一些額外資訊給使用者。也許在此使用範例 9-13 的技巧並不是最好的選擇,如同 第九章所提及的 ,panic! 的呼叫比較屬於程式設計問題,而不是使用問題。我們可以改使用第九章的其他技巧,像是 回傳 Result 來表達是成功還是失敗。 回傳 Result 而非呼叫 panic! 我們可以回傳 Result 數值,在成功時包含 Config 的實例並在錯誤時描述問題原因。我們也將函式名稱從 new 改為 build,因為許多開發者通常預期 new 函式不會失敗。當 Config::build 與 main 溝通時,我們可以使用 Result 型別來表達這裡有問題發生。然後我們改變 main 來將 Err 變體轉換成適當的錯誤訊息給使用者,而不是像呼叫 panic! 時出現圍繞著 thread 'main' 與 RUST_BACKTRACE 的文字。 範例 12-9 顯示我們得改變 Config::build 的回傳值並讓函式本體回傳 Result。注意到這還不能編譯,直到我們也更新 main 為止,這會在下個範例解釋。 檔案名稱:src/main.rs # use std::env;\n# use std::fs;\n# # fn main() {\n# let args: Vec = env::args().collect();\n# # let config = Config::new(&args);\n# # println!(\"搜尋 {}\", config.query);\n# println!(\"目標檔案為 {}\", config.file_path);\n# # let contents = fs::read_to_string(config.file_path)\n# .expect(\"應該要能夠讀取檔案\");\n# # println!(\"文字內容:\\n{contents}\");\n# }\n# # struct Config {\n# query: String,\n# file_path: String,\n# }\n# impl Config { fn build(args: &[String]) -> Result { if args.len() < 3 { return Err(\"引數不足\"); } let query = args[1].clone(); let file_path = args[2].clone(); Ok(Config { query, file_path }) }\n} 範例 12-9:從 Config::build 回傳 Result 我們的 build 函式現在會回傳 Result,在成功時會有 Config 實例,而在錯誤時會有個 &'static str。我們的錯誤值永遠會是有 'static 生命週期的字串字面值。 我們在 build 函式本體作出了兩項改變:不同於呼叫 panic!,當使用者沒有傳遞足夠引數時,我們現在會回傳 Err 數值。此外我們也將 Config 封裝進 Ok 作為回傳值。這些改變讓函式能符合其新的型別簽名。 從 Config::build 回傳 Err 數值讓 main 函式能處理 build 函式回傳的 Result 數值,並明確地在錯誤情況下離開程序。 呼叫 Config::build 並處理錯誤 為了能處理錯誤情形並印出對使用者友善的訊息,我們需要更新 main 來處理 Config::build 回傳的 Result,如範例 12-10 所示。我們還要負責用一個非零的錯誤碼來離開命令列工具,這原先是 panic! 會處理的,現在我們得自己實作。非零退出狀態是個常見信號,用來告訴呼叫程式的程序,該程式離開時有個錯誤狀態。 檔案名稱:src/main.rs # use std::env;\n# use std::fs;\nuse std::process; fn main() { let args: Vec = env::args().collect(); let config = Config::build(&args).unwrap_or_else(|err| { println!(\"解析引數時出現問題:{err}\"); process::exit(1); }); // --省略--\n# # println!(\"搜尋 {}\", config.query);\n# println!(\"目標檔案為 {}\", config.file_path);\n# # let contents = fs::read_to_string(config.file_path)\n# .expect(\"應該要能夠讀取檔案\");\n# # println!(\"文字內容:\\n{contents}\");\n# }\n# # struct Config {\n# query: String,\n# file_path: String,\n# }\n# # impl Config {\n# fn build(args: &[String]) -> Result {\n# if args.len() < 3 {\n# return Err(\"引數不足\");\n# }\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # Ok(Config { query, file_path })\n# }\n# } 範例 12-10:如果建立新的 Config 失敗時會用錯誤碼離開 在此範例中,我們使用一個還沒詳細介紹的方法 unwrap_or_else,這定義在標準函式庫的 Result 中。使用 unwrap_or_else 讓我們能定義一些自訂的非 panic! 錯誤處理。如果 Result 數值為 Ok,此方法行為就類似於 unwrap,它會回傳Ok 所封裝的內部數值。然而,如果數值為 Err 的話,此方法會呼叫 閉包 (closure)內的程式碼,這會是由我們所定義的匿名函式並作為引數傳給 unwrap_or_else。我們會在 第十三章 詳細介紹閉包。現在你只需要知道 unwrap_or_else 會傳遞 Err 的內部數值,在此例中就是我們在範例 12-9 新增的靜態字串「引數不足」,將此數值傳遞給閉包中兩條直線之間的 err 引數。閉包內的程式碼就可以在執行時使用 err 數值。 我們新增了一行 use 來將標準函式庫中的 process 引入作用域。在錯誤情形下要執行的閉包程式碼只有兩行:我們印出 err 數值並呼叫 process::exit。process::exit 函式會立即停止程式並回傳給予的數字來作為退出狀態碼。這與範例 12-8 我們使用 panic! 來處理的方式類似,但我們不再顯示多餘的輸出結果。讓我們試試看: $ cargo run Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.48s Running `target/debug/minigrep`\n解析引數時出現問題:引數不足 很好!這樣的輸出結果對使用者友善多了。","breadcrumbs":"I/O 專案:建立一個命令列程式 » 透過重構來改善模組性與錯誤處理 » 修正錯誤處理","id":"220","title":"修正錯誤處理"},"221":{"body":"現在我們完成配置解析的重構了,接下來輪到程式邏輯了。如同我們在 「分開執行檔專案的任務」 中所提及的,我們會提取一個函式叫做 run,這會存有目前 main 函式中除了設置配置或處理錯誤以外的所有邏輯。當我們完成後,main 會變得非常簡潔,且能輕鬆用肉眼來驗證,然後我就能對所有其他邏輯進行測試了。 範例 12-11 提取了 run 函式。目前我們在提取函式時,會逐步作出小小的改善。我們仍然在 src/main.rs 底下定義函式。 檔案名稱:src/main.rs # use std::env;\n# use std::fs;\n# use std::process;\n# fn main() { // --省略-- # let args: Vec = env::args().collect();\n# # let config = Config::build(&args).unwrap_or_else(|err| {\n# println!(\"解析引數時出現問題:{err}\");\n# process::exit(1);\n# });\n# println!(\"搜尋 {}\", config.query); println!(\"目標檔案為 {}\", config.file_path); run(config);\n} fn run(config: Config) { let contents = fs::read_to_string(config.file_path) .expect(\"應該要能夠讀取檔案\"); println!(\"文字內容:\\n{contents}\");\n} // --省略--\n# # struct Config {\n# query: String,\n# file_path: String,\n# }\n# # impl Config {\n# fn build(args: &[String]) -> Result {\n# if args.len() < 3 {\n# return Err(\"引數不足\");\n# }\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # Ok(Config { query, file_path })\n# }\n# } 範例 12-11:提取 run 函式來包含剩餘的程式邏輯 run 現在會包含 main 中從讀取文件開始的所有剩餘邏輯。run 函式會接收 Config 實例來作為引數。 從 run 函式回傳錯誤 隨著剩餘程式邏輯都移至 run 函式,我們可以像範例 12-9 的 Config::build 一樣來改善錯誤處理。不同於讓程式呼叫 expect 來恐慌,當有問題發生時,run 函式會回傳 Result。這能讓我們進一步穩固 main 中對使用者友善的處理錯誤邏輯。範例 12-12 展示我們對 run 的簽名與本體所需要做的改變。 檔案名稱:src/main.rs # use std::env;\n# use std::fs;\n# use std::process;\nuse std::error::Error; // --省略-- # # fn main() {\n# let args: Vec = env::args().collect();\n# # let config = Config::build(&args).unwrap_or_else(|err| {\n# println!(\"解析引數時出現問題:{err}\");\n# process::exit(1);\n# });\n# # println!(\"搜尋 {}\", config.query);\n# println!(\"目標檔案為 {}\", config.file_path);\n# # run(config);\n# }\n# fn run(config: Config) -> Result<(), Box> { let contents = fs::read_to_string(config.file_path)?; println!(\"文字內容:\\n{contents}\"); Ok(())\n}\n# # struct Config {\n# query: String,\n# file_path: String,\n# }\n# # impl Config {\n# fn build(args: &[String]) -> Result {\n# if args.len() < 3 {\n# return Err(\"引數不足\");\n# }\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # Ok(Config { query, file_path })\n# }\n# } 範例 12-12:變更 run 函式來回傳 Result 我們在此做了三項明顯的修改。首先,我們改變了 run 函式的回傳型別為 Result<(), Box>,此函式之前回傳的是單元型別 (),而它現在仍作為 Ok 條件內的數值。 對於錯誤型別,我們使用 特徵物件(trait object) Box(然後我們在最上方透過 use 陳述式來將 std::error::Error 引入作用域)。我們會在 第十七章 討論特徵物件。現在你只需要知道 Box 代表函式會回傳有實作 Error 特徵的型別,但我們不必指定回傳值的明確型別。這增加了回傳錯誤數值的彈性,其在不同錯誤情形中可能有不同的型別。dyn 關鍵字是「動態(dynamic)」的縮寫。 再來,我們移除了 expect 的呼叫並改為 第九章 所介紹的 ? 運算子。所以與其對錯誤 panic!,? 會回傳當前函式的錯誤數值,並交由呼叫者處理。 第三,run 函式現在成功時會回傳 Ok 數值。我們在 run 函式簽名中的成功型別為 (),這意味著我們需要將單元型別封裝進 Ok 數值。Ok(()) 這樣的語法一開始看可能會覺得有點奇怪,但這樣子使用 () 的確符合慣例,說明我們呼叫 run 只是為了它的副作用,它不會回傳我們需要的數值。 當你執行此程式時,它雖然能編譯但會顯示一個警告: $ cargo run the poem.txt Compiling minigrep v0.1.0 (file:///projects/minigrep)\nwarning: unused `Result` that must be used --> src/main.rs:19:5 |\n19 | run(config); | ^^^^^^^^^^^^ | = note: `#[warn(unused_must_use)]` on by default = note: this `Result` may be an `Err` variant, which should be handled warning: `minigrep` (bin \"minigrep\") generated 1 warning Finished dev [unoptimized + debuginfo] target(s) in 0.71s Running `target/debug/minigrep the poem.txt`\n搜尋 the\n目標檔案為 poem.txt\n文字內容:\nI'm nobody! Who are you?\nAre you nobody, too?\nThen there's a pair of us - don't tell!\nThey'd banish us, you know. How dreary to be somebody!\nHow public, like a frog\nTo tell your name the livelong day\nTo an admiring bog! Rust 告訴我們程式碼忽略了 Result 數值且 Result 數值可能代表會有錯誤發生。但我們沒有檢查是不是會發生錯誤,所以編譯器提醒我們可能要在此寫些錯誤處理的程式碼!我們現在就來修正此問題。 在 main 中處理 run 回傳的錯誤 我們會用類似範例 12-10 中處理 Config::build 的技巧來處理錯誤,不過會有一些差別: 檔案名稱:src/main.rs # use std::env;\n# use std::error::Error;\n# use std::fs;\n# use std::process;\n# fn main() { // --省略-- # let args: Vec = env::args().collect();\n# # let config = Config::build(&args).unwrap_or_else(|err| {\n# println!(\"解析引數時出現問題:{err}\");\n# process::exit(1);\n# });\n# println!(\"搜尋 {}\", config.query); println!(\"目標檔案為 {}\", config.file_path); if let Err(e) = run(config) { println!(\"應用程式錯誤:{e}\"); process::exit(1); }\n}\n# # fn run(config: Config) -> Result<(), Box> {\n# let contents = fs::read_to_string(config.file_path)?;\n# # println!(\"文字內容:\\n{contents}\");\n# # Ok(())\n# }\n# # struct Config {\n# query: String,\n# file_path: String,\n# }\n# # impl Config {\n# fn new(args: &[String]) -> Result {\n# if args.len() < 3 {\n# return Err(\"引數不足\");\n# }\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # Ok(Config { query, file_path })\n# }\n# } 我們使用 if let 而非 unwrap_or_else 來檢查 run 是否有回傳 Err 數值,並以此呼叫 process::exit(1)。run 函式沒有回傳數值,所以我們不必像處理 Config::build 得用 unwrap 取得 Config 實例。因為 run 在成功時會回傳 (),而我們只在乎偵測錯誤,所以我們不需要 unwrap_or_else 來回傳解封裝後的數值,因為它只會是 ()。 if let 的本體與 unwrap_or_else 函式則都做一樣的事情:印出錯誤並離開。","breadcrumbs":"I/O 專案:建立一個命令列程式 » 透過重構來改善模組性與錯誤處理 » 從 main 提取邏輯","id":"221","title":"從 main 提取邏輯"},"222":{"body":"我們的 minigrep 專案目前看起來不錯!接下來我們要將 src/main.rs 檔案分開來,將一些程式碼放入 src/lib.rs 檔案中。這樣讓我們可以測試程式碼,並讓 src/main.rs 檔案的負擔變得少一點。 讓我們將 main 以外的所有程式碼從 src/main.rs 移到 src/lib.rs : run 函式定義 相關的 use 陳述式 Config 的定義 Config::build 的函式定義 src/lib.rs 的內容應該要如範例 12-13 所示(為了簡潔,我們省略了函式本體)。注意到這還無法編譯,直到我們也修改 src/main.rs 成範例 12-14 為止。 檔案名稱:src/lib.rs use std::error::Error;\nuse std::fs; pub struct Config { pub query: String, pub file_path: String,\n} impl Config { pub fn build(args: &[String]) -> Result { // --省略--\n# if args.len() < 3 {\n# return Err(\"引數不足\");\n# }\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # Ok(Config { query, file_path }) }\n} pub fn run(config: Config) -> Result<(), Box> { // --省略--\n# let contents = fs::read_to_string(config.file_path)?;\n# # println!(\"文字內容:\\n{contents}\");\n# # Ok(())\n} 範例 12-13:將 Config 與 run 移至 src/lib.rs 我們對許多項目都使用了 pub 關鍵字,這包含 Config 與其欄位,以及其 build 方法,還有 run 函式。我們現在有個函式庫會提供公開 API 能讓我們來測試! 現在我們需要將移至 src/lib.rs 的程式碼引入執行檔 crate 的 src/main.rs 作用域中,如範例 12-14 所示。 檔案名稱:src/main.rs use std::env;\nuse std::process; use minigrep::Config; fn main() { // --省略--\n# let args: Vec = env::args().collect();\n# # let config = Config::build(&args).unwrap_or_else(|err| {\n# println!(\"解析引數時出現問題:{err}\");\n# process::exit(1);\n# });\n# # println!(\"搜尋 {}\", config.query);\n# println!(\"目標檔案為 {}\", config.file_path);\n# if let Err(e) = minigrep::run(config) { // --省略--\n# println!(\"應用程式錯誤:{e}\");\n# process::exit(1); }\n} 範例 12-14:在 src/main.rs 使用 minigrep 函式庫 crate 我們加上 use minigrep::Config 這行來將 Config 型別從函式庫 crate 引入執行檔 crate 的作用域中,然後我們使用 run 函式的方式是在其前面再加上 crate 的名稱。現在所有的功能都應該正常並能執行了。透過 cargo run 來執行程式並確保一切正常。 哇!辛苦了,不過我們為未來的成功打下了基礎。現在處理錯誤就輕鬆多了,而且我們讓程式更模組化。現在幾乎所有的工作都會在 src/lib.rs 中進行。 讓我們利用這個新的模組化優勢來進行些原本在舊程式碼會很難處理的工作,但在新的程式碼會變得非常容易,那就是寫些測試!","breadcrumbs":"I/O 專案:建立一個命令列程式 » 透過重構來改善模組性與錯誤處理 » 將程式碼拆到函式庫 Crate","id":"222","title":"將程式碼拆到函式庫 Crate"},"223":{"body":"現在我們提取邏輯到 src/lib.rs 並在 src/main.rs 留下引數收集與錯誤處理的任務,現在對程式碼中的核心功能進行測試會簡單許多。我們可以使用各種引數直接呼叫函式來檢查回傳值,而不用從命令列呼叫我們的執行檔。 在此段落中,我們會在 minigrep 程式中利用測試驅動開發(Test-driven development,TDD)來新增搜尋邏輯。此程式開發技巧遵循以下步驟: 寫出一個會失敗的測試並執行它來確保它失敗的原因如你所預期。 寫出或修改足夠的程式碼來讓新測試可以通過。 重構你新增或變更的程式碼並確保測試仍能持續通過。 重複第一步! 雖然這只是編寫軟體的許多方式之一,但 TDD 也有助於程式碼的設計。在寫出能通過測試的程式碼之前先寫好測試能夠協助在開發過程中維持高測試覆蓋率。 我們將用測試驅動功能的實作,而要實作的功能就是在檔案內容中找到欲搜尋的字串,並產生符合查詢字串的行數列表。我們會在一個叫做 search 的函式新增此功能。","breadcrumbs":"I/O 專案:建立一個命令列程式 » 透過測試驅動開發完善函式庫功能 » 透過測試驅動開發完善函式庫功能","id":"223","title":"透過測試驅動開發完善函式庫功能"},"224":{"body":"讓我們移除 src/lib.rs 與 src/main.rs 中用來檢查程式行為的 println! 陳述式,因為我們不再需要它們了。然後在 src/lib.rs 中,我們加上 tests 模組與一個測試函式,如我們在 第十一章 所做的一樣。測試函式會指定我們希望 search 函式所能擁有的行為,它會接收搜尋字串與一段要被搜尋的文字,然後它只回傳文字中包含該搜尋字串的行數。範例 12-15 展示了此測試,但還不能編譯。 檔案名稱:src/lib.rs # use std::error::Error;\n# use std::fs;\n# # pub struct Config {\n# pub query: String,\n# pub file_path: String,\n# }\n# # impl Config {\n# pub fn build(args: &[String]) -> Result {\n# if args.len() < 3 {\n# return Err(\"引數不足\");\n# }\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # Ok(Config { query, file_path })\n# }\n# }\n# # pub fn run(config: Config) -> Result<(), Box> {\n# let contents = fs::read_to_string(config.file_path)?;\n# # Ok(())\n# }\n# #[cfg(test)]\nmod tests { use super::*; #[test] fn one_result() { let query = \"duct\"; let contents = \"\\\nRust:\nsafe, fast, productive.\nPick three.\"; assert_eq!(vec![\"safe, fast, productive.\"], search(query, contents)); }\n} 範例 12-15:建立一個我們預期 search 函式該有的行為的失敗測試 此測試搜尋字串 \"duct\"。而要被搜尋的文字有三行,只有一行包含 \"duct\"(在雙引號開頭後方的斜線會告訴 Rust 別在此字串內容開始處換行)。我們判定 search 函式回傳的數值只會包含我們預期的那一行。 我們還無法執行此程式並觀察其失敗,因為測試還無法編譯,search 函式根本還不存在!按照 TDD 的準則,我們只要加上足夠的程式碼讓測試可以編譯並執行,而我們要加上的是 search 函式的定義並永遠回傳一個空的向量,如範例 12-16 所示。然後測試應該就能編譯並失敗,因為空向量並不符合包含 \"safe, fast, productive.\" 此行的向量。 檔案名稱:src/lib.rs # use std::error::Error;\n# use std::fs;\n# # pub struct Config {\n# pub query: String,\n# pub file_path: String,\n# }\n# # impl Config {\n# pub fn build(args: &[String]) -> Result {\n# if args.len() < 3 {\n# return Err(\"引數不足\");\n# }\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # Ok(Config { query, file_path })\n# }\n# }\n# # pub fn run(config: Config) -> Result<(), Box> {\n# let contents = fs::read_to_string(config.file_path)?;\n# # Ok(())\n# }\n# pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { vec![]\n}\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# fn one_result() {\n# let query = \"duct\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\";\n# # assert_eq!(vec![\"safe, fast, productive.\"], search(query, contents));\n# }\n# } 範例 12-16:定義足夠的 search 函式讓我們的測試能夠編譯 值得注意的是在 search 的簽名中需要定義一個顯式的生命週期 'a,並用於 contents 引數與回傳值。回想一下在 第十章 中生命週期參數會連結引數生命週期與回傳值生命週期。在此例中,我們指明回傳值應包含字串切片且其會參考 contents 引數的切片(而非引數 query)。 換句話說,我們告訴 Rust search 函式回傳的資料會跟傳遞給 search 函式的引數 contents 資料存活的一樣久。這點很重要!被切片參考的資料必須有效,這樣其參考才會有效。如果編譯器假設是在建立 query 而非 contents 的字串切片,它的安全檢查就會不正確。 如果我們忘記詮釋生命週期並嘗試編譯此函式,我們會得到以下錯誤: $ cargo build Compiling minigrep v0.1.0 (file:///projects/minigrep)\nerror[E0106]: missing lifetime specifier --> src/lib.rs:28:51 |\n28 | pub fn search(query: &str, contents: &str) -> Vec<&str> { | ---- ---- ^ expected named lifetime parameter | = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `query` or `contents`\nhelp: consider introducing a named lifetime parameter |\n28 | pub fn search<'a>(query: &'a str, contents: &'a str) -> Vec<&'a str> { | ++++ ++ ++ ++ For more information about this error, try `rustc --explain E0106`.\nerror: could not compile `minigrep` due to previous error Rust 無法知道這兩個引數哪個才是我們需要的,所以我們得告訴它。由於引數 contents 包含所有文字且我們想要回傳符合條件的部分文字,所以我們知道 contents 引數要用生命週期語法與回傳值做連結。 其他程式設計語言不會要求你要在簽名中連結引數與回傳值,但這寫久就會習慣了。你可能會想要將此例與第十章的 「透過生命週期驗證參考」 段落做比較。 現在讓我們執行測試: $ cargo test Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished test [unoptimized + debuginfo] target(s) in 0.97s Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) running 1 test\ntest tests::one_result ... FAILED failures: ---- tests::one_result stdout ----\nthread 'main' panicked at 'assertion failed: `(left == right)` left: `[\"safe, fast, productive.\"]`, right: `[]`', src/lib.rs:44:9\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::one_result test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass `--lib` 很好!測試如我們所預期地失敗。接下來我們要讓測試通過!","breadcrumbs":"I/O 專案:建立一個命令列程式 » 透過測試驅動開發完善函式庫功能 » 編寫失敗的測試","id":"224","title":"編寫失敗的測試"},"225":{"body":"目前我們的測試會失敗,因為我們永遠只回傳一個空向量。要修正並實作 search,我們的程式需要完成以下步驟: 遍歷內容的每一行。 檢查該行是否包含我們要搜尋的字串。 如果有的話,將它加入我們要回傳的數值列表。 如果沒有的話,不做任何事。 回傳符合的結果列表。 讓我們來完成每個步驟,先從遍歷每一行開始。 透過 lines 方法來遍歷每一行 Rust 有個實用的方法能逐步處理字串的每一行,這方法就叫 lines,而使用方式就如範例 12-17 所示。注意此例還無法編譯。 檔案名稱:src/lib.rs # use std::error::Error;\n# use std::fs;\n# # pub struct Config {\n# pub query: String,\n# pub file_path: String,\n# }\n# # impl Config {\n# pub fn build(args: &[String]) -> Result {\n# if args.len() < 3 {\n# return Err(\"引數不足\");\n# }\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # Ok(Config { query, file_path })\n# }\n# }\n# # pub fn run(config: Config) -> Result<(), Box> {\n# let contents = fs::read_to_string(config.file_path)?;\n# # Ok(())\n# }\n# pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { for line in contents.lines() { // 對每行做些事情 }\n}\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# fn one_result() {\n# let query = \"duct\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\";\n# # assert_eq!(vec![\"safe, fast, productive.\"], search(query, contents));\n# }\n# } 範例 12-17:在 contents 中遍歷每一行 lines 方法會回傳疊代器(iterator)。我們會在 第十三章 詳細解釋疊代器,不過回想一下你在 範例 3-5 就看過疊代器的用法了,我們對疊代器使用 for 迴圈來對集合中的每個項目執行一些程式碼。 檢查每行是否有要搜尋的字串 接著,我們要檢查目前的行數是否有包含我們要搜尋的字串。幸運的是,字串有個好用的方法叫做 contains 能幫我處理這件事!在 search 函式中加上方法 contains 的呼叫,如範例 12-18 所示。注意這仍然無法編譯。 檔案名稱:src/lib.rs # use std::error::Error;\n# use std::fs;\n# # pub struct Config {\n# pub query: String,\n# pub file_path: String,\n# }\n# # impl Config {\n# pub fn build(args: &[String]) -> Result {\n# if args.len() < 3 {\n# return Err(\"引數不足\");\n# }\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # Ok(Config { query, file_path })\n# }\n# }\n# # pub fn run(config: Config) -> Result<(), Box> {\n# let contents = fs::read_to_string(config.file_path)?;\n# # Ok(())\n# }\n# pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { for line in contents.lines() { if line.contains(query) { // 對每行做些事情 } }\n}\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# fn one_result() {\n# let query = \"duct\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\";\n# # assert_eq!(vec![\"safe, fast, productive.\"], search(query, contents));\n# }\n# } 範例 12-18:增加檢查行數是否包含 query 字串的功能 目前我們正在將功能實作出來。但要能夠編譯的話,我們需要從本體回傳函式簽名中指定的數值。 儲存符合條件的行數 要完成此函式的話,我們需要有個方式能儲存包含搜尋字串的行數。為此我們可以在 for 迴圈前建立一個可變向量然後對向量呼叫 push 方法來儲存 line。在 for 迴圈之後,我們回傳向量,如範例 12-19 所示。 檔案名稱:src/lib.rs # use std::error::Error;\n# use std::fs;\n# # pub struct Config {\n# pub query: String,\n# pub file_path: String,\n# }\n# # impl Config {\n# pub fn build(args: &[String]) -> Result {\n# if args.len() < 3 {\n# return Err(\"引數不足\");\n# }\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # Ok(Config { query, file_path })\n# }\n# }\n# # pub fn run(config: Config) -> Result<(), Box> {\n# let contents = fs::read_to_string(config.file_path)?;\n# # Ok(())\n# }\n# pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { let mut results = Vec::new(); for line in contents.lines() { if line.contains(query) { results.push(line); } } results\n}\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# fn one_result() {\n# let query = \"duct\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\";\n# # assert_eq!(vec![\"safe, fast, productive.\"], search(query, contents));\n# }\n# } 範例 12-19:儲存符合的行數讓我們可以回傳它們 現在 search 函式應該只會回傳包含 query 的行數,而我們的測試也該通過。讓我們執行測試: $ cargo test Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished test [unoptimized + debuginfo] target(s) in 1.22s Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) running 1 test\ntest tests::one_result ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests minigrep running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 我們的測試通過了,所以我們確定它運作無誤! 在此刻之後,我們可以考慮重構搜尋函式的實作,並確保測試能通過以維持功能不變。搜尋函式的程式碼並沒有很糟,但它沒有用到疊代器中的一些實用功能優勢。我們會在 第十三章 詳細探討疊代器之後,再回過頭來看這個例子,來看看如何改善。 在 run 函式中使用 search 函式 現在 search 函式能夠執行且也有測試過了,我們需要從 run 函式呼叫 search。我們需要將 config.query 數值與 run 從檔案讀取到的 contents 傳給 search 函式。然後 run 會印出 search 回傳的每一行: 檔案名稱:src/lib.rs # use std::error::Error;\n# use std::fs;\n# # pub struct Config {\n# pub query: String,\n# pub file_path: String,\n# }\n# # impl Config {\n# pub fn build(args: &[String]) -> Result {\n# if args.len() < 3 {\n# return Err(\"引數不足\");\n# }\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # Ok(Config { query, file_path })\n# }\n# }\n# pub fn run(config: Config) -> Result<(), Box> { let contents = fs::read_to_string(config.file_path)?; for line in search(&config.query, &contents) { println!(\"{line}\"); } Ok(())\n}\n# # pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {\n# let mut results = Vec::new();\n# # for line in contents.lines() {\n# if line.contains(query) {\n# results.push(line);\n# }\n# }\n# # results\n# }\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# fn one_result() {\n# let query = \"duct\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\";\n# # assert_eq!(vec![\"safe, fast, productive.\"], search(query, contents));\n# }\n# } 我們仍會使用 for 迴圈來取得 search 回傳的每一行並顯示出來。 現在整支程式應該都能執行了!讓我們來試試看。首先用一個只會在 Emily Dickinson 的詩中回傳剛好一行的單字「frog」: $ cargo run -- frog poem.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.38s Running `target/debug/minigrep frog poem.txt`\nHow public, like a frog 酷喔!現在讓我們試試看能符合多行的單字,像是「body」: $ cargo run -- body poem.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep body poem.txt`\nI'm nobody! Who are you?\nAre you nobody, too?\nHow dreary to be somebody! 最後,讓我們確保使用詩中沒出現的單字來搜尋時,我們不會得到任何一行,像是「monomorphization」: $ cargo run -- monomorphization poem.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep monomorphization poem.txt` 漂亮!我們建立了一個屬於自己的迷你經典工具,並學到了很多如何架構應用程式的知識。我們也學了一些檔案輸入與輸出、生命週期、測試與命令列解析。 為了讓此專案更完整,我們會簡單介紹如何使用環境變數,以及如何印出到標準錯誤(standard error),這兩項在寫命令列程式時都很實用。","breadcrumbs":"I/O 專案:建立一個命令列程式 » 透過測試驅動開發完善函式庫功能 » 寫出讓測試成功的程式碼","id":"225","title":"寫出讓測試成功的程式碼"},"226":{"body":"我們會新增一個額外的功能來改善 minigrep:使用者可以透過環境變數來啟用不區分大小寫的搜尋功能。我們可以將此功能設為命令列選項並要求使用者每次需要時就要加上它,但是這次我們選擇使用環境變數。這樣一來能讓使用者設置環境變數一次就好,然後在該終端機 session 中所有的搜尋都會是不區分大小寫的。","breadcrumbs":"I/O 專案:建立一個命令列程式 » 處理環境變數 » 處理環境變數","id":"226","title":"處理環境變數"},"227":{"body":"我們首先新增個 search_case_insensitive 函式在環境變數有數值時呼叫它。我們將繼續遵守 TDD 流程,所以第一步一樣是先寫個會失敗的測試。我們會為新函式 search_case_insensitive 新增一個測試,並將舊測試從 one_result 改名為 case_sensitive 以便清楚兩個測試的差別,如範例 12-20 所示。 檔案名稱:src/lib.rs # use std::error::Error;\n# use std::fs;\n# # pub struct Config {\n# pub query: String,\n# pub file_path: String,\n# }\n# # impl Config {\n# pub fn build(args: &[String]) -> Result {\n# if args.len() < 3 {\n# return Err(\"引數不足\");\n# }\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # Ok(Config { query, file_path })\n# }\n# }\n# # pub fn run(config: Config) -> Result<(), Box> {\n# let contents = fs::read_to_string(config.file_path)?;\n# # for line in search(&config.query, &contents) {\n# println!(\"{line}\");\n# }\n# # Ok(())\n# }\n# # pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {\n# let mut results = Vec::new();\n# # for line in contents.lines() {\n# if line.contains(query) {\n# results.push(line);\n# }\n# }\n# # results\n# }\n# #[cfg(test)]\nmod tests { use super::*; #[test] fn case_sensitive() { let query = \"duct\"; let contents = \"\\\nRust:\nsafe, fast, productive.\nPick three.\nDuct tape.\"; assert_eq!(vec![\"safe, fast, productive.\"], search(query, contents)); } #[test] fn case_insensitive() { let query = \"rUsT\"; let contents = \"\\\nRust:\nsafe, fast, productive.\nPick three.\nTrust me.\"; assert_eq!( vec![\"Rust:\", \"Trust me.\"], search_case_insensitive(query, contents) ); }\n} 範例 12-20:為準備加入的不區分大小寫函式新增個失敗測試 注意到我們也修改了舊測試 contents。我們新增了一行使用大寫 D 的 \"Duct tape.\",當我們以區分大小寫來搜尋時,就不會符合要搜尋的 \"duct\"。這樣變更舊測試能確保我們沒有意外破壞我們已經實作好的區分大小寫的功能。此測試應該要能通過,並在我們實作不區分大小寫的搜尋時仍能繼續通過。 新的不區分大小寫的搜尋測試使用 \"rUsT\" 來搜尋。在我們準備要加入的 search_case_insensitive 函式中,要搜尋的 \"rUsT\" 應該要能符合有大寫 R 的 \"Rust:\" 以及 \"Trust me.\" 這幾行,就算兩者都與搜尋字串有不同的大小寫。這是我們的失敗測試而且它還無法編譯,因為我們還沒有定義 search_case_insensitive 函式。歡迎自行加上一個永遠回傳空向量的骨架實作,就像我們在範例 12-16 所做的一樣,然後看看測試能不能編譯過並失敗。","breadcrumbs":"I/O 專案:建立一個命令列程式 » 處理環境變數 » 寫個不區分大小寫的 search 函式的失敗測試","id":"227","title":"寫個不區分大小寫的 search 函式的失敗測試"},"228":{"body":"範例 12-21 顯示的 search_case_insensitive 函式會與 search 函式幾乎一樣。唯一的不同在於我們將 query 與每個 line 都變成小寫,所以無論輸入引數是大寫還是小寫,當我們在檢查行數是否包含搜尋的字串時,它們都會是小寫。 檔案名稱:src/lib.rs # use std::error::Error;\n# use std::fs;\n# # pub struct Config {\n# pub query: String,\n# pub file_path: String,\n# }\n# # impl Config {\n# pub fn build(args: &[String]) -> Result {\n# if args.len() < 3 {\n# return Err(\"引數不足\");\n# }\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # Ok(Config { query, file_path })\n# }\n# }\n# # pub fn run(config: Config) -> Result<(), Box> {\n# let contents = fs::read_to_string(config.file_path)?;\n# # for line in search(&config.query, &contents) {\n# println!(\"{line}\");\n# }\n# # Ok(())\n# }\n# # pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {\n# let mut results = Vec::new();\n# # for line in contents.lines() {\n# if line.contains(query) {\n# results.push(line);\n# }\n# }\n# # results\n# }\n# pub fn search_case_insensitive<'a>( query: &str, contents: &'a str,\n) -> Vec<&'a str> { let query = query.to_lowercase(); let mut results = Vec::new(); for line in contents.lines() { if line.to_lowercase().contains(&query) { results.push(line); } } results\n}\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# fn case_sensitive() {\n# let query = \"duct\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\n# Duct tape.\";\n# # assert_eq!(vec![\"safe, fast, productive.\"], search(query, contents));\n# }\n# # #[test]\n# fn case_insensitive() {\n# let query = \"rUsT\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\n# Trust me.\";\n# # assert_eq!(\n# vec![\"Rust:\", \"Trust me.\"],\n# search_case_insensitive(query, contents)\n# );\n# }\n# } 範例 12-21:定義 search_case_insensitive 並在比較前將搜尋字串與行數均改為小寫 首先我們將 query 字串變成小寫並儲存到同名的遮蔽變數中。我們必須呼叫對要搜尋的字串 to_lowercase,這樣無論使用者輸入的是 \"rust\"、\"RUST\"、\"Rust\" 或 \"rUsT\",我們都會將字串視為 \"rust\" 並以此來不區分大小寫。雖然 to_lowercase 能處理基本的 Unicode,但它不會是 100% 準確的。如果我們是在寫真正的應用程式的話,我們需要處理更多條件,但在此段落是為了理解環境變數而非 Unicode,所以我們維持這樣寫就好。 注意到 query 現在是個 String 而非字串切片,因為呼叫 to_lowercase 會建立新的資料而非參考現存的資料。假設要搜尋的字串是 \"rUsT\" 的話,該字串切片並沒有包含小寫的 u 或 t 能讓我們來使用,所以我們必須配置一個包含 \"rust\" 的新 String。現在當我們將 query 作為引數傳給 contains 方法時,我們需要加上「&」,因為 contains 所定義的簽名接收的是一個字串切片。 接著,我們對每個 line 加上 to_lowercase 的呼叫。現在我們將 line 和 query 都轉換成小寫了。我們可以不區分大小寫來找到符合的行數。 讓我們來看看實作是否能通過測試: $ cargo test Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished test [unoptimized + debuginfo] target(s) in 1.33s Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) running 2 tests\ntest tests::case_insensitive ... ok\ntest tests::case_sensitive ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests minigrep running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 很好!測試通過。現在讓我們從 run 函式呼叫新的 search_case_insensitive 函式。首先,我們要在 Config 中新增一個配置選項來切換區分大小寫與不區分大小寫之間的搜尋。新增此欄位會造成編譯錯誤,因為我們還沒有初始化該欄位: 檔案名稱:src/lib.rs # use std::error::Error;\n# use std::fs;\n# pub struct Config { pub query: String, pub file_path: String, pub ignore_case: bool,\n}\n# # impl Config {\n# pub fn build(args: &[String]) -> Result {\n# if args.len() < 3 {\n# return Err(\"引數不足\");\n# }\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # Ok(Config { query, file_path })\n# }\n# }\n# # pub fn run(config: Config) -> Result<(), Box> {\n# let contents = fs::read_to_string(config.file_path)?;\n# # let results = if config.ignore_case {\n# search_case_insensitive(&config.query, &contents)\n# } else {\n# search(&config.query, &contents)\n# };\n# # for line in results {\n# println!(\"{line}\");\n# }\n# # Ok(())\n# }\n# # pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {\n# let mut results = Vec::new();\n# # for line in contents.lines() {\n# if line.contains(query) {\n# results.push(line);\n# }\n# }\n# # results\n# }\n# # pub fn search_case_insensitive<'a>(\n# query: &str,\n# contents: &'a str,\n# ) -> Vec<&'a str> {\n# let query = query.to_lowercase();\n# let mut results = Vec::new();\n# # for line in contents.lines() {\n# if line.to_lowercase().contains(&query) {\n# results.push(line);\n# }\n# }\n# # results\n# }\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# fn case_sensitive() {\n# let query = \"duct\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\n# Duct tape.\";\n# # assert_eq!(vec![\"safe, fast, productive.\"], search(query, contents));\n# }\n# # #[test]\n# fn case_insensitive() {\n# let query = \"rUsT\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\n# Trust me.\";\n# # assert_eq!(\n# vec![\"Rust:\", \"Trust me.\"],\n# search_case_insensitive(query, contents)\n# );\n# }\n# } 注意到我們新增了 ignore_case 欄位並存有布林值。接著,我們需要 run 函式檢查 ignore_case 欄位的數值並以此決定要呼叫 search 函式或是 search_case_insensitive 函式,如範例 12-22 所示。不過目前還無法編譯。 檔案名稱:src/lib.rs # use std::error::Error;\n# use std::fs;\n# # pub struct Config {\n# pub query: String,\n# pub file_path: String,\n# pub ignore_case: bool,\n# }\n# # impl Config {\n# pub fn build(args: &[String]) -> Result {\n# if args.len() < 3 {\n# return Err(\"引數不足\");\n# }\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # Ok(Config { query, file_path })\n# }\n# }\n# pub fn run(config: Config) -> Result<(), Box> { let contents = fs::read_to_string(config.file_path)?; let results = if config.ignore_case { search_case_insensitive(&config.query, &contents) } else { search(&config.query, &contents) }; for line in results { println!(\"{line}\"); } Ok(())\n}\n# # pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {\n# let mut results = Vec::new();\n# # for line in contents.lines() {\n# if line.contains(query) {\n# results.push(line);\n# }\n# }\n# # results\n# }\n# # pub fn search_case_insensitive<'a>(\n# query: &str,\n# contents: &'a str,\n# ) -> Vec<&'a str> {\n# let query = query.to_lowercase();\n# let mut results = Vec::new();\n# # for line in contents.lines() {\n# if line.to_lowercase().contains(&query) {\n# results.push(line);\n# }\n# }\n# # results\n# }\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# fn case_sensitive() {\n# let query = \"duct\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\n# Duct tape.\";\n# # assert_eq!(vec![\"safe, fast, productive.\"], search(query, contents));\n# }\n# # #[test]\n# fn case_insensitive() {\n# let query = \"rUsT\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\n# Trust me.\";\n# # assert_eq!(\n# vec![\"Rust:\", \"Trust me.\"],\n# search_case_insensitive(query, contents)\n# );\n# }\n# } 範例 12-22:依據 config.ignore_case 的數值來呼叫 search 或 search_case_insensitive 最後,我們需要檢查環境變數。處理環境變數的函式位於標準函式庫中的 env 模組中,所以我們在 src/lib.rs 最上方加上 use std::env; 來將該模組引入作用域。然後我們使用 env 模組中的 var 函式來檢查一個叫做 IGNORE_CASE 的環境變數有沒有設任何數值,如範例 12-23 所示。 檔案名稱:src/lib.rs use std::env;\n// --省略-- # use std::error::Error;\n# use std::fs;\n# # pub struct Config {\n# pub query: String,\n# pub file_path: String,\n# pub case_sensitive: bool,\n# }\n# impl Config { pub fn build(args: &[String]) -> Result { if args.len() < 3 { return Err(\"引數不足\"); } let query = args[1].clone(); let file_path = args[2].clone(); let ignore_case = env::var(\"IGNORE_CASE\").is_ok(); Ok(Config { query, file_path, ignore_case, }) }\n}\n# # pub fn run(config: Config) -> Result<(), Box> {\n# let contents = fs::read_to_string(config.file_path)?;\n# # let results = if config.ignore_case {\n# search_case_insensitive(&config.query, &contents)\n# } else {\n# search(&config.query, &contents)\n# };\n# # for line in results {\n# println!(\"{line}\");\n# }\n# # Ok(())\n# }\n# # pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {\n# let mut results = Vec::new();\n# # for line in contents.lines() {\n# if line.contains(query) {\n# results.push(line);\n# }\n# }\n# # results\n# }\n# # pub fn search_case_insensitive<'a>(\n# query: &str,\n# contents: &'a str,\n# ) -> Vec<&'a str> {\n# let query = query.to_lowercase();\n# let mut results = Vec::new();\n# # for line in contents.lines() {\n# if line.to_lowercase().contains(&query) {\n# results.push(line);\n# }\n# }\n# # results\n# }\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# fn case_sensitive() {\n# let query = \"duct\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\n# Duct tape.\";\n# # assert_eq!(vec![\"safe, fast, productive.\"], search(query, contents));\n# }\n# # #[test]\n# fn case_insensitive() {\n# let query = \"rUsT\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\n# Trust me.\";\n# # assert_eq!(\n# vec![\"Rust:\", \"Trust me.\"],\n# search_case_insensitive(query, contents)\n# );\n# }\n# } 範例 12-23:檢查環境變數 IGNORE_CASE 我們在此建立了一個新的變數 ignore_case。要設置其數值,我們可以呼叫 env::var 函式並傳入環境變數 IGNORE_CASE 的名稱。env::var 函式會回傳 Result,如果有設置環境變數的話,這就會是包含環境變數數值的成功 Ok變體;如果環境變數沒有設置的話,這就會回傳 Err 變體。 我們在 Result 使用 is_ok 方法來檢查環境變數是否有設置,也就是程式是否該使用區分大小寫的搜尋。如果 IGNORE_CASE 環境變數沒有設置任何數值的話,is_ok 會回傳否,所以程式就會進行區分大小寫的搜尋。我們不在乎環境變數的 數值 ,只在意它有沒有被設置而已,所以我們使用 is_ok 來檢查而非使用 unwrap、expect 或其他任何我們看過的 Result 方法。 我們將變數 ignore_case 的數值傳給 Config 實例,讓 run 函式可以讀取該數值並決定該呼叫 search_case_insensitive 還是 search,如範例 12-22 所實作的一樣。 讓我們試看看吧!首先,我們先不設置環境變數並執行程式來搜尋 to,任何包含小寫單字「to」的行數都應要符合: $ cargo run -- to poem.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep to poem.txt`\nAre you nobody, too?\nHow dreary to be somebody! 看起來運作仍十分正常!現在,讓我們設置 IGNORE_CASE 為 1 並執行程式來搜尋相同的字串 to。 $ IGNORE_CASE=1 cargo run -- to poem.txt 如果你使用的是 PowerShell,你需要將設置變數與執行程式分為不同的命令: PS> $Env:IGNORE_CASE=1; cargo run -- to poem.txt 這會在你的 shell session 中設置 IGNORE_CASE。它可以透過 Remove-Item cmdlet 來取消設置: PS> Remove-Item Env:IGNORE_CASE 我們應該會得到包含可能有大寫的「to」的行數: Are you nobody, too?\nHow dreary to be somebody!\nTo tell your name the livelong day\nTo an admiring bog! 太好了,我們也取得了包含「To」的行數!我們的 minigrep 程式現在可以進行不區分大小寫的搜尋並以環境變數配置。現在你知道如何使用命令列引數或環境變數來管理設置選項了。 有些程式允許同時使用引數 與 環境變數來配置。在這種情況下,程式會決定各種選項的優先層級。你想要練習的話,嘗試使用命令列引數與環境變數來控制區分大小寫的選項。並在程式執行時,其中一個設置為區分大小寫,而另一個為不區分大小寫時,自行決定該優先使用命令列引數還是環境變數。 std::env 模組還包含很多處理環境變數的實用功能,歡迎查閱其官方文件來瞭解有哪些可用。","breadcrumbs":"I/O 專案:建立一個命令列程式 » 處理環境變數 » 實作 search_case_insensitive 函式","id":"228","title":"實作 search_case_insensitive 函式"},"229":{"body":"目前我們使用 println! 巨集來將所有的輸出顯示到終端機。大多數的終端機都提供兩種輸出方式:用於通用資訊的 標準輸出(standard output, stdout) 以及用於錯誤訊息的 標準錯誤(standard error, stderr) 。這樣的區別讓使用者可以選擇將程式的成功輸出導向到一個檔案中,並仍能在螢幕上顯示錯誤訊息。 println! 巨集只能夠印出標準輸出,所以我們得用其他方式來印出標準錯誤。","breadcrumbs":"I/O 專案:建立一個命令列程式 » 將錯誤訊息寫入標準錯誤而非標準輸出 » 將錯誤訊息寫入標準錯誤而非標準輸出","id":"229","title":"將錯誤訊息寫入標準錯誤而非標準輸出"},"23":{"body":"讓我們來仔細瞧瞧你的「Hello, world!」程式。這是第一塊拼圖: fn main() { } 這幾行定義了一個 main 函式。main 是一個特別的函式:它是每個可執行的 Rust 程式永遠第一個執行的程式碼。第一行宣告了一個函式 main,它沒有參數也不回傳任何東西。如果有參數的話,它們會被加進括號 () 內。 函式本體被囊括在大括號 {} 內,Rust 要求所有函式都用大括號包起來。一般來說,良好的程式碼風格會要求將前大括號置於宣告函式的同一行,並用一個空格區隔開來。 如果你想要在不同 Rust 專案之間統一標準風格的話,rustfmt 可以格式化你的程式成特定的風格(更多 rustfmt 資訊請詳見 附錄 D )。Rust 團隊已經將此工具納入標準 Rust 發行版中,就像 rustc 一樣,它應該已經安裝到你的電腦上了! 在 main 函式本體中有以下程式碼: println!(\"Hello, world!\"); 此行負責了整支程式要做的事:它將文字顯示在螢幕上。這邊有四個細節要注意。 首先,Rust 的排版風格是 4 個空格而非一個 tab。 第二,println! 會呼叫一支 Rust 巨集(macro)。如果是呼叫函式的話,那則會是 println(去掉 !)。我們會在第十九章討論更多巨集的細節。現在你只需要知道使用 ! 代表呼叫一支巨集而非一個正常的函式,且該巨集遵守的規則不全都和函式一樣。 第三,\"Hello, world!\" 是一個字串,我們將此字串作為引數傳遞給 println!,然後該字串就會被顯示到螢幕上。 第四,我們用分號(;)作為該行結尾,代表此表達式的結束和下一個表達式的開始。多數的 Rust 程式碼都以分號做結尾。","breadcrumbs":"開始入門 » Hello, World! » 分析這支 Rust 程式","id":"23","title":"分析這支 Rust 程式"},"230":{"body":"首先,我們先來觀察 minigrep 目前寫到標準輸出的顯示內容,其中包含任何我們想改寫成標準錯誤的錯誤訊息。我們會將標準輸出重新導向至一個檔案並故意產生一個錯誤。因為我們不會重新導向標準錯誤,所以任何傳送至標準錯誤的內容會繼續顯示在螢幕上。 命令列程式應該要傳送錯誤訊息至標準錯誤,讓我們可以在重新導向標準輸出至檔案時,仍能在螢幕上看到錯誤訊息。所以我們的程式目前並不符合預期:我們會看到它儲存錯誤訊息輸出到檔案中! 要觀察此行為的方式的話,我們要透過 > 來執行程式並加上檔案名稱 output.txt ,這是我們要重新導向標準輸出到的地方。我們不會傳遞任何引數,這樣就應該會造成錯誤: $ cargo run > output.txt > 語法告訴 shell 要將標準輸出的內容寫入 output.txt 而不是顯示在螢幕上。我們沒有看到應顯示在螢幕上的錯誤訊息,這代表它一定跑到檔案中了。以下是 output.txt 包含的內容: 解析引數時出現問題:引數不足 是的,我們的錯誤訊息印到了標準輸出。像這樣的錯誤訊息印到標準錯誤會比較好,這樣才能只讓成功執行的資料存至檔案中。讓我們來修正吧。","breadcrumbs":"I/O 專案:建立一個命令列程式 » 將錯誤訊息寫入標準錯誤而非標準輸出 » 檢查該在哪裡寫錯誤","id":"230","title":"檢查該在哪裡寫錯誤"},"231":{"body":"我們會使用範例 12-24 的程式碼來改變錯誤訊息印出的方式。由於我們在本章前幾篇的重構,所有印出錯誤訊息的程式碼都位於 main 函式中。標準函式庫有提供 eprintln! 巨集來印到標準錯誤,所以讓我們變更兩個原本呼叫 println! 來印出錯誤的段落來改使用 eprintln!。 檔案名稱:src/main.rs # use std::env;\n# use std::process;\n# # use minigrep::Config;\n# fn main() { let args: Vec = env::args().collect(); let config = Config::build(&args).unwrap_or_else(|err| { eprintln!(\"解析引數時出現問題:{err}\"); process::exit(1); }); if let Err(e) = minigrep::run(config) { eprintln!(\"應用程式錯誤:{e}\"); process::exit(1); }\n} 範例 12-24:使用 eprintln! 來將錯誤訊息印至標準錯誤而非標準輸出 讓我們以相同方式再執行程式一次,沒有任何引數並用 > 重新導向標準輸出: $ cargo run > output.txt\n解析引數時出現問題:引數不足 現在我們看到錯誤顯示在螢幕上而且 output.txt 裡什麼也沒只有,這正是命令列程式所預期的行為。 讓我們加上不會產生錯誤的引數來執行程式,並仍重新導向標準輸出至檔案中,如以下所示: $ cargo run -- to poem.txt > output.txt 我們在終端機不會看到任何輸出,而 output.txt 會包含我們的結果: 檔案名稱:output.txt Are you nobody, too?\nHow dreary to be somebody! 這說明我們現在有對成功的輸出使用標準輸出,而且有妥善地將錯誤輸出傳至標準錯誤。","breadcrumbs":"I/O 專案:建立一個命令列程式 » 將錯誤訊息寫入標準錯誤而非標準輸出 » 將錯誤印出至標準錯誤","id":"231","title":"將錯誤印出至標準錯誤"},"232":{"body":"本章節回顧了你目前所學的一些重要概念,並介紹了如何在 Rust 中進行常見的 I/O 操作。透過使用命令列引數、檔案、環境變數與用來印出錯誤的 eprintln! 巨集,你現在已經準備好能寫出命令列應用程式了。結合前幾章的概念,你的程式的組織架構會非常穩固、資料都能有效率地儲存至適當的資料結構、完善地處理錯誤,並通過測試檢驗。 接下來,我們要探討些 Rust 受到函式語言啟發的功能:閉包與疊代器。","breadcrumbs":"I/O 專案:建立一個命令列程式 » 將錯誤訊息寫入標準錯誤而非標準輸出 » 總結","id":"232","title":"總結"},"233":{"body":"Rust 的設計靈感啟發自許多現有的語言與技術,其中一個影響十分顯著的就是 函式程式設計(functional programming) 。以函式風格的程式設計通常包含將函式視為數值並作為引數傳遞、將它們從其他函式回傳、將它們賦值給變數以便之後使用,以及更多。 在本章節中,我們不會討論哪些才是屬於函式程式設計或哪些不是,而是介紹一些 Rust 中類似於許多語言常視為函式語言特色的功能。 更明確來說,我們會涵蓋: 閉包(Closures) :類似函式的結構並可以賦值給變數 疊代器(Iterators) :遍歷一系列元素的方法 如何用閉包與疊代器來改善第十二章的 I/O 專案 閉包與疊代器的效能(先偷偷跟你說:它們比你想像的還要快!) 我們已經在其他章節提到的功能像是模式配對與列舉也都有被函式風格所影響。因為掌握閉包與疊代器是寫出符合語言風格與高效 Rust 程式碼中重要的一環,所以我們將用一章來介紹它們。","breadcrumbs":"函式語言功能:疊代器與閉包 » 函式語言功能:疊代器與閉包","id":"233","title":"函式語言功能:疊代器與閉包"},"234":{"body":"Rust 的閉包(closures)是個你能賦值給變數或作為其他函式引數的匿名函式。你可以在某處建立閉包,然後在不同的地方呼叫閉包並執行它。而且不像函式,閉包可以從它們所定義的作用域中獲取數值。我們將會解釋這些閉包功能如何允許程式碼重用以及自訂行為。","breadcrumbs":"函式語言功能:疊代器與閉包 » 閉包:獲取其環境的匿名函式 » 閉包:獲取其環境的匿名函式","id":"234","title":"閉包:獲取其環境的匿名函式"},"235":{"body":"我們首先會來研究我們如何用閉包來獲取定義在環境的數值並在之後使用。讓我們考慮以下假設情境:每隔一段時間,我們的襯衫公司會送出獨家限量版襯衫給郵寄清單的某位顧客來作為宣傳手段。郵寄清單的顧客可以在他們的設定中加入他們最愛的顏色。如果被選中的人有設定最愛顏色的話,他們就會獲得該顏色的襯衫。如果他們沒有指定任何最愛顏色的話,公司就會選擇目前顏色最多的選項。 要實作的方式有很多種。舉例來說,我們可以使用一個列舉叫做 ShirtColor 然後其變體有 Red 和 Blue(為了簡潔我們限制顏色的種類)。我們用 Inventory 來代表公司的庫存,然後用 shirts 欄位來包含 Vec 來代表目前庫存有的襯衫顏色。定義在 Inventory 的 giveaway 方法會取得免費襯衫得主的選擇性襯衫顏色偏好,然後回傳他們會拿到的襯衫顏色。如範例 13-1 所示: 檔案名稱:src/main.rs #[derive(Debug, PartialEq, Copy, Clone)]\nenum ShirtColor { Red, Blue,\n} struct Inventory { shirts: Vec,\n} impl Inventory { fn giveaway(&self, user_preference: Option) -> ShirtColor { user_preference.unwrap_or_else(|| self.most_stocked()) } fn most_stocked(&self) -> ShirtColor { let mut num_red = 0; let mut num_blue = 0; for color in &self.shirts { match color { ShirtColor::Red => num_red += 1, ShirtColor::Blue => num_blue += 1, } } if num_red > num_blue { ShirtColor::Red } else { ShirtColor::Blue } }\n} fn main() { let store = Inventory { shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue], }; let user_pref1 = Some(ShirtColor::Red); let giveaway1 = store.giveaway(user_pref1); println!( \"偏好 {:?} 的使用者獲得 {:?}\", user_pref1, giveaway1 ); let user_pref2 = None; let giveaway2 = store.giveaway(user_pref2); println!( \"偏好 {:?} 的使用者獲得 {:?}\", user_pref2, giveaway2 );\n} 範例 13-1:襯衫公司送禮的情境 定義在 main 中的 store 在這次的限量版宣傳中的庫存有兩件藍色襯衫與一件紅色襯衫。我們呼叫了 giveaway 方法兩次,一次是給偏好紅色襯衫的使用者,另一次則是給無任何偏好的使用者。 再次強調這可以用各種方式實作,只是在此我們想專注在閉包,所以除了用到我們已經學過的概念以外,giveaway 方法中還使用了閉包。在 giveaway 方法中,我們從參數型別 Option 取得使用者偏好,然後對 user_preference 呼叫 unwrap_or_else 方法。 Option 的 unwrap_or_else 方法 定義在標準函式庫中。它接收一個引數:一個沒有任何引數的閉包然後會回傳數值 T(該型別為 Option 的 Some 儲存的型別,在此例中就是 ShirtColor)。如果 Option 是 Some 變體,unwrap_or_else 就會回傳 Some 裡的數值。如果 Option 是 None 變體,unwrap_or_else 會呼叫閉包並回傳閉包回傳的數值。 我們寫上閉包表達式 || self.most_stocked() 作為 unwrap_or_else 的引數。這是個沒有任何參數的閉包(如果閉包有參數的話,它們會出現在兩條直線中間)。閉包本體會呼叫 self.most_stocked()。我們直接在此定義閉包,然後 unwrap_or_else 的實作就會在需要結果時執行閉包。 執行此程式的話就會印出: $ cargo r Compiling shirt-company v0.1.0 (file:///projects/shirt-company) Finished dev [unoptimized + debuginfo] target(s) in 0.00s Running `target/debug/shirt-company`\n偏好 Some(Red) 的使用者獲得 Red\n偏好 None 的使用者獲得 Blue 這裡值得注意的是我們對當前 Inventory 實例傳入的是一個呼叫 self.most_stocked() 的閉包。標準函式庫不需要知道我們定義的任何型別像 Inventory 與 ShirtColor,或是在此情境中我們需要使用的任何邏輯,閉包就會獲取 Inventory 實例的不可變參考 self,然後傳給我們在 unwrap_or_else 方法中指定的程式碼。反之,函式就無法像這樣獲取它們周圍的環境。","breadcrumbs":"函式語言功能:疊代器與閉包 » 閉包:獲取其環境的匿名函式 » 透過閉包獲取環境","id":"235","title":"透過閉包獲取環境"},"236":{"body":"函式與閉包還有更多不同的地方。閉包通常不必像 fn 函式那樣要求你要詮釋參數或回傳值的型別。函式需要型別詮釋是因為它們是顯式公開給使用者的介面。嚴格定義此介面是很重要的,這能確保每個人同意函式使用或回傳的數值型別為何。但是閉包並不是為了對外公開使用,它們儲存在變數且沒有名稱能公開給我們函式庫的使用者。 閉包通常很短,而且只與小範圍內的程式碼有關,而非適用於任何場合。有了這樣限制的環境,編譯器能可靠地推導出參數與回傳值的型別,如同其如何推導出大部分的變數型別一樣。(但在有些例外情形下編譯器還是需要閉包的型別詮釋) 至於變數的話,雖然不是必要的,但如果我們希望能夠增加閱讀性與清楚程度,我們還是可以加上型別詮釋。要在閉包詮釋型別的話,就會如範例 13-2 的定義所示。在此範例中,我們定義一個閉包並儲存至一個變數中,而非像範例 13-1 我們將閉包作為引數傳入。 檔案名稱:src/main.rs # use std::thread;\n# use std::time::Duration;\n# # fn generate_workout(intensity: u32, random_number: u32) { let expensive_closure = |num: u32| -> u32 { println!(\"緩慢計算中...\"); thread::sleep(Duration::from_secs(2)); num };\n# # if intensity < 25 {\n# println!(\"今天請做 {} 下伏地挺身!\", expensive_closure(intensity));\n# println!(\"然後請做 {} 下仰臥起坐!\", expensive_closure(intensity));\n# } else {\n# if random_number == 3 {\n# println!(\"今天休息!別忘了多喝水!\");\n# } else {\n# println!(\n# \"今天請慢跑 {} 分鐘!\",\n# expensive_closure(intensity)\n# );\n# }\n# }\n# }\n# # fn main() {\n# let simulated_user_specified_value = 10;\n# let simulated_random_number = 7;\n# # generate_workout(simulated_user_specified_value, simulated_random_number);\n# } 範例 13-2:對閉包加上選擇性的參數與回傳值型別詮釋 加上型別詮釋後,閉包的語法看起來就更像函式的語法了。我們在此定義了一個對參數加 1 的函式,以及一個有相同行為的閉包做為比較。我們加了一些空格來對齊相對應的部分。這顯示了閉包語法和函式語法有多類似,只是改用直線以及有些語法是選擇性的。 fn add_one_v1 (x: u32) -> u32 { x + 1 }\nlet add_one_v2 = |x: u32| -> u32 { x + 1 };\nlet add_one_v3 = |x| { x + 1 };\nlet add_one_v4 = |x| x + 1 ; 第一行顯示的是函式定義,而第二行則顯示有完成型別詮釋的閉包定義。在第三行我們移除了閉包定義的型別詮釋,然後在第四行我們移除了大括號,因為閉包本體只有一個表達式,所以這是選擇性的。這些都是有效的定義,並會在被呼叫時產生相同行為。而 add_one_v3 和 add_one_v4 一定要被呼叫,這樣編譯器才能從它們的使用方式中推導出型別。這就像 let v = Vec::new(); 需要型別詮釋,或是有某種型別的數值插入 Vec 中,Rust 才能推導出型別。 對於閉包定義,編譯器會對每個參數與它們的回傳值推導出一個實際型別。舉例來說,範例 13-3 展示一支只會將收到的參數作為回傳值的閉包定義。此閉包並沒有什麼意義,純粹作為範例解釋。注意到我們沒有在定義中加上任何型別詮釋。由於沒有型別詮釋,我們可以用任何型別來呼叫閉包,像我們第一次呼叫就用 String。如果我們接著嘗試用整數呼叫 example_closure,我們就會得到錯誤。 檔案名稱:src/main.rs # fn main() { let example_closure = |x| x; let s = example_closure(String::from(\"哈囉\")); let n = example_closure(5);\n# } 範例 13-3:嘗試呼叫被推導出兩個不同型別的閉包 編譯器會給我們以下錯誤: $ cargo run Compiling closure-example v0.1.0 (file:///projects/closure-example)\nerror[E0308]: mismatched types --> src/main.rs:5:29 |\n5 | let n = example_closure(5); | --------------- ^- help: try using a conversion method: `.to_string()` | | | | | expected struct `String`, found integer | arguments to this function are incorrect |\nnote: closure parameter defined here --> src/main.rs:3:28 |\n2 | let example_closure = |x| x; | ^ For more information about this error, try `rustc --explain E0308`.\nerror: could not compile `closure-example` due to previous error 當我們第一次使用 String 數值呼叫 example_closure 時,編譯器會推導 x 與閉包回傳值的型別為 String。這樣 example_closure 閉包內的型別就會鎖定,然後我們如果對同樣的閉包嘗試使用不同的型別的話,我們就會得到型別錯誤。","breadcrumbs":"函式語言功能:疊代器與閉包 » 閉包:獲取其環境的匿名函式 » 閉包型別推導與詮釋","id":"236","title":"閉包型別推導與詮釋"},"237":{"body":"閉包要從它們周圍環境取得數值有三種方式,這能直接對應於函式取得參數的三種方式:不可變借用、可變借用,與取得所有權。閉包會依照函式本體如何使用獲取的數值,來決定要用哪種方式。 在範例 13-4 中,我們定義一個閉包來獲取 list 向量的不可變參考,因為它只需要不可變參考就能印出數值: 檔案名稱:src/main.rs fn main() { let list = vec![1, 2, 3]; println!(\"定義閉包前:{:?}\", list); let only_borrows = || println!(\"來自閉包:{:?}\", list); println!(\"呼叫閉包前:{:?}\", list); only_borrows(); println!(\"呼叫閉包後:{:?}\", list);\n} 範例 13-4:定義並呼叫會獲取不可變參考的閉包 此範例還示範了變數能綁定閉包的定義,然後我們之後就可以用變數名稱加上括號來呼叫閉包,這樣變數名稱就像函式名稱一樣。 由於我們可以同時擁有 list 的多重不可變參考,list 在閉包定義前、在閉包定義後閉包呼叫前以及閉包呼叫時的程式碼中都是能使用的。此程式碼就會編譯、執行並印出: $ cargo run Compiling closure-example v0.1.0 (file:///projects/closure-example) Finished dev [unoptimized + debuginfo] target(s) in 0.43s Running `target/debug/closure-example\n定義閉包前:[1, 2, 3]\n呼叫閉包前:[1, 2, 3]\n來自閉包:[1, 2, 3]\n呼叫閉包後:[1, 2, 3] 接著在範例 13-5 中我們改變閉包本體,對 list 向量加上一個元素。這樣閉包現在就會獲取可變參考: 檔案名稱:src/main.rs fn main() { let mut list = vec![1, 2, 3]; println!(\"呼叫閉包前{:?}\", list); let mut borrows_mutably = || list.push(7); borrows_mutably(); println!(\"呼叫閉包後:{:?}\", list);\n} 範例 13-11:定義並呼叫會獲取可變參考的閉包 此程式碼會編譯、執行並印出: $ cargo run Compiling closure-example v0.1.0 (file:///projects/closure-example) Finished dev [unoptimized + debuginfo] target(s) in 0.43s Running `target/debug/closure-example`\n呼叫閉包前[1, 2, 3]\n呼叫閉包後:[1, 2, 3, 7] 注意到在 borrows_mutably 閉包的定義與呼叫之間的 println! 不見了:當 borrows_mutably 定義時,它會獲取 list 的可變參考。我們在閉包呼叫之後沒有再使用閉包,所以可變參考就結束。在閉包定義與呼叫之間,利用不可變參考印出輸出是不允許的,因為在可變參考期間不能再有其他參考。你可以試試看在那加上 println! 然後看看會收到什麼錯誤訊息! 如果你想要強迫閉包取得周圍環境數值的所有權的話,你可以在參數列表前使用 move 關鍵字。 此技巧適用於將閉包傳給新執行緒來移動資料,讓新的執行緒能擁有該資料。我們會在第十六章討論並行時,介紹為何你會想使用它們。但現在讓我們簡單探索怎麼在閉包使用 move 關鍵字開個新的執行緒就好。範例 13-6 更改了範例 13-4 讓向量在新的執行緒印出而非原本的主執行緒: 檔案名稱:src/main.rs use std::thread; fn main() { let list = vec![1, 2, 3]; println!(\"呼叫閉包前:{:?}\", list); thread::spawn(move || println!(\"來自執行緒:{:?}\", list)) .join() .unwrap();\n} 範例 13-6:使用 move 來迫使執行緒的閉包取得 list 的所有權 我們開了一個新的執行緒,將閉包作為引數傳入,閉包本體會印出 list。在範例 13-4 中,閉包只用不可變參考獲取 list,因為要印出 list 的需求只要這樣就好。而在此例中,儘管閉包本體仍然只需要不可變參考就好,我們在閉包定義時想要指定 list 應該要透過 move 關鍵字移入閉包。新的執行緒可能會在主執行緒之前結束,或者主執行緒也有可能會先結束。如果主執行緒持有 list 的所有權卻在新執行緒之前結束並釋放 list 的話,執行緒拿到的不可變參考就會無效了。因此編譯器會要求 list 移入新執行緒的閉包中,這樣參考才會有效。嘗試看看將 move 關鍵字刪掉,或是在主執行緒的閉包定義之後使用 list,看看你會收到什麼編譯器錯誤訊息!","breadcrumbs":"函式語言功能:疊代器與閉包 » 閉包:獲取其環境的匿名函式 » 獲取參考或移動所有權","id":"237","title":"獲取參考或移動所有權"},"238":{"body":"一旦閉包從其定義的周圍環境獲取了數值的參考或所有權(也就是說被 移入 閉包中),閉包本體的程式碼會定義閉包在執行結束後要對參考或數值做什麼事情(也就是說被 移出 閉包)。閉包本體可以做以下的事情:將獲取的數值移出閉包、改變獲取的數值、不改變且不移動數值,或是一開始就不從環境獲取任何值。 閉包從周圍環境獲取並處理數值的方式會影響閉包會實作哪種特徵,而這些特徵能讓函式與結構體決定它們想使用哪種閉包。閉包會依照閉包本體處理數值的方式,自動實作一種或多種 Fn 特徵: FnOnce 適用於可以呼叫一次的閉包。所有閉包至少都會有此特徵,因為所有閉包都能被呼叫。會將獲取的數值移出本體的閉包只會實作 FnOnce 而不會再實作其他 Fn 特徵,因為這樣它只能被呼叫一次。 FnMut 適用於不會將獲取數值移出本體,而且可能會改變獲取數值的閉包。這種閉包可以被呼叫多次。 Fn 適用於不會將獲取數值移出本體,而且不會改變獲取數值或是甚至不從環境獲取數值的閉包。這種閉包可以被呼叫多次,而且不會改變周圍環境,這對於並行呼叫閉包多次來說非常重要。 讓我們來觀察範例 13-1 中 Option 用到的 unwrap_or_else 方法定義: impl Option { pub fn unwrap_or_else(self, f: F) -> T where F: FnOnce() -> T { match self { Some(x) => x, None => f(), } }\n} 回想一下 T 是一個泛型型別,代表著 Option 的 Some 變體內的數值型別。型別 T 同時也是函式 unwrap_or_else 的回傳型別:比如說對 Option 呼叫 unwrap_or_else 的話就會取得 String。 接著注意到函式 unwrap_or_else 有個額外的泛型型別參數 F。型別 F 是參數 f 的型別,也正是當我們呼叫 unwrap_or_else 時的閉包。 泛型型別 F 指定的特徵界限是 FnOnce() -> T,也就是說 F 必須要能夠呼叫一次、不帶任何引數然後回傳 T。在特徵界限中使用 FnOnce 限制了 unwrap_or_else 只能呼叫 f 最多一次。在 unwrap_or_else 本體中,如果 Option 是 Some 的話,f 就不會被呼叫。如果 Option 是 None 的話,f 就會被呼叫一次。由於所有閉包都有實作 FnOnce,unwrap_or_else 能接受大多數各種不同的閉包,讓它的用途非常彈性。 注意:函式也可以實作這三種 Fn 特徵。如果我們不必獲取環境數值,在我們需要有實作其中一種 Fn 特徵的項目時,我們可以使用函式名稱而不必用到閉包。舉例來說,對於 Option> 的數值,我們可以呼叫 unwrap_or_else(Vec::new) 在數值為 None 時取得新的空向量。 現在讓我們來看看標準函式庫中切片定義的 sort_by_key 方法,來觀察它和 unwrap_or_else 有什麼不同,以及為何 sort_by_key 的特徵界限使用的是 FnMut 而不是 FnOnce。閉包會取得一個引數,這會是該切片當下項目的參考,然後回傳型別 K 的數值以供排序。當你想透過切片項目的特定屬性做排序時,此函式會很實用。在範例 13-7 中,我們有個 Rectangle 實例的列表,然後我們使用 sort_by_key 透過 width 屬性由低至高排序它們: 檔案名稱:src/main.rs #[derive(Debug)]\nstruct Rectangle { width: u32, height: u32,\n} fn main() { let mut list = [ Rectangle { width: 10, height: 1 }, Rectangle { width: 3, height: 5 }, Rectangle { width: 7, height: 12 }, ]; list.sort_by_key(|r| r.width); println!(\"{:#?}\", list);\n} 範例 13-7:使用 sort_by_key 依據寬度來排序長方形 此程式碼會印出: $ cargo run Compiling rectangles v0.1.0 (file:///projects/rectangles) Finished dev [unoptimized + debuginfo] target(s) in 0.41s Running `target/debug/rectangles`\n[ Rectangle { width: 3, height: 5, }, Rectangle { width: 7, height: 12, }, Rectangle { width: 10, height: 1, },\n] sort_by_key 的定義會需要 FnMut 閉包的原因是因為它得呼叫閉包好幾次,對切片的每個項目都要呼叫一次。閉包 |r| r.width 沒有獲取、改變或移動周圍環境的任何值,所以它符合特徵界限的要求。 反之,範例 13-8 示範了一個只實作 FnOnce 特徵的閉包,因為它有將數值移出環境。編譯器不會允許我們將此閉包用在 sort_by_key: 檔案名稱:src/main.rs #[derive(Debug)]\nstruct Rectangle { width: u32, height: u32,\n} fn main() { let mut list = [ Rectangle { width: 10, height: 1 }, Rectangle { width: 3, height: 5 }, Rectangle { width: 7, height: 12 }, ]; let mut sort_operations = vec![]; let value = String::from(\"by key called\"); list.sort_by_key(|r| { sort_operations.push(value); r.width }); println!(\"{:#?}\", list);\n} 範例 13-8:嘗試在 sort_by_key 使用 FnOnce 閉包 這裡嘗試用很糟糕且令人費解的方式計算 list 在排序時 sort_by_key 被呼叫了幾次。此程式碼嘗試計數的方式是把閉包周圍環境中型別為 String 的 value 變數放入 sort_operations 向量中。閉包會獲取 value,然後將 value 移出閉包,也就是將 value 的所有權轉移到 sort_operations 向量裡。此向量只能呼叫一次,嘗試呼叫第二次是無法成功的,因為 value 已經不存在於環境中了,無法再次放入 sort_operations!因此,此閉包僅實作了 FnOnce。當我們嘗試編譯此程式碼時,我們會收到錯誤訊息說明 value 無法移出閉包,因為閉包必須實作 FnMut: $ cargo run Compiling rectangles v0.1.0 (/Users/wuwayne/Desktop/book-tw/listings/ch13-functional-features/listing-13-08)\nerror[E0507]: cannot move out of `value`, a captured variable in an `FnMut` closure --> src/main.rs:18:30 |\n15 | let value = String::from(\"by key called\"); | ----- captured outer variable\n16 |\n17 | list.sort_by_key(|r| { | --- captured by this `FnMut` closure\n18 | sort_operations.push(value); | ^^^^^ move occurs because `value` has type `String`, which does not implement the `Copy` trait For more information about this error, try `rustc --explain E0507`.\nerror: could not compile `rectangles` due to previous error 錯誤訊息指出閉包本體將 value 移出環境的地方。要修正此問題的話,我們需要改變閉包本體,讓它不再將數值移出環境。要計算 sort_by_key 呼叫次數的話,在環境中放置一個計數器,然後在閉包本體增加其值是更直觀的計算方法。範例 13-9 的閉包就能用在 sort_by_key,因為它只獲取了 num_sort_operations 計數器的可變參考,因此可以被呼叫不只一次: 檔案名稱:src/main.rs #[derive(Debug)]\nstruct Rectangle { width: u32, height: u32,\n} fn main() { let mut list = [ Rectangle { width: 10, height: 1 }, Rectangle { width: 3, height: 5 }, Rectangle { width: 7, height: 12 }, ]; let mut num_sort_operations = 0; list.sort_by_key(|r| { num_sort_operations += 1; r.width }); println!(\"{:#?} 的排序經過 {num_sort_operations} 次運算\", list);\n} 範例 13-9:在 sort_by_key 使用 FnMut 閉包是允許的 當我們要在函式或型別中定義與使用閉包時,Fn 特徵是很重要的。在下個段落中,我們將討論疊代器。疊代器有許多方法都需要閉包引數,所以隨著我們繼續下去別忘了複習閉包的用法!","breadcrumbs":"函式語言功能:疊代器與閉包 » 閉包:獲取其環境的匿名函式 » Fn 特徵以及將獲取的數值移出閉包","id":"238","title":"Fn 特徵以及將獲取的數值移出閉包"},"239":{"body":"疊代器(Iterator)模式讓你可以對一個項目序列依序進行某些任務。疊代器的功用是遍歷序列中每個項目,並決定該序列何時結束。當你使用疊代器,你不需要自己實作這些邏輯。 在 Rust 中疊代器是 惰性 (lazy)的,代表除非你呼叫方法來使用疊代器,不然它們不會有任何效果。舉例來說,範例 13-10 的程式碼會透過 Vec 定義的方法 iter 從向量v1 建立一個疊代器來遍歷它的項目。此程式碼本身沒有啥實用之處。 # fn main() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter();\n# } 範例 13-10:建立一個疊代器 疊代器儲存在變數 v1_iter 中。一旦我們建立了疊代器,我們可以有很多使用它的方式。在第三章的範例 3-5 中,我們在 for 迴圈中使用疊代器來對每個項目執行一些程式碼。在過程中這就隱性建立並使用了一個疊代器,雖然我們當時沒有詳細解釋細節。 在範例 13-11 中,我們區隔了疊代器的建立與使用疊代器 for 迴圈。當使用 v1_iter 疊代器的 for 迴圈被呼叫時,疊代器中的每個元素才會在迴圈中每次疊代中使用,以此印出每個數值。 # fn main() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); for val in v1_iter { println!(\"取得:{}\", val); }\n# } 範例 13-11:在 for 迴圈使用疊代器 在標準函式庫沒有提供疊代器的語言中,你可能會用別種方式寫這個相同的函式,像是先從一個變數 0 作為索引開始、使用該變數索引向量來獲取數值,然後在迴圈中增加變數的值直到它抵達向量的總長。 疊代器會為你處理這些所有邏輯,減少重複且你可能會搞砸的程式碼。疊代器還能讓你靈活地將相同的邏輯用於不同的序列,而不只是像向量這種你能進行索引的資料結構。讓我們研究看看疊代器怎麼辦到的。","breadcrumbs":"函式語言功能:疊代器與閉包 » 使用疊代器來處理一系列的項目 » 使用疊代器來處理一系列的項目","id":"239","title":"使用疊代器來處理一系列的項目"},"24":{"body":"你剛剛執行了一個新建立的程式,讓我們來檢查過程中的每一個步驟吧。 在你執行一支 Rust 程式前,你必須用 Rust 編譯器來編譯它,也就是輸入 rustc 命令然後加上你的原始檔案,像這樣子: $ rustc main.rs 如果你已經有 C 或 C++ 的背景,你應該就會發現這和 gcc 或 clang 非常相似。編譯成功後,Rust 編譯器會輸出一個二進制執行檔(binary executable)。 在 Linux、macOS 和 Windows 上的 PowerShell,你可以在你的 shell 輸入 ls 來查看你的執行檔: $ ls\nmain main.rs 在 Linux 和 macOS,你會看到兩個檔案。而在 Windows 上的 PowerShell,你會和使用 CMD 一樣看到三個檔案。在 Windows 上的 CMD,你需要輸入: > dir /B %= /B 選項代表只顯示檔案名稱 =%\nmain.exe\nmain.pdb\nmain.rs 這顯示了副檔名為 .rs 的原始碼檔案、執行檔(在 Windows 上為 main.exe ;其他則為 main ),然後在 Windows 上會再出現一個副檔名為 .pdb 的除錯資訊文件。在這裡,你就可以像這樣執行 main 或 main.exe 檔案: $ ./main # 在 Windows 上則是 .\\main.exe 如果你的 main.rs 正是你的「Hello, world!」程式,這命令就會顯示 Hello, world! 到你的終端機。 如果你比較熟悉動態語言,像是 Ruby、Python 或 JavaScript,你可能會比較不習慣將編譯與執行程式分為兩個不同的步驟。Rust 是一門 預先編譯 (ahead-of-time compiled)的語言,代表你可以編譯完成後將執行檔送到其他地方,然後他們就算沒有安裝 Rust 一樣可以執行起來。但如果你給某個人 .rb 、 .py 或 .js 檔案,他們就需要 Ruby、Python 或 Javascript 分別都有安裝好。當然你在這些語言只需要一行命令就可以執行,在語言設計中這一切都只是取捨。 在簡單的程式使用 rustc 來編譯不會有什麼問題,但當你的專案成長時,你將會需要管理所有選擇並讓程式碼易於分享。接下來我們將介紹 Cargo 這項工具給你,它將協助你寫出真正的 Rust 程式。","breadcrumbs":"開始入門 » Hello, World! » 編譯和執行是不同的步驟","id":"24","title":"編譯和執行是不同的步驟"},"240":{"body":"所有的疊代器都會實作定義在標準函式庫的 Iterator 特徵。特徵的定義如以下所示: pub trait Iterator { type Item; fn next(&mut self) -> Option; // 以下省略預設實作\n} 注意到此定義使用了一些新的語法:type Item 與 Self::Item,這是此特徵定義的 關聯型別(associated type) 。我們會在第十九章進一步探討關聯型別。現在你只需要知道此程式碼表示要實作 Iterator 特徵的話,你還需要定義 Item 型別,而此 Item 型別會用在方法 next 的回傳型別中。換句話說,Item 型別會是從疊代器回傳的型別。 Iterator 型別只要求實作者定義一個方法:next 方法會用 Some 依序回傳疊代器中的每個項目,並在疊代器結束時回傳 None。 我們可以直接在疊代器呼叫 next 方法。範例 13-12 展示從向量建立的疊代器重複呼叫 next 每次會得到什麼數值。 檔案名稱:src/lib.rs # #[cfg(test)]\n# mod tests { #[test] fn iterator_demonstration() { let v1 = vec![1, 2, 3]; let mut v1_iter = v1.iter(); assert_eq!(v1_iter.next(), Some(&1)); assert_eq!(v1_iter.next(), Some(&2)); assert_eq!(v1_iter.next(), Some(&3)); assert_eq!(v1_iter.next(), None); }\n# } 範例 13-12:對疊代器呼叫 next 方法 注意到 v1_iter 需要是可變的:在疊代器上呼叫 next 方法會改變疊代器內部用來紀錄序列位置的狀態。換句話說,此程式碼 消耗 或者說使用了疊代器。每次 next 的呼叫會從疊代器消耗一個項目。而我們不必在 for 迴圈指定 v1_iter 為可變是因為迴圈會取得 v1_iter 的所有權並在內部將其改為可變。 另外還要注意的是我們從 next 呼叫取得的是向量中數值的不可變參考。iter 方法會從疊代器中產生不可變參考。如果我們想要一個取得 v1 所有權的疊代器,我們可以呼叫 into_iter 而非 iter。同樣地,如果我們想要遍歷可變參考,我們可以呼叫 iter_mut 而非 iter。","breadcrumbs":"函式語言功能:疊代器與閉包 » 使用疊代器來處理一系列的項目 » Iterator 特徵與 next 方法","id":"240","title":"Iterator 特徵與 next 方法"},"241":{"body":"標準函式庫提供的 Iterator 特徵有一些不同的預設實作方法,你可以查閱標準函式庫的 Iterator 特徵 API 技術文件來找到這些方法。其中有些方法就是在它們的定義呼叫 next 方法,這就是為何當你實作 Iterator 特徵時需要提供 next 方法的實作。 會呼叫 next 的方法被稱之為 消耗配接器(consuming adaptors) ,因為呼叫它們會使用掉疊代器。其中一個例子就是方法 sum,這會取得疊代器的所有權並重複呼叫 next 來遍歷所有項目,因而消耗掉疊代器。隨著遍歷的過程中,他會將每個項目加到總計中,並在疊代完成時回傳總計數值。範例 13-13 展示了一個使用 sum 方法的測試: 檔案名稱:src/lib.rs # #[cfg(test)]\n# mod tests { #[test] fn iterator_sum() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); let total: i32 = v1_iter.sum(); assert_eq!(total, 6); }\n# } 範例 13-13:呼叫 sum 方法來取得疊代器中所有項目的總計數值 我們呼叫 sum 之後就不再被允許使用 v1_iter 了,因為 sum 取得了疊代器的所有權。","breadcrumbs":"函式語言功能:疊代器與閉包 » 使用疊代器來處理一系列的項目 » 消耗疊代器的方法","id":"241","title":"消耗疊代器的方法"},"242":{"body":"疊代配接器 (iterator adaptors)是定義在 Iterator 特徵的方法,它們不會消耗掉疊代器。它們會改變原本疊代器的一些屬性來產生不同的疊代器。 範例 13-14 呼叫了疊代器的疊代配接器方法 map,它會取得一個閉包在進行疊代時對每個項目進行呼叫。map 方法會回傳個項目被改變過的新疊代器。這裡的閉包會將向量中的每個項目加 1 來產生新的疊代器: 檔案名稱:src/main.rs # fn main() { let v1: Vec = vec![1, 2, 3]; v1.iter().map(|x| x + 1);\n# } 範例 13-14:呼叫疊代配接器 map 來建立新的疊代器 不過此程式碼會產生個警告: $ cargo run Compiling iterators v0.1.0 (file:///projects/iterators)\nwarning: unused `Map` that must be used --> src/main.rs:4:5 |\n4 | v1.iter().map(|x| x + 1); | ^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: `#[warn(unused_must_use)]` on by default = note: iterators are lazy and do nothing unless consumed warning: `iterators` (bin \"iterators\") generated 1 warning Finished dev [unoptimized + debuginfo] target(s) in 0.47s Running `target/debug/iterators` 範例 13-14 的程式碼不會做任何事情,我們指定的閉包沒有被呼叫到半次。警告提醒了我們原因:疊代配接器是惰性的,我們必須在此消耗疊代器才行。 要修正並消耗此疊代器,我們將使用 collect 方法,這是我們在範例 12-1 搭配 env::args 使用的方法。此方法會消耗疊代器並收集結果數值至一個資料型別集合。 在範例 13-15 中,我們將遍歷 map 呼叫所產生的疊代器結果數值收集到一個向量中。此向量最後會包含原本向量每個項目都加 1 的數值。 檔案名稱:src/main.rs # fn main() { let v1: Vec = vec![1, 2, 3]; let v2: Vec<_> = v1.iter().map(|x| x + 1).collect(); assert_eq!(v2, vec![2, 3, 4]);\n# } 範例 13-15:呼叫方法 map 來建立新的疊代器並呼叫 collect 方法來消耗新的疊代器來產生向量 因為 map 接受一個閉包,我們可以對每個項目指定任何我們想做的動作。這是一個展示如何使用閉包來自訂行為,同時又能重複使用 Iterator 特徵提供的遍歷行為的絕佳例子。 你可以透過疊代配接器串連多重呼叫,在進行一連串複雜運算的同時,仍保持良好的閱讀性。但因為所有的疊代器都是惰性的,你必須呼叫能消耗配接器的方法來取得疊代配接器的結果。","breadcrumbs":"函式語言功能:疊代器與閉包 » 使用疊代器來處理一系列的項目 » 產生其他疊代器的方法","id":"242","title":"產生其他疊代器的方法"},"243":{"body":"許多疊代配接器都會拿閉包作為引數,而通常我們向疊代配接器指定的閉包引數都能獲取它們周圍的環境。 在以下例子中,我們要使用 filter 方法來取得閉包。閉包會取得疊代器的每個項目並回傳布林值。如果閉包回傳 true,該數值就會被包含在 filter 產生的疊代器中;如果閉包回傳 false,該數值就不會被包含在結果疊代器中。 在範例 13-16 中我們使用 filter 與一個從它的環境獲取變數 shoe_size 的閉包來遍歷一個有 Shoe 結構體實例的集合。它會回傳只有符合指定大小的鞋子: 檔案名稱:src/lib.rs #[derive(PartialEq, Debug)]\nstruct Shoe { size: u32, style: String,\n} fn shoes_in_size(shoes: Vec, shoe_size: u32) -> Vec { shoes.into_iter().filter(|s| s.size == shoe_size).collect()\n} #[cfg(test)]\nmod tests { use super::*; #[test] fn filters_by_size() { let shoes = vec![ Shoe { size: 10, style: String::from(\"運動鞋\"), }, Shoe { size: 13, style: String::from(\"涼鞋\"), }, Shoe { size: 10, style: String::from(\"靴子\"), }, ]; let in_my_size = shoes_in_size(shoes, 10); assert_eq!( in_my_size, vec![ Shoe { size: 10, style: String::from(\"運動鞋\") }, Shoe { size: 10, style: String::from(\"靴子\") }, ] ); }\n} 範例 13-16:使用 filter 方法與一個獲取 shoe_size 的閉包 函式 shoes_in_size 會取得鞋子向量的所有權以及一個鞋子大小作為參數。它會回傳只有符合指定大小的鞋子向量。 在 shoes_in_size 的本體中,我們呼叫 into_iter 來建立一個會取得向量所有權的疊代器。然後我們呼叫 filter 來將該疊代器轉換成只包含閉包回傳為 true 的元素的新疊代器。 閉包會從環境獲取 shoe_size 參數並比較每個鞋子數值的大小,讓只有符合大小的鞋子保留下來。最後呼叫 collect 來收集疊代器回傳的數值進一個函式會回傳的向量。 此測試顯示了當我們呼叫 shoes_in_size 時,我們會得到我們指定相同大小的鞋子。","breadcrumbs":"函式語言功能:疊代器與閉包 » 使用疊代器來處理一系列的項目 » 使用閉包獲取它們的環境","id":"243","title":"使用閉包獲取它們的環境"},"244":{"body":"有了疊代器這樣的新知識,我們可以使用疊代器來改善第十二章的 I/O 專案,讓程式碼更清楚與簡潔。我們來看看疊代器如何改善 Config::build 函式與 search 函式的實作。","breadcrumbs":"函式語言功能:疊代器與閉包 » 改善我們的 I/O 專案 » 改善我們的 I/O 專案","id":"244","title":"改善我們的 I/O 專案"},"245":{"body":"在範例 12-6 中,我們加了些程式碼來取得 String 數值的切片並透過索引切片與克隆數值來產生 Config 實例,讓 Config 結構體能擁有其數值。在範例 13-17 中,我們重現了範例 12-23 的 Config::build 函式實作: 檔案名稱:src/lib.rs # use std::env;\n# use std::error::Error;\n# use std::fs;\n# # pub struct Config {\n# pub query: String,\n# pub file_path: String,\n# pub ignore_case: bool,\n# }\n# impl Config { pub fn build(args: &[String]) -> Result { if args.len() < 3 { return Err(\"引數不足\"); } let query = args[1].clone(); let file_path = args[2].clone(); let ignore_case = env::var(\"IGNORE_CASE\").is_ok(); Ok(Config { query, file_path, ignore_case, }) }\n}\n# # pub fn run(config: Config) -> Result<(), Box> {\n# let contents = fs::read_to_string(config.file_path)?;\n# # let results = if config.ignore_case {\n# search_case_insensitive(&config.query, &contents)\n# } else {\n# search(&config.query, &contents)\n# };\n# # for line in results {\n# println!(\"{line}\");\n# }\n# # Ok(())\n# }\n# # pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {\n# let mut results = Vec::new();\n# # for line in contents.lines() {\n# if line.contains(query) {\n# results.push(line);\n# }\n# }\n# # results\n# }\n# # pub fn search_case_insensitive<'a>(\n# query: &str,\n# contents: &'a str,\n# ) -> Vec<&'a str> {\n# let query = query.to_lowercase();\n# let mut results = Vec::new();\n# # for line in contents.lines() {\n# if line.to_lowercase().contains(&query) {\n# results.push(line);\n# }\n# }\n# # results\n# }\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# fn case_sensitive() {\n# let query = \"duct\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\n# Duct tape.\";\n# # assert_eq!(vec![\"safe, fast, productive.\"], search(query, contents));\n# }\n# # #[test]\n# fn case_insensitive() {\n# let query = \"rUsT\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\n# Trust me.\";\n# # assert_eq!(\n# vec![\"Rust:\", \"Trust me.\"],\n# search_case_insensitive(query, contents)\n# );\n# }\n# } 範例 13-17:重現範例 12-23 的 Config::build 函式 當時我們說先不用擔心 clone 呼叫帶來的效率問題,因為我們會在之後移除它們。現在正是絕佳時機! 我們在此需要 clone 的原因為我們的參數 args 是擁有 String 元素的切片,但是 build 函式並不擁有 args。要回傳 Config 實例的所有權,我們必須克隆數值給 Config 的 query 與 file_path 欄位,Config 實例才能擁有其值。 有了我們新學到的疊代器,我們可以改變 build 函式來取得疊代器的所有權來作為引數,而非借用切片。我們會來使用疊代器的功能,而不是檢查切片長度並索引特定位置。這能讓 Config::build 函式的意圖更清楚,因為疊代器會存取數值。 一旦 Config::build 取得疊代器的所有權並不再使用借用的索引動作,我們就可以從疊代器中移動 String 的數值至 Config 而非呼叫 clone 來產生新的配置。 直接使用回傳的疊代器 請開啟你的 I/O 專案下的 src/main.rs 檔案,這應該會看起來像這樣: 檔案名稱:src/main.rs # use std::env;\n# use std::process;\n# # use minigrep::Config;\n# fn main() { let args: Vec = env::args().collect(); let config = Config::build(&args).unwrap_or_else(|err| { eprintln!(\"解析引數時出現問題:{err}\"); process::exit(1); }); // --省略--\n# # if let Err(e) = minigrep::run(config) {\n# eprintln!(\"應用程式錯誤:{e}\");\n# process::exit(1);\n# }\n} 我們首先會改變範例 12-24 的 main 函式開頭段落成範例 13-18 這段使用疊代器的程式碼。這在我們更新 Config::build 之前都還無法編譯。 檔案名稱:src/main.rs # use std::env;\n# use std::process;\n# # use minigrep::Config;\n# fn main() { let config = Config::build(env::args()).unwrap_or_else(|err| { eprintln!(\"解析引數時出現問題:{err}\"); process::exit(1); }); // --snip--\n# # if let Err(e) = minigrep::run(config) {\n# eprintln!(\"應用程式錯誤:{e}\");\n# process::exit(1);\n# }\n} 範例 13-18:傳遞 env::args 的回傳值給 Config::build env::args 函式回傳的是疊代器!與其收集疊代器的數值成一個向量再傳遞切片給 Config::build,現在我們可以直接傳遞 env::args 回傳的疊代器所有權給 Config::build。 接下來,我們需要更新 Config::build 的定義。在 I/O 專案的 src/lib.rs 檔案中,讓我們變更 Config::build 的簽名成範例 13-19 的樣子。這還無法編譯,因為我們需要更新函式本體。 檔案名稱:src/lib.rs # use std::env;\n# use std::error::Error;\n# use std::fs;\n# # pub struct Config {\n# pub query: String,\n# pub file_path: String,\n# pub ignore_case: bool,\n# }\n# impl Config { pub fn build( mut args: impl Iterator, ) -> Result { // --snip--\n# if args.len() < 3 {\n# return Err(\"引數不足\");\n# }\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # let ignore_case = env::var(\"IGNORE_CASE\").is_ok();\n# # Ok(Config {\n# query,\n# file_path,\n# ignore_case,\n# })\n# }\n# }\n# # pub fn run(config: Config) -> Result<(), Box> {\n# let contents = fs::read_to_string(config.file_path)?;\n# # let results = if config.ignore_case {\n# search_case_insensitive(&config.query, &contents)\n# } else {\n# search(&config.query, &contents)\n# };\n# # for line in results {\n# println!(\"{line}\");\n# }\n# # Ok(())\n# }\n# # pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {\n# let mut results = Vec::new();\n# # for line in contents.lines() {\n# if line.contains(query) {\n# results.push(line);\n# }\n# }\n# # results\n# }\n# # pub fn search_case_insensitive<'a>(\n# query: &str,\n# contents: &'a str,\n# ) -> Vec<&'a str> {\n# let query = query.to_lowercase();\n# let mut results = Vec::new();\n# # for line in contents.lines() {\n# if line.to_lowercase().contains(&query) {\n# results.push(line);\n# }\n# }\n# # results\n# }\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# fn case_sensitive() {\n# let query = \"duct\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\n# Duct tape.\";\n# # assert_eq!(vec![\"safe, fast, productive.\"], search(query, contents));\n# }\n# # #[test]\n# fn case_insensitive() {\n# let query = \"rUsT\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\n# Trust me.\";\n# # assert_eq!(\n# vec![\"Rust:\", \"Trust me.\"],\n# search_case_insensitive(query, contents)\n# );\n# }\n# } 範例 13-19:更新 Config::build 的簽名來接收疊代器 標準函式庫技術文件顯示 env::args 函式回傳的疊代器型別為 std::env::Args,而該疊代器有實作 Iterator 特徵並回傳 String 數值。 我們更新了 Config::build 函式的簽名,讓參數 args 擁有個特徵界限為 impl Iterator 的泛型型別而非 &[String]。我們在第十章的 「特徵作為參數」 段落討論過 impl Trait 的語法,這樣的用法讓 args 可以接收任何有實作 Iterator 並回傳 String 數值的型別。 因為我們取得了 args 的所有權,而且我們需要將 args 成為可變的讓我們可以疊代它,所以我們將關鍵字 mut 加到 args 的參數指定使其成為可變的。 使用 Iterator 特徵方法而非索引 接下來,我們要修正 Config::build 的本體。因為 args 有實作 Iterator 特徵,所以我們知道我們可以對它呼叫 next 方法!範例 13-20 更新了範例 12-23 的程式碼來使用 next 方法: 檔案名稱:src/lib.rs # use std::env;\n# use std::error::Error;\n# use std::fs;\n# # pub struct Config {\n# pub query: String,\n# pub file_path: String,\n# pub ignore_case: bool,\n# }\n# impl Config { pub fn build( mut args: impl Iterator, ) -> Result { args.next(); let query = match args.next() { Some(arg) => arg, None => return Err(\"無法取得搜尋字串\"), }; let file_path = match args.next() { Some(arg) => arg, None => return Err(\"無法取得檔案路徑\"), }; let ignore_case = env::var(\"IGNORE_CASE\").is_ok(); Ok(Config { query, file_path, ignore_case, }) }\n}\n# # pub fn run(config: Config) -> Result<(), Box> {\n# let contents = fs::read_to_string(config.file_path)?;\n# # let results = if config.ignore_case {\n# search_case_insensitive(&config.query, &contents)\n# } else {\n# search(&config.query, &contents)\n# };\n# # for line in results {\n# println!(\"{line}\");\n# }\n# # Ok(())\n# }\n# # pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {\n# let mut results = Vec::new();\n# # for line in contents.lines() {\n# if line.contains(query) {\n# results.push(line);\n# }\n# }\n# # results\n# }\n# # pub fn search_case_insensitive<'a>(\n# query: &str,\n# contents: &'a str,\n# ) -> Vec<&'a str> {\n# let query = query.to_lowercase();\n# let mut results = Vec::new();\n# # for line in contents.lines() {\n# if line.to_lowercase().contains(&query) {\n# results.push(line);\n# }\n# }\n# # results\n# }\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# fn case_sensitive() {\n# let query = \"duct\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\n# Duct tape.\";\n# # assert_eq!(vec![\"safe, fast, productive.\"], search(query, contents));\n# }\n# # #[test]\n# fn case_insensitive() {\n# let query = \"rUsT\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\n# Trust me.\";\n# # assert_eq!(\n# vec![\"Rust:\", \"Trust me.\"],\n# search_case_insensitive(query, contents)\n# );\n# }\n# } 範例 13-20:變更 Config::build 的本體來使用疊代器方法 我們還記得 env::args 回傳的第一個數值會是程式名稱。我們想要忽略該值並取得下個數值,所以我們第一次呼叫 next 時不會對回傳值做任何事。再來我們才會呼叫 next 來取得我們想要的數值置入 Config 中的 query 欄位。如果 next 回傳 Some 的話,我們使用 match 來提取數值。如果它回傳 None 的話,這代表引數不足,所以我們提早用 Err 數值回傳。我們對 file_path 數值也做一樣的事。","breadcrumbs":"函式語言功能:疊代器與閉包 » 改善我們的 I/O 專案 » 使用疊代器移除 clone","id":"245","title":"使用疊代器移除 clone"},"246":{"body":"我們也可以對 I/O 專案中的 search 函式利用疊代器的優勢,範例 13-21 重現了範例 12-19 的程式碼: 檔案名稱:src/lib.rs # use std::error::Error;\n# use std::fs;\n# # pub struct Config {\n# pub query: String,\n# pub file_path: String,\n# }\n# # impl Config {\n# pub fn build(args: &[String]) -> Result {\n# if args.len() < 3 {\n# return Err(\"引數不足\");\n# }\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # Ok(Config { query, file_path })\n# }\n# }\n# # pub fn run(config: Config) -> Result<(), Box> {\n# let contents = fs::read_to_string(config.file_path)?;\n# # Ok(())\n# }\n# pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { let mut results = Vec::new(); for line in contents.lines() { if line.contains(query) { results.push(line); } } results\n}\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# fn one_result() {\n# let query = \"duct\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\";\n# # assert_eq!(vec![\"safe, fast, productive.\"], search(query, contents));\n# }\n# } 範例 13-21:範例 12-19 的 search 函式實作 我們可以使用疊代配接器(iterator adaptor)方法讓此程式碼更精簡。這樣做也能避免我們產生過程中的 results 可變向量。函式程式設計風格傾向於最小化可變狀態的數量使程式碼更加簡潔。移除可變狀態還在未來有機會讓搜尋可以平行化,因為我們不需要去管理 results 向量的並行存取。範例 13-22 展示了此改變: 檔案名稱:src/lib.rs # use std::env;\n# use std::error::Error;\n# use std::fs;\n# # pub struct Config {\n# pub query: String,\n# pub file_path: String,\n# pub ignore_case: bool,\n# }\n# # impl Config {\n# pub fn build(\n# mut args: impl Iterator,\n# ) -> Result {\n# args.next();\n# # let query = match args.next() {\n# Some(arg) => arg,\n# None => return Err(\"無法取得搜尋字串\"),\n# };\n# # let file_path = match args.next() {\n# Some(arg) => arg,\n# None => return Err(\"無法取得檔案路徑\"),\n# };\n# # let ignore_case = env::var(\"IGNORE_CASE\").is_ok();\n# # Ok(Config {\n# query,\n# file_path,\n# ignore_case,\n# })\n# }\n# }\n# # pub fn run(config: Config) -> Result<(), Box> {\n# let contents = fs::read_to_string(config.file_path)?;\n# # let results = if config.ignore_case {\n# search_case_insensitive(&config.query, &contents)\n# } else {\n# search(&config.query, &contents)\n# };\n# # for line in results {\n# println!(\"{line}\");\n# }\n# # Ok(())\n# }\n# pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { contents .lines() .filter(|line| line.contains(query)) .collect()\n}\n# # pub fn search_case_insensitive<'a>(\n# query: &str,\n# contents: &'a str,\n# ) -> Vec<&'a str> {\n# let query = query.to_lowercase();\n# let mut results = Vec::new();\n# # for line in contents.lines() {\n# if line.to_lowercase().contains(&query) {\n# results.push(line);\n# }\n# }\n# # results\n# }\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# fn case_sensitive() {\n# let query = \"duct\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\n# Duct tape.\";\n# # assert_eq!(vec![\"safe, fast, productive.\"], search(query, contents));\n# }\n# # #[test]\n# fn case_insensitive() {\n# let query = \"rUsT\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\n# Trust me.\";\n# # assert_eq!(\n# vec![\"Rust:\", \"Trust me.\"],\n# search_case_insensitive(query, contents)\n# );\n# }\n# } 範例 13-22:對 search 函式實作使用疊代配接器方法 回想一下 search 函式的目的是要回傳 contents 中所有包含 query 的行數。類似於範例 13-16 的 filter 範例,此程式碼使用 filter 配接器來只保留 line.contains(query) 回傳為 true 的行數。我們接著就可以用 collect 收集符合的行數成另一個向量。這樣簡單多了!你也可以對 search_case_insensitive 函式使用疊代器方法做出相同的改變。","breadcrumbs":"函式語言功能:疊代器與閉包 » 改善我們的 I/O 專案 » 透過疊代配接器讓程式碼更清楚","id":"246","title":"透過疊代配接器讓程式碼更清楚"},"247":{"body":"接下來的邏輯問題是在你自己的程式碼中你應該與為何要使用哪種風格呢:是要原本範例 13-21 的程式碼,還是範例 13-22 使用疊代器的版本呢?大多數的 Rust 程式設計師傾向於使用疊代器。一開始的確會有點難上手,不過一旦你熟悉各種疊代配接器與它們的用途後,疊代器就會很好理解了。不同於用迴圈迂迴處理每一步並建構新的向量,疊代器能更專注在迴圈的高階抽象上。這能抽象出常見的程式碼,並能更容易看出程式碼中的重點部位,比如疊代器中每個元素要過濾的條件。 但是這兩種實作真的完全相等嗎?你的直覺可能會假設低階的迴圈可能更快些。讓我們來討論效能吧。","breadcrumbs":"函式語言功能:疊代器與閉包 » 改善我們的 I/O 專案 » 迴圈與疊代器之間的選擇","id":"247","title":"迴圈與疊代器之間的選擇"},"248":{"body":"為了決定該使用迴圈還是疊代器,你需要知道哪個實作比較快:是顯式 for 迴圈的版本,還是疊代器的版本。 我們可以透過讀取整本 Sir Arthur Conan Doyle 寫的 The Adventures of Sherlock Holmes 到 String 中並搜尋內容中的 the 來進行評測。以下為針對 search 函式使用 for 迴圈與使用疊代器的版本評測(benchmark): test bench_search_for ... bench: 19,620,300 ns/iter (+/- 915,700)\ntest bench_search_iter ... bench: 19,234,900 ns/iter (+/- 657,200) 疊代器版本竟然比較快一些!我們在此不會解釋評測的程式碼,因為這裡的重點不在於證明這兩種版本是一樣快的,而是要理解這兩種實作對效能的影響。 要做更全面的評測的話,你應該要檢查使用不同大小的不同文字來作為 contents、不同單字與不同長度來作為 query,以及所有各式各樣的可能性。這邊的重點在於:疊代器雖然是高階抽象,但其編譯出來的程式碼與你親自寫出低階的程式碼幾乎相同。疊代器是 Rust 其中一種 零成本抽象 (zero-cost abstractions),這指的是使用的抽象不會在執行時有額外的開銷。這是 C++ 的初始設計暨實作者 Bjarne Stroustrup 在《Foundations of C++》(2012)書中所定義的 零開銷 (zero-overhead)的概念: 大致上來說,C++ 的實作遵守著零開銷的原則:你沒有使用到的話,你就不必買單。而且你有使用到的話,你不可能再寫出更好的程式碼。 作為另一個例子,以下程式碼是音訊解碼器的其中一個段落。解碼演算碼使用線性預測數學運算,並依據之前樣本的線性函式來預估未來的數值。此程式碼使用一連串的疊代器來對作用域中的三個變數進行數學運算:資料切片 buffer、長度為 12 的 coefficients 陣列以及要偏移的數量 qlp_shift。我們在範例中宣告變數但沒有給予任何數值,雖然只看此程式碼的確沒有什麼意義,但是這仍然是個現實世界中的其中一個簡例,可以來看出 Rust 如何將高階的想法轉換成低階的程式碼。 let buffer: &mut [i32];\nlet coefficients: [i64; 12];\nlet qlp_shift: i16; for i in 12..buffer.len() { let prediction = coefficients.iter() .zip(&buffer[i - 12..i]) .map(|(&c, &s)| c * s as i64) .sum::() >> qlp_shift; let delta = buffer[i]; buffer[i] = prediction as i32 + delta;\n} 要計算 prediction 的數值的話,此程式碼會遍歷 coefficients 的 12 個數值並使用 zip 方法將係數數值與 buffer 中之前 12 個數值做配對。然後在每個配對中,我們將數值相乘,然後相加所有結果,最後對總和往右偏移 qlp_shift 位。 像音訊解碼器這種的應用程式運算通常最注重效率。我們在此建立了一個疊代器,使用兩個配接器,然後消化數值。這段 Rust 程式碼會產生什麼樣的組合語言程式碼呢?在本書撰寫時,它會編譯出與你自己手寫一樣的組合語言。遍歷 coefficients 的數值完全不需用到迴圈:Rust 知道一共有 12 次疊代,所以它會「展開(unroll)」迴圈。 展開 是一種優化方式,這會移除迴圈控制程式碼並改產生針對迴圈中每次疊代的重複程式碼。 所有的係數都會存在暫存器中,這意味著存取數值會非常地快。在執行時不會有對陣列的界限檢查。這些所有 Rust 能做的優化讓產生的程式碼可以十分迅速。現在既然你已經知道了,你就可以自在地使用疊代器與閉包!它們可以寫出高階的程式碼,但不會犧牲執行時的效能。","breadcrumbs":"函式語言功能:疊代器與閉包 » 比較效能:迴圈 vs. 疊代器 » 比較效能:迴圈 vs. 疊代器","id":"248","title":"比較效能:迴圈 vs. 疊代器"},"249":{"body":"閉包與疊代器是 Rust 啟發自函式程式語言的概念。它們讓 Rust 在表達高階概念的同時,仍能擁有低階程式碼的效能。閉包與疊代器的實作對執行時效能不會有影響。這是 Rust 竭力提供零成本抽象的目標之一。 現在我們改善了 I/O 專案的可讀性,讓我們看看 cargo 一些更多的功能,來幫助我們分享專案給全世界。","breadcrumbs":"函式語言功能:疊代器與閉包 » 比較效能:迴圈 vs. 疊代器 » 總結","id":"249","title":"總結"},"25":{"body":"Cargo 是 Rust 的建構系統與套件管理工具。大部分的 Rustaceans 都會用此工具來管理他們的專案,因為 Cargo 能幫你處理很多任務,像是建構你的程式碼、下載你程式碼所需要的依賴函式庫並建構它們。我們常簡稱程式碼所需要用到的函式庫為 依賴(dependencies) 。 簡單的 Rust 程式像是我們目前所寫的不會有任何依賴。當我們用 Cargo 建構「Hello, world!」專案時,Cargo 只會用到建構程式碼的那部分。隨著你寫的 Rust 程式越來越複雜,你將會加入一些依賴函式庫來幫助你。而如果你使用 Cargo 的話,加入這些依賴就會簡單很多。 既然大多數的 Rust 專案都是用 Cargo,所以接下來本書也將假設你也使用 Cargo。Cargo 在你使用 「安裝教學」 的官方安裝連結來安裝 Rust 時就已經連同安裝好了。如果你是用其他方式下載 Rust 的話,想要檢查 Cargo 有沒有下載好可以透過你的終端機輸入: $ cargo --version 如果你有看到版本號,那就代表你有安裝了!如果你看到錯誤訊息,像是 command not found,請查看你的安裝辦法的技術文件,尋找如何額外下載 Cargo。","breadcrumbs":"開始入門 » Hello, Cargo! » Hello, Cargo!","id":"25","title":"Hello, Cargo!"},"250":{"body":"目前我們只使用了 Cargo 最基本的功能來建構、執行與測試我們的程式碼,但它還能做更多事。在本章節中我們將討論這些其他的進階功能,你將瞭解如何做到以下動作: 透過發佈設定檔來自訂你的建構 發佈函式庫到 crates.io 透過工作空間組織大型專案 從 crates.io 安裝執行檔 使用自訂命令擴展 Cargo 的功能 Cargo 能做的事比本章會介紹到的功能還多,所以想要知道它所有功能的話,歡迎查閱 它的技術文件 。","breadcrumbs":"更多關於 Cargo 與 Crates.io 的內容 » 更多關於 Cargo 與 Crates.io 的內容","id":"250","title":"更多關於 Cargo 與 Crates.io 的內容"},"251":{"body":"在 Rust 中 發佈設定檔 (release profiles)是個預先定義好並可用不同配置選項來自訂的設定檔,能讓程式設計師掌控更多選項來編譯程式碼。每個設定檔的配置彼此互相獨立。 Cargo 有兩個主要的設定檔:dev 設定檔會在當你對 Cargo 執行 cargo build 時所使用;release 設定檔會在當你對 Cargo 執行 cargo build --release 時所使用。dev 設定檔預設定義為適用於開發時使用,而release 設定檔預設定義為適用於發佈時使用。 你可能會覺得這些設定檔名稱很眼熟,因為它們就已經顯示在輸出結果過: $ cargo build Finished dev [unoptimized + debuginfo] target(s) in 0.0s\n$ cargo build --release Finished release [optimized] target(s) in 0.0s dev 與 release 是編譯器會使用到的不同設定檔。 當專案的 Cargo.toml 中你沒有顯式加上任何 [profile.*] 段落的話,Cargo 就會使用每個設定檔的預設設置。透過對你想要自訂的任何設定檔加上 [profile.*] 段落,你可以覆寫任何預設設定的子集。舉例來說,以下是 dev 與 release 設定檔中 opt-level 設定的預設數值: 檔案名稱:Cargo.toml [profile.dev]\nopt-level = 0 [profile.release]\nopt-level = 3 opt-level 設定控制了 Rust 對程式碼進行優化的程度,範圍從 0 到 3。提高優化程度會增加編譯時間,所以如果你在開發過程中得時常編譯程式碼的話,你傾向於編譯快一點而不管優化的多寡,就算結果程式碼會執行的比較慢。這就是 dev 的 opt-level 預設為 0 的原因。當你準備好要發佈你的程式碼時,則最好花多點時間來編譯。你只需要在發佈模式編譯一次,但你的編譯程式則會被執行很多次,所以發佈模式選擇花費多點編譯時間來讓程式跑得比較快。這就是 release 的 opt-level 預設為 3 的原因。 你可以在 Cargo.toml 加上不同的數值來覆蓋預設設定。舉例來說,如果我們希望在開發設定檔使用優化等級 1 的話,我們可以在專案的 Cargo.toml 檔案中加上這兩行: 檔案名稱:Cargo.toml [profile.dev]\nopt-level = 1 這樣就會覆蓋預設設定 0。現在當我們執行 cargo build,Cargo 就會使用 dev 設定檔的預設值以及我們自訂的 opt-level。因為我們將 opt-level 設為 1,Cargo 會比原本的預設進行更多優化,但沒有發佈建構那麼多。 對於完整的設置選項與每個設定檔的預設列表,請查閱 Cargo 的技術文件 。","breadcrumbs":"更多關於 Cargo 與 Crates.io 的內容 » 透過發佈設定檔自訂建構 » 透過發佈設定檔自訂建構","id":"251","title":"透過發佈設定檔自訂建構"},"252":{"body":"我們已經使用過 crates.io 的套件來作為我們專案的依賴函式庫,但是你也可以發佈你自己的套件來將你的程式碼提供給其他人使用。 crates.io 會發行套件的原始碼,所以它主要用來託管開源程式碼。 Rust 與 Cargo 有許多功能可以幫助其他人更容易找到並使用你發佈的套件。我們會介紹其中一些功能並解釋如何發佈套件。","breadcrumbs":"更多關於 Cargo 與 Crates.io 的內容 » 發佈 Crate 到 Crates.io » 發佈 Crate 到 Crates.io","id":"252","title":"發佈 Crate 到 Crates.io"},"253":{"body":"準確地加上套件的技術文件有助於其他使用者知道如何及何時使用它們,所以投資時間在寫技術文件上是值得的。在第三章我們提過如何使用兩條斜線 // 來加上 Rust 程式碼註解。Rust 還有個特別的註解用來作為技術文件,俗稱為 技術文件註解(documentation comment) ,這能用來產生 HTML 技術文件。這些 HTML 顯示公開 API 項目中技術文件註解的內容,讓對此函式庫有興趣的開發者知道如何 使用 你的 crate,而不需知道 crate 是如何 實作 的。 技術文件註解使用三條斜線 /// 而不是兩條,並支援 Markdown 符號來格式化文字。技術文件註解位於它們對應項目的上方。範例 14-1 顯示了 my_crate crate 中 add_one 的技術文件註解。 檔案名稱:src/lib.rs /// Adds one to the number given.\n///\n/// # Examples\n///\n/// ```\n/// let arg = 5;\n/// let answer = my_crate::add_one(arg);\n///\n/// assert_eq!(6, answer);\n/// ```\npub fn add_one(x: i32) -> i32 { x + 1\n} 範例 14-1:函式的技術文件註解 我們在這裡加上了解釋函式 add_one 行為的描述、加上一個標題為 Examples 的段落並附上展示如何使用 add_one 函式的程式碼。我們可以透過執行 cargo doc 來從技術文件註解產生 HTML 技術文件。此命令會執行隨著 Rust 一起發佈的工具 rustdoc,並在 target/doc 目錄下產生 HTML 技術文件。 為了方便起見,你可以執行 cargo doc --open 來建構當前 crate 的 HTML 技術文件(以及 crate 所有依賴的技術文件)並在網頁瀏覽器中開啟結果。導向到函式 add_one 而你就能看到技術文件註解是如何呈現的,如圖示 14-1 所示: 圖示 14-1:函式 add_one 的 HTML 技術文件 常見技術文件段落 我們在範例 14-1 使用 # Examples Markdown 標題來在 HTML 中建立一個標題為「Examples」的段落。以下是 crate 技術文件中常見的段落標題: Panics :該函式可能會導致恐慌的可能場合。函式的呼叫者不希望他們的程式恐慌的話,就要確保他們沒有在這些情況下呼叫該函式。 Errors :如果函式回傳 Result,解釋發生錯誤的可能種類以及在何種條件下可能會回傳這些錯誤有助於呼叫者,讓他們可以用不同方式來寫出處理不同種錯誤的程式碼。 Safety : 如果呼叫的函式是 unsafe 的話(我們會在第十九章討論不安全的議題),就必須要有個段落解釋為何該函式是不安全的,並提及函式預期呼叫者要確保哪些不變條件(invariants)。 大多數的技術文件註解不全都需要這些段落,但這些可能是使用者有興趣瞭解的內容,你可以作為提醒你的檢查列表。 將技術文件註解作為測試 在技術文件註解加上範例程式碼區塊有助於解釋如何使用你的函式庫,而且這麼做還有個額外好處:執行 cargo test 也會將你的技術文件視為測試來執行!在技術文件加上範例的確是最佳示範,但是如果程式碼在技術文件寫完之後變更的話,該範例可能就會無法執行了。如果我們對範例 14-1 中有附上技術文件的函式 add_one 執行 cargo test 的話,我們會看見測試結果有以下這樣的段落: Doc-tests my_crate running 1 test\ntest src/lib.rs - add_one (line 5) ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.27s 現在如果我們變更函式或範例使其內的 assert_eq! 會恐慌並再次執行 cargo test 的話,我們會看到技術文件測試能互相獲取錯誤,告訴我們範例與程式碼已經不同步了! 包含項目結構的註解 風格為 //! 技術文件註解會對其包含該註解的項目加上的技術文件,而不是對註解後的項目加上技術文件。我們通常將此技術文件註解用於 crate 源頭檔(通常為 src/lib.rs )或模組來對整個 crate 或模組加上技術文件。 舉例來說,如果我們希望能加上技術文件來描述包含 add_one 函式的 my_crate 目的,我們可以用 //! 在 src/lib.rs 檔案開頭加上技術文件註解,如範例 14-2 所示: 檔案名稱:src/lib.rs //! # My Crate\n//!\n//! `my_crate` is a collection of utilities to make performing certain\n//! calculations more convenient. /// Adds one to the number given.\n// --省略--\n# ///\n# /// # Examples\n# ///\n# /// ```\n# /// let arg = 5;\n# /// let answer = my_crate::add_one(arg);\n# ///\n# /// assert_eq!(6, answer);\n# /// ```\n# pub fn add_one(x: i32) -> i32 {\n# x + 1\n# } 範例 14-2:描述整個 my_crate crate 的技術文件 注意到 //! 最後一行之後並沒有緊貼任何程式碼,因為我們是用 //! 而非 /// 來下註解,我們是對包含此註解的整個項目加上技術文件,而不是此註解之後的項目。在此例中,該項目就是 src/lib.rs 檔案,也就是 crate 的源頭。這些註解會描述整個 crate。 當我們執行 cargo doc --open,這些註解會顯示在 my_crate 技術文件的首頁,位於 crate 公開項目列表的上方,如圖示 14-2 所示: 圖示 14-2:my_crate 的技術文件,包含描述整個 crate 的註解 項目中的技術文件註解可以用來分別描述 crate 和模組。用它們來將解釋容器整體的目的有助於你的使用者瞭解該 crate 的程式碼組織架構。","breadcrumbs":"更多關於 Cargo 與 Crates.io 的內容 » 發佈 Crate 到 Crates.io » 寫上有幫助的技術文件註解","id":"253","title":"寫上有幫助的技術文件註解"},"254":{"body":"公開 API 的架構是發佈 crate 時要考量到的一大重點。使用 crate 的人可能並沒有你那麼熟悉其中的架構,而且如果你的 crate 模組分層越深的話,他們可能就難以找到他們想使用的部分。 在第七章中,我們介紹了如何使用 mod 關鍵字來組織我們的程式碼成模組、如何使用 pub 關鍵字來公開項目,以及如何使用 use 關鍵字來將項目引入作用域。然而在開發 crate 時的架構雖然對你來說是合理的,但對你的使用者來說可能就不是那麼合適了。你可能會希望用有數個層級的分層架構來組織你的程式碼,但是要是有人想使用你定義在分層架構裡的型別時,它們可能就很難發現這些型別的存在。而且輸入 use my_crate::some_module::another_module::UsefulType; 是非常惱人的,我們會希望輸入 use my_crate::UsefulType; 就好。 好消息是如果你的架構 不便於 其他函式庫所使用的話,你不必重新組織你的內部架構:你可以透過使用 pub use選擇重新匯出(re-export)項目來建立一個不同於內部私有架構的公開架構。重新匯出會先取得某處的公開項目,再從其他地方使其公開,讓它像是被定義在其他地方一樣。 舉例來說,我們建立了一個函式庫叫做 art 來模擬藝術概念。在函式庫中有兩個模組:kinds 模組包含兩個列舉 PrimaryColor 和 SecondaryColor;而 utils 模組包含一個函式 mix,如範例 14-3 所示: 檔案名稱:src/lib.rs //! # Art\n//!\n//! A library for modeling artistic concepts. pub mod kinds { /// The primary colors according to the RYB color model. pub enum PrimaryColor { Red, Yellow, Blue, } /// The secondary colors according to the RYB color model. pub enum SecondaryColor { Orange, Green, Purple, }\n} pub mod utils { use crate::kinds::*; /// Combines two primary colors in equal amounts to create /// a secondary color. pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor { // --省略--\n# unimplemented!(); }\n} 範例 14-3:函式庫 art 有兩個模組項目 kinds 和 utils 圖示 14-3 顯示了此 crate 透過 cargo doc 產生的技術文件首頁: 圖示 14-3:art 的技術文件首頁陳列了 kinds 和 utils 模組 注意到 PrimaryColor 與 SecondaryColor 型別沒有列在首頁,而函式 mix 也沒有。我們必須點擊 kinds 與 utils 才能看到它們。 其他依賴此函式庫的 crate 需要使用 use 陳述式來將 art 的項目引入作用域中,並指定當前模組定義的架構。範例 14-4 顯示了從 art crate 使用 PrimaryColor 和 mix 項目的 crate 範例: 檔案名稱:src/main.rs use art::kinds::PrimaryColor;\nuse art::utils::mix; fn main() { let red = PrimaryColor::Red; let yellow = PrimaryColor::Yellow; mix(red, yellow);\n} 範例 14-4:一個使用 art 並匯出內部架構項目的 crate 範例 14-4 中使用 art crate 的程式碼作者必須搞清楚 PrimaryColor 位於 kinds 模組中而 mix 位於 utils 模組中。art crate 的模組架構對開發 art crate 的開發者才比較有意義,對使用者來說就沒那麼重要。內部架構沒有提供什麼有用的資訊給想要知道如何使用 art crate 的人,還容易造成混淆,因為開發者得自己搞清楚要從何處找起,而且必須在 use 陳述式中指定每個模組名稱。 要從公開 API 移除內部架構,我們可以修改範例 14-3 中 art crate 的程式碼,並加上 pub use 陳述式來在頂層重新匯出項目,如範例 14-5 所示: 檔案名稱:src/lib.rs //! # Art\n//!\n//! A library for modeling artistic concepts. pub use self::kinds::PrimaryColor;\npub use self::kinds::SecondaryColor;\npub use self::utils::mix; pub mod kinds { // --省略--\n# /// The primary colors according to the RYB color model.\n# pub enum PrimaryColor {\n# Red,\n# Yellow,\n# Blue,\n# }\n# # /// The secondary colors according to the RYB color model.\n# pub enum SecondaryColor {\n# Orange,\n# Green,\n# Purple,\n# }\n} pub mod utils { // --省略--\n# use crate::kinds::*;\n# # /// Combines two primary colors in equal amounts to create\n# /// a secondary color.\n# pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {\n# SecondaryColor::Orange\n# }\n} 範例 14-5:加上 pub use 陳述式來重新匯出項目 cargo doc 對此 crate 產生的 API 技術文件現在就會顯示與連結重新匯出的項目到首頁中,如圖示 14-4 所示。讓PrimaryColor 與 SecondaryColor 型別以及函式 mix 更容易被找到。 圖示 14-:art 的技術文件首頁會連結重新匯出的結果 art crate 使用者仍可以看到並使用範例 14-3 的內部架構,如範例 14-4 所展示的方式,或者它們可以使用像範例 14-5 這樣更方便的架構,如範例 14-6 所示: 檔案名稱:src/main.rs use art::mix;\nuse art::PrimaryColor; fn main() { // --省略--\n# let red = PrimaryColor::Red;\n# let yellow = PrimaryColor::Yellow;\n# mix(red, yellow);\n} 範例 14-6:使用從 art crate 重新匯出項目的程式 如果你有許多巢狀模組(nested modules)的話,在頂層透過 pub use 重新匯出型別可以大大提升使用 crate 的體驗。另一項 pub use 的常見用途是重新匯出目前 crate 依賴的定義,讓那些 crate 定義成會你的 crate 公開 API 的一部分。 提供實用的公開 API 架構更像是一門藝術而不只是科學,而你可以一步步來尋找最適合使用者的 API 架構。使用 pub use 可以給你更多組織 crate 內部架構的彈性,並將內部架構與你要呈現給使用者的介面互相解偶(decouple)。你可以觀察一些你安裝過的程式碼,看看它們的內部架構是不是不同於它們的公開 API。","breadcrumbs":"更多關於 Cargo 與 Crates.io 的內容 » 發佈 Crate 到 Crates.io » 透過 pub use 匯出理想的公開 API","id":"254","title":"透過 pub use 匯出理想的公開 API"},"255":{"body":"在你可以發佈任何 crate 之前,你需要建立一個 crates.io 的帳號並取得一個 API token。請前往 crates.io 的首頁並透過 GitHub 帳號來登入(GitHub 目前是必要的,但未來可能會支援其他建立帳號的方式)一旦你登入好了之後,到你的帳號設定 https://crates.io/me/ 並索取你的 API key,然後用這個 API key 來執行 cargo login 命令,如以下所示: $ cargo login abcdefghijklmnopqrstuvwxyz012345 此命令會傳遞你的 API token 給 Cargo 並儲存在本地的 ~/.cargo/credentials 。注意此 token 是個 祕密(secret) ,千萬不要分享給其他人。如果你因為任何原因分享給任何人的話,你最好撤銷掉並回到 crates.io 產生新的 token。","breadcrumbs":"更多關於 Cargo 與 Crates.io 的內容 » 發佈 Crate 到 Crates.io » 設定 Crates.io 帳號","id":"255","title":"設定 Crates.io 帳號"},"256":{"body":"讓我們假設你有個 crate 想要發佈。在發佈之前,你需要加上一些詮釋資料(metadata),也就是在 crate 的 Cargo.toml 檔案中 [package] 的段落內加上更多資料。 你的 crate 必須要有個獨特的名稱。雖然你在本地端開發 crate 時,你的 crate 可以是任何你想要的名稱。但是 crates.io 上的 crate 名稱採先搶先贏制。一旦有 crate 名稱被取走了,其他人就不能再使用該名稱來發佈 crate。在嘗試發佈 crate 前,最好先搜尋你想使用的名稱。如果該名稱已被使用了,你就需要想另一個名稱,並在 Cargo.toml 檔案中 [package] 段落的 name 欄位使用新的名稱來發佈,如以下所示: 檔案名稱:Cargo.toml [package]\nname = \"guessing_game\" 當你選好獨特名稱後,此時執行 cargo publish 來發佈 crate 的話,你會得到以下警告與錯誤: $ cargo publish Updating crates.io index\nwarning: manifest has no description, license, license-file, documentation, homepage or repository.\nSee https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info.\n--省略--\nerror: failed to publish to registry at https://crates.io Caused by: the remote server responded with an error: missing or empty metadata fields: description, license. Please see https://doc.rust-lang.org/cargo/reference/manifest.html for how to upload metadata 這是因為你還缺少一些關鍵資訊:描述與授權條款是必須的,所以人們才能知道你的 crate 在做什麼以及在何種情況下允許使用。在 Cargo.toml 檔案中加上一兩句描述,它就會顯示在你的 crate 的搜尋結果中。至於 license 欄位,你需要給予 license identifier value 。 Linux Foundation’s Software Package Data Exchange (SPDX) 有列出你可以使用的標識符數值。舉例來說,要指定你的 crate 使用 MIT 授權條款的話,就加上 MIT 標識符: 檔案名稱:Cargo.toml [package]\nname = \"guessing_game\"\nlicense = \"MIT\" 如果你想使用沒有出現在 SPDX 的授權條款,你需要將該授權條款的文字儲存在一個檔案中、將該檔案加入你的專案中並使用 license-file 來指定該檔案名稱,而不使用 license。 你的專案適合使用什麼樣的授權條款超出了本書的範疇。不過 Rust 社群中許多人都會用 MIT OR Apache-2.0 雙授權條款作為它們專案的授權方式,這和 Rust 的授權條款一樣。這也剛好展示你也可以用 OR 指定數個授權條款,讓你的專案擁有數個不同的授權方式。 有了獨特名稱、版本、描述與授權條款,已經準備好發佈的 Cargo.toml 檔案會如以下所示: 檔案名稱:Cargo.toml [package]\nname = \"guessing_game\"\nversion = \"0.1.0\"\nedition = \"2021\"\ndescription = \"A fun game where you guess what number the computer has chosen.\"\nlicense = \"MIT OR Apache-2.0\" [dependencies] Cargo 技術文件 還介紹了其他你可以指定的詮釋資料,讓你的 crate 更容易被其他人發掘並使用。","breadcrumbs":"更多關於 Cargo 與 Crates.io 的內容 » 發佈 Crate 到 Crates.io » 新增詮釋資料到新的 Crate","id":"256","title":"新增詮釋資料到新的 Crate"},"257":{"body":"現在你已經建立了帳號、儲存了 API token、選擇了 crate 的獨特名稱並指定了所需的詮釋資料,你現在已經準備好發佈了!發佈 crate 會上傳一個指定版本到 crates.io 供其他人使用。 發佈 crate 時請格外小心,因為發佈是會 永遠 存在的。該版本無法被覆寫,而且程式碼無法被刪除。 crates.io 其中一個主要目標就是要作為儲存程式碼的永久伺服器,讓所有依賴 crates.io 的 crate 的專案可以持續正常運作。允許刪除版本會讓此目標幾乎無法達成。不過你能發佈的 crate 版本不會有數量限制。 再次執行 cargo publish 命令,這次就應該會成功了: $ cargo publish Updating crates.io index Packaging guessing_game v0.1.0 (file:///projects/guessing_game) Verifying guessing_game v0.1.0 (file:///projects/guessing_game) Compiling guessing_game v0.1.0\n(file:///projects/guessing_game/target/package/guessing_game-0.1.0) Finished dev [unoptimized + debuginfo] target(s) in 0.19s Uploading guessing_game v0.1.0 (file:///projects/guessing_game) 恭喜!你現在將你的程式碼分享給 Rust 社群了,任何人現在都可以輕鬆將你的 crate 加到他們的專案中作為依賴了。","breadcrumbs":"更多關於 Cargo 與 Crates.io 的內容 » 發佈 Crate 到 Crates.io » 發佈至 Crates.io","id":"257","title":"發佈至 Crates.io"},"258":{"body":"當你對你的 crate 做了一些改變並準備好發佈新版本時,你可以變更 Cargo.toml 中的 version 數值,並再發佈一次。請使用 語意化版本規則 依據你作出的改變來決定下一個妥當的版本數字。接著執行 cargo publish 來上傳新版本。","breadcrumbs":"更多關於 Cargo 與 Crates.io 的內容 » 發佈 Crate 到 Crates.io » 對現有 Crate 發佈新版本","id":"258","title":"對現有 Crate 發佈新版本"},"259":{"body":"雖然你無法刪除 crate 之前的版本,你還是可以防止任何未來的專案加入它們作為依賴。這在 crate 版本因某些原因而被破壞時會很有用。在這樣的情況下,Cargo 支援 撤回(yanking) crate 版本。 撤回一個版本能防止新專案用該版本作為依賴,同時允許現存依賴它的專案能夠繼續依賴該版本。實際上,撤回代表所有專案的 Cargo.lock 都不會被破壞,且任何未來產生的 Cargo.lock 檔案不會使用被撤回的版本。 要撤回一個 crate 的版本,在你先前發布的 crate 目錄底下執行 cargo yank 並指定你想撤回的版本。舉例來說,如果我們發布了一個 guessing_game crate 的版本 1.0.1,然讓我們想撤回的話,我們可以在 guessing_game 專案目錄底下執行: $ cargo yank --vers 1.0.1 Updating crates.io index Yank guessing_game@1.0.1 而對命令加上 --undo 的話,你還可以在復原撤回的動作,允許其他專案可以再次依賴該版本: $ cargo yank --vers 1.0.1 --undo Updating crates.io index Yank guessing_game@1.0.1 撤回 並不會 刪除任何程式碼。舉例來說,它並不會刪除任何不小心上傳的祕密訊息。如果真的出現這種情形,你必須立即重設那些資訊。","breadcrumbs":"更多關於 Cargo 與 Crates.io 的內容 » 發佈 Crate 到 Crates.io » 透過 cargo yank 棄用 Crates.io 的版本","id":"259","title":"透過 cargo yank 棄用 Crates.io 的版本"},"26":{"body":"讓我們來用 Cargo 建立一個專案,並來比較它和我們原本的「Hello, world!」專案有什麼差別。請回到你的 projects 目錄(或者任何你決定存放程式碼的地方),然後在任何作業系統上輸入: $ cargo new hello_cargo\n$ cd hello_cargo 第一道命令會建立一個新的目錄與專案叫做 hello_cargo 。我們將我們的專案命名為 hello_cargo ,然後 Cargo 就會產生相同名稱的目錄並產生所需的檔案。 進入 hello_cargo 然後顯示檔案的話,你會看到 Cargo 產生了兩個檔案和一個目錄: Cargo.toml 檔案以及一個 src 目錄,其內包含一個 main.rs 檔案。 它還會初始化成一個新的 Git repository 並附上 .gitignore 檔案。如果已經在 Git repository 內的話,執行 cargo new 則不會產生 Git 的檔案。你可以用 cargo new --vcs=git 覆寫這項行為。 注意:Git 是一個常見的版本控制系統。你可以加上 --vcs 來變更 cargo new 去使用不同的版本控制系統,或是不用任何版本控制系統。請執行 cargo new --help 來查看更多可使用的選項。 請用任何你喜歡的編輯器開啟 Cargo.toml ,它應該會看起來和範例 1-2 差不多。 檔案名稱:Cargo.toml [package]\nname = \"hello_cargo\"\nversion = \"0.1.0\"\nedition = \"2021\" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] 範例 1-2:用 cargo new 產生的 Cargo.toml 此檔案用的是 TOML ( Tom’s Obvious, Minimal Language )格式,這是 Cargo 配置文件的格式。 第一行的 [package] 是一個段落(section)標題,說明以下的陳述式(statement)會配置這個套件。隨著我們加入更多資訊到此文件,我們也會加上更多段落。 接下來三行就是 Cargo 編譯你的程式所需的配置資訊:名稱、版本、誰寫的以及哪個 Rust edition 會用到。我們會在 附錄 E 介紹什麼是 edition。 最後一行 [dependencies] 是用來列出你的專案會用到哪些依賴的段落。在 Rust 中,程式碼套件會被稱為 crates 。我們在此專案還不需要任何其他 crate。但是我們會在第二章開始用到,屆時我們會再來介紹。 現在請開啟 src/main.rs 來看看: 檔案名稱:src/main.rs fn main() { println!(\"Hello, world!\");\n} Cargo 預設會為你產生一個「Hello, world!」程式,就像我們範例 1-1 寫的一樣!目前我們寫的專案與 Cargo 產生的程式碼不同的地方在於 Cargo 將程式碼放在 src 目錄底下,而且我們還有一個 Cargo.toml 配置文件在根目錄。 Cargo 預期你的原始檔案都會放在 src 目錄底下。專案的根目錄是用來放 README 檔案、授權條款、配置檔案以及其他與你的程式碼不相關的檔案。使用 Cargo 能夠幫助你組織你的專案,讓一切井然有序。 如果你的專案還沒開始使用 Cargo 的話,像是我們剛剛寫的「Hello, world!」專案,你只要將程式碼移入 src 然後產生正確的 Cargo.toml 檔案,就可以將它轉換成能夠使用 Cargo 的專案。","breadcrumbs":"開始入門 » Hello, Cargo! » 使用 Cargo 建立專案","id":"26","title":"使用 Cargo 建立專案"},"260":{"body":"在第十二章中,我們建立的套件包含一個執行檔 crate 與一個函式庫 crate。隨著專案開發,你可能會發現函式庫 crate 變得越來越大,而你可能會想要將套件拆成數個函式庫 crate。Cargo 提供了一個功能叫做 工作空間 (workspaces)能來幫助管理並開發數個相關的套件。","breadcrumbs":"更多關於 Cargo 與 Crates.io 的內容 » Cargo 工作空間 » Cargo 工作空間","id":"260","title":"Cargo 工作空間"},"261":{"body":"工作空間 是一系列的共享相同 Cargo.lock 與輸出目錄的套件。讓我們建立個使用工作空間的專案,我們會使用簡單的程式碼,好讓我們能專注在工作空間的架構上。組織工作空間的架構有很多種方式,我們會介紹其中一種常見的方式。我們的工作空間將會包含一個執行檔與兩個函式庫。執行檔會提供主要功能,並依賴其他兩個函式庫。其中一個函式庫會提供函式 add_one,而另一個函式庫會提供函式 add_two。這三個 crate 會包含在相同的工作空間中,我們先從建立工作空間的目錄開始: $ mkdir add\n$ cd add 接著在 add 目錄中,我們建立會設置整個工作空間的 Cargo.toml 檔案。此檔案不會有 [package] 段落。反之,他會使用一個 [workspace] 段落作為起始,讓我們可以透過指定執行檔 crate 的套件路徑來將它加到工作空間的成員中。在此例中,我們的路徑是 adder : 檔案名稱:Cargo.toml [workspace] members = [ \"adder\",\n] 接下來我們會在 add 目錄下執行 cargo new 來建立 adder 執行檔 crate: $ cargo new adder Created binary (application) `adder` package 在這個階段,我們已經可以執行 cargo build 來建構工作空間。目錄 add 底下的檔案應該會看起來像這樣: ├── Cargo.lock\n├── Cargo.toml\n├── adder\n│ ├── Cargo.toml\n│ └── src\n│ └── main.rs\n└── target 工作空間在頂層有一個 target 目錄用來儲存編譯結果。adder 套件不會有自己的 target 目錄。就算我們在 adder 目錄底下執行 cargo build,編譯結果仍然會在 add/target 底下而非 add/adder/target 。Cargo 之所以這樣組織工作空間的 target 目錄是因為工作空間的 crate 是會彼此互相依賴的。如果每個 crate 都有自己的 target 目錄,每個 crate 就得重新編譯工作空間中的其他每個 crate 才能將編譯結果放入它們自己的 target 目錄。共享 target 目錄的話,crate 可以避免不必要的重新建構。","breadcrumbs":"更多關於 Cargo 與 Crates.io 的內容 » Cargo 工作空間 » 建立工作空間","id":"261","title":"建立工作空間"},"262":{"body":"接下來讓我們在工作空間中建立另一個套件成員 add_one。請修改頂層 Cargo.toml 來指定 add_one 的路徑到 members 列表中: 檔案名稱:Cargo.toml [workspace] members = [ \"adder\", \"add_one\",\n] 然後產生新的函式庫 crate add_one: $ cargo new add_one --lib Created library `add_one` package add 目錄現在應該要擁有這些目錄與檔案: ├── Cargo.lock\n├── Cargo.toml\n├── add_one\n│ ├── Cargo.toml\n│ └── src\n│ └── lib.rs\n├── adder\n│ ├── Cargo.toml\n│ └── src\n│ └── main.rs\n└── target 在 add_one/src/lib.rs 檔案中,讓我們加上一個函式 add_one: 檔案名稱:add_one/src/lib.rs pub fn add_one(x: i32) -> i32 { x + 1\n} 現在我們可以讓我們 adder 套件的執行檔依賴擁有函式庫的 add_one 套件。首先,我們需要將 add_one 的路徑依賴加到 adder/Cargo.toml 。 檔案名稱:adder/Cargo.toml [dependencies]\nadd_one = { path = \"../add_one\" } Cargo 不會假設工作空間下的 crate 會彼此依賴,我們要指定彼此之間依賴的關係。 接著讓我們在 adder 內使用 add_one crate 的 add_one 函式。開啟 adder/src/main.rs 檔案並在最上方加上 use 來將 add_one 函式庫引入作用域。然後變更 main 函式來呼叫 add_one 函式,如範例 14-7 所示。 檔案名稱:adder/src/main.rs use add_one; fn main() { let num = 10; println!( \"你好,世界!{num} 加一會是 {}!\", add_one::add_one(num); );\n} 範例 14-7:在 adder crate 中使 add_one 函式庫 crate 讓我們在頂層的 add 目錄執行 cargo build 來建構工作空間吧! $ cargo build Compiling add_one v0.1.0 (file:///projects/add/add_one) Compiling adder v0.1.0 (file:///projects/add/adder) Finished dev [unoptimized + debuginfo] target(s) in 0.68s 要執行 add 目錄的執行檔 crate,我們可以透過 -p 加上套件名稱使用 cargo run 來執行我們想要在工作空間中指定的套件: $ cargo run -p adder Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/adder`\n你好,世界!10 加一會是 11! 這就會執行 adder/src/main.rs 的程式碼,其依賴於 add_one crate。 在工作空間中依賴外部套件 注意到工作空間只有在頂層有一個 Cargo.lock 檔案,而不是在每個 crate 目錄都有一個 Cargo.lock 。這確保所有的 crate 都對所有的依賴使用相同的版本。如果我們加了 rand 套件到 adder/Cargo.toml 與 add_one/Cargo.toml 檔案中,Cargo 會將兩者的版本解析為同一個 rand 版本並記錄到同個 Cargo.lock 中。確保工作空間所有 crate 都會使用相同依賴代表工作空間中的 crate 永遠都彼此相容。讓我們將 rand crate 加到 add_one/Cargo.toml 檔案的 [dependencies] 段落中,使 add_one crate 可以使用 rand crate: 檔案名稱:add_one/Cargo.toml rand = \"0.8.5\" 我們現在就可以將 use rand; 加到 add_one/src/lib.rs 檔案中,接著在 add 目錄下執行 cargo build 來建構整個工作空間就會引入並編譯 rand crate。我們會得到一個警告,因爲我們還沒有開始使用引入作用域的 rand: $ cargo build Updating crates.io index Downloaded rand v0.8.5 --省略-- Compiling rand v0.8.5 Compiling add_one v0.1.0 (file:///projects/add/add_one)\nwarning: unused import: `rand` --> add_one/src/lib.rs:1:5 |\n1 | use rand; | ^^^^ | = note: `#[warn(unused_imports)]` on by default\nwarning: `add_one` (lib) generated 1 warning Compiling adder v0.1.0 (file:///projects/add/adder) Finished dev [unoptimized + debuginfo] target(s) in 10.18s 頂層的 Cargo.lock 現在就包含 add_one 有 rand 作為依賴的資訊。不過就算我們能在工作空間的某處使用 rand,並不代表我們可以在工作空間的其他 crate 中使用它,除非它們的 Cargo.toml 也加上了 rand。舉例來說,如果我們將 use rand; 加到 adder/src/main.rs 檔案中想讓 adder 套件也使用的話,我們就會得到錯誤: $ cargo build --省略-- Compiling adder v0.1.0 (file:///projects/add/adder)\nerror[E0432]: unresolved import `rand` --> adder/src/main.rs:2:5 |\n2 | use rand; | ^^^^ no external crate `rand` 要修正此問題,只要修改 adder 套件的 Cargo.toml 檔案,指示它也加入 rand 作為依賴就好了。這樣建構 adder 套件就會將在 Cargo.lock 中將 rand 加入 adder 的依賴,但是沒有額外的 rand 會被下載。Cargo 會確保工作空間中每個套件的每個 crate 都會使用相同的 rand 套件版本。這可以節省空間,並能確保工作空間中的 crate 彼此可以互相兼容。 在工作空間中新增測試 讓我們再進一步加入一個測試函式 add_one::add_one 到 add_one crate 之中: 檔案名稱:add_one/src/lib.rs pub fn add_one(x: i32) -> i32 { x + 1\n} #[cfg(test)]\nmod tests { use super::*; #[test] fn it_works() { assert_eq!(3, add_one(2)); }\n} 現在在頂層的 add 目錄執行 cargo test。像這樣在工作空間的架構下執行 cargo test 會執行工作空間下所有 crate 的測試: $ cargo test Compiling add_one v0.1.0 (file:///projects/add/add_one) Compiling adder v0.1.0 (file:///projects/add/adder) Finished test [unoptimized + debuginfo] target(s) in 0.27s Running unittests src/lib.rs (target/debug/deps/add_one-f0253159197f7841 running 1 test\ntest tests::it_works ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running unittests src/main.rs (target/debug/deps/adder-49979ff40686fa8e running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests add_one running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 輸出的第一個段落顯示了 add_one crate 中的 it_works 測試通過。下一個段落顯示 adder crate 沒有任何測試,然後最後一個段落顯示 add_one 中沒有任何技術文件測試。 我們也可以在頂層目錄使用 -p 並指定我們想測試的 crate 名稱來測試工作空間中特定的 crate: $ cargo test -p add_one Finished test [unoptimized + debuginfo] target(s) in 0.00s Running unittests src/lib.rs (target/debug/deps/add_one-b3235fea9a156f74 running 1 test\ntest tests::it_works ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests add_one running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 此輸出顯示 cargo test 只執行了 add_one crate 的測試並沒有執行 adder crate 的測試。 如果你想要發佈工作空間的 crate 到 crates.io ,工作空間中的每個 crate 必須分別獨自發佈。和 cargo test 一樣,我們可以用 -p 的選項來指定想要的 crate 名稱,來發布工作空間內的特定 crate。 之後想嘗試練習的話,你可以在工作空間中在加上 add_two crate,方式和 add_one crate 類似! 隨著你的專案成長,你可以考慮使用工作空間:拆成各個小部分比一整塊大程式還更容易閱讀。再者,如果需要經常同時修改的話,將 crate 放在同個工作空間中更易於彼此的協作。","breadcrumbs":"更多關於 Cargo 與 Crates.io 的內容 » Cargo 工作空間 » 在工作空間中建立第二個套件","id":"262","title":"在工作空間中建立第二個套件"},"263":{"body":"cargo install 命令讓你能本地安裝並使用執行檔 crates。這並不是打算要取代系統套件,這是為了方便讓 Rust 開發者可以安裝 crates.io 上分享的工具。注意你只能安裝有執行檔目標的套件。 執行檔目標 (binary target)是在 crate 有 src/main.rs 檔案或其他指定的執行檔時,所建立的可執行程式。而相反地,函式庫目標就無法單獨執行,因為它提供給其他程式使用的函式庫。通常 crate 都會提供 README 檔案說明此 crate 是函式庫還是執行檔目標,或者兩者都是。 所有透過 cargo install 安裝的執行檔都儲存在安裝根目錄的 bin 資料夾中。如果你是用 rustup.rs 安裝 Rust 且沒有任何自訂設置的話,此目錄會是 $HOME/.cargo/bin 。請確定該目錄有在你的 $PATH 中,這樣才能夠執行 cargo install 安裝的程式。 舉例來說,第十二章我們提到有個 Rust 版本的 grep 工具叫做 ripgrep 能用來搜尋檔案。要安裝 ripgrep 的話,我們可以執行以下命令: $ cargo install ripgrep Updating crates.io index Downloaded ripgrep v13.0.0 Downloaded 1 crate (243.3 KB) in 0.88s Installing ripgrep v13.0.0\n--省略-- Compiling ripgrep v13.0.0 Finished release [optimized + debuginfo] target(s) in 3m 10s Installing ~/.cargo/bin/rg Installed package `ripgrep v13.0.0` (executable `rg`) 輸出的最後兩行顯示了執行檔的安裝位置與名稱,在 ripgrep 此例中就是 rg。如稍早提到的,只要你的 $PATH 有包含安裝目錄,你就可以執行 rg --help 並開始使用更快更鏽的搜尋檔案工具!","breadcrumbs":"更多關於 Cargo 與 Crates.io 的內容 » 透過 cargo install 安裝執行檔 » 透過 cargo install 安裝執行檔","id":"263","title":"透過 cargo install 安裝執行檔"},"264":{"body":"Cargo 的設計能讓你在不用修改 Cargo 的情況下擴展新的子命令。如果你 $PATH 中有任何叫做 cargo-something 的執行檔,你就可以用像是執行 Cargo 子命令的方式 cargo something 來執行它。像這樣的自訂命令在你執行 cargo --list 時也會顯示出來。能夠透過 cargo install 來安裝擴展外掛並有如內建 Cargo 工具般來執行使用是 Cargo 設計上的一大方便優勢!","breadcrumbs":"更多關於 Cargo 與 Crates.io 的內容 » 透過自訂命令來擴展 Cargo 的功能 » 透過自訂命令來擴展 Cargo 的功能","id":"264","title":"透過自訂命令來擴展 Cargo 的功能"},"265":{"body":"透過 Cargo 與 crates.io 分享程式碼是讓 Rust 生態系統能適用於許多不同任務的重要部分之一。Rust 的標準函式庫既小又穩定,但是 crate 可以很容易地分享、使用,並在語言本身不同的時間線來進行改善。千萬別吝嗇於分享你認為實用的程式碼到 crates.io ,其他人可能也會覺得它很有幫助!","breadcrumbs":"更多關於 Cargo 與 Crates.io 的內容 » 透過自訂命令來擴展 Cargo 的功能 » 總結","id":"265","title":"總結"},"266":{"body":"指標 (pointer)是一個將變數儲存記憶體位址的通用概念。此位址參考或者說是「指向」一些其他資料。Rust 中最常見的指標種類就是第四章介紹的參考(reference)。參考以 & 符號作為指示並借用它們指向的數值。它們除了參考資料以外,沒有其他的特殊能力,也沒有任何額外開銷。 另一方面, 智慧指標 (Smart pointers)是個不只會有像是指標的行為,還會包含擁有的詮釋資料與能力。智慧指標的概念並不是 Rust 獨有的,智慧指標起源於 C++ 且也都存在於其他語言。Rust 在標準函式庫中有提供許多不同的智慧指標,不只能參考還具備更多的功能。為了探索各個概念,我們會來研究一些各種不同的智慧指標範例,包含 參考計數 (reference counting)智慧指標型別。此指標允許一個資料可以有多個擁有者,並追蹤擁有者的數量,當沒有任何擁有者時,就清除資料。 在 Rust 中,我們有所有權與借用的概念,所以參考與智慧指標之間還有一項差別:參考是只有借用資料的指標,但智慧指標在很多時候都 擁有 它們指向的資料。 雖然在前面的章節我們沒有這樣稱呼,但我們已經在本書中遇過一些智慧指標了,像是第八章的 String 和 Vec,雖然當時我們沒有稱呼它們為智慧指標。這些型別都算是智慧指標,因為它們都擁有一些記憶體並允許你操控它們。它們也有詮釋資料以及額外的能力或保障。像是 String 就會將容量儲存在詮釋資料中,並確保其資料永遠是有效的 UTF-8。 智慧指標通常都使用結構體實作。和一般結構體不同,智慧指標會實作 Deref 與 Drop 特徵。Deref 特徵允許智慧指標結構體的實例表現的像是參考一樣,讓你可以寫出能用在參考與智慧指標的程式碼。Drop 特徵允許你自訂當智慧指標實例離開作用域時要執行的程式碼。在本章節我們會討論這兩個特徵並解釋為何它們對智慧指標很重要。 有鑑於智慧指標在 Rust 是個常用的通用設計模式,本章不會涵蓋每一個現有的智慧指標。許多函式庫也都會提供它們自己的智慧指標,你甚至能寫個你自己的。我們會提及標準函式庫中最常用到的智慧指標: Box 將數值配置到堆積上 Rc, 參考計數型別來允許資料能有數個擁有者 透過 RefCell 來存取 Ref 與 RefMut ,這是在執行時而非編譯時強制執行借用規則的型別 除此之外,我們還會涵蓋到 內部可變性 (interior mutability)模式,這讓不可變參考的型別能提供改變內部數值的 API。我們還會討論 參考循環 (reference cycles)為何會導致記憶體泄漏以及如何預防它們。 讓我們開始吧!","breadcrumbs":"智慧指標 » 智慧指標","id":"266","title":"智慧指標"},"267":{"body":"最直白的智慧指標是 box 其型別為 Box。Box 允許你儲存資料到堆積上,而不是堆疊。留在堆疊上的會是指向堆積資料的指標。你可以回顧第四章瞭解堆疊與堆積的差別。 Box 沒有額外的效能開銷,就只是將它們的資料儲存在堆積上而非堆疊而已。不過相對地它們也沒有多少額外功能。你大概會在這些場合用到它們: 當你有個型別無法在編譯時期確定大小,而你又想在需要知道確切大小的情況下使用該型別的數值。 當你有個龐大的資料,而你想要轉移所有權並確保資料不會被拷貝。 當你想要擁有某個值,但你只在意該型別有實作特定的特徵,而不是何種特定型別。 我們會在 「透過 Box 建立遞迴型別」 段落解說第一種情形。而在第二種情形,轉移龐大的資料的所有權可能會很花費時間,因為在堆疊上的話會拷貝所有資料。要改善此情形,我們可以用 box 將龐大的資料儲存在堆積上。這樣就只有少量的指標資料在堆疊上被拷貝,而其參考的資料仍然保留在堆積上的同個位置。第三種情況被稱之為 特徵物件(trait object) ,第十七章會花整個 「允許不同型別數值的特徵物件」 段落來討論此議題。所以你在此學到的到第十七章會再次用上!","breadcrumbs":"智慧指標 » 使用 Box 指向堆積上的資料 » 使用 Box 指向堆積上的資料","id":"267","title":"使用 Box 指向堆積上的資料"},"268":{"body":"在我們討論 Box 在堆積儲存空間上的使用場合前,我們會先介紹語法以及如何對 Box 內儲存的數值進行互動。 範例 15-1 顯示如何使用 box 在堆積上儲存一個 i32 數值: 檔案名稱:src/main.rs fn main() { let b = Box::new(5); println!(\"b = {}\", b);\n} 範例 15-1:使用 box 在堆積上儲存一個 i32 數值 我們定義了變數 b 其數值為 Box 配置在堆積上指向的數值 5。程式在此例會印出 b = 5,在此例中我們可以用在堆疊上相同的方式取得 box 的資料。就像任何有所有權的數值一樣,當 box 離開作用域時會釋放記憶體,在此例就是當 b 抵達 main 結尾的時候。釋放記憶體作用於 box(儲存在堆疊上)以及其所指向的資料(儲存在堆積上)。 將單一數值放在堆積上的確沒什麼用處,所以你不會對這種類型經常使用 box。在大多數情況下將像 i32 這種單一數值預設儲存在堆疊的確比較適合。","breadcrumbs":"智慧指標 » 使用 Box 指向堆積上的資料 » 使用 Box 儲存資料到堆積上","id":"268","title":"使用 Box 儲存資料到堆積上"},"269":{"body":"遞迴型別 (recursive type)的數值可以用相同型別的其他數值作為自己的一部分。遞迴型別對 Rust 來說會造成問題,因為 Rust 得在編譯期間時知道型別佔用的空間。由於這種巢狀數值理論上可以無限循環下去,Rust 無法知道一個遞迴型別的數值需要多大的空間。然而 box 則有已知大小,所以將 box 填入遞迴型別定義中,你就可以有遞迴型別了。 讓我們來探索 cons list 來作為遞迴型別的範例。這是個在函式程式語言中常見的資料型別,很適合作為遞迴型別的範例。我們要定義的 cons list 型別除了遞迴的部分以外都很直白,因此這個例子的概念在往後你遇到更複雜的遞迴型別時會很實用。 更多關於 Cons List 的資訊 cons list 是個起源於 Lisp 程式設計語言與其方言的資料結構,用巢狀配對組成,相當於 Lisp 版的鏈結串列(linked list)。這名字來自於 Lisp 中的 cons 函式(「construct function」的縮寫),它會從兩個引數建構一個新的配對,而這通常包含一個數值與另一個配對。對其呼叫 cons 就我們能建構出擁有遞迴配對的 cons list。 舉例來說,以下是個 cons list 的範例,包含了用括號包起來的 1、2、3 列表配對: (1, (2, (3, Nil))) 每個 cons list 的項目都包含兩個元素:目前項目的數值與下一個項目。列表中的最後一個項目只會包含一個數值叫做 Nil,並不會再連接下一個項目。cons list 透過遞迴呼叫 cons 函式來產生。表示遞迴終止條件的名稱為 Nil。注意這和第六章提到的「null」或「nil」的概念不全然相同,這些代表的是無效或空缺的數值。 在 Rust 中 cons lists 不是常見的資料結構。大多數當你在 Rust 需要項目列表時,Vec 會是比較好的選擇。而其他時候夠複雜的遞迴資料型別 確實 在各種特殊情形會很實用,不過先從 cons list 開始的話,我們可以專注探討 box 如何讓我們定義遞迴資料型別。 範例 15-2 包含了 cons list 的列舉定義。注意到此程式碼還不能編譯過,因為 List 型別並沒有已知的大小,我們接下來會繼續說明。 檔案名稱:src/main.rs enum List { Cons(i32, List), Nil,\n}\n# # fn main() {} 範例 15-2:第一次嘗試定義一個列舉來代表有 i32 數值的 cons list 資料結構 注意:我們定義的 cons list 只有 i32 數值是為了範例考量。我們當然可以使用第十章討論過的泛型來定義它,讓 cons list 定義的型別可以儲存任何型別數值。 使用 List 型別來儲存 1, 2, 3 列表的話會如範例 15-3 的程式碼所示: 檔案名稱:src/main.rs # enum List {\n# Cons(i32, List),\n# Nil,\n# }\n# use crate::List::{Cons, Nil}; fn main() { let list = Cons(1, Cons(2, Cons(3, Nil)));\n} 範例 15-3:使用 List 列舉儲存列表 1, 2, 3 第一個 Cons 值會得到 1 與另一個 List 數值。此 List 數值是另一個 Cons 數值且持有 2 與另一個 List 數值。此 List 數值是另一個 Cons 數值且擁有 3 與一個 List 數值,其就是最後的 Nil,這是傳遞列表結尾訊號的非遞迴變體。 如果我們嘗試編譯範例 15-3 的程式碼,我們會得到範例 15-4 的錯誤: $ cargo run Compiling cons-list v0.1.0 (file:///projects/cons-list)\nerror[E0072]: recursive type `List` has infinite size --> src/main.rs:1:1 |\n1 | enum List { | ^^^^^^^^^ recursive type has infinite size\n2 | Cons(i32, List), | ---- recursive without indirection |\nhelp: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable |\n2 | Cons(i32, Box), | ++++ + For more information about this error, try `rustc --explain E0072`.\nerror: could not compile `cons-list` due to previous error 範例 15-4:嘗試定義遞迴列舉所得到的錯誤 錯誤顯示此型別的「大小為無限」,原因是因為我們定義的 List 有個變體是遞迴:它直接存有另一個相同類型的數值。所以 Rust 無法判別出它需要多少空間才能儲存一個 List 的數值。讓我們進一步研究為何會得到這樣的錯誤,首先來看 Rust 如何決定要配置多少空間來儲存非遞迴型別。 計算非遞迴型別的大小 回想一下第六章中,當我們在討論列舉定義時,我們在範例 6-2 定義的 Message 列舉: enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32),\n}\n# # fn main() {} 要決定一個 Message 數值需要配置多少空間,Rust 會遍歷每個變體來看哪個變體需要最大的空間。Rust 會看到 Message::Quit 不佔任何空間、Message::Move 需要能夠儲存兩個 i32 的空間,以此類推。因為只有一個變體會被使用,一個 Message 數值所需的最大空間就是其最大變體的大小。 將此對應到當 Rust 嘗試檢查像是範例 15-2 的 List 列舉來決定遞迴型別需要多少空間時,究竟會發生什麼事。編譯器先從查看 Cons 的變體開始,其存有一個 i32 型別與一個 List 型別。因此 Cons 需要的空間大小為 i32 的大小加上 List 的大小。為了要瞭解 List 型別需要的多少記憶體,編譯器再進一步看它的變體,也是從 Cons 變體開始。Cons 變體存有一個型別 i32 與一個型別 List,而這樣的過程就無限處理下去,如圖示 15-1 所示。 圖示 15-1:無限個 List 包含著無限個 Cons 變體 使用 Box 取得已知大小的遞迴型別 由於 Rust 無法判別出遞迴定義型別要配置多少空間,所以編譯器會針對此錯誤提供些實用的建議: help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable |\n2 | Cons(i32, Box), | ++++ + 在此建議中,「indirection」代表與其直接儲存數值,我們可以變更資料結構,間接儲存指向數值的指標。 因為 Box 是個指標,Rust 永遠知道 Box 需要多少空間:指標的大小不會隨著指向的資料數量而改變。這代表我們可以將 Box 存入 Cons 變體而非直接儲存另一個 List 數值。Box 會指向另一個存在於堆積上的 List 數值而不是存在 Cons 變體中。概念上我們仍然有建立一個持有其他列表的列表,但此實作更像是將項目接著另一個項目排列,而非包含另一個在內。 我們可以改變範例 15-2 的 List 列舉定義以及範例 15-3 List 的使用方式,將其寫入範例 15-5,這次就能夠編譯過了: 檔案名稱:src/main.rs enum List { Cons(i32, Box), Nil,\n} use crate::List::{Cons, Nil}; fn main() { let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));\n} 範例 15-5:使用 Box 定義的 List 就有已知大小 Cons 變體需要的大小為 i32 加上儲存 box 指標的空間。Nil 變體沒有儲存任何數值,所以它需要的空間比 Cons 變體少。現在我們知道任何 List 數值會佔的空間都是一個 i32 加上 box 指標的大小。透過使用 box,我們打破了無限遞迴,所以編譯器可以知道儲存一個 List 數值所需要的大小。圖示 15-2 顯示了 Cons 變體看起來的樣子。 圖示 15-2:不再是無限大小的 List,因為其 Cons 存的是 Box Boxes 只提供了間接儲存與堆積配置,它們沒有其他任何特殊功能,比如我們等下就會看到的其他智慧指標型別。它們也沒有任何因這些特殊功能產生的額外效能開銷,所以它們很適合用於像是 cons list 這種我們只需要間接儲存的場合。我們在第十七章還會再介紹到更多 box 的使用情境。 Box 型別是智慧指標是因為它有實作 Deref 特徵,讓 Box 的數值可以被視為參考所使用。當 Box 數值離開作用域時,該 box 指向的堆積資料也會被清除,因為其有 Drop 特徵實作。這兩種特徵對於本章將會討論的其他智慧指標型別所提供的功能,將會更加重要。讓我們來探討這兩種特徵的細節吧。","breadcrumbs":"智慧指標 » 使用 Box 指向堆積上的資料 » 透過 Box 建立遞迴型別","id":"269","title":"透過 Box 建立遞迴型別"},"27":{"body":"現在讓我們看看用 Cargo 產生的「Hello, world!」程式在建構和執行時有什麼差別!請在你的 hello_cargo 目錄下輸入以下命令來建構專案: $ cargo build Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo) Finished dev [unoptimized + debuginfo] target(s) in 2.85 secs 此命令會產生一個執行檔 target/debug/hello_cargo (在 Windows 上則是 target\\debug\\hello_cargo.exe ),而不是在你目前的目錄。因為預設的建構會是 debug build,Cargo 會將執行檔放進名為 debug 的目錄。你可以用以下命令運行執行檔: $ ./target/debug/hello_cargo # 在 Windows 上的話則是 .\\target\\debug\\hello_cargo.exe\nHello, world! 如果一切順利,Hello, world! 就會顯示在終端機上。第一次執行 cargo build 的話,還會在根目錄產生另一個新檔案: Cargo.lock 。此檔案是用來追蹤依賴函式庫的確切版本。不過此專案沒有任何依賴,所以目前這個檔案看起來內容會有點少。你不會需要去手動更改此檔案,Cargo 會幫你管理這個檔案的內容。 我們剛用 cargo build 建構完專案並用 ./target/debug/hello_cargo 執行它。不過我們其實也可以只用一道命令 cargo run 來編譯程式碼並接著運行產生的執行檔: $ cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs Running `target/debug/hello_cargo`\nHello, world! 使用 cargo run 通常比執行 cargo build 然後使用執行檔的完整路徑還要方便,所以多數開發者通常都直接使用 cargo run。 請注意到這次輸出的結果我們沒有看到 Cargo 有在編譯 hello_cargo 的跡象,這是因為 Cargo 可以知道檔案完全沒被更改過,所以它不用重新建構可以選擇直接執行執行檔。如果你有變更你的原始碼的話,Cargo 才會在執行前重新建構專案,你才會看到這樣的輸出結果: $ cargo run Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo) Finished dev [unoptimized + debuginfo] target(s) in 0.33 secs Running `target/debug/hello_cargo`\nHello, world! Cargo 還提供一道命令 cargo check,此命令會快速檢查你的程式碼,確保它能編譯通過但不會產生執行檔: $ cargo check Checking hello_cargo v0.1.0 (file:///projects/hello_cargo) Finished dev [unoptimized + debuginfo] target(s) in 0.32 secs 為何你會不想要產生執行檔呢?這是因為 cargo check 省略了產生執行檔的步驟,所以它執行的速度比 cargo build 還來的快。如果你在寫程式時需要持續檢查的話,使用 cargo check 可以加快整體過程,讓你知道你的專案是否能編譯!所以許多 Rustaceans 都會在寫程式的過程中時不時執行 cargo check 來確保它能編譯。最後當他們準備好要使用執行檔時,才會用 cargo build。 讓我們來回顧我們目前學到的 Cargo 內容: 我們可以用 cargo new 產生專案。 我們可以用 cargo build 建構專案。 我們可以用 cargo run 同時建構並執行專案。 我們可以用 cargo check 建構專案來檢查錯誤,但不會產生執行檔。 Cargo 會儲存建構結果在 target/debug 目錄底下,而不是放在與我們程式碼相同的目錄。 使用 Cargo 還有一項好處是在任何作業系統所使用的命令都是相同的,所以到這邊開始我們不再需要特別提供 Linux 和 macOS 相對於 Windows 不同的特殊命令。","breadcrumbs":"開始入門 » Hello, Cargo! » 建構並執行 Cargo 專案","id":"27","title":"建構並執行 Cargo 專案"},"270":{"body":"實作 Deref 特徵讓你可以自訂 解參考運算子(dereference operator) * 的行為(這不是相乘或全域運算子)。透過這種方式實作 Deref 的智慧指標可以被視為正常參考來對待,這樣操作參考的程式碼也能用在智慧指標中。 讓我們先看解參考運算子如何在正常參考中使用。然後我們會嘗試定義一個行為類似 Box 的自定型別,並看看為何解參考運算子無法像參考那樣用在我們新定義的型別。我們將會探討如何實作 Deref 特徵使智慧指標能像類似參考的方式運作。接著我們會看看 Rust 的 強制解參考(deref coercion) 功能並瞭解它如何處理參考與智慧指標。 注意:我們即將定義的 MyBox 型別與真正的 Box 有一項很大的差別,就是我們的版本不會將其資料儲存在堆積上。我們在此例會專注在 Deref 上,所以資料實際上儲存在何處,並沒有比指標相關行為來得重要。","breadcrumbs":"智慧指標 » 透過 Deref 特徵將智慧指標視為一般參考 » 透過 Deref 特徵將智慧指標視為一般參考","id":"270","title":"透過 Deref 特徵將智慧指標視為一般參考"},"271":{"body":"一般的參考是一種指標,其中一種理解指標的方式是看成一個會指向存於某處數值的箭頭。在範例 15-6 中我們建立了數值 i32 的參考,接著使用解參考運算子來追蹤參考的數值: 檔案名稱:src/main.rs fn main() { let x = 5; let y = &x; assert_eq!(5, x); assert_eq!(5, *y);\n} 範例 15-6:使用解參考運算子來追蹤數值 i32 的參考 變數 x 存有 i32 數值 5。我們將 y 設置為 x 的參考。我們可以判定 x 等於 5。不過要是我們想要判定 y 數值的話,我們需要使用 *y 來追蹤參考指向的數值(也就是 解參考 ),這樣編譯器才能比較實際數值。一旦我們解參考 y,我們就能取得 y 指向的整數數值並拿來與 5 做比較。 如果我們嘗試寫說 assert_eq!(5, y); 的話,我們會得到此編譯錯誤: $ cargo run Compiling deref-example v0.1.0 (file:///projects/deref-example)\nerror[E0277]: can't compare `{integer}` with `&{integer}` --> src/main.rs:6:5 |\n6 | assert_eq!(5, y); | ^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}` | = help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}` = note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info) = help: the following other types implement trait `PartialEq`: f32 f64 i128 i16 i32 i64 i8 isize and 6 others For more information about this error, try `rustc --explain E0277`.\nerror: could not compile `deref-example` due to previous error 比較一個數字與一個數字的參考是不允許的,因為它們是不同的型別。我們必須使用解參考運算子來追蹤其指向的數值。","breadcrumbs":"智慧指標 » 透過 Deref 特徵將智慧指標視為一般參考 » 追蹤指標的數值","id":"271","title":"追蹤指標的數值"},"272":{"body":"我們將範例 15-6 的參考改用 Box 重寫。範例 15-7 對 Box 使用解參考運算子的方式如就和範例 15-6 對參考使用解參考運算子的方式一樣: 檔案名稱:src/main.rs fn main() { let x = 5; let y = Box::new(x); assert_eq!(5, x); assert_eq!(5, *y);\n} 範例 15-7:對 Box 使用解參考運算子 範例 15-7 與範例 15-6 主要的差別在於這裡我們設置 y 為一個指向 x 的拷貝數值的 Box 實例,而不是指向 x 數值的參考。在最後的判定中,我們可以對 Box 的指標使用解參考運算子,跟我們對當 y 還是參考時所做的動作一樣。接下來,我們要來探討 Box 有何特別之處,讓我們可以對自己定義的型別也可以使用解參考運算子。","breadcrumbs":"智慧指標 » 透過 Deref 特徵將智慧指標視為一般參考 » 像參考般使用 Box","id":"272","title":"像參考般使用 Box"},"273":{"body":"讓我們定義一個與標準函式庫所提供的 Box 型別類似的智慧指標,並看看智慧指標預設行為與參考有何不同。然後我們就會來看能夠使用解參考運算子的方式。 Box 本質上就是定義成只有一個元素的元組結構體,所以範例 15-8 用相同的方式來定義 MyBox。我們也定義了 new 函式來對應於 Box 的 new 函式。 檔案名稱:src/main.rs struct MyBox(T); impl MyBox { fn new(x: T) -> MyBox { MyBox(x) }\n}\n# # fn main() {} 範例 15-8:定義 MyBox 型別 我們定義了一個結構體叫做 MyBox 並宣告一個泛型參數 T,因為我們希望我們的型別能存有任何型別的數值。MyBox 是個只有一個元素型別為 T 的元組結構體。MyBox::new 函式接受一個參數型別為 T 並回傳存有該數值的 MyBox 實例。 讓我們將範例 15-7 的 main 函式加到範例 15-8 並改成使用我們定義的 MyBox 型別而不是原本的 Box。範例 15-9 的程式碼無法編譯,因為 Rust 不知道如何解參考MyBox。 檔案名稱:src/main.rs # struct MyBox(T);\n# # impl MyBox {\n# fn new(x: T) -> MyBox {\n# MyBox(x)\n# }\n# }\n# fn main() { let x = 5; let y = MyBox::new(x); assert_eq!(5, x); assert_eq!(5, *y);\n} 範例 15-9:嘗試像使用 Box 和參考一樣的方式來使用 MyBox 以下是編譯結果出現的錯誤: $ cargo run Compiling deref-example v0.1.0 (file:///projects/deref-example)\nerror[E0614]: type `MyBox<{integer}>` cannot be dereferenced --> src/main.rs:14:19 |\n14 | assert_eq!(5, *y); | ^^ For more information about this error, try `rustc --explain E0614`.\nerror: could not compile `deref-example` due to previous error 我們的 MyBox 型別無法解參考因為我們還沒有對我們的型別實作該能力。要透過 * 運算子來解參考的話,我們要實作 Deref 特徵。","breadcrumbs":"智慧指標 » 透過 Deref 特徵將智慧指標視為一般參考 » 定義我們自己的智慧指標","id":"273","title":"定義我們自己的智慧指標"},"274":{"body":"如同第十章的 「為型別實作特徵」 段落所講過的,要實作一個特徵的話,我們需要提供該特徵要求的方法實作。標準函式庫所提供的 Deref 特徵要求我們實作一個方法叫做 deref,這會借用 self 並回傳內部資料的參考。範例 15-10 包含了對 MyBox 定義加上的 Deref 實作: 檔案名稱:src/main.rs use std::ops::Deref; impl Deref for MyBox { type Target = T; fn deref(&self) -> &Self::Target { &self.0 }\n}\n# # struct MyBox(T);\n# # impl MyBox {\n# fn new(x: T) -> MyBox {\n# MyBox(x)\n# }\n# }\n# # fn main() {\n# let x = 5;\n# let y = MyBox::new(x);\n# # assert_eq!(5, x);\n# assert_eq!(5, *y);\n# } 範例 15-10:對 MyBox 實作 Deref type Target = T; 語法定義了一個供 Deref 特徵使用的關聯型別。關聯型別與宣告泛型參數會有一點差別,但是你現在先不用擔心它們,我們會在第十九章深入探討。 我們對 deref 的方法本體加上 &self.0,deref 就可以回傳一個參考讓我們可以使用 * 運算子取得數值。回想一下第五章的 「使用無名稱欄位的元組結構體來建立不同型別」 段落,.0 可以取的元組結構體的第一個數值。範例 15-9 的 main 函式現在對 MyBox 數值的 * 呼叫就可以編譯了,而且判定也會通過! 沒有 Deref 特徵的話,編譯器只能解參考 & 的參考。deref 方法讓編譯器能夠從任何有實作 Deref 的型別呼叫 deref 方法取得 & 參考,而它就可以進一步解參考獲取數值。 當我們在範例 15-9 中輸入 *y 時,Rust 背後實際上是執行此程式碼: *(y.deref()) Rust 將 * 運算子替換為方法 deref 的呼叫再進行普通的解參考,所以我們不必煩惱何時該或不該呼叫 deref 方法。此 Rust 特性讓我們可以對無論是參考或是有實作 Deref 的型別都能寫出一致的程式碼。 deref 方法會回傳一個數值參考,以及括號外要再加上普通解參考的原因,都是因為所有權系統。如果 deref 方法直接回傳數值而非參考數值的話,該數值就會移出 self。我們不希望在此例或是大多數使用解參考運算子的場合下,取走 MyBox 內部數值的所有權。 注意到每次我們在程式碼中使用 * 時,* 運算子被替換成 deref 方法呼叫,然後再呼叫 * 剛好一次。因為 * 運算子不會被無限遞迴替換,我們能剛好取得型別 i32 並符合範例 15-9 assert_eq! 中與 5 的判定。","breadcrumbs":"智慧指標 » 透過 Deref 特徵將智慧指標視為一般參考 » 透過實作 Deref 特徵來將一個型別能像參考般對待","id":"274","title":"透過實作 Deref 特徵來將一個型別能像參考般對待"},"275":{"body":"強制解參考(Deref coercion) 會將有實作 Deref 特徵的型別參考轉換成其他型別的參考。舉例來說,強制解參考可以轉換 &String 成 &str,因為 String 有實作 Deref 特徵並能用它來回傳 &str。強制解參考是一個 Rust 針對函式或方法的引數的便利設計,且只會用在有實作 Deref 特徵的型別。當我們將某個特定型別數值的參考作為引數傳入一個函式或方法,但該函式或方法所定義的參數卻不相符時,強制解參考就會自動發生,並進行一系列的 deref 方法呼叫,將我們提供的型別轉換成參數所需的型別。 Rust 會加入強制解參考的原因是因為程式設計師在寫函式與方法呼叫時,就不必加上許多顯式參考 & 與解參考 *。強制解參考還讓我們可以寫出能同時用於參考或智慧指標的程式碼。 為了展示強制解參考,讓我們使用範例 15-8 定義的 MyBox 型別以及範例 15-10 所加上的 Deref 實作。範例 15-11 中定義的函式使用字串切片作為參數: 檔案名稱:src/main.rs fn hello(name: &str) { println!(\"Hello, {name}!\");\n}\n# # fn main() {} 範例 15-11:hello 函式且有參數 name 其型別為 &str 我們可以使用字串切片作為引數來呼叫函式 hello,比方說 hello(\"Rust\");。強制解參考讓我們可以透過 MyBox 型別數值的參考來呼叫 hello,如範例 15-12 所示: 檔案名稱:src/main.rs # use std::ops::Deref;\n# # impl Deref for MyBox {\n# type Target = T;\n# # fn deref(&self) -> &T {\n# &self.0\n# }\n# }\n# # struct MyBox(T);\n# # impl MyBox {\n# fn new(x: T) -> MyBox {\n# MyBox(x)\n# }\n# }\n# # fn hello(name: &str) {\n# println!(\"Hello, {name}!\");\n# }\n# fn main() { let m = MyBox::new(String::from(\"Rust\")); hello(&m);\n} 範例 15-12:利用強制解參考透過 MyBox 數值的參考來呼叫 hello 我們在此使用 &m 作為引數來呼叫函式 hello,這是 MyBox 數值的參考。因為我們在範例 15-10 有對 MyBox 實作 Deref 特徵,Rust 可以呼叫 deref 將 &MyBox 變成 &String。標準函式庫對 String 也有實作 Deref 並會回傳字串切片,這可以在 Deref 的 API 技術文件中看到。所以 Rust 會再呼叫 deref 一次來將 &String 變成 &str,這樣就符合函式 hello 的定義了。 如果 Rust 沒有實作強制解參考的話,我們就得用範例 15-13 的方式才能辦到範例 15-12 使用型別 &MyBox 的數值來呼叫 hello 的動作。 檔案名稱:src/main.rs # use std::ops::Deref;\n# # impl Deref for MyBox {\n# type Target = T;\n# # fn deref(&self) -> &T {\n# &self.0\n# }\n# }\n# # struct MyBox(T);\n# # impl MyBox {\n# fn new(x: T) -> MyBox {\n# MyBox(x)\n# }\n# }\n# # fn hello(name: &str) {\n# println!(\"Hello, {name}!\");\n# }\n# fn main() { let m = MyBox::new(String::from(\"Rust\")); hello(&(*m)[..]);\n} 範例 15-13:如果 Rust 沒有強制解參考,我們就得這樣寫程式碼 (*m) 會將 MyBox 解參考成 String,然後 & 和 [..] 會從 String 中取得等於整個字串的字串切片,這就符合 hello 的簽名。沒有強制解參考的程式碼就難以閱讀、寫入或是理解,因為有太多的符號參雜其中。強制解參考能讓 Rust 自動幫我們做這些轉換。 當某型別有定義 Deref 特徵時,Rust 會分析該型別並重複使用 Deref::deref 直到能取得與參數型別相符的參考。Deref::deref 需要呼叫的次數會在編譯時期插入,所以使用強制解參考沒有任何的執行時開銷!","breadcrumbs":"智慧指標 » 透過 Deref 特徵將智慧指標視為一般參考 » 函式與方法的隱式強制解參考","id":"275","title":"函式與方法的隱式強制解參考"},"276":{"body":"類似於你使用 Deref 特徵來覆蓋不可變參考的 * 運算子的方式,你也可以使用 DerefMut 特徵來覆蓋可變參考的 * 運算子。 當 Rust 發現型別與特徵實作符合以下三種情況時,它就會進行強制解參考: 從 &T 到 &U 且 T: Deref 從 &mut T 到 &mut U 且 T: DerefMut 從 &mut T 到 &U 且 T: Deref 前兩個除了第二個有實作可變性之外是相同的。第一個情況表示如果你有個 &T 且 T 有實作 Deref 到某個型別 U,你就可以直接得到 &U。第二種情況指的則是對可變參考的強制解參考。 第三種情況比較棘手:Rust 也能強制將可變參考轉為一個不可變參考。但反過來是 不可行 的:不可變參考永遠不可能強制解參考成可變參考。由於借用規則,如果你有個可變參考,該可變參考必須是該資料的唯一參考(不然程式無法編譯)。轉換可變參考成不可變參考不會破壞借用規則。轉換不可變參考成可變參考的話,就需要此不可變參考是該資料的唯一參考,但借用規則無法做擔保。因此 Rust 無法將不可變參考轉換成可變參考。","breadcrumbs":"智慧指標 » 透過 Deref 特徵將智慧指標視為一般參考 » 強制解參考如何處理可變性","id":"276","title":"強制解參考如何處理可變性"},"277":{"body":"第二個對智慧指標模式很重要的特徵是 Drop,這讓你能自訂數值離開作用域時的行為。你可以對任何型別實作 Drop 特徵,然後你指定的程式碼就能用來釋放像是檔案或網路連線等資源。我們在智慧指標的章節介紹 Drop 的原因是因為 Drop 特徵的功能幾乎永遠會在實作智慧指標時用到。舉例來說,當 Box 離開作用域時,它會釋放該 box 在堆積上指向的記憶體空間。 在某些語言中,當程式設計師使用完某些型別的實例後,每次都得呼叫釋放記憶體與資源的程式碼。例子包括檔案控制代碼(file handle)、插座(socket)或鎖。如果他們忘記的話,系統可能就會過載並崩潰。在 Rust 中你可以對數值離開作用域時指定一些程式碼,然後編譯器就會自動插入此程式碼。所以你就不用每次在特定型別實例使用完時,在程式的每個地方都寫上清理程式碼。而且你還不會泄漏資源! 透過實作 Drop 特徵我們可以指定當數值離開作用域時要執行的程式碼。Drop 特徵會要求我們實作一個方法叫做 drop,這會取得 self 的可變參考。為了觀察 Rust 何時會呼叫 drop,讓我們先用 println! 陳述式實作 drop。 範例 15-14 的結構體 CustomSmartPointer 只有一個功能,那就是在實例離開作用域時印出 Dropping CustomSmartPointer!。此範例能夠展示 Rust 何時會執行 drop 函式。 檔案名稱:src/main.rs struct CustomSmartPointer { data: String,\n} impl Drop for CustomSmartPointer { fn drop(&mut self) { println!(\"釋放 CustomSmartPointer 的資料 `{}`!\", self.data); }\n} fn main() { let c = CustomSmartPointer { data: String::from(\"我的東東\"), }; let d = CustomSmartPointer { data: String::from(\"其他東東\"), }; println!(\"CustomSmartPointers 建立完畢。\");\n} 範例 15-14:CustomSmartPointer 結構體實作了會放置清理程式碼的 Drop 特徵 Drop 特徵包含在 prelude 中,所以我們不需要特地引入作用域。我們對 CustomSmartPointer 實作 Drop 特徵並提供會呼叫 println! 的 drop 方法實作。drop 的函式本體用來放置你想要在型別實例離開作用域時執行的邏輯。我們在此印出一些文字來展示 Rust 如何呼叫 drop。 在 main 中,我們建立了兩個 CustomSmartPointer 實例並印出 CustomSmartPointers 建立完畢。在 main 結尾,我們的 CustomSmartPointer 實例會離開作用域,然後 Rust 就會呼叫我們放在 drop 方法的程式碼,也就是印出我們的最終訊息。注意到我們不需要顯式呼叫 drop 方法。 當我們執行此程式時,我們會看到以下輸出: $ cargo run Compiling drop-example v0.1.0 (file:///projects/drop-example) Finished dev [unoptimized + debuginfo] target(s) in 0.60s Running `target/debug/drop-example`\nCustomSmartPointers 建立完畢。\n釋放 CustomSmartPointer 的資料 `其他東東`!\n釋放 CustomSmartPointer 的資料 `我的東東`! 當我們的實例離開作用域時,Rust 會自動呼叫 drop,呼叫我們指定的程式碼。變數會以與建立時相反的順序被釋放,所以 d 會在 c 之前被釋放。此範例給了我們一個觀察 drop 如何執行的視覺化指引,通常你會指定該型別所需的清除程式碼,而不是印出訊息。","breadcrumbs":"智慧指標 » 透過 Drop 特徵執行清除程式碼 » 透過 Drop 特徵執行清除程式碼","id":"277","title":"透過 Drop 特徵執行清除程式碼"},"278":{"body":"不幸的是,我們無法直接了當地取消自動 drop 的功能。停用 drop 通常是不必要的,整個 Drop 的目的本來就是要能自動處理。不過有些時候你可能會想要提早清除數值。其中一個例子是使用智慧指標來管理鎖:你可能會想要強制呼叫 drop 方法來釋放鎖,好讓作用域中的其他程式碼可以取得該鎖。Rust 不會讓你手動呼叫 Drop 特徵的 drop 方法。不過如果你想要一個數值在離開作用域前就被釋放的話,你可以使用標準函式庫提供的 std::mem::drop 函式來呼叫。 如果我們嘗試修改範例 15-14 的 main 函式來手動呼叫 Drop 特徵的 drop 方法,如範例 15-15 所示,我們會得到編譯錯誤: 檔案名稱:src/main.rs # struct CustomSmartPointer {\n# data: String,\n# }\n# # impl Drop for CustomSmartPointer {\n# fn drop(&mut self) {\n# println!(\"釋放 CustomSmartPointer 的資料 `{}`!\", self.data);\n# }\n# }\n# fn main() { let c = CustomSmartPointer { data: String::from(\"某些資料\"), }; println!(\"CustomSmartPointer 建立完畢。\"); c.drop(); println!(\"CustomSmartPointer 在 main 結束前就被釋放了。\");\n} 範例 15-15:嘗試呼叫 Drop 特徵的 drop 方法來手動提早清除 當我們嘗試編譯此程式碼,我們會獲得以下錯誤: $ cargo run Compiling drop-example v0.1.0 (file:///projects/drop-example)\nerror[E0040]: explicit use of destructor method --> src/main.rs:16:7 |\n16 | c.drop(); | --^^^^-- | | | | | explicit destructor calls not allowed | help: consider using `drop` function: `drop(c)` For more information about this error, try `rustc --explain E0040`.\nerror: could not compile `drop-example` due to previous error 此錯誤訊息表示我們不允許顯式呼叫 drop。錯誤訊息使用了一個術語 解構子(destructor) ,這是通用程式設計術語中表達會清除實例的函式。 解構子 對應的術語就是 建構子(constructor) ,這會建立實例。Rust 中的 drop 函式就是一種特定的解構子。 Rust 不讓我們顯式呼叫 drop,因為 Rust 還是會在 main 結束時自動呼叫 drop。這樣可能會導致 重複釋放 (double free)的錯誤,因為 Rust 可能會嘗試清除相同的數值兩次。 當數值離開作用域時我們無法停用自動插入的 drop,而且我們無法顯式呼叫 drop 方法,所以如果我必須強制讓一個數值提早清除的話,我們可以用 std::mem::drop 函式。 std::mem::drop 函式不同於 Drop 中的 drop 方法,我們傳入想要強制提早釋放的數值作為引數。此函式也包含在 prelude,所以我們可以修改範例 15-15 的 main 來呼叫 drop 函式,如範例 15-16 所示: 檔案名稱:src/main.rs # struct CustomSmartPointer {\n# data: String,\n# }\n# # impl Drop for CustomSmartPointer {\n# fn drop(&mut self) {\n# println!(\"釋放 CustomSmartPointer 的資料 `{}`!\", self.data);\n# }\n# }\n# fn main() { let c = CustomSmartPointer { data: String::from(\"某些資料\"), }; println!(\"CustomSmartPointer 建立完畢。\"); drop(c); println!(\"CustomSmartPointer 在 main 結束前就被釋放了。\");\n} 範例 15-16:在數值離開作用域前呼叫 std::mem::drop 來顯示釋放數值 執行此程式會印出以下結果: $ cargo run Compiling drop-example v0.1.0 (file:///projects/drop-example) Finished dev [unoptimized + debuginfo] target(s) in 0.73s Running `target/debug/drop-example`\nCustomSmartPointer 建立完畢。\n釋放 CustomSmartPointer 的資料 `某些資料`!\nCustomSmartPointer 在 main 結束前就被釋放了。 釋放 CustomSmartPointer 的資料 `某些資料`! 這段文字會在 CustomSmartPointer 建立完畢。 與 CustomSmartPointer 在 main 結束前就被釋放了。 文字之間印出,顯示 drop 方法會在那時釋放 c。 你可以在許多地方使用 Drop 特徵實作所指定的程式碼,讓清除實例變得方便又安全。舉例來說,你可以用它來建立你自己的記憶體配置器!透過 Drop 特徵與 Rust 的所有權系統,你不必去擔心要記得清理,因為 Rust 會自動處理。 你也不必擔心會意外清理仍在使用的數值:所有權系統會確保所有參考永遠有效,並確保當數值不再需要使用時只會呼叫 drop 一次。 現在你看過 Box 以及一些智慧指標的特性了,讓我們來看看一些其他定義在標準函式庫的智慧指標吧。","breadcrumbs":"智慧指標 » 透過 Drop 特徵執行清除程式碼 » 透過 std::mem::drop 提早釋放數值","id":"278","title":"透過 std::mem::drop 提早釋放數值"},"279":{"body":"在大多數的場合,所有權是很明確的:你能確切知道哪些變數擁有哪些數值。然而還是有些情況會需要讓一個數值能有數個擁有者。舉例來說,在圖資料結構中數個邊可能就會指向同個節點,而該節點概念上就被所有指向它的邊所擁有。節點直到沒有任何邊指向它,也就是沒有任何擁有者時才會被清除。 你必須使用 Rust 的型別 Rc 才能擁有多重所有權,這是 參考計數 (reference counting)的簡寫。Rc 型別會追蹤參考其數值的數量來決定該數值是否還在使用中。如果數值沒有任何參考的話,該數值就可以被清除,因為不會產生任何無效參考。 想像 Rc 是個在客廳裡的電視,當有人進入客廳要看電視時,它們就會打開它。其他人也能進來觀看電視。當最後一個人離開客廳時,它們會關掉電視,因為沒有任何人會再看了。如果當其他人還在看電視時,有人關掉了它,其他在看電視的人肯定會生氣。 Rc 型別的使用時機在於當我們想要在堆積上配置一些資料給程式中數個部分讀取,但是我們無法在編譯時期決定哪個部分會最後一個結束使用數值的部分。如果我們知道哪個部分會最後結束的話,我們可以將那個部分作為資料的擁有者就好,然後正常的所有權規則就會在編譯時生效。 注意到 Rc 只適用於單一執行緒(single-threaded)的場合。當我們在第十六章討論並行(concurrency)時,我們會介紹如何在多執行緒程式達成參考計數。","breadcrumbs":"智慧指標 » Rc 參考計數智慧指標 » Rc 參考計數智慧指標","id":"279","title":"Rc 參考計數智慧指標"},"28":{"body":"當你的專案正式準備好要發佈的話,你可以使用 cargo build --release 來最佳化編譯結果。此命令會產生執行檔到 target/release 而不是 target/debug 。最佳化可以讓你的 Rust 程式碼跑得更快,不過也會讓編譯的時間變得更久。這也是為何 Cargo 提供兩種不同的設定檔(profile):一個用來作為開發使用,讓你可以快速並經常重新建構;另一個用來最終產生你要給使用者運行的程式用,它通常不會需要重新建構且能盡所能地跑得越快越好。如果你要做基準化分析(benchmarking)來檢測程式運行時間的話,請確認執行的是 cargo build --release 並使用 target/release 底下的執行檔做檢測。","breadcrumbs":"開始入門 » Hello, Cargo! » 建構發佈版本(Release)","id":"28","title":"建構發佈版本(Release)"},"280":{"body":"讓我們回顧範例 15-5 的 cons list 範例。回想一下我們當時適用 Box 定義。這次我們會建立兩個列表,它們會同時共享第三個列表的所有權。概念上會如圖示 15-3 所示: 圖示 15-3:兩個列表 b 和 c 共享第三個列表 a 的所有權 我們會建立列表 a 來包含 5 然後是 10。然後我們會在建立兩個列表:b 以 3 為開頭而 c 以 4 為開頭。b 與 c 列表會同時連接包含 5 與 10 的第一個列表 a。換句話說,兩個列表會同時共享包含 5 與 10 的第一個列表。 嘗試使用 Box 來定義這種情境的 List 的話會無法成功,如範例 15-17 所示: 檔案名稱:src/main.rs enum List { Cons(i32, Box), Nil,\n} use crate::List::{Cons, Nil}; fn main() { let a = Cons(5, Box::new(Cons(10, Box::new(Nil)))); let b = Cons(3, Box::new(a)); let c = Cons(4, Box::new(a));\n} 範例 15-17:展示我們無法用 Box 讓兩個列表嘗試共享第三個列表的所有權 當我們編譯此程式碼,我們會得到以下錯誤: $ cargo run Compiling cons-list v0.1.0 (file:///projects/cons-list)\nerror[E0382]: use of moved value: `a` --> src/main.rs:11:30 |\n9 | let a = Cons(5, Box::new(Cons(10, Box::new(Nil)))); | - move occurs because `a` has type `List`, which does not implement the `Copy` trait\n10 | let b = Cons(3, Box::new(a)); | - value moved here\n11 | let c = Cons(4, Box::new(a)); | ^ value used here after move For more information about this error, try `rustc --explain E0382`.\nerror: could not compile `cons-list` due to previous error Cons 變體擁有它們存有的資料,所以當我們建立列表 b 時,a 會移動到 b,所以 b 就擁有 a。然後當我們嘗試再次使用 a 來建立 c 時,這就不會被允許,因為 a 已經被移走了。 我們可以嘗試改用參考來變更 Cons 的定義,但是這樣我們就必須指定生命週期參數。透過指定生命週期參數,我們會指定列表中的每個元素會至少活得跟整個列表一樣久。範例 15-17 的元素和列表雖然可以這樣,但不是所有的場合都是如此。 我們最後可以改用 Rc 來變更 List 的定義,如範例 15-18 所示。每個 Cons 變體都會存有一個數值以及一個由 Rc 指向的 List。當我們建立 b 時,不會取走 a 的所有權,我們會克隆(clone) a 存有的 Rc,因而增加參考的數量從一增加到二,並讓 a 與 b 共享 Rc 資料的所有權。我們也在建立 c 時克隆 a,增加參考的數量從二增加到三。每次我們呼叫 Rc::clone 時,對 Rc 資料的參考計數就會成增加,然後資料不會被清除直到沒有任何參考為止。 檔案名稱:src/main.rs enum List { Cons(i32, Rc), Nil,\n} use crate::List::{Cons, Nil};\nuse std::rc::Rc; fn main() { let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); let b = Cons(3, Rc::clone(&a)); let c = Cons(4, Rc::clone(&a));\n} 範例 15-18:使用 Rc 來定義 List 我們需要使用 use 陳述式來將 Rc 引入作用域,因為它沒有被包含在 prelude 中。在 main 中,我們建立了一個包含 5 與 10 的列表並存入 a 的 Rc。然後當我們建立 b 與 c 時,我們會呼叫函式 Rc::clone 來將 a 的 Rc 參考作為引數傳入。 當然我們可以呼叫 a.clone() 而非 Rc::clone(&a),但是在此情形中 Rust 的慣例是使用 Rc::clone。Rc::clone 的實作不會像大多數型別的 clone 實作會深拷貝(deep copy)所有的資料。 Rc::clone 的呼叫只會增加參考計數,這花費的時間就相對很少。深拷貝通常會花費比較多的時間。透過使用 Rc::clone 來參考計數,我們可以以視覺辨別出這是深拷貝的克隆還是增加參考計數的克隆。當我們需要調查程式碼的效能問題時,我們就只需要考慮深拷貝的克隆,而不必在意 Rc::clone。","breadcrumbs":"智慧指標 » Rc 參考計數智慧指標 » 使用 Rc 來分享資料","id":"280","title":"使用 Rc 來分享資料"},"281":{"body":"讓我們改變範例 15-18 的範例,好讓我們能觀察參考計數在我們建立與釋放 a 的 Rc 參考時產生的變化。 範例 15-19,我們改變了 main 讓列表 c 寫在一個內部作用域中,然後我們就能觀察到當 c 離開作用域時參考計數產生的改變。 檔案名稱:src/main.rs # enum List {\n# Cons(i32, Rc),\n# Nil,\n# }\n# # use crate::List::{Cons, Nil};\n# use std::rc::Rc;\n# fn main() { let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); println!(\"建立 a 後的計數 = {}\", Rc::strong_count(&a)); let b = Cons(3, Rc::clone(&a)); println!(\"建立 b 後的計數 = {}\", Rc::strong_count(&a)); { let c = Cons(4, Rc::clone(&a)); println!(\"建立 c 後的計數 = {}\", Rc::strong_count(&a)); } println!(\"c 離開作用域後的計數 = {}\", Rc::strong_count(&a));\n} 範例 15-19:印出參考計數 在程式中每次參考計數產生改變的地方,我們就印出參考計數,我們可以透過呼叫函式 Rc::strong_count 來取得。此函式叫做 strong_count 而非 count 是因為 Rc 型別還有個 weak_count,我們會在 「避免參考循環:將 Rc 轉換成 Weak」 段落看到 weak_count 的使用方式。 此程式碼印出以下結果: $ cargo run Compiling cons-list v0.1.0 (file:///projects/cons-list) Finished dev [unoptimized + debuginfo] target(s) in 0.45s Running `target/debug/cons-list`\n建立 a 後的計數 = 1\n建立 b 後的計數 = 2\n建立 c 後的計數 = 3\nc 離開作用域後的計數 = 2 我們可以看到 a 的 Rc 會有個初始參考計數 1,然後我們每次呼叫 clone 時,計數會加 1。當 c 離開作用域時,計數會減 1。我們不必呼叫任何函式來減少參考計數,像呼叫 Rc::clone 時才會增加參考計數那樣。當 Rc 數值離開作用域時,Drop 特徵的實作就會自動減少參考計數。 我們無法從此例觀察到的是當 b 然後是 a 從 main 的結尾離開作用域時,計數會是 0,然後 Rc 在此時就會完全被清除。使用 Rc 能允許單一數值能有數個擁有者,然後計數會確保只要有任何擁有者還存在的狀況下,數值會保持是有效的。 透過不可變參考,Rc 能讓你分享資料給程式中數個部分來只做讀取的動作。如果 Rc 允許你也擁有數個可變參考的話,你可能就違反了第四章提及的借用規則:數個對相同位置的可變借用會導致資料競爭(data races)與不一致。但可變資料還是非常實用的!在下個段落,我們會討論內部可變性模式與 RefCell 型別,此型別能讓你搭配 Rc 使用來處理不可變的限制。","breadcrumbs":"智慧指標 » Rc 參考計數智慧指標 » 克隆 Rc 實例會增加其參考計數","id":"281","title":"克隆 Rc 實例會增加其參考計數"},"282":{"body":"內部可變性 (Interior mutability)是 Rust 中的一種設計模式,能讓你能對即使是不可變參考的資料也能改變。正常狀況下,借用規則是不允許這種動作的。為了改變資料,這樣的模式會在資料結構內使用 unsafe 程式碼來繞過 Rust 的常見可變性與借用規則。不安全(unsafe)的程式碼等於告訴編譯器我們會自己手動檢查,編譯器不會檢查全部的規則,我們會在第十九章討論更多關於不安全的程式碼。 當編譯器無法保障,但我們可以確保借用規則在執行時能夠遵循的話,我們就可以使用擁有內部可變性模式的型別。其內的 unsafe 程式碼會透過安全的 API 封裝起來,讓外部型別仍然是不可變的。 讓我們觀察擁有內部可變性模式的 RefCell 型別來探討此概念。","breadcrumbs":"智慧指標 » RefCell 與內部可變性模式 » RefCell 與內部可變性模式","id":"282","title":"RefCell 與內部可變性模式"},"283":{"body":"不像 Rc,RefCell 型別的資料只會有一個所有權。所以 RefCell 與 Box 這種型別有何差別呢?回憶一下你在第四章學到的借用規則: 在任何時候,我們要麼 只能有 一個可變參考,要麼可以有 任意數量 的不可變參考。 參考必須永遠有效。 對於參考與 Box,借用規則會在編譯期強制檢測。對於 RefCell,這些規則會在 執行時 才強制執行。對於參考來說,如果你打破這些規則,你會得到編譯錯誤。而對 RefCell 來說,如果你打破這些規則,你的程式會恐慌並離開。 在編譯時期檢查借用規則的優勢在於錯誤能在開發過程及早獲取,而且這對執行時的效能沒有任何影響,因為所有的分析都預先完成了。基於這些原因,在編譯時檢查借用規則在大多數情形都是最佳選擇,這也是為何這是 Rust 預設設置的原因。 在執行時檢查借用規則的優勢則在於能允許一些特定記憶體安全的場合,而這些原本是不被編譯時檢查所允許的。像 Rust 編譯器這種靜態分析本質上是保守的。有些程式碼特性是無法透過分析程式碼檢測出的,最著名的範例就是停機問題(Halting Problem),這超出本書的範疇,但是是個有趣的研究議題。 因為有些分析是不可能的,如果 Rust 編譯器無法確定程式碼是否符合所有權規則,它可能會拒絕一支正確的程式,所以由此觀點來看能知道 Rust 編譯器是保守的。如果 Rust 接受不正確的程式,使用者就無法信任 Rust 帶來的保障。然而如果 Rust 拒絕正確的程式,對程式設計師就會很不方便,但沒有任何嚴重的災難會發生。RefCell 型別就適用於當你確定你的程式碼有遵循借用規則,但是編譯器無法理解並保證的時候。 類似於 Rc,RefCell 也只能用於單一執行緒(single-threaded)的場合,所以如果你嘗試用在多執行緒上的話就會出現編譯時錯誤。我們會在第十六章討論如何在多執行緒程式擁有 RefCell 的功能。 以下是何時選擇 Box、Rc 或 RefCell 的理由: Rc 讓數個擁有者能共享相同資料;Box 與 RefCell 只能有一個擁有者。 Box 能有不可變或可變的借用並在編譯時檢查;Rc 則只能有不可變借用並在編譯時檢查:RefCell 能有不可變或可變借用但是在執行時檢查。 由於 RefCell 允許在執行時檢查可變參考,你可以改變 RefCell 內部的數值,就算 RefCell 是不可變的。 改變不可變數值內部的值稱為 內部可變性 模式。讓我們看看內部可變性何時會有用,且觀察為何是可行的。","breadcrumbs":"智慧指標 » RefCell 與內部可變性模式 » 透過 RefCell 在執行時強制檢測借用規則","id":"283","title":"透過 RefCell 在執行時強制檢測借用規則"},"284":{"body":"借用規則的影響是當你有個不可變數值,你就無法取得可變參考。舉例來說,以下程式碼會無法編譯: fn main() { let x = 5; let y = &mut x;\n} 如果你嘗試編譯此程式碼,你會獲得以下錯誤: $ cargo run Compiling borrowing v0.1.0 (file:///projects/borrowing)\nerror[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable --> src/main.rs:3:13 |\n2 | let x = 5; | - help: consider changing this to be mutable: `mut x`\n3 | let y = &mut x; | ^^^^^^ cannot borrow as mutable For more information about this error, try `rustc --explain E0596`.\nerror: could not compile `borrowing` due to previous error 然而在某些特定情況,我們會想要能夠有個方法可以改變一個數值,但該數值對其他程式碼而言仍然是不可變的。數值提供的方法以外的程式碼都無法改變其值。使用 RefCell 是取得內部可變性的方式之一。但 RefCell 仍然要完全遵守借用規則:編譯器的借用檢查器會允許這些內部可變性,然後在執行時才檢查借用規則。如果你違反規則,你就會得到 panic! 而非編譯錯誤。 讓我們用一個實際例子來探討如何使用 RefCell 來改變不可變數值,並瞭解為何這是很實用的。 內部可變性的使用案例:模擬物件 程式設計師有時在進行測試時會將一個型別替換成其他型別,用以觀察特定行為並判定是否有正確實作。這種型別就稱為 測試替身 (test double)。你可以想成這和影視產業中的「特技替身演員」類似,有個人會代替原本的演員來拍攝一些特定的場景。測試替身會在執行測試時代替其他型別。 模擬物件 (Mock objects)是測試替身其中一種特定型別,這能紀錄測試過程中發生什麼事並讓你能判斷動作是否正確。 Rust 的物件與其他語言中的物件概念並不全然相同,而且 Rust 的標準函式庫內也沒有如其他語言會內建的模擬物件功能。不過你還是可以有方法來建立結構體來作為模擬物件。 以下是我們要測試的情境:我們建立一個函式庫來追蹤一個數值與最大值的差距,並依據該差距傳送訊息。舉例來說,此函式庫就能用來追蹤使用者允許呼叫 API 次數的上限。 我們的函式庫提供的功能只有追蹤與最大值的距離以及何時該傳送什麼訊息。使用函式庫的應用程式要提供傳送訊息的機制,應用程式可以將訊息存在應用程式內、傳送電子郵件、傳送文字訊息或其他等等。函式庫不需要知道細節,它只需要在意會有項目實作我們提供的 Messenger 特徵。範例 15-20 顯示了函式庫的程式碼: 檔案名稱:src/lib.rs pub trait Messenger { fn send(&self, msg: &str);\n} pub struct LimitTracker<'a, T: Messenger> { messenger: &'a T, value: usize, max: usize,\n} impl<'a, T> LimitTracker<'a, T>\nwhere T: Messenger,\n{ pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> { LimitTracker { messenger, value: 0, max, } } pub fn set_value(&mut self, value: usize) { self.value = value; let percentage_of_max = self.value as f64 / self.max as f64; if percentage_of_max >= 1.0 { self.messenger.send(\"錯誤:你超過使用上限了!\"); } else if percentage_of_max >= 0.9 { self.messenger .send(\"緊急警告:你已經使用 90% 的配額了!\"); } else if percentage_of_max >= 0.75 { self.messenger .send(\"警告:你已經使用 75% 的配額了!\"); } }\n} 範例 15-20:追蹤某個值與最大值差距的函式庫並以此值的特定層級傳送警告 此程式碼其中一個重點是 Messenger 特徵有個方法叫做 send,這會接收一個 self 的不可變參考與一串訊息文字。此特徵就是我們的模擬物件所需實作的介面,讓我們能模擬和實際物件一樣的行爲。另一個重點是我們想要測試 LimitTracker 中 set_value 方法的行為。我們可以改變傳給參數 value 的值,但是 set_value 沒有回傳任何東西好讓我們做判斷。我們希望如果我們透過某個實作 Messenger 的型別與特定數值 max 來建立 LimitTracker 時,傳送訊息者能被通知要傳遞合適的訊息。 我們需要有個模擬物件,而不是在呼叫 send 時真的傳送電子郵件或文字訊息,我們只想紀錄訊息被通知要傳送了。我們可以建立模擬物件的實例,以此建立 LimitTracker、呼叫 LimitTracker 的 set_value,並檢查模擬物件有我們預期的訊息。範例 15-21 展示一個嘗試實作此事的模擬物件,但借用檢查器卻不允許: 檔案名稱:src/lib.rs # pub trait Messenger {\n# fn send(&self, msg: &str);\n# }\n# # pub struct LimitTracker<'a, T: Messenger> {\n# messenger: &'a T,\n# value: usize,\n# max: usize,\n# }\n# # impl<'a, T> LimitTracker<'a, T>\n# where\n# T: Messenger,\n# {\n# pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {\n# LimitTracker {\n# messenger,\n# value: 0,\n# max,\n# }\n# }\n# # pub fn set_value(&mut self, value: usize) {\n# self.value = value;\n# # let percentage_of_max = self.value as f64 / self.max as f64;\n# # if percentage_of_max >= 1.0 {\n# self.messenger.send(\"錯誤:你超過使用上限了!\");\n# } else if percentage_of_max >= 0.9 {\n# self.messenger\n# .send(\"緊急警告:你已經使用 90% 的配額了!\");\n# } else if percentage_of_max >= 0.75 {\n# self.messenger\n# .send(\"警告:你已經使用 75% 的配額了!\");\n# }\n# }\n# }\n# #[cfg(test)]\nmod tests { use super::*; struct MockMessenger { sent_messages: Vec, } impl MockMessenger { fn new() -> MockMessenger { MockMessenger { sent_messages: vec![], } } } impl Messenger for MockMessenger { fn send(&self, message: &str) { self.sent_messages.push(String::from(message)); } } #[test] fn it_sends_an_over_75_percent_warning_message() { let mock_messenger = MockMessenger::new(); let mut limit_tracker = LimitTracker::new(&mock_messenger, 100); limit_tracker.set_value(80); assert_eq!(mock_messenger.sent_messages.len(), 1); }\n} 範例 15-21:嘗試實作 MockMessenger 但借用檢查器不允許 此測試程式碼定義了一個結構體 MockMessenger 其有個 sent_messages 欄位並存有 String 數值的 Vec 來追蹤被通知要傳送的訊息。我們也定義了一個關聯函式 new 讓我們可以方便建立起始訊息列表為空的 MockMessenger。我們對 MockMessenger 實作 Messenger 特徵,這樣我們才能將 MockMessenger 交給 LimitTracker。在 send 方法的定義中,我們取得由參數傳遞的訊息,並存入 MockMessenger 的 sent_messages 列表中。 在測試中,我們測試當 LimitTracker 被通知將 value 設為超過 max 數值 75% 的某個值。首先,我們建立新的 MockMessenger,其起始為一個空的訊息列表。然後我們建立一個新的 LimitTracker 並將 MockMessenger 的參考與一個 max 為 100 的數值賦值給它。我們用數值 80 來呼叫 LimitTracker 的 set_value 方法,此值會超過 100 的 75%。然後我們判定 MockMessenger 追蹤的訊息列表需要至少有一個訊息。 但是此測試有個問題,如以下所示: $ cargo test Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)\nerror[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference --> src/lib.rs:58:13 |\n2 | fn send(&self, msg: &str); | ----- help: consider changing that to be a mutable reference: `&mut self`\n...\n58 | self.sent_messages.push(String::from(message)); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable For more information about this error, try `rustc --explain E0596`.\nerror: could not compile `limit-tracker` due to previous error\nwarning: build failed, waiting for other jobs to finish... 我們無法修改 MockMessenger 來追蹤訊息,因為 send 方法取得的是 self 的不可變參考。而我們也無法使用錯誤訊息中推薦使用的 &mut self,因為 send 的簽名就會與 Messenger 特徵所定義的不相符(你可以試看看並觀察錯誤訊息)。 這就是內部可變性能帶來幫助的場合!我們會將 sent_messages 存入 RefCell 內,然後 send 方法就也能夠進行修改存入訊息。範例 15-22 顯示了變更後的程式碼: 檔案名稱:src/lib.rs # pub trait Messenger {\n# fn send(&self, msg: &str);\n# }\n# # pub struct LimitTracker<'a, T: Messenger> {\n# messenger: &'a T,\n# value: usize,\n# max: usize,\n# }\n# # impl<'a, T> LimitTracker<'a, T>\n# where\n# T: Messenger,\n# {\n# pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {\n# LimitTracker {\n# messenger,\n# value: 0,\n# max,\n# }\n# }\n# # pub fn set_value(&mut self, value: usize) {\n# self.value = value;\n# # let percentage_of_max = self.value as f64 / self.max as f64;\n# # if percentage_of_max >= 1.0 {\n# self.messenger.send(\"錯誤:你超過使用上限了!\");\n# } else if percentage_of_max >= 0.9 {\n# self.messenger\n# .send(\"緊急警告:你已經使用 90% 的配額了!\");\n# } else if percentage_of_max >= 0.75 {\n# self.messenger\n# .send(\"警告:你已經使用 75% 的配額了!\");\n# }\n# }\n# }\n# #[cfg(test)]\nmod tests { use super::*; use std::cell::RefCell; struct MockMessenger { sent_messages: RefCell>, } impl MockMessenger { fn new() -> MockMessenger { MockMessenger { sent_messages: RefCell::new(vec![]), } } } impl Messenger for MockMessenger { fn send(&self, message: &str) { self.sent_messages.borrow_mut().push(String::from(message)); } } #[test] fn it_sends_an_over_75_percent_warning_message() { // --省略--\n# let mock_messenger = MockMessenger::new();\n# let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);\n# # limit_tracker.set_value(80); assert_eq!(mock_messenger.sent_messages.borrow().len(), 1); }\n} 範例 15-22:在外部數值為不可變時,使用 RefCell 來改變內部數值 sent_messages 欄位現在是型別 RefCell> 而非 Vec。在 new 函式中,我們用空的向量來建立新的 RefCell>。 至於 send 方法的實作,第一個參數仍然是 self 的不可變借用,這就符合特徵所定義的。我們在 self.sent_messages 對 RefCell> 呼叫 borrow_mut 來取得 RefCell> 內的可變參考數值,也就是向量。然後我們對向量的可變參考呼叫 push 來追蹤測試中的訊息。 最後一項改變是判定:要看到內部向量有多少項目的話,我們對 RefCell> 呼叫 borrow 來取得向量的不可變參考。 現在你已經知道如何使用 RefCell,讓我們進一步探討它如何運作的吧! 透過 RefCell 在執行時追蹤借用 當建立不可變與可變參考時,我們分別使用 & 和 &mut 語法。而對於 RefCell 的話,我們使用 borrow 和 borrow_mut 方法,這是 RefCell 所提供的安全 API 之一。borrow 方法回傳一個智慧指標型別 Ref,而 borrow_mut 回傳智慧指標型別 RefMut。這兩個型別都有實作 Deref,所以我們可以像一般參考來對待它們。 RefCell 會追蹤當前有多少 Ref 和 RefMut 智慧指標存在。每次我們呼叫 borrow 時,RefCell 會增加不可變借用計數。當 Ref 離開作用域時,不可變借用計數就會減一。就和編譯時借用規則一樣,RefCell 讓我們同一時間要麼只能有一個可變參考,要麼可以有數個不可變參考。 如果我們嘗試違反這些規則,我們不會像參考那樣得到編譯器錯誤,RefCell 的實作會在執行時恐慌。範例 15-23 修改了範例 15-22 的 send 實作。我們故意嘗試在同個作用域下建立兩個可變參考,來說明 RefCell 會不允許我們在執行時這樣做。 檔案名稱:src/lib.rs # pub trait Messenger {\n# fn send(&self, msg: &str);\n# }\n# # pub struct LimitTracker<'a, T: Messenger> {\n# messenger: &'a T,\n# value: usize,\n# max: usize,\n# }\n# # impl<'a, T> LimitTracker<'a, T>\n# where\n# T: Messenger,\n# {\n# pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {\n# LimitTracker {\n# messenger,\n# value: 0,\n# max,\n# }\n# }\n# # pub fn set_value(&mut self, value: usize) {\n# self.value = value;\n# # let percentage_of_max = self.value as f64 / self.max as f64;\n# # if percentage_of_max >= 1.0 {\n# self.messenger.send(\"錯誤:你超過使用上限了!\");\n# } else if percentage_of_max >= 0.9 {\n# self.messenger\n# .send(\"緊急警告:你已經使用 90% 的配額了!\");\n# } else if percentage_of_max >= 0.75 {\n# self.messenger\n# .send(\"警告:你已經使用 75% 的配額了!\");\n# }\n# }\n# }\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# use std::cell::RefCell;\n# # struct MockMessenger {\n# sent_messages: RefCell>,\n# }\n# # impl MockMessenger {\n# fn new() -> MockMessenger {\n# MockMessenger {\n# sent_messages: RefCell::new(vec![]),\n# }\n# }\n# }\n# impl Messenger for MockMessenger { fn send(&self, message: &str) { let mut one_borrow = self.sent_messages.borrow_mut(); let mut two_borrow = self.sent_messages.borrow_mut(); one_borrow.push(String::from(message)); two_borrow.push(String::from(message)); } }\n# # #[test]\n# fn it_sends_an_over_75_percent_warning_message() {\n# let mock_messenger = MockMessenger::new();\n# let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);\n# # limit_tracker.set_value(80);\n# # assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);\n# }\n# } 範例 15-23:在同個作用域建立兩個可變參考並觀察到 RefCell 會恐慌 我們從 borrow_mut 回傳的 RefMut 智慧指標來建立變數 one_borrow。然後我們再以相同方式建立另一個變數 two_borrow。這在同個作用域下產生了兩個可變參考,而這是不允許的。我們執行函式庫的測試時,範例 15-23 可以編譯通過,但是執行測試會失敗: $ cargo test Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker) Finished test [unoptimized + debuginfo] target(s) in 0.91s Running unittests src/lib.rs (target/debug/deps/limit_tracker-e599811fa246dbde) running 1 test\ntest tests::it_sends_an_over_75_percent_warning_message ... FAILED failures: ---- tests::it_sends_an_over_75_percent_warning_message stdout ----\nthread 'main' panicked at 'already borrowed: BorrowMutError', src/lib.rs:60:53\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::it_sends_an_over_75_percent_warning_message test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass `--lib` 注意到程式碼恐慌時的訊息 already borrowed: BorrowMutError。這就是 RefCell 如何在執行時處理違反借用規則的情況。 像我們在這裡選擇在執行時獲取借用錯誤而不是在編譯時,代表你會在開發過程之後才找到程式碼錯誤,甚至有可能一直到程式碼部署到正式環境後才查覺。而且你的程式碼也會多了一些小小的執行時效能開銷,作為在執行時而非編譯時檢查的代價。不過使用 RefCell 讓你能在只允許有不可變數值的環境中寫出能夠變更內部追蹤訊息的模擬物件。這是想獲得 RefCell 帶來的功能時,要與一般參考之間作出的取捨。","breadcrumbs":"智慧指標 » RefCell 與內部可變性模式 » 內部可變性:不可變數值的可變借用","id":"284","title":"內部可變性:不可變數值的可變借用"},"285":{"body":"RefCell 的常見使用方法是搭配 Rc。回想一下 Rc 讓你可以對數個擁有者共享相同資料,但是它只能用於不可變資料。如果你有一個 Rc 並存有 RefCell 的話,你就可以取得一個有數個擁有者 而且 可變的數值! 舉例來說,回憶一下範例 15-18 cons list 的範例我們使用了 Rc 來讓數個列表可以共享另一個列表的所有權。因為 Rc 只能有不可變數值,我們一旦建立它們後就無法變更列表中的任何數值。讓我們加上 RefCell 來獲得能改變列表數值的能力吧。範例 15-24 顯示了在 Cons 定義中使用 RefCell,這樣一來我們就可以變更儲存在列表中的所有數值: 檔案名稱:src/main.rs #[derive(Debug)]\nenum List { Cons(Rc>, Rc), Nil,\n} use crate::List::{Cons, Nil};\nuse std::cell::RefCell;\nuse std::rc::Rc; fn main() { let value = Rc::new(RefCell::new(5)); let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil))); let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a)); let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a)); *value.borrow_mut() += 10; println!(\"a 之後 = {:?}\", a); println!(\"b 之後 = {:?}\", b); println!(\"c 之後 = {:?}\", c);\n} 範例 15-24:使用 Rc> 建立一個可變的 List 我們建立了一個 Rc> 實例數值並將其存入變數 value 好讓我們之後可以直接取得。然後我們在 a 用持有 value 的 Cons 變體來建立 List。我們需要克隆 value,這樣 a 和 value 才能都有內部數值 5 的所有權,而不是從 value 轉移所有權給 a,或是讓 a 借用 value。 我們用 Rc 封裝列表 a,所以當我們建立列表 b 和 c 時,它們都可以參考 a,就像範例 15-18 一樣。 在我們建立完列表 a、b 和 c 之後,我們想對 value 的數值加上 10。我們對 value 呼叫 borrow_mut,其中使用到了我們在第五章討論過的自動解參考功能(請查閱 「-> 運算子跑去哪了?」 的段落)來解參考 Rc 成內部的 RefCell 數值。borrow_mut 方法會回傳 RefMut 智慧指標,而我們使用解參考運算子並改變其內部數值。 當我們印出 a、b 和 c 時,我們可以看到它們的數值都改成了 15 而非 5: $ cargo run Compiling cons-list v0.1.0 (file:///projects/cons-list) Finished dev [unoptimized + debuginfo] target(s) in 0.63s Running `target/debug/cons-list`\na 之後 = Cons(RefCell { value: 15 }, Nil)\nb 之後 = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))\nc 之後 = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil)) 此技巧是不是很厲害!透過使用 RefCell,我們可以得到一個外部是不可變的 List 數值,但是我們可以使用 RefCell 提供的方法來取得其內部可變性,讓我們可以在我們想要時改變我們的資料。執行時的借用規則檢查能防止資料競爭,並在某些場合犧牲一點速度來換取資料結構的彈性。注意到 RefCell 無法用在多執行緒的程式碼!Mutex 才是執行緒安全版的 RefCell,我們會在第十六章再討論 Mutex。","breadcrumbs":"智慧指標 » RefCell 與內部可變性模式 » 組合 Rc 與 RefCell 來擁有多個可變資料的擁有者","id":"285","title":"組合 Rc 與 RefCell 來擁有多個可變資料的擁有者"},"286":{"body":"意外情況下,執行程式時可能會產生永遠不會被清除的記憶體(通稱為 記憶體泄漏/memory leak )。Rust 的記憶體安全性雖然可以保證令這種情況難以發生,但並非絕不可能。雖然 Rust 在編譯時可以保證做到禁止資料競爭( data races ),但它無法保證完全避免記憶體泄漏,這是因為對 Rust 來說,記憶體泄漏是屬於安全範疇內的( memory safe )。透過使用 Rc 和 RefCell ,我們能觀察到 Rust 允許使用者自行產生記憶體泄漏:因為使用者可以產生兩個參考並互相參照,造成一個循環。這種情況下會導致記憶體泄漏,因為循環中的參考計數永遠不會變成 0,所以數值永遠不會被釋放。","breadcrumbs":"智慧指標 » 參考循環會導致記憶體泄漏 » 參考循環會導致記憶體泄漏","id":"286","title":"參考循環會導致記憶體泄漏"},"287":{"body":"讓我們看看參考循環是怎麼發生的,以及如何避免它。我們從範例 15-25 的 List 列舉定義與一個 tail 方法開始: 檔案名稱:src/main.rs use crate::List::{Cons, Nil};\nuse std::cell::RefCell;\nuse std::rc::Rc; #[derive(Debug)]\nenum List { Cons(i32, RefCell>), Nil,\n} impl List { fn tail(&self) -> Option<&RefCell>> { match self { Cons(_, item) => Some(item), Nil => None, } }\n} fn main() {} 範例 15-25:一個 cons list 定義並持有 RefCell,所以我們可以修改 Cons 變體參考的值 我們用的是範例 15-5 中 List 的另一種定義寫法。Cons 變體的第二個元素現在是 RefCell>,代表不同於範例 15-24 那樣能修改 i32 數值,我們想要能修改 Cons 變體指向的 List 數值。我們也加了一個 tail 方法讓我們如果有 Cons 變體的話,能方便取得第二個項目。 在範例 15-26 我們要加入 main 函式並使用範例 15-25 的定義。此程式碼建立了列表 a 與指向列表 a 的列表 b。然後它修改了列表 a 來指向 b,因而產生循環參考。在程序過程中 println! 陳述式會顯示不同位置時的參考計數。 檔案名稱:src/main.rs # use crate::List::{Cons, Nil};\n# use std::cell::RefCell;\n# use std::rc::Rc;\n# # #[derive(Debug)]\n# enum List {\n# Cons(i32, RefCell>),\n# Nil,\n# }\n# # impl List {\n# fn tail(&self) -> Option<&RefCell>> {\n# match self {\n# Cons(_, item) => Some(item),\n# Nil => None,\n# }\n# }\n# }\n# fn main() { let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil)))); println!(\"a 初始參考計數 = {}\", Rc::strong_count(&a)); println!(\"a 下個項目 = {:?}\", a.tail()); let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a)))); println!(\"a 在 b 建立後的參考計數 = {}\", Rc::strong_count(&a)); println!(\"b 初始參考計數 = {}\", Rc::strong_count(&b)); println!(\"b 下個項目 = {:?}\", b.tail()); if let Some(link) = a.tail() { *link.borrow_mut() = Rc::clone(&b); } println!(\"b 在變更 a 後的參考計數 = {}\", Rc::strong_count(&b)); println!(\"a 在變更 a 後的參考計數 = {}\", Rc::strong_count(&a)); // 取消下一行的註解可以看到循環產生 // 這會讓堆疊溢位 // println!(\"a 下個項目 = {:?}\", a.tail());\n} 範例 15-26:透過兩個彼此指向對方的 List 數值來產生參考循環 我們在變數 a 建立了一個 Rc 實例的 List 數值並持有 5, Nil 初始列表的。我們然後在變數 b 建立另一個 Rc 實例的 List 數值並持有數值 10 與指向的列表 a。 我們將 a 修改為指向 b 而非 Nil 來產生循環。我們透過使用 tail 方法來取得 a 的 RefCell> 參考,並放入變數 link 中。然後我們對 RefCell> 使用 borrow_mut 方法來改變 Rc 的值,從數值 Nil 改成 b 的 Rc。 當我們執行此程式並維持將最後一行的 println! 註解掉的話,我們會得到以下輸出: $ cargo run Compiling cons-list v0.1.0 (file:///projects/cons-list) Finished dev [unoptimized + debuginfo] target(s) in 0.53s Running `target/debug/cons-list`\na 初始參考計數 = 1\na 下個項目 = Some(RefCell { value: Nil })\na 在 b 建立後的參考計數 = 2\nb 初始參考計數 = 1\nb 下個項目 = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })\nb 在變更 a 後的參考計數 = 2\na 在變更 a 後的參考計數 = 2 在我們變更列表 a 來指向 b 後,a 和 b 的 Rc 實例參考計數都是 2。在 main 結束後,Rust 會釋放 b,讓 b 的 Rc 實例計數從 2 減到 1。此時堆積上 Rc 的記憶體還不會被釋放,因爲參考計數還有 1,而非 0。然後 Rust 釋放 a,讓 a 的 Rc 實例也從 2 減到 1。此實例的記憶體也不會被釋放,因爲另一個 Rc 的實例仍然參考著它。列表配置的記憶體會永遠不被釋放。為了視覺化參考循環,我們用圖示 15-4 表示。 圖示 15-4:列表 a 與 b 彼此指向對方的參考循環 如果你解除最後一個 println! 的註解並執行程式的話,Rust 會嘗試印出此循環,因為 a 會指向 b 會指向 a 以此循環下去,直到堆疊溢位(stack overflow)。 比起真實世界的程式,此循環造成的影響並不嚴重。因為當我們建立完循環參考,程式就結束了。不過要是有個更複雜的程式配置了大量的記憶體而產生循環,並維持很長一段時間的話,程式會用到比原本預期還多的記憶體,並可能壓垮系統,導致它將記憶體用光。 要產生循環參考並不是件容易的事,但也不是絕對不可能。如果你有包含 Rc 數值的 RefCell 數值,或是有類似具內部可變性與參考計數巢狀組合的話,你必須確保不會產生循環參考,你無法依靠 Rust 來檢查它們。產生循環參考是程式中的邏輯錯誤,你需要使用自動化測試、程式碼審查以及其他軟體開發技巧來最小化問題。 另一個避免參考循環的解決辦法是重新組織你的資料結構,確定哪些參考要有所有權,哪些參考不用。這樣一來,循環會由一些有所有權的關係與沒有所有權的關係所組成,而只有所有權關係能影響數值是否能被釋放。在範例 15-25 中。我們永遠會希望 Cons 變體擁有它們的列表,所以重新組織資料結構是不可能的。讓我們看看一個由父節點與子節點組成的圖形結構,來看看無所有權的關係何時適合用來避免循環參考。","breadcrumbs":"智慧指標 » 參考循環會導致記憶體泄漏 » 產生參考循環","id":"287","title":"產生參考循環"},"288":{"body":"目前,我們解釋過呼叫 Rc::clone 會增加 Rc 實例的 strong_count,而 Rc 只會在 strong_count 為 0 時被清除。你也可以對 Rc 實例呼叫 Rc::downgrade 並傳入 Rc 的參考來建立 弱參考(weak reference) 。強參考是你分享 Rc 實例的方式。弱參考不會表達所有權關係,它們的計數與 Rc 的清除無關。它們不會造成參考循環,因為弱參考的循環會在其強參考計數歸零時解除。 當你呼叫 Rc::downgrade 時,你會得到一個型別為 Weak 的智慧指標。不同於對 Rc 實例的 strong_count 增加 1,呼叫 Rc::downgrade 會對 weak_count 增加 1。Rc 型別使用 weak_count 來追蹤有多少 Weak 的參考存在,這類似於 strong_count。不同的地方在於 weak_count 不需要歸零才能將 Rc 清除。 由於 Weak 的參考數值可能會被釋放,要對 Weak 指向的數值做任何事情時,你都必須確保該數值還存在。你可以透過對 Weak 實例呼叫 upgrade 方法,這會回傳 Option>。如果 Rc 數值還沒被釋放的話,你就會得到 Some;而如果 Rc 數值已經被釋放的話,就會得到 None。因為 upgrade 回傳 Option>,Rust 會確保 Some 與 None 的分支都有處理好,所以不會取得無效指標。 為了做示範,與其使用知道下一項的列表的例子,我們會建立一個樹狀結構,每一個項目會知道它們的子項目 以及 它們的父項目。 建立樹狀資料結構:帶有子節點的 Node 首先我們建立一個帶有節點的樹,每個節點知道它們的子節點。我們會定義一個結構體 Node 來存有它自己的 i32 數值以及其子數值 Node 的參考: 檔案名稱:src/main.rs use std::cell::RefCell;\nuse std::rc::Rc; #[derive(Debug)]\nstruct Node { value: i32, children: RefCell>>,\n}\n# # fn main() {\n# let leaf = Rc::new(Node {\n# value: 3,\n# children: RefCell::new(vec![]),\n# });\n# # let branch = Rc::new(Node {\n# value: 5,\n# children: RefCell::new(vec![Rc::clone(&leaf)]),\n# });\n# } 我們想要 Node 擁有自己的子節點,而且我們想要透過變數分享所有權,讓我們可以在樹中取得每個 Node。為此我們定義 Vec 項目作為型別 Rc 的數值。我們還想要能夠修改哪些節點才是該項目的子節點,所以我們將 children 中的 Vec> 加進 RefCell。 接著,我們使用我們定義的結構體來建立一個 Node 實例叫做 leaf,其數值為 3 且沒有子節點;我們再建立另一個實例叫做 branch,其數值為 5 且有個子節點 leaf。如範例 15-27 所示: 檔案名稱:src/main.rs # use std::cell::RefCell;\n# use std::rc::Rc;\n# # #[derive(Debug)]\n# struct Node {\n# value: i32,\n# children: RefCell>>,\n# }\n# fn main() { let leaf = Rc::new(Node { value: 3, children: RefCell::new(vec![]), }); let branch = Rc::new(Node { value: 5, children: RefCell::new(vec![Rc::clone(&leaf)]), });\n} 範例 15-27:建立一個沒有子節點的 leaf 節點與一個有 leaf 作為子節點的 branch 節點 我們克隆 leaf 的 Rc 並存入 branch,代表 leaf 的 Node 現在有兩個擁有者:leaf 和 branch。我們可以透過 branch.children 從 branch 取得 leaf,但是從 leaf 無法取得 branch。原因是因為 leaf 沒有 branch 的參考且不知道它們之間是有關聯的。我們想要 leaf 能知道 branch 是它的父節點。這就是我們接下來要做的事。 新增從子節點到父節點的參考 要讓子節點意識到它的父節點,我們需要在我們的 Node 結構體定義中加個 parent 欄位。問題在於 parent 應該要是什麼型別。我們知道它不能包含 Rc,因為那就會造成參考循環,leaf.parent 就會指向 branch 且 branch.children 就會指向 leaf,導致同名的 strong_count 數值無法歸零。 讓我們換種方式思考此關係,父節點必須擁有它的子節點,如果父節點釋放的話,它的子節點也應該要被釋放。但子節點不應該擁有它的父節點,如果我們釋放子節點的話,父節點應該要還存在。這就是弱參考的使用時機! 所以與其使用 Rc,我們使用 Weak 來建立 parent 的型別,更明確的話就是 RefCell>。現在我們的 Node 結構體定義看起來會像這樣: 檔案名稱:src/main.rs use std::cell::RefCell;\nuse std::rc::{Rc, Weak}; #[derive(Debug)]\nstruct Node { value: i32, parent: RefCell>, children: RefCell>>,\n}\n# # fn main() {\n# let leaf = Rc::new(Node {\n# value: 3,\n# parent: RefCell::new(Weak::new()),\n# children: RefCell::new(vec![]),\n# });\n# # println!(\"leaf 的父節點 {:?}\", leaf.parent.borrow().upgrade());\n# # let branch = Rc::new(Node {\n# value: 5,\n# parent: RefCell::new(Weak::new()),\n# children: RefCell::new(vec![Rc::clone(&leaf)]),\n# });\n# # *leaf.parent.borrow_mut() = Rc::downgrade(&branch);\n# # println!(\"leaf 的父節點 {:?}\", leaf.parent.borrow().upgrade());\n# } 節點能夠參考其父節點但不會擁有它。在範例 15-28 中我們更新了 main 來使用新的定義,讓 leaf 節點有辦法參考它的父節點 branch: 檔案名稱:src/main.rs # use std::cell::RefCell;\n# use std::rc::{Rc, Weak};\n# # #[derive(Debug)]\n# struct Node {\n# value: i32,\n# parent: RefCell>,\n# children: RefCell>>,\n# }\n# fn main() { let leaf = Rc::new(Node { value: 3, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![]), }); println!(\"leaf 的父節點 {:?}\", leaf.parent.borrow().upgrade()); let branch = Rc::new(Node { value: 5, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![Rc::clone(&leaf)]), }); *leaf.parent.borrow_mut() = Rc::downgrade(&branch); println!(\"leaf 的父節點 {:?}\", leaf.parent.borrow().upgrade());\n} 範例 15-28:leaf 節點有其父節點 branch 的弱參考 建立 leaf 節點與範例 15-27 類似,只是要多加個 parent 欄位:leaf 一開始沒有任何父節點,所以我們建立一個空的 Weak 參考實例。 此時當我們透過 upgrade 方法嘗試取得 leaf 的父節點參考的話,我們會取得 None 數值。我們能在輸出結果的第一個 println! 陳述式看到: leaf 的父節點 None 當我們建立 branch 節點,它的 parent 欄位也會有個新的 Weak 參考,因為 branch 沒有父節點。我們仍然有 leaf 作為 branch 其中一個子節點。一旦我們有了 branch 的 Node 實例,我們可以修改 leaf 使其擁有父節點的 Weak 參考。我們對 leaf 中 parent 欄位的 RefCell> 使用 borrow_mut 方法,然後我們使用 Rc::downgrade 函式來從 branch 的 Rc 建立一個 branch 的 Weak 參考。 當我們再次印出 leaf 的父節點,這次我們就會取得 Some 變體其內就是 branch,現在 leaf 可以取得它的父節點了!當我們印出 leaf,我們也能避免產生像範例 15-26 那樣最終導致堆疊溢位(stack overflow)的循環,Weak 會印成 (Weak): leaf 的父節點 Some(Node { value: 5, parent: RefCell { value: (Weak) },\nchildren: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) },\nchildren: RefCell { value: [] } }] } }) 沒有無限的輸出代表此程式碼沒有產生參考循環。我們也能透過呼叫 Rc::strong_count 與 Rc::weak_count 的數值看出。 視覺化 strong_count 與 weak_count 的變化 讓我們看看 Rc 實例中 strong_count 與 weak_count 的數值如何變化,我們建立一個新的內部作用域,並將 branch 的產生移入作用域中。這樣我們就能看到 branch 建立與離開作用域而釋放時發生了什麼事。如範例 15-29 所示: 檔案名稱:src/main.rs # use std::cell::RefCell;\n# use std::rc::{Rc, Weak};\n# # #[derive(Debug)]\n# struct Node {\n# value: i32,\n# parent: RefCell>,\n# children: RefCell>>,\n# }\n# fn main() { let leaf = Rc::new(Node { value: 3, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![]), }); println!( \"leaf 的強參考 = {}、弱參考 = {}\", Rc::strong_count(&leaf), Rc::weak_count(&leaf), ); { let branch = Rc::new(Node { value: 5, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![Rc::clone(&leaf)]), }); *leaf.parent.borrow_mut() = Rc::downgrade(&branch); println!( \"branch 的強參考 = {}、弱參考 = {}\", Rc::strong_count(&branch), Rc::weak_count(&branch), ); println!( \"leaf 的強參考 = {}、弱參考 = {}\", Rc::strong_count(&leaf), Rc::weak_count(&leaf), ); } println!(\"leaf 的父節點 {:?}\", leaf.parent.borrow().upgrade()); println!( \"leaf 的強參考 = {}、弱參考 = {}\", Rc::strong_count(&leaf), Rc::weak_count(&leaf), );\n} 範例 15-29:在內部作用域建立 branch 並觀察強與弱參考計數 在 leaf 建立後,它的 Rc 有強計數為 1 與弱計數為 0。在內部作用域中,我們建立了 branch 並與 leaf 做連結,此時當我們印出計數時,branch 的 Rc 會有強計數為 1 與弱計數為 1(因為 leaf.parent 透過 Weak 指向 branch)。當我們印出 leaf 的計數時,我們會看到它會有強計數為 2,因為 branch 現在有個 leaf 的 Rc 克隆儲存在 branch.children,但弱計數仍為 0。 當內部作用域結束時,branch 會離開作用域且 Rc 的強計數會歸零,所以它的 Node 就會被釋放。leaf.parent 的弱計數 1 與 Node 是否被釋放無關,所以我們沒有產生任何記憶體泄漏! 如果我們嘗試在作用域結束後取得 leaf 的父節點,我們會再次獲得 None。在程式的最後,leaf 的 Rc 強計數為 1 且弱計數為 0,因為變數 leaf 現在是 Rc 唯一的參考。 所有管理計數與數值釋放都已經實作在 Rc 與 Weak,它們都有 Drop 特徵的實作。在 Node 的定義中指定子節點對父節點的關係應為 Weak 參考,讓你能夠將父節點與子節點彼此關聯,且不必擔心產生參考循環與記憶體泄漏。","breadcrumbs":"智慧指標 » 參考循環會導致記憶體泄漏 » 避免參考循環:將 Rc 轉換成 Weak","id":"288","title":"避免參考循環:將 Rc 轉換成 Weak"},"289":{"body":"本章節涵蓋了如何使用智慧指標來得到一些不同於 Rust 預設參考所帶來的保障以及取捨。Box 型別有已知大小並能將資料配置到堆積上。Rc 型別會追蹤堆積上資料的參考數量,讓該資料能有數個擁有者。RefCell 型別具有內部可變性,提供一個外部不可變的型別,但有方法可以改變內部數值,它會在執行時強制檢測借用規則,而非編譯時。 我們也討論了 Deref 與 Drop 特徵,這些對智慧指標提供了許多功能。我們探討了參考循環可能會導致記憶體泄漏以及如何使用 Weak 避免它們。 如果本章節引起你的興趣,讓你想要實作你自己的智慧指標的話,歡迎查閱 「The Rustonomicon」 來學習更多實用資訊。 接下來,我們將討論 Rust 的並行性。你還會再學到一些新的智慧指標。","breadcrumbs":"智慧指標 » 參考循環會導致記憶體泄漏 » 總結","id":"289","title":"總結"},"29":{"body":"雖然在簡單的專案下,Cargo 比起只使用 rustc 的確沒辦法突顯出什麼價值。但是當你的程式變得越來越複雜時,它將證明它的用途。當程式成長到好幾個檔案或需要依賴項目時,讓 Cargo 來協調你的專案會來的簡單許多。 儘管 hello_cargo 是個小專案,但它使用了你未來的 Rust 生涯中真實情況下會用到的工具。事實上,所有存在的專案,你幾乎都可以用以下命令完成:使用 Git 下載專案、移至專案目錄然後建構完成。 $ git clone example.org/someproject\n$ cd someproject\n$ cargo build 有關 Cargo 的更多資訊,請查看它的 技術文件 。","breadcrumbs":"開始入門 » Hello, Cargo! » 將 Cargo 視為常規","id":"29","title":"將 Cargo 視為常規"},"290":{"body":"能夠安全高效處理並行程式設計是 Rust 的另一項主要目標。 並行程式設計 (Concurrent programming)會讓程式的不同部分獨立執行,而 平行程式設計 (parallel programming)則是程式的不同部分同時執行。這些隨著電腦越能善用多處理器時也越顯得重要。歷史上,這種程式設計是很困難且容易出錯的,Rust 希望能改善這點。 起初 Rust 團隊認為確保記憶體安全與預防並行問題是兩個分別的問題,要用不同的解決方案。隨著時間過去,團隊發現所有權與型別系統同時是管理記憶體安全 以及 並行問題的強大工具!透過藉助所有權與型別檢查,許多並行錯誤在 Rust 中都是編譯時錯誤而非執行時錯誤。因此,你不用花大量時間嘗試重現編譯時並行錯誤出現時的特定情況,不正確的程式碼會在編譯時就被拒絕,並顯示錯誤解釋問題原因。這樣一來,你就可以在開發時就修正問題,而不用等到可能都部署到生產環境了才發現問題。我們稱呼這個 Rust 的特色為 無懼並行(fearless concurrency) 。無懼並行可以避免你寫出有微妙錯誤的程式碼,並能輕鬆重構,不用擔心產生新的程式錯誤。 注意:出於簡潔考量,我們將把許多問題歸類為 並行 ,而不是精確地區分是 並行與/或平行 。如果本書是本專注在並行與/或平行的書,我們才會更在意用詞。至於本章節,當我們使用 並行 的詞彙時,請記得這代表 並行與/或平行 。 許多語言對於處理並行問題所提供的解決方案都很有特色。舉例來說,Erlang 有非常優雅的訊息傳遞並行功能,但跨執行緒共享狀態就只有比較隱晦的方法。只提供支援可能解決方案的子集對於高階語言來說是合理的策略,因為高階語言所承諾的效益來自於犧牲一些掌控以換取大量的抽象層面。然而,低階語言則預期會提供在任何給定場合中能有最佳效能的解決方案,而且對硬體的抽象較少。因此 Rust 提供了多種工具來針對適合你的場合與需求將問題定義出來。 本章節中我們會涵蓋這些主題: 如何建立執行緒(threads)來同時執行多段程式碼 訊息傳遞 (Message-passing)並行提供通道(channels)在執行緒間傳遞訊息 共享狀態 (Shared-state)並行提供多執行緒可以存取同一位置的資料 Sync 與 Send 特徵擴展 Rust 的並行保障至使用者定義的型別與標準函式庫的型別中","breadcrumbs":"無懼並行 » 無懼並行","id":"290","title":"無懼並行"},"291":{"body":"在大部分的現代作業系統中,被執行的程式碼會在 程序(process) 中執行,作業系統會負責同時處理數個程序。在你的程式中,你也可以將各自獨立的部分同時執行。執行這些獨立部分的功能就叫做 執行緒(threads) 。舉例來說,一個網路伺服器可以有數個執行緒來同時回應一個以上的請求。 將程式中的運算拆成數個執行緒可以提升效能,不過這也同時增加了複雜度。因為執行緒可以同時執行,所以無法保證不同執行緒的程式碼執行的順序。這會導致以下問題: 競爭條件(Race conditions):數個執行緒以不一致的順序取得資料或資源 死結(Deadlocks):兩個執行緒彼此都在等待對方,因而讓執行緒無法繼續執行 只在特定情形會發生的程式錯誤,並難以重現與穩定修復 Rust 嘗試降低使用執行緒所帶來的負面效果,不過對於多執行緒程式設計還是得格外小心,其所要求的程式結構也與單一執行緒的程式有所不同。 不同程式語言會以不同的方式實作執行緒,許多作業系統都有提供 API 來建立新的執行緒。Rust 標準函式庫使用的是 1:1 的執行緒實作模型,也就是每一個語言產生的執行緒就是一個作業系統的執行緒。有其他 crate 會實作其他種執行緒模型,讓我們能與 1:1 模型之間做取捨。","breadcrumbs":"無懼並行 » 使用執行緒同時執行程式碼 » 使用執行緒同時執行程式碼","id":"291","title":"使用執行緒同時執行程式碼"},"292":{"body":"要建立一個新的執行緒,我們呼叫函式 thread::spawn 並傳入一個閉包(我們在第十三章談過閉包),其包含我們想在新執行緒執行的程式碼。範例 16-1 會在主執行緒印出一些文字,並在新執行緒印出其他文字: 檔案名稱:src/main.rs use std::thread;\nuse std::time::Duration; fn main() { thread::spawn(|| { for i in 1..10 { println!(\"數字 {} 出現在產生的執行緒中!\", i); thread::sleep(Duration::from_millis(1)); } }); for i in 1..5 { println!(\"數字 {} 出現在主執行緒中!\", i); thread::sleep(Duration::from_millis(1)); }\n} 範例 16-1:建立一個會印出一些字的新執行緒,而主執行緒會印出其他字 注意到當 Rust 程式的主執行緒完成的話,所有執行緒也會被停止,無論它有沒有完成任務。此程式的輸出結果每次可能都會有點不相同,但它會類似以下這樣: 數字 1 出現在主執行緒中!\n數字 1 出現在產生的執行緒中!\n數字 2 出現在主執行緒中!\n數字 2 出現在產生的執行緒中!\n數字 3 出現在主執行緒中!\n數字 3 出現在產生的執行緒中!\n數字 4 出現在主執行緒中!\n數字 4 出現在產生的執行緒中!\n數字 5 出現在產生的執行緒中! thread::sleep 的呼叫強制執行緒短時間內停止運作,讓不同的執行緒可以執行。執行緒可能會輪流執行,但並不保證絕對如此,這會依據你的作業系統如何安排執行緒而有所不同。在這一輪中,主執行緒會先顯示,就算程式中是先寫新執行緒的 println! 陳述式。而且雖然我們是寫說新執行緒印出 i 一直到 9,但它在主執行緒結束前只印到 5。 如果當你執行此程式時只看到主執行緒的結果,或者沒有看到任何交錯的話,你可以嘗試增加數字範圍來增加作業系統切換執行緒的機會。","breadcrumbs":"無懼並行 » 使用執行緒同時執行程式碼 » 透過 spawn 建立新的執行緒","id":"292","title":"透過 spawn 建立新的執行緒"},"293":{"body":"範例 16-1 的程式碼在主執行緒結束時不只會在大多數的時候提早結束新產生的執行緒,還不能保證執行緒運行的順序,我們甚至無法保證產生的執行緒真的會執行! 透過儲存 thread::spawn 回傳的數值為變數,我們可以修正產生的執行緒完全沒有執行或沒有執行完成的問題。thread::spawn 的回傳型別為 JoinHandle。JoinHandle 是個有所有權的數值,當我們對它呼叫 join 方法時,它就會等待它的執行緒完成。範例 16-2 顯示了如何使用我們在範例 16-1 中執行緒的 JoinHandle 並呼叫 join 來確保產生的執行緒會在 main 離開之前完成: 檔案名稱:src/main.rs use std::thread;\nuse std::time::Duration; fn main() { let handle = thread::spawn(|| { for i in 1..10 { println!(\"數字 {} 出現在產生的執行緒中!\", i); thread::sleep(Duration::from_millis(1)); } }); for i in 1..5 { println!(\"數字 {} 出現在主執行緒中!\", i); thread::sleep(Duration::from_millis(1)); } handle.join().unwrap();\n} 範例 16-2:從 thread::spawn 儲存 JoinHandle 以保障執行緒能執行完成 對其呼叫 join 會阻擋當前正在執行的執行緒中直到 JoinHandle 的執行緒結束為止。 阻擋 (Blocking)一條執行緒代表該執行緒不會繼續運作或離開。因為我們在主執行緒的 for 迴圈之後加上了 join 的呼叫,範例 16-2 應該會產生類似以下的輸出: 數字 1 出現在主執行緒中!\n數字 2 出現在主執行緒中!\n數字 1 出現在產生的執行緒中!\n數字 3 出現在主執行緒中!\n數字 2 出現在產生的執行緒中!\n數字 4 出現在主執行緒中!\n數字 3 出現在產生的執行緒中!\n數字 4 出現在產生的執行緒中!\n數字 5 出現在產生的執行緒中!\n數字 6 出現在產生的執行緒中!\n數字 7 出現在產生的執行緒中!\n數字 8 出現在產生的執行緒中!\n數字 9 出現在產生的執行緒中! 兩條執行緒會互相交錯,但是主執行緒這次會因為 handle.join() 而等待,直到產生的執行緒完成前都不會結束。 那如果我們如以下這樣將 handle.join() 移到 main 中的 for 迴圈前會發生什麼事呢: 檔案名稱:src/main.rs use std::thread;\nuse std::time::Duration; fn main() { let handle = thread::spawn(|| { for i in 1..10 { println!(\"數字 {} 出現在產生的執行緒中!\", i); thread::sleep(Duration::from_millis(1)); } }); handle.join().unwrap(); for i in 1..5 { println!(\"數字 {} 出現在主執行緒中!\", i); thread::sleep(Duration::from_millis(1)); }\n} 主執行緒會等待產生的執行緒完成才會執行它的 for 迴圈,所以輸出結果就不會彼此交錯,如以下所示: 數字 1 出現在產生的執行緒中!\n數字 2 出現在產生的執行緒中!\n數字 3 出現在產生的執行緒中!\n數字 4 出現在產生的執行緒中!\n數字 5 出現在產生的執行緒中!\n數字 6 出現在產生的執行緒中!\n數字 7 出現在產生的執行緒中!\n數字 8 出現在產生的執行緒中!\n數字 9 出現在產生的執行緒中!\n數字 1 出現在主執行緒中!\n數字 2 出現在主執行緒中!\n數字 3 出現在主執行緒中!\n數字 4 出現在主執行緒中! 像這樣將 join 呼叫置於何處的小細節,會影響你的執行緒會不會同時運行。","breadcrumbs":"無懼並行 » 使用執行緒同時執行程式碼 » 使用 join 等待所有執行緒完成","id":"293","title":"使用 join 等待所有執行緒完成"},"294":{"body":"我們通常會使用 thread::spawn 時都會搭配有 move 關鍵字的閉包,因為該閉包能獲取周圍環境的數值,轉移那些數值的所有權到另一個執行緒中。在第十三章的 「獲取參考或移動所有權」 段落我們討論過閉包如何運用 move。現在我們會來專注在 move 與 thread::spawn 之間如何互動。 在第十三章中,我們提到我們可以在閉包參數列表前使用 move 關鍵字來強制閉包取得其從環境獲取數值的所有權。此技巧在建立新的執行緒特別有用,讓我們可以從一個執行緒轉移數值所有權到另一個執行緒。 注意到範例 16-1 中我們傳入 thread::spawn 的閉包沒有任何引數,我們在產生的執行緒程式碼內沒有使用主執行緒的任何資料。要在產生的執行緒中使用主執行緒的資料的話,產生的執行緒閉包必須獲取它所需的資料。範例 16-3 嘗試在主執行緒建立一個向量並在產生的執行緒使用它。不過這目前無法執行,你會在稍後知道原因。 檔案名稱:src/main.rs use std::thread; fn main() { let v = vec![1, 2, 3]; let handle = thread::spawn(|| { println!(\"這是個向量:{:?}\", v); }); handle.join().unwrap();\n} 範例 16-3:嘗試在其他執行緒使用主執行緒建立的向量 閉包想使用 v,所以它得獲取 v 並使其成為閉包環境的一部分。因為 thread::spawn 會在新的執行緒執行此閉包,我們要能在新的執行緒內存取 v。但當我們編譯此範例時,我們會得到以下錯誤: $ cargo run Compiling threads v0.1.0 (file:///projects/threads)\nerror[E0373]: closure may outlive the current function, but it borrows `v`, which is owned by the current function --> src/main.rs:6:32 |\n6 | let handle = thread::spawn(|| { | ^^ may outlive borrowed value `v`\n7 | println!(\"這是個向量:{:?}\", v); | - `v` is borrowed here |\nnote: function requires argument type to outlive `'static` --> src/main.rs:6:18 |\n6 | let handle = thread::spawn(|| { | __________________^\n7 | | println!(\"這是個向量:{:?}\", v);\n8 | | }); | |______^\nhelp: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword |\n6 | let handle = thread::spawn(move || { | ++++ For more information about this error, try `rustc --explain E0373`.\nerror: could not compile `threads` due to previous error Rust 會 推斷 如何獲取 v 而且因為 println! 只需要 v 的參考,閉包得借用 v。不過這會有個問題,Rust 無法知道產生的執行緒會執行多久,所以它無法確定 v 的參考是不是永遠有效。 範例 16-4 提供了一個情境讓 v 很有可能不再有效: 檔案名稱:src/main.rs use std::thread; fn main() { let v = vec![1, 2, 3]; let handle = thread::spawn(|| { println!(\"這是個向量:{:?}\", v); }); drop(v); // 喔不! handle.join().unwrap();\n} 範例 16-4:執行緒的閉包嘗試獲取 v 的參考,但主執行緒會釋放 v 如果 Rust 允許執行此程式碼,產生的執行緒是有可能會置於背景而沒有馬上執行。產生的執行緒內部有 v 的參考,但主執行緒會立即釋放 v,使用我們在第十五章討論過的 drop 函式。然後當產生的執行緒開始執行時,v 就不再有效了,所以它的參考也是無效的了。喔不! 要修正範例 16-3 的編譯錯誤,我們可以使用錯誤訊息的建議: help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword |\n6 | let handle = thread::spawn(move || { | ++++ 透過在閉包前面加上 move 關鍵字,我們強制讓閉包取得它所要使用數值的所有權,而非任由 Rust 去推斷它是否該借用數值。範例 16-5 修改了範例 16-3 並能夠如期編譯與執行: 檔案名稱:src/main.rs use std::thread; fn main() { let v = vec![1, 2, 3]; let handle = thread::spawn(move || { println!(\"這是個向量:{:?}\", v); }); handle.join().unwrap();\n} 範例 16-5:使用 move 關鍵字強制閉包取得它所使用數值的所有權 我們可能會想嘗試用範例 16-4 做的事來修正程式碼,使用 move 閉包的同時在主執行緒呼叫 drop。但這樣的修正沒有用,因為範例 16-4 想做的事情會因為不同原因而不被允許。如果我們對閉包加上了 move,我們將會把 v 移入閉包環境,而在主執行緒將無法再對它呼叫 drop 了。我們會得到另一個編譯錯誤: $ cargo run Compiling threads v0.1.0 (file:///projects/threads)\nerror[E0382]: use of moved value: `v` --> src/main.rs:10:10 |\n4 | let v = vec![1, 2, 3]; | - move occurs because `v` has type `Vec`, which does not implement the `Copy` trait\n5 |\n6 | let handle = thread::spawn(move || { | ------- value moved into closure here\n7 | println!(\"這是個向量:{:?}\", v); | - variable moved due to use in closure\n...\n10 | drop(v); // 喔不! | ^ value used here after move For more information about this error, try `rustc --explain E0382`.\nerror: could not compile `threads` due to previous error Rust 的所有權規則再次拯救了我們!我們在範例 16-3 會得到錯誤是因為 Rust 是保守的,所以只會為執行緒借用 v,這代表主執行緒理論上可能會使產生的執行緒的參考無效化。透過告訴 Rust 將 v 的所有權移入產生的執行緒中,我們向 Rust 保證不會在主執行緒用到 v。如果我們用相同方式修改範例 16-4 的話,當我們嘗試在主執行緒使用 v 的話,我們就違反了所有權規則。move 關鍵字會覆蓋 Rust 保守的預設借用行為,且也不允許我們違反所有權規則。 有了對執行緒與執行緒 API 的基本瞭解,讓我們看看我們可以透過執行緒 做些 什麼。","breadcrumbs":"無懼並行 » 使用執行緒同時執行程式碼 » 透過執行緒使用 move 閉包","id":"294","title":"透過執行緒使用 move 閉包"},"295":{"body":"有一種確保安全並行且漸漸流行起來的方式是 訊息傳遞(message passing) ,執行緒或 actors 透過傳遞包含資料的訊息給彼此來溝通。此理念源自於 Go 語言技術文件 中的口號:「別透過共享記憶體來溝通,而是透過溝通來共享記憶體。」 對於訊息傳遞的並行,Rust 的標準函式庫有提供 通道 (channel)的實作。通道是一種程式設計的概念,會把資料從一個執行緒傳送到另一個。 你可以把程式設計的通道想像成水流的通道,像是河流或小溪。如果你將橡皮小鴨或船隻放入河流中,它會順流而下到下游。 一個通道會包含兩個部分:發送者(transmitter)與接收者(receiver)。發送者正是你會放置橡皮小鴨到河流中的上游,而接收者則是橡皮小鴨最後漂流到的下游。你程式碼中的一部分會呼叫發送者的方法來傳送你想要傳遞的資料,然後另一部分的程式碼會檢查接收者收到的訊息。當發送者或接收者有一方被釋放掉時,該通道就會被 關閉 。 我們在此將寫一支程式,它會在一個執行緒中產生數值,傳送給通道,然後另一個執行緒會接收到數值並印出來。我們會使用通道在執行緒間傳送簡單的數值來作為這個功能的解說。一旦你熟悉此技巧後,你可以使用通道讓執行緒間可以互相溝通。像是實作個聊天系統,或是一個利用數個執行緒進行運算,然後將結果傳入一個執行緒統整結果的分散式系統。 首先在範例 16 -6,我們會建立個通道但還不會做任何事。注意這樣不會編譯通過因為 Rust 無法知道我們想對通道傳入的數值型別為何。 檔案名稱:src/main.rs use std::sync::mpsc; fn main() { let (tx, rx) = mpsc::channel();\n} 範例 16-6:建立通道並賦值分別兩個部分給 tx 與 rx 我們使用 mpsc::channel 函式來建立新的通道,mpsc 指的是 多重生產者、唯一消費者(multiple producer, single consumer) 。簡單來說,Rust 標準函式庫實作通道的方式讓通道可以有多個 發送端 來產生數值,不過只有一個 接收端 能消耗這些數值。想像有數個溪流匯聚成一條大河流,任何溪流傳送的任何東西最終都會流向河流的下游。我們會先從單一生產者開始,等這個範例能夠執行後我們再來增加數個生產者。 mpsc::channel 函式會回傳一個元組,第一個元素是發送者然後第二個元素是接收者。tx 與 rx 通常分別作為 發送者 (transmitter)與 接收者 (receiver)的縮寫,所以我們以此作為我們的變數名稱。我們的 let 陳述式使用到了能解構元組的模式我們會在第十八章討論 let 陳述式的模式與解構方式。用這樣的方式使用 let 能輕鬆取出 mpsc::channel 回傳的元組每個部分。 讓我們將發送端移進一個新產生的執行緒並讓它傳送一條字串,這樣產生的執行緒就可以與主執行緒溝通了,如範例 16-7 所示。這就像是在河流上游放了一隻橡皮小鴨,或是從一條執行緒傳送一條聊天訊息給別條執行緒一樣。 檔案名稱:src/main.rs use std::sync::mpsc;\nuse std::thread; fn main() { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let val = String::from(\"嗨\"); tx.send(val).unwrap(); });\n} 範例 16-7:將 tx 移入產生的執行緒並傳送「hi」 我們再次使用 thread::spawn 來建立新的執行緒並使用 move 將 tx 移入閉包,讓產生的執行緒擁有 tx。產生的執行緒必須要擁有發送者才能夠傳送訊息至通道。發送端有個 send 方法可以接受我們想傳遞的數值。send 方法會回傳 Result 型別,所以如果接收端已經被釋放因而沒有任何地方可以傳遞數值的話,傳送的動作就會回傳錯誤。在此例中,我們呼叫 unwrap 所以有錯誤時就會直接恐慌。但在實際的應用程式中,我們會更妥善地處理它,你可以回顧第九章來複習如何適當地處理錯誤。 在範例 16-8 我們會在主執行緒中從接收者取得數值。這就像在河流下游取回順流而下的橡皮小鴨,或是像取得一條聊天訊息一樣。 檔案名稱:src/main.rs use std::sync::mpsc;\nuse std::thread; fn main() { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let val = String::from(\"嗨\"); tx.send(val).unwrap(); }); let received = rx.recv().unwrap(); println!(\"取得:{}\", received);\n} 範例 16-8:在主執行緒取得數值「hi」並顯示出來 接收者有兩個實用的方法:recv 與 try_recv。我們使用 recv 作為 接收 (receive)的縮寫,這會阻擋主執行緒的運行並等待直到通道有訊息傳入。一旦有數值傳遞,recv 會就以此回傳 Result。當發送者關閉時,recv 會回傳錯誤來通知不會再有任何數值出現了。 try_recv 方法則不會阻擋,而是會立即回傳 Result。如果有數值的話,就會是存有訊息的 Ok 數值,如果尚未有任何數值的話,就會是 Err 數值。try_recv 適用於如果此執行緒在等待訊息的同時有其他事要做的情形。我們可以寫個迴圈來時不時呼叫 try_recv,當有數值時處理訊息,不然的話就先做點其他事直到再次檢查為止。 我們出於方便考量在此例使用 recv,我們的主執行緒除了等待訊息以外沒有其他事好做,所以阻擋主執行緒是合理的。 當我們執行範例 16-8 的程式碼,我們會看到主執行緒印出的數值: 取得:嗨 太棒了!","breadcrumbs":"無懼並行 » 使用訊息傳遞在執行緒間傳送資料 » 使用訊息傳遞在執行緒間傳送資料","id":"295","title":"使用訊息傳遞在執行緒間傳送資料"},"296":{"body":"所有權規則在訊息傳遞中扮演了重要的角色,因為它們可以幫助你寫出安全的並行程式碼。在 Rust 程式中考慮所有權的其中一項好處就是你能在並行程式設計避免錯誤發生。讓我們做個實驗來看通道與所有權如何一起合作來避免問題發生,我們會在 val 數值傳送給通道 之後 嘗試使用其值。請嘗試編譯範例 16-9 的程式碼並看看為何此程式碼不被允許: 檔案名稱:src/main.rs use std::sync::mpsc;\nuse std::thread; fn main() { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let val = String::from(\"嗨\"); tx.send(val).unwrap(); println!(\"val 為 {}\", val); }); let received = rx.recv().unwrap(); println!(\"取得:{}\", received);\n} 範例 16-9:在我們將 val 傳入通道後嘗試使用其值 我們在這裡透過 tx.send 將 val 傳入通道之後嘗試印出其值。允許這麼做的話會是個壞主意,一旦數值被傳至其他執行緒,該執行緒就可以在我們嘗試再次使用該值之前修改或釋放其值。其他執行緒的修改有機會因為不一致或不存在的資料而導致錯誤或意料之外的結果。不過如果我試著編譯範例 16-9 的程式碼的話,Rust 會給我們一個錯誤: $ cargo run Compiling message-passing v0.1.0 (file:///projects/message-passing)\nerror[E0382]: borrow of moved value: `val` --> src/main.rs:10:31 |\n8 | let val = String::from(\"嗨\"); | --- move occurs because `val` has type `String`, which does not implement the `Copy` trait\n9 | tx.send(val).unwrap(); | --- value moved here\n10 | println!(\"val 為 {}\", val); | ^^^ value borrowed here after move | = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info) For more information about this error, try `rustc --explain E0382`.\nerror: could not compile `message-passing` due to previous error 我們的並行錯誤產生了一個編譯時錯誤。send 函式會取走其參數的所有權,並當數值移動時,接收端會再取得其所有權。這能阻止我們在傳送數值過後不小心再次使用其值,所有權系統會檢查一切是否符合規則。","breadcrumbs":"無懼並行 » 使用訊息傳遞在執行緒間傳送資料 » 通道與所有權轉移","id":"296","title":"通道與所有權轉移"},"297":{"body":"範例 16-8 的程式碼可以編譯通過並執行,但它並沒有清楚表達出兩個不同的執行緒正透過通道彼此溝通。在範例 16-10 中我們做了些修改來證明範例 16-8 的程式有正確執行,產生的執行緒現在會傳送數個訊息,並在每個訊息間暫停個一秒鐘。 檔案名稱:src/main.rs use std::sync::mpsc;\nuse std::thread;\nuse std::time::Duration; fn main() { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let vals = vec![ String::from(\"執行緒\"), String::from(\"傳來\"), String::from(\"的\"), String::from(\"嗨\"), ]; for val in vals { tx.send(val).unwrap(); thread::sleep(Duration::from_secs(1)); } }); for received in rx { println!(\"取得:{}\", received); }\n} 範例 16-10:傳送數個訊息並在之間暫停片刻 這次產生的執行緒有個字串向量,我們希望能傳送它們到主執行緒中。我們遍歷它們,單獨傳送每個值,然後透過 Duration 數值呼叫 thread::sleep 來暫停一秒。 在主執行緒中,我們不再顯式呼叫 recv 函式,我們改將 rx 作為疊代器使用。對每個接收到的數值,我們印出它。當通道關閉時,疊代器就會結束。 當執行範例 16-10 的程式碼,你應該會看到以下輸出,每一行會間隔一秒鐘: 取得:執行緒\n取得:傳來\n取得:的\n取得:嗨 因為我們在主執行緒中的 for 迴圈內沒有任何會暫停或延遲的程式碼,所以我們可以看出主執行緒是在等待產生的執行緒傳送的數值。","breadcrumbs":"無懼並行 » 使用訊息傳遞在執行緒間傳送資料 » 傳送多重數值並觀察接收者等待","id":"297","title":"傳送多重數值並觀察接收者等待"},"298":{"body":"稍早之前我們提過 mpsc 是 多重生產者、唯一消費者 (multiple producer, single consumer)的縮寫。讓我們來使用 mpsc 並擴展範例 16-10 的程式碼來建立數個執行緒,它們都將傳遞數值給同個接收者。為此我們可以克隆發送者,如範例 16-11 所示: 檔案名稱:src/main.rs # use std::sync::mpsc;\n# use std::thread;\n# use std::time::Duration;\n# # fn main() { // --省略-- let (tx, rx) = mpsc::channel(); let tx1 = tx.clone(); thread::spawn(move || { let vals = vec![ String::from(\"執行緒\"), String::from(\"傳來\"), String::from(\"的\"), String::from(\"嗨\"), ]; for val in vals { tx1.send(val).unwrap(); thread::sleep(Duration::from_secs(1)); } }); thread::spawn(move || { let vals = vec![ String::from(\"更多\"), String::from(\"給你\"), String::from(\"的\"), String::from(\"訊息\"), ]; for val in vals { tx.send(val).unwrap(); thread::sleep(Duration::from_secs(1)); } }); for received in rx { println!(\"取得:{}\", received); } // --省略--\n# } 範例 16-11:從多重生產者傳遞數個訊息 這次在我們建立第一個產生的執行緒前,我們會對發送者呼叫 clone。這能給我們一個新的發送者,讓我們可以移入第一個產生的執行緒。接著我們將原本的通道發送端移入第二個產生的執行緒中。這樣我們就有了兩條執行緒,每條都能傳送不同的訊息給接收者。 當你執行程式碼時,你的輸出應該會類似以下結果: 取得:執行緒\n取得:更多\n取得:傳來\n取得:給你\n取得:的\n取得:的\n取得:嗨\n取得:訊息 依據你的系統你可能會看到數值以不同順序排序。這正是並行程式設計既有趣卻又困難的地方。如果你加上 thread::sleep 來實驗,並在不同執行緒給予不同數值的話,就會發現每一輪都會更不確定,每次都會產生不同的輸出結果。 現在我們已經看完通道如何運作,接著讓我們來看看並行的不同方法吧。","breadcrumbs":"無懼並行 » 使用訊息傳遞在執行緒間傳送資料 » 透過克隆發送者來建立多重生產者","id":"298","title":"透過克隆發送者來建立多重生產者"},"299":{"body":"雖然我們之前說過 Go 語言技術文件中的口號:「別透過共享記憶體來溝通。」而訊息傳遞是個很好的並行處理方式,但它不是唯一的選項。另一種方式就是在多重執行緒間共享資料。 透過共享記憶體來溝通會是什麼樣子呢?除此之外,為何訊息傳遞愛好者不喜歡這種共享記憶體的方式呢? 任何程式語言的通道某方面來說類似於單一所有權,因為一旦你轉移數值給通道,你就不該使用該數值。共享記憶體並行則像多重所有權,數個執行緒可以同時存取同個記憶體位置。如同你在第十五章所見到的,智慧指標讓多重所有權成為可能,但多重所有權會增加複雜度,因為我們會需要管理這些不同的擁有者。Rust 的型別系統與所有權規則大幅地協助了正確管理這些所有權。作為範例就讓我們看看互斥鎖(mutexes),這是共享記憶體中常見的並行原始元件之一。","breadcrumbs":"無懼並行 » 共享狀態並行 » 共享狀態並行","id":"299","title":"共享狀態並行"},"3":{"body":"Rust 的各種特長讓它適用於很多人,我們來討論一些最重要的客群。","breadcrumbs":"介紹 » Rust 適用於誰","id":"3","title":"Rust 適用於誰"},"30":{"body":"你已經完成你的 Rust 旅途的第一步了!在本章節你學到了: 使用 rustup 安裝最新穩定版 Rust 更新到最新 Rust 版本 開啟本地端安裝的技術文件 直接使用 rustup 編寫並執行一支「Hello, world!」程式 使用 Cargo 建立並執行一個新專案 接下來是時候來建立一個更實際的程式來熟悉 Rust 程式碼的讀寫了。所以在第二章我們將寫出一支猜謎遊戲的程式。如果你想直接學習 Rust 的常見程式設計概念的話,你可直接閱讀第三章,之後再回來看第二章。","breadcrumbs":"開始入門 » Hello, Cargo! » 總結","id":"30","title":"總結"},"300":{"body":"互斥鎖 (Mutex)是 mutual exclusion 的縮寫,顧名思義互斥鎖在任意時刻只允許一條執行緒可以存取一些資料。要取得互斥鎖中的資料,執行緒必須先透過獲取互斥鎖的 鎖(lock) 來表示它想要進行存取。鎖是互斥鎖其中一部分的資料結構,用來追蹤當前誰擁有資料的獨佔存取權。因此互斥鎖被描述為會透過鎖定系統 守護 (guarding)其所持有的資料。 互斥鎖以難以使用著名,因為你必須記住兩個規則: 你必須在使用資料前獲取鎖。 當你用完互斥鎖守護的資料,你必須解鎖資料,所以其他的執行緒才能獲取鎖。 要用真實世界來比喻互斥鎖的話,想像在會議中有個座談會只有一支麥克風。如果有講者想要發言時,他們需要請求或示意他們想要使用麥克風。當他們取得麥克風時,他們想講多久都沒問題,直到將麥克風遞給下個要求發言的講者。如果講者講完後忘記將麥克風遞給其他人的話,就沒有人有辦法發言。如果麥克風的分享出狀況的話,座談會就無法如期進行! 互斥鎖的管理要正確處理是極為困難的,這也是為何這麼多人傾向於使用通道。然而有了 Rust 的型別系統與所有權規則,你就不會在鎖定與解鎖之間出錯了。 Mutex 的 API 作為使用互斥鎖的範例,讓我們先在單執行緒使用互斥鎖,如範例 16-12 所示: 檔案名稱:src/main.rs use std::sync::Mutex; fn main() { let m = Mutex::new(5); { let mut num = m.lock().unwrap(); *num = 6; } println!(\"m = {:?}\", m);\n} 範例 16-12:基於簡便考量先用單一執行緒探討 Mutex 的 API 就像許多型別一樣,我們使用關聯函式 new 建立 Mutex。要取得互斥鎖內的資料,我們使用 lock 方法來獲取鎖。此呼叫會阻擋當前執行緒做任何事,直到輪到它取得鎖。 如果其他持有鎖的執行緒恐慌的話 lock 的呼叫就會失敗。在這樣的情況下,就沒有任何人可以獲得鎖,因此當我們遇到這種情況時,我們選擇 unwrap 並讓此執行緒恐慌。 在我們獲取鎖之後,我們在此例可以將回傳的數值取作 num,作為內部資料的可變參考。型別系統能確保我們在使用數值 m 之前有獲取鎖,Mutex 並不是 i32,所以我們 必須 取得鎖才能使用 i32 數值。我們不可能會忘記這麼做,不然型別系統不會讓我們存取內部的 i32。 如同你所想像的,Mutex 就是個智慧指標。更精確的來說,lock 的呼叫會 回傳 一個智慧指標叫做 MutexGuard,這是我們從 LockResult 呼叫 unwrap 取得的型別。MutexGuard 智慧指標有實作 Deref 特徵來指向我們的內部資料。此智慧指標也有 Drop 的實作,這會在 MutexGuard 離開作用域時自動釋放鎖,也就是在內部作用域結尾就會執行此動作。這樣一來,我們就不會忘記釋放鎖,怕互斥鎖會阻擋其他執行緒,因為鎖會自動被釋放。 在釋放鎖之後,我們就能印出互斥鎖的數值並觀察到我們能夠變更內部的 i32 為 6。 在數個執行緒間共享 Mutex 現在讓我們來透過 Mutex 來在數個執行緒間分享數值。我們會建立 10 個執行緒並讓它們都會對一個計數增加 1,讓計數能從 0 加到 10。作為下個例子的範例 16-13 會出現一個編譯錯誤,我們會用此錯誤瞭解如何使用 Mutex 以及 Rust 如何協助我們來正確使用它。 檔案名稱:src/main.rs use std::sync::Mutex;\nuse std::thread; fn main() { let counter = Mutex::new(0); let mut handles = vec![]; for _ in 0..10 { let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!(\"結果:{}\", *counter.lock().unwrap());\n} 範例 16-13:十個執行緒都會對 Mutex 守護的計數增加 1 我們建立個變數 counter 並在 Mutex 內存有 i32,就像我們在範例 16-12 所做的一樣。接著我們透過指定的範圍建立 10 個執行緒。我們使用 thread::spawn 讓所有的執行緒都有相同的閉包,此閉包會將計數移入執行緒、呼叫 lock 以獲取 Mutex 的鎖,然後將互斥鎖內的數值加 1。當有執行緒執行完它的閉包時,num 會離開作用域並釋放鎖,讓其他的執行緒可以獲取它。 在主執行緒中,我們要收集所有的執行緒。然後如同我們在範例 16-2 所做的,我們呼叫每個執行緒的 join 來確保所有執行緒都有完成。在這時候,主執行緒就能獲取鎖並印出此程式的結果。 我們曾暗示範例不會編譯過,讓我們看看是為何吧! $ cargo run Compiling shared-state v0.1.0 (file:///projects/shared-state)\nerror[E0382]: use of moved value: `counter` --> src/main.rs:9:36 |\n5 | let counter = Mutex::new(0); | ------- move occurs because `counter` has type `Mutex`, which does not implement the `Copy` trait\n...\n9 | let handle = thread::spawn(move || { | ^^^^^^^ value moved into closure here, in previous iteration of loop\n10 | let mut num = counter.lock().unwrap(); | ------- use occurs due to use in closure For more information about this error, try `rustc --explain E0382`.\nerror: could not compile `shared-state` due to previous error 錯誤訊息表示 counter 數值在之前的迴圈循環中被移動了,所以 Rust 告訴我們我們無法將 counter 鎖的所有權移至數個執行緒中。讓我們用第十五章提到的多重所有權方法來修正此編譯錯誤吧。 多重執行緒中的多重所有權 在第十五章中,我們透過智慧指標 Rc 來建立參考計數數值讓該資料可以擁有數個擁有者。讓我們在此也做同樣的動作來看看會發生什麼事。我們會在範例 16-14 將 Mutex 封裝進 Rc 並在將所有權移至執行緒前克隆 Rc。 檔案名稱:src/main.rs use std::rc::Rc;\nuse std::sync::Mutex;\nuse std::thread; fn main() { let counter = Rc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Rc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!(\"結果:{}\", *counter.lock().unwrap());\n} 範例 16-14:嘗試使用 Rc 來允許數個執行緒擁有 Mutex 再編譯一次的話我們會得到... 不同的錯誤!編譯器真的是教了我們很多事。 $ cargo run Compiling shared-state v0.1.0 (file:///projects/shared-state)\nerror[E0277]: `Rc>` cannot be sent between threads safely --> src/main.rs:11:36 |\n11 | let handle = thread::spawn(move || { | ------------- ^------ | | | | ______________________|_____________within this `[closure@src/main.rs:11:36: 11:43]` | | | | | required by a bound introduced by this call\n12 | | let mut num = counter.lock().unwrap();\n13 | |\n14 | | *num += 1;\n15 | | }); | |_________^ `Rc>` cannot be sent between threads safely | = help: within `[closure@src/main.rs:11:36: 11:43]`, the trait `Send` is not implemented for `Rc>`\nnote: required because it's used within this closure --> src/main.rs:11:36 |\n11 | let handle = thread::spawn(move || { | ^^^^^^^\nnote: required by a bound in `spawn` For more information about this error, try `rustc --explain E0277`.\nerror: could not compile `shared-state` due to previous error 哇,這個錯誤訊息的內容真多!這是我們需要注意到的部分:`Rc>` cannot be sent between threads safely。編譯器也告訴了我們原因:the trait `Send` is not implemented for `Rc>` 。我們會在下一個段落討論 Send,這是其中一種確保我們在執行緒中所使用的型別可以用於並行場合的特徵。 不幸的是 Rc 無法安全地跨執行緒分享。當 Rc 管理參考計數時,它會在每個 clone 的呼叫增加計數,並在每個克隆釋放時減少計數。但是它沒有使用任何並行原始元件來確保計數的改變不會被其他執行緒中斷。這樣的計數可能會導致微妙的程式錯誤,像是記憶體泄漏或是在數值釋放時嘗試使用其值。我們需要一個型別和 Rc 一模一樣,但是其參考計數在執行緒間是安全的。 原子參考計數 Arc 幸運的是 Arc 正是 一個類似 Rc 且能安全用在並行場合的型別。字母 A 指的是 原子性 (atomic)代表這是個 原子性參考的計數 型別。原子型別是另一種我們不會在此討論的並行原始元件,你可以查閱標準函式庫的 std::sync::atomic 技術文件以瞭解更多詳情。在此你只需要知道原子型別和原始型別類似,但它們可以安全在執行緒間共享。 你可能會好奇為何原始型別不是原子性的,以及為何標準函式庫的型別預設不使用 Arc 來實作。原因是因為執行緒安全意味著效能開銷,你會希望在你真的需要時才買單。如果你只是在單一執行緒對數值做運算的話,你的程式碼就不必強制具有原子性的保障並能執行地更快。 讓我們回到我們的範例:Arc 與 Rc 具有相同的 API,所以我們透過更改 use 這行、new 的呼叫以及 clone 的呼叫來修正我們程式,。範例 16-15 的程式碼最終將能夠編譯並執行: 檔案名稱:src/main.rs use std::sync::{Arc, Mutex};\nuse std::thread; fn main() { let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!(\"結果:{}\", *counter.lock().unwrap());\n} 範例 16-15:使用 Arc 封裝 Mutex 來在數個執行緒間分享所有權 此程式碼會印出以下結果: 結果:10 我們辦到了!我們從 0 數到了 10,雖然看起來不是很令人印象深刻,但這的確教會了我們很多有關 Mutex 與執行緒安全的知識。你也可以使用此程式結構來做更多複雜的運算,而不只是數數而已。使用此策略,你可以將運算拆成數個獨立部分,將它們分配給執行緒,然後使用 Mutex 來讓每個執行緒更新該部分的結果。 如果你只是想做單純的數值運算,其實標準函式庫提供的 std::sync::atomic 模組會比 Mutex 型別來得簡單。這些型別對原生型別提供安全、並行且原子性的順取。我們在此例對原生型別使用 Mutex,是因為我們想解釋 Mutex 是如何運作的。","breadcrumbs":"無懼並行 » 共享狀態並行 » 使用互斥鎖在同時間只允許一條執行緒存取資料","id":"300","title":"使用互斥鎖在同時間只允許一條執行緒存取資料"},"301":{"body":"你可能已經注意到 counter 是不可變的,但我們卻可以取得數值其內部的可變參考,這代表 Mutex 有提供內部可變性,就像 Cell 家族一樣。我們在第十五章也以相同的方式使用 RefCell 來讓我們能改變 Rc 內部的數值,而在此我們使用 Mutex 改變 Arc 內部的內容。 另一個值得注意的細節是當你使用 Mutex 時,Rust 無法避免所有種類的邏輯錯誤。回憶一下第十五章使用 Rc 時會有可能產生參考循環的風險,兩個 Rc 數值可能會彼此參考,造成記憶體泄漏。同樣地,Mutex 有產生 死結 (deadlocks)的風險。這會發生在當有個動作需要鎖定兩個資源,而有兩個執行緒分別擁有其中一個鎖,導致它們永遠都在等待彼此。如果你對此有興趣的話,歡迎嘗試建立一個有死結的 Rust 程式,然後研究看看任何語言中避免的互斥鎖死結的策略,並嘗試實作它們在 Rust 中。標準函式庫中 Mutex 與 MutexGuard 的 API 技術文件可以提供些實用資訊。 接下來在本章結尾我們會來討論 Send 與 Sync 特徵以及我們如何在自訂型別中使用它們。","breadcrumbs":"無懼並行 » 共享狀態並行 » RefCell/Rc 與 Mutex/Arc 之間的相似度","id":"301","title":"RefCell/Rc 與 Mutex/Arc 之間的相似度"},"302":{"body":"有一個有趣的點是 Rust 語言提供的並行功能並 沒有很多 。本章節討論到的並行功能幾乎都來自於標準函式庫,並不是語言本身。你能處理並行的選項並不限於語言或標準函式庫,你可以寫出你自己的並行功能或使用其他人提供的。 然而,還有有兩個並行概念深植於語言中,那就是 std::marker 中的 Sync 與 Send 特徵。","breadcrumbs":"無懼並行 » 透過 Sync 與 Send 特徵擴展並行性 » 可延展的並行與 Sync 及 Send 特徵","id":"302","title":"可延展的並行與 Sync 及 Send 特徵"},"303":{"body":"Send 標記特徵(marker traits)指定有實作 Send 特徵的型別才能將其數值的所有權在執行緒間轉移。幾乎所有的 Rust 型別都有 Send,但有些例外。這包含 Rc,此型別沒有 Send 是因為如果你克隆了 Rc 數值並嘗試轉移克隆的所有權到其他執行緒,會有兩條執行緒可能同時更新參考計數。基於此原因,Rc 是用於當你不想要付出執行緒安全效能開銷時而在單一執行緒使用的情況。 因此 Rust 的型別系統與特徵界限確保你無法意外不安全地傳送 Rc 數值到其他執行緒。當我們嘗試範例 16-14 時,我們就會得到錯誤 the trait Send is not implemented for Rc>。當我們切換成有實作 Send 的 Arc 的話,程式碼就能編譯通過。 任何由具有 Send 的型別所組成的型別也都會自動標記為 Send。幾乎所有原始型別都是 Send,除了我們將在第十九章提及的裸指標(raw pointers)。","breadcrumbs":"無懼並行 » 透過 Sync 與 Send 特徵擴展並行性 » 透過 Send 來允許所有權能在執行緒間轉移","id":"303","title":"透過 Send 來允許所有權能在執行緒間轉移"},"304":{"body":"Sync 標記特徵指定有實作 Sync 的型別都能安全從多個執行緒來參考。換句話說,對於任何型別 T,如果 &T(對 T 的不可變參考)有 Send 的話,T 就是 Sync 的,這代表參考可以安全地傳給其他執行緒。與 Send 類似,原始型別都是 Sync,所以由具有 Sync 的型別所組成的型別也都有 Sync。 智慧指標 Rc 沒有 Sync 的原因和沒有 Send 的原因一樣。RefCell 型別(我們在第十五章提過)與其 Cell 也都沒有 Sync。 RefCell 在執行時的借用檢查實作沒有執行緒安全。智慧指標 Mutex 才有 Sync 並能像你在 「在數個執行緒間共享 Mutex」 段落看到的那樣用來在多個執行緒間分享存取。","breadcrumbs":"無懼並行 » 透過 Sync 與 Send 特徵擴展並行性 » 透過 Sync 來允許多重執行緒存取","id":"304","title":"透過 Sync 來允許多重執行緒存取"},"305":{"body":"因為由具有 Send 與 Sync 的型別組成的型別自動就會有 Send 與 Sync,我們不需要親自實作這些特徵。至於標記特徵,它們甚至沒有任何方法需要實作。它們只是用於強制確保並行相關的不變性。 要手動實作這些特徵會需要實作不安全(unsafe)的 Rust 程式碼。我們會在第十九章討論如何使用不安全的 Rust 程式碼,現在最重要的資訊是要從不具有 Send 與 Sync 的元件來組成新的並行型別需要格外小心才能確保其安全保障。 「The Rustonomicon」 有更多關於這些保障與如何維持它們的資訊。","breadcrumbs":"無懼並行 » 透過 Sync 與 Send 特徵擴展並行性 » 手動實作 Send 與 Sync 是不安全的","id":"305","title":"手動實作 Send 與 Sync 是不安全的"},"306":{"body":"這不會是你在本書中最後一次看到並行程式碼,第二十章的專案將會在更實際的場合中使用本章節的概念,而非這裡討論的簡單範例。 如之前提過的,因為 Rust 語言本身很少處理並行的部分,許多並行解決方案都實作成 crate。這些 crate 通常發展的比標準函式庫還快,所以別忘了到線上尋找目前最先進的 crate 來在多執行緒場合中使用喔。 Rust 標準函式庫提供訊息傳遞的通道與智慧指標,像是 Mutex 與 Arc,能夠在並行環境中安全使用。型別系統與借用檢查器中確保使用這些解決方案的程式碼不會發生資料競爭或是無效參考。一旦你讓你的程式碼能編譯通過後,你可以放心地認定它會開開心心地在多執行緒中執行,並且不會發生任何在其他語言中常見且難以追蹤的程式錯誤。並行程式設計就不再是個令人害怕的概念,無畏無懼地開發並行程式吧! 接下來,我們要討論當你的 Rust 程式成長時,定義出問題並組織解決辦法的慣用方案。除此之外,我們也將討論 Rust 有哪些與物件導向程式設計(object-oriented programming)類似的概念。","breadcrumbs":"無懼並行 » 透過 Sync 與 Send 特徵擴展並行性 » 總結","id":"306","title":"總結"},"307":{"body":"物件導向程式設計(Object-oriented programming,OOP)是一種模組化程式的方式,物件這種程式設計的概念始於 1960 年的 Simula。這些物件影響了 Alan Kay 的程式設計架構中物件彼此之間訊息的傳遞。他在 1967 年提出了 物件導向程式設計 來描述此架構。對於 OOP 的定義有許多種描述,有些定義會將 Rust 歸類為屬於物件導向的,而有些則不會。在本章節中,我們會探討特定常視為是物件導向的特色並看看這些特色如何轉換成慣用的 Rust 程式碼。然後我們會向你展示如何在 Rust 中實作物件導向設計模式,並討論這麼做與利用 Rust 自身的優勢實現的版本有何取捨差別。","breadcrumbs":"Rust 的物件導向程式設計特色 » Rust 的物件導向程式設計特色","id":"307","title":"Rust 的物件導向程式設計特色"},"308":{"body":"對於一個被視為物件導向的語言該有哪些功能,在程式設計語言社群中並沒有達成共識。Rust 受到許多程式設計理念影響,這當然包括 OOP。舉例來說,我們在第十三章探討了源自於函式語言的特性。同樣地,OOP 語言有一些特定常見特色,諸如物件、封裝(encapsulation)與繼承(inheritance)。讓我們看看這些特色分別是什麼意思以及 Rust 有沒有支援。","breadcrumbs":"Rust 的物件導向程式設計特色 » 物件導向語言的特色 » 物件導向語言的特色","id":"308","title":"物件導向語言的特色"},"309":{"body":"由 Erich Gamma、Richard Helm、Ralph Johnson 與 John Vlissides(Addison-Wesley Professional,1994)所寫的書《Design Patterns: Elements of Reusable Object-Oriented Software》俗稱為「The Gang of Four」,這是本物件導向設計模式的目錄。它是這樣定義 OOP 的: 物件導向程式由物件所組成。 物件 會包裝資料以及運作在資料上的行為。此行為常稱為 方法(methods) 或 操作(operations) 。 在此定義下,Rust 是物件導向的,結構體與列舉擁有資料,而 impl 區塊對結構體與列舉提供方法。就算有方法的結構體與列舉不會被稱為 物件 ,依據 Gang of Four 對物件的定義,它們還是有提供相同的功能。","breadcrumbs":"Rust 的物件導向程式設計特色 » 物件導向語言的特色 » 物件包含資料與行為","id":"309","title":"物件包含資料與行為"},"31":{"body":"讓我們親自動手一同完成一項專案來開始上手 Rust 吧!本章節會介紹一些常見 Rust 概念,展示如何在實際程式中使用它們。你會學到 let、match、方法、關聯函式、外部 crate 以及更多等等!我們會在之後的章節更詳細地探討這些概念。在本章中,你會練習到基礎概念。 我們會實作個經典新手程式問題:猜謎遊戲。它的運作方式如下:程式會產生 1 到 100 之間的隨機整數。接著它會通知玩家猜一個數字。在輸入猜測數字之後,程式會回應猜測的數字太低或太高。如果猜對的話,遊戲就會顯示祝賀訊息並關閉。","breadcrumbs":"設計猜謎遊戲程式 » 設計猜謎遊戲程式","id":"31","title":"設計猜謎遊戲程式"},"310":{"body":"另外一個常和 OOP 相關的概念就是 封裝(encapsulation) ,這指的是物件的實作細節不會讓使用物件的程式碼取得。因此要與該物件互動的方式是透過它的公開 API,使用物件的程式碼不該有辦法觸及物件內部並直接變更資料的行為。這讓程式設計師能變更並重構物件內部,無需擔心要變更使用物件的程式碼。 我們在第七章討論過如何控制封裝,我們可以使用 pub 關鍵字來決定程式中的哪些模組、型別、函式與方法要公開出來,且預設一切都是私有的。舉例來說,我們可以定義個結構體 AveragedCollection 並有個欄位包含一個 i32 數值的向量。此結構體還有個欄位包含向量數值的平均值,讓我們不必在每次呼叫時都得重新計算平均值。換句話說,AveragedCollection 會為我們快取計算出的平均值。範例 17-1 展示了結構體 AveragedCollection 的定義: 檔案名稱:src/lib.rs pub struct AveragedCollection { list: Vec, average: f64,\n} 範例 17-1:結構體 AveragedCollection 有個整數列表與集合中的項目平均值 collection 此結構體有 pub 標記所以其他程式碼可以使用它,但結構體內部的欄位是私有的。這在此例中是很重要的,因為我們希望在有數值加入或移出列表時,平均值也能更新。我們會實作結構體的 add、remove 與 average 方法來達成,如範例 17-2 所示: 檔案名稱:src/lib.rs # pub struct AveragedCollection {\n# list: Vec,\n# average: f64,\n# }\n# impl AveragedCollection { pub fn add(&mut self, value: i32) { self.list.push(value); self.update_average(); } pub fn remove(&mut self) -> Option { let result = self.list.pop(); match result { Some(value) => { self.update_average(); Some(value) } None => None, } } pub fn average(&self) -> f64 { self.average } fn update_average(&mut self) { let total: i32 = self.list.iter().sum(); self.average = total as f64 / self.list.len() as f64; }\n} 範例 17-2:對 AveragedCollection 實作公開方法 add、remove 與 average 公開的方法 add、remove 與 average 是存取或修改 AveragedCollection 實例資料的唯一方法。當有個項目透過 add 方法加入或透過 remove 方法移出 list 中時,每個方法會同時呼叫 update_average 方法來更新 average 欄位。 我們讓 list 與 average 欄位維持私有,所以外部的程式碼不可能直接新增或移除 list 欄位的項目。不然的話,average 欄位可能就無法與變更的 list 同步了。average 方法會回傳 average 欄位的數值,讓外部程式碼能夠讀取 average 但不會修改它。 由於我們封裝了 AveragedCollection 結構體的實作細節,我們可以在未來輕鬆變更像是資料結構等內部細節。舉例來說,我們可以用 HashSet 來替換 list 欄位的 Vec。只要 add、remove 與 average 的公開方法簽名維持一樣,使用到 AveragedCollection 的程式碼就不需要改變。如果我們讓 list 公開的話,情況可能就不相同了,HashSet 與 Vec 有不同的方法來新增和移除項目,所以外部的程式碼如果會直接修改 list 的話,可能會需要做些改變。 如果封裝是物件導向的必備條件的話,Rust 也符合此條件。對程式碼中不同部分使用 pub 可以封裝實作細節。","breadcrumbs":"Rust 的物件導向程式設計特色 » 物件導向語言的特色 » 隱藏實作細節的封裝","id":"310","title":"隱藏實作細節的封裝"},"311":{"body":"繼承 (Inheritance)是指一個物件可以繼承其他物件定義的機制,使其可以獲取繼承物件的資料與行為,不必再定義一次。 如果一個語言一定要有繼承才算物件導向語言的話,那麼 Rust 就不是。在定義結構體時我們無法繼承父結構體欄位的方法實作,除非使用巨集。 然而如果你在程式設計時常常用到繼承的話,依據你想使用繼承的原因,Rust 還是有提供其他方案。 你想選擇繼承通常會有兩個主要原因。第一個是想能重複使用程式碼,你可以定義一個型別的特定行為,然後繼承讓你可以在不同的型別重複使用該實作。為此你可以使用預設的特徵方法實作來分享 Rust 程式碼,你在範例 10-14 就有看到我們在 Summary 特徵加上的預設 summarize 方法實作。任何有實作 Summary 特徵的型別都不必加上更多程式碼就能有 summarize 可以呼叫。這就類似於父類型(class)實作的方法可以在繼承的子類型擁有該方法實作。我們也可以在實作 Summary 特徵時,覆寫 summarize 方法的預設實作,這就類似於子類型覆寫父類型的方法實作。 另一個想使用繼承的原因與型別系統有關,讓子類型可以視為父類型來使用。這也稱為 多型(polymorphism) ,代表要是數個物件有共享特定特性的話,你可以在執行時彼此替換使用。","breadcrumbs":"Rust 的物件導向程式設計特色 » 物件導向語言的特色 » 作為型別系統與程式碼共享來繼承","id":"311","title":"作為型別系統與程式碼共享來繼承"},"312":{"body":"對許多人來說,多型就是繼承的代名詞。不過這其實是個更通用的概念,用來指程式碼可適用於多種型別資料。而對繼承來說,這些型別通常都是子類型。 Rust 則是使用泛型來抽象化不同可能的型別,並以特徵界限來加強約束這些型別必須提供的內容。這有時會稱為 限定的參數多型(bounded parametric polymorphism) 。 近年來像繼承這種程式設計的解決方案在許多程式設計語言中都漸漸失寵了,因為這經常有分享不必要程式碼的風險。子類型不應該永遠分享其父類型的所有特性,但繼承會這樣做。這會讓程式的設計較不具有彈性。這還可能產生不具意義或導致錯誤的子類型方法呼叫,因為該方法不適用於子類型。除此之外,有些語言只會允許單一繼承,也就是一個子類型只能繼承一個類別,進一步限制了程式設計的彈性。 基於這些原因,Rust 採取了不同的方案,使用特徵物件(trait objects)而非繼承。讓我們看看 Rust 的特徵物件如何達成多型。","breadcrumbs":"Rust 的物件導向程式設計特色 » 物件導向語言的特色 » 多型","id":"312","title":"多型"},"313":{"body":"在第八章中,我們提及向量其中一項限制是它儲存的元素只能有一種型別。我們在範例 8-9 提出一個替代方案,那就是我們定義 SpreadsheetCell 列舉且其變體能存有整數、浮點數與文字。這讓我們可以對每個元素儲存不同的型別,且向量仍能代表元素的集合。當我們的可變換的項目有固定的型別集合,而且我們在編譯程式碼時就知道的話,這的確是完美的解決方案。 然而,有時我們會希望函式庫的使用者能夠在特定的情形下擴展型別的集合。為了展示我們如何達成,我們來建立個圖形使用者介面(graphical user interface,GUI)工具範例來遍歷一個項目列表,呼叫其內每個項目的 draw 方法將其顯示在螢幕上,這是 GUI 工具常見的技巧。我們會建立個函式庫 crate 叫做 gui,這會包含 GUI 函式庫的結構體。此 crate 可能會包含一些人們會使用到的型別,像是 Button 或 TextField。除此之外,gui 使用者也能夠建立他們自己的型別來顯示出來。舉例來說,有些開發者可以加上 Image 而其他人可能會加上 SelectBox。 我們在此例中不會實作出整個 GUI 函式庫,但會展示各個元件是怎麼組合起來的。在寫函式庫時,我們無法知道並定義開發者想建立出來的所有型別。但我們知道 gui 需要追蹤許多不同型別的數值,且它需要能夠對這些不同的型別數值呼叫 draw 方法。它不需要知道當我們呼叫 draw 方法時實際發生了什麼事,只需要知道該數值有我們可以呼叫的方法。 在有繼承的語言中,我們可能會定義一個類型(class)叫做 Component 且其有個方法叫做 draw。其他的類型像是 Button、Image 和 SelectBox 等等,可以繼承 Component 以取得 draw 方法。它們可以覆寫 draw 方法來定義它們自己的自訂行為,但是整個框架能將所有型別視為像是 Component 實例來對待,並對它們呼叫 draw。但由於 Rust 並沒有繼承,我們需要其他方式來組織 gui 函式庫,好讓使用者可以透過新的型別來擴展它。","breadcrumbs":"Rust 的物件導向程式設計特色 » 允許不同型別數值的特徵物件 » 允許不同型別數值的特徵物件","id":"313","title":"允許不同型別數值的特徵物件"},"314":{"body":"要定義我們希望 gui 能擁有的行為,我們定義一個特徵叫做 Draw 並有個方法叫做 draw。然後我們可以定義一個接收 特徵物件 (trait object)的向量。一個特徵物件會指向有實作指定特徵的型別以及一個在執行時尋找該型別方法的尋找表(look up table)。要建立特徵物件,我們指定一些指標,像是參考 & 或者智慧指標 Box,然後加上 dyn 關鍵字與指定的相關特徵。(我們會在第十九章的 「動態大小型別與 Sized 特徵」 段落討論特徵物件必須使用指標的原因)我們可以對泛型或實際型別使用特徵物件。當我們使用特徵物件時,Rust 的型別系統會確保在編譯時該段落使用到的任何數值都有實作特徵物件的特徵。於是我們就不必在編譯時知道所有可能的型別。 我們提到在 Rust 中,我們避免將結構體和列舉稱為「物件」,來與其他語言的物件做區別。在結構體或列舉中,結構體欄位中的資料與 impl 區塊的行為是分開來的。在其他語言中,資料與行為會組合成一個概念,也就是所謂的物件。然而特徵物件才比較像是其他語言中的物件,因為這才會將資料與行為組合起來。但特徵物件與傳統物件不同的地方在於,我們無法向特徵物件新增資料。特徵物件不像其他語言的物件那麼通用,它們是特別用於對共同行為產生的抽象概念。 範例 17-3 定義了一個特徵叫做 Draw 以及一個方法叫做 draw: 檔案名稱:src/lib.rs pub trait Draw { fn draw(&self);\n} 範例 17-3:Draw 特徵的定義 此語法和我們在第十章介紹過的特徵定義方式相同。接下來才是新語法用到的地方,範例 17-4 定義了一個結構體叫做 Screen 並持有個向量叫做 components。此向量的型別為 Box,這是一個特徵物件,這代表 Box 內的任何型別都得有實作 Draw 特徵。 檔案名稱:src/lib.rs # pub trait Draw {\n# fn draw(&self);\n# }\n# pub struct Screen { pub components: Vec>,\n} 範例 17-4:定義結構體 Screen 且有個 components 欄位來持有一個實作 Draw 特徵的特徵物件向量 在 Screen 結構體中,我們定義了一個方法叫做 run 來對其 components 呼叫 draw 方法,如範例 17-5 所示: 檔案名稱:src/lib.rs # pub trait Draw {\n# fn draw(&self);\n# }\n# # pub struct Screen {\n# pub components: Vec>,\n# }\n# impl Screen { pub fn run(&self) { for component in self.components.iter() { component.draw(); } }\n} 範例 17-5:Screen 的方法 run 會呼叫每個 component 的 draw 方法 這與定義一個結構體並使用附有特徵界限的泛型型別參數的方式不相同。泛型型別參數一次只能替換成一個實際型別,特徵物件則是在執行時允許數個實際型別能填入特徵物件中。舉例來說,我們可以使用泛型型別與特徵界限來定義 Screen,如範例 17-6 所示: 檔案名稱:src/lib.rs # pub trait Draw {\n# fn draw(&self);\n# }\n# pub struct Screen { pub components: Vec,\n} impl Screen\nwhere T: Draw,\n{ pub fn run(&self) { for component in self.components.iter() { component.draw(); } }\n} 範例 17-6:Screen 結構體的另種實作方式,它的方法 run 則使用泛型與特徵界限 這樣我們會限制 Screen 實例必須擁有一串全是 Button 型別或全是 TextField 型別的列表。如果你只會有同型別的集合,使用泛型與特徵界限的確是比較合適的,因為其定義就會在編譯時單型化為使用實際型別。 另一方面,透過使用特徵物件的方法,Screen 實例就能有個同時包含 Box