測試
在我們瞭解模組後,測試正是現在學習的好主題。在 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_iter
比 while let Some(entry) = math_iter.next()
簡單得多。而 for
迴圈實際上是個疊代器,所以我們沒有任何理由要寫 .iter()
。謝謝 clippy!而且我們也不需要做 math_iter
:我們可以只要寫 for entry in result_vec
。
現在我們將開始做些真正的重構。我們將建立 Calculator
結構體,而不是單獨的變數。這將擁有我們使用的所有變數。我們將改變兩個名字來讓它更清楚。result_vec
將變成 results
,push_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()
差很多。