首頁 > 軟體

Rust如何進行模組化開發技巧分享

2023-01-16 14:02:15

類似es6的模組化,Rust通過package、create、module來實現程式碼的模組化管理

Rust如何進行模組化開發?

Rust的程式碼組織包括:哪些細節可以暴露,哪些細節是私有的,作用域內哪些名稱有效等等。

  • Package(包):Cargo的特性,讓你構建、測試、共用create
  • Create(單元包):一個模組樹,它可以產生一個library或可執行檔案
  • Module(模組)、use:讓你控制程式碼的組織、作用域、私有路徑
  • Path(路徑):為struct、function或module等項命名的方式

Package和Create

create的型別:

  • binary(二進位制create)
  • library(庫create)

其中,關於Create,還有個概念——Create Root:

是原始碼檔案Rust編譯器從這裡開始,組成你的Create的根Module

一個Package:

  • 包含一個Cargo.toml,它描述瞭如何構建這些Crates
  • 只能包含0-1個library create(庫create)
  • 可以包含任意數量的binary create(二進位制create)
  • 但必須至少包含一個create(library或binary)

我們使用cargo新建一個專案

然後會提示: Created binary (application) my-project package,這代表我們建立了一個二進位制的應用程式,名叫my-project的package

我們進入這個資料夾:

我們可以看到src/min.rs檔案,這是我們程式的入口檔案,但是我們在Cargo.toml中並沒有看到相關的設定:

[package]
name = "my-project"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies] 

這是因為cargo有一些慣例

Cargo的慣例

  • src/main.rs是binary create的create root* create的名與package名相同如果我們還有一個這個檔案:src/lib.rs,那麼:
  • 表明package包含一個library create
  • 它是library create的create root
  • create的名與package名相同

Cargo將會把create root檔案交給rustc(rust編譯器)來構建library或者binary

一個Package可以同時包含src/main.rs和src/lib.rs

一個Package也可以有多個binary create:

  • 檔案放在src/bin,放在這裡的每個檔案都是單獨的binary create

Create的作用

將相關功能組合到一個作用域內,便於在專案間進行共用。

同時,這也能防止命名衝突,例如rand create,存取它的功能需要通過它的名字:rand

定義module來控制作用域和私有性

Module:

  • 在一個create內,將程式碼進行分組
  • 增加可讀性,易於複用
  • 控制專案(item)的私有性。public,private

建立module:

  • mod關鍵字
  • 可巢狀
  • 可包含其他項(struct、enum、常數、trait、函數等)的定義
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() {}}
} 

src/main.rs 和 src/lib.rs 叫做create roots:

  • 這兩個檔案(任意一個)的內容形成了名為create的模組,位於整個模組樹的根部
  • 整個模組樹在隱式的模組下

路徑Path

路徑的作用是為了在rust的模組中找到某個條目

路徑的兩種形式:

  • 絕對路徑:從create root開始,使用create名或字面值create
  • 相對路徑:從當前模組開始,使用self(本身),super(上一級)或當前模組的識別符號

路徑至少由一個識別符號組成,識別符號之間使用::

舉個例子(下面這段程式將報錯,我們將在後面講到如何解決):

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

pub fn eat_at_restaurant() {crate::front_of_house::hosting::add_to_waitlist();//絕對路徑front_of_house::hosting::add_to_waitlist();//相對路徑
} 

那麼為什麼會報錯呢?

我們檢視報錯的原因:module hosting is private,編譯器告訴我們,hosting這個module是私有的。至此,為了解決這個問題,我們應該去了解一下私有邊界

私有邊界(private boundary)

  • 模組不僅可以組織程式碼,還可以定義私有邊界
  • 如果把函數或struct等設為私有,可以將它放到某個模組中。
  • rust中所有的條目(函數,方法,struct,enum,模組,常數)預設情況下是私有的
  • 父級模組無法存取子模組中的私有條目
  • 但是在子模組中可以使用所有祖先模組中的條目

為什麼rust預設這些條目是私有的呢?因為rust希望能夠隱藏內部的實現細節,這樣就會讓開發者明確知道:更改哪些內部程式碼的時候,不會破壞外部的程式碼。同時,我們可以使用pub關鍵字將其宣告為公共的。

pub關鍵字

rust預設這些條目為私有的,我們可以使用pub關鍵字來將某些條目標記為公共的。

我們將hosting宣告pub,add_to_waitlist這個function也要宣告pub

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

pub fn eat_at_restaurant() {crate::front_of_house::hosting::add_to_waitlist();//絕對路徑front_of_house::hosting::add_to_waitlist();//相對路徑
} 

為什麼front_of_house這個mod不需要新增pub呢?因為它們是同級的。

super關鍵字

super:用來存取父級模組路徑中的內容,類似檔案系統中的..

fn serve_order() {}
mod front_of_house {fn fix_incorrect_order() {cook_order();super::serve_order();}fn cook_order() {}
} 

pub struct

宣告一個公共的struct就是將pub放在struct前:

mod back_of_house {pub struct Breakfast {}
} 

宣告了一個公共的struct後:

  • struct是公共的
  • struct的欄位預設是私有的

而我們想讓struct中的欄位為公有的必須在前面加上pub

mod back_of_house {pub struct Breakfast {pub toast: String,//公有的seasonal_fruit: String, //私有的}
} 

也就是說:struct的欄位需要單獨設定pub來變成公有

我們看一個例子:

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() {let mut meal = back_of_house::Breakfast::summer("Rye");meal.toast = String::from("Wheat");println!("I'd like {} toast please", meal.toast);meal.seasonal_fruit = String::from("blueberries");//報錯:field `seasonal_fruit` is private
} 

pub enum

宣告一個公共的enum就是將pub放在enum前:

mod back_of_house {pub enum Appetizer {}
} 

我們宣告了一個公共的enum後:

  • enum是公共的
  • enum的變體也都是公共的
mod back_of_house {pub enum Appetizer {Soup,//公共的Salad, //公共的}
} 

為什麼呢?因為列舉裡面只有變體,只有變體是公共的這個列舉才有用。而struct中某些部分為私有的也不影響struct的使用,所以rust規定公共的struct中的欄位預設為私有的。

Use關鍵字

我們可以使用use關鍵字將路徑匯入到作用域內,而我們引入的東西也任然遵循私有性規則(公共的引入的才能用)

mod front_of_house {pub mod hosting {pub fn add_to_waitlist() {}fn some_function() {}//私有的,使用use匯入後,外部依然不能呼叫這個函數}
}

use crate::front_of_house::hosting;
// 相當於mod hosting {}

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

使用use來指定相對路徑(和使用條目時的規則相同):

use front_of_house::hosting; 

我們可以注意到我們呼叫的add_to_waitlist是匯入的hostingmod下的,那我們可不可以直接匯入function呢?

當然是可以的(不過並不推薦直接匯入方法):

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

use crate::front_of_house::hosting::add_to_waitlist;
// 相對於mod hosting {}

pub fn eat_at_restaurant() {add_to_waitlist();
} 

use的習慣用法

當我們直接匯入方法時,我們有可能就搞不清楚是從其他模組匯入的還是在這個作用域下宣告的。

所以,通常情況下,我們匯入的通常為父級模組。

//...
use crate::front_of_house::hosting;

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

不過,struct,enum,其他:指定完整路徑(指定到本身)

use std::collections::HashMap;
fn main() {let mut map = HashMap::new();map.insert(1, 2);
} 

但是同名的條目,我們在引入時需指定父級模組(比如下面的例子,兩個型別都叫Result)

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

fn f1() -> fmt::Result {//...
}

fn f2() -> io::Result {//...
}
//... 

as關鍵字

關於上面同名的問題,還有另一種解決方法:使用as關鍵字

as關鍵字可以為引入的路徑指定原生的別名

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

fn f1() -> Result {//...
}

fn f2() -> IoResult {//...
} 

使用 pub use 重新匯出名稱

使用 use 將路徑(名稱)匯入到作用域內後,該名稱在此作用域內是私有的,外部的模組是沒辦法存取use匯入的模組的。

由前面pub的作用可知,類似pub fn、pub mod,我們可以使用pub use來匯入,相當於它匯入了這個內容,然後又將它匯出了。

(當我們使用pub use時會發現沒有警告:“匯入了但沒有使用”,因為它同時也匯出了,也被視作使用了這個匯入的內容)

匯入外部包

我們通過在Cargo.toml中的[dependencies]新增依賴:

# ...
[dependencies]
rand = "^0.8.5" 

出現:Blocking waiting for file lock on package cache

刪除User/.cargo資料夾中的.package-cache檔案。重新執行cargo build下載依賴。

很多時候我們的下載速度很慢,我們可以將下載源換到國內,在使用者資料夾下的.cargo資料夾中新增 config 檔案,寫入以下內容:

[source.crates-io]
registry = "https://github.com/rust-lang/crates.io-index"
replace-with = 'ustc'
[source.ustc]
registry = "git://mirrors.ustc.edu.cn/crates.io-index"
# 如果所處的環境中不允許使用 git 協定,可以把上面的地址改為
# registry = "https://mirrors.ustc.edu.cn/crates.io-index"
#[http]
#check-revoke = false 

這時候cargo build就會很快了。

我們這樣匯入:

use rand::Rng; 

另外:標準庫也被當做外部包,需要匯入,並且:

  • 我們不需要修改Cargo.toml來新增依賴
  • 需要使用use將std的特定條目匯入到當前作用域 use多次匯入(巢狀匯入)
use std::{ascii, io};
//相當於:use std::ascii;
// use std::io; 

這樣的匯入該如何簡寫呢?

use std::io;
use std::io::Chain; 

可以使用self

use std::io::{self, Chain}; 

如何將模組放入其他檔案?

假如我們的src/lib.rs中的內容是這樣:

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

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

我們可以在lib.rs同級目錄下新建front_of_house.rs,然後將模組內容寫在檔案中:

front_of_house.rs

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

lib.rs

mod front_of_house;
//... 

如果我們想將hosting模組的內容單獨存放呢?

我們需要新建一個front_of_house資料夾,並新建hosting.rs檔案

hosting.rs

pub fn add_to_waitlist() {} 

front_of_house.rs

pub mod hosting; 

lib.rs

mod front_of_house;//... 

原來的檔案內容:

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

隨著模組逐漸變大,這項功能將能夠幫助我們更好的管理程式碼

到此這篇關於Rust如何進行模組化開發的文章就介紹到這了,更多相關Rust模組化開發內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com