iT邦幫忙

2025 iThome 鐵人賽

DAY 18
0
Rust

大家一起跟Rust當好朋友吧!系列 第 18

Day 18: 模組系統 (Module System):整理你的專案

  • 分享至 

  • xImage
  •  

嗨嗨!大家好!歡迎來到 Rust 三十天挑戰的第十八天!

經過前面的學習,我們已經深入探索了 Rust 的核心概念、錯誤處理、泛型與特徵,以及非同步程式設計。今天我們要來學習一個在實際專案開發中至關重要的主題:模組系統 (Module System)

隨著程式專案規模的增長,將所有程式碼都寫在一個檔案中會變得越來越難以維護。我們需要一套系統來組織程式碼、控制可見性、避免命名衝突,並讓程式碼更容易重用和測試。Rust 的模組系統就是為了解決這些問題而設計的。

老實說,剛開始接觸 Rust 的模組系統時,我覺得它比其他語言複雜一些。但隨著專案經驗的累積,我發現這套系統其實設計得非常優雅和強大,它不僅能幫我們組織程式碼,還能在編譯時期就確保模組間的依賴關係正確無誤。

今天就讓我們一起掌握 Rust 模組系統的精髓,為接下來的部落格專案做好準備!

模組系統的基本概念

什麼是模組?

模組(Module)是 Rust 中組織程式碼的基本單位。它可以包含:

  • 函式
  • 結構與列舉
  • 常數
  • 其他模組(子模組)

模組系統的核心概念

1. 模組樹(Module Tree):程式碼被組織成樹狀結構
2. 路徑(Paths):用來參考模組樹中的項目
3. 可見性(Visibility):控制哪些程式碼可以被外部存取
4. use 關鍵字:將路徑引入當前作用域

定義模組

在同一檔案中定義模組

// src/main.rs 或 src/lib.rs

// 定義一個模組
mod utils {
    // 公開函式
    pub fn format_message(msg: &str) -> String {
        format!("[INFO] {}", msg)
    }
    
    // 私有函式(預設)
    fn internal_helper() -> String {
        "這是內部輔助函式".to_string()
    }
    
    // 巢狀模組
    pub mod math {
        pub fn add(a: i32, b: i32) -> i32 {
            a + b
        }
        
        pub fn multiply(a: i32, b: i32) -> i32 {
            a * b
        }
    }
}

// 另一個模組
mod config {
    pub struct Settings {
        pub debug_mode: bool,
        pub port: u16,
    }
    
    impl Settings {
        pub fn new() -> Self {
            Settings {
                debug_mode: false,
                port: 8080,
            }
        }
        
        pub fn enable_debug(&mut self) {
            self.debug_mode = true;
        }
    }
}

fn main() {
    // 使用絕對路徑
    let message = crate::utils::format_message("系統啟動");
    println!("{}", message);
    
    // 使用相對路徑
    let result = utils::math::add(5, 3);
    println!("5 + 3 = {}", result);
    
    // 建立設定
    let mut settings = config::Settings::new();
    settings.enable_debug();
    println!("除錯模式:{}", settings.debug_mode);
}

使用 use 關鍵字簡化路徑

mod utils {
    pub mod math {
        pub fn add(a: i32, b: i32) -> i32 { a + b }
        pub fn subtract(a: i32, b: i32) -> i32 { a - b }
        pub fn multiply(a: i32, b: i32) -> i32 { a * b }
        pub fn divide(a: i32, b: i32) -> Option<i32> {
            if b != 0 { Some(a / b) } else { None }
        }
    }
    
    pub mod string_utils {
        pub fn capitalize(s: &str) -> String {
            let mut chars = s.chars();
            match chars.next() {
                None => String::new(),
                Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
            }
        }
        
        pub fn reverse(s: &str) -> String {
            s.chars().rev().collect()
        }
    }
}

// 引入特定函式
use utils::math::add;
use utils::string_utils::capitalize;

// 引入整個模組
use utils::math;

// 引入多個項目
use utils::string_utils::{capitalize as cap, reverse};

// 引入所有公開項目(通常不建議使用)
// use utils::math::*;

fn main() {
    // 直接使用引入的函式
    let sum = add(10, 20);
    println!("10 + 20 = {}", sum);
    
    let name = capitalize("rust");
    println!("大寫:{}", name);
    
    // 使用引入的模組
    let product = math::multiply(6, 7);
    println!("6 × 7 = {}", product);
    
    // 使用別名
    let capitalized = cap("programming");
    println!("大寫:{}", capitalized);
    
    let reversed = reverse("hello");
    println!("反轉:{}", reversed);
}

檔案系統模組

將模組拆分到不同檔案

隨著專案規模增長,我們需要將模組拆分到不同檔案中:

專案結構:

src/
├── main.rs
├── config.rs
├── utils.rs
└── models/
    ├── mod.rs
    ├── user.rs
    └── post.rs

src/main.rs

// 宣告模組(Rust 會尋找同名的 .rs 檔案)
mod config;
mod utils;
mod models;

use config::Settings;
use models::user::User;
use models::post::Post;
use utils::format_date;

fn main() {
    let settings = Settings::new();
    println!("應用程式設定:{:?}", settings);
    
    let user = User::new("Alice".to_string(), "alice@example.com".to_string());
    println!("用戶:{:?}", user);
    
    let post = Post::new(
        "Rust 模組系統".to_string(),
        "學習 Rust 的模組系統...".to_string(),
        user.id(),
    );
    println!("文章:{:?}", post);
    
    let formatted_date = format_date();
    println!("今日日期:{}", formatted_date);
}

src/config.rs

#[derive(Debug)]
pub struct Settings {
    pub database_url: String,
    pub port: u16,
    pub debug_mode: bool,
}

impl Settings {
    pub fn new() -> Self {
        Settings {
            database_url: "sqlite://blog.db".to_string(),
            port: 3000,
            debug_mode: cfg!(debug_assertions),
        }
    }
    
    pub fn from_env() -> Self {
        Settings {
            database_url: std::env::var("DATABASE_URL")
                .unwrap_or_else(|_| "sqlite://blog.db".to_string()),
            port: std::env::var("PORT")
                .unwrap_or_else(|_| "3000".to_string())
                .parse()
                .unwrap_or(3000),
            debug_mode: std::env::var("DEBUG")
                .map(|v| v == "true")
                .unwrap_or(false),
        }
    }
}

impl Default for Settings {
    fn default() -> Self {
        Self::new()
    }
}

src/utils.rs

use chrono::{DateTime, Local};

pub fn format_date() -> String {
    let now: DateTime<Local> = Local::now();
    now.format("%Y-%m-%d %H:%M:%S").to_string()
}

pub fn slugify(title: &str) -> String {
    title
        .to_lowercase()
        .chars()
        .map(|c| if c.is_alphanumeric() || c == ' ' { c } else { ' ' })
        .collect::<String>()
        .split_whitespace()
        .collect::<Vec<&str>>()
        .join("-")
}

pub fn truncate_text(text: &str, max_length: usize) -> String {
    if text.len() <= max_length {
        text.to_string()
    } else {
        format!("{}...", &text[..max_length])
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_slugify() {
        assert_eq!(slugify("Hello World!"), "hello-world");
        assert_eq!(slugify("Rust & Programming"), "rust-programming");
    }
    
    #[test]
    fn test_truncate_text() {
        assert_eq!(truncate_text("Short", 10), "Short");
        assert_eq!(truncate_text("This is a long text", 10), "This is a ...");
    }
}

src/models/mod.rs

// 宣告子模組
pub mod user;
pub mod post;

// 重新匯出常用型別
pub use user::User;
pub use post::Post;

// 共用的型別和常數
pub type UserId = u32;
pub type PostId = u32;

pub const MAX_TITLE_LENGTH: usize = 255;
pub const MAX_CONTENT_LENGTH: usize = 10000;

src/models/user.rs

use super::{UserId, MAX_TITLE_LENGTH};

#[derive(Debug, Clone)]
pub struct User {
    id: UserId,
    username: String,
    email: String,
    created_at: chrono::DateTime<chrono::Utc>,
}

impl User {
    pub fn new(username: String, email: String) -> Self {
        Self {
            id: Self::generate_id(),
            username,
            email,
            created_at: chrono::Utc::now(),
        }
    }
    
    pub fn id(&self) -> UserId {
        self.id
    }
    
    pub fn username(&self) -> &str {
        &self.username
    }
    
    pub fn email(&self) -> &str {
        &self.email
    }
    
    pub fn created_at(&self) -> chrono::DateTime<chrono::Utc> {
        self.created_at
    }
    
    fn generate_id() -> UserId {
        // 簡單的 ID 生成(實際專案中會使用更複雜的邏輯)
        use std::collections::hash_map::DefaultHasher;
        use std::hash::{Hash, Hasher};
        
        let mut hasher = DefaultHasher::new();
        std::time::SystemTime::now().hash(&mut hasher);
        (hasher.finish() % u32::MAX as u64) as u32
    }
}

src/models/post.rs

use super::{PostId, UserId, MAX_TITLE_LENGTH, MAX_CONTENT_LENGTH};
use crate::utils::slugify;

#[derive(Debug, Clone)]
pub struct Post {
    id: PostId,
    title: String,
    content: String,
    slug: String,
    author_id: UserId,
    published: bool,
    created_at: chrono::DateTime<chrono::Utc>,
    updated_at: chrono::DateTime<chrono::Utc>,
}

impl Post {
    pub fn new(title: String, content: String, author_id: UserId) -> Result<Self, String> {
        if title.len() > MAX_TITLE_LENGTH {
            return Err(format!("標題長度不能超過 {} 字元", MAX_TITLE_LENGTH));
        }
        
        if content.len() > MAX_CONTENT_LENGTH {
            return Err(format!("內容長度不能超過 {} 字元", MAX_CONTENT_LENGTH));
        }
        
        let slug = slugify(&title);
        let now = chrono::Utc::now();
        
        Ok(Self {
            id: Self::generate_id(),
            title,
            content,
            slug,
            author_id,
            published: false,
            created_at: now,
            updated_at: now,
        })
    }
    
    pub fn id(&self) -> PostId {
        self.id
    }
    
    pub fn title(&self) -> &str {
        &self.title
    }
    
    pub fn content(&self) -> &str {
        &self.content
    }
    
    pub fn slug(&self) -> &str {
        &self.slug
    }
    
    pub fn author_id(&self) -> UserId {
        self.author_id
    }
    
    pub fn is_published(&self) -> bool {
        self.published
    }
    
    pub fn publish(&mut self) {
        self.published = true;
        self.updated_at = chrono::Utc::now();
    }
    
    pub fn unpublish(&mut self) {
        self.published = false;
        self.updated_at = chrono::Utc::now();
    }
    
    pub fn update_content(&mut self, new_content: String) -> Result<(), String> {
        if new_content.len() > MAX_CONTENT_LENGTH {
            return Err(format!("內容長度不能超過 {} 字元", MAX_CONTENT_LENGTH));
        }
        
        self.content = new_content;
        self.updated_at = chrono::Utc::now();
        Ok(())
    }
    
    fn generate_id() -> PostId {
        use std::collections::hash_map::DefaultHasher;
        use std::hash::{Hash, Hasher};
        
        let mut hasher = DefaultHasher::new();
        std::time::SystemTime::now().hash(&mut hasher);
        (hasher.finish() % u32::MAX as u64) as u32
    }
}

可見性與存取控制

pub 關鍵字的各種用法

mod library {
    // 完全公開
    pub struct PublicStruct {
        pub field: i32,
    }
    
    // 結構公開,但欄位私有
    pub struct MixedStruct {
        pub public_field: i32,
        private_field: String,  // 私有欄位
    }
    
    impl MixedStruct {
        pub fn new(value: i32) -> Self {
            Self {
                public_field: value,
                private_field: "private".to_string(),
            }
        }
        
        // 公開方法提供對私有欄位的存取
        pub fn get_private(&self) -> &str {
            &self.private_field
        }
    }
    
    // 只在當前 crate 內公開
    pub(crate) fn crate_visible_function() {
        println!("只有同一個 crate 可以看到我");
    }
    
    // 只在父模組中公開
    pub(super) fn parent_visible_function() {
        println!("只有父模組可以看到我");
    }
    
    // 完全私有(預設)
    fn private_function() {
        println!("只有當前模組可以看到我");
    }
    
    pub mod sub_module {
        // 可以存取父模組的 pub(super) 項目
        pub fn call_parent() {
            super::parent_visible_function();
        }
    }
}

fn main() {
    let public = library::PublicStruct { field: 42 };
    println!("公開欄位:{}", public.field);
    
    let mixed = library::MixedStruct::new(100);
    println!("公開欄位:{}", mixed.public_field);
    println!("私有欄位(透過方法):{}", mixed.get_private());
    
    // 呼叫 crate 可見的函式
    library::crate_visible_function();
    
    // 呼叫子模組的函式
    library::sub_module::call_parent();
}

重新匯出與組織 API

使用 pub use 重新匯出

// src/lib.rs
mod internal {
    pub mod database {
        pub struct Connection;
        pub fn connect() -> Connection { Connection }
    }
    
    pub mod auth {
        pub struct User;
        pub fn authenticate() -> Result<User, String> { Ok(User) }
    }
    
    pub mod utils {
        pub fn hash_password(password: &str) -> String {
            format!("hashed_{}", password)
        }
    }
}

// 重新匯出,為使用者提供更清潔的 API
pub use internal::database::{Connection, connect};
pub use internal::auth::{User, authenticate};
pub use internal::utils::hash_password;

// 也可以重新匯出並重命名
pub use internal::database::connect as db_connect;

現在使用者可以這樣使用:

// 不需要知道內部模組結構
use my_library::{connect, authenticate, hash_password};

fn main() {
    let conn = connect();
    let user = authenticate().unwrap();
    let hashed = hash_password("secret123");
}

今天的收穫

今天我們深入學習了 Rust 的模組系統:

基本概念

  • ✅ 理解模組樹與路徑系統
  • ✅ 掌握 modpubuse 關鍵字
  • ✅ 學會控制可見性與存取權限

檔案組織

  • ✅ 將模組拆分到不同檔案
  • ✅ 使用 mod.rs 組織子模組
  • ✅ 重新匯出與 API 設計

進階技巧

  • ✅ 條件編譯與特徵閘門
  • pub(crate)pub(super) 的使用
  • ✅ 實際專案的模組結構設計

明天預告

明天我們將進入第四週的第一天:專案起手式:規劃我們的部落格後端 API!我們將正式開始最終專案的開發,運用前面學到的所有知識,從零開始構建一個功能完整的部落格後端服務。

本日挑戰

為了練習今天學到的模組系統,試著完成以下挑戰:

挑戰目標:設計一個簡易圖書館管理系統的模組結構

功能需求

  1. 圖書管理:新增圖書、查詢圖書、更新圖書資訊
  2. 會員系統:註冊新會員、查詢會員資料
  3. 借還書功能:借書、還書、查詢借閱記錄

模組設計要求

  • 合理的檔案結構與模組層次
  • 適當的可見性控制
  • 清晰的 API 重新匯出
  • 基礎的錯誤處理模組
  • 實用的工具函式模組

建議的專案結構

src/
├── main.rs
├── models/
│   ├── mod.rs
│   ├── book.rs
│   └── member.rs
├── services/
│   ├── mod.rs
│   └── library.rs
└── utils/
    ├── mod.rs
    └── validation.rs
// 在 main.rs 中使用重新匯出的 API
use models::{Book, Member};
use services::LibraryService;

這個挑戰將讓你綜合運用今天學到的所有模組系統知識,設計出一個可維護、可擴展的大型專案結構!

我們明天見!


上一篇
Day 17: 非同步程式設計 (Async/Await) 入門 with Tokio
系列文
大家一起跟Rust當好朋友吧!18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言