嗨嗨!大家好!歡迎來到 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();
}
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 的模組系統:
基本概念:
mod
、pub
、use
關鍵字檔案組織:
mod.rs
組織子模組進階技巧:
pub(crate)
與 pub(super)
的使用明天我們將進入第四週的第一天:專案起手式:規劃我們的部落格後端 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;
這個挑戰將讓你綜合運用今天學到的所有模組系統知識,設計出一個可維護、可擴展的大型專案結構!
我們明天見!