1. 程式人生 > >Rust入坑指南:有條不紊

Rust入坑指南:有條不紊

隨著我們的坑越來越多,越來越大,我們必須要對各種坑進行管理了。Rust為我們提供了一套坑務管理系統,方便大家有條不紊的尋找、管理、填埋自己的各種坑。

Rust提供給我們一些管理程式碼的特性:

  • Packages:Cargo的一個特性,幫助你進行構建、測試和共享crates
  • Crates:生成庫或可執行檔案的模組樹
  • Modules和use:用於控制程式碼組織、範圍和隱私路徑
  • Paths:struct、function和module的命名方法

下面我們來具體看一下這些特性是如何幫助我們組織程式碼的。

Packages和Crates

package可以理解為一個專案,而crate可以理解為一個程式碼庫。crate可以供多個專案使用。那我們的專案中package和crate是怎麼定義的呢?

之前我們總是通過IDEA來新建專案,今天我們換個方法,在命令列中使用cargo命令來建立。

$ cargo new hello-world
     Created binary (application) `hello-world` package
$ ls hello-world
Cargo.toml
src
$ ls hello-world/src
main.rs

可以看到,我們使用cargo建立專案後,只有兩個檔案,Cargo.toml和src目錄下的main.rs。

Cargo.toml是管理專案依賴的檔案,每個Cargo.toml定義一個package。main.rs檔案的存在表示package中包含一個二進位制crate,它是二進位制crate的入口檔案,crate的名稱和package相同。如果src目錄下存在lib.rs檔案,說明package中包含一個和package名稱相同的庫crate。

一個package可以包含多個二進位制crate,它們由src/lib目錄下的檔案定義。如果你的專案想引用他人的crate,可以在Cargo.toml檔案中增加依賴。每個crate都有自己的名稱空間,因此如果你引入了一個crate裡面定義了一個名為hello的函式,你仍然可以在自己的crate中再定義一個名為hello的函式。

Module

Module幫助我們在crate中組織程式碼,同時Module也是封裝程式碼的重要工具。接下來還是通過一個栗子來詳細瞭解Module。

前面我們說過,庫crate定義在src/lib.rs檔案中。這裡首先建立一個包含了庫crate的package:

cargo new --lib restaurant

然後在src中定義一些module和函式。

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() {}
    }
}

可以看到我們使用關鍵字mod來定義Module,Module中可以繼續定義Module或函式。這樣我們就可以比較方便的把相關的函式放到一個Module中,併為Module命名,提高程式碼的可讀性。另外Module中還可以定義struct和列舉。由於Module中可以巢狀定義子Module,最終我們定義出來的程式碼類似一個樹形。

那麼如何訪問Module中的函式呢?這就要提到Path了。這部分比較好理解,Module樹相當於系統檔案目錄,而Path則是目錄的路徑。

Path

這裡的路徑和系統檔案路徑一樣,都分為相對路徑和絕對路徑兩種。其中絕對路徑必須以crate開頭,因為它程式碼整個Module樹的根節點。路徑之間使用的是雙冒號來表示引用。

現在我來嘗試在一個函式中呼叫add_to_waitlist函式:

可以看到這裡不管用絕對路徑還是相對路徑都報錯了,錯誤資訊是模組hosting和函式add_to_waitlist是私有(private)的。我們先暫時放下這個錯誤,根據這裡的錯誤提示,我們知道了當我們定義一個module時,預設情況下是私有的,我們可以通過這種方法來封裝一些程式碼的實現細節。

OK,回到剛才的問題,那我們怎麼才能解決這個錯誤呢?地球人都知道應該把對應的模組與函式公開出來。Rust中標識模組或函式為公有的關鍵字是pub

我們用pub關鍵字來把對應的模組和函式公開

這樣我們就可以在module外來呼叫module內的函數了。

Rust中的私有規則

現在我們再回過頭來看Rust中的一些私有規則,如果你試驗了上面的例子,也許會有一些發現。

Rust中私有規則適用於所有項(函式、方法、結構體、列舉、模組和常量),它們預設都是私有的。父模組中的項不能訪問子模組中的私有項,而子模組中的項可以訪問其祖輩(父模組及以上)中的項。

Struct和Enum的私有性

Struct和Enum的私有性略有不同,對於Struct來講,我可以只將其中的某些欄位設定為公有的,其他欄位可以仍然保持私有。

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("peaches"),
            }
        }
    }
}

pub fn eat_at_restaurant() {
    // Order a breakfast in the summer with Rye toast
    let mut meal = back_of_house::Breakfast::summer("Rye");
    // Change our mind about what bread we'd like
    meal.toast = String::from("Wheat");
    println!("I'd like {} toast please", meal.toast);
}

而對於Enum,如果一個Enum是公有的,那麼它的所有值都是公有的,因為私有的值沒有意義。

相對路徑和絕對路徑的選擇

這種選擇不存在正確與否,只有是否合適。因此這裡我們只是舉例說明一些合適的情況。

我們仍以上述程式碼為例,如果我們可以預見到以後需要把front_of_house模組和eat_at_restaurant函式移動到一個新的名為customer_experience的模組中,就應該使用相對路徑,這樣我們就對其進行調整。

類似的,如果我們需要把eat_at_restaurant函式移動到dining模組中,那麼我們選擇絕對路徑的話就不需要做調整。

綜上,我們需要對程式碼的優化方向有一些前瞻性,並以此來判斷需要使用相對路徑還是絕對路徑。

相對路徑除了以當前模組開頭外,還可以以super開頭。它表示的是父級模組,類似於檔案系統中的兩個點(..)。

use關鍵字

絕對路徑和相對路徑可以幫助我們找到指定的函式,但用起來也非常的麻煩,每次都要寫一大長串路徑。還好Rust為我們提供了use關鍵字。在很多語言中都有import關鍵字,這裡的use就有些類似於import。不過Rust會提供更加豐富的用法。

use最基本的用法就是引入一個路徑。我們就可以更加方便的使用這個路徑下的一些方法:

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
}

這個路徑可以是絕對路徑,也可以是相對路徑,但如果是相對路徑,就必須要以self開頭。上面的例子可以寫成:

use self::front_of_house::hosting;

這與我們前面講的相對路徑似乎有些矛盾,Rust官方說會在之後的版本處理這個問題。

use還可以更進一步,直接指向具體的函式或Struct或Enum。但習慣上我們使用函式時,use後面使用的是路徑,這樣可以在呼叫函式時知道它屬於哪個模組;而在使用Struct/Enum時,則具體指向它們。當然,這只是官方建議的程式設計習慣,你也可以有自己的習慣,不過最好還是按照官方推薦或者是專案約定的規範比較好。

對於同一路徑下的某些子模組,在引入時可以合併為一行,例如:

use std::io;
use std::cmp::Ordering;
// 等價於
use std::{cmp::Ordering, io};

有時我們還會遇到引用不同包下相同名稱Struct的情況,這時有兩種解決辦法,一是不指定到具體的Struct,在使用時加上不同的路徑;二是使用as關鍵字,為Struct起一個別名。

方法一:

use std::fmt;
use std::io;

fn function1() -> fmt::Result {
    // --snip--
}

fn function2() -> io::Result<()> {
    // --snip--
}

方法二:

use std::fmt::Result;
use std::io::Result as IoResult;

fn function1() -> Result {
    // --snip--
}

fn function2() -> IoResult<()> {
    // --snip--
}

如果要匯入某個路徑下的全部模組或函式,可以使用*來表示。當然我是非常不建議使用這種方法的,因為匯入全部的話,如果出現名稱衝突就會很難排查問題。

對於外部的依賴包,我們需要先在Cargo.toml檔案中新增依賴,然後就可以在程式碼中使用use來引入依賴庫中的路徑。Rust提供了一些標準庫,即std下的庫。在使用這些標準庫時是不需要新增依賴的。

有些同學看到這裡可能要開始抱怨了,說好了介紹怎麼拆分檔案,到現在還是在一個檔案裡玩,這不是欺騙讀者嘛。

別急,這就開始拆分。

開始拆分

我們拿剛才的一段程式碼為例

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() {}
    }
}

首先我們可以把front_of_house模組下的內容拆分出去,需要在src目錄下新建一個front_of_house.rs檔案,然後把front_of_house模組下的內容寫到檔案中。lib.rs檔案中,只需要宣告front_of_house模組即可,不需要具體的定義。宣告模組時,將花括號即內容改為分號就可以了。

mod front_of_house;

然後我們可以繼續拆分front_of_house模組下的hosting模組和serving模組,這時需要新建一個名為front_of_house的檔案件,在該資料夾下放置要拆分的模組的同名檔案,把模組定義的內容寫在檔案中,front_of_house.rs檔案同樣只保留宣告即可。

拆分後的檔案目錄如圖

本文主要講了Rust中Package、Crate、Module、Path的概念和用法,有了這些基礎,我們後面才有可能開發一些比較大的專案。

ps:本文的程式碼示例均來自the book