Last active
March 4, 2026 15:06
-
-
Save jiayun/00cabf3f20ea756f196c01ea3751471d to your computer and use it in GitHub Desktop.
Ch27 Development Practices — 完整範例
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| //! # 開發實務工具箱 | |
| //! | |
| //! 本模組示範 Rust 文件註解(rustdoc)的最佳實務, | |
| //! 包含函式文件、範例、以及 doc test。 | |
| /// 計算兩個整數的最大公因數(GCD)。 | |
| /// | |
| /// 使用歐幾里得演算法,時間複雜度為 O(log(min(a, b)))。 | |
| /// | |
| /// # Arguments | |
| /// | |
| /// * `a` - 第一個非負整數 | |
| /// * `b` - 第二個非負整數 | |
| /// | |
| /// # Examples | |
| /// | |
| /// ``` | |
| /// use ch27_development_practices::gcd; | |
| /// | |
| /// assert_eq!(gcd(12, 8), 4); | |
| /// assert_eq!(gcd(7, 0), 7); | |
| /// assert_eq!(gcd(0, 5), 5); | |
| /// assert_eq!(gcd(0, 0), 0); | |
| /// ``` | |
| /// | |
| /// # Panics | |
| /// | |
| /// 此函式不會 panic。 | |
| pub fn gcd(mut a: u64, mut b: u64) -> u64 { | |
| while b != 0 { | |
| let temp = b; | |
| b = a % b; | |
| a = temp; | |
| } | |
| a | |
| } | |
| /// 語意版本號(SemVer)。 | |
| /// | |
| /// 遵循 [Semantic Versioning 2.0.0](https://semver.org/) 規範, | |
| /// 格式為 `MAJOR.MINOR.PATCH`。 | |
| /// | |
| /// # Examples | |
| /// | |
| /// ``` | |
| /// use ch27_development_practices::SemVer; | |
| /// | |
| /// let v = SemVer::parse("1.42.3").unwrap(); | |
| /// assert_eq!(v.major, 1); | |
| /// assert_eq!(v.minor, 42); | |
| /// assert_eq!(v.patch, 3); | |
| /// assert_eq!(v.to_string(), "1.42.3"); | |
| /// ``` | |
| #[derive(Debug, Clone, PartialEq, Eq)] | |
| pub struct SemVer { | |
| /// 主版本號 — 不相容的 API 變更 | |
| pub major: u64, | |
| /// 次版本號 — 向下相容的功能新增 | |
| pub minor: u64, | |
| /// 修訂版本號 — 向下相容的錯誤修正 | |
| pub patch: u64, | |
| } | |
| impl SemVer { | |
| /// 建立新的語意版本號。 | |
| /// | |
| /// # Examples | |
| /// | |
| /// ``` | |
| /// use ch27_development_practices::SemVer; | |
| /// | |
| /// let v = SemVer::new(2, 0, 1); | |
| /// assert_eq!(v.to_string(), "2.0.1"); | |
| /// ``` | |
| pub fn new(major: u64, minor: u64, patch: u64) -> Self { | |
| Self { | |
| major, | |
| minor, | |
| patch, | |
| } | |
| } | |
| /// 從字串解析語意版本號。 | |
| /// | |
| /// 格式必須為 `MAJOR.MINOR.PATCH`,每個部分都是非負整數。 | |
| /// | |
| /// # Examples | |
| /// | |
| /// ``` | |
| /// use ch27_development_practices::SemVer; | |
| /// | |
| /// assert_eq!( | |
| /// SemVer::parse("1.0.0"), | |
| /// Ok(SemVer::new(1, 0, 0)) | |
| /// ); | |
| /// | |
| /// assert!(SemVer::parse("not-a-version").is_err()); | |
| /// assert!(SemVer::parse("1.2").is_err()); | |
| /// ``` | |
| pub fn parse(s: &str) -> Result<Self, String> { | |
| let parts: Vec<&str> = s.split('.').collect(); | |
| if parts.len() != 3 { | |
| return Err(format!("expected 3 components, got {}", parts.len())); | |
| } | |
| let major = parts[0] | |
| .parse::<u64>() | |
| .map_err(|e| format!("invalid major: {e}"))?; | |
| let minor = parts[1] | |
| .parse::<u64>() | |
| .map_err(|e| format!("invalid minor: {e}"))?; | |
| let patch = parts[2] | |
| .parse::<u64>() | |
| .map_err(|e| format!("invalid patch: {e}"))?; | |
| Ok(Self::new(major, minor, patch)) | |
| } | |
| /// 判斷是否為正式發布版本(major >= 1)。 | |
| /// | |
| /// 根據 SemVer 規範,`0.x.y` 是初始開發階段, | |
| /// 公開 API 不保證穩定。 | |
| /// | |
| /// # Examples | |
| /// | |
| /// ``` | |
| /// use ch27_development_practices::SemVer; | |
| /// | |
| /// assert!(!SemVer::new(0, 9, 0).is_stable()); | |
| /// assert!(SemVer::new(1, 0, 0).is_stable()); | |
| /// ``` | |
| pub fn is_stable(&self) -> bool { | |
| self.major >= 1 | |
| } | |
| /// 判斷此版本是否與另一個版本 API 相容。 | |
| /// | |
| /// 根據 SemVer: | |
| /// - major 為 0 時,只有 minor 相同才相容 | |
| /// - major >= 1 時,major 相同即相容 | |
| /// | |
| /// # Examples | |
| /// | |
| /// ``` | |
| /// use ch27_development_practices::SemVer; | |
| /// | |
| /// let v1 = SemVer::new(1, 2, 0); | |
| /// let v2 = SemVer::new(1, 5, 3); | |
| /// assert!(v1.is_compatible_with(&v2)); | |
| /// | |
| /// let v3 = SemVer::new(2, 0, 0); | |
| /// assert!(!v1.is_compatible_with(&v3)); | |
| /// | |
| /// // 0.x 系列:minor 不同就不相容 | |
| /// let pre1 = SemVer::new(0, 1, 0); | |
| /// let pre2 = SemVer::new(0, 2, 0); | |
| /// assert!(!pre1.is_compatible_with(&pre2)); | |
| /// ``` | |
| pub fn is_compatible_with(&self, other: &Self) -> bool { | |
| if self.major == 0 && other.major == 0 { | |
| self.minor == other.minor | |
| } else { | |
| self.major == other.major | |
| } | |
| } | |
| } | |
| impl std::fmt::Display for SemVer { | |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
| write!(f, "{}.{}.{}", self.major, self.minor, self.patch) | |
| } | |
| } | |
| impl PartialOrd for SemVer { | |
| fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { | |
| Some(self.cmp(other)) | |
| } | |
| } | |
| impl Ord for SemVer { | |
| fn cmp(&self, other: &Self) -> std::cmp::Ordering { | |
| self.major | |
| .cmp(&other.major) | |
| .then(self.minor.cmp(&other.minor)) | |
| .then(self.patch.cmp(&other.patch)) | |
| } | |
| } | |
| /// 檢查字串是否為有效的 Rust 識別碼。 | |
| /// | |
| /// 有效的識別碼以字母或底線開頭,後接字母、數字或底線。 | |
| /// | |
| /// # Examples | |
| /// | |
| /// ``` | |
| /// use ch27_development_practices::is_valid_identifier; | |
| /// | |
| /// assert!(is_valid_identifier("hello_world")); | |
| /// assert!(is_valid_identifier("_private")); | |
| /// assert!(!is_valid_identifier("123abc")); | |
| /// assert!(!is_valid_identifier("")); | |
| /// ``` | |
| pub fn is_valid_identifier(s: &str) -> bool { | |
| let mut chars = s.chars(); | |
| match chars.next() { | |
| Some(c) if c.is_alphabetic() || c == '_' => chars.all(|c| c.is_alphanumeric() || c == '_'), | |
| _ => false, | |
| } | |
| } | |
| /// 將 snake_case 轉換為 CamelCase。 | |
| /// | |
| /// # Examples | |
| /// | |
| /// ``` | |
| /// use ch27_development_practices::to_camel_case; | |
| /// | |
| /// assert_eq!(to_camel_case("hello_world"), "HelloWorld"); | |
| /// assert_eq!(to_camel_case("my_struct_name"), "MyStructName"); | |
| /// assert_eq!(to_camel_case("single"), "Single"); | |
| /// ``` | |
| pub fn to_camel_case(s: &str) -> String { | |
| s.split('_') | |
| .map(|word| { | |
| let mut chars = word.chars(); | |
| match chars.next() { | |
| None => String::new(), | |
| Some(c) => { | |
| let upper: String = c.to_uppercase().collect(); | |
| upper + &chars.collect::<String>() | |
| } | |
| } | |
| }) | |
| .collect() | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use super::*; | |
| #[test] | |
| fn test_gcd_basic() { | |
| assert_eq!(gcd(12, 8), 4); | |
| assert_eq!(gcd(54, 24), 6); | |
| } | |
| #[test] | |
| fn test_gcd_with_zero() { | |
| assert_eq!(gcd(0, 5), 5); | |
| assert_eq!(gcd(7, 0), 7); | |
| assert_eq!(gcd(0, 0), 0); | |
| } | |
| #[test] | |
| fn test_gcd_coprime() { | |
| assert_eq!(gcd(13, 7), 1); | |
| } | |
| #[test] | |
| fn test_semver_parse() { | |
| let v = SemVer::parse("1.42.3").unwrap(); | |
| assert_eq!(v, SemVer::new(1, 42, 3)); | |
| } | |
| #[test] | |
| fn test_semver_parse_invalid() { | |
| assert!(SemVer::parse("1.2").is_err()); | |
| assert!(SemVer::parse("abc").is_err()); | |
| assert!(SemVer::parse("1.2.3.4").is_err()); | |
| } | |
| #[test] | |
| fn test_semver_ordering() { | |
| let v1 = SemVer::new(1, 0, 0); | |
| let v2 = SemVer::new(1, 1, 0); | |
| let v3 = SemVer::new(2, 0, 0); | |
| assert!(v1 < v2); | |
| assert!(v2 < v3); | |
| } | |
| #[test] | |
| fn test_semver_compatibility() { | |
| let v1 = SemVer::new(1, 2, 0); | |
| let v2 = SemVer::new(1, 9, 5); | |
| assert!(v1.is_compatible_with(&v2)); | |
| let v3 = SemVer::new(2, 0, 0); | |
| assert!(!v1.is_compatible_with(&v3)); | |
| } | |
| #[test] | |
| fn test_valid_identifier() { | |
| assert!(is_valid_identifier("foo")); | |
| assert!(is_valid_identifier("_bar")); | |
| assert!(!is_valid_identifier("1bad")); | |
| assert!(!is_valid_identifier("")); | |
| } | |
| #[test] | |
| fn test_to_camel_case() { | |
| assert_eq!(to_camel_case("hello_world"), "HelloWorld"); | |
| assert_eq!(to_camel_case("single"), "Single"); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /// 示範 Rust 開發實務中的文件註解與工具鏈使用。 | |
| /// | |
| /// 本程式展示: | |
| /// - rustdoc 文件註解格式(`///` 與 `//!`) | |
| /// - doc test 內嵌於文件中 | |
| /// - clippy 友善的程式碼風格 | |
| /// - 模組化設計 | |
| use ch27_development_practices::{SemVer, gcd, is_valid_identifier, to_camel_case}; | |
| /// 展示格式化設定的效果。 | |
| /// | |
| /// rustfmt 會自動處理: | |
| /// - 縮排(4 個空格) | |
| /// - 行寬限制(預設 100) | |
| /// - 尾隨逗號 | |
| fn demo_formatting() { | |
| println!("=== rustfmt 格式化 ==="); | |
| // rustfmt 會自動對齊、排版這些結構 | |
| let config_options = vec![ | |
| ("edition", "2024"), | |
| ("max_width", "100"), | |
| ("tab_spaces", "4"), | |
| ("use_field_init_shorthand", "true"), | |
| ]; | |
| for (key, value) in &config_options { | |
| println!(" {key} = {value}"); | |
| } | |
| } | |
| /// 展示 clippy 友善的程式碼寫法。 | |
| /// | |
| /// 以下每段程式碼都遵循 clippy 建議的最佳實務。 | |
| fn demo_clippy_friendly() { | |
| println!("\n=== clippy 友善寫法 ==="); | |
| // Good: 用 is_empty() 而非 len() == 0 | |
| let items: Vec<i32> = vec![]; | |
| if items.is_empty() { | |
| println!(" 集合是空的(用 is_empty() 而非 len() == 0)"); | |
| } | |
| // Good: 用 if let 處理單一模式 | |
| let maybe_value: Option<i32> = Some(42); | |
| if let Some(v) = maybe_value { | |
| println!(" 取得值:{v}(用 if let 而非 match 單一分支)"); | |
| } | |
| // Good: 用 unwrap_or_default 而非 match None => default | |
| fn get_nickname() -> Option<String> { | |
| None | |
| } | |
| let fallback: String = get_nickname().unwrap_or_default(); | |
| println!(" 預設值:'{fallback}'(用 unwrap_or_default)"); | |
| // Good: 用 iter 方法鏈而非手動迴圈 | |
| let numbers = [1, 2, 3, 4, 5]; | |
| let sum: i32 = numbers.iter().sum(); | |
| println!(" 總和:{sum}(用 .iter().sum() 而非手動迴圈)"); | |
| // Good: 用 to_string() 而非 format!("{}", x) | |
| let n = 42; | |
| let s = n.to_string(); | |
| println!(" 數字轉字串:{s}(用 .to_string() 而非 format!)"); | |
| // Good: 用 contains 而非手動搜尋 | |
| let text = "hello, world"; | |
| if text.contains("world") { | |
| println!(" 找到子字串(用 .contains() 方法)"); | |
| } | |
| } | |
| /// 展示文件註解的功能。 | |
| fn demo_rustdoc() { | |
| println!("\n=== rustdoc 文件註解 ==="); | |
| // 使用 lib.rs 中有完整文件的函式 | |
| let result = gcd(48, 18); | |
| println!(" gcd(48, 18) = {result}"); | |
| let result = gcd(100, 75); | |
| println!(" gcd(100, 75) = {result}"); | |
| // 識別碼驗證 | |
| println!( | |
| " is_valid_identifier(\"hello\") = {}", | |
| is_valid_identifier("hello") | |
| ); | |
| println!( | |
| " is_valid_identifier(\"123\") = {}", | |
| is_valid_identifier("123") | |
| ); | |
| // 命名轉換 | |
| println!( | |
| " to_camel_case(\"my_struct\") = {}", | |
| to_camel_case("my_struct") | |
| ); | |
| } | |
| /// 展示語意版本號操作。 | |
| fn demo_semver() { | |
| println!("\n=== 語意版本號(SemVer) ==="); | |
| let versions = ["0.1.0", "1.0.0", "1.4.2", "2.0.0-beta"]; | |
| for v_str in &versions { | |
| match SemVer::parse(v_str) { | |
| Ok(v) => { | |
| println!( | |
| " {v_str} → major={}, minor={}, patch={}, stable={}", | |
| v.major, | |
| v.minor, | |
| v.patch, | |
| v.is_stable() | |
| ); | |
| } | |
| Err(e) => { | |
| println!(" {v_str} → 解析失敗:{e}"); | |
| } | |
| } | |
| } | |
| // 版本比較 | |
| let v1 = SemVer::new(1, 2, 0); | |
| let v2 = SemVer::new(1, 5, 3); | |
| let v3 = SemVer::new(2, 0, 0); | |
| println!("\n 版本相容性:"); | |
| println!(" {v1} 與 {v2} 相容?{}", v1.is_compatible_with(&v2)); | |
| println!(" {v1} 與 {v3} 相容?{}", v1.is_compatible_with(&v3)); | |
| // 版本排序 | |
| let mut versions = vec![ | |
| SemVer::new(2, 1, 0), | |
| SemVer::new(1, 0, 0), | |
| SemVer::new(1, 9, 5), | |
| SemVer::new(0, 1, 0), | |
| ]; | |
| versions.sort(); | |
| println!("\n 排序後:"); | |
| for v in &versions { | |
| println!(" {v}"); | |
| } | |
| } | |
| /// 展示條件編譯。 | |
| fn demo_conditional_compilation() { | |
| println!("\n=== 條件編譯 ==="); | |
| // cfg! 巨集在執行期回傳 bool | |
| println!(" debug_assertions = {}", cfg!(debug_assertions)); | |
| println!(" target_os = {}", std::env::consts::OS); | |
| println!(" target_arch = {}", std::env::consts::ARCH); | |
| // #[cfg] 屬性在編譯期決定是否包含程式碼 | |
| #[cfg(debug_assertions)] | |
| println!(" 這行只在 debug 模式下出現"); | |
| #[cfg(not(debug_assertions))] | |
| println!(" 這行只在 release 模式下出現"); | |
| } | |
| /// 展示測試模式。 | |
| fn demo_testing_patterns() { | |
| println!("\n=== 測試模式 ==="); | |
| println!(" Rust 測試架構:"); | |
| println!(" - 單元測試:#[cfg(test)] mod tests,與程式碼同檔案"); | |
| println!(" - 整合測試:tests/ 目錄,測試公開 API"); | |
| println!(" - doc test:文件中的 ``` 區塊,同時是文件和測試"); | |
| println!(" - cargo test:一次執行所有測試"); | |
| println!(); | |
| println!(" 常用測試指令:"); | |
| println!(" cargo test # 執行所有測試"); | |
| println!(" cargo test -p <package> # 測試特定套件"); | |
| println!(" cargo test --doc # 只跑 doc test"); | |
| println!(" cargo test -- --nocapture # 顯示 println! 輸出"); | |
| println!(" cargo test test_name # 跑名稱匹配的測試"); | |
| } | |
| fn main() { | |
| println!("Ch27: Rust 開發實務\n"); | |
| demo_formatting(); | |
| demo_clippy_friendly(); | |
| demo_rustdoc(); | |
| demo_semver(); | |
| demo_conditional_compilation(); | |
| demo_testing_patterns(); | |
| println!("\n=== GitHub Actions CI 範例 ==="); | |
| println!(" 典型 Rust CI pipeline:"); | |
| println!(" 1. push / PR 觸發"); | |
| println!(" 2. cargo fmt -- --check (格式檢查)"); | |
| println!(" 3. cargo clippy -- -D warnings (lint 檢查)"); | |
| println!(" 4. cargo test (測試)"); | |
| println!(" 5. cargo build --release (建置)"); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment