測試

在我們瞭解模組後,測試正是現在學習的好主題。在 Rust 中測試你的程式碼是非常容易的,因為你可以立刻在你的程式碼旁寫測試。

開始測試最簡單的方法就是在函式上面加上 #[test]。這裡是個簡單的範例:

#![allow(unused)]
fn main() {
#[test]
fn two_is_two() {
    assert_eq!(2, 2);
}
}

但如果你試圖在 Playground 中執行它,它會給出錯誤:error[E0601]: `main` function not found in crate `playground。這是因為你不使用 Run 來進行測試,你要使用的是 Test。另外,你不使用 main() 函式進行測試 - 它們在外面執行。要在 Playground 中執行這個,點選 RUN 旁邊的 ···,然後把它改為 Test。現在如果你點選它,它將會跑測試。(如果你已經安裝了 Rust,你將輸入 cargo test 來做測試)

這裡是輸出內容:

running 1 test
test two_is_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

讓我們把 assert_eq!(2, 2) 改成 assert_eq!(2, 3),看看會有什麼結果。當測試失敗時,你會得到更多的資訊:

running 1 test
test two_is_two ... FAILED

failures:

---- two_is_two stdout ----
thread 'two_is_two' panicked at 'assertion failed: `(left == right)`
  left: `2`,
 right: `3`', src/lib.rs:3:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    two_is_two

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

assert_eq!(left, right) 是 Rust 中測試函式的主要方法。如果它不成功,它將會顯示值的不同:左邊有 2,但右邊有 3。

RUST_BACKTRACE=1 是什麼意思?這是電腦上的設定,可以提供更多關於錯誤的資訊。幸好 Playground 也有:點選 STABLE 旁邊的 ···,然後設定 Backtrace 為 ENABLED。如果你這樣做,它會給你 很多 的資訊:

running 1 test
test two_is_two ... FAILED

failures:

---- two_is_two stdout ----
thread 'two_is_two' panicked at 'assertion failed: 2 == 3', src/lib.rs:3:5
stack backtrace:
   0: backtrace::backtrace::libunwind::trace
             at /cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.46/src/backtrace/libunwind.rs:86
   1: backtrace::backtrace::trace_unsynchronized
             at /cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.46/src/backtrace/mod.rs:66
   2: std::sys_common::backtrace::_print_fmt
             at src/libstd/sys_common/backtrace.rs:78
   3: <std::sys_common::backtrace::_print::DisplayBacktrace as core::fmt::Display>::fmt
             at src/libstd/sys_common/backtrace.rs:59
   4: core::fmt::write
             at src/libcore/fmt/mod.rs:1076
   5: std::io::Write::write_fmt
             at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/libstd/io/mod.rs:1537
   6: std::io::impls::<impl std::io::Write for alloc::boxed::Box<W>>::write_fmt
             at src/libstd/io/impls.rs:176
   7: std::sys_common::backtrace::_print
             at src/libstd/sys_common/backtrace.rs:62
   8: std::sys_common::backtrace::print
             at src/libstd/sys_common/backtrace.rs:49
   9: std::panicking::default_hook::{{closure}}
             at src/libstd/panicking.rs:198
  10: std::panicking::default_hook
             at src/libstd/panicking.rs:215
  11: std::panicking::rust_panic_with_hook
             at src/libstd/panicking.rs:486
  12: std::panicking::begin_panic
             at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/libstd/panicking.rs:410
  13: playground::two_is_two
             at src/lib.rs:3
  14: playground::two_is_two::{{closure}}
             at src/lib.rs:2
  15: core::ops::function::FnOnce::call_once
             at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/libcore/ops/function.rs:232
  16: <alloc::boxed::Box<F> as core::ops::function::FnOnce<A>>::call_once
             at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/liballoc/boxed.rs:1076
  17: <std::panic::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
             at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/libstd/panic.rs:318
  18: std::panicking::try::do_call
             at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/libstd/panicking.rs:297
  19: std::panicking::try
             at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/libstd/panicking.rs:274
  20: std::panic::catch_unwind
             at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/libstd/panic.rs:394
  21: test::run_test_in_process
             at src/libtest/lib.rs:541
  22: test::run_test::run_test_inner::{{closure}}
             at src/libtest/lib.rs:450
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.


failures:
    two_is_two

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

除非你真的找不到問題所在,否則你不需要使用回溯(Backtrace)。但幸運的是你也不需要全部理解。如果你繼續閱讀,你最終會看到第 13 行,那裡寫著 playground──那是它提到的你的程式碼的位置。其它的一切都是關於 Rust 為了執行你的程式,在其他函式庫中所做的事情。但是這兩行告訴你,它看的是 playground 的第 2 行和第 3 行,這是個要檢查那裡的提示。這裡重複那個部分:

  13: playground::two_is_two
             at src/lib.rs:3
  14: playground::two_is_two::{{closure}}
             at src/lib.rs:2

編輯:Rust 在 2021 年初改進了它的回溯訊息,只顯示最有意義的資訊。現在更容易閱讀了:

failures:

---- two_is_two stdout ----
thread 'two_is_two' panicked at 'assertion failed: `(left == right)`
  left: `2`,
 right: `3`', src/lib.rs:3:5
stack backtrace:
   0: rust_begin_unwind
             at /rustc/cb75ad5db02783e8b0222fee363c5f63f7e2cf5b/library/std/src/panicking.rs:493:5
   1: core::panicking::panic_fmt
             at /rustc/cb75ad5db02783e8b0222fee363c5f63f7e2cf5b/library/core/src/panicking.rs:92:14
   2: playground::two_is_two
             at ./src/lib.rs:3:5
   3: playground::two_is_two::{{closure}}
             at ./src/lib.rs:2:1
   4: core::ops::function::FnOnce::call_once
             at /rustc/cb75ad5db02783e8b0222fee363c5f63f7e2cf5b/library/core/src/ops/function.rs:227:5
   5: core::ops::function::FnOnce::call_once
             at /rustc/cb75ad5db02783e8b0222fee363c5f63f7e2cf5b/library/core/src/ops/function.rs:227:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.


failures:
    two_is_two

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s

現在讓我們再把回溯關閉,回到常規的測試。現在我們將會寫一些其他函式,並使用測試函式來測試它們。這裡有幾個範例:

#![allow(unused)]
fn main() {
fn return_two() -> i8 {
    2
}
#[test]
fn it_returns_two() {
    assert_eq!(return_two(), 2);
}

fn return_six() -> i8 {
    4 + return_two()
}
#[test]
fn it_returns_six() {
    assert_eq!(return_six(), 6)
}
}

現在都能執行成功:

running 2 tests
test it_returns_two ... ok
test it_returns_six ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

這不是太難。

通常你會想把你的測試放在它們自己的模組中。要做到這一點,需要使用相同的 mod 關鍵字,並在它前面加上 #[cfg(test)](記住:cfg 的意思是"組態")。你還想要繼續在每一個測試前面寫 #[test]。這是因為以後當你安裝 Rust 時,你可以做更復雜的測試。你將可以執行一個測試、全部測試、或者其中一些測試。另外別忘了要寫 use super::*;,因為測試模組需要使用它上層的函式。現在它看起來會像這樣:

#![allow(unused)]
fn main() {
fn return_two() -> i8 {
    2
}
fn return_six() -> i8 {
    4 + return_two()
}

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

    #[test]
    fn it_returns_six() {
        assert_eq!(return_six(), 6)
    }
    #[test]
    fn it_returns_two() {
        assert_eq!(return_two(), 2);
    }
}
}

測試驅動開發

在閱讀 Rust 或其他語言時,你可能會看到"測試驅動開發(Test-driven development)"這個詞。這是編寫程式的一種方式,有些人喜歡它,而有些人則喜歡其他的方式。"測試驅動開發"的意思是"先寫測試,再寫程式碼"。當你這樣做的時候,你將會有很多測試程式碼給所有你想要你的程式碼去做的事情。然後你才開始寫程式碼,並執行測試來看你是否做對了。接著當你加入和重寫你的程式碼時,測試程式碼會一直在那裡告訴你是否有什麼東西出了問題。這在 Rust 中相當容易,因為編譯器給出了很多關於待修復內容的資訊。讓我們寫個測試驅動開發的小範例,來看看它像什麼樣子。

讓我們想像可以接受使用者輸入的計算機。它可以加 (+),也可以減 (-)。如果使用者寫 "5 + 6",它應該回傳 11,如果使用者寫 "5 + 6 - 7",它應該回傳 4,以此類推。所以我們將先從測試函式開始。你也可以看到,測試中的函式名通常都相當長。這是因為你可能會執行很多的測試,並且你想瞭解哪些測試失敗了。

我們將想像有個名為 math() 的單獨函式會做完所有工作。它將回傳 i32(我們將不會使用浮點數)。因為它需要回傳一些東西,我們每次都將只會回傳 6。然後我們將寫三個測試函式。當然它們都會失敗。現在的程式碼像這樣:

#![allow(unused)]
fn main() {
fn math(input: &str) -> i32 {
    6
}

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

    #[test]
    fn one_plus_one_is_two() {
        assert_eq!(math("1 + 1"), 2);
    }
    #[test]
    fn one_minus_two_is_minus_one() {
        assert_eq!(math("1 - 2"), -1);
    }
    #[test]
    fn one_minus_minus_one_is_two() {
        assert_eq!(math("1 - -1"), 2);
    }
}
}

它給我們這些資訊:

running 3 tests
test tests::one_minus_minus_one_is_two ... FAILED
test tests::one_minus_two_is_minus_one ... FAILED
test tests::one_plus_one_is_two ... FAILED

以及關於 thread 'tests::one_plus_one_is_two' panicked at 'assertion failed: `(left == right)` 的所有資訊。我們不需要在這裡全部印出來。

現在來思考如何做出計算機。我們將接受任何數字,以及 +- 符號。我們將允許空格,但不允許其他任何東西。所以讓我們從帶有 const 並包含以上所有字元的字串開始。然後我們將使用 .chars() 按字元進行疊代,並使用 .all() 確保它們都在裡面。

然後,我們將新增一個會恐慌的測試。要做到這一點,要加上 #[should_panic] 屬性:現在如果它恐慌了測試就會成功。

現在程式碼看起來像這樣:

#![allow(unused)]
fn main() {
const OKAY_CHARACTERS: &str = "1234567890+- "; // 別忘記結尾的空白

fn math(input: &str) -> i32 {
    if !input.chars().all(|character| OKAY_CHARACTERS.contains(character)) {
        panic!("Please only input numbers, +-, or spaces");
    }
    6 // 現在我們仍然還是回傳 6
}

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

    #[test]
    fn one_plus_one_is_two() {
        assert_eq!(math("1 + 1"), 2);
    }
    #[test]
    fn one_minus_two_is_minus_one() {
        assert_eq!(math("1 - 2"), -1);
    }
    #[test]
    fn one_minus_minus_one_is_two() {
        assert_eq!(math("1 - -1"), 2);
    }

    #[test]
    #[should_panic]  // 這裡是我們的新測試 - 它應該要恐慌
    fn panics_when_characters_not_right() {
        math("7 + seven");
    }
}
}

現在當我們執行測試時,我們得到這樣的結果:

running 4 tests
test tests::one_minus_two_is_minus_one ... FAILED
test tests::one_minus_minus_one_is_two ... FAILED
test tests::panics_when_characters_not_right ... ok
test tests::one_plus_one_is_two ... FAILED

有一個成功了!我們的 math() 函式現在只能接受設定好的輸入了。

下一步是編寫實際的計算機。這就是先有測試的有趣之處:實際的程式碼要晚很多才開始出現。首先我們將把計算機的邏輯放在一起。我們要做到以下幾點:

  • 所有的空白都應該被移除。這很容易用 .filter() 實作。
  • 所有輸入容應該變成 Vec 中的元素。+ 不需要成為輸入,但是當程式看到 + 時,應該知道數字已經完成處理了。例如,輸入 11+1 應該像這樣做:1) 看到 1,把它推到一個空字串中。1) 看到另一個 1,把它推到字串中(現在是 "11")。3) 看到 +,知道數字已經結束,把字串推到向量裡,然後清空字串。
  • 程式必須計算出 - 的數量。奇數(1、3、5...)表示減法,偶數(2、4、6...)表示加法。所以 "1--9" 應該是 10,而不是 -8。
  • 程式應該移除最後一個數字後面的任何東西。5+5+++++---- 都是由出現在 OKAY_CHARACTERS 中的所有字元組成,但它應該清理變成 5+5。這很容易用 .trim_end_matches() 做到,它能讓你把符合 &str 結尾的東西都去掉。

順便說一下,.trim_end_matches().trim_start_matches() 曾經是 trim_right_matches()trim_left_matches()。但後來人們注意到有些語言是從右到左(波斯語、希伯來語等),所以左右都是錯的。你可能還能在一些程式碼中看到舊名字,但它們是一樣的。

首先我們只想通過所有的測試。通過測試後,我們就可以"重構(Refactor)"了。重構的意思是讓程式碼變得更好,通常是透過像結構體、列舉和方法等方式。這裡是我們使測試通過的程式碼:

#![allow(unused)]
fn main() {
const OKAY_CHARACTERS: &str = "1234567890+- ";

fn math(input: &str) -> i32 {
    if !input.chars().all(|character| OKAY_CHARACTERS.contains(character)) ||
       !input.chars().take(2).any(|character| character.is_numeric())
    {
        panic!("Please only input numbers, +-, or spaces.");
    }

    let input = input.trim_end_matches(|x| "+- ".contains(x)).chars().filter(|x| *x != ' ').collect::<String>(); // 移除結尾的 + 和 -, 和全部空白
    let mut result_vec = vec![]; // Results 放在這裡
    let mut push_string = String::new(); // 這是我們每次推送資料的字串. 我們將會在迴圈裡持續重複使用它.
    for character in input.chars() {
        match character {
            '+' => {
                if !push_string.is_empty() { // 如果字串是空的, 我們不想把 "" 推到 result_vec 裡
                    result_vec.push(push_string.clone()); // 但如果不是空的, 它就會是數字. 把它推到向量裡
                    push_string.clear(); // 接著清除字串
                }
            },
            '-' => { // 如果我們得到的是 -,
                if push_string.contains('-') || push_string.is_empty() { // 檢查看看是否為空或有 -
                    push_string.push(character) // 如果是如此, 那麼把它推到字串裡
                } else { // 不然, 它將會包含數字
                result_vec.push(push_string.clone()); // 那麼把數字推到 result_vec 裡, 清除字串後再把 - 推進去
                push_string.clear();
                push_string.push(character);
                }
            },
            number => { // number 在這裡的意思是 "其它任何匹配到的東西". 也是我們所選擇的名字
                if push_string.contains('-') { // 我們可能有一些 - 字元要先推進去
                    result_vec.push(push_string.clone());
                    push_string.clear();
                    push_string.push(number);
                } else { // 但如果沒有, 那就表示我們可以把數字推進去
                    push_string.push(number);
                }
            },
        }
    }
    result_vec.push(push_string); // 迴圈結束後把字串推進去. 沒有 .clone() 的必要因為我們不會再使用它了

    let mut total = 0; // 現在是時候算數學了. 從總合開始
    let mut adds = true; // true = 加法, false = 減法
    let mut math_iter = result_vec.into_iter();
    while let Some(entry) = math_iter.next() { // 疊代元素過去
        if entry.contains('-') { // 如果有 - 字元, 檢查奇數或偶數
            if entry.chars().count() % 2 == 1 {
                adds = match adds {
                    true => false,
                    false => true
                };
                continue; // 繼續處理下一個元素
            } else {
                continue;
            }
        }
        if adds == true {
            total += entry.parse::<i32>().unwrap(); // 如果沒有 '-', 肯定是數字. 那我們解包很安全
        } else {
            total -= entry.parse::<i32>().unwrap();
            adds = true;  // 減完後, 重設 adds 為 true.
        }
    }
    total // 終於要回傳總合
}
   /// 我們將多加上一些測試來確認行為

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

    #[test]
    fn one_plus_one_is_two() {
        assert_eq!(math("1 + 1"), 2);
    }
    #[test]
    fn one_minus_two_is_minus_one() {
        assert_eq!(math("1 - 2"), -1);
    }
    #[test]
    fn one_minus_minus_one_is_two() {
        assert_eq!(math("1 - -1"), 2);
    }
    #[test]
    fn nine_plus_nine_minus_nine_minus_nine_is_zero() {
        assert_eq!(math("9+9-9-9"), 0); // 這是新測試
    }
    #[test]
    fn eight_minus_nine_plus_nine_is_eight_even_with_characters_on_the_end() {
        assert_eq!(math("8  - 9     +9-----+++++"), 8); // 這是新測試
    }
    #[test]
    #[should_panic]
    fn panics_when_characters_not_right() {
        math("7 + seven");
    }
}
}

現在測試都通過了!

running 6 tests
test tests::one_minus_minus_one_is_two ... ok
test tests::nine_plus_nine_minus_nine_minus_nine_is_zero ... ok
test tests::one_minus_two_is_minus_one ... ok
test tests::eight_minus_nine_plus_nine_is_eight_even_with_characters_on_the_end ... ok
test tests::one_plus_one_is_two ... ok
test tests::panics_when_characters_not_right ... ok

test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

你可以看到,在測試驅動的開發中有來回的過程。它是像這樣的:

  • 首先你要寫出所有你能想得到的測試。
  • 然後你開始寫程式碼。
  • 當你寫程式碼的時候,你會得到其他測試的想法。
  • 你新增測試,你的測試隨著你的進展而成長。你有的測試越多,你的程式碼被檢查的次數就越多。

當然測試並不能檢查所有的東西,認為"通過所有測試 = 完美的程式碼"是錯誤的。但是測試對於修改程式碼是很棒的。如果你以後修改了程式碼並執行測試,如果其中有一個測試不成功,你就會知道什麼該修復。

現在我們可以重寫(重構)一點程式碼。一個好方式是用 Clippy 開始。如果你安裝了 Rust,那麼你可以輸入 cargo clippy。如果你使用的是 Playground,那麼點選 TOOLS,選擇 Clippy。Clippy 會檢閱你的程式碼,並給出能讓你的程式碼更精簡的提示。我們的程式碼沒有任何錯誤,但它能更好。

Clippy 告訴我們兩件事:

warning: this loop could be written as a `for` loop
  --> src/lib.rs:44:5
   |
44 |     while let Some(entry) = math_iter.next() { // Iter through the items
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `for entry in math_iter`
   |
   = note: `#[warn(clippy::while_let_on_iterator)]` on by default
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#while_let_on_iterator

warning: equality checks against true are unnecessary
  --> src/lib.rs:53:12
   |
53 |         if adds == true {
   |            ^^^^^^^^^^^^ help: try simplifying it as shown: `adds`
   |
   = note: `#[warn(clippy::bool_comparison)]` on by default
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#bool_comparison

這是真的:for entry in math_iterwhile let Some(entry) = math_iter.next() 簡單得多。而 for 迴圈實際上是個疊代器,所以我們沒有任何理由要寫 .iter()。謝謝 clippy!而且我們也不需要做 math_iter:我們可以只要寫 for entry in result_vec

現在我們將開始做些真正的重構。我們將建立 Calculator 結構體,而不是單獨的變數。這將擁有我們使用的所有變數。我們將改變兩個名字來讓它更清楚。result_vec 將變成 resultspush_string 將變成 current_input(currenㄙㄨ的意思是 "現在")。而到目前為止,它只有一種方法:new。

#![allow(unused)]
fn main() {
// 🚧
#[derive(Clone)]
struct Calculator {
    results: Vec<String>,
    current_input: String,
    total: i32,
    adds: bool,
}

impl Calculator {
    fn new() -> Self {
        Self {
            results: vec![],
            current_input: String::new(),
            total: 0,
            adds: true,
        }
    }
}
}

現在我們的程式碼實際上更長了一點,但也更容易讀懂。比如 if adds 現在是 if calculator.adds,這就跟讀英文完全一樣。看起來像這樣:

#![allow(unused)]
fn main() {
#[derive(Clone)]
struct Calculator {
    results: Vec<String>,
    current_input: String,
    total: i32,
    adds: bool,
}

impl Calculator {
    fn new() -> Self {
        Self {
            results: vec![],
            current_input: String::new(),
            total: 0,
            adds: true,
        }
    }
}

const OKAY_CHARACTERS: &str = "1234567890+- ";

fn math(input: &str) -> i32 {
    if !input.chars().all(|character| OKAY_CHARACTERS.contains(character)) ||
       !input.chars().take(2).any(|character| character.is_numeric()) {
        panic!("Please only input numbers, +-, or spaces");
    }

    let input = input.trim_end_matches(|x| "+- ".contains(x)).chars().filter(|x| *x != ' ').collect::<String>();
    let mut calculator = Calculator::new();

    for character in input.chars() {
        match character {
            '+' => {
                if !calculator.current_input.is_empty() {
                    calculator.results.push(calculator.current_input.clone());
                    calculator.current_input.clear();
                }
            },
            '-' => {
                if calculator.current_input.contains('-') || calculator.current_input.is_empty() {
                    calculator.current_input.push(character)
                } else {
                calculator.results.push(calculator.current_input.clone());
                calculator.current_input.clear();
                calculator.current_input.push(character);
                }
            },
            number => {
                if calculator.current_input.contains('-') {
                    calculator.results.push(calculator.current_input.clone());
                    calculator.current_input.clear();
                    calculator.current_input.push(number);
                } else {
                    calculator.current_input.push(number);
                }
            },
        }
    }
    calculator.results.push(calculator.current_input);

    for entry in calculator.results {
        if entry.contains('-') {
            if entry.chars().count() % 2 == 1 {
                calculator.adds = match calculator.adds {
                    true => false,
                    false => true
                };
                continue;
            } else {
                continue;
            }
        }
        if calculator.adds {
            calculator.total += entry.parse::<i32>().unwrap();
        } else {
            calculator.total -= entry.parse::<i32>().unwrap();
            calculator.adds = true;
        }
    }
    calculator.total
}

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

    #[test]
    fn one_plus_one_is_two() {
        assert_eq!(math("1 + 1"), 2);
    }
    #[test]
    fn one_minus_two_is_minus_one() {
        assert_eq!(math("1 - 2"), -1);
    }
    #[test]
    fn one_minus_minus_one_is_two() {
        assert_eq!(math("1 - -1"), 2);
    }
    #[test]
    fn nine_plus_nine_minus_nine_minus_nine_is_zero() {
        assert_eq!(math("9+9-9-9"), 0);
    }
    #[test]
    fn eight_minus_nine_plus_nine_is_eight_even_with_characters_on_the_end() {
        assert_eq!(math("8  - 9     +9-----+++++"), 8);
    }
    #[test]
    #[should_panic]
    fn panics_when_characters_not_right() {
        math("7 + seven");
    }
}
}

最後我們增加兩個新方法。一個叫做 .clear(),清除 current_input()。另一個叫做 push_char(),把輸入推到 current_input() 上。這裡是我們重構後的程式碼:

#![allow(unused)]
fn main() {
#[derive(Clone)]
struct Calculator {
    results: Vec<String>,
    current_input: String,
    total: i32,
    adds: bool,
}

impl Calculator {
    fn new() -> Self {
        Self {
            results: vec![],
            current_input: String::new(),
            total: 0,
            adds: true,
        }
    }

    fn clear(&mut self) {
        self.current_input.clear();
    }

    fn push_char(&mut self, character: char) {
        self.current_input.push(character);
    }
}

const OKAY_CHARACTERS: &str = "1234567890+- ";

fn math(input: &str) -> i32 {
    if !input.chars().all(|character| OKAY_CHARACTERS.contains(character)) ||
       !input.chars().take(2).any(|character| character.is_numeric()) {
        panic!("Please only input numbers, +-, or spaces");
    }

    let input = input.trim_end_matches(|x| "+- ".contains(x)).chars().filter(|x| *x != ' ').collect::<String>();
    let mut calculator = Calculator::new();

    for character in input.chars() {
        match character {
            '+' => {
                if !calculator.current_input.is_empty() {
                    calculator.results.push(calculator.current_input.clone());
                    calculator.clear();
                }
            },
            '-' => {
                if calculator.current_input.contains('-') || calculator.current_input.is_empty() {
                    calculator.push_char(character)
                } else {
                calculator.results.push(calculator.current_input.clone());
                calculator.clear();
                calculator.push_char(character);
                }
            },
            number => {
                if calculator.current_input.contains('-') {
                    calculator.results.push(calculator.current_input.clone());
                    calculator.clear();
                    calculator.push_char(number);
                } else {
                    calculator.push_char(number);
                }
            },
        }
    }
    calculator.results.push(calculator.current_input);

    for entry in calculator.results {
        if entry.contains('-') {
            if entry.chars().count() % 2 == 1 {
                calculator.adds = match calculator.adds {
                    true => false,
                    false => true
                };
                continue;
            } else {
                continue;
            }
        }
        if calculator.adds {
            calculator.total += entry.parse::<i32>().unwrap();
        } else {
            calculator.total -= entry.parse::<i32>().unwrap();
            calculator.adds = true;
        }
    }
    calculator.total
}

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

    #[test]
    fn one_plus_one_is_two() {
        assert_eq!(math("1 + 1"), 2);
    }
    #[test]
    fn one_minus_two_is_minus_one() {
        assert_eq!(math("1 - 2"), -1);
    }
    #[test]
    fn one_minus_minus_one_is_two() {
        assert_eq!(math("1 - -1"), 2);
    }
    #[test]
    fn nine_plus_nine_minus_nine_minus_nine_is_zero() {
        assert_eq!(math("9+9-9-9"), 0);
    }
    #[test]
    fn eight_minus_nine_plus_nine_is_eight_even_with_characters_on_the_end() {
        assert_eq!(math("8  - 9     +9-----+++++"), 8);
    }
    #[test]
    #[should_panic]
    fn panics_when_characters_not_right() {
        math("7 + seven");
    }
}
}

現在大概已經夠好了。我們可以寫更多的方法,但是很多行像是 calculator.results.push(calculator.current_input.clone()); 已經很清楚了。重構的時機最好是在你的程式碼完成後還能輕鬆閱讀的時候。你不希望只是為了讓程式碼變短而重構:例如,clc.clr() 就比 calculator.clear() 差很多。