182 Commits

Author SHA1 Message Date
Hector PENG
f9b5204ec1 Updated 'src/packages_crates_and_modules/packages_and_crates.md'. 2025-06-09 16:37:00 +08:00
Hector PENG
86a0a7e584 Merge pull request #4 from chenjjiaa/patch-1
Update packages_and_crates.md
2025-06-09 16:35:23 +08:00
Hector PENG
338c63c5e0 Updated 'src/io_project/test_driven_dev.md'. 2025-06-09 16:30:16 +08:00
励志买套上海苏河湾大平层
a72a276fba Update packages_and_crates.md
Fix spelling errors.
2025-06-09 10:43:05 +08:00
Hector PENG
a227b1ffd2 Updated 'src/common_collections/strings.md'. 2025-06-07 18:14:14 +08:00
Hector PENG
426e1794e1 Updated 'src/common_collections/strings.md'. 2025-06-07 18:14:00 +08:00
Hector PENG
228baef40c Updated 'src/common_collections/vectors.md'. 2025-06-06 16:04:21 +08:00
Hector PENG
04c76e51c9 Updated 'src/packages_crates_and_modules/separating_modules.md'. 2025-05-30 16:27:32 +08:00
Hector PENG
03fdce38ca Updated 'src/Ch16_1_async_programming.md'. 2025-05-30 10:49:31 +08:00
Hector PENG
316acdc6a0 Updated 'src/SUMMARY.md'. 2025-05-30 10:48:03 +08:00
Hector PENG
7a830434c7 Updated 'src/adv_features/unsafe.md'. 2025-05-29 16:04:26 +08:00
Hector PENG
b1153edb3a Updated 'src/smart_pointers'. 2025-05-29 16:00:34 +08:00
Hector PENG
b855906ce2 Updated 'src/common_collection/strings.md'. 2025-05-28 16:28:05 +08:00
Hector PENG
e7f349894b Updated 'src/common_collection/hash_maps.md'. 2025-05-28 16:25:39 +08:00
Hector PENG
f960a01ff0 Updated 'src/SUMMARY.md'. 2025-05-28 08:45:59 +08:00
Hector PENG
20660c4561 Finished 'src/async/all_together.md'. 2025-05-27 15:58:36 +08:00
Hector PENG
82fefd52d6 Finished 'src/async/async_traits.md'. 2025-05-27 13:49:07 +08:00
Hector PENG
85cdd687ee Added 'src/async/async_traits.md'. 2025-05-23 17:02:10 +08:00
Hector PENG
c02d31ac7c Finished 'src/async/streams.md'. 2025-05-23 16:34:46 +08:00
Hector PENG
2be1acdd9d Added 'src/async/streams.md'. 2025-05-21 17:32:36 +08:00
Hector PENG
af46faf1eb Finished 'src/async/multiple_futures.md'. 2025-05-21 09:35:00 +08:00
Hector PENG
b63ceb2d7b Finished 'src/async/concurrency_n_async.md'. 2025-05-19 16:45:12 +08:00
Hector PENG
bbcf7e598a Updated 'src/enums_and_pattern_matching/if-let_control_flow.md'. 2025-05-16 17:08:14 +08:00
Hector PENG
369e680fdd Updated 'src/enums_and_pattern_matching/if-let_control_flow.md'. 2025-05-16 17:07:59 +08:00
Hector PENG
8cc7677249 Updated 'src/enums_and_pattern_matching/match_control_flow.md'. 2025-05-15 11:51:38 +08:00
Hector PENG
35b6269dde Updated 'src/enums_and_pattern_matching/match_control_flow.md'. 2025-05-14 16:46:19 +08:00
Hector PENG
1a744d46e1 Added 'src/async/futures.md'. 2025-05-14 15:02:40 +08:00
Hector PENG
d4f4534e8c Merge pull request #1 from szabgab/patch-1
remove the unused and deprecated `multilingual` field from `book.toml`
2025-05-07 13:18:45 +08:00
Gábor Szabó
311826df49 remove the unused and deprecated multilingual field from book.toml 2025-04-11 12:06:47 +03:00
Hector PENG
38cd58a58d Updated 'src/smart_pointers/refcell-t.md'. 2025-01-19 07:17:20 +08:00
Hector PENG
c3b2827f9b Updated 'src/smart_pointers/ref-cycles.md'. 2025-01-18 23:05:25 +08:00
Hector PENG
ba3ab0b6d3 Updated 'theme/index.hbs'. 2025-01-17 09:20:43 +08:00
Hector PENG
5ee2934f80 Updated 'theme/index.hbs'. 2025-01-17 07:03:13 +08:00
Hector PENG
e827ff71a5 Updated 'theme/index.hbs'. 2025-01-16 20:32:54 +08:00
Hector PENG
1f15d567a9 Updated src/ALL.md. 2025-01-10 07:35:51 +08:00
Hector PENG
c7d90566b5 Updated 'theme/index.hbs'. 2024-10-29 11:31:51 +08:00
Hector PENG
50183da076 Updated 'src/getting_start/installation.md'. 2024-10-09 14:37:29 +08:00
Hector PENG
7fcba013b0 Refined 'src/packages_crates_and_modules/the_use_keyword.md'. 2024-10-07 20:41:10 +08:00
Hector PENG
4750e1292d Updated 'src/packages_crates_and_modules/the_use_keyword.md'. 2024-10-07 18:12:55 +08:00
Hector PENG
a2f1181c1c Updated 'src/packages_crates_and_modules/the_use_keyword.md'. 2024-10-07 18:11:03 +08:00
Hector PENG
cb96f0188d Refined 'src/packages_crates_and_modules/paths.md'. 2024-10-07 18:00:25 +08:00
Hector PENG
2789602795 Updated 'src/packages_crates_and_modules/paths.md'. 2024-10-06 20:27:23 +08:00
Hector PENG
d109663cef Updated 'src/packages_crates_and_modules/paths.md'. 2024-10-06 19:45:19 +08:00
Hector PENG
37ed9084bc Updated 'src/getting_started/installation.md'. 2024-08-14 09:23:53 +08:00
Hector PENG
a907f3dec7 Updated 'theme/index.hbs'. 2024-07-29 09:46:39 +08:00
Hector PENG
a842a68142 Updated 'src/packages_crates_and_modules/paths.md'. 2024-05-04 18:10:35 +08:00
Hector PENG
427e07c826 Updated 'src/packages_crates_and_modules/paths.md'. 2024-05-04 17:59:03 +08:00
Hector PENG
074b2b10fb Updated 'src/packages_crates_and_modules/paths.md'. 2024-05-04 17:06:54 +08:00
Hector PENG
c8f7342468 Updated 'src/packages_crates_and_modules/paths.md'. 2024-05-04 16:47:39 +08:00
Hector PENG
2265fb78f1 Updated 'src/packages_crates_and_modules/paths.md'. 2024-01-17 21:12:08 +08:00
Hector PENG
b10dc79c44 Updated 'src/packages_crates_and_modules/paths.md'. 2024-01-15 21:58:13 +08:00
Hector PENG
cb714f589b Updated 'src/packages_crates_and_modules/paths.md'. 2024-01-15 09:22:38 +08:00
Hector PENG
c2d8dab258 Updated 'src/packages_crates_and_modules/paths.md'. 2024-01-14 21:41:11 +08:00
Hector PENG
f924f613cb Updated 'src/packages_crates_and_modules/paths.md'. 2024-01-14 21:15:07 +08:00
Hector PENG
571ab64d2a Updated 'src/packages_crates_and_modules/paths.md'. 2024-01-14 21:05:13 +08:00
Hector PENG
11857e3f04 Updated 'src/packages_crates_and_modules/paths.md'. 2024-01-11 07:52:00 +08:00
Hector PENG
812465b782 Updated 'src/packages_crates_and_modules/paths.md'. 2024-01-11 07:46:17 +08:00
Hector PENG
dd216bbd7d Updated 'src/packages_crates_and_modules/paths.md'. 2024-01-10 21:53:53 +08:00
Lenny Peng
370a82c6af Updated 'src/packages_crate_and_modules/paths.md'. 2024-01-02 07:33:33 +08:00
Peng Hailin,
9d790a19d2 Updated 'src/packages_crates_and_modules/defining_modules.md'. 2024-01-01 19:52:45 +08:00
Peng Hailin,
ccbb7d5823 Updated 'src/packages_crates_and_modules/defining_modules.md'. 2024-01-01 19:33:41 +08:00
Peng Hailin,
47ef6011a5 Updated 'src/packages_crates_and_modules/defining_modules.md'. 2023-12-24 20:37:07 +08:00
Peng Hailin,
c773279aef Refining Ch07. 2023-12-19 22:02:21 +08:00
Peng Hailin,
cda727fba7 Fixed a typo-error in 'src/SUMMARY.md'. 2023-12-19 22:00:27 +08:00
Peng Hailin,
1ae19e883e Refining Ch07. 2023-12-19 21:52:54 +08:00
rust-lang.xfoss.com
e9c6e88ab8 Refining Ch07. 2023-12-19 17:24:13 +08:00
rust-lang.xfoss.com
a1209bc6f3 Refining Ch07. 2023-12-19 17:00:30 +08:00
rust-lang.xfoss.com
3519e4b7ef Refining Ch07. 2023-12-19 16:46:42 +08:00
rust-lang.xfoss.com
3908b22807 Refining Ch07. 2023-12-19 15:46:29 +08:00
rust-lang.xfoss.com
4f1628529f Refined Ch06. 2023-12-19 15:28:38 +08:00
rust-lang.xfoss.com
842bcc2937 Refined Ch06. 2023-12-19 15:26:55 +08:00
rust-lang.xfoss.com
29509e54da Refining Ch06. 2023-12-19 15:03:23 +08:00
Peng Hailin,
5bd5d3f2ed Fixed a typo-error in 'src/SUMMARY.md'. 2023-12-18 18:53:28 +08:00
rust-lang.xfoss.com
fd90ec4ff5 Refining Ch06. 2023-12-18 17:25:11 +08:00
rust-lang.xfoss.com
4c15d51218 Refining Ch06. 2023-12-18 17:17:50 +08:00
rust-lang.xfoss.com
de42aca5d9 Refining Ch06. 2023-12-18 16:23:02 +08:00
rust-lang.xfoss.com
358daf454c Refining Ch06. 2023-12-18 15:15:11 +08:00
rust-lang.xfoss.com
2706dfe0c7 Refining Ch06. 2023-12-18 13:53:02 +08:00
rust-lang.xfoss.com
55746ebe6a Refining Ch06. 2023-12-18 13:17:33 +08:00
rust-lang.xfoss.com
f80e61a96f Refining Ch06. 2023-12-18 11:31:52 +08:00
rust-lang.xfoss.com
138c582ccb Refining Ch06. 2023-12-18 09:43:31 +08:00
Peng Hailin,
9a35a7310f Refining Ch04. 2023-12-17 20:13:54 +08:00
rust-lang.xfoss.com
a36eb064b2 Refining Ch06. 2023-12-15 17:27:22 +08:00
rust-lang.xfoss.com
6bae7063bf Refining Ch06. 2023-12-15 17:06:06 +08:00
rust-lang.xfoss.com
bd23125ee6 Refining Ch06. 2023-12-15 16:56:42 +08:00
rust-lang.xfoss.com
9e7b57f06b Refined Ch05. 2023-12-15 16:49:34 +08:00
rust-lang.xfoss.com
2012f87111 Refining Ch05. 2023-12-15 16:00:03 +08:00
rust-lang.xfoss.com
e1efe9d86b Refining Ch05. 2023-12-15 15:03:49 +08:00
rust-lang.xfoss.com
da1d65a5d8 Refining Ch05. 2023-12-14 17:51:33 +08:00
rust-lang.xfoss.com
a763af3eb5 Refining Ch05. 2023-12-14 16:08:55 +08:00
rust-lang.xfoss.com
79ad726f36 Refining Ch05. 2023-12-14 13:46:39 +08:00
rust-lang.xfoss.com
152b836c96 Refining Ch05. 2023-12-14 11:06:43 +08:00
rust-lang.xfoss.com
bbe074c010 Refining Ch05. 2023-12-14 11:02:34 +08:00
rust-lang.xfoss.com
1c5748a6b9 Refining Ch05. 2023-12-13 17:28:04 +08:00
rust-lang.xfoss.com
d168f56747 Refining Ch05. 2023-12-13 16:51:15 +08:00
rust-lang.xfoss.com
d82f9e9a76 Refined Ch04. 2023-12-13 16:39:50 +08:00
rust-lang.xfoss.com
d850f94421 Refining Ch04. 2023-12-13 15:41:03 +08:00
rust-lang.xfoss.com
c0d1fe7d9d Refining Ch04. 2023-12-13 14:22:57 +08:00
rust-lang.xfoss.com
0394578416 Refining Ch04. 2023-12-13 14:14:13 +08:00
rust-lang.xfoss.com
96e4d187ce Refining Ch04. 2023-12-13 11:18:50 +08:00
rust-lang.xfoss.com
157ea4c20c Refining Ch04. 2023-12-13 11:00:21 +08:00
rust-lang.xfoss.com
95bd669618 Refining Ch04. 2023-12-13 10:22:33 +08:00
rust-lang.xfoss.com
c3495c3ed9 Refining Ch04. 2023-12-13 09:17:52 +08:00
Peng Hailin,
73d1310b76 Refining Ch04. 2023-12-12 21:58:56 +08:00
Peng Hailin,
de725f1230 Refining Ch04. 2023-12-12 21:52:09 +08:00
Peng Hailin,
377ccdf205 Refining Ch04. 2023-12-12 21:48:05 +08:00
rust-lang.xfoss.com
60bd1a1a61 Refining Ch04. 2023-12-12 17:58:19 +08:00
rust-lang.xfoss.com
584cf03a38 Refining Ch04. 2023-12-12 17:38:18 +08:00
rust-lang.xfoss.com
f6694b1b61 Refining Ch04. 2023-12-12 16:59:12 +08:00
rust-lang.xfoss.com
632b92bf61 Refining Ch04. 2023-12-12 16:24:45 +08:00
rust-lang.xfoss.com
d6a3759844 Refining Ch04. 2023-12-11 17:11:50 +08:00
rust-lang.xfoss.com
42fe623157 Refined Ch03. 2023-12-11 17:08:48 +08:00
rust-lang.xfoss.com
9196580e9c Refining Ch03. 2023-12-11 16:40:57 +08:00
rust-lang.xfoss.com
d3c6298114 Refining Ch03. 2023-12-11 15:24:51 +08:00
rust-lang.xfoss.com
f400886623 Refining Ch03. 2023-12-11 15:10:32 +08:00
rust-lang.xfoss.com
a46c942060 Refining Ch03. 2023-12-11 13:31:37 +08:00
rust-lang.xfoss.com
12bf2a6d30 Refining Ch03. 2023-12-11 13:18:43 +08:00
rust-lang.xfoss.com
4f8e95406b Refining Ch03. 2023-12-11 11:26:05 +08:00
rust-lang.xfoss.com
b8ba01720b Refining Ch03. 2023-12-08 17:58:26 +08:00
rust-lang.xfoss.com
3dec2ad5a5 Refining Ch03. 2023-12-08 17:21:36 +08:00
rust-lang.xfoss.com
f768d5f9c3 Refining Ch03. 2023-12-08 16:54:21 +08:00
rust-lang.xfoss.com
33161694d1 Refining Ch03. 2023-12-08 15:54:06 +08:00
rust-lang.xfoss.com
f413255aae Refining Ch03. 2023-12-08 14:39:07 +08:00
rust-lang.xfoss.com
82e42ad6cf Refining Ch03. 2023-12-08 14:05:36 +08:00
rust-lang.xfoss.com
a5477569a7 Refining Ch03. 2023-12-08 13:40:44 +08:00
rust-lang.xfoss.com
9e01554d49 Refining Ch03. 2023-12-08 11:14:36 +08:00
rust-lang.xfoss.com
1e2d49aeb6 Refining Ch03. 2023-12-08 10:54:04 +08:00
rust-lang.xfoss.com
4965f88387 Refining Ch03. 2023-12-07 17:42:50 +08:00
rust-lang.xfoss.com
98e520e02b Refining Ch03. 2023-12-07 16:41:41 +08:00
rust-lang.xfoss.com
a9a63fe8a6 Refined Ch02. 2023-12-07 16:08:12 +08:00
rust-lang.xfoss.com
084fbfcf88 Refining Ch02. 2023-12-07 15:23:09 +08:00
rust-lang.xfoss.com
1cf113fa00 Merge remote-tracking branch 'refs/remotes/origin/main' 2023-12-07 15:20:40 +08:00
rust-lang.xfoss.com
af5388f8af Refining Ch02. 2023-12-07 15:19:51 +08:00
Penn Hiln
37458bb4a3 Refining Ch02. 2023-12-07 14:26:42 +08:00
Penn Hiln
28d531e05e Refining Ch02. 2023-12-07 14:13:26 +08:00
Chat SCM
e2fcd9625b Refining Ch02. 2023-12-07 14:09:45 +08:00
rust-lang.xfoss.com
d88a799af5 Refining Ch02. 2023-12-07 14:06:55 +08:00
rust-lang.xfoss.com
707f3c9289 Refining Ch02. 2023-12-07 13:22:04 +08:00
rust-lang.xfoss.com
f0eb0850ee Refining Ch02. 2023-12-07 11:21:53 +08:00
rust-lang.xfoss.com
a331e13975 Refining Ch02. 2023-12-06 17:34:36 +08:00
rust-lang.xfoss.com
77b6275b2e Refining Ch02. 2023-12-06 17:18:50 +08:00
rust-lang.xfoss.com
bc2422406e Refining Ch02. 2023-12-06 17:16:41 +08:00
rust-lang.xfoss.com
c03cf61066 Refined Ch01. 2023-12-06 13:02:39 +08:00
rust-lang.xfoss.com
6d4e3171fc Refining Ch01. 2023-12-01 18:20:15 +08:00
rust-lang.xfoss.com
73c7f702c8 Refining Ch01. 2023-12-01 17:48:14 +08:00
rust-lang.xfoss.com
2b0d2238d5 Refining Ch01. 2023-12-01 16:24:21 +08:00
rust-lang.xfoss.com
b5611aed65 Refining Ch01. 2023-12-01 16:21:21 +08:00
rust-lang.xfoss.com
cbdf192b38 Updated the last-updated. 2023-12-01 14:34:49 +08:00
rust-lang.xfoss.com
aa11f3ee1a Fixed a typo. 2023-12-01 14:11:13 +08:00
rust-lang.xfoss.com
ecd2e9e6da Finished re-constructure 2023-12-01 14:09:46 +08:00
rust-lang.xfoss.com
e3b5cc8c71 Re-constructured Ch19. 2023-12-01 13:34:21 +08:00
rust-lang.xfoss.com
0339bebd1a Re-constructured Ch18. 2023-12-01 13:20:04 +08:00
rust-lang.xfoss.com
5ec1539d3f Re-constructured Ch17. 2023-12-01 11:18:19 +08:00
rust-lang.xfoss.com
3785312c64 Re-constructured Ch16. 2023-12-01 11:08:30 +08:00
rust-lang.xfoss.com
71a00fa204 Re-constructured Ch15. 2023-12-01 10:56:31 +08:00
rust-lang.xfoss.com
6fc28b5a1f Re-constructured Ch14. 2023-12-01 10:24:07 +08:00
Peng Hailin,
400a9f9543 Refined Ch13. 2023-11-30 21:28:39 +08:00
Peng Hailin,
8def6bdb0e Refined Ch12. 2023-11-30 21:05:32 +08:00
rust-lang.xfoss.com
0a570430f3 Re-constructuring Ch12. 2023-11-30 18:09:09 +08:00
rust-lang.xfoss.com
15fe5395cf Re-constructured Ch11. 2023-11-30 17:14:53 +08:00
rust-lang.xfoss.com
fd9c6053a1 Re-constructured Ch10. 2023-11-30 16:49:59 +08:00
rust-lang.xfoss.com
6aef347b4f Re-constructured Ch09. 2023-11-30 16:21:45 +08:00
rust-lang.xfoss.com
6021b30392 Re-constructured Ch08. 2023-11-30 15:59:53 +08:00
rust-lang.xfoss.com
ca7180b868 Re-constructured Ch07. 2023-11-30 15:12:54 +08:00
rust-lang.xfoss.com
ba4aee75ec Re-constructured Ch07. 2023-11-30 15:11:00 +08:00
rust-lang.xfoss.com
6d8b71b354 Re-constructured Ch06. 2023-11-30 14:44:34 +08:00
rust-lang.xfoss.com
84aa72bf3a Re-constructured Ch05. 2023-11-30 14:29:47 +08:00
rust-lang.xfoss.com
2164b236f6 Re-constructured Ch04. 2023-11-30 14:11:05 +08:00
rust-lang.xfoss.com
ddd61ca769 Re-constructured Ch03. 2023-11-30 13:45:42 +08:00
rust-lang.xfoss.com
f66928a6a8 Re-constructured Ch01. 2023-11-30 12:06:41 +08:00
rust-lang.xfoss.com
600bbe04af Updated 'theme/index.hbs'. 2023-11-24 14:43:50 +08:00
Peng Hailin,
4a6e42fc75 Updated 'theme/index.hbs'. 2023-11-24 07:28:12 +08:00
rust-lang.xfoss.com
964a05ed3b Updated 'book.toml'. 2023-10-10 13:57:54 +08:00
rust-lang.xfoss.com
73f5912845 Modified Ch01. 2023-09-19 10:39:51 +08:00
Peng Hailin,
5bb6facf5b Update Ch02 2023-07-22 22:02:53 +08:00
Peng Hailin,
121a6f312a Update Ch02 2023-07-22 21:56:06 +08:00
Unisko PENG,
ac12049942 Update book.toml 2023-07-14 08:02:34 +08:00
rust-lang.xfoss.com
6a17790f4f Updated. 2023-07-13 17:27:21 +08:00
rust-lang.xfoss.com
158cd092e6 Refactored. 2023-07-07 10:24:14 +08:00
rust-lang.xfoss.com
f946d94fab Released a epub format e-book. 2023-07-06 14:21:30 +08:00
rust-lang.xfoss.com
ebd28e9ad7 Released a epub format e-book. 2023-07-06 14:19:03 +08:00
rust-lang.xfoss.com
3573ef699c Released a epub format e-book. 2023-07-06 14:18:38 +08:00
171 changed files with 26670 additions and 20750 deletions

1
.gitignore vendored
View File

@@ -2,3 +2,4 @@ book
target
Cargo.lock
/index.html
*.tar.gz

View File

@@ -13,51 +13,10 @@ rustc 1.68.0 (2c8cc3432 2023-03-06)
在线阅读: [rust-lang.xfoss.com](https://rust-lang.xfoss.com)
本地阅读:[`mdbook` 本地运行](./src/local_serving.md)
---
## 在本地阅读
在本地阅读本书,需要安装 `mdbook` 程序。根据操作系统的不同,安装 `mdbook` 程序有所不同。
### 在 Linux 系统上
```console
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
cargo install mdbook
```
### 在 Windows 上
在 “Powershell管理员"Administrator: Windows Powershell" 中,先安装 `choco`
```powershell
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
```
经由 `choco` 安装 `msys2`
```powershell
choco install -y msys2
```
`msys2` 中安装 `mdbook`
```console
pacman -S mingw-w64-x86_64-mdbook
```
安装好 `mdbook` 后, 带一些命令行参数和开关运行服务器:
```console
mdbook serve ~/rust-lang-zh_CN -p 8080 -n 127.0.0.1 --open
```
> 注:当在 Windows 系统上时,咱们要在 `msys2` 的终端窗口中运行此命令。
此时,将在操作系统的默认浏览器中,打开本书。
# 前言和简介
虽然这样说有些含糊其辞,但基本上可说 Rust 编程语言,是一种 *赋能empowerment*不管你当前在用哪种语言编写代码Rust 都可以赋予你更大能力,在编程之路上走得更远,在你先前的各种领域,更具信心地编写程序。
@@ -157,4 +116,4 @@ Rust 语言也希望带给众多其他用户以支持;这里提到的只是一
## 本书的源码
本书所产生的源码,可在 [Github: gnu4cn/rust-lang](https://github.com/gnu4cn/rust-lang) 下载到。
本书所产生的源码,可在 [Github: gnu4cn/rust-lang](https://github.com/gnu4cn/rust-lang-zh_CN/releases/tag/v0.2.0) 下载到。

6
append_end.sh Normal file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
PWD="$(pwd)"
for f in $(find "src/" -type f -name "*.md" ); do
if [[ "${f}" == *"SUMMARY"* ]] || [[ "${f}" == *"README"* ]]; then continue; fi
echo -e "\n\nEnd\n\n" >> "$PWD/$f"
done

View File

@@ -1,21 +1,27 @@
[book]
authors = ["Lenny Peng"]
language = "zh"
multilingual = false
src = "src"
title = "Yet another Chinese rust-lang book."
description = "又一本 rust-lang 书Yet another Chinese rust-lang book。"
[preprocessor.last-changed]
command = "mdbook-last-changed"
renderer = ["html"]
[preprocessor.pagetoc]
[output.html]
additional-css = ["theme/pagetoc.css"]
additional-js = ["theme/pagetoc.js"]
git-repository-url = "https://github.com/gnu4cn/rust-lang-zh_CN"
git-repository-icon = "fa-github"
google-analytics="G-1D4GZBE5C5"
[output.html.print]
enable = false
[output.html.search]
enable = false
[output.html.favicon]
png = true

View File

@@ -0,0 +1,8 @@
[package]
name = "backyard"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@@ -0,0 +1 @@
pub mod vegetables;

View File

@@ -0,0 +1,2 @@
#[derive(Debug)]
pub struct Asparagus {}

View File

@@ -0,0 +1,8 @@
use crate::garden::vegetables::Asparagus;
pub mod garden;
fn main() {
let plant = Asparagus {};
println! ("I'm growing {:?}!", plant);
}

View File

@@ -1,7 +1,6 @@
fn main() {
let condition = true;
let number = if condition { 5 } else { "six" };
println! ("number 的值为:{}", number);
println! ("number 的值为:{number}");
}

View File

@@ -0,0 +1,8 @@
[package]
name = "data_types"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@@ -0,0 +1,7 @@
fn main() {
let c = 'z';
let z: char = ''; // 带有显式的类型注解
let heart_eyed_cat = '😻';
println! ("c 为 {c}, z 为 {z}, 爱心猫{heart_eyed_cat}");
}

View File

@@ -1,9 +1,9 @@
fn main() {
let x = plus_one(-1);
let x = plus_one(5);
println! ("x 的值为:{}", x);
println! ("x 的值为:{x}");
}
fn plus_one(x: i32) -> i32 {
x + 1
x + 1;
}

View File

@@ -8,4 +8,4 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rand = "0.8.3"
rand = "0.8.5"

View File

@@ -1,29 +1,11 @@
use rand::Rng;
use std::{cmp::Ordering, io, process};
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic! ("Guess 类型值必须在 1 与 100 之间,收到的是 {}", value);
}
Guess { value }
}
pub fn value(&self) -> i32 {
self.value
}
}
fn main() {
loop {
println! ("\n---猜出这个数来!---");
let secret_number: i32 = rand::thread_rng().gen_range(1..101);
let secret_number: u32 = rand::thread_rng().gen_range(1..101);
// println! ("随机生成的秘密数字为:{}", secret_number);
@@ -34,31 +16,27 @@ fn main() {
io::stdin()
.read_line(&mut guess)
.expect("读取行失败......");
.expect("读取行失败/failed to read line");
if guess.trim().eq("Q") || guess.trim().eq("quit") { process::exit(0); }
// let guess: u32 = guess.trim().parse().expect("请输入一个数字!");
let guess: i32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => { println! ("请输入一个数字!"); continue },
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => { println! ("请输入一个数字!"); continue },
};
if guess < 1 || guess > 100 {
println! ("秘密数字将在 1 和 100 之间");
continue
}
println! ("你猜的数为:{}", guess);
match guess.cmp(&secret_number) {
Ordering::Less => println! ("太小"),
Ordering::Greater => println! ("太大"),
Ordering::Less => println! ("太小!"),
Ordering::Greater => println! ("太大!"),
Ordering::Equal => {
println! ("你赢了!");
println! ("你赢了!");
break
},
}
}
}
}

View File

@@ -1,6 +1,6 @@
fn main() {
use std::collections::HashMap;
use std::collections::HashMap;
fn main() {
let text = "hello world wonderful world";
let mut map = HashMap::new();
@@ -11,4 +11,6 @@ fn main() {
}
println! ("{:?}", map);
}

View File

@@ -0,0 +1,7 @@
[package]
name = "hello-async"
version = "0.1.0"
edition = "2021"
[dependencies]
trpl = "0.2.0"

View File

@@ -0,0 +1,18 @@
use std::{thread, time::Duration};
fn main() {
let (tx, mut rx) = trpl::channel();
thread::spawn(move || {
for i in 1..11 {
tx.send(i).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
trpl::run(async {
while let Some(message) = rx.recv().await {
println!("{message}");
}
});
}

View File

@@ -0,0 +1,6 @@
[package]
name = "idomatic_use"
version = "0.1.0"
edition = "2021"
[dependencies]

View File

@@ -0,0 +1,7 @@
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert(1, 2);
println! ("{:#?}", map);
}

View File

@@ -0,0 +1,6 @@
[package]
name = "io_project"
version = "0.1.0"
edition = "2021"
[dependencies]

View File

@@ -0,0 +1,38 @@
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq! (vec! ["safe, fast, productive."], search(query, contents));
}
}
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>>{
let contents = fs::read_to_string(config.file_path)?;
for line in search(&config.query, &contents) {
println! ("{line}");
}
Ok(())
}

View File

@@ -0,0 +1,3 @@
fn main() {
println!("Hello, world!");
}

View File

@@ -0,0 +1,8 @@
[package]
name = "loop_label"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@@ -0,0 +1,24 @@
fn main() {
let mut count = 0;
'counting_up: loop {
println! ("count = {count}");
let mut remaining = 10;
loop {
println! ("remaining = {remaining}");
if remaining == 9 {
break;
}
if count == 2 {
break 'counting_up;
}
remaining -= 1;
}
count += 1;
}
println! ("End count = {count}");
}

View File

@@ -1,7 +1,7 @@
fn main() {
for number in (1..4).rev() {
println! ("{}!", number);
}
let a = [10, 20, 30, 40, 50];
println! ("发射!!");
for el in a {
println! ("the value is: {el}");
}
}

View File

@@ -0,0 +1,8 @@
[package]
name = "match_demo"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@@ -0,0 +1,78 @@
#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
Alabama,
Alaska,
Arizona,
Arkansas,
California,
Colorado,
Connecticut,
Delaware,
Florida,
Georgia,
Hawaii,
Idaho,
Illinois,
Indiana,
Iowa,
Kansas,
Kentucky,
Louisiana,
Maine,
Maryland,
Massachusetts,
Michigan,
Minnesota,
Mississippi,
Missouri,
Montana,
Nebraska,
Nevada,
NewHampshire,
NewJersey,
NewMexico,
NewYork,
NorthCarolina,
NorthDakota,
Ohio,
Oklahoma,
Oregon,
Pennsylvania,
RhodeIsland,
SouthCarolina,
SouthDakota,
Tennessee,
Texas,
Utah,
Vermont,
Virginia,
Washington,
WestVirginia,
Wisconsin,
Wyoming,
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println! ("来自 {:?} 州的 25 美分硬币!", state);
25
}
}
}
fn main() {
let coin = Coin::Quarter(UsState::Wyoming);
println!("{}", value_in_cents(coin));
}

View File

@@ -0,0 +1,3 @@
在文件 poem.txt 中检索to
Are you nobody, too?
How dreary to be somebody!

View File

@@ -0,0 +1,8 @@
[package]
name = "my-project"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@@ -0,0 +1,3 @@
fn main() {
println!("Hello, world!");
}

View File

@@ -0,0 +1,8 @@
[package]
name = "option_demo"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@@ -0,0 +1,6 @@
fn main() {
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y;
}

View File

@@ -1,42 +1,9 @@
fn main() {
let s = String::from("The quick brown fox jumps over the lazy dog.");
// 函数 first_word 在 String 值的切片上有效,不管是部分还是全部的切片
let word = first_word(&s[0..6]);
println! ("{}", word);
let word = first_word(&s[..]);
println! ("{}", word);
// 函数 first_word 还在 String 变量的引用上有效,而 String 变量的引用
// 与 String 值的整个切片是等价的
let word = first_word(&s);
println! ("{}", word);
let s_string_literal = "hello word";
// 函数 first_word 在字符串字面值上有效,不论是部分还是整体
let word = first_word(&s_string_literal[0..6]);
println! ("{}", word);
let word = first_word(&s_string_literal[..]);
println! ("{}", word);
// 由于字符串字面值已经 是 字符串切片,因此无需切片语法,这
// 也是有效的!
let word = first_word(s_string_literal);
println! ("{}", word);
let reference_to_nothing = dangle();
}
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
fn dangle() -> &String {
let s = String::from("hello");
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
&s
}

View File

@@ -9,17 +9,13 @@ impl Rectangle {
self.width * self.height
}
fn width(&self) -> bool {
self.width > 0
fn can_hold(&self, other: &Rectangle) -> bool {
(self.width > other.width && self.height > other.height)
|| (self.width > other.height && self.height > other.width)
}
fn can_hold(&self, other: &Rectangle) -> bool {
(self.width > other.width && self.height > other.height) ||
(self.width > other.height && self.height > other.width)
}
fn square(size: u32) -> Rectangle {
Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
@@ -28,25 +24,21 @@ impl Rectangle {
fn main() {
let rect1 = Rectangle {
width: 30,
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
width: 10,
height: 40,
};
let rect3 = Rectangle {
width: 45,
height: 25,
width: 48,
height: 28,
};
let sq1 = Rectangle::square(28);
let sq2 = Rectangle::square(35);
println! ("rect1 可以装下 rect2 吗?{}", rect1.can_hold(&rect2));
println! ("rect1 可以装下 rect3 吗?{}", rect1.can_hold(&rect3));
println! ("rect1 可以装下 sq1 吗?{}", rect1.can_hold(&sq1));
println! ("rect1 可以装下 sq2 吗?{}", rect1.can_hold(&sq2));
println! ("rect1 可以容纳 rect2 吗?{}", rect1.can_hold(&rect2));
println! ("rect1 可以容纳 rect3 吗?{}", rect1.can_hold(&rect3));
}

View File

@@ -20,7 +20,7 @@ impl List {
fn main() {
let a = Rc::new(
Cons(
5,
5,
RefCell::new(Rc::new(Nil))
)
);
@@ -30,7 +30,7 @@ fn main() {
let b = Rc::new(
Cons(
10,
10,
RefCell::new(Rc::clone(&a))
)
);

View File

@@ -0,0 +1,8 @@
[package]
name = "slices"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@@ -0,0 +1,20 @@
fn main() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
assert_eq! (slice, &[2, 3]);
}
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}

View File

@@ -2,10 +2,10 @@ fn main() {
let s = "नमस्ते";
for c in s.chars() {
println!("{}", c);
println! ("{}", c);
}
for b in s.bytes() {
println!("{}", b);
println! ("{}", b);
}
}

View File

@@ -0,0 +1,29 @@
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
let user2 = User {
email: String::from("another@example.com"),
..user1
};
println! ("{}", user2.email);
}

View File

@@ -0,0 +1,8 @@
[package]
name = "tuple_demo"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@@ -0,0 +1,22 @@
use std::io;
fn main() {
let a = [1, 2, 3, 4, 5];
println! ("请输入一个数组索引。");
let mut index = String::new();
io::stdin()
.read_line(&mut index)
.expect("读取行失败failed to read line");
let index: usize = index
.trim()
.parse()
.expect("输入的所以并非一个数字");
let element = a[index];
println! ("位于索引 {index} 出的元素值为:{element}");
}

View File

@@ -1,29 +1,4 @@
use std::io;
use std::process;
fn main() {
let a = [1, 2, 3, 4, 5];
println! ("请输入一个数组索引。");
let mut index = String::new();
io::stdin()
.read_line(&mut index)
.expect("读取行失败");
let index: usize = match index.trim()
.parse() {
Ok(num) => num,
Err(_) => {
println! ("输入的索引并非数字");
process::exit(0);
}
};
let element = a[index];
println! (
"位于索引 {} 处的元素值为:{}",
index, element);
let mut spaces = " ";
spaces = spaces.len();
}

View File

@@ -1,26 +1,8 @@
fn main() {
let mut v = vec! [100, 32, 57];
for i in &mut v {
*i += 50;
println! ("{}", i);
}
let mut v = vec! [1, 2, 3, 4];
#[derive(Debug)]
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let last = v.pop().unwrap();
let row = vec! [
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
println! ("{:#?}", &row[0])
// dbg! (v);
println!("{last}, {:?}", v);
}

View File

@@ -7,3 +7,8 @@
This URL is invalid, sorry. Please use the navigation bar or search to continue. It will be redirected to home in <span class="sec-count" style="font-style: italic;font-weight:bold;font-size:large;">n</span> seconds...
End

View File

@@ -13,51 +13,10 @@ rustc 1.68.0 (2c8cc3432 2023-03-06)
在线阅读: [rust-lang.xfoss.com](https://rust-lang.xfoss.com)
本地阅读:[`mdbook` 本地运行](./local_serving.md)
---
## 在本地阅读
在本地阅读本书,需要安装 `mdbook` 程序。根据操作系统的不同,安装 `mdbook` 程序有所不同。
### 在 Linux 系统上
```console
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
cargo install mdbook
```
### 在 Windows 上
在 “Powershell管理员"Administrator: Windows Powershell" 中,先安装 `choco`
```powershell
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
```
经由 `choco` 安装 `msys2`
```powershell
choco install -y msys2
```
`msys2` 中安装 `mdbook`
```console
pacman -S mingw-w64-x86_64-mdbook
```
安装好 `mdbook` 后, 带一些命令行参数和开关运行服务器:
```console
mdbook serve ~/rust-lang-zh_CN -p 8080 -n 127.0.0.1 --open
```
> 注:当在 Windows 系统上时,咱们要在 `msys2` 的终端窗口中运行此命令。
此时,将在操作系统的默认浏览器中,打开本书。
# 前言和简介
这并不总是那么清楚,但 Rust 编程语言从根本上讲是关于 *赋能empowerment*无论你现在写的是哪种代码Rust 都能使你达到更远的地方,在比以前更广泛的领域自信地编程。
@@ -137,7 +96,7 @@ Rust 语言希望也能支持许多其他用户;这里提到的只是一些最
第 10 章深入探讨了泛型、特性和生命期,他们让咱们有能力定义出适用于多种类型的代码。第 11 章是关于测试的,即便有 Rust 的安全保证,为确保咱们程序逻辑正确,测试仍是必要不可缺少的。在第 12 章中,我们将对 `grep` 命令行工具中的一个子集的功能建立自己的实现,他可以在文件中搜索文本。为此,我们将使用我们在前几章中讨论的许多概念。
第 13 章探讨了闭包和迭代器:来自于函数式编程语言的 Rust 特性。在第 14 章中,我们将更深入地研究 Cargo并讲到与他人共享库的最佳实践。第 15 章讨论了标准库提供的智能指针和实现其功能的特质。
第 13 章探讨了闭包和迭代器:来自于函数式编程语言的 Rust 特性。在第 14 章中,我们将更深入地研究 Cargo并讲到与他人共享库的最佳实践。第 15 章讨论了标准库提供的灵巧指针和实现其功能的特质。
第 16 章,咱们将过目不同的并发编程模型,并探讨 Rust 如何帮助咱们大胆地以多线程方式编程。第 17 章着眼于 Rust 习语如何与您可能熟悉的面向对象编程原则进行比较。
@@ -155,4 +114,9 @@ Rust 语言希望也能支持许多其他用户;这里提到的只是一些最
## 本书的源码
本书所产生的源码,可在 [Github: gnu4cn/rust-lang](https://github.com/gnu4cn/rust-lang-zh_CN) 下载到。
本书所产生的源码,可在 [Github: gnu4cn/rust-lang](https://github.com/gnu4cn/rust-lang-zh_CN/releases/tag/v0.2.0) 下载到。
End

View File

@@ -3,392 +3,12 @@
现在就开始 Rust 之旅!有很多要掌握的东西,不过千里之行,始于足下。本章将讨论:
- 在 Linux、macOS 及 Windows 上安装 Rust;
- 编写一个打印出 `Hello, world!` 的程序来;
- Rust 的包管理器和构建系统 Cargo 的使用。
- 编写一个打印出 `Hello, world!` 的程序;
## 安装
- 使用 Rust 的包管理器与构建系统 Cargo 。
第一步即是安装 Rust。这里将通过 `rustup` 这个用于管理 Rust 版本及相关工具的命令行工具,来下载 Rust。要下载 Rust就需要互联网连接。
> 注意:若由于某些原因而不愿使用 `rustup`,那么请参考 [其他 Rust 安装方式页面](https://forge.rust-lang.org/infra/other-installation-methods.html) 了解更多选项。
End
接下来就是要按照最新的稳定版 Rust 编译器。Rust 的稳定性保证了本书中所有示例都将在较新的 Rust 版本下可持续编译。由于 Rust 经常会改进错误消息和告警,因此在不同版本之间,输出可能会略有不同。也就是说,任何使用以下步骤所安装的较新、稳定版 Rust都将如本书内容中所期望的那样工作。
> 关于**命令行注释**
> 在本章及全书中,都会给出一些在终端中用到的命令。他们是一些应在以 `$` 开始的终端中输入的行。至于这个 `$` 字符,是无需输入的;这个字符表示每条命令的开头。那些不以 `$` 开头的行,通常给出的是上一命令的输出。此外,那些特定于 `PowerShell` 的示例中,将使用 `>` 而不是 `$`。
### 在 Linux 与 macOS 上安装 `rustup`
若使用的是 Linux 或 macOS那么请打开一个终端然后输入下面的命令
```console
$ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
```
此命令会下载一个脚本并开始 `rustup` 工具的安装,而 `rustup` 将安装最新的稳定版 Rust。可能会提示输入 `sudo` 密码。在安装成功后,就会出现下面这行!
```console
Rust is isntalled now. Great!
```
这里还将需要一个连接器linker这是个Rust要用来将其编译好的输出组合起来形成一个文件的程序。似乎你的电脑上以及有了一个这样的连接器了。若收到连接器错误信息那么就应安装一个 C 语言编译器C 编译器通常会包含着连接器的。由于一些常用 Rust 包对 C 代码有依赖且需要 C 编译器,因此 C 编译器也是有用的。
在 macOS 上,可通过运行下面的命令,获取到一个 C 编译器:
```console
$ xcode-select --install
```
Linux 用户一般都会安装 GCC 或 Clang至于具体哪种 C 编译器,则是依据他们所用 Linux 分发版本的文档可以确定。比如若使用的是 Ubuntu那么就可以安装 `build-essential` 软件包。
### 在 Windows 上安装 `rustup`
在 Windows 上,请前往 [https://www.rust-lang.org/tools/install](https://www.rust-lang.org/tools/install) 页面,并按照安装 Rust 的指令进行安装。在安装过程的某个时刻,将收到为何需要 Visual Studio 2013 或更新版本的 C++ 构建工具的说明。而最简单的获取到构建工具的方法,则是安装 [Visual Studio 2019 构建工具](https://visualstudio.microsoft.com/visual-cpp-build-tools/)。在询问将要安装何种工作负载workloads请确保 `C++ build tolls` 被选中,还要确保包含 Windows 10 SDK 及英语语言包。
本书接下来用到的命令,在 `cmd.exe``PowerShell` 中都可工作。若其中有特定区别,本书将会解释要用哪个。
## 更新与卸载
在通过 `rustup` 安装了 Rust 后,更新到最新版本就容易了。在 `shell` 中运行下面的更新脚本:
```console
$ rustup update
```
而要卸载 Rust 和 `rustup`,只需在 `shell` 中运行下面的卸载脚本:
```java
$ rustup self uninstall
```
## 问题排除
要检查当前是否安装了 Rust, 请开启一个 `shell` 并敲入这行命令:
```console
$ rustc --version
```
就会看到版本编号、合并哈希(`commit` hash以及已发布的该最新稳定版本合并日期以下面这种格式
```console
rustc x.y.z (abcabcadc yyyy-mm-dd)
```
若看到这个信息,那么就已成功安装了 Rust若看不到这个信息且是在 Windows 上,那么就请在 `%PATH%` 系统变量中检查一下 Rust 在不在里面。若那一点问题都没有而 Rust 仍就不工作,那么可在数个地方需求帮助。其中最便利的就是 [Rust 官方 Discord](https://discord.gg/rust-lang) 上的 `#beginners` 频道了。在那里可与其他 Rust 公民(一种无厘头的自我称呼)聊天,他们可以帮助到你。其他不错的资源包括 [用户论坛](https://users.rust-lang.org/) 和 [Stack Overflow](https://stackoverflow.com/questions/tagged/rust)。
## 本地文档
Rust 的安装,也包含了一份本地文档,因此可离线阅读到这本地文档。运行 `rustup doc` 即可在浏览器中打开这本地文档。
在任何时候遇到标准库所提供的类型或函数,而又确定他做些什么或该怎样使用这类型或函数时,就可以使用 API 文档来搞明白他是怎么回事!
## `Hello, World!`
既然已经安装好了 Rust, 那么就来编写第一个 Rust 程序吧。在掌握一门新语言时,传统就是要编写一个小的、打印出文字 `Hello, World!` 到屏幕上的程序,因此这里也会干这同样的事情!
> 注意本书假定读者对命令行有着基本的熟悉。Rust 对代码在何处编辑和使用何种工具编辑没有特别要求,因此若优先选择某种集成开发环境,而非命令行,那么使用喜好的 IDE 即可。许多 IDE 都有某种程度的 Rust 支持;请查看 IDE 文档了解有关细节信息。近来Rust 团队已着手启动良好的IDE支持且此方面已取得极大进展
### 创建一个项目目录
这里是以构造一个保存 Rust 代码的目录开始的。对于 Rust 来说,代码位居何处并不重要,不过对于本书中的练习与项目,是建议在主目录下构造一个 `projects` 目录,并把全部项目放在那里的。
请打开一个终端,并输入下面的这些命令来构造一个 `projects` 的目录,和一个在 `projects` 下用于 "Hello, World!" 项目的目录。
对于 Linux、macOS 和 Windows 上的 `PowerShell`, 请输入:
```console
$ mkdir ~/rust-lang/projects
$ cd ~/rust-lang/projects
$ mkdir hello_world
$ cd hello_world
```
而对于 Windows 的 CMD 请输入:
```console
> mkdir "%USERPROFILE%\rust-lang\projects"
> cd /d "%USERPROFILE%\rust-lang\projects"
> mkdir hello_world
> cd hello_world
```
### 编写及运行 Rust 程序
接下来,就要构造一个源代码文件,并命名为 `main.rs`。Rust 文件总是以 `.rs` 扩展名结束。若要在文件名中是一多个单词,那么请使用下划线来将这些单词隔开。比如,请使用 `hello_world.rs` 而不是 `helloworld.rs`
现在就要打开这个刚创建出的 `main.rs` 文件,并敲入清单 1-1 中的代码。
文件名:`main.rs`
```rust
fn main() {
println!("Hello, World!");
}
```
*清单 1-1打印`Hello, World!` 的程序*
保存这个文件并回到终端窗口。在 Linux 或 macOS 上,请输入下面的命令来编译和运行这个文件:
```console
$ rustc main.rs
$ ./main
Hello, World!
```
在 Windows 上,就要输入命令 `.\main.exe` 而不是 `./main`
```console
> rustc main.rs
> .\main.exe
Hello, World!
```
而不论所在操作系统为何,字符串 `Hello, World!` 都应打印到终端。而若没有看到这个输出,那么请回到安装小节的 [“问题排除”](#问题排除) 部分获取帮助。
如确实打印出了 `Hello, World!`,那么恭喜你!你已正式编写除了一个 Rust 程序了。那就让你成为了一名 Rust 程序员了 -- 欢迎!
### Rust 程序解析
来仔细回顾一下刚才在 “Hello World” 程序中发生了什么。这是谜团中第一部分:
```rust
fn main() {
}
```
这些行定义了 Rust 中的一个函数。这个 `main` 函数比较特殊:在每个可执行的 Rust 程序中,他总是第一个开始运行的代码。这第一行声明了一个名为 `main` 的、没有参数且不返回任何值的参数。若函数有参数,那么参数就应位处圆括号`()`内部。
还有就是,请注意函数体是包裹在花括号`{}`中的。Rust 要求将全部函数体都用花括号包裹起来。将开头的花括号与函数声明放在同一行,并在二者之间加上一个空格,是良好的代码风格。
若想要在多个 Rust 项目之间保持一种标准的编码风格,那么就可以使用一个名为 `rustfmt` 的自动格式化工具,来以一种特定样式对代码进行格式化。与 `rustc` 一样Rust 团队已将此工具包含在标准的 Rust 发布中,因此在你的电脑上就应该已经有了这个格式化工具了!请查看在线文档了解更多详情。
在这个`main` 函数里头,是下面的代码:
```rust
println!("Hello, World!");
```
这行代码完成了此小程序的全部工作:他将文字打印到屏幕。这里有四个需要注意的重要细节。
首先Rust 编码风格是缩进四个空格,而非一个制表符;
其次,`println!` 调用了一个 Rust 的宏a Rust macro。若他调用的是个函数那么就应输入 `println` (是不带 `!` 的)。在后续的第 19 章,将详细讨论 Rust 的宏。而现在,则只需知道 `!` 的使用表示是在调用某个宏而不是普通函数,同时宏不会总是遵循与函数同样的规则;
第三,就是看到的 `Hello, World!` 这个字符串了。这里时将此字符串作为参数,传递给 `println!` 的,且这个字符串是被打印到屏幕上的;
最后,这行语句是以分号(`;`结束的这表示该表达式结束同时下一表达式已准备好开始。Rust 代码的多数行,都是以分号结束的。
### 编译和运行是分开的步骤
这里刚刚运行了一个新近创建出的程序,那么来检视一下该过程的每个步骤。
在运行某个 Rust 程序之前,必须要通过敲入 `rustc` 命令并将源代码文件名字,作为`rustc`的参数加以传入,这样来使用 Rust 编译器对其进行编译,像下面这样:
```console
$ rustc main.rs
```
若你有 C 或 C++ 的背景知识,那么就会注意到这与 `gcc``clang` 类似。在成功编译后Rust 就会输出一个二进制可执行文件。
在 Linux、macOS 和 Windows 上的 PowerShell 之上,就可以通过在 `shell` 中敲入 `ls` 看到这个可执行文件。在 Linux 与 macOS 上,将看到下面这两个文件。而在 Windows 上的 PowerShell 中,则会看到与使用 CMD 一样的以下三个文件。
```console
$ ls
main main.rs
```
在 Windows 的 CMD 中,就应输入下面的东西:
```console
> dir /B %= 这里的 /B 选项表示只显示文件名 =%
main.exe
main.pdb
main.rs
```
这显示了带有 `.rs` 扩展名的源代码文件、那个可执行文件Windows 上的 `main.exe`,对于其他平台则是 `main`),以及,在使用 Windows 时,一个包含了调试信息的、带有 `.pdb` 扩展名的文件。从此处,就像下面这样来运行这里的 `main``main.exe`
```console
$ ./main # 或在 Windows 上的 .\main.exe
```
若这里的 `main.rs` 就是那个 “Hello, World!” 程序,那么这行命令就会将 `Hello, World!` 打印到你的终端了。
若你对某门动态语言,诸如 Ruby、Python 或者 JavaScript 更为熟悉那么可能就不习惯于将编译和运行某个程序作为分开的步骤。Rust 是门 *提前编译* 语言an *ahead-of-time compiled* language这意味着可对程序进行编译而将可执行文件交给他人他们可在未安装 Rust 的情况下运行编译好的可执行文件。而若将某个 `.rb``.py`,或者 `.js` 文件交给某人时,他们就需要安装好相应的 Ruby、Python 或 JavaScript 实现。不过在这些语言中,仅需一个命令来编译和运行他们的程序。在编程语言设计中,每件事都有所取舍。
对于简单的程序来说,用 `rustc` 编译就足够了,但随着项目的成长,就希望对所有选项进行管理,并令到代码分享更为简便。接下来,就要介绍 Cargo 工具了,这工具将帮助我们编写出实用的 Rust 程序。
## 你好Cargo
Cargo 是 Rust 的构建系统和包管理器。由于 Cargo 处理了很多任务,诸如构建代码、下载代码所依赖的库,以及这些库的构建等等,因此绝大多数 Rust 公民都使用这个工具,来管理他们的 Rust 项目。我们把这些库叫做代码需要依赖we call the libraries that your code needs *dependencies*)。)
对于最简单的那些 Rust 程序,比如才写的那个,是没有任何依赖的。因此若使用 Cargo 来构建这个 `Hello, World!` 项目,那么就只会用到 Cargo 处理代码构建的部分。而随着更为复杂 Rust 程序的编写,就会添加依赖,而在开始一个用到 Cargo 的项目时,完成依赖添加就会容易得多。
由于广大 Rust 项目都用到了 Cargo本书其余部分就假定了也使用 Cargo。若使用了在 [安装](#安装) 小节中提到的官方安装器进行的 Rust 安装那么Cargo就已与 Rust 一起安装好了。而若是以其他方式安装的 Rust那么就要通过在终端中敲入下面的命令来检查 Cargo 是否已安装妥当:
```console
$ cargo --version
```
若能看到版本号,那么就有了这个工具!而若看到错误,诸如 `command not found`,就请查看你的安装方式的文档,找到怎样单独安装 Cargo 的方法。
### 使用 Cargo 创建项目
下面来使用 Cargo 创建一个新项目,并看看与原先的 “Hello, World!” 项目有何不同。现在导航至 `projects` 目录(或确定下来的保存代码的其他地方)。然后不论在那个操作系统之上,运行下面的命令:
```console
$ cargo new hello_cargo
$ cd hello_cargo
```
这第一个命令创建出了一个新的名为 `hello_cargo` 目录。这里就已将项目命名为了 `hello_cargo`,然后 Cargo 将其文件创建在了同名的目录里面。
进入到 `hello_cargo` 目录并列出那些文件。就会看到 Cargo 已经为我们生成了两个文件和一个目录:一个 `Cargo.toml`文件与一个里头有着 `main.rs` 文件的 `src` 目录。
`cargo new` 还初始化了一个新的、带有 `.gitignore` 文件的 Git 代码仓库。若是在一个既有的 Git 代码仓库运行的 `cargo new`,那么就不会生成那些 Git 文件;通过运用 `cargo new --vcs=git` 可重写此行为。
> 注意Git 是种常用的版本控制系统。可通过上面的 `--vcs` 命令行参数,让 `cargo new` 使用其他版本控制系统或不使用版本控制系统。请运行 `cargo new --help`命令来查看所有可用选项。
文件名:`Cargo.toml`
```toml
[package]
name = "hello_cargo"
version = "0.1.0"
edition = '2021'
[dependencies]
```
*清单 1-2由 `cargo new` 所生成的 `Cargo.toml` 的内容*
该文件是 [TOML](https://toml.io/) *Tom's Obvious, Minimal Language* 格式的,这是 Cargo 的配置格式。
该文件的第一行, `[package]`,是个小节标题,表示接下来的语句是在对一个包进行配置。随着往这个文件添加越来越多的信息,就会添加其他小节。
接下来的三行,对 Cargo 用于编译程序所需的信息进行了配置:项目名称、版本号及要使用的 Rust 版本。在 [附录 E](Appendix_E.md) 中会讲到这个 `edition` 关键字。
`Cargo.toml` 的最后一行,`[dependencies]`,是要列出项目全部依赖小节开始的地方。在 Rust 中,代码包被称为 *包裹crates*。此项目无需任何其他包裹,在第 2 章中的头一个项目,就会用到依赖包裹,因此在那时就会用到这个依赖小节。
现在打开 `src/main.rs` 然后看看:
文件名:`src/main.rs`
```rust
fn main() {
println! ("Hello, World!");
}
```
Cargo 以及为我们生成了一个 “Hello, World!” 的程序,这个自动生成的程序就跟之前在清单 1-1 中的一样!到现在,先前的项目与这个 Cargo 生成的项目的不同之处,就是 Cargo 是将代码放在那个 `src` 目录中的,同时在顶层目录还有了一个 `Cargo.toml` 配置文件。
Cargo 希望那些源代码文件,存留在 `src` 目录里头。而顶层的项目目录,只用于 `README` 文件、许可证信息、配置文件及其他与代码无关的东西。使用 Cargo 有助于对项目的组织。一切都有了个地方且一切都在各自的地方there's a place for everything, and everything is in its place
若没有使用 Cargo 来开始项目,就如同先前在 “Hello, World!” 项目中所做那样,那么仍旧可使用 Cargo 将其转换为一个项目。将项目代码移入到 `src` 目录并创建出一个适当的 `Cargo.toml` 文件来:
```console
$ cd hello_world
$ mkdir src
$ mv main.rs src/
$ cargo init
```
### 构建和运行一个 Cargo 项目
现在来看看在使用 Cargo 来构建和运行那个 “Hello, World!” 程序有什么不同之处!在 `hello_cargo` 目录,通过敲入下面的命令,来构建该项目:
```console
$ cargo build  ✔
Compiling hello_cargo v0.1.0 (/home/peng/rust-lang/projects/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 0.45s
```
此命令创建出在 `target/debug/hello_cargo`(或 Windows 上的`target\debug\hello_cargo.exe`)中,而非当前目录下的一个可执行文件。可使用下面这个命令运行那个可执行程序:
```console
$ ./target/debug/hello_cargo # 或者在 Windows 上的 .\target\debug\hello_cargo.exe
Hello, world!
```
若一切顺利,那么 `Hello, World!` 就会被打印到终端。首次运行 `cargo build`,还会造成 Cargo 在顶层目录创建一个新文件:`Cargo.lock`。该文件会跟踪项目中各个依赖的精确版本。由于这个项目没有依赖因此该文件有些稀疏。绝无必要手动修改此文件Cargo 会为我们管理他的内容。
文件名:`Cargo.lock`
```toml
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "hello_cargo"
version = "0.1.0"
```
*清单 1-3, `Cargo.lock`*
这里刚刚使用 `cargo build` 构建了一个项目,并用 `./target/debug/hello_cargo` 运行了这个项目,不过这里还可以将代码编译和运行编译结果,全部在一个命令,`cargo run`,中完成:
```console
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/hello_cargo`
Hello, World!
```
请注意这次并未见到表示 Cargo 曾在编译 `hello_cargo` 的输出。Cargo 发现这些文件并未发生改变,因此他就运行了那个二进制文件。若曾修改过源代码,那么 Cargo 就会在运行这个项目之前,重新构建该项目,从而会看到这样的输出:
```console
$ cargo run  ✔
Compiling hello_cargo v0.1.0 (/home/peng/rust-lang/projects/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/hello_cargo`
Hello, Cargo!
```
Cargo 还提供了一个叫做 `cargo check` 的命令。此命令会对代码进行快速检查,以确保代码可被编译,但该命令不会产生出可执行程序:
```console
$ cargo check  ✔
Checking hello_cargo v0.1.0 (/home/peng/rust-lang/projects/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 0.35s
```
这里为何不要一个可执行文件呢?通常,由于 `cargo check` 跳过了产生出可执行程序的步骤,因此他要比 `cargo build` 快得多。但在编写代码时,要持续检查已完成的工作时,那么 `cargo check` 的使用,就会加速工作流程!由于这个原因,许多的 Rust 公民,都会在编写他们的程序时,定期运行 `cargo check`,来确保程序通过编译。而在准备好使用可执行文件的时候,在运行 `cargo build`
来概括一下到现在,已经掌握的有关 Cargo 的内容:
- 使用 `cargo new` 就可以创建出项目;
- 使用 `cargo build` 就可以构建出项目;
- 使用 `cargo run` 就可以一步完成项目的构建和运行;
- 使用 `cargo check`就可以在不产生出二进制程序的情况下,对项目加以构建以进行错误检查;
- Cargo 是将构建结果保存在 `target/debug` 目录,而不是保存在与源代码同样的目录。
使用 Cargo 的一个额外优势,就是不论是在何种操作系统上工作,那些命令都是同样的。基于这个原因,本书后续就不再提供针对 Linux 与 macOS以及Windows 的特别说明了。
### 发布目的的构建
在项目最终准备好发布时,就可以使用 `cargo build --release` 来带优化地对其进行编译了。该命令将创建出一个位于 `target/release`,而非 `target/debug` 中的可执行文件。其中的那些优化,会令到项目的 Rust 代码运行得更快,不过开启这些优化,将增加程序编译的时间。这就是为什么有两种不同配置文件的原因:一个配置是为开发目的,在希望快速且频繁地对项目进行重新构建时使用的配置,而另一个,则是为构建要给到用户的、不会反复重新构建的、将尽可能快速运行的最终程序所用到的配置。在要对程序进行性能测试时,就一定要运行 `cargo build --release`,并对 `target/release` 中的可执行程序进行性能测试。
### 约定俗成的 Cargo
对于那些简单项目,相比于使用 `rustc`Cargo 并未提供到很多价值然而在程序变得愈加错综复杂时他就会证明他的价值了。对于那些由多个代码箱crates 构成的复杂项目,让 Cargo 来对构建进行协调,就要容易得多。
即使这个`hello_cargo` 项目如此,此刻也用到了将在接下来的 Rust 编程生涯中会用到的真正工具。事实上,对于在任何既有的 Rust 项目,都应使用下面这些命令,使用 Git 来检出代码,然后前往到项目目录,进而加以构建:
```console
$ git clone example.org/someproject
$ cd someproject
$ cargo build
```
更多有关 Cargo 的信息,请查看看[Cargo 文档](https://doc.rust-lang.org/cargo/)。

View File

@@ -2,13 +2,18 @@
**Programming a Guessing Game**
让我们通过一起完成一个实践项目来学习 Rust 吧! 本章介绍了一些常见的 Rust 概念,告诉咱们如何在一个真正的程序中使用他们。咱们将学习到 `let``match`、方法、关联函数、外部代码箱等!在接下来的章节中,我们将更详细地探讨这些概念。在这一章中,咱们将只是练习基础知识。
我们将实现一个经典的初级编程问题:一个猜数游戏。他是这样工作的:程序将生成一个 `1``100` 之间的随机整数。然后他将提示玩家输入一个猜测。在输入猜测后,程序将显示猜测是否过低或过高。如果猜测正确,游戏将打印一条祝贺信息并退出
咱们来一起通过一个实践项目,了解 Rust 吧!本章通过演示如何在一个实际程序中,如何运用他们,从而介绍一些常见 Rust 概念。咱们将了解 `let``match`、方法、关联函数、外部代码箱等!在接下来的章节中,我们将更详细地探讨这些概念。在本章中,咱们将只练习这些基本知识
我们将实现一个经典的初学者编程问题:猜数游戏。其原理如下:程序将随机生成一个介于 1 和 100 之间的整数。然后,程序会提示玩家,输入一个猜测值。猜测值输入后,程序会显示猜测值是过低还是过高。如猜测正确,游戏将打印一条祝贺信息并退出。
## 建立一个新项目
要建立一个新的项目,请进入咱们在第一章中创建的 `projects` 目录,并使用 Cargo 构造一个新项目,像下面这样:
**Setting Up a New Project**
要建立一个新项目,请进入咱们在第 1 章中,创建的 `projects` 目录,并使用 Cargo 创建一个新项目,像这样:
```console
@@ -16,9 +21,10 @@ $ cargo new guessing_game
$ cd guessing_game
```
第一条命令,`cargo new`项目名`guessing_game`)作为第一个参数。第二条命令则是前往到这个新项目的目录。
第一条命令,`cargo new`项目名`guessing_game`)作为第一个参数。第二条命令会更改到新项目的目录。
查看生成的 `Cargo.toml` 文件:
看一下生成的 `Cargo.toml` 文件:
文件名:`Cargo.toml`
@@ -33,7 +39,8 @@ edition = "2021"
[dependencies]
```
正如咱们在第 1 章中所看到的,`cargo new` 咱们生成一个 "Hello, world!" 程序。请看 `src/main.rs` 文件:
正如咱们在第 1 章中所看到的,`cargo new` 会给咱们生成一个 "Hello, world!" 程序。请`src/main.rs` 文件:
文件名:`src/main.rs`
@@ -53,15 +60,18 @@ $ cargo run
Hello, world!
```
当咱们需要快速迭代项目时,`run` 命令就会派上用场,就像我们在这个游戏中所做的那样,在继续下一迭代之前快速测试每个迭代
当咱们需要在某个项目快速迭代,就像我们在这个游戏中将要做的,在进入下一迭代之前快速测试每一次迭代时,`run` 这个命令就会派上用场
请重新打开 `src/main.rs` 文件。咱们将在这个文件中,编写所有代码。
重新打开 `src/main.rs` 文件。咱们将在这个文件中编写所有的代码。
## 处理一个猜数
**Processing a Guess**
这个猜数游戏的第一部分,将请求用户的输入、处理那个输入,进而检查该输入是否有着正确格式。这里将实现玩家输入一个猜数开始。请敲入清单 2-1 中的代码到 `src/main.rs` 里去。
猜数游戏程序的第一部分,将请求用户输入,处理输入信息,并检查输入信息是否符合预期形式。首先,我们将允许玩家输入一个猜测。请在 `src/main.rs` 中,输入清单 2-1 中的代码。
文件名:`src/main.rs`
@@ -69,41 +79,46 @@ Hello, world!
use std::io;
fn main() {
println! ("这个数");
println! ("猜这个数!");
println! ("请输入你的数。");
println! ("请输入你的数。");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("读取行失败");
.expect("读取行失败/failed to read line");
println! ("你猜的数为{}", guess);
println! ("你猜的{guess}");
}
```
*清单 2-1从用户获取一个猜数并将其打印出来的代码*
*清单 2-1从用户获取一个猜数并将其打印出来的代码*
这段代码包含了大量信息,所以我们来逐行查看。要获取用户输入,然后将结果打印输出,我们就需要将 `io` 这个输入/输出库,带入作用域。`io` 库来自标准库,即 `std`
此代码包含了很多信息,那么这里就来一行一行的走一遍。要获取到用户输入并将结果打印出来,就需要将 `io` 输入/输出库带入到作用域中。而 `io` 库则是来自名为 `std` 的标准库:
```rust
use std::io;
```
默认情况下Rust 只有少数几个定义在标准库中、由标准库带入到每个程序的项目by default, Rust has a few items defined in the standard library that it brings into the scope of every program。这个集合被称为 Rust 序曲(`prelude`),在 [标准库文档](https://doc.rust-lang.org/std/prelude/index.html) 中可找到全部的标准库 `prelude` 项目。
在要使用的类型,不在 Rust 序曲集合中时,就必须将那个类型,显式地通过 `use` 语句带入到作用域中。`std::io` 库的使用,提供了数个有用特性,包括接收用户输入的能力
默认情况下Rust 在标准库中定义了一组,其会带入到每个程序作用域中的项目。这组项目被称为 *前奏prelude*,咱们可以在 [标准库文档](https://doc.rust-lang.org/std/prelude/index.html) 中,查看他当中的全部项目
如果咱们打算使用的某个类型不在前奏中,那么就必须用一条 `use` 语句,显式地将该类型带入作用域。使用 `std::io` 库,提供到咱们许多有用功能,包括接受用户输入的能力。
正如咱们在第 1 章所看到的,`main` 函数是该程序的入口the entry point into the program
就跟在第 1 章所见到的那样,`main` 函数即是这个程序的进入点:
```rust
fn main() {
```
`fn` 语法声明了一个函数,而这个圆括号`()`,表示这里没有参数,同时那个花括号,`{`该函数的函数体的开始
`fn` 语法声明了一个函数括号 `()` 表明没有参数;花括号,`{`开启了该函数的。
同样如同咱们在第 1 章中所掌握的,`println!` 是个将字符串打印到屏幕上的宏a macro
同样与在第 1 章中所了解的那样,`println!` 是个将字符串打印到屏幕的宏macro
```console
println! ("猜出这个数来!");
@@ -111,82 +126,104 @@ fn main() {
println! ("请输入你猜的数。");
```
这段代码打印提示消息,表明该游戏是什么及正在请求用户输入。
这段代码打印出说明游戏是什么,以及要求用户输入的提示信息
## 使用变量保存那些值
接下来,就要创建一个 *变量variable* 来存储用户输入,像下面这样:
### 使用变量存储值
**Storing Values with Variables**
接下来,我们将创建一个 *变量variable*,来存储用户输入,就像这样:
```rust
let mut guess = String::new();
```
现在这个程序就变得有趣起来了!这小小一行,可是有很多东西。这里使用了 `let` 语句来创建这个变量。下面是另一个示例:
现在,程序开始变得有趣起来!在这短短一行中,发生了很多事情。我们使用 `let` 语句,创建这个变量。下面是另一个例子:
```rust
let apples = 5;
```
行代码创建了一个新的名为 `apples` 的变量,并将其绑定到了值 `5`。在 Rust 中,默认变量是不可变的immutable)。在后续第 3 章 [变量可变性](Ch03_Common_Programming_Concepts.md#变量及可变性) 小节,将对此概念加以讨论。而要让变量可变,就要将变量名字前加 `mut` 关键字:
一行创建了个名为 `apples`变量,并将其与值 5 绑定。在 Rust 中,变量默认是不可变的immutable,这意味着一旦我们赋给变量某个值,该值就不会改变。我们将在第 3 章 [变量可变性](programming_concepts/variables_and_mutability.md) 小节中,详细讨论这一概念。要使某个变量可变,我们就要在该变量名字前,添`mut` 关键字:
```rust
let apples = 5; // 不可变immutable
let mut bananas = 5; // 可变mutable
```
> 注意:这里的 `//` 语法,开始一条持续到那个行结束的代码注释。Rust 会忽略注释中的全部内容。在 [第 3 章](Ch03_Common_Programming_Concepts.md#注释) 将更加详细讨论代码注释。
> **注意**:其中的 `//` 语法,开始一条持续到行尾的注释。Rust 会忽略注释中的所有内容。我们将在 [第 3 章](programming_concepts/comments.md) 详细讨论注释。
回到这个猜数游戏程序,那么此刻就明白了那个 `let mut guess` 将引入一个名为 `guess` 的可变变量。而那个等号(`=`),则是告诉 Rust现在要将某个东西绑定到该变量了。等号右边就是要绑定到 `guess` 的那个值,而这个值则是调用 `String::new` 的结果,这个 `String::new`,则又是一个返回一个 `String` 实例的函数。`String` 是由标准库提供的一个字符串类型,为一个可增大的、经 UTF-8 位编码的文本a growable, UTF-8 encoded bit of text
回到猜数游戏程序,咱们现在知道,`let mut guess` 将引入一个名为 `guess` 的可变变量。等号(`=`)告诉 Rust我们现在打算给变量绑定某个东西。等号右边是 `guess` 要被绑定到的,调用 `String::new` 函数的结果,该函数会返回一个 `String` 的新实例。而 [`String`](https://doc.rust-lang.org/std/string/struct.String.html) 是标准库所提供的一种字符串类型是可增长的、UTF-8 编码的文本。
`::new` 代码行中的 `::` 语法,表明 `new``String` 类型的一个关联函数。所谓 *关联函数associated function*,是实现于某个类型(此示例中即 `String`)上,实现的一个函数。这个 `new` 函数,会创建一个新的空字符串。在许多类型上,咱们都会发现一个 `new` 函数,因为他是个那些构造某种新值函数的通用名称。
在那个 `::new` 代码行中的 `::` 语法,表示其中的 `new``String` 类型的一个关联函数an associated funtion of the `String` type。至于 *关联函数associated function*,指的是应用到某种类型上的函数,在此实例中,类型就是 `String` 了。这个 `new` 函数创建了一个新的、空空的字符串。由于`new` 是个构造某种新值的常见函数,因此在许多类型上,都将找到 `new` 函数。
整体上看,这个 `let mut guess = String::new();` 语句,完成了一个当前绑定到新的、`String` 类型空实例的可变变量的创建。总算讲清楚了
总的来说,`let mut guess = String::new();` 这行,创建了当前绑定了一个新的、空的 `String` 实例的一个可变变量。呼
## 接收用户输入
回顾程序第一行上,以 `use std::io;` 从标准库所包含进来的输入/输出功能。那么现在就要调用那个 `io` 模组中的 `stdin` 函数,该函数将实现对用户输入的处理:
### 接收用户输入
**Receiving User Input**
回顾一下,在程序的第一行,我们使用 `use std::io;`,包含了标准库中的输入/输出功能。现在,我们将调用 `io` 模组中,将允许咱们处理用户输入的 `stdin` 函数:
```rust
io:stdin()
.readline(&mut guess)
```
在程序开头不曾以 `std::io` 方式,将 `io`导入,那么仍然可以将该函数写作 `std::io::stdin` 形式,而对其进行使用`stdin` 函数返回的是 `std::io::Stdin` 的实例, 而 `std::io::Stdin` 则表示终端标准输入句柄的类型the `stdin` function returns an instance of `std::io::Stdin`, which is a type that represents a handle to the standard input for your terminal
如果我们没有在程序开头,使用 `use std::io;` 导入 `io`,我们仍然可以通过将此函数调用,写成 `std::io::stdin` 来使用这个函数`stdin` 函数返回 [`std::io::Stdin`](https://doc.rust-lang.org/std/io/struct.Stdin.html) 的一个实例,而这是一种表示终端标准输入句柄的类型,a type that represents a handle to the standard input for your terminal。
接下来的代码行 `.readling(&mut guess)` 调用了标准输入句柄类型实例上的 `read_line` 方法,用于获取用户输入。这里还将 `&mut guess` 作为 `read_line` 的参数进行了传递,以告诉 `read_line` 函数,将用户输入存入到哪个字符串中。`read_line`整个职能,就要将用户敲入到标准输入的东西,追加到某个字符串(在不覆盖掉这个字符串内容的情况下),因此这里是将那个字符串作为参数传递的。为了这个 `read_line` 方法可以修改其内容,这里的字符串就要是可变的
接下来`.read_line(&mut guess)` 这一行,调用了标准输入句柄上的 `read_line` 方法,获取用户输入。我们还将 `&mut guess` 作为参数,传递给 `read_line`告诉他将用户输入的内容,存储在哪个字符串中。`read_line`全部工作,就是接收用户输入标准输入的内容,并将其追加到某个字符串中(不会覆盖其内容),因此我们要将该字符串作为参数传递给他。这个字符串参数,必须是可变的,这样这个方法才能更改该字符串的内容
其中的 `&` 表明该参数是个 *引用reference*而引用则是一种无需将数据多次拷贝到内存中的情况下,就可以实现代码多个部分对该数据进行读写的特性(注:在 C 家族语言中,`&`表示内存地址,因此 Rust 中的引用,与指针有类似之处)。引用是一项复杂特性,同时 Rust 的主要优之一,就是安全而便利地运用引用的方式。对于完成这个猜数游戏,是不必对这些细节有过多了解的。现在要明白的是,与变量类似,引用默认也是不可变的。因此,这里就要写 `&mut guess` 而不是 `&guess`,来令到这个到 `guess` 的引用为可变的。(第 4 章将更详细地对引用进行解释。)
其中的 `&`,表示该参数是个 *引用reference*其提供了一种,让咱们的代码多个部分,在无需多次将某个数据复制到内存中的情况下,即可访问该数据的方法。引用是一项复杂特性, Rust 的主要优之一,就是引用的使用,既安全又简单。对于完成现在这个程序,咱们并不需要知道很多的这些细节。现在,咱们只需知道引用与变量一样,默认情况下是不可变的。因此,咱们需要写 `&mut guess` 而不是 `&guess`,来使其可变。(第 4 章将更详细地解释引用)。
## 处理潜在的带有 `Result` 的程序失效
**Handle Potential Failure with the `Result` Type**
### 使用 `Result` 处理潜在失效
**Handle Potential Failure with `Result`**
我们仍在研究这行代码。我们现在讨论的是第三行文字,但请注意,他仍然是单个逻辑行代码的一部分。下一部分,便是这个方法:
这里还在解析代码行。尽管这里讨论的是代码文本的第三行,但他仍是单个逻辑代码行的一部分。接下来的部分是这个方法:
```rust
.expect("读取输入失败");
```
这代码本可以写成下面这样:
我们本可以将这段代码写成:
```rust
io::stdin().read_line(&mut guess).expect("读取输入失败");
```
不过这样的一个长代码行,难于阅读,因此最好将其分开为多个断行。在以 `.method_name()` 语法调用方法时,通过引入另起一行及缩进,来将长的代码行拆分为短代码行,通常是明智的。下面就来说说这一行完成了什么。
不过,一个长行难于阅读,所以最好将其分开。在咱们使用 `.method_name()` 语法调用某个方法时,引入一个换行符,以及另外的空白,来帮助拆分长行,通常是明智之举。现在我们来讨论一下,这一行完成了什么。
前面讲过`read_line`方法将用户入的东西,放入到传递给他的那个字符串中,然而 `read_line` 还会返回一个值 -- 在此实例中,返回的就是一个 `io::Result` 类型值。Rust 在他的标准库中,有着数个名为 `Result` 的类型:这是一个泛型的 `Result`,对于那些子模组都有着特定版本,比如这里的 `io::Result``Result` 的那些类型都属于 [枚举enumerations](Ch06_Enums_and_Pattern_Matching.md#定义一个枚举),枚举常被写`enums`枚举有着一套被称作 *变种variants* 的可能值。枚举常常是和 `match` 关键字一起使用的,而 `match` 则是一种条件判断,在符合某个条件时,就可以很方便地根据枚举中的哪个变种,来执行不同代码
如早先曾提到的`read_line`将用户入的任何内容,放入我们传给他的字符串中,但他还会返回一个 `Result` 值。[`Result`](https://doc.rust-lang.org/std/result/enum.Result.html) 是个 [*枚举enumeration*](Ch06_Enums_and_Pattern_Matching.md),通常称`enum`是可处于多种可能状态之一的一种类型。我们称每种可能状态,为一个 *变种variant*
第 6 章将深入涵盖到枚举数据结构。而这些 `Result` 类型的目的,则是对错误处理信息进行编码
[第 6 章](Ch06_Enums_and_Pattern_Matching.md) 将详细介绍枚举。这些 `Result` 类型的目的,是要编码错误处理信息。
这个 `Result` 的变种,就是 `Ok` `Err``Ok`表示操作成功的,而在 `Ok`部,就是成功生成的值。相反 `Err`种,则意味着操作失败,同时 `Err` 包含了关于操作失败的方式与原因
`Result` 变体,为 `Ok` `Err``Ok`表示操作成功,且 `Ok` 内是成功生成的值。`Err`体表示操作失败,同时 `Err` 包含了操作如何失败,或为何失败的信息
`Result` 类型的那些值,跟其他任何类型都差不多,在这些值上都定义了一些方法。`io::Result` 实例,就有一个可供调用的 [`expect` 方法](https://doc.rust-lang.org/std/result/enum.Result.html#method.expect)。在这个 `io::Result` 实例是个 `Err` 变种时,那么`expect` 方法就会导致程序崩溃,并传递给 `expect` 方法的参数显示出来。若 `read_line` 方法返回了一个 `Err`,那很可能是来自所采用操作系统错误的结果if the `read_line` method returns an `Err`, it would likely be the result of an error coming from the underlying operating system。而若该 `io::Result` 实例是个 `Ok` 值,那么 `expect` 方法就会取得那个 `Ok` 所保存的返回值,并将该值返回,从而就可以使用到这个返回值。在此实例中,那个值,就是用户输入的字节数
与任何类型的值一样,`Result` 类型的值,也有定义于其上的一些方法。`Result` 实例,有个咱们可以调用的 `expect` 方法。如果 `Result` 实例是个 `Err` 值,`expect` 就将导致程序崩溃,并显示咱们作为参数传递给 `expect` 那条信息。在 `read_line` 方法返回了一个 `Err`,那很可能是底层操作系统出错所致。在这个 `Result` 实例是个 `Ok``expect` 取得那个 `Ok` 持有的返回值,并将该值返回给咱们,以便咱们可以使用他。在本例中,该值就是用户输入的字节数。
如果咱们不调用 `expect`,这个程序会编译,但会收到警告:
若这里没有对 `expect` 方法进行调用,那么该程序会编译,不过会收到一条告警信息:
```console
$ cargo build  ✔
$ cargo build  ✔
Compiling guessing_game v0.1.0 (/home/peng/rust-lang/projects/guessing_game)
warning: unused `Result` that must be used
--> src/main.rs:10:5
@@ -202,37 +239,45 @@ warning: `guessing_game` (bin "guessing_game") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 0.48s
```
Rust 警告说不曾对返回自 `read_line` `Result`进行使用,表程序没有对可能的错误加以处理。
Rust 警告说咱们不曾使用 `read_line` 返回的那个 `Result` 值,表程序没有处理可能出现的错误
消除该警告信息的正确方式,就是要老老实实地编写错误处理代码,而在这个实例中,则只要在问题发生时,崩溃掉这个程序即可,因此这里就可以使用 `expect`。在第 9 章的 [带有 Result 的可恢复错误](Ch09_Error_Handling.md#带有-result-的可恢复错误) 小节,会掌握到如何从错误中恢复过来
消除这条警告的正确方法,是着手编写错误处理代码,但在我们的例子中,我们只打算在某个问题出现时,让程序崩溃,因此咱们可以使用 `expect`。咱们将在 [第 9 章](error_handling/result.md) 中,学习如何从错误中恢复。
## 使用 `println!` 的占位符将值打印出来
### 使用 `println!` 占位符打印值
**Printing Values with `println!` Placeholders**
紧接着那个结束花括号前面,就只有剩下的一行代码要讨论了:
这段代码中,除了结尾的大括号,到目前为止就只有一行需要讨论了:
```rust
println! ("你猜的数是:{}", guess);
println! ("你猜的数是:{guesss}");
```
行代码是将此刻包含了用户输入的那个字符串打印出来。其中的那套花括号 `{}` 就是一个占位符placeholder请将`{}`当作是些在那个地方留有一个值的小螃蟹。使用一些这样的花括号,就可以打印出多个值来:第一套花括号保留着在格式字符串之后列出的第一个值,第二套保留着第二个值,如此等等。一个 `println!` 调用中多个值的打印,看起来会是下面这样
一行会打印现在包含了用户输入的那个字符串。其中的 `{}` 花括号组,是个占位符:可以把 `{}` 想象成一对用来固定某个值于某处的小蟹钳。在打印某个变量的值时,变量名可以放在这对花括号内。在打印表达式的计算结果时,就要在格式字符串中,放置空的大括号,然后在格式字符串后,添加以逗号分隔的表达式列表,并按照相同的顺序打印到各个空的大括号占位符中。在一次 `println!` 调用中,打印一个变量和一个表达式的结果,将如下所示
```rust
let x = 5;
let y = 10;
println! ("x = {} 同时 y = {}", x, y);
println! ("x = {x} 而 y + 2 = {}", y + 2);
```
此代码将打印出 `x = 5 同时 y = 10`
此代码将打印出 `x = 5 y + 2 = 12`
## 对第一部分的测试
下面就来测试一下这猜数游戏的第一部分。用 `cargo run` 运行他:
### 测试第一部分
**Testing the First Part**
我们来测试一下,这个猜数游戏的第一部分。请使用 `cargo run` 运行他:
```console
$ cargo run  ✔
$ cargo run  ✔
Compiling guessing_game v0.1.0 (/home/peng/rust-lang/projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 0.68s
Running `target/debug/guessing_game`
@@ -242,133 +287,138 @@ $ cargo run  ✔
你猜的数为6
```
,这游戏的第一部分就算完成了:这里正从键盘获取输入,并随后将输入打印出来。
此,这游戏的第一部分已经完成:我们从键盘获取输入,然后打印出来。
## 生成秘密数字
接下来,就需要生成一个用户将要试着去猜的秘密数字了。生成的秘密数字应每次都不相同,这样这游戏在多次玩的时候才有趣。为了不让这个游戏太难,这里要用一个 `1``100` 之间的随机数。Rust 在其标准库中尚未包含随机数功能。不过 Rust 团队还真的提供了一个 [`rand` 代码箱](https://crates.io/crates/rand),这里就姑且把这样的代码箱,称之为功能吧。
**Generating a Secret Number**
### 运用代码箱a Crate 获取到更多功能
请记住,所谓代码箱,即为一些 Rust 源代码文件的集合。之前曾构建好的项目,则是一个 *二进制的代码箱binary crate*,那是个可执行程序。而 `rand` 代码箱,则是个 *库代码箱library crate*这样的库代码箱包含了预期将在其他程序中会用到的代码同时库代码箱自身并不能执行the `rand` crate is a *library crate*, which contains code intended to be used in other programs, and can't be executed on its own
接下来,我们需要生成一个用户将尝试猜测的秘密数字。秘密数字应每次都不一样,这样游戏才会有趣,才能玩多次。我们将使用 1 到 100 之间的某个随机数这样游戏就不会太难。Rust 尚未在其标准库中包含随机数功能。不过Rust 团队提供了一个包含上述功能的 [`rand` 代码箱](https://crates.io/crates/rand)
### 使用代码箱获得更多功能
**Using a Crate to Get More Functionality**
请记住,代码箱是一些 Rust 源代码文件的集合。我们正在构建的项目,是个 *二进制代码箱binary crate*,这是个可执行代码箱。而 `rand` 代码箱,则是个 *库代码箱library crate*,其中包含的代码,旨在用于其他程序,而不能在其自身上执行。
Cargo 的外部板块的协调能力,正是 Cargo 的真正亮点所在。在编写用到 `rand` 的代码之前,我们需要修改那个 `Cargo.toml` 文件,将 `rand` 代码箱作为一个依赖项。现在请打开该文件,在 Cargo 为咱们创建的 `[dependencies]` 小节标题下,添加下面一行。请务必使用这个版本号,准确指定 `rand`,否则本教程中的代码示例,可能无法运行:
Cargo 对外部代码箱的协调能力,正是 Cargo 真正闪耀之处。在能够编写出用到 `rand` 库代码箱的代码之前,先要将 `Cargo.toml` 加以修改,将 `rand` 代码箱作为依赖包含进来。打开那个文件并将下面的行,添加到底部、那个 Cargo 创建出的`[dependencies]` 小节标题之下。要确保像这里一样,带着版本号地精确指明 `rand` 代码箱,否则此教程中的代码示例就不会工作。
文件名:`Cargo.toml`
```toml
rand = "0.8.3"
rand = "0.8.5"
```
在这 `Cargo.toml` 文件中,凡在某个标题之后的东西,都是那个小节的一部分,到另一小节开始为止。在 `[dependencies]` 小节,告诉 Cargo 的是项目依赖哪些外部代码箱external crates以及所需的这些代码箱版本。在此实例中,就指明了有着语义版本指示符the semantic version specifier `0.8.3` `rand` 代码箱。Cargo 能明白 [语义版本控制(Sementic Versioning](http://semver.org/)有时也叫做 *`SemVer`*,这是编制版本号的标准。数字 `0.8.3` 实际上是 `^0.8.3` 的缩写,表示高于 `0.8.3` 低于 `0.9.0` 的任何版本。Cargo 认为这些版本有着与 `0.8.3` 兼容的公共 APIs同时这样的规定确保了将获取到在本章中代码仍可编译的情况下最新的补丁发布。那些 `0.9.0` 及更高的版本,无法保证接下来示例用到同样的 API。
在这 `Cargo.toml` 文件中,某个头部之后的所有内容,都是小节的一部分,一直持续到另一小节开始。在 `[dependencies]` 中,咱们告诉 Cargo,咱们的项目依赖哪些外部代码箱,以及咱们需要这些代码箱的哪些版本。在例中,我们使用语义版本说明符 `0.8.5`,指定了 `rand` 这个代码箱。Cargo 能够理解语义版本编号,Semantic Versioning有时也称为 *SemVer*,这是一种编写版本号的标准。`0.8.5` 实际上是 `^0.8.5` 的缩写,表示至少是 `0.8.5` 低于 `0.9.0` 的任何版本。
Cargo 会认为,这些版本具有与 `0.8.5` 版兼容的公共 API而这一规范确保了咱们将得到仍可与本章中的代码编译的最新补丁发布。任何 `0.9.0` 或更高版本,都不能保证有着与接下来的示例中,用到的相同 API。
现在,在不修改任何代码的情况下,我们来构建一下这个项目,如清单 2-2 所示。
现在,在不修改任何代码的情况下,来构建一下这个项目,如清单 2-2 所示:
```console
$ cargo build
Updating crates.io index
Downloaded rand v0.8.3
Downloaded libc v0.2.86
Downloaded getrandom v0.2.2
Downloaded cfg-if v1.0.0
Downloaded ppv-lite86 v0.2.10
Downloaded rand_chacha v0.3.0
Downloaded rand_core v0.6.2
Compiling rand_core v0.6.2
Compiling libc v0.2.86
Compiling getrandom v0.2.2
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.10
Compiling rand_chacha v0.3.0
Compiling rand v0.8.3
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 2.53s
```
*清单 2-2-1在添加了作为依赖的 `rand` 代码箱后运行 `cargo build` 的输出(书上的输出)*
```console
$ cargo build  ✔
Updating crates.io index
Downloaded cfg-if v1.0.0
Downloaded ppv-lite86 v0.2.17
Downloaded rand_chacha v0.3.1
Downloaded rand_core v0.6.3
Downloaded getrandom v0.2.7
Downloaded ppv-lite86 v0.2.16
Downloaded cfg-if v1.0.0
Downloaded rand_core v0.6.4
Downloaded getrandom v0.2.11
Downloaded rand v0.8.5
Downloaded libc v0.2.126
Downloaded 7 crates (773.8 KB) in 3.41s
Compiling libc v0.2.126
Downloaded libc v0.2.150
Downloaded 7 crates (910.0 KB) in 4.63s
Compiling libc v0.2.150
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.16
Compiling getrandom v0.2.7
Compiling rand_core v0.6.3
Compiling ppv-lite86 v0.2.17
Compiling getrandom v0.2.11
Compiling rand_core v0.6.4
Compiling rand_chacha v0.3.1
Compiling rand v0.8.5
Compiling guessing_game v0.1.0 (/home/peng/rust-lang/projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 56.66s
Compiling guessing_game-xfossdotcom v0.1.1 (/home/chat/rust-lang-zh_CN/projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 7.08s
```
*清单 2-2-2在添加了作为依赖的 `rand` 代码箱后运行 `cargo build` 的输出(实际输出)*
*清单 2-2:将 rand 代码箱添加为依赖项后运行 `cargo build` 的输出**
这里可能会看到不同的一些版本号(归功于 `SemVer`,这些不同版本号将与示例代码全都兼容!)、不同的输出行(取决于所在的操作系统),以及这些行可能以不同顺序出现。
在包含外部依赖时Cargo 会从 *登记处registry* 拉取到那个依赖所需的全部最新版本的代码箱,而所谓登记处,则是 [Crates.io](https://crates.io/) 数据的一份拷贝。Crates.io 是 Rust 生态中的人们,发布给其他人使用的开放源代码项目的地方
咱们可能会看到一些不同的版本号(但他们都与代码兼容,这要归功于 SemVer和不同的一些行取决于操作系统而且这些行的顺序也可能不同
在更新了登记处索引之后Cargo 就对 `[denpendencies]` 小节进行查看,并下载所列代码箱中尚未下载的那些。在此实例中,尽管只列出了依赖 `rand`Cargo 还抓取了其他 `rand` 赖以运作的一些代码箱。在下载了这些代码箱之后Rust 会对他们进行了编译,并随后以这些可用的依赖,对这项目进行了编译
当我们包含了某个外部依赖项时Cargo 会从作为 [Crates.io](https://crates.io/) 上数据的一份拷贝的 *登记簿registry*获取该依赖项所需的所有内容的最新版本。Crates.io 是 Rust 生态系统中的人们,发布开源 Rust 项目供他人使用的地方
更新登记簿后Cargo 会检查 `[dependencies]` 小节,并下载列出的任何尚未下载的代码箱。在本例中,虽然我们只将 `rand` 列为依赖项,但 Cargo 还抓取了 `rand` 运作所依赖的其他代码箱。下载完这些代码箱后Rust 会对他们进行编译,然后使用这些可用依赖项,编译项目。
如果咱们不做任何修改,就立即再次运行 `cargo build`,那么除了 `Finished` 那行外咱们不会得到任何输出。Cargo 知道他已经下载并编译了依赖项,而咱们也没有在 `Cargo.toml` 文件中对依赖项做任何修改。Cargo 也知道咱们没有修改代码,所以也不会重新编译项目。无事可做,他就直接退出了。
若不做任何修改,就立即再次运行 `cargo build`,那么除了那行 `Finished` 输出之外就再也没有别的输出了。Cargo 明白他以及下载并编译好了那些依赖,还明白尚未对 `Cargo.toml` 文件做任何修改。Cargo 还知道,这里并未对项目代码做任何修改,因此他也没有对项目代码重新编译。既然无事可做,那么他就直接退出了。
```console
$ cargo build  ✔
$ cargo build  ✔
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
```
若此时打开 `src/main.rs` 文件,做个细微修改,然后保存并再次构建,那么就只会看到下面这两行输出:
如果咱们打开 `src/main.rs` 文件,进行一些简单的更改,然后保存并再次构建,咱们将只会看到两行输出:
```console
cargo build  ✔
cargo build  ✔
Compiling guessing_game v0.1.0 (/home/peng/rust-lang/projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 0.50s
```
这些行显示 Cargo 只更新了对 `src/main.rs` 文件细微修改的构建。由于依赖不曾改变,因此 Cargo 清除他可以重用那些已经下载和编译好的依赖。
这几行显示Cargo 只会根据咱们对 `src/main.rs` 文件的微小改动,来更新构建。咱们的依赖依赖并没有改变,因此 Cargo 知道,他可以重复使用已经下载并编译好的那些依赖项。
### 使用 `Cargo.lock` 文件确保可重现的构建
**Ensuring Reproducible Builds with the `Cargo.lock` File**
Cargo 具备一种不论是自己还是其他要构建代码的人来说确保每次都可以构建出同样程序组件the same artifact的机制除非另有指定Cargo 都将只使用在 `[denpendencies]` 小节中所指定的依赖版本。比如说下周 `0.8.4` 版本的 `rand` 就要释出且那个版本包含了一个重要的错误修复但也包含了一个会破坏咱们代码的特性撤回。为了应对这样的情况Rust 在首次运行 `cargo build`时,就创建了 `Cargo.lock` 文件,也就是现在在 `guessing_game` 目录下就有这么个文件。
在首次构建项目时Cargo 会找出那些依赖满足条件的所有版本,并将其写入到这 `Cargo.lock` 文件。在今后对项目进行构建时Cargo 就会查看是否存在那个 `Cargo.lock` 文件,并使用其中所指定的那些版本,而不会再次完成找出那些版本的工作了。这样就自动实现了可重现的构建。也就是说,得益于这个 `Cargo.lock` 文件,除非显式地升级了 `rand` 的版本号,项目将保持其版本为 `0.8.3`
Cargo 有着一种可以确保咱们,或其他人每次构建代码时,都能重建出相同产物的机制: Cargo 将只使用咱们所指定的依赖项版本,除非咱们另有指示。例如,下周 `rand` 代码箱的 `0.8.6` 版本将发布该版本包含了一个重要的错误修复但同时也包含了一个会破坏咱们代码的回退。为了处理这个问题Rust 会在咱们第一次运行 `cargo build` 时,创建 `Cargo.lock` 文件,所以在 `guessing_game` 目录下,我们现在会有这个文件
当咱们首次构建某个项目时Cargo 会计算出符合条件依赖项的全部版本,然后将其写入 `Cargo.lock` 文件。在咱们以后再构建项目时Cargo 就会发现 `Cargo.lock` 文件的存在,并会使用其中指定的版本,而不会再重新计算版本。这样,咱们就能自动进行可重现的构建。换句话说,由于有了 `Cargo.lock` 文件,在咱们明确升级之前,咱们的项目将保持在 `0.8.5` 版本。由于 `Cargo.lock` 文件对于可重现性构建非常重要,因此他通常会与项目中的其他代码一起,进入源代码控制系统。
### 更新代码箱来获取新版本
**Updating a Crate to Get a New Version**
在确实要更新某个代码箱时Cargo 提供了 `update` 命令,该命令会忽略 `Cargo.lock` 文件,并找出与`Cargo.toml`中的那些规格相适合的全部最新版本。Cargo 随后将把这些版本写入到 `Cargo.lock` 文件。否则的话,默认 Cargo 就会只查找那些高于 `0.8.3` 且低于 `0.9.0` 的版本。在 `rand` 库代码箱已发布了两个新的 `0.8.4``0.9.0` 版本时,此时若运行 `cargo update`,就会看到下面的输出:
当咱们确实打算更新某个代码箱时Cargo 提供了 `update` 命令,他会忽略 `Cargo.lock` 文件,并找出所有符合咱们在 `Cargo.toml` 中所要求的最新版本。然后Cargo 会把这些版本写入 `Cargo.lock` 文件。否则默认情况下Cargo 只会查找大于 `0.8.5` 且小于 `0.9.0` 的版本。如果 `rand` 代码箱发布了 `0.8.6``0.9.0` 这两个新版本,那么运行 `cargo update` 时就会看到下面的内容:
```console
$ cargo update
Updating crates.io index
Updating rand v0.8.3 -> v0.8.4
Updating rand v0.8.5 -> v0.8.6
```
Cargo 忽略了那个 `0.9.0` 的发布。此刻还会注意到在 `Cargo.lock` 文件中,一处标记现在所用 `rand` 代码箱版本为 `0.8.4` 的改变。要使用版本 `0.9.0` 或任何 `0.9.x` 系列中某个版本的 `rand`,就必须将 `Cargo.toml` 更新为下面这样:
Cargo 会忽略 `0.9.0` 的版本。此时,咱们还会注意到,`Cargo.lock` 文件中的一处变化,即咱们现在使用的 `rand` 代码箱,版本为 `0.8.6`。要使用 `rand``0.9.x` 系列中的任何版本,咱们必须更新 `Cargo.toml` 文件,使其看起来像这样:
```toml
[dependencies]
rand = "0.9.0"
```
在下次运行 `cargo build`Cargo 会更新可用代码箱的登记,并根据所指定的新版本,重新 `rand` 需求加以评估
咱们下次运行 `cargo build`Cargo 会更新可用代码箱的登记簿the registry of creates available,并根据咱们所指定的新版本,重新计算咱们的 `rand` 需求。
关于 [Cargo](http://doc.crates.io/) 及 [其生态](http://doc.crates.io/crates-io.html),还有很多内容要讲,我们将在第 14 章进行讨论但现在这就是咱们需要了解的全部内容。Cargo 让重用库变得非常容易,因此 Rustaceans 可以编写出,由多个包组合而成的小型项目。
关于 [Cargo](http://doc.crates.io/) 及 [Cargo 生态](http://doc.crates.io/crates-io.html),有很多要讲的东西,这些在第 14 章会讨论到而此时了解上面这些就够了。Cargo 实现了非常便利的库重用,因此 Rust 公民们就能够编写出,从数个软件包组合而来的那些体量较小的项目。
### 生成随机数
现在就来开始使用 `rand` 库代码箱,生成用于猜测的数字。接下来的步骤就是更新 `src/main.rs`,如下清单 2-3 所示:
**Generating a Random Number**
咱们来开始使用 `rand`,生成一个要猜的数字。下一步是要更新 `src/main.rs`,如下清单 2-3 所示。
文件名:`src/main.rs`
@@ -377,38 +427,42 @@ use std::io;
use rand::Rng;
fn main() {
println! ("猜出这个数来");
println! ("请猜数");
let secret_number = rand::thread_rng().gen_range(1..101);
let secret_number = rand::thread_rng().gen_range(1..=100);
println! ("秘密数字为:{}", secret_number);
println! ("秘密数字为:{secret_number}");
println! ("请输入你的数。");
println! ("请输入你的数。");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("读取行失败......");
.expect("读取行失败/failed to read line");
println! ("你猜的数为{}", guess);
println! ("你猜的{guess}");
}
```
*清单 2-3添加生成随机数的代码*
*清单 2-3添加代码以生成随机数*
首先,这里添加了那行 `use rand::Rng`。这 `Rng` 特质the `Rng` trait定义了一些随机数生成器实现的方法而为了使用这些方法此特质就必须要在作用域中。第 10 章将详细涵盖到特质traits
接下来在中间部分,添加了两行新代码。在第一行代码中,调用了 `rand::thread_rng` 函数,该函数给到了这里即将用到的特定随机数生成器:一个相对于当前执行线程,属于本地的随机数生成器,其用到的种子由操作系统提供。随后在这个随机数生成器实例上的 `gen_range` 方法。该方法是由前面 `use rand::Rng` 语句带入到作用域的 `Rng` 特质定义。这 `gen_range` 方法取的是一个范围表达式,这里用到的范围表达式,所采取的是 `start..end` 形式,该范围表达式包含了左边界,但排除了右边界,因此就要指定 `1..101` 来求得一个 `1``100` 之间的数字。或者也可以传递范围 `1..=100`,这是等价的
首先,我们添加 `use rand::Rng;` 这行。`Rng` 特质the `Rng` trait定义了随机数生成器所实现的那些方法而这个特质必须位于咱们要用到那些方法的作用域中。第 10 章将详细介绍特质
> 注意:对于不知道到底该使用那个 Rust 特质以及要调用代码箱的那些方法和函数的情况那么每个代码箱都有着如何使用他的说明文档。Cargo 的另一灵巧特性,便是通过运行 `cargo doc --open` 命令,就会构建出由全部本地依赖提供的文档来,并在浏览器中打开这些文档。比如说若对 `rand` 这个代码箱的其他功能感兴趣,那么运行 `cargo doc --open` 命令然后点击左侧边栏中的 `rand` 即可进一步了解
接下来,我们在中间添加两行。在第一行中,我们调用了给到我们要用到随机数生成器的 `rand::thread_rng` 函数:一个相对于当前执行线程本地的,由操作系统提供种子的随机数发生器。然后,我们调用了这个随机数生成器上的 `gen_range` 方法。该方法由咱们已使用 `use rand::Rng;` 语句,带入到作用域的 `Rng` 特质所定义。`gen_range` 方法,取一个范围表达式作为参数,并生成该范围内的一个随机数。我们这里使用的范围表达式类别,形式为 `start...=end`,并包含下上边界,因此我们需要指定 `1...=100`,以请求一个介于 1 和 100 之间的数字
那第二个新行,则是打印出那个秘密数字。在开发这个程序期间,这是有用的,这样能够对程序进行测试,不过在最终版本那里就会删除这行代码。若程序在一开始就打印出谜底,显然这就算不上是个游戏了。
尝试运行几次这个程序:
> **注意**咱们不会只要知道使用哪个特质、调用某个代码箱的哪些方法与函数因此每个代码箱都有使用说明文档。Cargo 的另一个特色便是,运行 `cargo doc --open` 命令,就会在本地构建出咱们所有依赖项提供的文档,并在浏览器中打开。例如,如果咱们对 `rand` 代码箱的其他功能感兴趣,那么请运行 `cargo doc --open`,并点击左侧边栏中的 `rand`。
第二新的行,会打印秘密数字。这在我们开发程序时很有用,可以用来测试程序,但我们会在最终版本中删除他。如果程序一开始就打印出答案,那就不算是个游戏了!
请试着运行几次程序:
```console
$ cargo run  ✔  4s 
$ cargo run  ✔  4s 
Compiling guessing_game v0.1.0 (/home/peng/rust-lang/projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 0.54s
Running `target/debug/guessing_game`
@@ -418,7 +472,7 @@ $ cargo run  ✔ 
86
你猜的数为86
$ cargo run  ✔  9s 
$ cargo run  ✔  9s 
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/guessing_game`
猜出这个数来!
@@ -429,12 +483,17 @@ $ cargo run  ✔ 
```
就会得到不同的随机数字,且他们都应是 `1``100` 之间的数字。非常棒
咱们应得到不同的随机数字,且他们都应是 1 到 100 之间的数字。干得好
## 将猜数与秘数相比较
既然有了用户输入和随机数,就可以加以比较了。比较的步骤在下面的清单 2-4 中给出了。请注意这个代码还不会编译,原因后面会解释。
**Comparing the Guess to the Secret Number**
现在我们有了用户输入和随机数,我们可以对他们进行比较。该步骤如下清单 2-4 所示。请注意,这段代码还不能编译,我们将对此进行说明。
<a name="list_2-4"></a>
文件名:`src/main.rs`
```rust
@@ -448,27 +507,31 @@ fn main() {
println! ("你猜的数为:{}", guess);
match guess.cmp(&secret_number) {
Ordering::Less => println! ("太小"),
Ordering::Greater => println! ("太大"),
Ordering::Less => println! ("太小!"),
Ordering::Greater => println! ("太大!"),
Ordering::Equal => println! ("你赢了!"),
}
}
```
*清单 2-4对比较两个数可能的返回值进行处理*
*清单 2-4对比较两个数可能的返回值进行处理*
首先这里添加了另一个 `use` 语句,将标准库的一个名为 `std::cmp::Ordering` 的类型,带入到作用域。这 `Ordering` 了新是另一个枚举,且其有着 `Less``Greater``Equal` 共计三个变种。这些就是在对两个值进行比较时,三个可能的输出了。
随后在该程序底部,添加了用到这 `Ordering` 类型的五行新代码。其中的 `cmp` 方法是对两个值进行比较,并可在任何被可比较物上进行调用。`cmp` 方法会取一个要与之相比的引用a reference这里他是在将 `guess``secret_number` 相比。随后他就返回了前面用 `use` 语句带入到作用域的 `Ordering` 枚举的一个变种。这里用一个 `match` 表达式,根据以 `guess``secret_number` 中的值,对 `cmp` 调用所返回具体 `Odering` 变种,而确定出下一步要做什么
首先,我们添加了另一条 `use` 语句,从标准库中,引入名为 `std::cmp::Ordering` 的类型。`Ordering` 类型是另一个枚举,并具有 `Less``Greater``Equal` 三种变体。这正是在比较两个值时,可能出现的三种结果
`match` 表达式由数个 *支臂arms* 构成。每个支臂是由要与之匹配的 *模式pattern* ,及在给到 `match` 的值与该支臂的模式符合时应运行的代码所组成。Rust 取给到 `match` 的值,并以此检视各个支臂的模式。模式及 `match` 结构,是强大的 Rust 特性,实现对代码可能遇到的各种情况的表达,并确保对全部的这些情况进行处理。在第 6 章和第 18 章,相应地将详细涵盖到这些特性
然后,我们在底部,添加了用到这个 `Ordering` 类型的五个新行。`cmp` 这个方法,会比较两个值,并可以在任何可被比较的项目上调用。他会取一个到咱们打算比较的任何值的引用:这里他是将 `guess``secret_number` 进行比较。然后,他会返回我们通过那条 `use` 语句,带入作用域的 `Ordering` 枚举的某个变种。我们使用了一个 `match` 表达式,根据以 `guess``secret_number` 中的值调用 `cmp` 时,所返回的何种 `Ordering` 变体,来决定下一步的操作
下面就来对这里使用的 `match` 表达式的一个示例走一遍。假设说用户猜的数是 `50`,同时随机生成的秘密数这次是 `38`。在代码将 `50``38` 作比较时,由于 `50``38` 大,因此那个 `cmp` 方法就会返回 `Odering::Greater`。于是 `match` 表达式就获取到值 `Odering::Greater` 并开始对各个支臂的模式进行检查。他看了第一个支臂的模式,是 `Ordering::Less`,并发现值 `Ordering::Greater``Odering::Less` 不匹配,那么他就会忽略第一个支臂中的代码而移步到下一支臂。下一支臂的模式为 `Ordering::Greater`,这正好与 `Odering::Greater` 相匹配!那个支臂中的相关代码就会执行,进而将 `太大了!`打印到屏幕。在此场景中,由于`match` 表达式无需检视那最后的支臂,因此他就结束了
`match` 表达式由数个 *支臂arms* 组成。而一个支臂则由一个要与之匹配的 *模式pattern*,以及在给到 `match` 的值符合该支臂的模式时要运行的代码组成。Rust 会取给到 `match` 的值,并依次查看每个支臂的模式。模式与这种 `match` 结构,是 Rust 的强大功能:二者可以让咱们,表达出代码可能遇到的各种情况,并确保咱们能处理全部的这些情况。第 6 章和第 18 章,将分别详细介绍这些特性
咱们来以这里用到的这个 `match` 表达式,看一个示例。假设用户猜的是 50而这次随机生成的秘密数字是 38。
当代码将 50 与 38 比较时,`cmp` 方法将返回 `Ordering::Greater`,因为 50 大于 38。这个 `match` 表达式就会得到 `Ordering::Greater` 这个值,并开始检查每个支臂的模式。他会查看第一个支臂的模式 `Ordering::Less`,发现值 `Ordering::Greater``Ordering::Less` 不匹配,因此他会忽略该支臂的代码,而转到下一支臂。下一支臂的模式是 `Ordering::Greater`,这 *确实* 匹配 `Ordering::Greater`!该支臂中的相关代码将执行,并打印 `太大!` 到屏幕。这个 `match` 表达式在第一次成功匹配后,就会结束,因此在这种情况下,其不再查看最后一个支臂。
然而,清单 2-4 中的代码还无法编译。咱们来尝试一下:
然而清单 2-4 中的代码并不会编译。这里试着编译一下:
```console
$ cargo build  ✔
$ cargo build  ✔
Compiling guessing_game v0.1.0 (/home/peng/rust-lang/projects/guessing_game)
error[E0308]: mismatched types
--> src/main.rs:22:21
@@ -483,9 +546,10 @@ For more information about this error, try `rustc --explain E0308`.
error: could not compile `guessing_game` due to previous error
```
这些错误状态的核心,指向的是存在 *不匹配的类型mismatched types*。Rust 有着强静态类型系统Rust has a strong, static type system。不过他也有着类型推导type inference。在写下 `let mut guess = String::new()`Rust 当时就能推导出 `guess` 应是个 `String`而没有要求一定要写出该类型i`String`。但对于 `secret_number` 来说,则是一个数字类型。有几种 Rust 数字类型都可以保有一个 `1``100` 之间的值:`i32`32 位整数;`u32`32 位无符号整数;`i64`64 位整数还有一些其他的。除非有特别指明Rust 默认都是个 `i32` 整数,除非在某处给 `secret_number` 添加了引起 Rust 推断出不同数字类型的类型信息,那么 `secret_number` 的类型就会是 `i32`。上面错误的原因,就是 Rust 无法将字符串与数字类型相比较。
最后,这里就要将程序以输入形式读取到的 `String`,转换成具体数字类型,如此就可以将其与`secret_number`进行数学上的比较。这里通过将下面这行添加到 `main` 函数体完成的:
错误的核心,表明存在 *不匹配的类型mismatched types*。Rust 有着强大的静态类型系统。不过他也有着类型推断Rust has a strong, static type system. However, it also has type inference。当我们写下 `let mut guess = String::new()`Rust 就能推断出,`guess` 应是个 `String`,而未曾让我们写下类型。另一方面,`secret_number` 是一个数字类型。Rust 的一些数字类型,可以有着介于 1 和 100 之间的某个:`i32`,某个 32 位的数字;`u32`,某个无符号的 32 位数字;`i64`,某个 64 位的数字;以及其他类型。除非另有说明,否则 Rust 默认会使用 `i32`,这即为 `secret_number` 的类型,除非在其他地方,添加了导致 Rust 推断出不同的数值类型的类型信息。上面这个报出的原因,是 Rust 无法比较字符串和数字类型。
最后,我们打算将程序读取的字符串输入,转换为某个真正的数字,这样咱们就可以将其与秘密数字,进行数值比较。我们要通过在那个 `main` 函数主体中,添加下面这行,完成这一点:
文件名:`src/main.rs`
@@ -509,22 +573,38 @@ error: could not compile `guessing_game` due to previous error
}
```
添加的那行就是
该行为
```rust
let guess: u32 = guess.trim().parse().expect("请输入一个数字!");
```
这里创建了一个名为 `guess` 的变量。不过稍等一下,这个程序不是已经有一个名为 `guess` 的变量了吗?他确实已经有了个名为 `guess` 的变量,然而好在 Rust 允许以一个新的 `guess` 变量,对其先前的值进行 *遮蔽shadow* 操作的。这样的遮蔽特性,实现了对`guess` 这个变量名的重用,而非强制创建两个诸如 `guess_str``guess` 这样的独特变量。在第 3 章将对此进行更详细的讲解,此时只要明白,此特性通常用在要将某个值从一种类型转换另一类型的时候
我们创建了一个名为 `guess` 的变量。但是等等,程序不是已经有一个名为 `guess` 的变量了吗?是有的,但好在 Rust 允许我们用一个新值,对 `guess` 的前一个值进行遮蔽处理。*遮蔽特性shadowing* 允许咱们,重复使用这个 `guess` 变量名,而不必被迫创建出,诸如 `guess_str``guess` 这样的两个唯一变量。我们将在 [第 3 章](programming_concepts/variables_and_mutability.md#遮蔽shadowing) 中详细介绍这一功能,而现在我们要知道,当咱们打算将某个值从一种类型转换另一类型时,就经常会用到这一特性
这里将这个新变量,绑定到了表达式 `guess.trim().parse()`表达式中的 `guess` 援引的是原来那个包含着字符串形式输入的 `guess`。而作用在 `String` 实例上的 `trim` 方法,将消除开头和结尾的全部空白,必须要进行这个操作,才能将字符串转换到 `u32` 类型,`u32`只能包含数字数据。为了满足 `read_line` 并输入他们的猜数,用户必须要按下回车键,这样就会将一个换行字符添加到那个字符串。比如在用户敲入了 `5` 然后按下回车键`guess`看起来就会是这样:`5\n`其中的 `\n` 表示 “换行newline”。在 Windows ,按下回车键会导致一个回车字符和一个换行字符,即 `\r\n`)。这 `trim` 会将 `\n``\r\n` 消除,而结果就只 `5` 了。
我们将这个新变量,绑定到 `guess.trim().parse()` 这个表达式。表达式中的 `guess`,指的是包含了作为字符串输入的那个原始 `guess` 变量。某个 `String` 实例上的 `trim` 方法,将消除开头和结尾的空白,我们必须这样做才能将字符串 `u32` 进行比较,而 `u32` 只能包含数字数据。用户必须按下回车键,来满足 `read_line` 并输入他们的猜数,这会添加一个换行符到输入字串。例如,如果用户输入 5 并按回车键,`guess` 就会看起来是这样`5\n``\n` 表示 “换行/newline”。(在 Windows 系统中,按下回车键会产生是回车和换行,即 `\r\n`)。<sup>译注 1</sup> `trim` 方法可以去掉 `\n``\r\n`结果就只 `5` 了。
[字符串上的 `parse` 方法](https://doc.rust-lang.org/std/primitive.str.html#method.parse) 将只会在那些逻辑上可被转换成数字的字符上运作,而因此就很可能引起错误。比如说在字符串包含了 `A👍%` 时,就没有办法将其转换成一个数字。由于 `parse` 方法会失败,因此他返回的是个 `Result` 类型,这与 `read_line` 方法所做的一样(在早先的 [用 `Result` 类型处理潜在失败](#处理潜在的带有-result-的程序失效) 中讨论过)。这里再次使用 `expect` 方法对这个`Result` 进行了同样的处理。在因为 `parse` 无法从字符串创建出一个数字,而返回了一个 `Err``Result` 变种时,这个 `expect` 就会令到游戏崩溃,并将给他的那条消息打印出来。而在 `parse` 可成功将那个字符串,转换成数字时,`expect` 就会返回 `Result``Ok` 变种,同时 `expect` 会返回这里想要的、`Ok` 值中的数字。
> **译注 1**:这也是为何先前的代码:
>
```rust
let bytes = io::stdin()
.read_line(&mut guess)
.expect("读取行失败/failed to read line");
```
>
> 在 Windows 的 MSYS2 上运行时,`bytes` 的输出始终会比咱们看到的字符串,要多两个字节的原因。
[字符串上的 `parse` 方法](https://doc.rust-lang.org/std/primitive.str.html#method.parse),可将字符串转换为另一类型。在这里,我们要用他,将字符串转换为数字。我们需要使用 `let guess: u32`,告诉 Rust 我们想要的确切数字类型。`guess` 后面的冒号(`:`),告诉 Rust 我们将注解这个变量的类型。Rust 有几种内置的数字类型;这里所看到的 `u32`,是一种无符号的 32 位整数。对于小的正数来说,这是一种不错的默认选择。咱们将在 [第 3 章](https://doc.rust-lang.org/book/ch03-02-data-types.html#integer-types),了解其他数字类型。
此外,本示例程序中的这个 `u32` 注解,及那个与 `secret_number` 的比较,意味着 Rust 将推断出 `secret_number` 也应是个 `u32`。因此,现在这个比较,将是在两个相同类型值之间的了!
`parse` 这个方法,只适用于逻辑上可以转换成数字的那些字符,因此很容易出错。例如,如果字符串包含着 `A👍%`,就无法将其转换为数字。因为其可能会失败,所以 `parse` 方法会返回一个结果类型,就像 `read_line` 方法一样(早先曾在 [“使用 `Result` 处理潜在失败”](#使用-result-处理潜在失效) 小节中讨论过)。我们将再次通过使用 `expect` 方法,以同样方式处理这个 `Result`。如果 `parse` 因无法从那个字符串,创建出一个数字而返回 `Err` 的 `Result` 变种,则 `expect` 这个调用,将导致游戏崩溃,并打印出我们给到他的信息。如果 `parse` 能成功将那个字符串转换为数字,他将返回 `Result` 的 `Ok` 变种,而 `expect` 将从这个 `Ok` 值,返回我们想要的数字。
现在咱们来运行一下这个程序:
现在来运行一下这个程序!
```console
$ cargo run  101 ✘  3s 
$ cargo run  101 ✘  3s 
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/guessing_game`
猜出这个数来!
@@ -535,15 +615,19 @@ $ cargo run  101 ✘ 
太大了!
```
很棒!尽管在猜数前加了一些空格,程序仍然算出了用户猜的是 `76`。多运行几次这个程序,来验证在各种输入时其不同的表现:猜对一个数、猜个太大的数,以及猜个过小的数。
现在这个游戏大致在工作了,然而用户只能猜一次。下面就来通过添加循环对其进行修改!
不错!即使在猜数前添加了空格,程序仍然能判断出,用户猜测的数字是 76。请多运行几次程序验证在不同输入情况下的不同行为猜对数字、猜的数字太大、猜的数字太小等等。
## 用循环来实现多次猜数
我们现在已经让这个游戏的大部分工作了,但用户只能猜一次数。我们就来通过添加一个循环,改变这种情况!
## 通过循环实现多次猜数
**Allowing Multiple Guesses with Looping**
关键字 `loop` 创建出无限循环。这里就要添加一个循环,来让用户有更多机会去猜数:
`loop` 关键字会创建出一个无限循环。我们将添加一个让用户有更多机会猜出数字的循环:
文件名:`src/main.rs`
@@ -558,17 +642,18 @@ $ cargo run  101 ✘ 
// --跳过--
match guess.cmp(&secret_number) {
Ordering::Less => println! ("太小"),
Ordering::Greater => println! ("太大"),
Ordering::Equal => { println! ("你赢了!"); break },
Ordering::Less => println! ("太小!"),
Ordering::Greater => println! ("太大!"),
Ordering::Equal => println! ("你赢了!"),
}
}
}
```
可以看到,这里已将自猜数输入提示开始的全部代码,移入到循环中。请确保循环的那些代码行,都另外缩进四个空格,然后再次运行这个程序现在程序将会一直要求另一猜数,这实际上引入了新的问题。好像是用户无法退出
正如咱们所看到的,我们把从猜测输入提示开始的所有内容,都移到了一个循环中。请务必将循环的那些行,缩进另外四个空格,然后再次运行程序。这个程序现在将一直不停要求另一猜数,这实际上引入了一个新问题。用户似乎无法退出!
用户可以始终通过使用键盘快捷键 `ctrl-c` 来中断这个程序。但还有一种方法可以摆脱这个贪得无厌的怪物,正如 [“将猜测与秘密数字进行比较”](#将猜数与秘数相比较) 小节,`parse` 的讨论中所提到的:如果用户输入的答案不是数字,这个程序就会崩溃。我们可以利用这一点,允许用户退出,如下所示:
用户可一直通过键盘快捷键 `Ctrl-C`,来中断这个程序。不过还是有别的方法,来退出这头贪厌的怪兽,就像在 [将猜数与秘密数字比较](#将猜数与秘数相比较)中对 `parse` 方法讨论中提到的那样:在用户输入了非数字的答案时,程序就会崩溃。这里就利用了那个,来实现用户退出,如下所示:
```console
$ cargo run
@@ -602,11 +687,16 @@ thread 'main' panicked at '请输入一个数字!: ParseIntError { kind: Inval
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```
`quit` 就会退出这游戏,不过正如所注意到的,这样做将就要敲入别的非数字输入。至少可以是这种做法是次优的;这里想要在猜到了正确数字时,游戏也停止。
`quit` 退出这游戏,但咱们会发现,输入任何其他非数字输入,也会退出游戏。至少可以说,这是次优的;我们希望在猜中正确数字后,这个游戏也停止。
## 猜对后的退出
下面就来通过添加一条 `break` 语句,将游戏编程为在用户赢了时退出:
### 猜对后的退出
**Quitting After a Correct Guess**
我们来通过添加一个 `break` 语句,将这个游戏编程为在用户获胜后退出:
文件名:`src/main.rs`
@@ -614,23 +704,29 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
// --跳过--
match guess.cmp(&secret_number) {
Ordering::Less => println! ("太小"),
Ordering::Greater => println! ("太大"),
Ordering::Less => println! ("太小!"),
Ordering::Greater => println! ("太大!"),
Ordering::Equal => {
println! ("你赢了!");
break
println! ("你赢了!");
break;
},
}
}
}
```
`你赢了!` 后添加 `break` 代码行,令到游戏在用户猜中了秘密数字时,退出那个循环。由于该循环是 `main` 函数体的最后部分,因此退出循环也意味着退出这个程序
`你赢了!`添加 `break` 行,令到程序在用户猜秘密数字时,退出那个循环。退出那个循环,也意味着退出这个程序,因为该循环是 `main` 的最后部分。
> **译注**:这里有个有趣的地方,`break` 后的分号可有可无,`match` 表达式最后支臂后的逗号,也是可有可无的。
## 无效输入的处理
### 处理无效输入
**Handling Invalid Input**
为进一步完善游戏行为,我们可以让游戏忽略非数字,这样用户就可以继续猜测,而不是在用户输入非数字时程序崩溃。通过修改 `guess` 从字符串转换为 `u32` 的行,咱们就可以做到这一点,如下清单 2-5 所示。
为了进一步改进游戏表现,而不要在用户输入了非数字时将程序崩溃掉,那么接下来就要使得游戏忽略非数字,从而用户可以继续猜数。通过把`guess``String` 转换为 `u32` 的那行加以修改,来完成这个目的,如下面的清单 2-5 所示:
文件名:`src/main.rs`
@@ -639,33 +735,32 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
io::stdin()
.read_line(&mut guess)
.expect("读取行失败......");
.expect("读取行失败/failed to read line");
if guess.trim().eq("Q") || guess.trim().eq("quit") { process::exit(0); }
// let guess: u32 = guess.trim().parse().expect("请输入一个数字!");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => { println! ("请输入一个数字!"); continue },
Ok(num) => num,
Err(_) => continue,
};
println! ("你猜的数为{}", guess);
println! ("你猜的{guess}");
// --跳过--
```
*清单 2-5忽略非数字的猜解进而询问另一猜数,而不再是崩溃掉程序*
*清单 2-5忽略非数字的猜数并请求另一猜数,而不是让程序崩溃*
这里将原来的 `expect` 调用,转换到了一个 `match` 表达式,而实现了一错误就程序崩溃,到对错误进行处理的转变。请记住 `parse` 返回的是个 `Result` 类型,而 `Result` 则是个枚举,有着变种 `Ok``Err`。与先前对 `cmp` 方法返回结果 `Ordering` 的处理一样,这里运用了一个 `match` 表达式。
`parse` 能够成功将那个字符串,转换为数字时,他就会返回一个包含了所得结果数的 `Ok` 值。那 `Ok` 值就会匹配上第一个支臂的模式,而这个 `match` 表达式将值返回 `parse` 产生的、放在`Ok` 值里头的那个 `num` 值。那个数字就会刚好放在这里想要他呆的地方,即这里正在创建的那个新 `guess` 变量了
我们从一个 `expect` 调用,切换到了一个 `match` 表达式,以从出错时崩溃程序,转换为处理这个出错。请记住,`parse` 回返回一个 `Result` 类型,而 `Result` 是个枚举,有 `Ok``Err` 两个变种。我们在这里使用了个 `match` 表达式,就像在在处理 `cmp` 方法的 `Ordering` 结果时一样
`parse` 无法将那个字符串转换数字,他就会返回一个包含了有关该错误详细信息`Err` 值。该 `Err`与第一`match` 支臂中的 `Ok(num)` 模式匹配,不过却正好匹配第二个支臂中的 `Err(_)` 模式。其中的下划线,`_`是个收集错误信息的值a catch-all value在此示例中就是要匹配所有 `Err` 值,而不管这些 `Err` 值中包含了什么信息。那么程序就会执行第二支臂的代码,即 `continue`,这是告诉程序前往到那个 `loop` 循环的下一次迭代,进而询问另一个猜数。就这样,有效地方让程序忽略了全部 `parse` 可能会发生的错误了!
如果 `parse` 成功地将那个字符串转换数字,他返回一个包含结果数字`Ok` 值。该 `Ok`与第一支臂的模式匹配,而这个 `match` 表达式将只返回 `parse` 所生成并放入 `Ok` 值的那个 `num` 值。这个数字最终会出现在,我们要创建的新 `guess` 变量中。
如果 `parse` ** 法将该字符串转化为数字,他将返回一个其中包含了更多该错误的信息的 `Err` 值。`Err` 值不会匹配到第一个 `match` 支臂中的 `Ok(num)` 模式,但会匹配到第二个支臂中的 `Err(_)` 模式。其中的下划线 `_`是个总括值a catchall value在这个示例中我们表示要匹配所有 `Err` 值,无论他们包含什么信息。因此,程序将执行第二个支臂的代码 `continue`,这告诉程序,要前往循环的下一次迭代,而请求另一个猜数。因此,实际上,程序会忽略 `parse` 可能遇到的所有错误!
现在,程序中的一切都应按预期运行。我们来试一下他:
现在程序各方面就应如预期那样工作了。就来试试:
```console
$ cargo run  ✔
$ cargo run  ✔
Compiling guessing_game v0.1.0 (/home/peng/rust-lang/projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 0.57s
Running `target/debug/guessing_game`
@@ -681,15 +776,14 @@ $ cargo run  ✔
你赢了!
```
非常棒!只需最后一个小的优化,就将完成这个猜数游戏了。没忘记这个程序仍是把秘密数字打印出来的吧。那样做对测试来说没有问题,但却毁掉了这个游戏。这里就来将输出秘密数字的那个 `prinln!` 给删掉。下面的清单 2-6 给出了最终代码。
太棒了!最后再做一个小的调整,我们就可以完成这个猜数游戏了。请注意,程序仍在打印出秘密数字。这对测试很有效,但却毁掉了这个游戏。咱们来删除那个输出秘密数字的 `println!`清单 2-6 给出了最终代码。
文件名:`src/main.rs`
```rust
use rand::Rng;
use std::cmp::Ordering;
use std::io;
use std::process;
use std::{cmp::Ordering, io, process};
fn main() {
loop {
@@ -706,23 +800,23 @@ fn main() {
io::stdin()
.read_line(&mut guess)
.expect("读取行失败......");
.expect("读取行失败/failed to read line");
if guess.trim().eq("Q") || guess.trim().eq("quit") { process::exit(0); }
// let guess: u32 = guess.trim().parse().expect("请输入一个数字!");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => { println! ("请输入一个数字!"); continue },
Ok(num) => num,
Err(_) => { println! ("请输入一个数字!"); continue },
};
println! ("你猜的数为:{}", guess);
match guess.cmp(&secret_number) {
Ordering::Less => println! ("太小"),
Ordering::Greater => println! ("太大"),
Ordering::Less => println! ("太小!"),
Ordering::Greater => println! ("太大!"),
Ordering::Equal => {
println! ("你赢了!");
println! ("你赢了!");
break
},
}
@@ -733,8 +827,16 @@ fn main() {
*清单 2-6完全的猜数游戏代码*
## 小结
到了这里,就成功构建了这个猜数游戏。恭喜!
至此,咱们已经成功构建了这个猜数游戏。恭喜!
## 本章小结
这个项目以实践的方式,向咱们介绍了许多新的 Rust 概念:`let``match`、函数、外部代码箱的使用等等。在接下来的几章中,咱们将更详细地了解这些概念。第 3 章涵盖了大多数编程语言都有的概念,如变量、数据类型和函数等,并展示了如何在 Rust 中使用他们。第 4 章探讨了所有权,这是 Rust 不同于其他语言的一个特性。第 5 章会讨论结构体和方法语法,第 6 章解释了枚举的工作原理。
End
该项目以动手的方式,教了许多新的 Rust 概念:`let``match` 等关键字,函数、运用外部代码箱及更多。在接下来的几章中,会更深入地掌握这些概念。第 3 章涵盖了大多数编程语言都有的一些概念,诸如变量、数据类型及函数,并展示了如何在 Rust 中使用他们。第 4 章对 Rust 中的所有权ownership进行了探索所有权是一项令到 Rust 不同于其他语言的特性。第 5 章对结构体structs和方法语法method syntax进行了讨论而第 6 章解释了枚举的原理。

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,693 +1,12 @@
# 用结构体来组织相关数据
# 使用结构体结构化相关数据
**Using Structs to Structure Related Data**
*结构体(struct*,或者说 *结构(structure*实现了将多个相关值打包在一起并取个名字,而构成一个有意义的组别。在熟悉面向对象语言的情况下,那么 *结构体* 就像是对象的那些数据属性。在本章中,把元组与结构体加以比照,从而在既有认识之上,构建出对结构体的认识,并对使用结构体作为一种更佳的数据组织方式的时机,进行演示。这里会对如何定义及初始化结构体进行演示。还会讨论如何定义关联函数,尤其是那种叫做 *方法* 的关联函数,来指明与某个结构体类型相关联的行为。结构体与枚举(将在第 6 章讨论到),这两种数据结构,是充分利用 Rust 的编译时类型检查特性,在程序域中创建新类型的构件
结构体,`struct`,或 *structure*是一种自定义数据类型,允许咱们将多个构成一个有意义的组的相关值打包在一起并取个名字。如果咱们熟悉某门面向对象语言,那么一个 `struct` 就像是某个对象的数据属性。在本章中,我们把元组与结构体进行对比,以在咱们已有知识的基础上,说明结构体在何时是更好的数据组织方式
## 结构体的定义及初始化
我们将演示如何定义和实例化结构体。我们将讨论如何定义关联函数,尤其是称为 *方法methods* 的关联函数,以指定出与某个结构体类型相关的行为。结构体和枚举(会在第 6 章中讨论),是在咱们程序域中,创建出新类型,以充分利用 Rust 的编译时类型检查的两个基本构建模块。
结构体与之前 [元组类型](Ch03_Common_Programming_Concepts.md#元组类型) 小节中讨论过的元组数据结构类似,二者都保存着多个相关数据。和元组一样,结构体的各个数据片段可以是不同类型。与原则不同的是,在结构体中将给各个数据片段命名,如此各个值表示什么就清楚了。加上这些名字,就意味着相比于元组更为灵活了:不必为了给某个实例指定他的那些值,或要访问实例的那些值,而对实例数据的顺序有所依赖了。
要定义出一个结构体,就要敲入关键字 `struct`,及整个结构体的名字。结构体名字,应对安排在一起的这些数据片段的意义加以描述。随后,就要这一对花括号里头,定义出各个数据片段的名称与类型,这些数据片段,就叫做 *字段fields*。比如,下面的清单 5-1 就给出了一个保存用户账号信息的结构体。
End
```rust
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64
}
```
*清单 5-1`User` 结构体的定义*
在定义出了结构体后,要用上这个结构体,就要通过给各个字段指定具体值,创建出那个结构体的 *实例instance* 来。通过指明结构的名字,并随后加上包含了 `key: value` 键值对的一对花括号这样创建出一个实例来。键值对中的那些键就是那些字段的名字而其中的那些值则是打算保存在这些字段中的数据。不必按照在结构体中声明那些字段的顺序来对这些字段进行指明we don't have to specify the fields in the same order in which we declared them in the struct。也就是说结构体定义就如同该类型的通用模板而实例则将特定数据填充到那个木板中从而创建出这个类型的值来。比如就可如下面清单 5-2 中所展示的那样,声明出一个特定的用户来:
```rust
fn main() {
let user1 = User {
email: String::from("rust@xfoss.com"),
username: String::from("unisko"),
active: true,
sign_in_count: 1
};
}
```
*清单 5-2创建出结构体 `User` 的一个实例来*
而要从结构体中获取到指定值,就要使用点表示法(`.`)。在要的仅是该用户的电子邮件地址时,就可以在那些要用到这个值的地方,使用 `user1.email` 。而在该实例为可变时,那么就可以通过使用点表示法,进而给特定字段赋值,而对某个值加以修改。下面的清单 5-3 展示了如何来修改某个可变 `User` 实例 `email` 字段中的值。
文件名:`src/main.rs`
```rust
fn main() {
let mut user1 = User {
email: String::from("rust@xfoss.com"),
username: String::from("unisko"),
active: true,
sign_in_count: 1
};
user1.email = String::from("java@xfoss.com");
}
```
*清单 5-3对某个 `User` 实例中的 `email` 字段进行修改*
请注意这整个实例必须是可变的Rust 不允许仅将一些字段标记为可变。与所有表达式一样可以函数体中最后的表达式形式构造出结构体的新实例来隐式地返回那个新实例as with any expression, we can construct a new instance of the struct as the last expression in the function body to implicity return that new instance
下面的清单 5-4展示了一个以给定电子邮件和用户名返回一个 `User` 实例的 `build_user` 函数。其中的 `active` 字符会得到值 `true`,而那个 `sign_in_count` 则会得到值 `1`
```rust
fn build_user(email: String, username: String) -> User {
User {
email: email,
username: username,
active: true,
sign_in_count: 1,
}
}
```
*清单 5-4一个取得电子邮件和用户名并返回一个 `User` 实例的 `build_user` 函数*
将函数参数命名为与结构体字段同样的名字,是有意义,但由此而不得不重复那 `email``username` 的字段名字与变量,就有点烦人了。在结构体有更多字段时,这样重复各个名字就会变得更加烦人。幸运的是,有种方便的简写法!
### 使用字段初始化简写法
由于在清单 5-4 中的参数名字与结构体字段名字完全一样,因此就可以 *字段初始化简写field init shorthand* 语法,来重写 `build_user` 方法,如此一来,`build_user` 函数在没有 `email``username` 重复的情况下,也有与之前版本同样的表现,如下清单 5-5 所示:
```rust
fn build_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}
```
*清单 5-5由于 `email` 与 `username` 参数与结构体字段有着同样名字,而使用了字段初始化简写的 `build_user` 函数*
在这里,正创建一个 `User` 结构体的新实例,该结构体有一个名为 `email` 的字段。这里打算将 `email` 字段的值,设置为 `build_user` 函数的 `email` 参数中的值。由于 `email` 字段与 `email` 参数有着同样的名字,因此只就需写下 `email`,而非 `email: email`
### 使用结构体更新语法,从其他实例创建出实例
创建出包含另一实例绝大部分值,而修改一些值的新实例,通常是有用的做法。而使用 *结构体更新语法struct update syntax* 就能做到这点。
首先,在下面的清单 5-6 中展示了如何按常规,不使用更新语法的情况下,创建出在 `user2` 中的一个新 `User` 实例。这里给 `email` 设置了一个新的值,而在其他方面,则使用了来自之前在清单 5-1 中创建的 `user1` 的那些同样值。
```rust
fn main() {
// --跳过代码--
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("java@xfoss.com"),
sign_in_count: user1.sign_in_count,
};
}
```
*清单 5-6使用一个 `user1` 的值创建出一个新的 `User` 实例*
而使用结构体更新语法,就可以较少代码,达成同样效果,如下面的清单 5-7 中所给出的那样。其中的 `..` 语法,指明了未显式设置的其余字段,将有着与所给实例中的字段同样的值。
```rust
fn main() {
// --跳过代码--
let user2 = User {
email: String::from("java@xfoss.com"),
..user1
};
}
```
*清单 5-7使用结构体更新语法来设置 `User` 实例的 `email` 字段值,而使用来自 `user1` 的其余值*
清单 5-7 中的代码同样创建了在变量 `user2` 中,一个有着 `email` 的不同值,但有着来自 `user1``username``active``sign_in_count` 同样值。其中的 `..user1` 必须要在最后,这样来指明全部剩余字段都应从 `user1` 中的相应字段获取值但对于其他字段值的指定则可选择所要的任意字段以任意顺序进行而不论在结构体定义中这些字段的顺序为何the `..user1` must come last to specify that any remaining fields should get their values from the corresponding fields in `user1`, but we can choose to specify values for as many fields as we want in any order, regardless of the order of the fields in the struct's definition
请注意结构体更新语法,像赋值一样使用了 `=`;这是由于结构体更新语法迁移了数据,就跟在之前的 ["变量与数据互动方式:迁移"](Ch04_Understanding_Ownership.md#变量与数据互操作方式之一迁移所有权) 小节中看到的那样。在此示例中,在创建了 `user2` 之后,由于变量 `user1` 中的 `username` 字段中的 `String` 值,已被迁移到 `user2` 中了,因此就再也不能使用变量 `user1` 了。若给到 `user2``email``username` 字段都是新的 `String` 值,而因此只使用来自 `user1``active``sign_in_count` 值,那么在创建了 `user2` 之后,`user1` 仍将是有效的。因为 `active``sign_in_count` 的类型,都是实现了 `Copy` 特质的类型,因此就会应用在 [唯栈数据:拷贝](Ch04_Understanding_Ownership.md#唯栈数据拷贝stack-only-data-copy) 小节中的行为表现。
### 使用没有命名字段的元组结构体来创建不同的类型
**Using Tuple Structs without Named Fields to Create Different Types**
Rust 还支持看起来像元组的结构体,叫做 *元组结构体tuple structs*。元组结构体这一类型,多了类型名称中结构体这一部分所提供的意义,却并没有与各字段相关联的名字;而是,元组结构体他们那些字段的类型。在要给予整个元组一个名字,并令到元组成为不同于其他元组的一种类型,且在如同在常规结构体中那样,给各个字段取名字是多余的等等,在这样的情况下,元组结构体就会有用。
要定义一个元组结构体,就要以 `struct` 关键字和该结构体的名字开头,接着是一些在元组中的类型。比如,下面分别定义和使用了两个元组结构体 `Color``Point`:
```rust
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
let white = Color(255, 255, 255);
let origin = Point(0, 0, 0);
}
```
请注意,由于这里的 `black``origin` 两个值是不同元组结构体的实例,因此他们属于不同类型。尽管结构体里的那些字段有着同样类型,对于所定义每个结构体,都是其自身的类型。比如,某个接收类型 `Color` 参数的函数,就无法接收 `Point` 值做参数,尽管这两种类型都是由三个 `i32` 值构成的。除此之外,元组结构体的实例,与元组表现一样:可将他们解构为三个独立部分,可使用 `.` 后面跟上索引,来访问单独值,等等。
### 没有字段的类单元结构
**Unit-Like Structs Without Any Fields**
还可以定义没有任何字段的结构体!由于这些没有任何字段的结构体,与曾在 [元组类型](Ch03_Common_Programming_Concepts.md#元组类型) 小节提到过的单元类型 `()` 表现类似,因此他们叫做 *类单元结构体unit-like structs*。当需要在某类型上实现某个特质trait却又不希望将任何数据存储在那个类型自身里面时类单元结构体就就有用unit-like structs can be useful when you need to implement a trait on some type but don't have any data that you want to store in the type itself。在第 10 章就会讨论到特质。下面是一个声明和初始化名为 `AlwaysEqual` 的单元结构体的示例:
```rust
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
```
要定义出 `AlwaysEqual`,就要使用 `struct` 关键字、想要的名字,随后一个分号即可。是不需要花括号或圆括号的!随后就可以类似方式,得到一个在 `subject` 变量中的 `AlwaysEqual` 的示例了:使用定义的名字,不带任何花括弧或原括弧。设想稍后就要将此类型的表现,实现为每个 `AlwaysEqual` 的实例总是等于任何其他类型的每个实例这样做或许是为测试目的而要有这样的已知结果imagine that later we'll implement behavior for this type such that every instance of `AlwaysEqual` is always equal to every instance of any other type, perhaps to have a known result for testing purposes。对于这样的行为表现是不需要任何数据的在第 10 章就会看到怎样定义特质,以及在包括类单元结构体在内的任何类型上,怎样实现特质。
> **结构体数据的所有权**
>
> 在前面清单 5-1 中的 `User` 结构体定义里,使用的是带有所有权的 `String` 类型,而非 `&str` 字符串切片类型。由于那里是要该结构体的各个实例拥有他自己的数据,且是要在整个结构体有效期间,实例数据有效,因此那里使用 `String` 类型而非 `&str` 类型就是有意而为之的了。
>
> 结构体存储到其他变量持有数据的引用,也是可能的,但这样做就需要用到 *生命周期lifetimes*,而生命周期则是会在后面的第 10 章会讨论到的一个 Rust 特性。生命周期确保某个结构体引用到的数据,会在该结构体有效期间保持有效。譬如说如同下面这样,在尝试在某个结构体中存储不带生命周期的引用时;这就不会工作:
>
> 文件名:`src/main.rs`
```rust
struct User {
active: bool,
username: &str,
email: &str,
sign_in_count: u64,
}
fn main() {
let user1 = User {
email: "someone@example.com",
username: "someusername123",
active: true,
sign_in_count: 1,
};
}
```
> 编译器会抱怨他需要生命周期说明符:
```console
$ cargo run
Compiling structs_demo v0.1.0 (/home/peng/rust-lang/projects/structs_demo)
error[E0106]: missing lifetime specifier
--> src/main.rs:3:15
|
3 | username: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
1 ~ struct User<'a> {
2 | active: bool,
3 ~ username: &'a str,
|
error[E0106]: missing lifetime specifier
--> src/main.rs:4:12
|
4 | email: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
1 ~ struct User<'a> {
2 | active: bool,
3 | username: &str,
4 ~ email: &'a str,
|
For more information about this error, try `rustc --explain E0106`.
error: could not compile `structs_demo` due to 2 previous errors
```
> 在第 10 章中,就会讨论怎样来修复这些错误,尔后就可以在结构体中存储引用变量了,而至于现在,则只会使用像是 `String` 这样的具有所有权的类型,而避开使用像是 `&str` 这样的引用,来解决这个问题。
## 一个使用结构体的示例程序
为搞明白何时会想要使用结构体,下面就来编写一个计算矩形面积的程序。这里会先从使用单个变量开始,并在随后对这个程序进行重构,直到使用结构体为止。
下面就来以 `Cargo` 构造一个名为 `rectangles` 的新二进制项目,该项目将取得以像素指定的矩形宽和高,并计算出该矩形的面积。下面的清单 5-8 给出了一个简短的程序,该程序正是有着在这个项目的 `src/main.rs` 中的做法:
```rust
fn main() {
let width1 = 30;
let height1 = 50;
println! (
"该矩形的面积为 {} 平方像素。",
area(width1, height1)
);
}
fn area(width: u32, height: u32) -> u32 {
width * height
}
```
*清单 5-8计算由单独宽和高变量指明的矩形面积*
现在,使用 `cargo run` 允许这个程序:
```console
$ cargo run
Compiling rectangles v0.1.0 (/home/peng/rust-lang/projects/rectangles)
Finished dev [unoptimized + debuginfo] target(s) in 0.17s
Running `target/debug/rectangles`
该矩形的面积为 1500 平方像素。
```
这段代码通过以两个边长调用 `area` 函数,而成功计算出了该矩形的面积,不过还可以进一步让这段代码更为清晰已读。
这段代码的问题,体现在 `area` 函数签名中:
```rust
fn area(width: u32, height: u32) -> u32 {
```
`area` 函数是要计算某个矩形面积的,但这里编写的该函数,有着两个参数,同时在这个程序中,并未清楚表明那两个参数是有联系的。将宽和高组织在一起,代码就会更具易读性,且更具可管理性。在第 3 章的 [元组类型](Ch03_Common_Programming_Concepts.md#元组类型) 小节,就已讨论过一种可能那样做的方式:使用元组。
### 以元组进行重构
下面的清单 5-9 给出了使用了元组的另一版本的这个程序。
文件名:`src/main.rs`
```rust
fn main() {
let rect1 = (30, 50);
println! (
"该矩形的面积为 {} 平方像素。",
area(rect1)
);
}
fn area(dimensions: (u32, u32)) -> u32 {
dimensions.0 * dimensions.1
}
```
*清单 5-9以一个元组来对矩形的宽和高进行指定*
一方面,这个程序更好了。元组实现了一些代码结构的加入,且现在传递的只有了一个参数。但在另一方面,这个版本变得更不清楚了:元组不会给他的各个元素命名,因此就不得不索引到该元组的各部分,从而令到这里的计算不那么直观了。
将宽和高混合起来对于面积计算并不重要,但在要将这个矩形绘制在屏幕上时,那就会有影响了!那时就必须要记住元组中索引 `0` 的是 `width`,而 `height` 是索引 `1`。这对那些将要使用到这代码的其他人来说,将会更难。由于没有在代码中传达数据的意义,因此现在更易于引入错误。
### 以结构体进行重构:加入更多意义
这里要使用结构体,通过给数据打上标签,来加入更多意义。可将这里正在使用的元组,以给整体命名,同时还给那些部分命名,而转换成为一个结构体。如下清单 5-10 所示。
文件名:`src/main.rs`
```rust
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println! (
"该矩形的面积为 {} 平方像素。",
area(&rect1)
);
}
fn area(rectangle: &Rectangle) -> u32 {
rectangle.width * rectangle.height
}
```
*清单 5-10定义一个 `Rectangle` 结构体*
这里就已定义了一个结构体,并将其命名为了 `Rectangle`。在那对花括弧内部,以 `width``height` 定义了两个字段,两个字段都具有 `u32` 类型。随后在 `main` 函数中,创建出了 `Rectangle` 的一个宽为 `30`,高为 `50` 的特定实例。
现在的 `area` 函数被定义为带有一个参数,该参数被命名为 `rectangle`,其类型是结构体 `Rectangle` 实例的不可变借用。如同在第 4 章中提到的那样,这里是要借用那个结构体,而非要取得那个结构体的所有权。在此方式下,`main` 函数仍保留着那个结构体实例的所有权,进而可继续使用变量 `rect1`,这就是在函数 `area` 签名与函数调用中,使用 `&` 符号的原因。
`area` 函数会访问那个 `Rectangle` 实例的 `width``height` 字段。`area` 的函数签名现在表达的正是这里想要的了:使用 `Rectangle``width``height` 字段,计算出他的面积。这就传达出了这里的宽与高是相互关联,同时这样做还给到了这些值描述性的名称,而非使用之前元组的索引 `0``1` 了。这在代码清晰上得了一分。
### 使用派生特质加入有用功能
**Adding Useful Functionality with Derived Traits**
如果能在调试程序期间打印出 `Rectangle` 的实例,并查看到所有字段的值,那就会派上用场。下面的清单 5-11 尝试了使用之前各章已经用到 [`println!` 宏](https://doc.rust-lang.org/std/macro.println.html)。不过这段代码不会工作。
```rust
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println! ("rect1 为:{}", rect1);
}
```
*清单 5-11尝试打印出一个 `Rectangle` 实例*
在编译这段代码时,会得到有着以下核心消息的错误:
```console
error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
```
`println!` 宏可完成许多种类的格式化,而默认情况下,那对花括号告诉 `println!` 的是,要使用名为 `Display` 的格式化操作即用于最终用户直接消费的输出the `println!` macro can do many kinds of formatting, and by default, the curly brackets tell `println!` to use formatting known as `Display`: output intended for direct end user consumption。因为在要将一个 `1` 或其他任何原生类型,展示给用户时,都只有唯一的一种方式,因此,对于至今为止已见到过的那些原生类型来说,默认都是实现了 `Display` 的。而对于结构体来说,由于存在更多的显示可能:是要逗号还是不要?要打印出那对花括号吗?所有字段都要展示出来吗?因此 `println!` 对输出进行格式化的方式就不那么清楚了。正是因为这种模棱两可Rust 于是就不尝试猜测代码编写者想要的样子,而结构体也就没有一个事先提供的、与 `println!``{}` 占位符一起使用的 `Display` 实现了。
在继续阅读该错误消息时,就会发现下面这个有用注解:
```console
= help: the trait `std::fmt::Display` is not implemented for `Rectangle`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
```
来试一下!这个 `println!` 的宏调用,现在看起来是这样 `println! ("rect1 为 {:?}", rect1);`。将说明符 `:?` 放在那对花括号里头,就会告诉 `println!`,这里是要使用一个名为 `Debug` 的输出。而 `Debug` 特质就令到这里可将那个结构体,以对开发者有用的方式打印出来,如此就可以在对代码进行调试时,看到那个结构体的值了。
在此改变下,对该代码进行编译。见鬼!还是得到个错误:
```console
error[E0277]: `Rectangle` doesn't implement `Debug`
```
不过编译器再度给到一个帮助性注释:
```console
= help: the trait `Debug` is not implemented for `Rectangle`
= note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle`
```
Rust *确实* 带有打印输出调试信息的功能,不过这里必须显式地选择上那功能,从而使得那功能对这个结构体可用。而要实现这个目的,就要在紧接着结构体定义之前,加上外层属性 `#[derive(Debug)]`the outer attribute `#[derive(Debug)`),如下面的清单 5-12 所示。
文件名:`src/main.rs`
```rust
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println! ("rect1 为:{:?}", rect1);
}
```
*清单 5-12加入派生 `Debug` 特质的属性,进而运用调试格式化将那个 `Rectangle` 实例打印出来*
此时在运行这个程序时,就不会收到任何错误了,且会看到下面的输出:
```console
$ cargo run
Compiling rectangles v0.1.0 (/home/peng/rust-lang/projects/rectangles)
Finished dev [unoptimized + debuginfo] target(s) in 0.20s
Running `target/debug/rectangles`
rect1 为Rectangle { width: 30, height: 50 }
```
很棒!这虽不是最漂亮的输出,但他给出了该实例全部字段的值,这无疑在调试期间会有帮助。在有着较大的结构体时,让输出更容易阅读一点就会有用;对于那些更大结构体的情形,就可在 `println!` 中使用 `{:#?}` 而非 `{:?}`。而在这个示例中,使用 `{:#?}` 样式将输出:
```console
cargo run
Compiling rectangles v0.1.0 (/home/peng/rust-lang/projects/rectangles)
Finished dev [unoptimized + debuginfo] target(s) in 0.18s
Running `target/debug/rectangles`
rect1 为Rectangle {
width: 30,
height: 50,
}
```
使用 `Debug` 格式化将某个值打印出来的另一种方式,就是使用 [`dbg!` 宏](https://doc.rust-lang.org/std/macro.dbg.html),这个 `dbg!` 宏会占据某个表达式的所有权,而将那个 `dbg!` 宏调用出现在代码中所在的文件与行号与那个表达式的结果值一并打印出来同时返回结果值的所有权another way to print out a value using the [`dbg!` macro](https://doc.rust-lang.org/std/macro.dbg.html), which takes ownership of an expression, prints the file and line number of where that `dbg!` macro call occurs in your code along with the resulting value of that expression, and returns ownership of the value
> 注意:对 `dbg!` 宏的调用会打印到标准错误控制台流the standard error console stream, `stderr`),这与 `println!` 宏打印到标准输出控制台流the standard output console stream, `stdout`)相反。在第 12 章中的 [将错误消息写到标准错误而非标准输出](Ch12_An_I_O_Project_Building_a_Command_Line_Program.md#把错误消息写到标准错误而非标准输出) 小节,将讲到更多有关 `stderr` 与 `stdout` 的内容。
以下是个其中对赋值给 `width` 字段,以及在变量 `rect1` 中的整个结构体的值感兴趣的示例:
```rust
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let scale = 2;
let rect1 = Rectangle {
width: dbg! (30 * scale),
height: 50,
};
dbg! (&rect1);
}
```
这里可将 `dbg!` 放在表达式 `30 * scale` 附近,同时由于 `dbg!` 返回了该表达式值的所有权,因此 `width` 字段将获取到与不在此处调用 `dbg!` 同样的值。由于这里不想要 `dbg!` 取得 `rect1` 的所有权,因此在下一个对 `dbg!` 的调用中,使用到到 `rect1` 的引用。下面就是这个示例输出的样子:
```console
cargo run
Compiling rectangles v0.1.0 (/home/peng/rust-lang/projects/rectangles)
Finished dev [unoptimized + debuginfo] target(s) in 0.22s
Running `target/debug/rectangles`
[src/main.rs:11] 30 * scale = 60
[src/main.rs:15] &rect1 = Rectangle {
width: 60,
height: 50,
}
```
这里就可以看到,输出的第一部分来自 `src/main.rs` 文件的第 10 行,正是对表达式 `30 * scale` 进行调式的地方,而该表达式的结果值即为 `60`(在整数原生值上实现的 `Debug` 格式化只打印他们的值)。在 `src/main.rs` 第 14 行上的 `dbg!` 调用,输出了 `rect1`,即那个 `Rectangle` 结构体的值。这个输出使用了 `Rectangle` 类型的良好 `Debug` 格式化。在尝试搞清楚代码在做什么时,这个 `dbg!` 宏真的会相当有用!
`Debug` 特质外Rust 业已提供了数个与 `derive` 属性一起使用的其他特质这些特质把有用的行为表现添加到那些定制类型。Rust 提供的那些特质及其行为,在 [附录 C](Ch21_Appendix.md#附录-c派生特质) 小节中有列出。在第 10 章中,就会涉及到怎样去实现这些有着定制行为的特质,以及怎样创建自己的特质。除了 `derive` 之外,同样还有许多别的属性;有关属性的更多信息,请参阅 [Rust 参考手册的 “属性” 小节](https://doc.rust-lang.org/reference/attributes.html)。
这里的 `area` 函数,是相当专用的:他只会计算矩形的面积。由于 `area` 方法不会在其他任何类型上工作,因此将此行为与这里的 `Rectangle` 结构体更紧密的联系起来,就会变得有帮助。接下来就要看看,怎样通过将这个 `area` 函数,转变成一个定义在这里的 `Rectangle` 类型上的方法,而继续重构这段代码。
## 方法语法
*方法* 与函数类似:是以 `fn` 关键字和一个名称,来声明出方法,方法可以有参数和返回值,同时包含了在某个地方方法被调用时,运行的一些代码。与函数不同的地方在于,方法是在结构体(或者枚举或特质对象,关于枚举即特质对象,将分别在第 6 和 17 章讲到)的语境里面定义的,且方法的首个参数将始终是 `self`,这个 `self` 表示方法被调用的那个结构体实例本身。
### 方法的定义
下面就来将那个将一个 `Rectangle` 实例作为参数的 `area` 函数,修改为定义在 `Rectangle` 结构体上的 `area` 方法,如下清单 5-13 所示:
```rust
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println! ("该矩形的面积为 {} 平方像素。",
rect1.area()
);
}
```
*清单 5-13在 `Rectangle` 结构体上定义一个 `area` 方法*
为定义 `Rectangle` 上下文中的函数,这里开启了一个 `Rectangle ``impl` implementation代码块。此 `impl` 代码块里头的所有物件,都会与那个 `Rectangle` 类型相关联。随后这里就把原来那个 `area` 函数,移入到这个 `impl` 的花括弧里,并将函数签名中的首个(而在此情形下,也是唯一的)参数,及函数体中的各处,均修改为 `self`。在 `main` 函数,即原先调用 `area` 函数与将 `rect1` 作为参数传递的地方,现在就可以使用 *方法语法* 来调用那个在 `Rectangle` 实例上的 `area` 方法了。方法语法the method syntax是在实例之后添加一个带着方法名字、括号及全部参数的点。
`area` 的签名中,使用了 `&self` 而不再是 `rectangle: &Rectangle`。这个 `&self` 实际上是 `self: &Self` 的简写。在 `impl` 代码块内部,类型 `Self` 就是该 `impl` 代码块所针对的类型。方法必定有着这么一个名为 `self` 类型为 `Self` 的参数,作为他们的首个参数,因此 Rust 这才允许将首个参数位置上的该参数,简写为只是他的名称 `self`。请注意这里仍然需要在 `self` 简写前使用 `&` 运算符,来表示此方法借用了 `Self` 类型的实例,这就跟 `rectangle: &Rectangle` 一样。方法可以取得 `self` 的所有权的、不可变地借用 `self` 变量,或者可变地借用 `self` 变量,对于方法的其他参数,也是这样的。
> `&self` - 不可变借用;`&mut self` 可变借用;`self` - 取得所有权,发生所有权转移,`self` 所指向的内存堆上的值原来的所有值将失效。
这里选择了 `&self`,有着与方法版本中使用 `&Rectangle` 有着同样理由:那就是不打算取得所有权,同时只打算读取结构体中的数据,而不打算写入。在作为方法要执行的一部分,要修改方法调用所在实例时,就要使用 `&mut self` 作为首个参数了。通过仅使用 `self` 作为首个参数,而取得实例所有权的情况,就非常少见了;通常在要将 `self` 转换为其他类型的数据,而要在这样的转换之后,阻止其他调用者使用原先的实例时,会用到这样的技巧。
使用方法而不是函数的主要原因,除了提供到方法语法及不必在每个方法签名中重复 `self` 的类型外,那就是为了代码的组织了。这里已将可由某个类型实例完成的事情,放在一个 `impl` 代码块中,而不是要那些后来的代码使用者,在这里提供的库的各个地方去找寻 `Rectangle` 的那些能力。
请注意可选择给方法一个与结构体字段相同的名字。比如,这里就可以在 `Rectangle` 上定义一个同样命名为 `width` 的方法:
文件名:`src/main.rs`
```rust
impl Rectangle {
fn width(&self) -> bool {
self.width > 0
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
if rect1.width() {
println! ("该矩形的宽不为零;他的宽为 {}", rect1.width);
}
}
```
这里,就选择了让 `width` 方法,在实例的 `width` 字段中的值大于 `0` 时返回 `true`,在值为 `0` 时返回 `false`:在名称与某个字段相同的方法里面,可将该字段用于任何目的。在 `main` 方法中,当这里在 `rect1.width` 后跟上一对圆括号时,那么 Rust 就明白这里指的是方法 `width`了。而在没有使用一对圆括号时Rust 就知道那里表示的是字段 `width`
通常,但并非总是这样,在给到方法与某个字段同样名字时,要的是让那个方法返回与其同名字段中的值,而不会去干别的事情。像这样的方法,就叫做 *获取器getters*,而 Rust 并未像其他语言所做的那样,自动实现结构体字段的获取器。由于可将字段构造为私有,而将方法构造为公开,而由此实现对作为类型的公开 API一部分的字段的只读访问。在第 7 章中就会讨论到何为公开与私有,以及怎样将字段或方法指定为公开或私有。
#### `->` 操作符the `->` operator哪去了呢
> 在 C 和 C++ 中,方法调用会用到两个操作符:直接调用在对象上的方法时,要用到 `.`,而在对象的指针上调用方法时,则要用 `->` 操作符,这时还先要对该指针解除引用。换句话说,在 `object` 是个指针时,`object -> something()` 是类似于 `(*object) -> something()` 的。
> Rust 并无 `->` 操作符的等价操作符相反Rust 有着一项名为 *自动引用与解引用automatic referencing and dereferencing* 的特性。而方法调用就是 Rust 中有着这种行为表现的少数几个地方之一。
>
> 以下就是该特性的工作原理:在以 `object.something()` 调用某个方法时Rust 会自动加上 `&`、`&mut` 或 `*`,这样 `object` 就会匹配上该方法的签名。换句话说,下面的语句是一致的:
>
```rust
p1.distance(&p2);
(&p1).distance(&p2);
```
>
> 第一个语句看起来要清楚不少。由于方法有着明确的接收者 -- 即 `self` 的类型因此这种自动引用的行为会生效。在给定了接收者和方法名字后Rust 就可明确地确定出该方式到底是在读取(`&self`)、改变(`&mut self`),或者是在消费(`self`。Rust 实现了方法接收者的隐式借用这一事实是为实现所有权系统在编程实践中符合人机交互而所做努力的较大部分the fact that Rust makes borrowing implicit for method receivers is a big part of making ownership ergonomic in practice
### 有多个参数的方法
下面就来通过在 `Rectangle` 结构体上实现另一个方法,练习一下方法的运用。这次就要 `Rectangle` 的一个实例,去取得另一个 `Rectangle` 的实例,并在第二个 `Rectangle` 完全能放入到 `self` (即第一个 `Rectangle` )里头时返回 `true`;否则这个方法就会返回 `false`。也就是,一旦定义了这个 `can_hold` 方法,就要能够编写下面清单 5-14 中的那个程序。
文件名:`src/main.rs`
```rust
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 40,
};
let rect3 = Rectangle {
width: 60,
height: 45,
};
println! ("rect1 可以装下 rect2 吗?{}", rect1.can_hold(&rect2));
println! ("rect1 可以装下 rect3 吗?{}", rect1.can_hold(&rect3));
}
```
*清单 5-14对尚未成文的 `can_hold` 方法进行使用*
由于 `rect2` 的两个边都小于 `rect1` 的两个边,而 `rect3` 的两个边都要长于 `rect1` 的两个边,因此预期的输出将看起来像下面这样:
```console
rect1 可以装下 rect2 吗true
rect1 可以装下 rect3 吗false
```
这里知道要定义的是个方法,因此那将会在 `impl Rectangle` 代码块内部。而方法的名称将是 `can_hold`,同时他会取得作为参数的另一 `Rectangle` 值的不可变借用。通过观察调用该方法的代码,就可以得出那个参数的类型了:`rect1.can_hold(&rect2)` 传入的是 `&rect2`,正是到变量 `rect2` 的不可变借用,而 `rect2` 又是 `Rectangle` 的一个实例。由于这里只需要读取 `rect2`(而非写入,那就意味着将需要一个可变借用了),同时这里是想要 `main` 函数保留 `rect2` 的所有权,这样就可以在 `can_hold` 方法调用之后,还可以再度使用 `rect2`,因此这样做是有理由的。`can_hold` 方法的返回值,将是个布尔值,而该方法的实现会检查 `self` 的宽和高,相应地是否都大于另一个 `Rectangle` 的宽和高。下面就把这个新的 `can_hold` 方法,加入到清单 5-13 的 `impl` 代码块,如下清单 5-15 所示。
文件名:`src/main.rs`
```rust
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn can_hold(&self, other: &Rectangle) -> bool {
(self.width > other.width && self.height > other.height) ||
(self.width > other.height && self.height > other.width)
}
}
```
*清单 5-15对在 `Rectangle` 上的、取另一 `Rectangle` 实例作为参数的 `can_hold` 方法进行实现*
在以清单 5-14 中的 `main` 函数来运行此代码是,就会得到想要的输出。方法可取得在 `self` 参数之后添加到其签名的多个参数,同时这些参数就像函数中的参数一样生效。
### 关联函数associated functions
由于定义在 `impl` 代码块内部的全部函数,都是与那个在 `impl` 关键字之后命名的类型相关联的,因此他们都叫做 *关联函数associated functions*。因为一些关联函数不需要用到该类型的实例,因此可把这些函数定义为不将 `self` 作为首个参数的关联函数(而这样的话,这些函数就不是方法了)。前面就已用到过这样的一个关联函数:`String::from` 函数就是定义在 `String` 类型上的。
非方法的关联函数,通常用于将会返回一个该结构体新实例的构造函数。比如,这里就可提供有着一维参数,并将该一维参数同时用作宽和高的这么一个关联函数,如此就令到相比于两次指定同样值,而更容易创建除正方形的 `Rectangle`
文件名:`src/main.rs`
```rust
impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
}
```
要调用这个关联函数,就要使用带有结构体名字的 `::` 语法;`let sq = Rectangle::square(3);` 就是一个示例;该函数是是在那个结构体的命名空间之下的:`::` 语法,同时用于关联函数,与由模组创建出的命名空间。在第 7 章会讨论到 Rust 的模组概念。
### 多个 `impl` 代码块
所有结构体都允许有多个 `impl` 代码块。比如前面的清单 5-15 就与下面清单 5-16 给出的代码等价,其中各个方法都在各自的 `impl` 代码块中:
```rust
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
(self.width > other.width && self.height > other.height) ||
(self.width > other.height && self.height > other.width)
}
}
```
*清单 5-16使用多个 `impl` 代码块对清单 5-15 进行重写*
虽然这里并无将这些方法分开到多个 `impl` 代码块中的理由,不过这样做也是有效的语法。在第 10 章讨论到泛型和特质时,就会看到多个 `impl` 代码块是有用的情形。
## 本章小节
结构体实现了创建对于特定领域有意义的定制类型。通过运用结构体,就可以将有关联的数据片段相互连接起来,并给各个数据取名字来让代码清晰。在 `impl` 代码块中,可定义与类型关联的函数,而方法则是一类实现了指定结构体实例所拥有行为的关联函数。
然而结构体并非能够创建定制类型的唯一方式:加下了就要转向到 Rust 的枚举特性,将另一工具加入到编程工具箱。

View File

@@ -2,580 +2,10 @@
**Enums and Pattern Matching**
在本章,将会对 *枚举enumerations* 进行审视,枚举也被当作 *enums*。枚举实现了通过列举出类型可能的 *变种variants*,来定义出一个类型。这里会首先定义并使用一个枚举,来展示枚举能如何将意义和数据编码起来。接下来,就会探索一个特别有用、名为 `Option` 的枚举,该枚举表示了某个值既可以是某事物,也可以什么也不是。随后就会看看在 `match` 表达式中的模式匹配,是怎样令到根据枚举中不同的值,而运行各异的代码容易起来的。最后,将会讲到 `if let` 结构是怎样成为另一种处理代码中枚举值的、便利而简洁的习惯用法的。
许多语言都有枚举这一特性不过在各个语言中的枚举能力是不同的。Rust 的枚举与那些函数式语言,诸如 F#、OCaml 及 Haskell 等中的 *代数数据类型algebraic data types* 最为相似
在本章中,我们将介绍 *枚举enumerations*,也称为 *枚举enums*。枚举允许咱们,通过枚举出其可能的 *变种variants*,来定义某种类型。首先,我们将定义并使用一个枚举,以展示枚举如何与数据一起,编码意义。接下来,我们将探究一个名为 `Option` 的特别有用的枚举,他表示某个值可以是某物,也可以是无。然后,我们将了解 `match` 表达式中的模式匹配,如何使我们可以轻松地针对枚举的不同值,运行不同代码。最后,我们将介绍 `if let` 结构,怎样成为咱们代码中,处理枚举的另一方便简洁的习惯用法
## 定义一个枚举
枚举是不同于结构体的第二种定义定制数据类型的方式。下面就来看看一种在代码中可能表达的情形,并见识一下为何在此情形下,相比于结构体,枚举是有用且更恰当的。假设说这里需要对 IP 地址进行处理。目前仅有两种用于 IP 地址的标准:版本四和版本六。由于这两个标准是程序将遇到的 IP 地址仅有的可能性,因此就可以 *列举出enumerate* 全部可能的变种这正是枚举enumeration 名字得来之处。
End
任何 IP 地址都只能是版本四或版本六的地址,而不会同时两个都是。由于枚举值只能是枚举变种之一,那么 IP 地址的这个属性令到枚举数据结构the enum data structure恰当起来。而版本四和版本六两种地址从根本上说都是 IP 地址,那么在代码对适用于任意类别 IP 地址的情形加以处理时,版本四和版本六地址都应当作同一类型对待。
在代码中,可通过定义一个 `IpAddrKind` 枚举,并列出 IP 地址可能的类别,即 `V4``V6`,来表达这个概念。下面就是该枚举的变种:
```rust
enum IpAddrKind {
V4,
V6,
}
```
现在 `IpAddrKind` 就是一个可在代码中别的地方使用的定制数据类型了。
### 枚举取值
可像下面这样,创建出 `IpAddrKind` 两个变种的实例来:
```rust
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
```
请注意,该枚举的两个变种,是在其标识符的命名空间之下的,且这里使用了双冒号将标识符和变种分隔开。由于现在这两个值 `IpAddrKind::V4``IpAddrKind::V6` 都是这同一类型:`IpAddrKind`,因此这就变得有用了。随后就可以,比如,定义一个取任意 `IpAddrKind` 类型值的函数:
```rust
fn route(ip_kind: IpAddrKind) {}
```
进而就能以这两个变种对这个函数进行调用了:
```rust
route(IpAddrKind::V4);
route(IpAddrKind::V6);
```
枚举的使用甚至还有更多好处。在还没有一种存储具体 IP 地址 *数据data* 的时候,就要进一步思考一下这里的 IP 地址类型;这是只知道 IP 地址数据为什么 *类别king*。根据在第 5 章中掌握的结构体知识,那么可能很想用下面清单 6-1 中的结构体来解决这个问题。
```rust
enum IpAddrKind {
V4,
V6,
}
struct IpAddr {
kind: IpAddrKind,
address: String,
}
fn main() {
let home = IpAddr {
kind: IpAddrKind::V4,
address: String::from("127.0.0.1"),
};
let loopback = IpAddr {
kind: IpAddrKind::V6,
address: String::from("::1"),
};
}
```
*清单 6-1使用结构体 `struct` 来存储 IP 地址的数据与 `IpAddrKind` 变种*
这里已定义了有着两个字段的结构体 `IpAddr`:一个类型为 `IpAddrKind` (即先前定义的那个枚举)的 `kind` 字段,以及一个类型为 `String``address` 字段。这里有该结构体的两个实例。第一个是 `home`,而他有着与地址数据 `127.0.0.1` 关联的 `IpAddrKind::V4` 作为其 `kind` 的值。第二个实例为 `loopback`。这个实例则有不同的 `IpAddrKind` 变种作为其 `kind` 的值,即 `V6`,与 `kind` 关联的是地址 `::1`。由于这里使用了结构体将 `kind``address` 值捆绑在一起,因此现在这个 `IpAddrKind` 的变种就与那个 `String` 值关联起来了。
不过,仅使用一个枚举来表示这同一概念,就会更加简练:与其将枚举放在结构体内部,可将数据直接放在各个枚举变种里头。那么这新的 `IpAddr` 枚举定义,就是说 `V4``V6` 两个变种,将同时有着关联的 `String` 值:
```rust
enum IpAddr {
V4(String),
V6(String),
}
fn main() {
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));
}
```
这里把数据直接附加到枚举的各个变种上,因此就无需额外的结构体了。这里还更易于发现枚举工作原理的另一细节:所定义的各个枚举变种的名字,还成为了构造该枚举实例的函数。那就是说,`IpAddr::V4()` 现在是个取 `String` 参数并返回该 `IpAddr` 类型实例的函数调用了。作为定义枚举的结果,这里让这个构造函数自动就定义好了。
这里还有另一个使用枚举而非结构体的好处:各个变种可以有不同类型及数量的关联数据。版本四类型的 IP 地址,将始终有着四个会有着 `0``255` 之间值的数字部分。在希望将 `V4` 地址存储为四个 `u8` 值,而仍然将 `V6` 地址表示为一个 `String` 值时,那就没法用结构体了,而枚举则能轻易处理这样的情况:
```rust
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
fn main() {
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
}
```
到这里,就已经给出了好几种定义用于存储版本四和版本六 IP 地址的数据结构了。然而事实表明,想要存储 IP 地址,及对这些 IP 地址所属类别进行编码是如此普遍,以致 [标准库就有一个可加以使用的定义](https://doc.rust-lang.org/std/net/enum.IpAddr.html)!下面就来看看,标准库是怎样定义 `IpAddr` 的:他有着与这里曾定义和使用过的相同枚举和变种,不过标准库是将地址数据,以两个不同结构体的形式,嵌入到变种里的,对两个枚举变种,定义了不同的结构体。
```rust
struct Ipv4Addr {
// --跳过--
}
struct Ipv4Addr {
// --跳过--
}
enum IpAddr {
V4(Ipv4Addr),
V6(Ipv6Addr),
}
```
这段代码说明可将任何类别的数据放在枚举变种里面:比如字符串、数字类型,或结构体等等。甚至可以包含另一枚举!还说明了,标准库类型,通常也并不比咱们自己编写的代码复杂多少。
请注意,由于这里不曾将标准库的 `IpAddr` 定义带入到这里的作用域,因此即使标准库包含了一个 `IpAddr` 的定义,这里也仍然可以毫无冲突地创建与使用自己的 `IpAddr` 定义。在第 7 章就会讲到有关带入类型到作用域的问题。
来看看下面清单 6-2 中另一个枚举的示例:这个枚举有着嵌入到其各个变种中的种类繁多的类型。
```rust
enum Message {
Quit,
Move { x: i32, y: i32 },
Write (String),
ChangeColor(i32, i32, i32),
}
```
*清单 6-2每个变种都存储了不同数量和类型值的 `Message` 枚举*
这个枚举有着四个带有不同类型数据的变种:
- `Quit` 变种完全没有与其关联的数据;
- `Move` 变种像结构体一样,有着两个命名的字段;
- `Write` 变种包含了单个 `String`
- `ChangeColor` 编程包含了三个 `i32` 的值。
定义一个有着一些如上面清单 6-2 中变种的枚举,与定义不同种类的结构体定义类似,不同在于枚举未使用关键字 `struct`,且所有变种在 `Message` 类型下组织在了一起。下面这些结构体,就可保存之前各个枚举变种所保存的那些同样数据:
```rust
struct QuitMessage; // 单元结构体
struct MoveMessage {
x: i32,
y: i32,
}
struct WriteMessage(String); // 元组结构体
struct ChangeColorMessage(i32, i32, i32); // 元组结构体
```
不过假如这里使用了不同的、有着各自类型的结构体,那么就无法轻易地定义出一个接收原本在清单 6-2 中定义的、单一类型的 `Message` 枚举那样的,接收全部这些类别消息的函数了。
枚举与结构体之间,还有另外一个相似点:正如在结构体上使用 `impl` 关键字定义出一些方法,在枚举上定义方法也是可以的。下面就是一个可定义在这里的 `Message` 枚举上、名为 `call` 的方法:
```rust
enum Message {
Quit,
Move { x: i32, y: i32 },
Write (String),
ChangeColor(i32, i32, i32),
}
impl Message {
fn call(&self) {
// 方法体将定义在这里
}
}
fn main() {
let m = Message::Write(String::from("hello"));
m.call();
}
```
方法体将使用 `self`,来获取方法调用所在变种实例值。在此示例中,已创建了一个有着值 `Message::Write(String::from("hello"))` 的变量 `m`,而那就是在 `m.call()` 运行时,`call` 方法体中的那个 `self`
下面就来看看标准库中另一个甚为常见和有用的枚举:`Option`
### `Option` 枚举及其超越空值的诸多优点
**The `Option` Enum and Its Advantages Over Null Values**
本小节会探讨 `Option` 的案例研究,`Option` 是由标准库定义的另一个枚举。`Option` 类型编码了某个值可能会是某个物件或可能什么都不属于的这种甚为常见的场景the `Option` type encodes the very common scenario in which a value could be something or it could be nothing。比如在请求某个含有一些项目的清单的第一个项目时就会得到一个值。而在请求某个空清单的第一个项目时则会什么也得不到。以类型系统字眼来表达这个概念就表示编译器能够对是否已处理了本应处理的全部情形进行检查此项功能可阻止那些在其他编程语言中极为常见的代码错误。
编程语言的设计通常要考量包含哪些特性而要排除哪些特性也至关重要。Rust 没有许多其他语言都有的空值特性。*空值Null* 是一个表示此处无值的值。在带有 `null` 的那些语言中,变量总是会处于下面两个状态之一:空值或非空值。
`null` 的发明人Tony Hoare 于 2009 年的演讲 “空值引用10 亿美金代价失误Null Reference: The Billion Dollar Mistake” 中,就讲了这个问题:
> 我把他叫做我的10亿美元失误。那个时候我正在设计某门面向对象语言中的首个综合类型系统。目的是要在编译器自动执行的检查之下确保全部引用使用都应绝对安全。仅仅因为空值引用变量实现起来很容易我当时就没能顶住诱惑把他加入到特性集了。这个举动业已造成了数不胜数的代码错误、漏洞及系统崩溃等等问题在过去 40 余年里,这些问题可能已经造成大概 10 亿美金的痛苦和伤害。
`null` 值的问题在于,当尝试将 `null` 值用作非 `null` 值时,就会得到某种错误。由于这种 `null` 或非 `null` 的属性遍布各处,因此极容易犯下此类错误。
`null` 试图表达的概念,还是有用的:`null` 是个因某些原因,而当前为无效或空缺的值。
问题不是真的在于这个概念而在于针对性的实现。由于这些原因Rust 就没有空值,但他确实有一个可对值存在或空缺这个概念,进行编码的枚举。这个枚举就是 `Option<T>`,而这个枚举 [由标准库定义](https://doc.rust-lang.org/std/option/enum.Option.html) 为下面这样:
```rust
enum Option<T> {
None,
Some(T),
}
```
这个 `Option<T>` 是如此重要,以至于在 Rust 序曲the prelude中甚至都包含了是不需要显式地将其带入到作用域的*原生类型、这里的 `Option<T>`,以及前面的 `String` 类型等等,就是这样的包含在序曲中的类型,无需显式地带入到作用域,就可以直接使用*)。该枚举的变种也已包含在 Rust 序曲中:可直接在不带前缀 `Option::` 的情况下直接使用 `Some``None`(注:*那么 `Some``None` 就被列为了 Rust 关键字了*)。`Option<T>` 仍然只是常规枚举,而 `Some<T>``None` 仍然是类型 `Option<T>` 的变种。
这里的 `<T>` 语法,是个到目前为止还未讲到的 Rust 特性。他是个泛型参数,而在第 10 章将更详细的涉及到泛型。至于现在,只需明白 `<T>` 表示 `Option` 枚举的 `Some` 变种,可保存任意类型的一条数据,而在 `T` 位置处用到的各个具体类型,会让整个 `Option<T>` 类型成为各异的类型for now, all you need to know is that `<T>` means the `Some` variant of the `Option` enum can hold one piece of data of any type, and that each concrete type that gets used in place of `T` makes the overall `Option<T>` type a different type。以下是使用 `Option` 来保存数字与字符串类型的一些示例:
```rust
let some_numer = Some(5);
let some_string = Some("一个字符串");
let absent_number: Option<i32> = None;
```
`some_number` 的类型为 `Option<i32>``some_string` 的类型为 `Option<&str>`,是个不同的类型。由于这里已在 `Some` 变种里面指定了值,因此 Rust 可推导出这些类型来。而对于 `absent_number`Rust 就要求注释整个 `Option` 类型:编译器无法通过仅查看一个 `None` 值,而推导出相应的 `Some` 变种的类型来。这里告诉了 Rust这里计划的是 `absent_number` 为类型 `Option<i32>`
在有着一个 `Some` 值时,就知道存在着一个值,且该值是保存在 `Some` 内部的。而在有个 `None` 值时,某种意义上讲,这表示了与空值同样的情况:没有一个有效值。那么究竟为什么有着 `Option<T>` 就是要比有着空值 `null` 好呢?
简而言之,由于 `Option<T>``T` (其中的 `T` 可以是任意类型) 为不同类型,因此编译器就不会允许将一个 `Option<T>` 值,当作一个必然的有效值来使用。比如,由于下面这段代码是在尝试将一个 `i8` 值,添加到某个 `Option<i8>` 上,因此这段代码不会编译:
```rust
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y;
```
在运行这段代码时,就会得到下面这样的错误消息:
```console
$ cargo run  ✔
Compiling enum_demo v0.1.0 (/home/peng/rust-lang/projects/enum_demo)
error[E0277]: cannot add `Option<i8>` to `i8`
--> src/main.rs:24:17
|
24 | let sum = x + y;
| ^ no implementation for `i8 + Option<i8>`
|
= help: the trait `Add<Option<i8>>` is not implemented for `i8`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `enum_demo` due to previous error
```
强悍!事实上,这个错误消息表示,由于 `i8``Option<i8>` 属于不同类型,因此 Rust 不知道怎样将一个 `i8` 值与一个 `Option<i8>` 值相加。当在 Rust 中有着一个类型好比 `i8` 这样的值时,编译器就会保证始终有个有效值。在对那个值进行使用前,可不必检查他是不是 `null`,而可放心地加以处理。仅当有个 `Option<i8>` 类型(或其他任何正在使用的`Option<T>` 枚举类型值)的变量时,才真地必须关心可能并无值,同时编译器将确保在使用该值前,显式地处理无值的情况。
也就是说,在对 `Option<T` 执行类型参数 `T` 的那些操作前,必须将 `Option<T>` 类型转换为 `T` 类型。通常,这样做有助于捕获到 `null` 最常见问题之一:在某个东西实际上是 `null` 时,错误地将其设想为了他不是 `null`
这种消除了不正确的假定某个非 `null` 值的做法,有助于增强代码自信。为了使用一个可能为 `null` 的值,就必须显式地通过将那个值的构造为 `Option<T>`,来带入这个值。在某个类型不为 `Option<T>` 值出现的每个地方,就都可以假定该值不是 `null`。这是 Rust 有意的设计决定,用以限制 `null` 的无处不在,及提升 Rust 代码的安全性。
那么在有一个类型为 `Option<T>` 值的时候,该怎么从 `Some` 变种获取到 `T` 这个值,从而就可以用上那个值呢?枚举 `Option<T>` 有着大量的、在不同情形下有用的方法;在 [`Option<T>` 文档](https://doc.rust-lang.org/std/option/enum.Option.html) 便可查看到这些方法。熟悉 `Option<T>` 上的这些方法,将对 Rust 编程生涯极为有用。
总的来说,为了使用某个 `Option<T>` 值,就要有将会处理各个变种的代码。要有一些只会在有着一个 `Some<T>` 的值时运行的代码,而此情况下就会允许这代码使用那个内部的 `T` 类型变量。在有着 `None` 值时,则还要有别的代码来允许了,而这代码就没有可用的 `T` 类型值了。在与枚举一起使用的时候,`match` 表达式正是实现此特性的控制流结构:`match` 表达式将依据枚举有着哪些变种,而运行相应的不同代码,以及哪些代码可使用匹配值内部的数据。
## `match` 控制流结构
Rust 有值一种即为强大的、名为 `match` 的控制流结构,此控制流结构实现了将某个值与一系列模式的比较,并根据所匹配模式而执行相应的代码。模式可由字面值、变量名字、通配符及其他事物等构成;第 18 章会涵盖到全部不同种类的模式及其所完成的事情。`match` 的强大来自模式的表达能力,以及编译器对全部可能情形都被处理进行确认这一事实。
请将 `match` 表达式设想为一种类似硬币分选机这样的东西:硬币随一个滑道滚下,沿着这滑道有不同尺寸的洞,那么每个硬币都会在他碰到的第一个大小合适的洞那里掉落。同样道理,所有值都会历经 `match` 表达式中的各个模式,而在值 “适合” 的第一个模式处,那个值就会掉入到相关代码块,而在执行过程中被使用到。既然讲到了硬币,那么下面就来将其用作一个用到 `match` 表达式的示例!这里可以编写一个接收未知硬币,并以与点数机类似方式,判断出该硬币是何硬币而返回以分计的值来的函数,如下面清单 6-3 中所示。
```rust
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
```
*清单 6-3一个枚举与一个将该枚举的那些变种作为其模式的 `match` 表达式*
这里就来把在 `value_in_cents` 函数中的那个 `match` 拆开讲讲。首先,这里是将 `match` 关键字后面跟上一个表达式,这里也就是 `coin` 后,列出来的。这就跟与 `if` 关键字一起使用的表达式极为相似,然而有个大的区别:在 `if` 之下,表达式需要返回一个布尔值,而在这里,该表达式可返回任意类型。此示例中 `coin` 的类型,即是这里在第一行上所定义的枚举 `Coin`
接下来就是这个 `match` 表达式的各个支臂了。一个支臂有着两个部分:一个模式与一些代码。这里的第一个支臂,有着值为 `Coin::Penny` 的模式,同时其中的箭头运算符 `=>` 将模式与要运行的代码分隔开来。此情形下的代码,就只是值 `1`。各个支臂则是以逗号接着分开的。
在这个 `match` 表达式执行时,他就会依序将结果值与各个支臂的模式加以比较。在某个模式与该值匹配时,与那个模式关联的代码就被执行。而在那个模式不与该值匹配时,执行就会继续到下一支臂,就跟硬币分选机是一样的。这里需要多少支臂,就可以有多少支臂:在清单 6-3 中的 `match` 表达式,就有四个支臂。
与各个支臂关联的代码,是个表达式,而在匹配支臂中的表达式返回值,就是整个 `match` 表达式所返回的值。
正如清单 6-3 中,每个支臂只是返回一个值那样,在 `match` 表达式支臂代码,为简短代码时,就通常不会使用花括号。而在要于某个 `match` 支臂中运行多行代码时,就必须使用花括号。比如下面的代码,在该方法每次以 `Coin::Penny` 被调用时,都会打印出 “幸运便士!”,不过仍会返回该代码块的最后值,`1`
```rust
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => {
println! ("幸运便士!");
1
},
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
```
### 绑定到值的模式
`match` 支臂的另一有用特性便是这些支臂可绑定到值与模式进行匹配值的多个部分another useful feature of `match` arms is that they can bind to the parts of the values that match the pattern。这就是从枚举变种提取出值的原理。
作为一个示例,下面就来将这里的枚举变种之一,修改为其内部保存数据。自 1999 年到 2008 年,美国在 25 美分硬币的一面,铸造上 50 个州不同的设计。别的硬币则没有这样的州份设计,因此只有这些 25 美分硬币才有这额外价值。那么就可以通过修改这个 `Quarter` 变种为内部包含一个 `UsState` 值,来将此信息添加到这里的 `enum` 类型,就如同下面清单 6-4 中所做的。
```rust
#[derive(Debug)] // 这样就可以很快对州份进行检查
enum UsState {
Alabama,
Alaska,
// --跳过--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
```
来设想一下有个朋友是在尝试收集全部 50 个州份的 25 美分硬币。在按照硬币类型对零钱进行分类的同时,还将叫出与每个 25 美分硬币关联的州份名字,如此就可以在发现那个 25 美分硬币,是那位朋友还没有的时候,就可以把那个硬币添加到收藏。
而在这个代码的 `match` 表达式中,就要添加一个名为 `state` 的变量到匹配变种 `Coin::Quarter` 的那些值。在有 `Coin::Quarter` 匹配时,这个 `state` 变量就会绑定到那个 25 美分硬币的状态值。随后就可以在那个支臂的代码里,使用 `state` 变量了,如同下面这样:
```rust
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state: UsState) => {
println! ("来自 {:?} 州份的 25 美分硬币!", state);
25
}
}
}
```
这时在调用了 `value_in_cents(Coin::Quarter(UsState::Alaska))` 后,`coin` 就将是 `Coin::Quarter(UsState::Alaska)`。在将该值与各支臂进行比较时,在到达 `Coin::Quarter(state: UsState)` 支臂之前,是不会有任何支臂于其匹配的。而在 `Coin::Quarter(state: UsState)` 支臂处,`state` 所绑定的,将是值 `UsState::Alaska`。这时就可以使用那个 `println!` 表达式中的绑定,进而就从 `Quarter``Coin` 枚举变种,获取到那个内部 `state` 值了。
### `Option<T>` 下的模式匹配
在前一小节,那里是想要在运用 `Option<T>` 时,从 `Some` 情形中获取到那个内部的 `T` 值;像前面对 `Coin` 枚举所做的那样,也可以这样来对 `Option<T>` 加以处理!比较的不再是那些硬币,而是将比较 `Option<T>` 的两个变种,不过那个 `match` 表达式的原理还是一样的。
下面就假设说要编写一个取 `Option<i32>` 类型值的函数,同时当 `Option<i32>` 里面有个值时,就将 `1` 加到那个值上。在 `Option<i32>` 里没有值时,该函数则会返回 `None` 值,并不会尝试执行任何运算。
归功于 `match` 表达式,这个函数写起来很容易,他将看起来像下面清单 6-5 这样。
```rust
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(n) => Some(n + 1),
}
}
fn main() {
let five = Some(5);
let none = None;
println! ("{:?}, {:?}", plus_one(five), plus_one(none));
}
```
*清单 6-5在 `Option<i32>` 类型上运用了 `match` 表达式的一个函数*
下面就来详细检视一下 `plus_one` 函数的首次执行。在调用 `plus_one(five)` 时,`plus_one` 函数体中的变量 `x` 将有着值 `Some(5)`。之后就会与 `match` 表达式的各个支臂进行比较。
```rust
None => None,
```
`Some(5)` 值不与模式 `None` 匹配,因此就会继续到下一支臂。
```rust
Some(n) => Some(n + 1),
```
`Some(5)``Some(n)` 匹配吗?当然是匹配的!这里有着同样的变种。这个 `n` 绑定的是包含在 `Some` 中的那个值,因此 `n` 就会取到值 `5`。随后该 `match` 支臂中的代码就会被执行,从而就会将 `1` 加到 `n` 的值,并创建出一个新的、内部有着这里的和 `6``Some` 值来。
现在来看看清单 6-5 中第二个 `plus_one` 的调用,其中 `x` 则是 `None` 了。这里进入到那个 `match` 表达式,并与第一个支臂进行比较。
```rust
None => None,
```
他是匹配的!就没有要加的值了,因此程序就停下来并返回 `=>` 右侧上的那个 `None` 值。由于第一个支臂已经匹配,因此就不会再比较其他支臂了。
在许多场合,将 `match` 表达式与枚举结合都是有用的。在 Rust 代码中将会看到很多这样的模式:对某个枚举的 `match` 操作,将某个变量绑定到内部数据,并随后据此执行代码(`match` against an enum, bind a variable to the data inside, and then execute code based on it。在刚开始的时候这显得有些难以琢磨而一旦熟悉了这种模式就会希望在全部语言中都有这样的模式。这样的模式一直是编程者的最爱。
### 匹配要彻底Matches Are Exhaustive
这里有个需要讨论到的 `match` 表达式的另一方面。想想这个有着代码错误而不会编译的 `plus_one` 版本:
```rust
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(n) => Some(n + 1),
}
}
```
这里没有对 `None` 情形加以处理,因此该代码就会引起错误。幸运的是,那是个 Rust 知道怎样取捕获的代码错误。在尝试编译此代码时,就会得到这样的错误:
```console
$ cargo run
Compiling enum_demo v0.1.0 (/home/peng/rust-lang/projects/enum_demo)
error[E0004]: non-exhaustive patterns: `None` not covered
--> src/main.rs:2:11
|
2 | match x {
| ^ pattern `None` not covered
|
note: `Option<i32>` defined here
= note: the matched value is of type `Option<i32>`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
|
3 ~ Some(n) => Some(n + 1),
4 ~ None => todo!(),
|
For more information about this error, try `rustc --explain E0004`.
error: could not compile `enum_demo` due to previous error
```
Rust 是知道这里未曾覆盖到每种可能情形,并甚至清楚这里忘记了那个模式! Rust 中的 `match` 表达式要是 *彻底的exhaustive*:为了让代码有效,就必须穷尽所有的可能性。尤其是在 `Option<T>` 这个示例中,在 Rust 阻止这里忘记显式地处理 `None` 这个情形时,在这里可能会有个 `null` 值时,他就保护了避免有个值的错误假设,进而让那个先前讨论到的十亿美金错误成为不可能了。
### 捕获所有模式与 `_` 占位符Catch-all Patterns and the `_` Placeholder
运用枚举,还可以对少数特定值采取特别动作,而对所有其他值采取一种默认动作。设想正在实现某个游戏,其中在投中了骰子上的 3 点时,游戏角色就不会移动,而是会收到一顶新的帽子道具。而在投中 7 点时,游戏角色会失去一定道具帽子。对于其他所有点数值,游戏角色都会在游戏板上移动相应数目的格子。下面就是个实现了该逻辑的 `match` 表达式,其中的骰子点数结果,是硬编码而非随机值,至于其他由不带函数体的函数所表示的逻辑,则是由于实现这些函数超出了本示例的范围:
```rust
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
other => move_player(other),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player() {}
```
对于前两个支臂,模式为字面值 `3``7`。而那最后的最比,则涵盖了所有其他可能的值,该模式为这里以选为命名为 `other` 的那个变量。为该 `other` 支臂所运行的代码,通过将这个 `other` 变量传递给 `move_player` 函数,而用到了这个变量。
由于那最后的模式将匹配到未特别列出的全部值,因此尽管这里并未列出 `u8` 类型变量有的全部可能值,这段代码仍会编译。这种捕获全部的模式,满足了 `match` 表达式务必彻底的要求。请注意由于这些模式是求值的,因此这里必须将那个捕获全部支臂放在最后。若在捕获全部之后,添加了其他支臂,那么 Rust 就会告警,这是由于这些在捕获全部之后的支臂根本不会匹配到!
Rust 还有一种在不愿使用捕获全部模式中的值时,可使用的一种模式:`_`,这是一种特别的、未与该值绑定的其他所有值。这种模式告诉 Rust 这里将不会使用该值,因此 Rust 就不会发出有关某个未用到变量的告警了Rust also has a pattern we can use when we don't want to use the value in the catch-all pattern: `_`, which is a special pattern that matches any value and doen't not bind to that value. This tells Rust we aren't going to use the value, so Rust won't warn us about an unused varialbe
下面就来将那个游戏的规则修改为,在投中骰子的三点和七点之外别的点数时,就必须再投一次骰子。那么这里就不需要用到那个点数值了,因此就可以将这里的代码修改为使用 `_` 而不是那个名为 `other` 的变量:
```rust
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => reroll(),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn reroll() {}
```
由于这里在最后的支臂中,显式地忽略了全部其他值,因此该示例也是满足 `match` 表达式的穷尽要求的;这里并未忘记掉任何东西。
若再一次修改此游戏的规则,修改为在抛出即非三点也非七点的其他点数时,什么也不会发生,那么就可以通过使用单元值(即在 [元组类型](Ch03_Common_Programming_Concepts.md#元组类型) 小节中讲到的那个空元组类型)作为该 `_` 支臂后的代码,来表达这样的游戏规则:
```rust
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => (),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
```
这里就显式地告诉 Rust这里将不使用那些不与先前支臂匹配的全部其他值且在此情形下这里不要运行任何代码。
在 [第 18 章](Ch18_Patterns_and_Matching.md) 将涉及到更多有关模式与匹配的内容。而现在就要移步到 `if let` 语法,在那些使用 `match` 表达式显得多余的情形下,`if let` 语法就会有用。
## `if let` 下的简洁控制流
`if let` 语法,实现了将 `if``let` 关键字结合为不那么冗长的处理与一个模式相匹配而忽略其余模式的一些值的处理方式the `if let` syntax lets you combine `if` and `let` into a less verbose way to handle that match one pattern while ignoring the rest。设想下面清单 6-6 中的这个程序,该程序是要对 `config_max` 变量中的 `Option<u8>` 值进行匹配,而只打算在该值为 `Some` 变种时,才执行代码。
```rust
let config_max = Some(3u8);
match config_max {
Some(max) => println! ("极大值被配置为了 {}" max);
_ => ();
}
```
*清单 6-6一个仅在乎当值为 `Some` 时运行代码的 `match` 表达式*
在该值为 `Some` 时,这里就通过将那个 `Some` 变种中的值,绑定到这个模式中的变量 `max`,而打印出该值来。这里并不想要对那个 `None` 值做什么操作。为满足 `match` 表达式的要求,这里必须在处理仅仅一个变种之后,添加 `_ => ()`,这就是要添加的恼人样板代码。
相反,可使用 `if let` 语法,以较简短方式写出来。下面的代码与清单 6-6 中的 `match` 表达式表现一致:
```rust
let config_max = Some(3u8);
if let Some(max) = config_max {
println! ("极大值被设置为了 {}", max);
}
```
`if let` 语法会接收由等号分隔的一个模式与一个表达式。他与 `match` 原理相同,其中的表达式被给到 `match` 表达式,而其中的模式就是 `match` 表达式的第一支臂。在此示例中,模式即为 `Some(max)`,而这个 `max` 就绑定到了 `Some` 里面的那个值。由此,这里随后就可以与在相应的 `match` 支臂中使用 `max` 的同样方式,在后面的那个 `if let` 代码块中对 `max` 进行使用。而在该值 `config_max` 不与该模式匹配时,那个 `if let` 代码块中的代码,就不会运行。
> ***注***`if let` 实际上是两部分,其中 `let Some(max) = config_max` 是个 scrutinee expression。
使用 `if let` 语法,就意味着较少输入、较少的缩进,以及更少的样板代码。不过会损失 `match` 表达式强制要求的穷尽检查。是根据特定情形下,手头正在做的事情,在 `match` 表达式与 `if let` 语法之间加以选择的,以及考量为收获到简洁,而是否值得损失穷尽性检查。
也就是说,可将 `if let` 语法当作,在值与某个模式匹配时运行代码,并在之后忽略所有其他值的 `match` 表达式的语法糖in other words, you can think of `if let` as syntax sugar for a `match` that runs code when the value matches one pattern and then ignores all other values
这里可以在 `if let` 之下,包含一个 `else` 关键字。`else` 所带的代码块,与在和 `if let``else` 等价的 `match` 表达式中, `_` 情形所带代码块相同。回想起清单 6-4 中的那个 `Coin` 枚举定义,其中的 `Quarter` 变种还有一个 `UsState` 值。在要通告出那些 25 美分硬币的州份的同时,还要清点出找到的全部非 25 美分数目,那么就可以使用下面这样的 `match` 表达式:
```rust
let mut count = 0;
match coin {
Coin::Quarter(state) => println! ("这是来自州份 {:?} 的 25 美分硬币!", state),
_ => count += 1,
}
```
或者这里还可以使用一个像下面这样的 `if let``else` 的表达式:
```rust
let mut count = 0;
if let Coin::Quarter(state) = coin {
println! ("这是来自州份 {:?} 的 25 美分硬币!", state);
} else {
count += 1;
}
```
在遇到程序中使用 `match` 显得太过繁复的逻辑这样情形时,就要记住在 Rust 工具箱中还有 `if let`语法呢。
## 总结
本章已经讲过,怎样运用枚举,来创建可作为一套一一列出数值之一的定制类型。这里给出了标准库的 `Option<T>` 类型,是怎样在运用该类型下,防止代码错误的原理。在枚举值有着内部值时,根据所要处理的多少种情况,而可使用 `match` 表达式或 `if let` 语法,来提取并使用这些值。
现在的 Rust 程序,就可以使用结构体与枚举,对所在领域的那些概念加以表达了。通过在自己构建的 API 使用的定制类型而确保了类型安全Rust 编译器将令到 API 中的那些函数,只获取到这些函数所期望类型的那些值。
而为了将可直接使用上的、组织良好的 API 提供到用户,并只暴露 API 的用户所需要部分,那么就要了解一下 Rust 的模组特性了。

View File

@@ -1,865 +1,31 @@
# 用代码包、代码箱与模组来对日益增长的项目进行管理
# 使用包、代码箱与模组管理不断增长的项目
在编写大型程序时,由于在头脑里对整个程序保持追踪已成为不可能,因此代码组织就尤为重要。通过将相关功能分组,并以截然不同的特性而将代码加以分离,就会搞清楚在哪里去找到实现了某个特定特性的代码,以及在哪里去修改某项特性的运作方式。
**Managing Growing Projects with Packages, Crates and Modules**
到目前为止这里所编写的程序都是在一个模组的一个文件中的。而随着项目的增长就可以通过将项目分解为多个模组及多个文件来对代码加以组织。一个代码包可以包含多个二进制的代码箱并可有选择地包含一个库代码箱。本章会涵盖到所有的这些技巧。对于那些极为大型、有着一套互相关联而共同演化的项目Cargo 工具提供了工作区workspaces概念关于工作区将在第 14 章的 [Cargo 工作区](Ch14_More_about_Cargo_and_Crates_io.md#cargo-工作区)中讲到。
除了实现功能上的分组grouping functionality对功能实现细节的封装还实现了更高层次上的代码重用一旦实现了某个操作其他代码就可以在无需掌握其实现原理的情况下通过该代码的公共接口对该实现代码加以调用。编写代码的方式就定义了哪些部分是公开给其他代码使用的哪些部分是私有实现细节而对其修改权力有所保留。这是对那些必须保留在头脑中细节实现数量而有所限制的另一种方式in addition to grouping functionality, encapsulating implementation details lets you reuse code at a higher level: once you've implemented an operation, other code can call that code via the code's pulic interface without knowing how the implementation works. The way you write code defines which part are public for other code to use and which parts are private implementation details that you reserve the right to change. This is another way to limit the amount of detail you have to keep in your head
当咱们编写大型程序时,组织咱们的代码,将变得日益重要。通过对相关功能进行分组,并将具有不同功能的代码分开,咱们就会弄清楚,在哪里可以找到实现某项特定功能的代码,以及在哪里可以改变某项特性的工作方式
而与此相关的概念便是作用域scope代码被编写出处的嵌套上下文有着定义在所谓 “在作用域中in scope” 的一套名字。在读、写及编译代码时,程序员与编译器,二者都需要掌握,在某个特定点位处的某个特定名字,是否是指向了某个变量、函数、结构体、枚举、模组、常量或别的项目,以及该名字所指向项目的意义。创建作用域,及将一些名字的在或不在某个作用域加以修改,都是可行的。在同一作用域中,不能有两个名字相同的项目;有一些工具,可用于找出名字冲突
到目前为止我们编写的程序都是在一个文件的一个模块中。随着项目的增长咱们应通过把代码拆分为多个模块然后拆分为多个文件来组织代码。一个包可以包含多个二进制代码箱也可以包含一个库代码箱。随着包的增长咱们可以将各个部分提取到独立的代码箱中成为外部依赖。本章将介绍所有这些技巧。对于由一组相互关联共同发展的包组成的超大型项目Cargo 提供了 *工作空间workspaces*,我们将在第 14 章的 [Cargo 工作空间](../crates-io/workspace.md) 小节中介绍
对于包括哪些细节被暴露、哪些细节为私有以及程序中各个作用域中有哪些名字等的代码组织Rust 有着数种特性实现对其的管理。Rust 的这些有关代码组织的特性,有时被统称为 *模组系统module system*,包括了:
我们还将讨论对实现细节进行封装的问题,这可以让咱们在更高层次上重用代码:一旦咱们实现了某个操作,其他代码就可以通过其公共接口,调用你的代码,而不必知道该操作是如何实现的。咱们编写代码的方式,定义了哪些部分是公开供其他代码使用的,哪些部分是私有的,咱们保留更改权利的实现细节。这是减少咱们必须记住的细节数量(从而减轻编程者负担)的方法。
- **代码包packages**实现代码箱crates的构建、测试与分享的 Cargo 特性;
- **代码箱crates**产生出库或可执行文件的模组树a tree of modules that produces a library or executable
- **模组modules** 与 **`use`关键字**实现对代码组织、作用域及路径私有的控制let you control the organization, scope, and privacy of paths
- **路径paths**对结构体、函数或模组等进行命名的方式a way of naming an item, such as a struct, function, or module
一个相关的概念是作用域:代码被写下之处的嵌套上下文,有着一组被定义为 “在作用域中” 的名字。在读取、编写和编译代码时,程序员和编译器都需要知道,某个特定点位上的特定名字,是否引用了某个变量、函数、结构体、枚举、模组、常量,或其他项目,以及该项目的含义。咱们可以创建出作用域,并改变哪些名字,在作用域内,哪些在作用域外。在同一个作用域中,不能有两个同名的项目;有些工具,可以解决名字冲突问题。
在本章中,就要涉及到这些特性,讨论他们之间互动的原理,以及如何运用这些特性,来对作用域加以管理。在本章结束时,就会对 Rust 的模组系统有扎实掌握,并能够像专业 Rust 程序员那样,以作用域来编写程序!
Rust 有数项特性,可以让咱们管理咱们代码的组织,包括哪些细节是公开的,哪些细节是私有的,以及咱们程序中各个作用域中的有哪些名字。这些特性有时统称为 *模组系统*,包括:
## 代码包与代码箱
这里将讲到的 Rust 模组系统的头几个部分,即为代码包与代码箱。
- **包packages**:让咱们构建、测试和共享代码箱的 Cargo 特性;
- **代码箱crates**:产生库或可执行程序的模组树;
*代码箱a crate* 是 Rust 编译器一次识别到的最低数量的代码a *crate* is the smallest amount of code that the Rust compiler considers as a time。即使运行 `rustc` 而非 `cargo`,并传递单个源码文件(就如同在第 1 章 [“编写并运行一个 Rust 程序”](Ch01_Getting_Started.md#hello-world) 小节中曾干过的),编译器也会将那个文件,视为一个代码箱。代码箱可以包含一些模组,而这些模组则会被定义在其他的、与该代码箱一同被编译的一些文件中,就如同在接下来的小节中将看到的那样。
- **模组modules** 与 **`use` 关键字**:让咱们控制路径的组织、作用域和隐私;
代码箱有着两种形式二进制代码箱a binary crate或库代码箱(a library crate)。*二进制代码箱binary crates* 是一些可编译为能够运行的可执行程序的一些程序,譬如命令行程序或服务器。二进制代码箱必须有着一个叫做 `main` 的、定义了在可执行文件运行时所发生事情的函数。到目前为止本书中创建的全部代码箱,都是二进制代码箱
- **路径paths**:命名项目(例如结构体、函数或模块)的一种方式
*库代码箱* 是没有 `main` 函数的,且他们不会编译到可执行文件。相反,他们定义的是计划在多个项目下共用的功能。比如在 [第二章](Ch02_Programming_a_Guessing_Game.md#生成随机数) 中用到的 `rand` 代码箱,就提供了生成随机数的功能。在多数时候当 Rust 公民提到 “代码箱crate” 时,他们指的就是库代码箱,并且他们将 “代码箱crate” 与一般编程概念中的 “库library” 互换使用。
*代码箱根crate root* 是个 Rust 编译器开始之处的源文件并构成了代码箱的根模组the *crate root* is a source file that the Rust compiler starts from and makes up the root module of your crate. 后面在 [定义控制作用域和私有化的模组](#定义控制作用域和隐私的模组) 小节,将深入探讨到模组概念)。
在本章中,我们将介绍所有这些功能,讨论他们如何交互,并探讨如何使用他们来管理作用域。到本章结束时,咱们应对模组系统有扎实的了解,并能像专业人士一样,使用作用域!
*a package* 即为提供了一套功能的一个或多个代码箱的捆绑包a *package* is a bundle of one or more crates that provides a set of functionality。包包含了描述如何构建那些代码箱的一个 `Cargo.toml` 文件。Cargo 本身实际上就是包含了前面曾用于构建代码的命令行工具二进制代码箱的包。Cargo 包还包含了一个该二进制代码箱所依赖的库代码箱。别的项目便可依靠这个 Cargo 库代码箱,来运用与 Cargo 命令行工具,所用到的同样逻辑。
代码包能包含些什么,是由数条规则所确定的。一个代码包,可包含尽可能多的二进制代码箱,但却只能包含至多一个的库代码箱。一个代码包必须包含至少一个代码箱,不管是库或二进制代码箱。
End
下面就来看看在创建代码包时,会发生些什么。首先,这里要敲入命令 `cargo new`:
```console
$ cargo new my-project
Created binary (application) `my-project` package
$ ls my-project  ✔
Cargo.toml src
$ ls my-project/src  ✔
main.rs
```
在运行了 `cargo new` 之后,这里便使用 `ls` 来查看 Cargo 创建了些什么。在该项目目录下,有着一个 `Cargo.toml` 文件,这就给到咱们一个代码包。其中还有一个包含了 `main.rs``src` 目录。在文本编辑器中打开 `Cargo.toml` 文件,就会注意到其中并未提及 `src/main.rs`。Cargo 遵循了 `src/main.rs` 即为与该代码包同名二进制代码箱箱根这样一条约定。与此类似Cargo 还知道,在代码包目录包含了 `src/lib.rs` 时,那么这个代码包就包含了与该包同名的一个库代码箱,而那个 `src/lib.rs` 就是该库代码箱的箱根。Cargo 会将代码箱根文件,传递给 `rustc`,来构建出相应的库或二进制程序。
这里有一个只包含了 `src/main.rs` 的代码包,意味着他只包含了名为 `my-project` 的一个二进制代码箱。而在代码包同时包含了 `src/main.rs``src/lib.rs` 时,他就会有两个代码箱:一个二进制和一个库代码箱,二者都有着与该代码包同样的名字。通过将一些文件放入到 `src/bin` 目录Rust 包就可以有多个二进制代码箱:其中的每个文件,都将是单独的二进制代码箱。
## 定义控制作用域和隐私的模组
在本小节中这里会讲到模组与模组系统的其他部分分别是实现对各种项目items, 变量、函数、结构体、枚举、模组、常量或别的项目)命名的 *路径paths*;将某个路径引入到作用域的 `use` 关键字;以及将那些项目构造为公共项目的 `pub` 关键字。这里还会讨论到 `as` 关键字、外部代码包以及全局操作符the glob operator等等。
首先,这里将以今后在对代码进行组织时,易于参考的一个规则列表开始。随后就会对这些规则详细解释。
### 模组备忘单modules cheat sheet
下面就是模组、路径、`use` 关键字与 `pub` 关键字在编译器中工作原理的快速参考,以及多数开发者组织他们代码的方式。贯穿这一整章,都将逐一介绍这些规则,而这也是作为理解 Rust 模组工作原理的极佳之处。
- **自代码箱根开始start from the crate root**:在编译代码箱时,编译器首先看的是代码箱根文件(对于库代码箱,通常为 `src/lib.rs`,或者二进制代码箱的 `src/main.rs`)中,要编译的代码;
+ **模组的声明declaring modules**:在代码箱根文件中,就可声明一些新的模组;比方说,使用 `mod gargen;`,而声明出一个 `garden` 模组。编译器将在以下位置,查找该模组的代码:
- 内联代码inline位于紧随 `mod garden` 之后,取代分号的花括号里;
- 文件 `src/garden.rs` 中;
- 文件 `src/garden/mod.rs` 中;
+ **子模组的声明declaring submodules**:在任何非代码箱根文件中,都可声明出一些子模组来。比如,或许就要在 `src/garden.rs` 中,声明出 `mod vegetables`;编译器将在那个以父模组命名的目录里的以下地方,查找那些子模组的代码:
- 内联代码,直接跟在 `mod vegetables` 之后,位处取代分号的花括号中;
- 文件 `src/garden/vegetables.rs` 中;
- 文件 `src/garden/vegetables/mod.rs` 中。
- **模组中代码的路径paths to code in modules**:一旦模组成为代码箱的一部分,就可以在这同一个代码箱中的任何地方,在隐私规则允许的情况下,运用代码路径,对那个模组中的代码加以引用。比如,那个 “garden” “vegetables” 模组中的 `Asparagus` 类型,就可在 `crate::garden::vegetables::Asparagus` 处找到。
- **私有与公共private vs public**:模组里的代码,默认对该模组的父模组是私有的。要令到模组成为公共的,就要使用 `pub mod` 而非 `mod` 来声明该模组。而要令到公共模组里的各个项目也成为公共的,就要在这些项目的声明之前,使用 `pub` 关键字。
- **`use` 关键字**:在某个作用域里,`use` 关键字创建出到项目的快捷方式,以减少长路径的重复。在任何能够引用到 `crate::garden::vegetables::Asparagus` 的作用域中,都可以使用 `use crate::garden::vegetables::AspAragus;` 语句,创建出一个快捷方式,并于随后只需写出 `Asparagus`,就可在该作用域中,使用那个类型。
下面是个名为 `backyard`、对这些规则加以演示的二进制代码箱。该代码箱的目录,也叫做 `backyard`,包含了下面这些文件与目录:
```console
backyard
├── Cargo.lock
├── Cargo.toml
└── src
├── garden
│   └── vegetables.rs
├── garden.rs
└── main.rs
```
该代码箱的根文件,在此实例中即为 `src/main.rs`,包含下面的代码:
文件名:`src/main.rs`
```rust
use crate::garden::vegetables::Asparagus;
pub mod garden;
fn main() {
let plant = Asparagus {};
println! ("I'm growing {:?}!", plant);
}
```
语句 `pub mod garden;`,表示编译器会包含他在 `src/garden.rs` 中找到的代码,也就是:
文件名:`src/garden.rs`
```rust
pub mod vegetables;
```
而语句 `pub mod vegetables;` 表示在 `src/garden/vetables.rs` 中的代码也会被编译器包含:
```rust
#[derive(Debug)]
pub struct Asparagus {}
```
现在就来进入到这些规则的细节,并在实际操作中对他们进行演示吧!
### 在模组中把有关联的代码组织起来
**Grouping Related Code in Modules**
*模组modules* 实现了为代码可读性与易于重用目的,而将代码,组织在代码箱里。由于模组里的代码,默认是私有的,因此模组还实现了各个项目 *隐私privacy* 的控制。私有项目是一些对外部用途不可用的内部实现细节。可将模组及模组中的那些程序项目,构造为公开,这样就把他们暴露出来,从而允许外部代码使用及依赖于他们。
举例来说,这里要编写一个提供饭馆功能的库代码箱。那么就会定义出一些函数签名,不过要将这些函数的函数体留作空白,而非在代码中具体实现一个饭馆出来,以专注于代码组织。
在餐饮行业,饭馆的一些部分被称作 *前台front of house*,而其余部分则被称作 *后厨back of house*。前台是顾客们所在的地方;这是饭馆领台给食客安排位置、服务员拿到菜单和买单,以及调酒师制作饮品的地方。而后厨则是大厨和厨师们在厨房做菜、洗碗工做清洁工作,以及经理们完成行政工作的地方。
为了以此种方式架构起这里代码,那么就可以将其函数,组织进一些嵌套模组中。通过运行 `cargo new --lib restaurant` 命令,创建出一个新的、名为 `restaurant` 的库;然后把下面清单 7-1 中的代码,敲入到文件 `src/lib.rs` 里,而定义出一些模组与函数签名。下面是饭馆前台部分:
文件名:`src/lib.rs`
```rust
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
fn seat_at_table() {}
}
mod serving {
fn take_order() {}
fn serve_order() {}
fn take_payment() {}
}
}
```
*清单 7-1包含着别的一些、又包含了一些函数的模组的一个 `front_of_house` 模组a `front_of_house` module containing other modules that then contain functions*
这里使用跟着模组名字(此示例中,即 `front_of_house`)的关键字 `mod`定义的一个模组。随后就是位处一对花括号中的模组代码体the body of the module。在模组内部可以有其他模组如同此示例中的 `hosting``serving` 模组。模组还可以驻留一些别的项目诸如结构体、枚举、常量、特质traits以及 -- 如同在清单 7-1 中那样的 -- 一些函数等等。
经由模组的使用,就可以将有关联的一些定义,组织在一起,并以他们因何相关而取个名字。使用此代码的程序员们,就可以根据这些分组,而非通读全部的这些定义,来浏览代码,那么在找到他们想要使用的那些定义时,就会容易一些。而对于要往该代码增加新功能的那些程序员,就清楚在哪里放置代码,来保持程序组织有序。
早先曾提到 `src/main.rs``src/lib.rs` 都叫做代码箱根crate root。他们之所以有着这样的名字是由于这两个文件的内容都形成了位处该代码箱的模组结构the root of the crate's module structure又称为 *模组树module tree*根部处,名为 `crate` 的模组。
以下清单 7-2 给出了清单 7-1 中结构的模组树(模组结构):
```console
crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment
```
*清单 7-2清单 7-1 中代码的模组树*
该树展示了一些模组是怎样嵌套在另一模组内部的(比如,`hosting` 就嵌套在 `front_of_house` 里头)。该树还展示了一些模组与其他模组互为 *姊妹关系siblings*,即他们是定义在同一模组中的(`hosting``serving` 都是定义在 `front_of_house` 模组中)。继续以家族作比喻,那么在模组 A 为包含在模组 B 里头时,就说模组 A 是模组 B 的 *子模组child*,而模组 B 即为模组 A 的 *父模组parent*。请注意这里的整个模组树,都是以那个隐式的、名为 `crate` 模组,作为的根。
模组树或许会令人想到计算机上文件系统的目录树;这可是一个极为恰当的类比!就跟文件系统中的目录一样,使用模组是为对代码进行组织。而正如目录中的那些文件,这里需要一种找到那些模组的方法。
## 用于引用目录树中项目的路径
**Paths for Referring to an Item in the Module Tree**
这里以与在对文件系统进行导航时,所用到路径的同样方式,使用路径来给 Rust 指出,在何处找到模组树中的某个项目。要调用某个函数,那么就需要获悉他的路径。
而路径则可以有两种形式:
- *绝对路径an absolute path* 是从代码箱根开始的完整路径。对于外部代码箱的代码,绝对路径是以代码箱名字开头,而对于当前代码箱的代码,则是以字面值 `crate` 开头;
- *相对路径a relative path* 是从当前模组开始,并使用了 `self``super` 关键字,或当前模组中的某个标识符。
绝对与相对路径,后面跟着的都是以双冒号(`::`)分隔的一个或多个标识符。
回到清单 7-1 中的示例,比方说这里打算调用那个 `add_to_waitlist` 函数。这就跟问及:“那个 `add_to_waitlist` 函数的路径为何?“ 是同样的。下面的清单 7-3 包含清单 7-1不过移除了一些模组与函数。
这里将给出从定义在该代码箱根部的一个新函数 `eat_at_restaurant`,调用那个 `add_to_waitlist` 函数的两种方式。其中那些路径都是正确的,但由于存在别的问题,而将阻止此示例如往常那样编译。这里会稍加解释为何会这样。
其中的 `eat_at_restaurant` 函数,是这里的库代码箱公共 API 的一部分,因此要将其以 `pub` 关键字进行标记。在后面的 [使用 `pub` 关键字对路径进行暴露](#使用-pub-关键字对路径进行暴露) 小节,深入到更多有关 `pub` 关键字的细节。
文件名:`src/lib.rs`
```rust
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// 绝对路径方式
crate::front_of_house::hosting::add_to_waitlist();
// 相对路径方式
front_of_house::hosting::add_to_waitlist();
}
```
*清单 7-3使用绝对与相对路径两种方式调用 `add_to_waitlist` 函数*
`eat_at_restaurant` 中,第一次调用那个 `add_to_waitlist` 函数使用的是绝对路径。由于这个 `add_to_waitlist` 函数,是定义在与 `eat_at_restaurant` 同一个代码箱中,这意味着此处可以使用 `crate` 关键字,来开始一个绝对路径。随后这里包括了到那个 `add_to_waitlist` 为止的各个后续模组。可以设想有着这同样结构的一个文件系统:即要指明路径 `/front_of_house/hosting/add_to_waitlist`,来运行那个 `add_to_waitlist` 程序;使用 `crate` 字面值名字,而自该代码箱根部开始,就跟使用 `/` 来从 shell 中文件系统根部开始类似。
`eat_at_restaurant` 里第二次调用 `add_to_waitlist` 时,这里使用了绝对路径。该路径是以 `front_of_house`,即那个与 `eat_at_restaurant` 定义在模组树的同一级别的模组名字,开始的。此处文件系统的等价物,将是使用路径 `front_of_house/hosting/add_to_waitlist`。以模组名字开始,就意味着该路径是绝对的。
至于究竟要选择相对路径,还是绝对路径,是要基于手头项目,而将作出的决定,并取决于是更倾向于把程序项目定义代码迁移到单独的地方,还是要把他们和要用到他们的代码放在一起。比如,在将 `front_of_house` 模组与 `eat_at_restaurant` 函数,移入到一个名为 `customer_experience` 的模组中时,那么就需要更新那个到 `add_to_waitlist` 的绝对路径,但那个相对路径则仍将有效。但如果将 `eat_at_restaurant` 函数单独移入到一个名为 `dining` 的模组,那么到 `add_to_waitlist` 函数的绝对路径就会依旧保持那样,但那个相对路径则需要被更新。由于今后多半要把代码定义和项目调用,迁移为各自独立,因此总体上偏好是要指明绝对路径。
接下来就要尝试编译清单 7-3并找出他为何不编译的原因所得到的错误如下清单 7-4 所示。
```console
$ cargo build  ✔
Compiling restaurant v0.1.0 (/home/peng/rust-lang/restaurant)
error[E0603]: module `hosting` is private
--> src/lib.rs:9:28
|
9 | crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^ private module
|
note: the module `hosting` is defined here
--> src/lib.rs:2:5
|
2 | mod hosting {
| ^^^^^^^^^^^
error[E0603]: module `hosting` is private
--> src/lib.rs:12:21
|
12 | front_of_house::hosting::add_to_waitlist();
| ^^^^^^^ private module
|
note: the module `hosting` is defined here
--> src/lib.rs:2:5
|
2 | mod hosting {
| ^^^^^^^^^^^
For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` due to 2 previous errors
```
*清单 7-4构建清单 7-3 中代码时的编译器错误*
该错误消息讲到,模组 `hosting` 是私有的。也就是说,这里的 `hosting` 模组与 `add_to_waitlist` 函数的路径都是正确的,但由于 Rust 没有到私有部分的访问权限,而不会允许咱们使用他们。在 Rust 中,全部程序项目(函数、方法、结构体、枚举、模组,以及常量等),默认对父模组都是私有的。在打算将某个项目,比如函数或结构体,构造为私有时,就要将其放在某个模组里。
父模组中的项目是无法使用子模组内部的那些私有项目的但子模组中的项目则可以使用他们祖辈模组中的项目。这是由于子模组封装并隐藏了他们的实现细节但子模组却可以看到他们被定义处的上下文the context in which they're defined。继续之前的那个比喻请把这些隐私规则想做是饭馆后台the back office of a restaurant那里所发生的事情对饭馆顾客是隐私的但后台经理们却可以看到并完成他们所运营饭馆里的全部事情。
Rust 选择了让模组系统以这种方式发挥作用从而默认就将内部实现细节给隐藏了。如此一来就清楚可修改内部代码的哪些部分而不会破坏外层代码。尽管如此Rust 还是提供了通过使用 `pub` 关键字,把某个程序项目构造为公共项目,而将子模组代码的内层部分,暴露给外层祖辈模组的选项。
### 使用 `pub` 关键字对路径进行暴露
下面回到清单 7-4 中,告知 `hosting` 模组为私有的那个错误。这里希望在父模组中的 `eat_at_restaurant` 函数,有着到那个 `hosting` 子模组中的 `add_to_waitlist` 函数的访问权限,因此就要将该模组,以 `pub` 关键字标记起来,如下面清单 7-5 中所示。
文件名:`src/lib.rs`
```rust
mod front_of_house {
pub mod hosting {
fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// 绝对路径
crate::front_of_house::hosting::add_to_waitlist();
// 相对路径
front_of_house::hosting::add_to_waitlist();
}
```
*清单 7-5将 `hosting` 模组声明为 `pub` 以在 `eat_at_restaurant` 中使用他*
不幸的是,清单 7-5 中的代码仍以错误告终,如下清单 7-6 中所示:
```console
$ cargo build
Compiling restaurant v0.1.0 (/home/peng/rust-lang/restaurant)
error[E0603]: function `add_to_waitlist` is private
--> src/lib.rs:9:37
|
9 | crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^^^^^^^^^ private function
|
note: the function `add_to_waitlist` is defined here
--> src/lib.rs:3:9
|
3 | fn add_to_waitlist() {}
| ^^^^^^^^^^^^^^^^^^^^
error[E0603]: function `add_to_waitlist` is private
--> src/lib.rs:12:30
|
12 | front_of_house::hosting::add_to_waitlist();
| ^^^^^^^^^^^^^^^ private function
|
note: the function `add_to_waitlist` is defined here
--> src/lib.rs:3:9
|
3 | fn add_to_waitlist() {}
| ^^^^^^^^^^^^^^^^^^^^
For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` due to 2 previous errors
```
*清单 7-6构造清单 7-5 中代码时的编译器错误*
怎么回事呢?将 `pub` 关键字加在 `mod hosting` 前面,是把该模组构造为公共模组。有了此修改,那么在能够访问 `front_of_house` 时,就能够访问 `hosting`。但 `hosting` 模组的 *内容contents* 仍是私有的;将模组构造为公开,并不会将其内容构造为公开。在模组上的 `pub` 关键字,只是让其祖辈模组中的代码可以引用到他,而不是访问其内层代码。由于模组是些容器,因此仅将模组构造为公开,是做不了什么的;这就需要更进一步,而选择将模组里的一个或更多的程序项目,也构造为公开。
清单 7-6 中的错误说到,那个 `add_to_waitlist` 函数是私有的。适用于结构体、枚举、函数即方法等的隐私规则,与适用于模组的一样。
下面就来通过把 `pub` 关键字,添加在 `add_to_waitlist` 函数定义之前,而将其构造为公开函数,如下清单 7-7 所示。
文件名:`src/lib.rs`
```rust
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// 绝对路径
crate::front_of_house::hosting::add_to_waitlist();
// 相对路径
front_of_house::hosting::add_to_waitlist();
}
```
*清单 7-7把 `pub` 关键字添加到 `mod hosting` 与 `fn add_to_waitlist`,实现从 `eat_at_restaurant` 调用该函数*
现在该代码就会编译了!接下来看看其中绝对与相对路径,以弄明白为何添加 `pub` 关键字,就实现了在遵循隐私规则之下,使用到 `add_to_waitlist` 中的这些路径的原因。
在那个绝对路径中,是以这里代码箱模组树的根、字面值 `crate` 开始的。那个 `front_of_house` 模组,即为被定义在该代码箱根中。尽管 `front_of_house` 模组不是公开的,但由于 `eat_at_restaurant` 函数被定义在与 `front_of_house` 同一模组中(即 `eat_at_restaurant``front_of_house` 是姊妹关系),因此是可以从 `eat_at_restaurant` 函数引用 `front_of_house` 的。接下来就是那个被标记了 `pub``hosting` 模组了。由于这里可以访问 `hosting` 的父模组,因此就可以访问 `hosting`。最后,由于那个 `add_to_waitlist` 函数被 `pub` 标记过,且这里可以访问他的父模组,因此该函数调用就生效了!
在那个相对路径中,除了第一步外,其中的逻辑与绝对路径相同:与从代码箱根开始不同,该相对路径是从 `front_of_house` 处开始的。这个 `front_of_house` 模组,是定义在与 `eat_at_restaurant` 函数同样的模组中,那么从 `eat_at_restaurant` 定义所在处的这个模组开始的相对路径,就是有效的。随后由于 `hosting``add_to_waitlist` 都是以 `pub` 关键字标记过,那么该路径其余部分就都工作了,同时此函数调用就是有效的了!
在计划分享库代码箱,进而其他项目可使用到其代码时,公开 API 即是与该代码箱用户的合约,定下了与库代码箱代码互动的方式。在管理对公共 API 的修改方面,则有着诸多考量,以让人们更易于依赖到咱们的代码箱。这些考量超出了本书的范围;若对这方面感兴趣,那么请参阅 [Rust API 指南](https://rust-lang.github.io/api-guidelines/)。
> **带有一个二进制与一个库的 Rust 代码包最佳实践Best Practice for Packages with a Binary and a Library**
>
> 前面提到过 Rust 包可以同时包含一个 `src/main.rs` 二进制代码箱根,与一个 `src/lib.rs` 库代码箱根,且这两个代码箱都将默认有着该 Rust 包的名字。一般来说,这种同时包含了一个库及二进制代码箱模式下的包,都会在二进制代码箱中,仅有着足够启动一个会调用到库代码箱代码的可执行程序的少量代码。由于库代码箱的代码可被共享,因此这就实现了别的项目,受益于该 Rust 包所提供的绝大部分功能。
>
> 模组树应定义在 `src/lib.rs` 中。随后,全部的公开程序项目,都可通过以该包名字开头的路径,在那个二进制代码箱中被使用。这个二进制代码箱,就像是个将用到那个库代码箱的完整外部箱,成了库代码箱的一名用户:他只能使用公共 API。这样做有助于设计出良好的 API你不仅是库代码箱的作者还是一名库代码箱的客户了
>
> 在 [第 12 章](Ch12_An_I_O_Project_Building_a_Command_Line_Program.md),将以一个会同时包含二进制代码箱与库代码箱的命令行程序,对这种代码组织方式实践加以演示。
### 使用 `super` 关键字开始相对路径
**Starting Relative Paths with `super`**
通过在路径开头使用 `super` 关键字,就可以构建出在父模组处,而非当前模组或代码箱根处开始的相对路径。这与以 `..` 语法开始的文件系统路径相似。使用 `super` 实现了对已知在父模组中某个程序项目的引用,在模组与其父模组密切相关,但该父模组在某个时候可能会被迁移到模组树中别的地方时,这种使用 `super` 关键字的相对路径,就能让模组树的重新安排更为容易。
设想下面清单 7-8 中,建模了一位大厨修正某个不正确点餐,并亲自将其交给顾客的代码。其中定义在 `back_of_house` 模组中的函数 `fix_incorrect_order`,通过以 `super` 作为开头指明的 `deliver_order` 路径,调用了定义在父模组中的该 `deliver_order` 函数:
文件名:`src/lib.rs`
```rust
fn deliver_order() {}
mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::deliver_order();
}
fn cook_order() {}
}
```
*清单 7-8使用以 `super` 开头的相对路径调用某个函数*
这个 `fix_incorrect_order` 函数是在 `back_of_house` 模组中,因此就可以使用 `super` 关键字,前往到 `back_of_house` 的父模组,那就是此示例中的 `crate`,亦即代码箱根。在那里,就会查找 `deliver_order` 进而找到他。大功告成!这里把 `back_of_house` 模组与 `deliver_order` 函数,设想作可能维持这同样关系,并在今后决定要对这个代码箱的模组树,进行重新组织时,他们会一起被移动。因此,这里使用了 `super`,从而今后在此代码被移入到别的模组时,要更新代码的地方就会少一些。
### 将结构体与枚举构造为公共项目
**Making Structs and Enums Public**
这里还可以使用 `pub` 关键字,来将结构体与枚举,指定为公开项目,但结构体与枚举下 `pub` 的用法,有着几个额外情况。在结构体定义前使用 `pub` 关键字时,就将该结构体构造为了公开,但该结构体的那些字段,仍将是私有。可根据具体情况,把各个字段构造为公开或不公开。在下面清单 7-9 中,就定义了有着公开 `toast` 字段,和私有 `seasonal_fruit` 字段的一个公开的 `back_of_house::Breakfast` 结构体。这就对在某个饭馆中,顾客可在何处挑选与正餐搭配的面包类型,而主厨则会根据当季及仓库里有些什么,而决定由哪些水果来搭配正餐,这种情形进行了建模。可用的水果变化很快,因此顾客就无法对水果进行选择,甚至他们看不到会得到什么样的水果。
文件名:`src/lib.rs`
```rust
mod back_of_house {
pub struct Breakfast {
pub toast: String,
seasonal_fruit: String,
}
impl Breakfast {
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}
}
pub fn eat_at_restaurant() {
// 点下一份带有黑麦土司的夏日早餐, rye, US /raɪ/, UK /rai/, n.黑麦, 黑麦粒
let mut meal = back_of_house::Breakfast::summer("Rye");
meal.toast = String::from("Wheat");
println! ("请给我一份 {} 土司", meal.toast);
// 若不把接下来的行注释掉,那么就不会编译;这里不允许查看或修改
// 餐食搭配的应季水果
// meal.seasonal_fruit = String::from("blueberries");
}
```
*清单 7-9有着一些公共字段与私有字段的一个结构体*
由于 `back_of_house::Breakfast` 结构体中的 `toast` 字段是公开的,因此在 `eat_at_restaurant` 中就可以使用点符号(`.`),对 `toast` 字段进行写入与读取。请注意由于 `seasonal_fruit` 是私有的,因此这里不能在 `eat_at_restaurant` 中使用那个 `seasonal_fruit` 字段。尝试将那个对 `seasonal_fruit` 字段值进行修改的行解除注释,看看将得到什么样的错误!
```console
$ cargo build
Compiling restaurant v0.1.0 (/home/peng/rust-lang/restaurant)
error[E0616]: field `seasonal_fruit` of struct `Breakfast` is private
--> src/lib.rs:25:10
|
25 | meal.seasonal_fruit = String::from("blueberries");
| ^^^^^^^^^^^^^^ private field
For more information about this error, try `rustc --explain E0616`.
error: could not compile `restaurant` due to previous error
```
还请留意由于 `back_of_restaurant::Breakfast` 有个私有字段,那么该结构体就需要提供一个公开的、构造出`Breakfast` 实例的关联函数(这里将该函数命名为了 `summer`)。若 `Breakfast` 没有这样一个函数,那么由于在 `eat_at_restaurant` 中无法设置那个私有 `seasonal_fruit` 字段的值,因此就没法在 `eat_at_restaurant` 中创建处一个 `Breakfast` 的实例来。
与此相比,在将枚举构造为公开时,该枚举的全部变种此时都是公开的。这里就只需在 `enum` 关键字前的 `pub` 关键字,如下清单 7-10 中所示。
文件名:`src/lib.rs`
```rust
mod back_of_house {
pub enum Appetizer {
Soup,
Salad,
}
}
pub fn eat_at_restaurant() {
let order1 = back_of_house::Appetizer::Soup;
let order2 = back_of_house::Appetizer::Salad;
}
```
> appetizer, US/ˈæpəˌtaɪzər/, UK/ˈæpəˌtaɪzə(r)/ n.(餐前的)开胃品
*清单 7-10将枚举指定为公开则会将其全部变种构造为公开*
由于这里将那个 `Appetizer` 枚举构造为了公开,因此就可以在 `eat_at_restaurant` 中使用 `Soup``Salad` 变种。除非枚举的各个变种是公开的,否则枚举就不是非常有用了;若在所有场合,都必须以 `pub` 关键字来对全部枚举变种进行注解,那就会让人觉得烦恼不已,因此默认枚举变种就是公开的。而结构体则通常无需其字段保持公开就有用处,因此结构体的那些字段,就遵循了除非以 `pub` 关键字注释,而默认全部为私有的一般规则。
还有一个尚未讲到的涉及 `pub` 关键字的情况,那也是最后的一项模组系统特性:`use` 关键字。后面会先讲 `use` 本身,然后再给出怎样结合 `pub``use`
## 使用 `use` 关键字将路径带入作用域
为调用一些函数,而不得不写出他们的路径,就会感到不便与重复。比如在清单 7-7 中,对于到 `add_to_waitlist` 函数,无论是选择绝对路径还是相对路径,在每次在打算调用 `add_to_waitlist` 时,都必须还要指明 `front_of_house``hosting`。幸运的是,有简化此过程的办法:这里可以使用 `use` 关键字,一次性创建出到某个路径的快捷方式,尔后就可以在该作用域中所有地方,使用这个较短名字了。
在下面清单 7-11 中,就将 `crate::front_of_house::hosting` 模组,带入到了 `eat_at_restaurant` 函数的作用域,由此就只须指明 `hosting::add_to_wait`,而在 `eat_at_restaurant` 中调用这个 `add_to_waitlist` 函数了。
文件名:`src/lib.rs`
```rust
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
```
*清单 7-11使用 `use` 关键字,将模组带入到作用域*
在作用域中添加 `use` 及某个路径,与在文件系统中创建一个符号链接类似。通过在该代码箱根处,添加 `use crate::front_of_house::hosting`,那么 `hosting` 现在就是一个有效的名字,就如同这个 `hosting` 模组,已在该代码箱根中被定义过一样。使用 `use` 关键字带入到作用域中的那些路径,与任何其他路径一样,同样会检查隐私性。
请注意 `use` 关键字只会针对在该 `use` 出现的特定作用域,创建快捷方式。下面清单 7-12 将 `eat_at_restaurant` 移入到了新的名为 `customer` 的子模组中,这个模组就与那个 `use` 语句属于不同作用域了,因此那个函数体就不会编译:
文件名:`src/lib.rs`
```rust
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting;
mod customer {
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
}
```
*清单 7-12`use` 语句只适用于其所在的作用域*
编译器错误指出,在 `customer` 模组里头,那个快捷方式不再适用:
```console
$ cargo build
Compiling restaurant v0.1.0 (/home/peng/rust-lang/restaurant)
error[E0433]: failed to resolve: use of undeclared crate or module `hosting`
--> src/lib.rs:33:9
|
33 | hosting::add_to_waitlist();
| ^^^^^^^ use of undeclared crate or module `hosting`
warning: unused import: `crate::front_of_house::hosting`
--> src/lib.rs:28:5
|
28 | use crate::front_of_house::hosting;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(unused_imports)]` on by default
For more information about this error, try `rustc --explain E0433`.
warning: `restaurant` (lib) generated 1 warning
error: could not compile `restaurant` due to previous error; 1 warning emitted
```
请注意这里还有那个 `use` 在其作用域中已不再被使用的一个告警!为修复此问题,就同时要将那个 `use` 语句,移入到那个 `customer` 模组内部,或者在那个子 `customer` 模组内部,以 `super::hosting` 来引用父模组中的那个快捷方式。
### 创建惯用 `use` 路径
在上面的清单 7-11 中,你或许会想,为什么那里指定了 `use crate::front_of_house::hosting`,并随后在 `eat_at_restaurant` 函数中调用了 `hosting::add_to_waitlist`,而不是将那个 `use` 路径,指定为一直到那个 `add_to_waitlist` 函数,以达到同样目的,即如下清单 7-13 中那样。
文件名:`src/lib.rs`
```rust
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting::add_to_waitlist;
pub fn eat_at_restaurant() {
add_to_waitlist();
}
```
*清单 7-13使用 `use` 将 `add_to_waitlist` 带入到作用域,此为非惯用做法*
尽管清单 7-11 与 7-13 都完成了同样任务,但清单 7-11 则是以 `use` 关键字将函数带入到作用域的惯用方式。以 `use` 关键字将函数的父模组带入到作用域中,就意味着在调用该函数时,必须指明父模组。而在调用函数时指明父模组,就令到该函数是非本地函数,这一事实变得明了,同时仍旧减少了完整路径的重复。而清单 7-13 中的代码,对于 `add_to_waitlist` 在何处创建,则并不清楚。
另一方面,在使用 `use` 关键字,将结构体、枚举及其他程序项目带入时,惯用的就是指明完整路径了。下面清单 7-14 给出了将标准库的 `HashMap`,带入到某个二进制代码箱的惯用方式。
文件名:`src/lib.rs`
```rust
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert(1, 2);
}
```
*清单 7-14以惯用方式将 `HashMap` 带入到作用域*
这种惯用语法背后并没有什么有力理由:他不过是业已形成的约定,且人们已经习惯了以这样的方式,阅读和编写 Rust 代码。
由于 Rust 不允许使用 `use` ,将两个有着同样名字的程序项目带入到作用域,那么这就正是此惯用语法的例外了。下面清单 7-15 给出了,怎样将两个有着同样名字,但父模组不同的 `Result` 类型带入作用域,及怎样去引用他们。
文件名:`src/lib.rs`
```rust
use std::fmt;
use std::io;
fn function1() -> fmt::Result {
// --跳过--
}
fn function2() -> io::Result {
// --跳过--
}
```
*清单 7-15将有着同样名字的两种类型带入到同一作用域就要求使用他们的父模组*
可以看到,这里使用父模组,就将两个 `Result` 类型区分开了。相反如果指明的是 `use std::fmt::Result;``use std::io::Result;`,就会得到同一作用域中的两个 `Result` 类型,而 Rust 就不明白在使用 `Result` 时,到底是要哪个了。
### 使用 `as` 关键字提供新名字
解决以 `use` 关键字将有着同样名字的两个类型,带入到同一作用域的问题,还有另一方法:在路径后面,可指定 `as`,与该类型的一个新本地名字,或者说 *别名alias*。下面清单 7-16 给出了通过将那两个 `Result` 类型中的一个,使用 `as` 关键字进行重命名,而编写清单 7-15 中代码的另一种方式。
文件名:`src/lib.rs`
```rust
use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {
// --跳过--
}
fn function2() -> IoResult {
// --跳过--
}
```
*清单 7-16在将某个类型带入作用域时使用 `as` 关键字对其进行重命名*
在第二个 `use` 语句中,选择了 `IoResult` 作为 `std::io::Result` 类型的新名字,这就不会与同时带入到作用域的、来自 `std::fmt``Result` 冲突了。清单 7-15 与清单 7-16 都被当作惯用方式,因此选择哪个就随你所愿了!
### 使用 `pub use` 将名字重新导出
**Re-exporting Names with `pub use`**
在使用 `use` 关键字将某个名字带入到作用域中时,这个在新作用域中可用的名字即为私有的。为了那些会调用到引入作用域代码的其他代码,能够像这个名字是被定义在引入到作用域的代码的作用域中一样,对这个名字进行引用,这时就可以结合上 `pub``use` 关键字。由于这里是将某个程序项目带入到作用域,而又同时将那个程序项目构造为可被其他代码将其带入他们的作用域,因此该技巧被称为 *重导出re-exporting*
下面清单 7-17 给出了将根模组中的 `use` 修改为 `pub use` 后,清单 7-11 中的代码。
文件名:`src/lib.rs`
```rust
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
```
*清单 7-17使用 `pub use`,于一个新作用域处将某个名字构造为对任意代码可用*
在此项修改之前,外部代码必须通过使用路径 `restaurant::front_of_house::hosting::add_to_waitlist()`,来调用其中的 `add_to_waitlist` 函数。现在既然这个 `pub use` 已将该 `hosting` 模组,自根模组中重新导出,那么外部代码现在就可以使用 `restaurant::hosting::add_to_waitlist()` 路径了。
在所编写代码的内部结构,与调用代码的程序员们对该领域有着不同设想时,重导出就是有用的。比如,在这个饭馆的比喻中,运营该饭馆的人设想的是“前厅”与“后厨”。但造访饭馆的食客,或许不会用这样的词汇,来认识饭馆的这些部位。有了 `pub use`,就可以一种结构来编写代码,而以另一种结构将代码暴露出来。这样做就让这个库,对于在该库上编写代码的程序员,与调用这个库的程序员,均具备良好的组织。在第 14 章的 [“运用 `pub use` 导出便利的公共 API”](Ch14_More_about_Cargo_and_Crates_io.md#使用-pub-use-导出好用的公开-api) 小节,将会看到另一个 `pub use` 的示例,并了解他是怎样影响到代码箱的文档。
### 使用外部 Rust 包
在第 2 章中,那里曾编写了用到名为 `rand` 外部包来获取一个随机数的猜数游戏项目。为了在项目中使用 `rand`,那里曾添加下面这行到 `Cargo.toml` 文件:
文件名:`Cargo.toml`
```toml
rand = `0.8.3`
```
`rand` 作为依赖项添加到 `Cargo.toml`,就告诉 Cargo去 [crates.io](https://crates.io/) 下载那个 `rand` 包和任何的依赖项,而令到 `rand` 对此项目可用。
随后为了将 `rand` 的一些定义,带入到所编写的包中,这里添加了以代码箱名字,`rand`,开头,并列出了打算要带入到作用域中的那些条目的一个 `use` 行。回顾第 2 章中的 [“生成一个随机数”](Ch02_Programming_a_Guessing_Game.md#生成随机数) 小节,那里就将那个 `Rng` 特质,带入到了作用域,并调用了 `rand::thread_rng` 函数:
```rust
use rand::Rng;
fn main() {
let secret_number = rand::thread_rng().gen_rang(1..=100);
}
```
Rust 社群业已构造了可在 [crates.io](https://crates.io/) 上取得的许多 Rust 包,而将任意的这些包,拉取进入自己的包,都涉及到这些同样步骤:将他们列在自己包的 `Cargo.toml` 文件中,并使用 `use` 来将他们代码箱中的条目,带入到作用域中。
请注意标准库(`std`)同样是个相对本地包的外部代码箱。由于标准库是 Rust 语言本身附带的,因此就无需修改 `Cargo.toml` 文件为包含 `std`。但为了将 `std` 中的条目带入到本地包作用域,是需要以 `use` 来引用他。比如,以 `HashMap` 来说,就要使用下面这行:
```rust
use std::collections::HashMap;
```
这是一个以 `std`,即标准库代码箱名字,开头的绝对路径。
### 运用嵌套路径来清理大量的 `use` 清单
**Using Nested Paths to Clean Up Large `use` Lists**
在用到定义在同一代码箱或同一模组中的多个条目时,若各自行上地列出这些条目,那么就会占据文件中的很多纵向空间。比如,清单 2-4 中的猜数游戏里,就有下面这两个 `use` 语句,他们将 `std` 中的两个条目带入到作用域:
文件名:`src/main.rs`
```rust
// --跳过--
use std::cmp::Ordering;
use std::io;
// --跳过--
```
相反,这里就可以使用嵌套路径,来在一个行中,把来自同一代码箱或包的那些条目,带入到作用域。通过指明路径的共同部分,接上一对冒号,及随后的花括号封闭包围起来的那些路径各异部分的清单,就完成了这一点,如下代码清单 7-18 所示。
文件名:`src/main.rs`
```rust
// --跳过--
use std::{cmp::Ordering, io};
// --跳过--
```
*清单 7-18指定出嵌套路径来将多个有着同样前缀的程序项目带入到作用域*
在更为大型的程序中,使用嵌套路径,将许多的程序项目,从同一代码箱或模组带入到作用域,可极大地减少所需的单独 `use` 语句数目!
在路径中的任何级别,都可使用嵌套路径,在对两个共用了子路径的 `use` 语句进行组合时,这是有用的。比如下面清单 7-19 就给出了两个 `use` 语句:一个将 `std::io` 带入到作用域,而另一个则是将 `std::io::Write` 带入到作用域。
文件名:`src/lib.rs`
```rust
use std::io;
use std::io::Write;
```
*清单 7-19其中一个为另一个子路径的两个 `use` 语句*
这两个路径的共同部分,即是 `std::io`,且那就是完整的第一个路径。为将这两个路径融合为一个 `use` 语句,这里可在嵌套路径中,使用 `self` 关键字,如下清单 7-20 中所示。
文件名:`src/main.rs`
```rust
use std::io::{self, Write};
```
*清单 7-20将清单 7-19 中的两个路径组合为一个 `use` 语句*
这行代码就将 `std::io``std::io::Write` 带入到了作用域。
### 全局操作符
在打算将某个路径中的 *全部all* 公开条目,都带入到作用域时,可将那个路径,后面跟上 `*`,即全局操作符,而予以指定:
```rust
use std::collections::*;
```
这个 `use` 语句,将定义在 `std::collections` 中的全部公开项目,都带入到了当前作用域。在使用这个全局操作符时要当心!全局带入,会导致更难于分清哪些名字是作用域中,与在所编写程序中用到的名字,是在何处定义的。
通常是在测试时,要将正在测试的全部程序项目带入到 `tests` 模组,才使用这个全局操作符;在第 11 章中的 [怎样编写测试](Ch11_Writing_Automated_Tests.md#怎样编写测试) 小节就会讲到这个问题。在序曲模式the prelude pattern有时也会用到全局操作符请参阅 [标准库文档](https://doc.rust-lang.org/std/prelude/index.html#other-preludes),了解有关更多序曲模式的知识。
## 将模组拆分为不同文件
**Separating Modules into Different Files**
到目前为止,本章的全部示例,都是将多个模组定义在一个文件中的。在模组变得大起来时,就会打算将他们的定义,迁移到单独文件,从而令到代码易于导览。
比如,这里就从清单 7-17 中的代码开始,并将那些模组提取到文件中,而非将所有那些模组,都定义在那个代码箱根文件里。在此情况下,代码箱根文件为 `src/lib.rs`,但这个过程同样对那些根文件为 `src/main.rs` 的二进制代码箱有效。
首先,会将那个 `front_of_house` 模组,提取到他自己的文件。要移除 `front_of_house` 模组花括号里头的代码,而仅留下 `mod front_of_house;` 语句声明,这样那个 `src/lib.rs` 就会包含如下清单 7-21 中展示的代码了。请注意在创建出后面清单 7-22 中的 `src/front_of_house.rs` 文件之前,这是不会编译的。
文件名:`src/lib.rs`
```rust
mod front_of_house;
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
```
*清单 7-21声明出其模组代码体将在 `src/front_of_house.rs` 中的 `front_of_house` 模组*
接下来,就要把原先在花括号中的代码,放入到一个新的名为 `src/front_of_house.rs` 文件中,如下清单 7-22 中所示。由于编译器在该代码箱根中,找到了名字 `front_of_house`,因此他就明白要在这个文件中看看。
文件名:`src/front_of_house.rs`
```rust
pub mod hosting {
pub fn add_to_waitlist() {}
}
```
*清单 7-22文件 `src/front_of_house.rs` 中 `front_of_house` 模组内部的定义*
请注意只需在模组树中的某处,使用一次 `mod` 声明,而将某个文件的内容加载进来。一旦编译器获悉该文件是项目的一部分(且由已将那个 `mod` 语句放置于于何处,而掌握了该代码在模组树中所处的位置),项目中的其他文件,则应如同之前 [用于引用模组树中项目的路径](#用于引用目录树中项目的路径) 小节中,曾讲到的到模组声明处的路径,来引用那个文件中的代码。也就是说,这里的 `mod` *并非* 其他编程语言有的那种 “include” 操作。
接下来,就要将那个 `hosting` 模组,提取到他自己的文件了。而由于 `hosting``front_of_house` ,而非根模组,的子模组,因此过程略有不同。这里将把 `hosting` 模组的文件,放在模组树中以其父辈命名的一个新目录中,此示例中即为 `src/front_of_house`
这里要将 `src/front_of_house.rs` 文件,修改为只包含 `hosting` 模组声明,以开始对 `hosting` 的迁移:
文件名:`src/front_of_house.rs`
```rust
pub mod hosting;
```
随后就要创建一个 `src/front_of_house` 的目录,和一个文件 `src/front_of_house/hosting.rs`,来包含在 `hosting` 模组中构造的那些定义:
文件名:`src/front_of_house/hosting.rs`
```rust
pub fn add_to_waitlist() {}
```
相反如果将 `hosting.rs` 放在 `src` 目录,那么编译器就会以为 `hosting.rs` 的代码,是在声明于代码箱根部的 `hosting` 模组中的,而不是那个 `front_of_house` 模组的子模组中的。为了获取模组代码,而要查看那些文件方面的编译器规则,就表明这些目录与文件,甚为紧密地于模组树结构相匹配。
#### 备用文件路径
>
> 本小节讲的是 Rust 编译器所用到的最惯用的文件路径;但较早的文件路径仍被支持。
>
> 对于定义在代码箱根部的名为 `front_of_house` 模组,编译器会在下面这些地方查找该模组的代码:
- `src/front_of_house.rs` (即这里讲到的);
- `src/front_of_house/mod.rs` (较早的,仍被支持的路径)。
> 而对于作为 `front_of_house` 的子模组的名为 `hosting` 的模组,编译器会在以下地方查找该模组的代码:
- `src/front_of_house/hosting.rs` (即这里讲到的);
- `src/front_of_house/hosting/mod.rs` (较早的,仍被支持的路径)。
> 对于同一模组,若同时使用这两种文件路径,那么就会得到一个编译器错误。而对同一项目中的不同模组,采用不同方式的文件路径是被允许的,只是会对那些导览项目的人造成困扰。
>
> 使用名为 `mod.rs` 文件方式的主要缺点,即那样的话,项目最终会以许多名为 `mod.rs` 文件而终结,在代码编辑器中,同时打开这些 `mod.rs` 文件,那么就会感到混乱。
将各个模组的代码,移入到单独文件现在就完成了,而模组树还是保持原来那样。尽管模组定义存在于不同文件中,但是无需任何修改,那个在 `eat_at_restaurant` 中的函数调用仍会工作。这种技巧,就实现了在模组大小增长时,将其迁移到新的文件中。
请注意 `src/lib.rs` 中的那个 `pub use crate::front_of_house::hosting;` 语句,同样不曾改变,而那个 `use` 也不会对哪些文件作为代码箱的部分,而被编译有任何的影响。`mod` 关键字定义了模组,而 Rust 则会在与该模组有着同样名字的文件中,查找要进到那个模组中的代码。
## 总结
Rust 实现了包拆分为多个代码箱,进而将代码箱拆分为多个模组,这样就可以从一个模组,对定义在另一模组中的程序项目加以引用。通过指明绝对或相对路径,就可以做到这点。使用 `use` 语句,就可以将这些程序项目的路径,带入到作用域,如此就可以在那个作用域中,多次用到所带入的程序项目时,使用较简短的路径。默认下模组代码是私有的,但可通过添加 `pub` 关键字,而将一些定义构造为公开的。
下一章中就会看看可在本地组织良好代码中使用到的标准库中的一些集合数据结构collection data structures

View File

@@ -13,770 +13,6 @@ Rust 标准库中包含了几种名为 *集合collections* 的有用数据
这里将讨论怎样创建与更新矢量、字符串与哈希映射,同时会讨论他们因何而变得特殊。
## 使用矢量类型,对值清单进行存储
End
**Storing Lists of Values with Vectors**
这里要看的第一个集合,便是 `Vec<T>`,也叫做 *矢量vector* 类型。矢量类型允许将多个值,存储在单个的、将全部这些值挨个放入内存的数据结构中。矢量类型仅能存储同一类型的这些值。在有着某个数据项目清单,比如某个文件中的那些文本行,或购物车中那些货品价格时,那么矢量类型就是有用的。
### 创建一个新的矢量值
要创建出一个新的空矢量值,就要调用 `Vec::new()` 函数,如下清单 8-1 所示:
```rust
let v: Vec<i32> = Vec::new();
```
*清单 8-1创建一个新的、用于保持一些类型 `i32` 值的空矢量*
请注意这里添加了个类型注解。由于这里没有往这个矢量插入任何值,因此 Rust 是不清楚这里要存储何种类别元素的。这是个重点。矢量值是使用泛型实现的;在后面第 10 章中,就会讲到怎样在自己的类型中使用泛型。而此刻,就要明白由标准库提供的这个 `Vec<T>` 可以保存任何类型。在创建保存特定类型的矢量时,可在尖括号里头指定那个类型。在清单 8-1 中,就告诉了 Rust`v` 中的那个 `Vec<T>` 将保存 `i32` 类型的元素。
而更为常见的则是,会创建带有初始值的 `Vec<T>`,同时 Rust 就会推断出要存储的值类型那么就很少会进行这样的类型注解。Rust 贴心地提供了 `vec!` 这个宏,这个宏就会创建出一个新的、保存给到他的那些值的矢量来。下面清单 8-2 就创建了一个新的、保存了值 `1``2``3``Vec<i32>`。之所以那个整数类型为 `i32`,是由于 `i32` 正是默认的整数类型,如同第 3 章的 ["数据类型"](Ch03_Common_Programming_Concepts.md#数据类型) 中所讨论的那样。
```rust
let v = vec! [1, 2, 3];
```
*清单 8-2创建一个新的包含了值的矢量*
由于这里已经给定了一些初始化 `i32` 的值,因此 Rust 就可以推断出 `v` 的类型为 `Vec<i32>`,而那个类型注解就不是必要的了。接下来,就要看看怎样修改矢量。
### 更新矢量
要创建出一个矢量,并随后将一些元素添加给他,就可以使用 `push` 方法,如下清单 8-3 中所示。
```rust
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
```
*清单 8-3使用 `push` 方法来把一些值添加到某个矢量*
正如第 3 章中所讨论的,这里与任何变量一样,在想要能修改矢量的值时,就要使用 `mut` 关键字,将其构造为可变。这里在矢量内部的数字,全部都是 `i32` 类型,而 Rust 就会从这些数据,推断出这个类型,因此这里不需要 `Vec<i32>` 类型注解。
### 丢弃某个矢量,就会丢弃他的元素
**Dropping a Vector Drops Its Elements**
与其他任何 `struct` 一样,矢量在超出作用域时,就会被释放掉,如下清单 8-4 所示。
```rust
{
let v = vec! [1, 2, 3, 4];
// 对 v 执行一些操作
} // 这里 v 就超出了作用域,而被释放掉
```
*清单 8-4对矢量及其元素在何处被丢弃进行展示*
在这个矢量被丢弃时,那么他所有内容也会被丢弃,即他保存的那些整数将被清理掉。这初一看似乎直接明了,然而在开始触及到一些到该矢量元素的引用时,事情就会变得复杂。接下来就要解决这个问题!
### 读取矢量的元素
引用存储在矢量中某个值的方式有两种:经由索引,或使用 `get` 方法。在接下来的示例中,为讲得更清楚的原因,已经对从这些方法返回的值进行了注释。
下面清单 8-5 给出了访问矢量某个值的两种方式,即索引语法与 `get` 方法。
```rust
let v = vec! [1, 2, 3, 4];
let third: &i32 = &v[2];
println! ("第三个元素为 {}", third);
match v.get(2) {
Some(third) => println! ("第三个元素为 {}", third),
None => println! ("没有第三个元素。"),
}
```
*清单 8-5使用索引语法或 `get` 方法访问矢量中的某个元素*
请留意这里的两个细节。首先,由于矢量是以从零开始的数字进行索引,因此这里使用了索引值 `2` 来获取那第三个元素。其次,这里是通过同时使用 `&``[]`,获取第三个元素的,这就给到一个引用变量,而使用带有作为参数传递索引的 `get` 方法,给到的却是个 `Option<&T>` 值。
Rust 提供这两种引用某个元素方式的原因在于,有了这两种方式,就可以在尝试使用某个超出了既有元素范围的索引值时,对程序此时的表现加以选择。比如,下面就来看看在有着一个五个元素的矢量,而随后尝试以两种技巧,来访问位于索引 `100` 处元素时,会发生什么事情,如下清单 8-6 中所示。
```rust
let v = vec! [1, 2, 3, 4, 5];
let does_not_exist = &v[100];
let does_not_exist = v.get(100);
```
*清单 8-6尝试访问包含五个元素矢量中索引 `100` 处的元素*
在运行此代码时,由于第一种 `[]` 方式引用了不存在的元素,因此将导致程序死机。在有着某个对超出矢量末端的元素进行访问的尝试,而打算将程序崩溃掉时,那么用这种方式是最佳的。
而在传递给 `get` 方法的索引,位于矢量外部时,他就会返回不会程序死机的 `None` 值。在寻常情况下,就会时不时出现对超出矢量范围元素的访问时,就应使用这种方式。这时代码就将有如同第 6 章中所讨论的,处理 `Some(&element)``None` 值的逻辑。比如,那个索引可以是来自某人输入的数字。在他们不小心输入了一个过大的数字时,程序就会得到一个 `None` 值,这时就可以告诉用户在当前矢量中有多少个项目,并给到他们又一次输入有效值的机会。比起由于输入错误而将程序崩溃掉,这将是更加用户友好!
当程序有了有效引用时Rust 的借用检查器,就会强制执行所有权与借用规则检查(在第 4 章讲到过),来确保该引用及全部其他的、到这个矢量内容的引用是有效的。请回顾那条表明了不能在同一作用域中,有着多个可变与不可变引用的规则。那条规则就适用于下面清单 8-7清单中有着一个到矢量首个元素的可变引用并尝试将一个元素添加到示例末尾。若同时在那个函数中尝试引用那个元素那么该程序就不会工作
```rust
let mut v = vec! [1, 2, 3, 4, 5];
let first: &i32 = &v[0];
v.push(6);
println! ("首个元素为:{}", first);
```
*清单 8-7在保留到矢量某个条目的引用同时尝试将一个元素添加到该矢量*
对此代码进行编译,将引发下面这个错误:
```console
$ cargo run  ✔
Compiling vec_demo v0.1.0 (/home/peng/rust-lang/projects/vec_demo)
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> src/main.rs:6:5
|
4 | let first: &i32 = &v[0];
| - immutable borrow occurs here
5 |
6 | v.push(6);
| ^^^^^^^^^ mutable borrow occurs here
7 |
8 | println! ("首个元素为:{}", first);
| ----- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `vec_demo` due to previous error
```
清单 8-7 中的代码看起来似乎可以工作为何到矢量首个元素的引用会牵连到该矢量末尾的变化呢这个报错是由于矢量工作原理由于矢量是将他的那些值挨个放在内存中的那么将新元素添加到矢量末尾而在该矢量当前存储处没有足够场所来挨个放置全部这些元素时这时就会要求分配新内存并将那些旧有元素拷贝到新内存空间。在那种情况下到首个元素的引用就会指向已解分配内存deallocated memory。而正是这些 Rust 的借用规则,防止程序已这样的情形而告终。
> **请注意**:更多有关 `Vec<T>` 类型的实现细节,请参考 [Rust 专论The Rustonomicon](https://doc.rust-lang.org/nomicon/vec/vec.html)。
### 对矢量中那些值的迭代
要依次访问矢量中的各个元素,就要迭代全部元素,而非使用那些索引值,一次访问一个了。下面清单 8-8 展示了怎样使用 `for` 循环,来获取到一个 `i32` 矢量值中各个元素的不可变引用,并将这些元素打印出来。
```rust
let v = vec! [100, 32, 57];
for i in &v {
println! ("{}", i);
}
```
*清单 8-8通过使用 `for` 循环对各个元素进行迭代,而打印出矢量中的每个元素*
也可以为了对全部元素进行修改,而对可变示例中的各个元素,进行可变引用的迭代。下面清单 8-9 中的 `for` 循环,将把 `50` 添加到各个元素。
```rust
let mut v = vec! [100, 32, 57];
for i in &mut v {
*i += 50;
}
```
*清单 8-9对实例中各个元素进行可变引用的迭代*
要修改可变引用所指向的值,就必须使用 `*` 解引用操作符the `*` dereference operator在能够使用 `+=` 运算符之前,获取到 `i` 中的那个值。在后面第 15 章的 [“以解引用操作符,顺着指针找到值”](Ch15_Smart_Pointers.md#跟随指针到其值) 小节,就会讲到这个解引用操作符。
### 使用枚举存储多种类型
矢量只能存储同一类型的值。这就会不方便;显然是有需要存储不同类型条目清单的使用场景。幸运的是,枚举的那些变种,就是定义在同一枚举类型之下的,那么在需要某种表示那些不同类型元素的类型时,就可以定义并使用一个枚举!
好比说这里要从电子表格的某行,其中该行的一些列包含了整数,另一些列包含了浮点数,而其他列则包含了字符串,而要从这行获取到这些数据。那么就可以定义这样一个枚举,他的那些变种将保存这些不同值类型,而全部这些变种,就会被看着是同一类型:即为该枚举。随后就可以创建一个矢量,来保存那个枚举,进而最终保存了这些不同类型。下面清单 8-10 中就对此进行了演示。
```rust
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec! [
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
```
*清单 8-10定义一个 `enum` 来在矢量中存储不同类型的值*
Rust 需要在编译时了解那个矢量中会有些什么类型,这样他就清楚存储该矢量的每个元素,所需要的内存堆上内存准确数量。同时必须显式声明该矢量中允许哪些类型。若 Rust 允许矢量保存任意类型,那么就会存在一个或多个类型,将引发在该矢量的元素上执行操作错误的可能。运用枚举加上 `match` 表达式,就意味着 Rust 将在编译时,确保所有可能情形都被处理,如同第 6 章中讨论的那样。
但若不清楚运行时程序会在矢量中收到的详尽类型集合那么这个枚举技巧就不会有用。相反这个时候就可以使用特质对象a trait object而在后面的第 17 章就会讲到特质对象。
既然这里已经讨论了使用矢量的一些最常用方式,那就一定要看看 [API 文档](https://doc.rust-lang.org/std/vec/struct.Vec.html),了解那些定义在 `Vec<T>` 上,由标准库所定义的全部有用方法。比如,除了 `push` 之外,`pop` 方法会移除并返回矢量的最后一个元素。下面就移步到下一个集合类型:`String` 吧!
## 使用 `String` 存储 UTF-8 编码的文本
在第 4 章中,就曾谈到过字符串,而现在则要深入审视他们。萌新 Rust 公民,通常会由于以下三个搅在一起的原因,而被字符串给卡住:作为 Rust 暴露各种可能错误的选择;相比于许多程序员意识到的复杂度,字符串是一种更具复杂度的数据结构;还有就是 UTF-8。在从别的语言转到 Rust 时,这些因素就会有以看起来有难度的方式,纠缠在一起。
由于字符串是作为字节集合,加上一些在将这些字节解析为文本时,提供有用功能的方法,这样来实现的,因此这里就在集合的语境中,来讨论字符串了。在本小节,将谈及在 `String` 类型上的那些所有集合都有的操作,比如创建、更新与读取等等。这里也会讨论 `String` 与其他集合的不同之处,即通过对比人类与机器解读 `String` 类型数据的不同之处,来搞清楚索引进到某个 `String` 有何等复杂。
### 何为 `String`
这里首先就要定义一下,*字符串string* 这个名词指的是什么。Rust 在其核心语言中,只有一种字符串类型,那就是字符串切片类型 `str`,该类型通常是以其被借用的形式 `&str` 而出现。在第 4 章中,就讲到过 *字符串切片string slices*,他们是到一些存储在各处的、以 UTF-8 编码的字符串数据的引用。
`String` 类型,则是由 Rust 标准库所提供而非编码进核心语言的一种可增长、可变、所有权被持有的、UTF-8 编码的字符串类型the `String` type, which is provided by Rust's standard library rather than coded into the core lanuage, is a growable, mutable, owned, UTF-8 encoded string type。在 Rust 公民提到 Rust 中的 “字符串” 时,他们可能指的既是 `String`,也可可能是字符串切片的 `&str` 类型,而不仅仅是这些类型其中之一。虽然这个小节很大部分讲的是 `String`,但在 Rust 标准库中,两种类型都有重度使用,且 `String` 与字符串切片,都是 UTF-8 编码的。
Rust 标准库还包含了一些其他字符串类型,比如 `OsString``OsStr``CString``CStr` 等等。一些库代码箱则可提供到甚至更多的用于存储字符串数据的选项。发现这些名称都是以 `String``Str` 结尾的了吧?他们指向的都是是有所有权的与借用的变种,就跟先前所见到的 `String``str` 类型一样。比如,这些字符串类型就可存储不同编码或在内存中以不同方式表示的文本。本章中不会讨论这些其他字符串类型;请参阅 API 文档,了解更多有关如何使用他们,以及何时哪个是恰当的字符串类型的更多知识。
### 创建一个新的 `String`
`Vec<T>` 的许多同样操作,对 `String` 也是可用的,这里就以创建一个新字符串的 `new` 函数开始,如下清单 8-11 中所示。
```rust
let mut s = String::new();
```
*清单 8-11创建一个新的空 `String`*
这行代码就创建了一个新的、名为 `s` 的空字符串,随后就可以将数据加载进这个空字符串了。通常,这里会有一些初始数据作为字符串的开头。为此,就要使用 `to_string` 方法,这个方法在所有实现了 `Display` 特质the `Display` trait正如字符串字面值这样的类型上都是可用的。下面清单 8-12 给出了两个示例。
```rust
let data = "初始内容";
let s = data.to_string();
// 该方法同样直接工作于字面值之上
let s = "初始内容".to_string();
```
*清单 8-12使用 `to_string` 方法自字符串字面值创建出一个 `String`*
此代码传教了一个包含 `初始内容` 的字符串。
这里也可以使用函数 `String::from` 来从字符串字面值创建 `String`。下面清单 8-13 众多的代码,与使用 `to_string` 的清单 8-12 中的代码等价。
```rust
let s = String::from("初始内容");
```
*清单 8-13使用 `String::from` 函数,从字符串字面值创建一个 `String`*
由于字符串有相当多的用途,因此就可以使用字符串的众多不同的通用 API而赋予到很多选择。这些字符串通用 API 中的一些,可能看起来是重复的,但他们全都有他们的用处!就在这个示例中,`String::from``to_string` 两个函数完成的是同样的事情,那么选择哪个,就关乎代码风格与可读性了。
请记住字符串都是 UTF-8 编码的,因此就可以将任何编码恰当的数据,包含在字符串中,如下清单 8-14 中所示。
```rust
let hello = String::from("السلام عليكم");
let hello = String::from("Dobrý den");
let hello = String::from("Hello");
let hello = String::from("שָׁלוֹם");
let hello = String::from("नमस्ते");
let hello = String::from("こんにちは");
let hello = String::from("안녕하세요");
let hello = String::from("你好");
let hello = String::from("Olá");
let hello = String::from("Здравствуйте");
let hello = String::from("Hola");
let hello = String::from("👋");
```
*清单 8-14以不同语言在字符串中存储问候语*
全部这些都是有效的 `String` 值。
### 更新 `String`
在将更多数据压入到其中时,`String` 可以增长大小,且就跟 `Vec<T>` 的内容一样,内容可以改变。此外,还可以方便地使用 `+` 运算符或 `format!` 宏,来连接一些 `String` 值。
**使用 `push_str` 与 `push`,往 `String` 追加数据**
通过使用 `push_str` 方法来追加一个字符串切片,就可以增大 `String`,如下清单 8-15 中所示的那样。
```rust
let mut s = String::from("foo");
s.push_str("bar");
println! ("{}", s);
```
*清单 8-15使用 `push_str` 方法将一个字符串切片追加到某个 `String`*
在这两行之后,`s` 就会包含 `foobar`。由于这里并非真的想要取得那个参数的所有权,因此这个 `push_str` 方法取的是个字符串切片。而比如在下面 8-16 中的代码里,就打算在将 `s2` 的内容追加到 `s1` 后,能够对 `s2` 进行使用。
```rust
let mut s1 = String::from("foo");
let s2 = "bar";
s1.push_str(s2);
println! ("s2 为 {}", s2);
```
*清单 8-16在将一个字符串切片的内容追加到某个 `String` 后再对其加以使用*
若这个 `push_str` 方法取得了 `s2` 的所有权,那么这里就无法在最后一行打印其值。然而,这段代码正如预期那样运作了!
相比 `push_str` 方法,这个 `push` 方法则会取单个字符作为参数,并将其添加到 `String`。下面清单 8-17 就使用这个 `push` 方法,将字母 "l" 添加到了一个 `String`
```rust
let mut s = String::from("lo");
s.push('l');
```
*清单 8-17使用 `push` 方法,将一个字符添加到某个 `String`*
作为上面代码的结果,`s` 将包含 `lol`
#### 使用 `+` 运算符或 `format!` 宏的字符串连接
通常,会想要将两个既有字符串合在一起。完成此操作的一种方式,就是使用 `+` 运算符,如下清单 8-18 中所示。
```rust
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // 请注意这里的 s1 已被迁移,而不再能被使用了
```
*清单 8-18运用 `+` 运算符来将两个 `String` 值结合为一个新的 `String` 值*
这个字符串 `s3` 将包含 `Hello, world!``s1` 在该字符串加法之后不再有效的原因,以及这里使用到 `s2` 引用的原因,与这里使用 `+` 运算符时,被调用的那个方法的签名有关。这个 `+` 运算符使用了 `add` 方法,而该方法的签名使用了 `add` 方法,`add` 方法的签名,看起来与下面的类似:
```rust
fn add(self, s: &str) -> String
```
在标准库中,就会看到使用泛型定义的 `add` 函数。这里已将泛型的那些参数,用具体类型进行了替换,即在以 `String` 类型值调用是所发生的。在第 10 章就会讨论泛型。这个函数签名,提供了了解那个 `+` 运算符棘手之处所需的线索。
首先,这里的 `s2` 有个 `&`,表示这里这里正将第二个字符串的 *引用*,添加到第一个字符串。这是由于 `add` 函数中的那个 `s` 参数的原因:这里只能将一个 `&str` 添加到某个 `String`;这里是无法将两个 `String` 相加在一起的。不过稍等一下 -- `&s2` 的类型是 `&String`,而非在 `add` 函数的第二个参数中所指明的 `&str`。那为何清单 8-18 会编译呢?
这里之所以能在到 `add` 的调用中使用 `&s2` 的原因,在于编译器可将那个 `&String` 参数,*强制转换* 为 `&str` 类型。在调用 `add` 方法时Rust 使用了 *解引用强制转换deref coercion* 特性,在这里该特性就将 `&s2` 转换为了 `&s2[..]`。在第 15 章就将深入讨论这个解引用强制转换。由于 `add` 方法并未占据那个 `s` 参数的所有权,因此 `s2` 在此运算之后,仍将有效。
其次,这里可以看到,在该方法签名中,由于 `self` *没有* `&`,那么 `add` 就取得了 `self` 的所有权。这就意味着清单 8-18 中的 `s1` 将被迁移到那个 `add` 调用中,并在那之后便不再有效。这样看来,尽管 `let s3 = s1 + &s2;` 这个语句看起来将同时拷贝这两个字符串,并创建一个新的字符串,不过此语句实际上是要取得 `s1` 的所有权,追加 `s2` 内容的一份拷贝,进而随后返回该运算结果的所有权。也就是说,看起来这行语句构造了很多拷贝,但并没有;这样的实现比拷贝更为高效。
在需要连接多个字符串时,这个 `+` 运算符的行为就变得笨拙了:
```rust
let s1 = String::from("tic");
let s2 = String::from("toc");
let s3 = String::from("toe");
let s = s1 + "-" + &s2 + "-" + &s3;
```
此处 `s` 将为 `tic-toc-toe`。由于有些全部的 `+``"` 字符,因此就难于看清发生了什么。对于较复杂的字符串合并,可这个 `format!` 宏:
```rust
let s1 = String::from("tic");
let s2 = String::from("toc");
let s3 = String::from("toe");
let s = format! ("{}-{}-{}", s1, s2, s3);
```
这段代码同样把 `s` 设置为了 `tic-toc-toe`。这个 `format!` 宏与 `println!` 宏的运作类似,而与将输出打印到屏幕不同,他会将结果内容,以一个 `String` 加以返回。使用 `format!` 这个版本的代码,读起来容易得多,且由于 `format!` 宏所生成的代码,使用的是引用,那么这个调用就不会占据任何一个其参数的所有权。
### 索引到 `String` 内部
再许多其他编程语言中,经由通过索引而引用字符串中的一些单独字符,都是有效且常见的操作。不过在 Rust 中,当尝试使用索引来访问某个 `String` 的一些部分时,就会收到错误。请考虑下面清单 8-19 中的无效代码。
```rust
let s1 = String::from("hello");
let h = s1[0];
```
*清单 8-19尝试在某个字符串上使用索引语法*
该代码将引发下面的错误:
```console
$ cargo run
Compiling string_demo v0.1.0 (/home/peng/rust-lang/projects/string_demo)
error[E0277]: the type `String` cannot be indexed by `{integer}`
--> src/main.rs:3:13
|
3 | let h = s1[0];
| ^^^^^ `String` cannot be indexed by `{integer}`
|
= help: the trait `Index<{integer}>` is not implemented for `String`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `string_demo` due to previous error
```
这个报错和提示讲清了缘由Rust 的字符串不支持索引。但为什么不支持呢?要回到这个问题,就要探讨一下 Rust 是怎样在内存中存储字符串的。
**内部表示**
`String` 是对 `Vec<u8>` 的一种封装a `String` is a wrapper over a `Vec<v8>`)。下面来看看清单 8-14 中,那里的一些以 UTF-8 良好编码的示例字符串。首先是这个:
```rust
let hello = String::from("Hola");
```
在此示例中,`len` 将为 `4`,这表示这个存储着字符串 “Hola” 的矢量长度为 4 个字节。这些字母在以 UTF-8 编码时,每个占用 1 个字节。而接下来的这行,就会惊讶到你了。(请注意这个字符串是以大写西里尔字母 `Ze` 开头,而非阿拉伯数字 `3`。)
```rust
let hello = String::from("Здравствуйте");
```
在被问及这个字符串有多长时,你可能会讲是 `12`。事实上Rust 的答案是 `24`:由于那个字符串中的每个 Unicode 标量值,都有占用 2 字节的存储,故那就是以 UTF-8 编码 `Здравствуйте` 所用占用的字节数。由于这个缘故,到该字符串的那些字节的所以,就不会总是对应到某个有效的 Unicode 标量值了。为对此加以演示,请设想下面这段无效的 Rust 代码:
```rust
let hello = String::from("Здравствуйте");
let answer = &hello[0];
```
这里当然清楚 `answer` 将不会是那第一个字母 `З`。在以 UTF-8 编码时,`З` 的第一个字节是 `208`,同时第二个字节为 `151`,因此看起来 `answer` 事实上应该是 `208`,但 `208` 本身并不是个有效的字符。在用户要求该字符串的首个字母时,返回 `208` 就不会是他们所想要;然而,那却是 Rust 在字节索引 `0` 处有的唯一数据了。即使字符串只包含拉丁字母,用户也通常不想要那个返回的字节值:即便 `&"hello"[0]` 是返回了字节值的有效代码,他也会返回 `104`,而非 `h`
那么答案就是为避免返回一个不期望的值以及避免引发一些可能无法立即发现的程序错误Rust 就根本不会编译此代码,并阻止了在开发过程早期阶段的这些误解。
**字节、标量值与字素簇我的天Bytes and Scalar Values and Grapheme Clusters! Oh My!**
有关 UTF-8 的另一点,即为从 Rust 视角看待字符串,事实上有三种相关方式:视为字节、标量值与字素簇(而字素簇则是与我们称之为 *文字/letters* 的东西的最接近的事物了)。
在看到以梵文字书写的印地语词汇 `"नमस्ते"` 时,这个词汇是以看起来像下面这样的一些 `u8` 类型值的矢量存储的:
```rust
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164, 224, 165, 135]
```
这是 18 个字节,也是计算机最终存储该数据的方式。而在将他们视为 Unicode 的标量值,即 Rust 的 `char` 类型时,则这些字节看起来是这样的:
```rust
['न', 'म', 'स', '्', 'त', 'े']
```
这里就有了六个 `char` 值了但其中第四与第六个并不是文字letters他们是自身并无意义的变音符号。最后在将这些 Unicode 标量值视为字素簇时,就得到了人类所称呼的、四个构成了那个印地词语的四个文字:
```rust
["", "", "स्", "ते"]
```
Rust 提供了解析计算机存储的原始字符串数据的数种不同方式,因此各个程序就可以选择他所需的解析方式,这与该数据为何种人类语言无关。
Rust 不允许索引进入 `String` 来获取某个字符的终极原因,即是索引操作,被认为总是消耗固定的时间(即 `O(1)`)。但由于 Rust 将不得不从开头遍历到索引位置,来确定那里有多少个有效字符,由此对于在 `String` 上执行索引操作,所消耗的时间是无法确保持续一致的。
### 对字符串进行切片操作
**Slicing Strings**
由于字符串索引操作的返回值类型不明朗:可能是字节值、字符、字素簇,或者字符串切片,因此索引到字符串中去,通常是个糟糕的主意。而在确实需要使用索引来创建字符串切片时,那么 Rust 就要求提供更具体的索引。
与使用带有单个数字的 `[]` 相比,可使用带有范围的 `[]`,来创建包含一些特定字节的字符串切片:
```rust
let hello = String::from("Здравствуйте");
let s = &hello[0..4];
```
这里的 `s` 将是个包含该字符串头 4 个字节的 `&str`。早先曾提到过,每个的这些字符都是 2 字节,那么这就意味着 `s` 将为 `Зд`
而在尝试使用类似 `&hello[0..1]` 这样的操作来对某个字符的那些字节的一部分进行切片时Rust 就会在运行时,以与在矢量中访问无效索引时同样的方式终止运行:
```console
thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `Здравствуйте`'...
```
由于使用范围来创建出字符串切片这样的操作,可能将程序崩溃掉,因此进行这样操作时应小心谨慎。
### 对字符串进行迭代的一些方法
在字符串的各个片段上进行操作的最好方式,就是显示地指明是要字符还是字节。对于单独的 Unicode 标量值,就使用 `chars` 方法。在 `नमस्ते` 上调用 `chars`,就会分理出并返回六个类型 `char` 的值来,进而就可以对结果进行迭代,而访问到各个元素:
```rust
for c in "नमस्ते".chars() {
println!("{}", c);
}
```
该代码将打印下面的东西:
```rust
```
此外,`bytes` 方法返回的则是各个原始字节,对与特定领域,这方法可能正好:
```rust
for b in s.bytes() {
println!("{}", b);
}
```
该代码将打印出构成这个 `String` 的 18 个字节来:
```console
224
164
168
224
164
174
224
164
184
224
165
141
224
164
164
224
165
135
```
不过要确保记住,有效的 Unicode 标量值可能有多余 1 个字节组成。
而从字符串获取字素簇则是复杂的,因此这项功能并未由标准库提供。在需要该功能时,在 [crates.io](https://crates.io/) 上有一些可用的代码箱。
### 字符串并不简单
总的来说字符串是复杂的。不同编程语言在以何种程度将这种复杂度呈现给编程者上做出了不同的选择。Rust 选择了将正确处理 `String` 数据,作为所有 Rust 程序的默认行为,这就意味着 Rust 程序员就必须在处理 UTF-9 数据时,要提前投入更多思考。这种权衡暴露了相较于其他编程语言,更多的字符串复杂度,但这防止了在软件开发生命周期后期,将涉及到的非 ASCII 字符的错误处理。
接下来就要切换到些许不那么复杂的事情:哈希图!
## 在哈希图中存储关联的键与值
最后一个常用集合,就是 *哈希图hash map* 了。类型 `HashMap<K, V>`,存储了使用确定如何将这些类型为 `K` 的键,与类型为 `V` 的值放置于内存中的 *散列函数a hashing function*,而建立的键与值映射关系。许多编程语言都支持这种数据结构,不过他们通常使用了别的名称,比如哈希、图、对象、哈希表、字典,或者关系数组,这里仅举几例。
在打算不使用如同矢量中那样的索引,而是通过使用可为任意类型的键,来查找数据时,哈希图就是有用的了。比如在某个游戏中,就可在各个键为战队名字,值为战队得分的哈希图中,保持对这些战队得分的追踪。在给到战队名字后,就可获取到他的得分。
本小节将审视哈希图集合数据结构的基本 API不过有数不尽的哈希图好处都是隐藏在由标准库所定义、`HashMap<K, V>` 上的那些函数里。与往常一样,请查看标准库文档,来了解更多信息。
### 创新一个新的哈希图
创建空哈希图的一种方式,即为使用 `new` 方法,与使用 `insert` 方法进行元素添加。在下面清单 8-20 中,就要对两个名字分别为 *蓝队Blue**黄队Yellow* 的战队得分,进行追踪。蓝队以 10 分开始,而黄队以 50 分开始。
```rust
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("蓝队"), 10);
scores.insert(String::from("红队"), 50);
```
*清单 8-20创建一个新的哈希图并插入一些键与值*
请注意这里需要首先 `use` 这个来自标准库集合部分的 `HashMap`。三个常用集合中,这个是最少用到的,因此他就没有包含在那些 Rust 程序前奏the prelude自动带入的特性里。哈希图受标准库的支持也较少比如标准库中就没有内建的构造哈希图的宏。
与矢量一样,哈希图是将他们的数据存储在内存堆上的。示例中的这个 `HashMap` 键的类型为 `String`,值的类型为 `i32`。与矢量类似哈希图都是同质的homogeneous所有键都必须有着同样类型且所有值也必须有着同样类型。
另一种构造哈希图的方式,即为通过使用迭代器,与元组矢量上的 `collect` 方法,而元组矢量中各个元组,则是由一个键与其值组成。在 [第 13 章的 “使用迭代器处理一系列的条目” 小节](Ch13_Functional_Language_Features_Iterators_and_Closures.md#使用迭代器对条目系列进行处理),就会深入到迭代器的有关细节及其相关方法。`collect` 方法会将数据收集到包括 `HashMap` 在内数种集合类型中。比如,在将战队名字与初始得分放在两个单独矢量中时,那么就可以使用 `zip` 方法,来创建一个元组的迭代器,其中 `Blue` 就会与 `10` 结对,并以此类推。随后就可以使用 `collect` 方法类将那个元组迭代器,转换到一个哈希图了,如下清单 8-21 中所示。
```rust
use std::collections::HashMap;
let teams = vec! [String::from("蓝队"), String::from("红队")];
let initial_scores = vec! [10, 50];
let mut scores: HashMap<_, _> = teams
.into_iter()
.zip(initial_scores.into_iter())
.collect();
```
*清单 8-21从战队清单与得分清单创建一个哈希图*
由于有可能 `collect` 进到许多不同数据结构,而除非有指明,那么 Rust 就不清楚所想要的是何种数据结构,因此这里的类型注解 `HashMap<_, _>` 是需要的。不过对于键与值类型的泛型参数,这里使用了下划线(`_`),而 Rust 可基于那两个矢量中数据的类型,而推断出该哈希图的类型。在上面清单 8-21 中,键的类型将是 `String`,而值类型将为 `i32`,就跟清单 8-20 中的一样。
### 哈希图与所有权
对于实现了 `Copy` 特质the `Copy` trait 的那些类型,比如 `i32`,那么他们的值就被拷贝到哈希图里。而对于像是 `String` 这样的被持有值,他们的所有值就会被迁移,进而哈希图会成为这些值的所有者,如同在下面清单 8-22 中所演示的那样。
```rust
use std::collections::HashMap;
let field_name = String::from("喜好颜色");
let field_value = String::from("蓝色");
let mut map = HashMap::new();
map.insert(field_name, field_value);
println! ("{}, {}", field_name, field_value);
// 到这里 field_name 与 field_value 就无效了,请尝试对
// 他们进行使用,并看看会收到什么样的编译器错误!
```
*清单 8-25对一旦被插入到哈希图键与值就被哈希图持有的展示*
```console
$ cargo run  ✔
Compiling hashmap_demo v0.1.0 (/home/peng/rust-lang/hashmap_demo)
error[E0382]: borrow of moved value: `field_name`
--> src/main.rs:10:25
|
4 | let field_name = String::from("喜好颜色");
| ---------- move occurs because `field_name` has type `String`, which does not implement the `Copy` trait
...
8 | map.insert(field_name, field_value);
| ---------- value moved here
9 |
10 | println! ("{}, {}", field_name, field_value);
| ^^^^^^^^^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0382]: borrow of moved value: `field_value`
--> src/main.rs:10:37
|
5 | let field_value = String::from("蓝色");
| ----------- move occurs because `field_value` has type `String`, which does not implement the `Copy` trait
...
8 | map.insert(field_name, field_value);
| ----------- value moved here
9 |
10 | println! ("{}, {}", field_name, field_value);
| ^^^^^^^^^^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0382`.
error: could not compile `hashmap_demo` due to 2 previous errors
```
在对 `insert` 调用而导致 `field_name``field_value` 被迁移到那个哈希图中之后,这里就无法使用这两个变量了。
而在将到值的引用插入进哈希图时,这些值就不会被迁移进哈希图。对于这些引用所指向的值,则只要哈希图尚有效,那么他们便一直有效。在后面第 10 章的 [“以声明周期对引用有效性进行验证”](Ch10_Generic_Types_Traits_and_Lifetimes.md#使用生命周期对引用加以验证) 小节中,将进一步讲到这些问题。
### 访问哈希图中的值
通过将某个值的键提供给 `get` 方法,就可以从哈希图中获取到该值来,如下清单 8-23 中所示。
```rust
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("蓝队"), 10);
scores.insert(String::from("红队"), 50);
let team_name = String::from("蓝队");
let score = scores.get(&team_name);
```
*清单 8-23对存储在哈希图中的蓝队得分进行访问*
这里,`score` 将具有与蓝队关联的取值,同时结果将为 `Some(&10)`。由于 `get` 方法返回的是 `Option<&V>` 类型,因此该结构是封装在 `Some` 中的;当在哈希图中没有那个键的值时,`get` 就会返回 `None`。程序就需要以在第 6 章中所讲到的那些方式之一,对这个返回的 `Option` 加以处理。
可以与对矢量进行迭代的类似方式,即使用 `for` 循环,对哈希图中的各个键/值对加以迭代:
```rust
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("蓝队"), 10);
scores.insert(String::from("红队"), 50);
for (key, value) in &scores {
println! ("{}, {}", key, value);
}
```
此代码将以任意顺序,打印出各个键值对:
```console
蓝队, 10
红队, 50
```
### 更新哈希图
虽然键值对数目是可增长的,但每个键在某个时刻,只能有一个与其关联的值。在要修改哈希图中的数据时,就必须定下来怎样处理某个键已经指定了值的情形。可以将原有值替换为新值,而完全忽略原有值。可以保留原有值而忽视掉新值,而在键 *尚未* 有值时,仅将新值进行添加。或者可以将原有值与新值结合在一起。那就来看看怎样处理这些各种情况!
**重写某个值**
在将一个键与一个值插入到哈希图,并在随后再插入同样键与一个不同值,那么与那个键关联的值就会被替换掉。尽管下面清单 8-24 中的代码调用了 `insert` 两次,由于这里两次都是插入 “蓝队” 的值,因此那个哈希图将只包含一个键/值对。
```rust
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("蓝队"), 10);
scores.insert(String::from("蓝队"), 25);
println! ("{:?}", scores);
```
*清单 8-24以特定键对存储的某个值进行替换*
此代码打印 `{"蓝队": 25}`。原先的值 `10` 已被重写。
**在键无值时而仅插入值**
检查某个特定键是否有值,并在其没有值时,为其插入一个值,这样的情况是常见的。为此哈希图有个特别的、名为 `entry` 的 API他会取要检查的键作为参数。`entry` 方法的返回值,是个叫做 `Entry` 的枚举,表示一个可能存在或可能不存在的值。那么下面就假设这里想要检查黄队的键有无与其关联的值。在其没有关联值时,就想要插入值 `50`,对于蓝队也同样操作。运用这个 `entry` API代码看起来就如同下面清单 8-25 一样。
```rust
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("蓝队"), 10);
scores.entry(String::from("黄队")).or_insert(50);
scores.entry(String::from("蓝队")).or_insert(50);
println! ("{:?}", scores);
```
*清单 8-25使用 `entry` 方法仅在键尚无值时插入值*
`Entry` 类型上的 `or_insert` 方法,被定义为在键存在时,返回相应 `Entry` 键的值的可变应用,而若键不存在,那么就会将其参数作为该键的新值插入,并返回到该新值的可变引用。此技巧相比于咱们自己来编写该逻辑,要清楚得多,此外,在以借用规则检查器进行检查时,进行得也更好。
运行清单 8-25 中的代码,将打印出 `{"黄队": 50, "蓝队": 10}`。其中首次到 `entry` 的调用,由于黄队尚无值,因此就会插入黄队的键与值 `50`。而第二个到 `entry` 的调用,因为蓝队已经有了值 `10`,因此就不会修改这个哈希图。
**基于原有值而对某个值进行更新**
哈希图的另一个常见使用情形,即是查找某个键的值,并随后根据原有值对其更新。举例来说,下面清单 8-26 给出了对在一些文字中各个词出现次数进行计数的代码。这里使用了一个以词汇作为键的哈希图,并对值进行增加来追踪已见到那个词了多少次。而在首次见到某个词时,就会首先插入值 `0`
```rust
use std::collections::HashMap;
let text = "hello world wonderful world";
let mut map = HashMap::new();
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0);
*count += 1;
}
println! ("{:?}", map);
```
*清单 8-26使用存储词汇与计数的哈希图对词汇出现次数进行计数*
此代码将打印 `{"wonderful": 1, "world": 2, "hello": 1}`。这个 `split_withespace` 方法会对 `text` 中的那个值的、以空格分隔的子切片进行迭代。而那个 `or_insert` 方法,返回的时到指定键的值的可变引用(`&mut V`)。这里是将那个可变引用存储在变量 `count` 中的,因此为了对那个值进行赋值,这里就必须首先使用星号(`*`)对 `count` 解引用。在那个 `for` 循环结尾,该可变引用就超出了作用域,因此所有这些修改都是安全的,同时为借用规则所允许。
### 散列函数Hashing Functions
默认情况下,`HashMap` 使用了一个可提供抵抗哈希表有关的拒绝服务攻击的、名为 *`SipHash`* 的散列函数(参见:[https://en.wikipedia.org/wiki/SipHash](https://en.wikipedia.org/wiki/SipHash))。这并非可用的最快散列算法,不过这种为了更好安全性,而在性能上的舍弃,是值得的。在对自己代码进行推敲,而发现这个默认散列函数对于自己目的太慢时,是可以通过指定别的哈希器,切换到另一函数的。 *哈希器a hasher* 是一种实现了 `BuildHasher` 特质the `BuildHasher` trait的类型。在第 10 章中就会谈到特质及其如何实现。不必从头实现自己的哈希器;[crates.io](https://crates.io/) 就有由其他 Rust 使用者共享的、提供对许多常用散列算法进行实现的哈希器的库。
## 本章小结
矢量、字符串与哈希图,在程序中需要存储、访问与修改数据时,就会提供大量必要功能。下面就是一些现在应有能力解决的练习:
- 给定一个整数清单,请使用矢量,并返回这些数的中位数(即在这些数排序后,位于中间位置的值)与众数(最常出现的那个值;此时哈希图将有帮助);
- 将字符串(英语)转换为拉丁语式的结尾。每个词汇的第一个常量,会被迁移到该词汇的末尾,同时会加上 “ay”那么 “first” 就变成了 “irst-fay” 了。以元音开头的词汇,则会将 “hay” 添加到词汇末尾(比如 “apple” 就成了 “apple-hay”。请牢记有关 UTF-8 编码的那些细节!
- 运用哈希图与矢量,创建一个实现程序用户把员工名字添加到某公司里的某个部门的文本接口。比如,“添加 Sally 到工程部” 或 “添加 Amir 到销售部”。随后让用户获取到某个部门全体人员清单,或以部门字母排序的公司全体人员名单。
标准库 API 文档对矢量、字符串及哈希图有着的、对这些练习将有帮助的方法都有说明!
接下来就要进入到一些其中某些操作可能失败的程序,那么现在就是讨论错误处理的最佳时机。下一章就要来完成对错误处理的讨论了!

View File

@@ -7,630 +7,6 @@ Rust 将错误分组为两个主要类别: *可恢复recoverable* 与 *
大多数语言都没有区分这两种错误而以同样方式使用诸如异常的机制处理这两种错误。Rust 没有异常。相反Rust 有着用于可恢复错误的类型 `Result<T, E>`,以及在程序发生了不可恢复错误时,停止程序执行的 `panic!`the `panic!` macro。本章将首先涵盖对 `panic!` 的调用,并在随后讲解那些返回的 `Result<T, E>` 值。此外,这里会对在决定是否要尝试从错误中恢复,还是要停止程序的执行时的诸多考虑,进行探讨。
## 带 `panic!` 的不可恢复错误
End
**Unrecoverable Errors with `panic!`**
某些时候在代码中不好的事情发生了而对其无计可施。在这些情形下Rust 有着 `panic!` 宏。在 `panic!` 宏执行时程序就会打印一条失败消息释放unwind并清理掉栈并在随后退出。在侦测到某种类别的代码错误且在编写程序时刻尚不清楚怎样处理这个故障时就会触发一个程序中止invoke a panic
> **对程序终止进行响应的栈解除或栈终止Unwinding the Stack or Aborting in Response to a Panic**
>
> 默认情况下,在程序终止发生时,程序就开始 *解除栈unwinding*,这是指 Rust 对栈进行回退,并清理他所遇到的各个函数的数据。然而,这样的回退与清理,是很多的工作量。那么因此 Rust 就允许编程者选择立即 *终止aborting* 的替代方案,该替代方案会不加清理的结束程序。程序曾用过的内存,这时就需要由操作系统来清理。如过在项目中,需要将生成的二进制执行文件构造得尽可能小,你们就可以通过把 `panic= 'abort'`,添加到 `Cargo.toml` 文件中恰当的 `[profile]` 小节,而从程序中止的栈解除切换为立即终止。比如,若想要在发布模式中,于程序中止时立即终止,那么就要添加这个:
```toml
[profile.release]
panic = 'abort'
```
下面就来在一个简单程序中,尝试调用 `panic!` 宏:
文件名:`src/main.rs`
```rust
fn main() {
panic! ("崩溃并燃烧");
}
```
在运行该程序时,就会看到下面这样的东西:
```console
$ cargo run
Compiling error_handling_demo v0.1.0 (/home/lenny/rust-lang/error_handling_demo)
Finished dev [unoptimized + debuginfo] target(s) in 0.40s
Running `target/debug/error_handling_demo`
thread 'main' panicked at '崩溃并燃烧', src/main.rs:2:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```
这个到 `panic!` 的调用,引起了包含在最后两行中的错误消息。第一行给出了程序中止消息及源码里程序中止发生的位置:`src/main.rs:2:5` 表示是在这里的 `src/main.rs` 文件的第二行、第五个字符处。
在此情况下,所指出的那行,就是这里代码的一部分,而在前往到那行时,就会看到那个 `panic!` 宏调用。在别的情况下,`panic!` 调用可能会在所编写代码调用的代码中,那么由该错误消息报告出的文件名与行号,就会是 `panic!` 被调用所在之处的其他人的代码,而不会是最终引起那个 `panic!` 调用的自己编写的代码行。这里可以使用该 `panic!` 调用来自那些函数的回溯,来弄清楚此处代码的哪个部分导致了该问题。接下来就要详细讨论这种回溯。
### 运用 `panic!` 回溯
现在来看一下另一个示例,看看由于代码中的编码错误,而非由于在代码中直接调用 `panic!` 宏时,来自库的 `panic!` 调用到底会是什么样子。下面清单 9-1 有一些尝试访问某个矢量中超出了有效索引范围索引的代码。
文件名:`src/main.rs`
```rust
fn main() {
let v = vec! [1, 2, 3];
v[99];
}
```
*清单 9-1尝试访问某个超出了矢量末端的元素这会导致一个到 `panic!` 的调用*
这里正尝试访问这里矢量的第 100 个元素(由于索引开始于零处,故那是在索引 99 处),但这个矢量只有 3 个元素。在此情况下Rust 就会中止。使用 `[]` 被认为是要返回一个元素的,但在传递某个无效索引时,这里就没有 Rust 可返回的正确元素。
在 C 语言中,尝试读取超出某种数据结构,属于未定义的行为。那么就可以会得到与该数据结构中元素对应的、内存中那个位置处的任何东西,即便该内存不属于那个数据结构。这就叫做 *缓冲区重读取a buffer overread*,并能在攻击者可以这样的方式操作索引,来读取存储在该数据结构之后的、本不应允许他们读取的数据时,导致安全漏洞。
Rust 为保护程序免受这类漏洞的危害,就会在尝试位于某个不存在索引处的元素时,停止程序的执行而拒绝继续下去。来尝试运行一下上面的代码看看:
```console
$ cargo run lenny@vm-manjaro
Compiling error_handling_demo v0.1.0 (/home/lenny/rust-lang/error_handling_demo)
Finished dev [unoptimized + debuginfo] target(s) in 0.47s
Running `target/debug/error_handling_demo`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```
此错误指出了,在这里 `main.rs` 的第 4 行,其中尝试访问索引 `99` 处。接下来的注解行,讲到这里可将 `RUST_BACKTRACE` 环境变量,设置为获取究竟发生什么,才导致了这个错误。所谓 *回溯a backtrace*即为到此已调用的全部函数的清单。Rust 中的回溯,与其他语言中的回溯完成的事情一样:阅读回溯的冠军,就是要从顶部开始,一直要读到自己编写的文件为止。那便是该问题缘起之处。在那个点位之上的那些行,就是所编写代码曾调用过的代码;而所编写代码之下的那些行,则是调用所编写代码的代码。这些前前后后的行,就可能包含核心 Rust 代码、 标准库代码,或者正使用着的代码箱。下面就来通过将 `RUST_BACKTRACE` 环境变量,设置为除 `0` 之外的任何值,尝试获取到回溯。下面清单 9-2 展示了与将会看到的类似输出。
```console
$ RUST_BACKTRACE=1 cargo run lenny@vm-manjaro
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/error_handling_demo`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5
stack backtrace:
0: rust_begin_unwind
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/std/src/panicking.rs:584:5
1: core::panicking::panic_fmt
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:142:14
2: core::panicking::panic_bounds_check
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:84:5
3: <usize as core::slice::index::SliceIndex<[T]>>::index
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/slice/index.rs:242:10
4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/slice/index.rs:18:9
5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/alloc/src/vec/mod.rs:2591:9
6: error_handling_demo::main
at ./src/main.rs:4:5
7: core::ops::function::FnOnce::call_once
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/ops/function.rs:248:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
```
*清单 9-2在设置了 `RUST_BACKTRACE` 环境变量时,由到 `panic!` 调用生成的回溯被显示了出来*
那可是很多的输出了!具体看到的输出,可能根据操作系统与 Rust 版本而有所不同。为从此信息中获得回溯,就要开启那些调试符号。在不带 `--release` 标志使用 `cargo build``cargo run` 时,如同这里这样,这些调试符号默认就是开启的。
在上面清单 9-2 里的输出中,回溯所指向到这里项目中行的第 6 行,就是导致问题的行:即 `src/main.rs` 的第 4 行。在不想要这个程序中止时,就应在首个提到了自己所编写文件的行,所指向的那个位置,开始排查。在之前的清单 9-1 中,那里有意编写了会中止的代码,而修正程序中止的办法,就是不要请求某个超出那个矢量索引范围的元素。而在今后代码中止时,就需要搞清楚代码是在对什么值进行何种操作,而导致了中止,以及代码应该怎么做。
在本章的 [要 `panic!` 或不要 `panic!`](#要-panic-还是不要-panic) 小节,将回到 `panic!` 这个话题,并讨论在何时要用 `panic!`,何时不应使用 `panic!` 来处理不同错误情形。接下来,就会看看怎样使用 `Result`,从错误中恢复过来。
## 带有 `Result` 的可恢复错误
多数错误都没有严重到要求程序整个地停止运行。某些时候,在某个函数失败时,必定是由于某种可易于解释进而加以响应的原因。比如在尝试打开某个文件,而因为要打开的文件不存在,那个操作失败了时,那么可能希望创建该文件,而不是中止这个进程。
回顾第二章中的 [处理潜在带有 `Result` 类型的程序失败](Ch02_Programming_a_Guessing_Game.md#处理潜在的带有-result-的程序失效) 小节,其中的 `Result` 枚举被定义为有两个变种,`Ok``Err`,如下所示:
```rust
enum Result<T, E> {
Ok<T>,
Err<E>,
}
```
这里的 `T``E`都属于泛型参数generic type parameters在第 10 章就会更深入讨论泛型。此刻需要明白的是,这里的 `T` 表示在操作成功情形下,那个 `Ok` 变种里返回值的类型,而这里的 `E`,则表示在失效情形下,将返回的在 `Err` 变种里错误的类型。由于 `Result` 有着这些泛型参数,因此就可以在打算返回成功值与错误值有所区别的许多不同情形下,使用到这个 `Result` 及定义在其上的函数。
下面就来调用一个由于其会失败,而返回 `Result` 值的函数。在下面清单 9-3 中,是尝试打开一个文件。
文件名:`src/main.rs`
```rust
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
}
```
*清单 9-3打开某个文件*
怎样知道 `File::open` 会返回一个 `Result` 呢?这里就可以看看 [标准库 API 文档](https://doc.rust-lang.org/std/fs/struct.File.html#method.open),或者可以询问一下编译器!在赋予 `f` 一个明知 *不是* 该函数返回值类型的类型注解,并随后尝试编译该代码时,编译器就会告知,这两个类型不匹配。给出的错误消息,就会告诉 `f` 的类型是什么。来试试吧!这里已知 `File::open` 的返回类型不是 `u32`,因此就把那个 `let f` 语句修改为下面这样:
```rust
let f: u32 = File::open("hello.txt");
```
现在尝试编译,就会给到接下来的输出:
```console
$ cargo run lennyp@vm-manjaro
Compiling error_handling_demo v0.1.0 (/home/lennyp/rust-lang/error_handling_demo)
error[E0308]: mismatched types
--> src/main.rs:4:18
|
4 | let f: u32 = File::open("hello.txt");
| --- ^^^^^^^^^^^^^^^^^^^^^^^ expected `u32`, found enum `Result`
| |
| expected due to this
|
= note: expected type `u32`
found enum `Result<File, std::io::Error>`
For more information about this error, try `rustc --explain E0308`.
error: could not compile `error_handling_demo` due to previous error
```
这就是说,`File::open` 函数的返回类型,是个 `Result<T, E>`。泛型参数 `T`,在这里已被使用成功值的类型,`std::fs::File`即一个文件句柄a file handle填充。而用于错误值的类型 `E`,则为 `std::io::Error`
这样的返回值类型,表示到 `File::open` 的调用,可能会成功而返回一个能够自该处读取,或写入到该处的文件句柄。该函数调用同样可能失败:比如该文件可能不存在,或可能没有访问该文件的权限。那么这个 `File::open` 函数,就需要具备已知告知其是否成功或失败的方式,与此同时给到一个文件句柄,或者错误信息。这样的信息,正是这个 `Result` 枚举所要表达的。
此示例中,在 `File::open` 成功处,变量 `f` 中的值就会是包含了一个文件句柄的 一个 `Ok` 实例。而在其失败的情况下,`f` 中的那个值,就会是包含了有关所发生错误类别的更多信息的一个 `Err` 实例。
这里就需要对清单 9-3 中代码进行添加,从而根据 `File::open` 所返回值,而采取不同措施。下面清单 9-4 就给出了一种使用基本工具,即在第 6 章中曾讨论过的 `match` 表达式,对那个 `Result` 进行处理的方法。
文件名:`src/main.rs`
```rust
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(e) => panic! ("打开文件出现问题:{:?}", e),
};
}
```
*清单 9-4运用 `match` 表达式来处理可能返回的各个 `Result` 变种*
请注意,与 `Option` 枚举类似,这个 `Result` 枚举及其变种,是已由 Rust 前奏the prelude带入到作用域中了的因此这里无需在那两个 `match` 支臂中的 `Ok``Err` 变种之前,指明 `Result::`
在返回结果为 `Ok` 时,此代码就会返回从 `Ok` 变种抽出的那个内部的 `file` 值,且这里随后就把那个文件句柄值,指派给那个变量 `f`。在这个 `match` 之后,就可以将这个文件句柄,用于读取或写入了。
而那个 `match` 的另一支臂,则处理了从 `File::open` 得到一个 `Err` 值的情形。在此示例中,选择了调用 `panic!` 宏。在当前目录中没有名为 `hello.txt` 的文件,并运行此代码时,就会看到来自那个 `panic!` 宏的如下输出:
```console
$ cargo run lennyp@vm-manjaro
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/error_handling_demo`
thread 'main' panicked at '打开文件出现问题Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:8:19
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```
与往常一样,此输出告知了到底什么出错了。
### 匹配各异的错误
上面清单 9-4 中的代码,不论 `File::open` 因何而失败,都会 `panic!`。然而,这里是要因应不同失败原因,而采取不同措施:在 `File::open` 因为那个文件不存在而失败时,就要创建该文件并返回到那个新建文件的句柄。在那个 `File::open` 因别的其他原因失败 -- 比如没有打开该文件的权限时,这里仍要该代码以清单 9-4 中所做的同样方式,`panic!` 掉。为此,这里就要添加一个内部的 `match` 表达式,如下清单 9-5 中所示。
文件名:`src/main.rs`
```rust
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(e) => match e.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(error) => panic! ("创建该文件时出现问题:{:?}", error),
},
other_error => panic! ("打开文件出现问题:{:?}", other_error),
},
};
}
```
*清单 9-5以不同方式处理不同类别的错误*
`File::open` 所返回的位于 `Err` 变种内部的值的类型为 `io::Error`,他是一个由标准库提供的结构体。该结构体有个可供调用以获取到 `io::ErrorKind` 值的方法 `kind`。而枚举 `io::ErrorKind` 亦是由标准库提供,并有着表示那些可能自某个 `io` 操作而引起的,不同类别错误的一些变种。这里打算使用的变种为 `ErrorKind::NotFound`,表示了正尝试打开的文件尚不存在。因此这里既对 `f` 进行了匹配,而同时还有了在 `e.kind()` 上的一个内层匹配。
这里打算检查的那个内层匹配中的条件,则是由 `e.king()` 所返回的那个值,是否为 `ErrorKind` 枚举的 `NotFound` 变种。在 `e.kind()` 返回的值为 `ErrorKind``NotFound` 变种时,这里就尝试以 `File::create` 来创建该文件。然而由于 `Fiel::create` 仍会失败,因此这里就需要在那个内层 `match` 表达式中的第二个支臂。在该文件无法被创建出来时,就会打印出一条不同的错误消息。外层那个 `match` 表达式的第二支臂保持原样,因此该程序会在除了文件未找到错误之外的其他任何错误时,都会中止运行。
> **这种结合`Result<T, E>` 运用 `match` 表达式的替代方案**
>
> 那可是有好多的 `match` `match` 表达式是很有用,但同样也是很原始的。在第 13 章就会了解到闭包closures这种与定义在 `Result<T, E>` 上的众多方法一起使用的特性。在对代码中的 `Result<T, E>` 值进行处理时,比起使用 `match` 表达式,这样的闭包方式可以简练得多。
> 比如,下面就是编写与清单 9-5 中同样逻辑,不过却使用了闭包特性与 `unwrap_or_else` 方法的另一种方式。
```rust
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt").unwrap_or_else(|e| {
if e.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error| {
panic! ("创建文件时发生问题:{:?}", error);
})
} else {
panic! ("打开文件时出现问题:{:?}", e);
}
});
println! ("{:?}", f);
}
```
> 尽管此代码与清单 9-5 有着同样行为,但他并未包含任何的 `match` 表达式,且读起来更清楚。请在读完了第 13 章,并看看标准库文档中的这个 `unwrap_or_else` 方法后,再回到这个示例。在对错误进行处理时,许多别的这些方法,都可以清理掉大量嵌套的 `match` 表达式。
### 因错误而中止的快捷方式:`unwrap` 与 `expect`
**Shortcuts for Panic on Error: `unwrap` and `expect`**
运用 `match` 运作足够良好,不过那样可能有点冗长,且不总是良好地传达了意图。这个 `Result<T, E>` 类型,其上本来就定义了许多用于完成各种各样的、更为具体任务的辅助方法。其中的 `unwrap` 方法,就是一个实现了刚好与前面清单 9-4 中所编写的 `match` 表达式类似的快捷方法。在 `Result` 的值为 `Ok` 变种时,`unwrap` 就会返回那个 `Ok` 内部的值。而在该 `Result``Err` 变种时,`unwrap` 则会代为调用 `panic!` 宏。下面就是运作中的一个 `unwrap` 示例:
文件名:`src/main.rs`
```rust
use std::fs::File;
fn main() {
let f = File::open("hello.txt").unwrap();
}
```
在没有 `hello.txt` 文件下运行此程序时,就会看到一条来自由这个 `unwrap` 方法做出的 `panic!` 宏调用的错误消息:
```console
$ cargo run lennyp@vm-manjaro
Finished dev [unoptimized + debuginfo] target(s) in 0.03s
Running `target/debug/error_handling_demo`
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:4:37
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```
同样,`Result<T, E>` 上的 `expect` 方法,则可实现对这条 `panic!` 错误消息的选取。使用 `expect` 而非 `unwrap` 并提供良好的错误消息,就能够传达到自己的意图,进而令到追踪程序中止缘由更为容易。`expect` 方法的语法如下所示:
文件名:`src/main.rs`
```rust
use std::fs::File;
fn main() {
let f = File::open("hello.txt").expect("打开 hello.txt 失败");
}
```
这里以与 `unwrap` 同样方式,使用了 `expect`:用于返回文件句柄,或者对 `panic!` 宏进行调用。而在 `expect` 调用 `panic!` 时用到的错误消息,就将是这里传递给 `expect` 的那个参数,而不再是 `unwrap` 所用到的那个默认 `panic!` 消息了。下面就是该错误消息看起来的样子:
```console
$ cargo run lennyp@vm-manjaro
Compiling error_handling_demo v0.1.0 (/home/lennyp/rust-lang/error_handling_demo)
Finished dev [unoptimized + debuginfo] target(s) in 1.23s
Running `target/debug/error_handling_demo`
thread 'main' panicked at '打开 hello.txt 失败: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:4:37
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```
由于此错误消息是以这里所指定的,`打开 hello.txt 失败` 开始,因此就会更易于搞清楚,此错误消息来自代码中的何处。而若在多处使用 `unwrap`,那么在要精准找出到底是那个 `unwrap` 导致了程序中止时,就会因为所有这些调用了 `panic!``unwrap`,都打印出同样消息,而要耗费更多时间。
### 传递错误Propagating Errors
在某函数实现调用了可能失败的某些东西时,与其在该函数自身里头对错误进行处理,还可以将该错误返回给调用该函数的代码,这样调用该函数的代码就可以自己决定要做些什么。这就叫做 *传递propagating* 错误,而将更多的控制,给到调用该函数的代码,相比于当前实现的函数代码,调用代码中可能会有更多决定该错误应如何被处理的信息或逻辑。
比如,下面清单 9-6 就给出了一个从某个文件读取用户名的函数。在那个文件不存在或无法读取时,这个函数就会将这些错误返回给调用该函数的代码。
```rust
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let username_file_result = File::open("hello.txt");
let mut username_file = match username_file_result {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut username = String::new();
match username_file.read_to_string(&mut username) {
Ok(_) => Ok(username),
Err(e) => Err(e),
}
}
```
*清单 9-6使用 `match` 将错误返回给调用代码的一个函数*
虽然可以简单得多的方式,来重写该函数,不过为了对错误处理进行探索,因此这里就要通过亲自动手完成其大部分代码开头;在结束时,就会给出那更简短的方式。首先来看看该函数的返回值类型:`Result<String, io::Error>`。这表示该函数要返回一个类型 `Result<T, E>` 的值,其中的泛型参数 `T` 已被具体类型 `String` 填充,而那个泛型 `E` 则已被具体类型 `io::Error` 填充。若此函数不带任何问题的成功运行,那么调用该函数的代码,就会收到一个保存着一个 `String``Ok` 值 -- 即该函数从那个文件中读取到的用户名。而在该函数出现任何问题时,那么调用代码就会收到一个,保存着包含了有关所出现问题更多信息的 `io::Error` 示例的 `Err` 值。这里之所以选择 `io::Error` 作为此函数的返回值,是因为在该函数的函数体中所调用的两个都可能失败的操作:`File::open``read_to_string`,他们所返回的错误值都是这个 `io::Error` 类型。
该函数的函数体,是以调用 `File::open` 函数开始的。随后这里就以与清单 9-4 中类似方式,使用了一个 `match` 处理 `File::open` 返回的 `Result`。在 `File::open` 成功时,那么在模式变量 `file` 中的文件句柄,就成为那个可变变量 `f` 中的值,且函数会继续执行。而在 `Err` 情形下,这里使用了 `return` 关键字,早早地就从这个函数 `return` 了出去,同时将来自 `File::open` 的那个错位值,此时是在模式变量 `e` 中,作为该函数的错误值,传回给调用该函数的代码。
因此在 `username_file` 有着一个文件句柄时,该函数随后就会创建一个在变量 `username` 中的新 `String`,并调用 `username_file` 中文件句柄上的 `read_to_string` 方法,来将该文件中的内容,读取到 `username` 中。因为即使 `File::open` 运行成功,这个 `read_to_string` 仍可能失败,因此他同样会返回一个 `Result`。那么这里就需要另一个 `match`,来处理这个 `Result`:在 `read_to_string` 成功时,那么接下来这个函数就成功执行了,进而就从这个文件,返回到此时位于封装在一个 `Ok` 中的 `username` 中的用户名来。而在 `read_to_string` 失败时,这里就会以与之前在那个处理 `File::open` 返回值的 `match` 中返回错误值的同样方式,返回现在这个 `read_to_string` 的错误值。不过,由于这是该函数中的最后一个表达式,因此这里无需显示地写下 `return`
调用此代码的代码,随后就会对收到的包含了用户名 `Ok` 值,或者包含了一个 `io::Error` 类型的 `Err` 值进行处理。至于要对这些值做何处理,则取决于调用代码了。在调用代码收到 `Err` 值时,他就可以采取好比调用 `panic!` 并崩溃掉该程序,可以使用某个默认用户名,或者从相比该文件的其他地方,查找该用户名等操作。这里没有关于那个调用代码确切地尝试要做什么的足够信息,因此这里就把全部的成功或错误信息,向上传递给调用代码,让调用代码进行适当处理。
由于在 Rust 中这样的传递错误模式是如此普遍,以致于 Rust 提供了问好操作符the question mark operator, `?`),来令到错误传递更加容易。
### 传递错误的快捷方式:`?` 操作符
下面清单 9-7 给出了与清单 9-6 有着同样功能的一个 `read_username_from_file` 实现,只是此实现使用了 `?` 操作符。
文件名:`src/main.rs`
```rust
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username_file = File::open("hello.txt")?;
let mut username = String::new();
username_file.read_to_string(&mut username)?;
Ok(username)
}
```
*清单 9-7一个使用 `?` 操作符将错误返回给调用代码的函数*
那个放在某个 `Result` 值后面的 `?`,被定义为几乎与之前所定义的那些,用于处理清单 9-5 中那些 `Result` 值的 `match` 表达式,以同样方式运作。在 `Result` 的值为 `Ok` 时,那么那个 `Ok` 内部的值,就会从该表达式得以返回,且程序将继续运行。而在该 `Result` 值为一个 `Err` 时,则会如同之前曾用到的 `return` 关键字一样,将自这整个函数,返回这个 `Err` 值,进而这个错误值,就被传递给了调用代码。
清单 9-6 中的 `match` 表达式完成的事情,与这个 `?` 操作符完成的事情有个不同点:调用了这个 `?` 操作符的错误值,会经过定义在标准库中 `From` 特质the `From` trait in the standard library中定义的 `from` 函数,而该函数被用于将一种类型的值,转换到另一种类型中。当 `?` 操作符调用 `from` 函数时,被接收到的错误类型,就被转换为了定义在当前函数返回值类型中的类型了(即 `Result<String, io::Error>`)。在某个函数可能失败,即便该函数的一些部分而不是整个函数,由于许多不同原因而失败,而返回一种表示这些全部失败方式的一种错误类型时,这个不同之处就会有用。
比如,这里本可将清单 9-7 中的 `read_username_from_file` 函数,修改为返回一个自己定义的名为 `OurError` 的定制错误类型。而在同时给 `OurError` 定义了 `impl From<io::Error>`,以从 `io::Error` 构造出一个 `OurError` 的实例时,那么随后无需添加任何代码到这个函数,`read_username_from_file` 函数中的这些 `?` 操作符,就会调用 `from` 并对那些错误类型进行转换。
在清单 9-7 的语境下,位于 `File::open` 调用末尾的那个 `?`,将返回一个 `Ok` 内部的值给变量 `username_file`。而在有错误发生时,这个 `?` 操作符,就会早早地从整个函数退出,并把任何的 `Err` 值给到调用代码。对于那个 `read_to_string` 调用末尾处的 `?`,适用这同样的情况。
`?` 操作符消除了很多样板代码a lot of boilerplate并令到此函数的实现更为简单。通过将这些方法调用在整个 `?` 即刻链接起来,甚至可以进一步缩短此代码,如下清单 9-8 中所示。
文件名:`src/main.rs`
```rust
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username = String::new();
File::open("hello.txt")?.read_to_string(&mut username)?;
Ok(username)
}
```
*清单 9-8在 `?` 操作符后将方法调用链接起来*
这里已将那个 `username` 中的新 `String` 的创建,挪到了该函数的开头;整个函数就整个部分未作改动。这里没有了变量 `username_file` 的创建,而是已将到 `read_to_string` 的函数调用,直接链接到了 `File::open("hello.txt")?` 的结果上。在 `read_to_string` 调用的末尾仍有一个 `?`,同时在这两个 `File::open``read_to_string` 调用都成功,而不返回错误时,这里就会返回一个包含了 `username``Ok` 值。功能仍旧与清单 9-6 和清单 9-7 中是一样的;这只是一种不同的、更为符合人体工程学的编写方式。
下面清单 9-9 给出了使用 `fs::read_to_string` 的一种甚至更加简短的方式。
文件名:`src/main.rs`
```rust
use std::fs;
use std::io;
fn read_username_from_file() -> Result<String, io::Error> {
fs::read_to_string("hello.txt")
}
```
*清单 9-9使用 `fs::read_to_string` 而非打开在读取那个文件*
将某个文件读取到字符串中,是个相当常见的操作,因此标准库提供了便捷的打开文件、创建一个新 `String`、读取文件内容、将内容放入到那个 `String`,并将其返回的 `fs::read_to_string` 函数。当然,`fs::read_to_string` 的使用,并不能赋予到这里对全部错误处理加以解释的机会,因此这里才要走过前面那些常常的过程。
### 哪些地方可以使用 `?` 操作符
`?` 操作符仅可用于那些返回值类型,与这个 `?` 被用于的那个值类型兼容的函数中。这是由于 `?` 操作符被定义为与在清单 9-6 中,所定义的那个 `match` 表达式类似方式,执行一个该函数早期阶段的退出。在清单 9-6 中,那个 `match` 使用的是一个 `Result` 值,同时那个先期返回支臂返回的是一个 `Err(e)` 值。那么那个函数的返回值类型,就必须是个 `Result`,这样才与这个 `return` 兼容。
在下面清单 9-10 中,就要看看一个有着与其上使用了 `?` 的类型值不兼容返回值的 `main` 函数中,使用 `?` 操作符会收到的错误:
文件名:`src/mian.rs`
```rust
use std::fs::File;
fn main() {
let greating_file = File::open("hello.txt");
}
```
*清单 9-10尝试在返回 `()` 的 `main` 函数中使用 `?` 就不会编译*
此代码是要打开一个文件,这就可能失败。那个 `?` 操作符接续了由 `File::open` 所返回的 `Return` 值,然而这个 `main` 函数的返回值类型为 `()`,而非 `Result`。那么在编译此代码时,就会得到以下的错误消息:
```console
$ cargo run lennyp@vm-manjaro
Compiling error_handling_demo v0.1.0 (/home/lennyp/rust-lang/error_handling_demo)
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
--> src/main.rs:4:48
|
3 | / fn main() {
4 | | let greating_file = File::open("hello.txt")?;
| | ^ cannot use the `?` operator in a function that returns `()`
5 | | }
| |_- this function should return `Result` or `Option` to accept `?`
|
= help: the trait `FromResidual<Result<Infallible, std::io::Error>>` is not implemented for `()`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `error_handling_demo` due to previous error
```
此错误指出了只允许在返回 `Result``Option` 或别的实现了 `FromResidual` 的类型的函数中,使用 `?` 操作符。
而要修正这个错误,则有两个选择。一个选择是在没有修改函数返回值类型的限制时,那么就将其修改为与在其上使用 `?` 操作符的值类型兼容。另一技巧,则是使用一个 `match` 表达式,或某个 `Result<T, E>` 的那些方法,来以某种恰当方式对这个 `Result<T, E>` 进行处理了。
这个错误消息还提到,`?` 还可与 `Option<T>` 类型的值一同使用。与在 `Result` 上使用 `?` 一样,可在返回一个 `Option` 的函数中的 `Option` 上使用 `?`。在某个 `Option<T>` 上调用 `?` 操作符的行为,与在 `Result<T, E>` 上其被调用时的行为类似:在该值为 `None` 时,`None` 就会在那个地方及早地从该函数被返回。而在该值为 `Some` 时,那么这个 `Some` 内部的值,就是该表达式的结果值,同时函数会继续执行。下面清单 9-11 有着一个在给定文本中找到第一行最后一个字符的函数示例:
```rust
fn last_char_of_first_line(text: &str) -> Option<char> {
text.lines().next()?.chars().last()
}
```
*清单 9-11在某个 `Option<T>` 的值上使用 `?` 操作符*
由于可能那里有个字符,不过同样坑能那里没有字符,因此此函数返回的是 `Option<char>`。这个代码取那个 `text` 字符串切片参数,并在其上调用了 `lines` 方法,该方法返回的是对该字符串中那些文本行的一个迭代器。由于此函数是要对首个文本行进行检查,因此他调用了那个迭代器上的 `next`,来从迭代器上获取头一个值。在 `text` 为空字符串时,那么这个到 `next` 的调用,就会返回 `None`,这也就是这里使用 `?` 来停止这个 `last_char_of_first_line` 函数,并自其返回 `None` 的情形。而在 `text` 不为空字符串时,`next` 就会返回一个包含了在 `text` 中第一行文本的字符串切片的 `Some` 值。
此时 `?` 操作符会提取这个字符串切片,进而就可以在那个字符串切片上调用 `chars`,来获取到他那些字符的一个迭代器。这里关心的是第一行文本中的最后一个字符,因此就要调用 `last` 来返回迭代器中的最后一个条目。因为首个文本行为空字符串是可能的,比如在 `text` 以空行开头却在其他行上有一些字符,如同在 `"\nhi"` 中一样,因此 `last` 得到一个就是个 `Option` 值。不过在首行上有最后一个字符时,这个字符就会在 `Some` 变种里被返回。中间的 `?` 操作符,给到了一种表达此逻辑的简洁方式,运行在一个行里来实现该函数。若无法在 `Option` 上运用这个 `?` 操作符,那么就必须使用更多方法调用,或 `match` 表达式来实现此逻辑。
注意在返回 `Result` 函数中的 `Result` 上,可以使用 `?` 操作符,而在返回 `Option` 函数中的 `Option` 上,可使用 `?` 操作符,但不能混用及进行匹配。`?` 操作符不会自动将 `Result` 转换为 `Option`,或反过来将 `Option` 转换为 `Result`;在这些情况下,是可以在 `Result` 上使用诸如 `ok`,或在 `Option` 上使用 `ok_or` 这样的方法,来显示地完成转换。
到目前为止,这里使用过的所有 `main` 函数,返回的都是 `()`。由于 `main` 函数是可执行程序的进入与退出点,因此他是特殊的,而关于其返回值类型可以是什么,为了程序如预期那样执行,是有一些限制的。
幸运的是,`main` 函数同样可以返回 `Result<(), E>`。下面清单 9-12 有着来自 9-10 的代码,不过这里将 `main` 函数的返回值类型,改成了 `Result<(), Box<dyn Error>>`,并在最后添加了一个返回值 `Ok(())`。现在该代码就会编译了:
```rust
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let greeting_file = File::open("hello.txt")?;
Ok(())
}
```
*清单 9-12将 `main` 修改为返回 `Result<(), E>`,就实现了在 `Result` 值上 `?` 操作符的使用*
这里的 `Box<dyn Error>` 类型,是个 *特质对象trait object*,在第 17 章中的 [“使用允许不同类型值的特质对象”](Ch17_Object_Oriented_Programming_Features_of_Rust.md#使用允许不同类型值的特质对象) 小节,就会讲到这个特性。而现在,可将 `Box<dyn Error>` 理解为表示 “任何类别的错误”。由于 `?` 操作符允许将任何 `Err` 值及早返回,因此将 `?` 用在有着错误类型 `Box<dyn Error>``main` 函数中, 某个 `Result` 值上是允许的。即使这个 `main` 函数的函数体,将只会返回类型 `std::io::Error` 的那些错误,而经由指定 `Box<dyn Error>`,即使将返回其他错误的代码添加到 `main` 的函数体,该函数签名 `fn main() -> Result<(), Box<dyn Error>>` 仍将无误。
`main` 函数返回了一个 `Result<(), E>` 时,那么若 `main` 返回的是 `Ok(())`,则该可执行程序就会以值 `0` 退出,并在 `main` 返回 `Err` 值时以非零值退出。C 语言编写的可执行程序,在退出时返回的是些整数:成功退出的程序返回整数 `0`,而出错的程序返回某些非 `0` 的整数。Rust 从可执行程序返回的也是整数,从而与此约定兼容。
`main` 函数可能返回任何实现了 [`std::process::Termination` 特质the `std::process::Termination`](https://doc.rust-lang.org/std/process/trait.Termination.html) 的任何类型,该特质包含了返回某个 `ExitCode``report` 函数。请参考标准库文档,了解更多有关实现自己类型 `Termination` 的信息。
现在既然已经讨论了调用 `panic!` 或返回 `Result` 的细节,那么就要回到怎样判断,在何种情形下,使用哪种方式属于恰当的话题了。
## 要 `panic!` 还是不要 `panic!`
那么该怎样确定,什么时候应该调用 `panic!`,以及什么时候应该返回 `Result` 呢?在代码中止时,就没有办法恢复了。当然可以在任何错误情形下调用 `panic!`,而不管存不存在可能的恢复方式,不过这个时候就是代码编写者本人,代替代码在做出这种情形为不可恢复的确定了。而在选择了返回某个 `Result` 值是就赋予了调用代码the calling code各种选项。调用代码就可以根据其自身情况而选择尝试恢复或者他可以决定在此情形下的某个 `Err` 是不可恢复的,进而他就可以调用 `panic!` 而将可恢复错误,转变为不可恢复错误。这样看来,在对某个可能失败的函数进行定义时,返回一个 `Result` 就是良好的默认选择。
而在示例程序、原型代码及测试等中,那么比起返回 `Result`编写程序中止代码就要更合适。接下来就要探讨一下为何这样讲随后就要讨论一些编译器无法搞清楚但作为代码编写者的人类却明白程序失败不可能发生的情形。本章将以一些有关在库代码中如何确定要不要中止程序的守则结束in situations such as examples, prototype code, and tests, it's more approciate to write code that panics instead of returning a `Result`. Let's explore why, then discuss situations in which the compiler can't tell that failure is impossible, but you as a human can. The chapter will conclude with some general guidelines on how to decide whether to panic in library code
### 示例程序、原型代码与测试
在编写用于演示某些概念的示例程序时,若同时包含一些健壮的错误处理代码,就会令到示例程序不那么明晰。在示例程序中,到某个诸如 `unwrap` 这样的可能会中止程序运行方法的调用,确信就表明那是一个是要这个应用程序对错误进行处理的占位符,而根据接下来代码所做的事情,这样的调用会有所不同。
与此类似,`unwrap``expect` 方法在构造原型程序,尚未准备好确定如何处理错误时,是十分方便的。这些方法在代码中留下了清楚的一些记号,这些记号在已做好准备让程序更为健壮时会用到。
而当方法调用在测试中失败时,即使那个方法并非正在测试的某些功能,也会希望整个测试失败。由于 `panic!` 正是将测试标记为失败的方式,那么调用 `unwrap``expect`,则正是要用到的了。
### 相比于编译器,代码编写者掌握了更多信息的情形
在有着确保了 `Result` 将有着 `Ok` 值的其他某些逻辑,但编译器对此逻辑却一无所知时,调用 `unwrap``expect` 也是恰当的。这时将仍然有个需要处理的 `Result` 值:对于不论所调用的什么操作,即使在当前特定情形下,逻辑上失败绝无可能,但所调用的操作原本总体上仍是有失败可能。在经由亲自检查代码,而能确保绝不会有 `Err` 变种时,调用 `unwrap` 就是完美可接受的,且将自己设想的绝不会有 `Err` 变种的原因,在 `expect` 文本中撰写出来,这样做甚至更佳。下面就是一个示例:
```rust
fn main () {
use std::net::IpAddr;
let home: IpAddr = "127.0.0.1"
.parse()
.expect("硬编码的 IP 地址应是有效的");
}
```
这里是在通过解析硬编码字符串,创建一个 `IpAddr`。可以看到 `127.0.0.1` 是个有效的 IP 地址,因此这里使用 `expect` 是可接受的。然而,有着一个硬编码的、有效的字符串,并未改变 `parse` 方法返回值类型:这里仍将得到一个 `Result` 类型,同时由于编译器不是足够聪明到发现这个字符串总是个有效的 IP 地址,那么编译器仍将要求,以 `Err` 变种是一种可能性那样,对这个 `Result` 进行处理。在这个 IP 地址为来自用户输入,而非这里的硬编码到程序中,进而 *确实* 有着失败可能时,无疑就要打算对这个 `Result` 进行更为健壮的处理了。这种提及这个 IP 地址为硬编码的假设,将提醒到在将来,需要从其他来源获取这个 IP 地址时,就要把 `expect` 修改为更好的错误处理代码。
### 错误处理守则
在代码可能以糟糕状态结束运行时,那么让代码中止运行就是明智的。在这种情形下,所谓 *糟糕状态a bad state* 就是在某种假设、保证、合约,或恒值已被破坏,譬如在无效值、矛盾值,或缺失值被传递到所编写代码 -- 加上以下的一项或多项:
- 糟糕状态是某些不期望的东西,他们与偶发的东西相反,比如用户输入的错误格式数据;
- 在此处之后的代码,需要依赖于不处在这种糟糕状态,而不是在接下来的每一步都检查这个问题;
- 没有以自己所使用的类型,来编码该信息的好办法。在第 17 章的 [“将状态与行为编码为类型”](Ch17_Object_Oriented_Programming_Features_of_Rust.md#将状态与行为当作类型编码) 小节,就会贯穿一个这里所意指的示例。
在有人调用到咱们的代码,并传入了无意义的值时,在可以的情况下,最好返回一个错误,这样库用户就可以确定在那样的情况下,他们打算做什么。然而在继续执行下去会不安全或有危害的情形中,那么最佳选择就会时调用 `panic!`,并警醒使用到咱们库的人他们代码中的错误,这样在他们开发过程中就可以修好那个代码错误。与此类似,在调用不在掌控中的外部代码,且该外部代码返回了无法修复的无效状态时,那么 `panic!` 通常就是恰当选择。
不过在失败为预期的时,那么相比于构造一个 `panic!` 调用,返回一个 `Result` 则更为恰当。这类示例包括给到解析器错误格式数据,或某个返回了表示已达到访问数限制的 HTTP 请求等。在这些情况下,返回一个 `Result` 就表示失败是一种调用代码必须确定如何处理的预期可能。
在所编写代码被使用无效值调用,而执行了某种可能将用户置于危险境地的操作时,那么代码就应首先对这些值进行检查,并在这些值无效时中止运行。这主要是处于安全原因:尝试运行于无效数据,就会将代码暴露于漏洞。这就是在尝试超出边界的内存访问时,标准库会调用 `panic!` 的主要原因:尝试访问不属于当前数据结构的内存,是个常见的安全问题。函数通常有着 *合约contracts*:只在输入满足特定要求时,他们的行为才有保证。那么由于合约破坏总是表明调用者侧的代码错误,且这种错误并非是要调用代码必须显式处理的那种错误,因此在合约被破坏时的中止运行就说得通了。实际上,调用代码是没有恢复的合理方法的;调用的 *代码编写者* 需要修复该代码。应在函数的 API 文档中,解释函数的合约,尤其是在合约破坏会导致中止运行时。
但是,在全部的函数中,进行大量错误检查,则会显得冗长而烦人。幸运的是,可使用 Rust 的类型系统(并因此由编译器完成类型检查),来完成许多的检查。在函数有着作为参数的特定类型时,就可以在知悉编译器已经确保有着有效值的情况下,着手处理代码的业务逻辑。比如,在有着一个不同于 `Option` 的类型时,程序就期望有 *某个东西something* 而非 *什么也没有nothing*。代码这时就不必处理 `Some``None` 变种的两种情形:无疑将只有一种有着某个值的情形。尝试将无值传递给该函数的代码,甚至都不会编译,那么该函数就不必在运行时对那样的情况进行检查了。另一个示例则是使用某个诸如 `u32` 无符号整数,这就确保了参数绝不会是个负数。
### 创建用于验证的定制类型
接下来将这个运用 Rust 的类型系统,来确保有着有效值的概念,进行进一步拓展,而看看创建一个用于验证的定制类型。回顾在第二章中的猜数游戏,其中的代码要求用户猜出一个 `1``100` 之间的数字。在将用户猜的数字与那里的秘密数字比对之前,是绝无对用户猜数是否处于 `1``100` 之间,进行过验证的;那里只验证过猜数为正数。在这个示例中,后果并不是非常可怕:这里的输出 “太大了” 或 “太小了” 仍将正确。但引导用户朝向有效的猜数,并在用户猜出不在该范围的数,与用户敲入了比如一些字母时,而有不同的表现,将是一项有用的功能增强。
完成此功能增强的一种方式,将是将猜数解析为一个 `i32` 而非仅仅为一个 `u32`,从而允许潜在的负数,并在随后键入一个该数位于范围中的检查,像下面这样:
```rust
loop {
// --跳过--
let guess: i32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => { println! ("请输入一个数字!"); continue },
};
if guess < 1 || guess > 100 {
println! ("秘密数字将在 1 和 100 之间");
continue;
}
match guess.cmp(&secret_number) {
// --跳过--
}
```
其中的 `if` 表达式,对这里的值是否超出范围进行了检查,告诉用户这个问题,并调用 `continue` 来开始下一次循环迭代而请求另一个猜数。在这个 `if` 表达式之后,就可继续进行 `guess` 与秘密数字之间的比较,获悉 `guess` 是在 `1``100` 之间。
然而这并非一种理想的方案:若程序只运行在 `1``100` 之间的值这一点至关重要,且程序有着许多有此要求的函数,而在每个函数中都进行这样的一个检查,就会显得冗长乏味(并可能影响性能)。
相反,这里可以构造一种新类型,并将那些验证放入某个函数,从而创建出该类型的一个示例,而非在各个地方重复这些验证。那样的话,这些函数就可以在他们的签名中,安全地使用这种新类型,并信心十足地使用他们接收到的那些值了。下面清单 9-13 给出了一种定义 `Guess` 类型的方式,在 `new` 函数接收到一个 `1``100` 之间的值时,这种方式下将只创建一个 `Guess` 的实例。
```rust
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic! ("Guess 类型值必须在 1 与 100 之间,收到的是 {}", value);
}
Guess { value }
}
pub fn value(&self) -> i32 {
self.value
}
}
```
*清单 9-13只会在值处于 `1` 与 `100` 之间,才继续执行的一个 `Guess` 类型*
首先,这里定义了一个名为 `Guess`,带有一个叫做 `value`、保存了一个 `i32` 值字段的结构体。这就是要存储数字的地方。
随后这里在 `Guess` 上实现了一个名为 `new` 的关联函数,其创建出一个 `Guess` 类型的实例。这个 `new` 函数被定义为有着一个名为 `value`、类型为 `i32` 的参数,以及要返回一个 `Guess` 类型值。`new` 函数体中的代码,对 `value` 进行了测试,从而确保 `value` 是在 `1``100` 之间。若 `value` 未通过此测试,那么就做出一个 `panic!` 调用,由于创建一个超出此范围的 `Guess` 会破坏 `Guess::new` 所依赖的合约,因此这就会警醒到编写调用代码的程序员,他们有个需要修复的代码错误。`Guess::new` 可能中止运行的条件,应在其公开的 API 文档中,进行说明。在第 14 章就会涉及到在所创建的文档中,表示 `panic!` 可能性的一些约定。在 `value` 通过该测试时,这里就会创建一个将 `value` 字段设置为那个 `value` 参数的新 `Guess` 类型值,并返回这个 `Guess` 类型值。
接下来,这里实现了一个名为 `value`、借用了 `self`,不带任何其他参数,并返回一个 `i32` 的方法。由于这类方法的目的,是要从一些字段获取数据并加以返回,因此有时就被叫做 *取值方法getter*。因为 `Guess` 结构体的这个 `value` 字段是私有的,那么这个公开方法就是必要的。这个 `value` 字段作为私有至关重要,这样使用这个 `Guess` 结构体的代码就不被允许直接设置 `value`:该模组外部的代码,*必须* 使用 `Guess::new` 函数,来创建 `Guess` 的实例,这样就确保了 `Guess` 不会有未经 `Guess::new` 函数中条件检查的 `value`
现在某个有着一个 `1``100` 之间参数,或只返回 `1``100` 之间数字的函数,就可以在其函数签名中,声明他所取参数或其返回值为 `Guess` 类型而非 `i32` 类型,而不再需要在其函数体中完成任何额外检查了。
## 小结
Rust 的那些错误处理特性,被设计用于帮助编写更为健壮的代码。`panic!` 这个宏,发出了程序处于其无法处理状态的信号,并让咱们告知进程停下来,而不是尝试以无效或不正确的一些值继续运行。而 `Result` 这个枚举则使用了 Rust 的类型系统来表示以代码可以从中恢复过来的某种方式的一些操作失败the `Result` enum uses Rust's type system to indicate that operations might fail in a way that your code could recover from。还可使用 `Result` 来告诉调用了咱们代码的代码,需要处理潜在的成功与失败情形。在一些适当情形下,运用 `panic!``Result` 就会令到咱们的代码在各种不可避免的问题面前,更加可靠。
既然这里已经见识到标准库在 `Option``Result` 枚举上,运用到泛型的一些有用方式,那么接下来就要谈及泛型的原理,以及怎样在咱们的代码中运用泛型。

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -14,872 +14,6 @@
相比咱们在本章会讲到的功能Cargo 甚至能完成更多,因此对于 Cargo 全部特性的完整阐释,请参阅 [他的文档](https://doc.rust-lang.org/cargo/)。
## 使用不同发布配置文件,对构建进行定制
End
**Customizing Builds with Release Profiles**
在 Rust 中,所谓 *发布配置文件release profiles*,是带有实现程序员对编译代码有着更多掌控的,一些预定义及可定制的配置文件。相对其他配置文件,每个配置文件都是被独立配置的。
Cargo 有两个主要发布配置文件:运行 `cargo build` 时 Cargo 用到的 `dev` 配置文件,与运行 `cargo build --release` 时 Cargo 用到的 `release` 配置文件。`dev` 配置文件被定义为有着用于开发的一些良好默认配置,而 `release` 配置文件有着用于发布构建的良好默认配置。
从咱们构建的输出中,这些配置文件名字或许不陌生:
```console
$ cargo build
Finished dev [unoptimized + debuginfo] target(s) in 0.0s
$ cargo build --release
Finished release [optimized] target(s) in 0.0s
```
其中 `dev``release`,即由编译器用到的不同配置文件。
Cargo 有着在咱们在项目的 `Cargo.toml` 文件中,未曾显式添加任何 `[profile.*]` 小节时,所适用的各个配置文件的默认设置。通过添加咱们打算定制的任何配置文件的 `[profile.*]` 小节,咱们就会覆盖掉默认设置的任何子集。比如,下面是 `dev``release` 配置文件中 `opt-level` 设置的默认值:
文件名:`Cargo.toml`
```toml
[profile.dev]
opt-level = 0
[profile.release]
opt-level = 3
```
这个 `opt-level` 设置项,控制了 Rust 将应用到咱们代码的优化数目,有着范围 `0``3` 的取值范围。应用更多优化会延长编译时间,因此若咱们是在开发过程中而频繁编译代码,那么即使产生出的代码运行较慢,咱们也会想要更少的优化来更快地编译。因此默认的 `opt-level` 就是 `0`。而在咱们已准备好发布咱们的代码时,那么就最好用更多时间来编译。咱们将只以发布模式编译一次,但会运行编译好的程序许多次,因此发布模式就以较长的编译时间,换取到运行较快的代码。那就是 `release` 配置文件的 `opt-level` 默认为 `3` 的原因。
通过在 `Cargo.toml` 中,给某个默认值添加不同的值,就可以覆盖掉这个默认值。比如,在打算于开发配置文件中使用优化级别 `1` 时,就可以把下面这两行,添加到项目的 `Cargo.toml`
文件名:`Cargo.toml`
```toml
[profile.dev]
opt-level = 1
```
此代码会覆盖默认设置 `0`。现在当咱们运行 `cargo build`Cargo 将使用 `dev` 配置文件的默认设置,加上咱们对 `opt-level` 的定制。由于咱们把 `opt-level` 设置为了 `1`Cargo 将应用相比于默认设置更多,但不如发布构建那样多的优化。
对于各个配置文件的完整配置项清单与默认设置,请参阅 [Cargo 文档](https://doc.rust-lang.org/cargo/reference/profiles.html)。
## 将代码箱发布到 Crates.io
**Publishing a Crate to Crates.io**
咱们已将 [crates.io](https://crates.io) 上的一些包,用作了咱们项目的依赖,而通过发布自己的包,咱们还可以与其他人分享咱们自己的代码。位于 [crates.io](https://crates.io) 网站的代码箱登记,会分发咱们包的源码,因此其主要保存开放源码的代码。
Rust 与 Cargo均有着令到咱们所发布的包易于为他人找到并使用的一些特性。咱们将讲到其中一些特性并讲解怎样发布包how to publish a package。
### 制作有用的文档注释
**Making Useful Documentation Comments**
准确地为咱们的包编写文档,将帮助到其他使用者获悉怎样及何时来使用他们,因此投入时间来编写文档是值得的。第 3 章中,咱们曾讨论过如何使用双斜杠 `//`来注释 Rust 代码。Rust 还有用于文档的一种将生成 HTML 文档的特殊注释,而被方便地称作 *文档注释documentation comment*。这些 HTML 会显示出公开 API 项目的文档注释内容,这些内容是为对了解怎样 *使用use* 咱们的代码箱,而非咱们代码箱如何实现感兴趣的程序员所准备的。
文档注释用的是三斜杠 `///` 而非双斜杠,并支持用于格式化文本的 Markdown 写法。要把文档注释恰好放在他们要注释的项目前,而紧接着注释项目。下面清单 14-1 给出了名为 `cargo_features_demo` 代码箱中,`add_one` 函数的文档注释。
文件名:`src/lib.rs`
~~~rust
/// 将一加到所给数字。
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = cargo_features_demo::add_one(arg);
///
/// assert_eq! (6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
x + 1
}
~~~
*清单 14-1函数的文档注释*
这里,咱们给到了 `add_one` 函数完成什么的描述,以标题 `Examples` 开始了一个小节,并随后提供了演示怎样使用 `add_one` 函数的代码。咱们可通过运行 `cargo doc` 命令,生成文档注释的 HTML 文档。这个命令会运行与 Rust 一起分发的 `rustdoc` 工具,并将生成的 HTML 文档放在 `target/doc` 目录中。
处于便利目的,运行 `cargo doc --open` 将构建出当前代码箱文档(以及咱们代码箱全部依赖的文档)的 HTML并随后在 web 浏览器中打开得到的结果。导航到那个 `add_one` 函数,咱们将看到文档注释中的文本如何渲染出来,如下图片 14-01 中所示:
![`add_one` 函数的 HTML 文档](images/14-01.png)
*图 14-01`add_one` 函数的 HTML 文档*
#### 经常用到的小节
**Commonly Used Sections**
咱们曾使用清单 14-1 中的 `# Examples` Markdown 标题,来创建出 HTML 中带有标题 “Examples” 的小节。下面是代码箱作者们,经常在他们文档中用到的一些其他小节:
- **Panics**:被文档注释的函数可能终止运行的情形。那些不愿其程序终止运行的调用者,就应确保在这些情形下他们不会调用该函数;
- **Errors**:若函数返回了 `Result`,那么描述出可能发生的各种错误,及何种条件下会造成那些错误的返回,就能有效帮助到调用者,从而他们可以编写出以不同方式,处理不同类别错误的代码;
- **Safety**:若函数调用起来是 `unsafe` 的(在第 19 章咱们就会讨论到不安全那么就应有一个解释为何该函数不安全并说明该函数期望调用者要遵守哪些不变因素的小节if the funciton is `unsafe` to call(we discuss unsafety in Chapter 19), there should be a section explaining why the function is unsafe and covering the invariants that the function expects callers to uphold。
多数的文档注释并不需要全部这些小节,但这仍不失为一个提醒咱们,关于咱们代码使用者将有兴趣了解的各方面的一个良好检查单。
#### 作为测试的文档注释
**Documentation Comments as Tests**
在文档注释中添加一些示例代码块可以帮助演示怎样使用咱们的库且这样做有着附带的好处an additional bonus运行 `cargo test` 将把文档中示例代码作为测试运行!带有示例的文档属实很好。而在文档编写好后,由于代码已被修改而造成示例不工作,也是极为糟糕的。当咱们以清单 14-1 中 `add_one` 函数的文档,运行 `cargo test`,就将在测试结果中看到这样一个小节:
```console
Doc-tests cargo_features_demo
running 1 test
test src/lib.rs - add_one (line 7) ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.15s
```
现在当咱们修改那个函数或者那个示例,从而让示例中的 `assert_eq!` 终止运行,并再次运行 `cargo tset` 时咱们将看到文档测试the doc tests捕获到示例与代码不再相互同步
> 注:此状况下的输出为:
```console
Doc-tests cargo_features_demo
running 1 test
test src/lib.rs - add_one (line 7) ... FAILED
failures:
---- src/lib.rs - add_one (line 7) stdout ----
Test executable failed (exit status: 101).
stderr:
thread 'main' panicked at 'assertion failed: `(left == right)`
left: `6`,
right: `7`', src/lib.rs:7:1
stack backtrace:
0: 0x5620cf499480 - std::backtrace_rs::backtrace::libunwind::trace::h32eb3e08e874dd27
at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/std/src/../../backtrace/src/back trace/libunwind.rs:93:5
// ...
36: 0x0 - <unknown>
failures:
src/lib.rs - add_one (line 7)
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.15s
error: doctest failed, to rerun pass `--doc`
```
> 注:执行 `cargo test --doc`,将只运行文档注释中的示例代码。
#### 注释被包含所在项目
**Commenting Contained Items代码箱、模组整体的注释**
`//!` 样式的文档注释,会把文档添加到包含注释的条目,而非注释之后的条目。咱们通常在代码箱根文件里(依惯例即 `src/lib.rs`或模组里添加这些文档注释来将代码箱或模组作为整体而为其编写文档the style of doc comment `//!` adds documentation to the item contains the comments rather than to the items following the comments. We typically use these doc comments inside the crate root file(`src/lib.rs` by convention) or inside a module to document the crate or the module as a whole。
比如,要添加描述包含了 `add_one` 函数的 `cargo_features_demo` 代码箱目的的文档,咱们就要添加以 `//!` 开始的文档注释,到 `src/lib.rs` 文件的开头,如下清单 14-2 中所示:
文件:`src/lib.rs`
```rust
//! # Cargo 特性示例代码箱
//!
//! `cargo_features_demo` 是令到执行某些确切计算更便利
//! 的一些工具的集合。
//!
/// 将一加到所给数字。
// --跳过代码--
```
*清单 14-2作为一个整体的 `cargo_features_demo` 代码箱的文档*
请注意由于咱们是以 `//!` 而非 `///` 开始的这些注释,因此在以 `//!` 开始的最后一行后,并无任何代码的,咱们是在给包含此注释的程序项目,而非紧接着此注释的程序项目编写文档。在此示例中,那个程序项目就是 `src/lib.rs` 文件,为代码箱根。这些注释描述了整个代码箱。
当咱们运行 `cargo doc --open` 时,这些注释将显示在 `cargo_features_demo` 代码箱文档的首页he front page位处代码箱公开项目的清单之上如下图 14-02 中所示:
![渲染出的 `cargo_features_demo` 代码箱文档](images/14-02.png)
*图 14-02渲染出的 `cargo_features_demo` 代码箱文档, 包括着将该代码箱作为整体描述的注释*
程序项目里的文档注释用于对描述代码箱及模组尤其有用。使用他们来解释容器the container的整体目标有助于咱们的用户们理解代码箱的组织结构。
### 使用 `pub use` 导出便利的公开 API
**Exporting a Convinient Public API with `pub use`**
在咱们发布代码箱时,公开 API 的结构是主要的考量。相比与咱们,使用咱们代码箱的人们对代码箱结构的没有那么熟悉,并在咱们的代码箱有着大型模组层次结构时,难于找到他们打算使用的部分。
在第 7 章中,咱们曾讲到过怎样使用 `pub` 关键字把一些程序项目构造为公开,与怎样使用 `use` 关键字,把程序项目带入到作用域。但是,咱们在开发某个代码箱时,对咱们有意义的组织结构(模组树),对于咱们的用户则可能不那么便利。咱们会打算把代码箱结构组织为包含多个级别的层次,但随后某个想要使用定义在层次结构深处类型的人,就可能在找出那个类型是否存在上遇到麻烦。他们可能还会对必须敲入 `use cargo_features_demo::some_module::another_module::UsefulType;`,而非敲入 `use cargo_features_demo::UsefulType;` 而感到恼火。
可喜的是,若代码箱组织结构 *不* 便于其他人在另一库中使用,咱们不必重新调整代码箱的内部组织:相反,咱们可通过使用 `pub use`重新导出程序项目而构造出一种不同于咱们私有组织结构的公开组织结构。重新导出re-export会取一处的公开程序项目而在另一处将其构造为公开就跟这个项目是在那另一处被定义过一样。
比如说,咱们构造了用于建模美术概念的一个名为 `art` 的库。这个库里有两个模组:包含了两个名为 `PrimaryColor` 与 `SeccondaryColor` 枚举的 `kinds` 模组,与包含了名为 `mix` 函数的 `utils` 模组,如下清单 14-3 中所示:
文件名:`src/lib.rs`
```rust
//! # art
//!
//! 建模诸多美术概念的一个库。
pub mod kinds {
/// RYB 颜色模型下的主要颜色。
pub enum PrimaryColor {
Red,
Yellow,
Blue,
}
/// RYB 颜色模型下的次要颜色。
pub enum SecondaryColor {
Orange,
Green,
Purple,
}
}
pub mod utils {
use crate::kinds::*;
/// 结合两种等量的主要颜色,创建出
/// 某种次要颜色。
pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {
// --跳过代码--
SecondaryColor::Purple
}
}
```
*清单 14-3有着组织到 `kinds` 与 `utils` 两个模组中的一些程序项目的 `art` 库*
下图 14-03 展示了由 `cargo doc` 产生出的该代码箱文档首页,看起来的样子:
![列出 `kinds` 与 `utils` 两个模组的 `art` 代码箱文档首页](images/14-03.png)
*图 14-3列出 `kinds` 与 `utils` 两个模组的 `art` 代码箱文档首页*
请注意 `PrimaryColor` 与 `SecondaryColor` 两个类型,及 `mix` 函数都未在首页上列出。要看到他们,咱们必须点击 `kinds` 与 `utils`。
依赖于这个库的另一代码箱,将需要把程序项目从 `art` 带入到作用域的 `use` 语句,与指明当前定义的模组结构。下面清单 14-4 给出了用到 `art` 代码箱中 `PrimaryColor` 与 `mix` 两个程序项目的代码箱示例:
文件名:`src/main.rs`
```rust
use art::kinds::PrimaryColor;
use art::utils::mix;
fn main() {
let red = PrimaryColor::Red;
let yellow = PrimaryColor::Yellow;
mix(red, yellow);
}
```
*清单 14-4用到 `art` 代码箱以内部组织结构导出程序项目的代码箱*
> **注**:使用本地未发布代码箱的方法,是在 `Cargo.toml` 的 `[dependencies]` 小节中,列出要使用的本地未发布代码箱。参见 [How to use a local unpublished crate?](https://stackoverflow.com/a/33025972)
文件:`Cargo.toml`
```toml
// --跳过代码--
[dependencies]
art = { path = "../art" }
```
清单 14-4 中用到 `art` 代码箱代码的作者,不得不搞清楚 `PrimaryColor` 是在 `kinds` 模组中,及 `mix` 函数是在 `utils` 模组中。`art` 代码箱的模组结构(即模组树),相比于用到该代码箱的开发者,与在 `art` 代码箱上编写代码的开发者要更为密切。对于试图搞清楚怎样使用 `art` 代码箱的人来说,其内部组织结构并未包含任何有用信息,而因为要用到他的开发者,不得不搞明白要在那里去查看,且必须在 `use` 语句中指明那些模组名字,这反而会造成混乱。
要从公开 API 中移除内部组织结构,咱们可把清单 14-3 中 `art` 代码箱的代码,修改为添加一些 `pub use` 语句来在顶层处重导出程序项目to re-export the items at the top level如下清单 14-5 中所示:
文件名:`src/lib.rs`
```rust
//! # art
//!
//! 建模诸多美术概念的一个库。
pub use self::kinds::PrimaryColor;
pub use self::kinds::SecondaryColor;
pub use self::utils::mix;
pub mod kinds;
pub mod utils;
```
*清单 14-5添加 `pub use` 语句来重导出程序项目*
如下图 14-04 中所示,`cargo doc` 为此代码箱所产生出的 API 文档,现在将在首页上列出并链接到重导出项,从而令到 `PrimaryColor` 与 `SecondaryColor` 两个类型及 `mix` 函数更易于找到。
![列出了重导出项目的 `art` 代码箱文档首页](images/14-04.png)
*图 14-4列出重导出项的 `art` 代码箱文档首页*
`art` 代码箱的用户,依然可以像清单 14-4 中所演示的那样,发现及使用清单 14-3 的内部结构,或者他们可使用清单 14-5 中更为便利的结构,如下清单 14-6 中所示:
文件名:`src/main.rs`
```rust
use art::mix;
use art::PrimaryColor;
fn main() {
let red = PrimaryColor::Red;
let yellow = PrimaryColor::Yellow;
mix(red, yellow);
}
```
*清单 14-6使用着 `art` 代码箱重导出项的程序*
其中有许多嵌套模组的情形下,以 `pub use` 在顶层重导出类型,可在用到该代码箱的人的体验方面,造成显著不同。`pub use` 的另一常见用途则是,为将依赖代码箱的定义构造为咱们自己代码箱公开 API 的一部分,而重导出当前代码箱中某个依赖的定义。
创建出有用的公开 API 结构,与其说是一门科学,不如说是一门艺术,而咱们可不断迭代,来找到对用户运作最佳的 API。选择 `pub use` 会给到咱们在内部组织代码箱方式上的灵活性,并解除了内部结构与呈现给代码箱用户的组织结构的耦合。请查看咱们曾安装的代码箱代码,来发现他们的内部结构,是否不同于其公开 API。
### 建立 Crates.io 帐号
在咱们能发布代码箱之前,咱们需要在 [crates.io](https://crates.io) 上创建帐号,并得到 API 令牌an API token。而要这样做就要访问 [crates.io](https://crates.io) 处的主页,并通过 GitHub 帐号登录。(目前 GitHub 帐号是必须的,但该站点今后可能会支持其他创建帐号途径。)在登录后,咱们就要访问 [https://crates.io/me/](https://creates.io/me/) 处的帐号设置,而获取自己的 API 密钥API key。然后使用咱们的 API 密钥,运行 `cargo login` 命令,如下:
```console
$ cargo login abcdefghijklmnopqrstuvwxyz012345
```
此命令将告知 Cargo 咱们的 API 令牌,并在 `~/.cargo/credentials` 文件中本地存储起来。请注意此令牌是个 *秘密secret*:不要与任何人分享。不论因何种缘故,与任何人分享了,咱们都应吊销他,并在 [crates.io](https://crates.io) 上生成新的令牌。
### 添加元数据到新代码箱
Adding Metadata to a New Crate**
假设咱们有了个打算发布的代码箱。在发布前,咱们将需要在代码箱的 `Cargo.toml` 文件的 `[package]` 小节中,添加一些元数据。
咱们的代码箱将需要一个独特的名字。当咱们在本地于代码箱上工作时,咱们可以给代码箱取任意喜欢的名字。但是,[crates.io](https://crates.io) 上代码箱的名字则是以先到先得的原则分配的allocated on a first-come, first-served basis。一旦某个代码箱名字已被占用其他人就不能发布有着那个名字的代码箱。在尝试发布某个代码箱之前咱们要检索一下打算使用的名字。若这个名字已被使用咱们将需要找到另一名字并编辑 `Cargo.toml` 文件中 `[package]` 小节下的 `name` 字段,来使用这个用作发布的新名字,像下面这样:
文件名:`Cargo.toml`
```toml
[package]
name = "guessing_game"
```
即使咱们已选了个独特的名字,当咱们此时运行 `cargo publish` 来发布这个代码箱时,仍将得到一条告警及随后的报错:
```console
cargo publish lennyp@vm-manjaro
Updating crates.io index
warning: manifest has no description, license, license-file, documentation, homepage or repository.
See https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info.
Packaging guessing_game v0.1.0 (/home/lennyp/rust-lang/guessing_game)
Verifying guessing_game v0.1.0 (/home/lennyp/rust-lang/guessing_game)
Compiling libc v0.2.132
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.16
Compiling getrandom v0.2.7
Compiling rand_core v0.6.3
Compiling rand_chacha v0.3.1
Compiling rand v0.8.5
Compiling guessing_game v0.1.0 (/home/lennyp/rust-lang/guessing_game/target/package/guessing_game-0.1.0)
Finished dev [unoptimized + debuginfo] target(s) in 3.55s
Uploading guessing_game v0.1.0 (/home/lennyp/rust-lang/guessing_game)
error: failed to publish to registry at https://crates.io
Caused by:
the remote server responded with an error: missing or empty metadata fields: description, license. Please see https://doc.rust-lang.org/cargo/reference/manifest.html for how to upload metadata
```
此报错是由于咱们缺失了一些重要信息:描述及许可证是必须的,由此人们就会明白咱们的代码箱完成的什么,及在何种条件下他们可以使用他。在 `Cargo.toml` 中,由于代码箱的描述,会与咱们的代码箱一起呈现在搜索结果中,因此请添加仅仅一两句话的描述。而对于 `license` 字段,则需要提供 *某个许可证标识符值a licence identifier value*。[Linux 基金会的软件包数据交换站Linux Foundation's Software Package Data Exchange, SPDXspdx.org](http://spdx.org/licenses/) 列出了可供这个值使用的标识符。比如,为指明咱们已使用 MIT 许可证,授权咱们的软件包,就要添加 `MIT` 的许可证标识符:
文件名:`Cargo.toml`
```toml
[package]
name = "guessing_game"
license = "MIT"
```
若咱们打算使用某个未出现于 SPDX 中的许可证,咱们就需要把那种许可证的文本,放置于某个文件里,把这个文件包含在咱们的项目中,并于随后使用 `license-file` 来指出那个文件的名字,而不再使用 `license` 键the `license` key。
至于哪种许可证适合于咱们的项目方面的指南是超出这本书的范围的。Rust 社区的许多人,都以 Rust 项目同样的方式,即采用 `MIT OR Apache-2.0` 双重许可证,授权他们的项目。这种实践表明,咱们也可以通过 `OR` 来指定出多个许可证标识符,从而让咱们的项目有着多种许可证。
在添加了独特名字、版本号、代码箱描述及许可证后,已准备好发布项目的 `Cargo.toml`文件,就会看起来像下面这样:
文件名:`Cargo.toml`
```toml
[package]
name = "guessing_game"
license = "MIT"
version = "0.1.0"
description = "一个在其中猜出计算机所选数字的有趣游戏。"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rand = "0.8.3"
```
[Cargo 文档](https://doc.rust-lang.org/cargo/) 介绍了为确保其他人能更容易发现并使用咱们代码箱,而可指明的别的一些元数据。
### 发布到 Crates.io
既然咱们已经创建了账号,保存了 API 令牌,选择了代码箱名字,并指定了必需的元数据,那么咱们就准备好发布了!发布代码箱,会上传特定版本到 [crates.io](https://crates.io),供其他人使用。
因为发布是 *永久性的permanent*,因此要当心。版本绝无可能被覆盖,且代码无法被删除。[crates.io](https://crates.io) 的一个主要目标,是要充当代码的永久存档,以便依赖于 [crates.io](https://crates.io) 中代码箱的所有项目构建都将持续工作。而允许版本的删除,就会令到实现那个目标几无可能。不过,在咱们可发布的代码箱版本数目上没有限制。
再度运行 `cargo publish` 命令。现在他就应成功了:
```console
$ cargo publish lennyp@vm-manjaro
Updating crates.io index
warning: manifest has no documentation, homepage or repository.
See https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info.
Packaging guessing_game-xfossdotcom v0.1.0 (/home/lennyp/rust-lang/guessing_game)
Verifying guessing_game-xfossdotcom v0.1.0 (/home/lennyp/rust-lang/guessing_game)
Compiling libc v0.2.132
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.16
Compiling getrandom v0.2.7
Compiling rand_core v0.6.3
Compiling rand_chacha v0.3.1
Compiling rand v0.8.5
Compiling guessing_game-xfossdotcom v0.1.0 (/home/lennyp/rust-lang/guessing_game/target/package/guessing_game-xfossdotcom-0.1.0)
Finished dev [unoptimized + debuginfo] target(s) in 2.73s
Uploading guessing_game-xfossdotcom v0.1.0 (/home/lennyp/rust-lang/guessing_game)
```
恭喜!现在咱们就已与 Rust 社区分享了咱们的代码,且任何人都可将咱们的代码箱,添加为他们项目的依赖。
> 注:在 Crates.io 上的账号电子邮箱未验证时,将报出如下错误:
```console
Caused by:
the remote server responded with an error: A verified email address is required to publish crates to crates.io. Visit https://crates.io/me to set and verify your email address.
```
### 发布既有代码箱的新版本
咱们完成咱们代码箱的修改,而准备好发布新版本时,咱们要修改 `Cargo.toml` 中所指定的 `version` 值并重新发布。请运用 [语义版本控制规则Semantic Versioning rules](http://semver.org/),根据咱们已做出修改的类别,来确定出恰当的下一版本编号为何。然后运行 `cargo publish` 来上传新版本。
### 使用 `cargo yank` 命令弃用 Crates.io 上的版本
**Depracating Versions from Crates.io with `cargo yank`**
尽管咱们无法移除代码箱的先前版本但咱们可以阻止任何今后的项目将其添加为新的依赖项。这在某个代码箱版本由于某种原因或别的问题而损坏时是有用的。在诸如此类的情形下Cargo 支持把某个代码箱版本 *抽出来*in such situations, Cargo supports *yanking* a crate version。
抽出某个版本,在允许所有依赖该版本的既有项目继续工作的同时,会阻止新项目依赖那个版本。本质上,一次版本抽出,表示带有 `Cargo.lock` 的全部项目不会破坏,而任何今后生成的 `Cargo.lock` 文件,都将不使用被抽出的版本。
要抽出代码箱的某个版本,就要在咱们先前已发布的代码箱目录中,运行 `cargo yank` 并指定出要抽出的版本。比如,咱们曾发布了名为 `guessing_game` 代码箱的 `0.1.0` 版本,而打算抽出他,咱们就要在 `guessing_game` 的项目目录下,运行下面的命令:
```console
$ cargo yank --vers 0.1.0 4s lennyp@vm-manjaro
Updating crates.io index
Yank guessing_game-xfossdotcom@0.1.0
```
通过把 `--undo` 添加到这个命令,咱们还可以撤销某次抽出,而允许项目开始再度依赖于某个版本:
```console
$ cargo yank --vers 0.1.0 --undo lennyp@vm-manjaro
Updating crates.io index
Unyank guessing_game-xfossdotcom@0.1.0
```
抽出版本,*不会* 删除任何代码。比如,其无法删除那些不小心上传的机密信息。若发生了机密信息被上传的情况,咱们必须立即重置这些机密信息。
## Cargo 工作区
**Cargo Workspaces**
在第 12 章中咱们曾构建了个包含二进制代码箱和库代码箱的包a package。随着咱们项目的持续开发咱们会发现库代码箱会持续变大而咱们就会想要把咱们的包进一步拆分为多个库代码箱。Cargo 提供了可帮助管理多个齐头并进开发的相关包,名为 *工作区workspace* 的特性。
> 注:总结 Rust 开发的层次结构如下工作区workspace -> 包package -> 代码箱crate -> 模组module -> 语句statement。
### 创建工作区
*工作区a workspace* 是共享了同一 `Cargo.lock` 文件与输出目录的包集合。咱们来构造一个用到工作区的项目 -- 咱们将使用一些简单代码,这样咱们便可着重于工作区的结构。组织工作区有多种方式,因此咱们将只给出一种常见方式。咱们将会有包含着一个二进制代码箱,与两个库代码箱的一个工作区。其中的二进制代码箱,将提供主要功能,其将依赖于其中的两个库代码箱。而一个库代码箱将提供 `add_one` 函数,另一个则会提供 `add_two` 函数。这三个代码箱,都将是同一工作区的一部分。咱们将以创建出工作区目录开始:
```console
$ mkdir add
$ cd add
```
接下来,在 `add` 目录中,咱们就要创建出将对整个工作区加以配置的 `Cargo.toml` 文件。这个文件不会有 `[package]` 小节。相反,他会以 `[workspace]` 小节开始,其将允许咱们,通过指定出有着咱们的二进制代码箱的包路径,而把成员添加到工作区;在这个示例中,那个路径为 `adder`:
文件名:`Cargo.toml`
```toml
[workspace]
members = [
"adder",
]
```
接着,咱们将通过在 `add` 目录里运行 `cargo new`,而创建出 `adder` 二进制代码箱:
```console
$ cargo new adder
Created binary (application) `adder` package
```
到这里,咱们就可通过运行 `cargo build` 构建出工作区。`add` 目录下的文件,看起来应像下面这样:
```console
.
├── adder
│   ├── Cargo.toml
│   └── src
│   └── main.rs
├── Cargo.lock
├── Cargo.toml
└── target
```
在其顶层,工作区有个 `target` 目录那些编译出的物件the compiled artifacts就会放入其中`adder` 包没有自己的 `target` 目录。即使咱们在 `adder` 目录内运行 `cargo build`,那些编译出的物件,仍将出现在 `add/target` 中,而不是 `add/adder/target` 目录里。Cargo 之所以像这样来组织 `target` 目录,是因为工作区中的代码箱是为了依赖于彼此。若各个代码箱都有自己的 `target` 目录,那么为了把编译出的物件放在自己的 `target` 目录中,就不得不重新编译工作区中其他各个代码箱。经由共用一个 `target` 目录,代码箱就可以避免不必要的重新构建。
### 在工作区中创建第二个包
**Creating the Second Package in the Workspace**
接着,咱们来创建工作区中的另一个成员包,并将其叫做 `add_one`。请修改顶层的 `Cargo.toml`,在 `members` 清单中指明 `add_one` 的路径:
文件名:`Cargo.toml`
```toml
[workspace]
members = [
"adder",
"add_one",
]
```
随后生成名为 `add_one` 的新库代码箱:
```console
$ cargo new add_one --lib lennyp@vm-manjaro
Created library `add_one` package
```
`add` 目录现在应该有这些目录与文件:
```console
.
├── adder
│   ├── Cargo.toml
│   └── src
│   └── main.rs
├── add_one
│   ├── Cargo.toml
│   └── src
│   └── lib.rs
├── Cargo.lock
├── Cargo.toml
└── target
```
在 `add_one/src/lib.rs` 文件中,咱们来添加一个 `add_one` 函数:
文件名:`add_one/src/lib.rs`
```rust
pub fn add_one(x: i32) -> i32 {
x + 1
}
```
现在咱们就可以让有着咱们二进制代码箱的 `adder` 包,依赖于有着咱们库代码箱的 `add_one` 包了。首先,咱们将需要把有关 `add_one` 的路径依赖a path dependency添加到 `adder/Cargo.toml`。
文件名:`adder/Cargo.toml`
```toml
[dependencies]
add_one = { path = "../add_one" }
```
Cargo并不假设工作区中的箱子会相互依赖所以我们需要明确说明依赖关系。
接下来,咱们就要在 `adder` 代码箱中,使用 `add_one` 函数(来自 `add_one` 代码箱)。请打开 `adder/src/main.rs` 文件,并在其顶部使用一个 `use` 行,把新的 `add_one` 库代码箱带入到作用域。随后修改 `main` 函数来调用 `add_one` 函数,如下清单 14-7 中所示。
文件名:`adder/src/main.rs`
```rust
use add_one::add_one;
fn main() {
let num = 10;
println!("你好,世界!{num} 加一为 {}!", add_one(num));
}
```
*清单 14-7在 `adder` 代码箱中使用 `add_one` 库代码箱*
咱们来通过在 `add` 目录顶层运行 `cargo build`,构建工作区!
```console
$ cargo build lennyp@vm-manjaro
Compiling add_one v0.1.0 (/home/lennyp/rust-lang/add/add_one)
Compiling adder v0.1.0 (/home/lennyp/rust-lang/add/adder)
Finished dev [unoptimized + debuginfo] target(s) in 0.40s
```
而要在 `add` 目录运行二进制代码箱,咱们可通过使用 `-p` 命令行参数,指明咱们打算允许工作区中的哪个包,及与 `cargo run` 运行的包名字:
```console
$ cargo run -p adder lennyp@vm-manjaro
Compiling adder v0.1.0 (/home/lennyp/rust-lang/add/adder)
Finished dev [unoptimized + debuginfo] target(s) in 0.35s
Running `target/debug/adder`
你好,世界!
10 加 1 为 11!
```
这会运行 `adder/src/main.rs` 中的代码,其依赖于 `add_one` 代码箱。
### 于工作区中依赖外部代码箱
**Depending on an External Package in a Workspace**
请注意工作区只有一个在顶层的 `Cargo.lock` 文件,而非各个代码箱目录中都有 `Cargo.lock`。这确保了工作区的全部代码箱,都使用着同一版本的所有依赖。若咱们把 `rand` 包添加到 `adder/Cargo.toml` 及 `add_one/Cargo.toml` 两个文件,那么 Cargo 将把那两个依赖,解析为一个版本的 `rand`,并将其记录在那一个的 `Cargo.lock` 中。
让工作区中全部代码箱使用同样的依赖,意味着这些代码箱将始终相互兼容。咱们来把 `rand` 代码箱添加到 `add_one/Cargo.toml` 文件的 `[dependencies]` 小节,这样咱们便可在 `add_one` 代码箱中使用 `rand` 代码箱:
文件名:`add_one/Cargo.toml`
```toml
rand = "0.8.3"
```
现在咱们便可把 `use rand;` 添加到 `add_one/src/lib.rs` 文件了,而通过在 `add` 目录中运行 `cargo build` 构建整个工作区,就会带入并编译 `rand` 代码箱。由于咱们没有引用咱们已带入到作用域中的 `rand`,因此咱们将得到一条告警:
```console
$ cargo build lennyp@vm-manjaro
Updating crates.io index
Downloaded rand_core v0.6.4
Downloaded ppv-lite86 v0.2.17
Downloaded getrandom v0.2.8
Downloaded libc v0.2.137
Downloaded 4 crates (681.6 KB) in 1.29s
Compiling libc v0.2.137
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.17
Compiling getrandom v0.2.8
Compiling rand_core v0.6.4
Compiling rand_chacha v0.3.1
Compiling rand v0.8.5
Compiling add_one v0.1.0 (/home/lennyp/rust-lang/add/add_one)
warning: unused import: `rand`
--> add_one/src/lib.rs:1:5
|
1 | use rand;
| ^^^^
|
= note: `#[warn(unused_imports)]` on by default
warning: `add_one` (lib) generated 1 warning
Compiling adder v0.1.0 (/home/lennyp/rust-lang/add/adder)
Finished dev [unoptimized + debuginfo] target(s) in 6.76s
```
顶层的 `Cargo.lock`,现在包含了有关 `add_one` 对 `rand` 的依赖信息。但是,即使 `rand` 在工作区中的某处被用到,除非把 `rand` 添加到其他代码箱的 `Cargo.toml` 文件,否则咱们就不能在其他代码箱中使用他。比如,若咱们把 `use rand;` 添加到 `adder` 包的 `adder/src/main.rs` 文件,咱们将得到一个报错:
```console
$ cargo build lennyp@vm-manjaro
--跳过前面的告警--
Compiling adder v0.1.0 (/home/lennyp/rust-lang/add/adder)
error[E0432]: unresolved import `rand`
--> adder/src/main.rs:1:5
|
1 | use rand;
| ^^^^ no external crate `rand`
For more information about this error, try `rustc --explain E0432`.
error: could not compile `adder` due to previous error
```
要修正这个错误,就也要编辑 `adder` 包的 `Cargo.toml` 文件,而表明 `rand` 是其依赖项。构建 `adder` 包就将把 `rand`,添加到 `Cargo.lock` 中 `adder` 的依赖项清单,但不会有额外的 `rand` 拷贝将被下载。Cargo 已确保工作区中,每个用到 `rand` 包的包中的每个代码箱,都将使用同一版本,从而给咱们节省空间,并确保工作区中的代码箱都将兼容于彼此。
### 添加测试到工作区
**Adding a Test to a Workspace**
为说明另一项改进,咱们来添加一个 `add_one` 代码箱里 `add_one::add_one` 函数的测试:
文件名:`add_one/src/lib.rs`
```rust
pub fn add_one(x: i32) -> i32 {
x + 1
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add_one(2);
assert_eq!(result, 3);
}
}
```
现在请于顶层的 `add` 目录中运行 `cargo test`。在像这样组织起来的工作区中,运行 `cargo test`,就会运行工作区中所有代码箱的测试:
```console
$ cargo test lennyp@vm-manjaro
Compiling add_one v0.1.0 (/home/lennyp/rust-lang/add/add_one)
Compiling adder v0.1.0 (/home/lennyp/rust-lang/add/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.68s
Running unittests src/lib.rs (target/debug/deps/add_one-837c2ad0efe6b80c)
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/debug/deps/adder-2277ab1084738161)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests add_one
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
```
输出的首个部分,显示 `add_one` 代码箱中的 `it_works` 测试通过了。下一小节显示,在 `adder` 代码箱中找到零个测试,而随后的最后小节,显示在 `add_one` 代码箱中找到零个文档测试。(*注*:二进制代码箱中不会有文档测试?)
咱们还可通过使用 `-p` 命令行标志,并指明要测试的代码箱名字,而在顶层目录处运行工作区中特定代码箱的测试:
```console
$ cargo test -p add_one lennyp@vm-manjaro
Finished test [unoptimized + debuginfo] target(s) in 0.01s
Running unittests src/lib.rs (target/debug/deps/add_one-837c2ad0efe6b80c)
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests add_one
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
```
此输出展示出,`cargo test` 只运行了 `add_one` 代码箱的测试,而未运行 `adder` 代码箱的测试。
若咱们把工作区中的代码箱发布到 `crates.io` ,工作区中的各个代码箱将需要被单独发布。与 `cargo test` 类似,咱们可通过使用 `-p` 命令行标志,并指明打算发布的代码箱名字,而发布工作区中的特定代码箱。
作为附加练习,请以与 `add_one` 代码箱类似方式,把 `add_two` 添加到这个工作区!
当咱们的项目日渐增长时,请考虑使用工作区:相比于一大块代码,要搞清楚较小的、单独的组件就更容易一些。再者,当代码箱经常同时被修改时,把这些代码箱保持在工作区中,就能令到他们之间的协作更容易。
## 使用 `cargo install` 安装二进制代码箱
**Installing Binaries with `cargo install`**
`cargo install` 命令允许咱们在本地安装和使用二进制的代码箱。这并不是要取代系统包system packages它的目的是为 Rust 开发者提供一种方便的方式来安装别人在 [crates.io](https://crates.io) 上分享的工具。请注意咱们只能安装有二进制目标的包packages that have binary targets。所谓 *二进制目标binary target*即与本身为非可运行而适合于在其他程序中包含的库目标a libary target相反因为代码箱有着一个 `src/main.rs` 文件,或有着被指定为二进制的另一文件时,而创建出的可以运行的程序。通常,代码箱会在 `README` 文件中,有着关于其是否为库代码箱,还是有着二进制目标,或二者皆具方面的信息。
使用 `cargo install` 安装的全部二进制程序文件,都被存储在安装根的 `bin` 文件中in the installation root's `bin` folder。在使用 `rustup.rs` 安装 Rust且没做任何定制配置时这个目录将是 `$HOME/.cargo/bin`。为能运行咱们使用 `cargo install` 安装的程序,就要确保那个目录在 `$PATH` 中。
> 注:可在任意位置运行 `cargo install` 命令,来安装 Crates.io 上的 Rust 二进制程序,这些程序都将被安装在 `$HOME/.cargo/bin` 下。若已安装了某个 Rust 程序后再安装他,那么就会有如下输出:
```console
$ cargo install ripgrep 1m 4s lennyp@vm-manjaro
Updating crates.io index
Ignored package `ripgrep v13.0.0` is already installed, use --force to override
```
比如,咱们曾在第 12 章中提到,有个用于搜索文件,`grep` 工具的 Rust 实现 `ripgrep`。要安装 `ripgrep`,咱们可运行如下命令:
```console
$ cargo install ripgrep
Updating crates.io index
Installing ripgrep v13.0.0
Compiling memchr v2.5.0
Compiling cfg-if v1.0.0
Compiling libc v0.2.137
Compiling log v0.4.17
Compiling proc-macro2 v1.0.47
Compiling lazy_static v1.4.0
Compiling regex-automata v0.1.10
Compiling quote v1.0.21
Compiling unicode-ident v1.0.5
Compiling bstr v0.2.17
Compiling syn v1.0.103
Compiling aho-corasick v0.7.20
Compiling regex-syntax v0.6.28
Compiling serde_derive v1.0.147
Compiling encoding_rs v0.8.31
Compiling serde v1.0.147
Compiling regex v1.7.0
Compiling grep-matcher v0.1.5
Compiling serde_json v1.0.89
Compiling unicode-width v0.1.10
Compiling fnv v1.0.7
Compiling same-file v1.0.6
Compiling once_cell v1.16.0
Compiling thread_local v1.1.4
Compiling globset v0.4.9
Compiling textwrap v0.11.0
Compiling encoding_rs_io v0.1.7
Compiling memmap2 v0.5.8
Compiling bitflags v1.3.2
Compiling crossbeam-utils v0.8.14
Compiling bytecount v0.6.3
Compiling itoa v1.0.4
Compiling ryu v1.0.11
Compiling strsim v0.8.0
Compiling termcolor v1.1.3
Compiling clap v2.34.0
Compiling grep-searcher v0.1.10
Compiling atty v0.2.14
Compiling base64 v0.13.1
Compiling grep-printer v0.1.6
Compiling grep-cli v0.1.6
Compiling grep-regex v0.1.10
Compiling ripgrep v13.0.0
Compiling walkdir v2.3.2
Compiling ignore v0.4.18
Compiling grep v0.2.10
Compiling num_cpus v1.14.0
Finished release [optimized + debuginfo] target(s) in 1m 09s
Installing /home/lennyp/.cargo/bin/rg
Installed package `ripgrep v13.0.0` (executable `rg`)
```
输出的倒数第二行显示出已安装二进制程序的位置与名字,在这个示例中名字便是 `rg`。正如前面提到的,只要安装目录是在 `$PATH` 中,随后咱们就可以运行 `rg --help`,并启动一个用于检索文件的更快、更具 Rust 风格的工具了!
## 使用定制命令扩展 Cargo
**Extending Cargo with Custom Commands**
Cargo 被设计为在无需修改 Cargo 下,咱们就可以使用新的子命令,对其加以扩展。若咱们的 `$PATH` 中有名为 `cargo-something` 的二进制程序,咱们便可通过运行 `cargo something`,将其作为 Cargo 的子命令运行。像这样的定制命令,还会在咱们运行 `cargo --list` 时给列出来。使用 `cargo install` 安装扩展,并随后跟运行内建的 Cargo 工具一样运行他们的这种能力,正是 Cargo 设计的一项超级便利的好处!
## 本章小结
运用 Cargo 与 [crates.io](https://crates.io) 分享代码,是令到 Rust 生态对于许多不同任务都有用的一个方面。Rust 的标准库是小型且稳定的,但在不同于语言本身的时间线上,代码箱则易于共享、运用以及改进。请不要羞于在 [crates.io](https://crates.io) 上分享对自己有用的代码;那些代码或许对其他人也同样有用!

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,92 @@
# 异步编程基础:异步、等待、未来值与流
许多我们要求计算机执行的操作,都需要一段时间才能完成。在等待这些长时间运行的进程完成时,若我们能做一些其他事情,那就再好不过了。现代计算机提供了两种同时处理多个操作的技术:并行机制与并发操作。不过,一旦我们开始编写那些涉及并行或并发操作的程序时,我们很快就会遇到 *异步编程* 所固有的一些新挑战,即操作可能无法按照他们启动的顺序,依次完成。本章建立在第 16 章中使用线程的并行机制,及通过引入另一种异步编程方法: Rust 的 Futures、流与支持他们的 `async``await` 语法,以及在异步操作间进行管理及协调的一些工具,实现的并发上。
我们来举个例子。假设咱们要导出一段家庭庆祝活动的视频,这个操作可能需要几分钟到几小时不等。视频导出将尽可能多地用到其做能使用的 CPU 和 GPU。如果咱们只有一个 CPU 内核,而咱们的操作系统在导出完成前,不会暂停该导出 -- 也就是说,如果他 *同步地* 执行该导出,那么在该任务运行期间,咱们就无法在咱们的计算机上做任何其他事情。这将是非常令人沮丧的经历。幸运的是,咱们电脑的操作系统可以,而且也确实经常隐蔽地中断该导出,让咱们可同时完成其他工作。
现在,假设咱们正下载某个别人共享的视频,这也需要一段时间,但不会占用那么多 CPU 时间。在这种情况下CPU 必须等待数据自网络到达。虽然数据开始到达后咱们就可以开始读取,但可能需要一些时间才能全部显示出来。即使数据全部都有了,但如果视频相当大,那么加载数据也可能需要至少一两秒才能全部加载。这听起来可能不算什么,但对于每秒可以执行数十亿次运算的现代处理器来说,这已经是很长的时间了。同样,在等待网络调用结束时,咱们的操作系统会隐形地中断咱们的程序,允许 CPU 执行其他工作。
视频输出是个 *CPU 密集CPU-bound**计算密集compute-bound* 操作的例子。他受限于计算机 CPU 或 GPU 的潜在数据处理速度,以及能将多少速度用于该操作。而视频下载则是个 *IO 密集IO-bound* 操作的例子,因为他受计算机 *输入及输出* 速度的限制;他只能以通过网络发送数据的速度运行。
在这两个例子中操作系统的隐形中断the operating system's invisible interrupts均提供了某种形式的并发性。不过这种并发只发生在整个程序的层面上操作系统会中断某个程序以让其他程序完成工作。在很多情况下由于我们对咱们程序的掌握要比操作系统更细粒度因此我们可以发现操作系统所无法看到的并发机会。
例如,若我们正开发某个管理文件下载的工具,我们应能将咱们的程序,编写成启动一次下载不会锁定用户界面,且用户应能同时启动多个下载。不过,多数操作系统与网络交互的 API却都是 *阻塞式的blocking*;也就是说,他们会阻塞程序的进程,直到他们正处理的数据完全就绪。
> 注意:仔细想想,这正是 *大多数* 函数调用的工作方式。不过,*阻塞* 一词通常保留给那些与文件、网络或计算机上其他资源交互的函数调用,因为在这些情况下,单个程序会从 *非阻塞non-blocking* 的操作中受益。
通过生成一个专门下载各个文件的线程,我们可以避免阻塞咱们的主线程。不过,这些线程的开销,最终会成为问题。若调用一开始就不阻塞,那就更好了。此外,如果我们能采用与阻塞代码相同的直接写法,效果会更好,就像下面这样:
```rust
let data = fetch_data_from(url).await;
println!("{data}");
```
这正是 Rust 的 `async`(异步,*asynchronous* 的缩写)抽象所给到我们的。在本章中,咱们将学到有关 `async` 的所有知识,包括以下主题:
- 怎样使用 Rust 的 `async``await` 语法;
- 如何使用异步模型,解决我们在第 16 章中遇到的一些同样难题;
- 多线程和异步如何提供,咱们可在许多情况下结合使用的互补方案。
不过在了解异步在实际中的工作原理前我们需要先绕道讨论一下并行和并发之间的区别the difference between parallelism and concurrency。
## 并行与并发机制
**Parrallelism and Concurrency**
到目前为止,我们都将并行和并发,看作是可以互换的。现在,我们需要更准确地区分他们,因为他们间的区别,会在我们开始工作时显现出来。
请设想某个团队在某个软件项目中,分工的不同方式。咱们可以给单个成员分配多项任务,也可以给每名成员分配一项任务,或者混合使用这两种方式。
当某单个成员在几项不同任务中的任何一项完成前,都工作于这些任务上时,这就是 *并发*。或许咱们在咱们电脑上,有着两个不同项目,当咱们在一个项目上感到无聊或卡住时,咱们就会切换到另一项目。咱们只是一个人,所以咱们无法同一时间在两项任务上取得进展,但咱们可以多任务处理,通过在两个任务之间切换,一次在一个任务上取得进展(见图 17-1
![并发工作流程,在任务 A 和任务 B 之间切换](./images/trpl17-01.svg)
*图 17-1一种并发工作流程在任务 A 和任务 B 之间切换*
而在团队采取让每个成员单独完成某项任务方式,拆分一组任务时,这就是 *并行*。团队中的每个人,在同一时间团队中的每个人都能取得进展(见图 17-2
![并行工作流程,任务 A 和任务 B 的工作独立进行](./images/trpl17-02.svg)
*图 17-2一种并行工作流程其中任务 A 和任务 B 上的工作独立进行*
在这两种工作流程中,咱们都可能需要协调不同任务。也许咱们 *会以为* 分配给某个人的任务,是完全独立于其他人工作的,但该任务实际上需要团队中另一人先完成他们的任务。有些工作可并行的完成,但有些工作实际上是 *串行的serial*:其只能以序列方式进行,一项任务接着另一项任务,如图 17-3 所示。
![部分并行的工作流程,任务 A 和任务 B 的工作独立进行,直到任务 A3 被任务 B3 的结果阻断。](./images/trpl17-03.svg)
*图 17-3一种部分并行的工作流程任务 A 与任务 B 的工作独立进行,直到任务 A3 被任务 B3 的结果阻塞*
同样,咱们也可能会发现,自己的一项任务依赖于另一项任务。现在,咱们的并行工作也变成串行的了。
并行与并发也会相互交叉。如果咱们得知某名同事在我们完成我们的某项任务前卡住了,咱们就可能会把所有精力都放在这项任务上,以 “解除” 该名同事的阻塞。这样,咱们和咱们的同事就无法再并行工作,咱们也无法再并发地执行咱们自己的任务。
软件和硬件的基本动态也是如此。在只有一个 CPU 核的机器上CPU 一次只能执行一个操作,但其仍可并发地工作。利用线程、进程及异步等工具,计算机可暂停一项活动,并切换到其他活动,最后再次循环到第一项活动。在有着多个 CPU 核的机器上,计算机还可以并行工作。一个核心可以执行一项任务,而另一个核心可以执行完全无关的任务,这些操作实际上是在同一时间进行的。
在 Rust 中使用异步时,我们总是要处理并发问题。根据硬件、操作系统和我们所使用的异步运行时(稍后将详细介绍异步运行时),并发也可能在其表象之下,使用并行。
现在我们来深入了解一下Rust 中异步编程的工作原理。

View File

@@ -13,910 +13,14 @@
以下即为本章咱们将涵盖的几个话题:
- 怎样创建出线程来在同一时间运行代码的不同片段how to create threads to run multiple pieces of code at the same time
- *消息传递message-passing* 方面的并发,其中有着于线程间发送消息的一些通道;
- *状态共用shared-state* 方面的并发,其中多个线程均对某个数据加以访问;
- `Sync``Send` 特质,他们俩把 Rust 并发方面的保证,扩展到 Rust 使用者所定义的类型,以及由标准库所提供的那些类型。
## 运用线程来同步运行代码
End
**Using Threads to Run Code Simutaneously**
在绝大多数当前的操作系统中,被执行的程序代码,都是运行于 *进程a process* 中的,而所在的操作系统,则会同时管理多个进程。在程序内部,咱们同样可以有着同步运行的一些独立部分。运行这些独立部分的特性,便被称作 *线程threads*。比如web 服务器就可以有多个线程,如此他就可以在同一时间,响应多于一个的请求。
将咱们程序的运算拆分为多个线程来在同一时间运行多个任务可以提升性能但这样也增加了复杂度。由于线程能够同步运行因此在于不同线程上将要运行代码哪个部分的顺序方面就没有了某种固有保证because threads can run simultaneously, there's no inherent guarantee about the order in which parts of your code on different threads will run。这就会导致一些问题诸如
- 竞争局面,其中线程正以不一致顺序,访问着一些数据或资源;
- 死锁问题,其中两个线程正相互等待,而阻止了他们继续运行下去;
- 只在一些确切情形下才发生,而难于重现并可靠修复的代码错误。
Rust 试图消除这些运用线程方面的负面影响,但在多线程情景下的编程,仍要深思熟虑,并要求与运行在单线程下程序,截然不同的代码架构。
诸多编程语言,都是以少数几种不同途径,实现的线程,且多数操作系统,均提供了编程语言为可以创建出线程而调用的 API。Rust 标准库使用的是线程实现的 1:1 模型,由此程序就会以一个语言线程,对应使用一个操作系统线程。也有实现了别的线程操作模型的代码箱,对这种 1:1 模型做出了取舍。
### 使用 `spawn` 函数创建出一个新的线程
要创建出一个新的线程,咱们就要调用 `thread::spawn` 函数,并传递给他一个包含了打算在这个新线程中运行代码的闭包(在第 13 章中曾谈到过闭包)。下面清单 16-1 中的示例,会打印出来自主线程的一些文本,以及来自新线程的一些文本:
文件名:`src/main.rs`
```rust
use std::thread;
use std::time::Duration;
fn main() {
thread::spawn(|| {
for i in 1..10 {
println! ("\t- 你好,这是来自生成线程的数字 {} !", i);
thread::sleep(Duration::from_millis(20));
}
});
for i in 1..5 {
println! ("- 你好,这是来自主线程的数字 {} !", i);
thread::sleep(Duration::from_millis(20));
}
}
```
*清单 16-1创建出一个新线程来打印某物件与此同时主线程也在打印着其他东西*
请注意在 Rust 程序主线程完毕时,全部生成的线程就被关闭了,而不论他们是否已结束运行。该程序的输出每次都会有些许不同,但其看起来将如下所示:
```console
- 你好,这是来自主线程的数字 1 !
- 你好,这是来自生成线程的数字 1 !
- 你好,这是来自主线程的数字 2 !
- 你好,这是来自生成线程的数字 2 !
- 你好,这是来自主线程的数字 3 !
- 你好,这是来自生成线程的数字 3 !
- 你好,这是来自主线程的数字 4 !
- 你好,这是来自生成线程的数字 4 !
- 你好,这是来自生成线程的数字 5 !
```
`thread::sleep` 的调用,强制线程停止其执行短暂的时间,而允许别的线程运行。这些线程可能会轮流运行,但那并无保证:这取决于咱们的操作系统调度线程的方式。在此运行中,主线程就先行打印了,即便生成的线程中的打印语句,首先出现在代码中。而即便这里告诉了生成的线程,打印直到 `i``9` 的时候,但 `i` 在主线程关闭之前,仍只到了 `5`
若在运行此代码时,只看到主线程的输出,或未看到任何重叠部分,那么就要尝试增加其中那个范围(`1..10`, `1..5`)的数字,来给操作系统创造出,更多的与线程之间切换的机会。
### 使用 `join` 把手,等待全部线程结束
**Waiting for All Threads to Finish Using `join` Handles**
清单 16-1 中的代码,不仅会由于主线程的结束而提前停止生成线程,并因为在线程运行的顺序上没有保证,咱们还根本无法确保其中的生成线程将得到完整运行!
> **注**:在 `thred::sleep` 为 `1ms` 时,将偶发出现下面的运行结果:
```console
- 你好,这是来自主线程的数字 1 !
- 你好,这是来自生成线程的数字 1 !
- 你好,这是来自主线程的数字 2 !
- 你好,这是来自生成线程的数字 2 !
- 你好,这是来自生成线程的数字 3 !
- 你好,这是来自主线程的数字 3 !
- 你好,这是来自主线程的数字 4 !
- 你好,这是来自生成线程的数字 4 !
- 你好,这是来自生成线程的数字 %
```
咱们可以通过将 `thread::spawn` 的返回值,保存在一个变量中,来修复该生成线程不运行或提前结束的问题。`thread::spawn` 的返回值类型为 `JoinHandle`。而 `JoinHandle` 值则是一个自有值,在咱们于其上调用 `join` 方法时,他将等待其线程执行完毕。下面清单 16-2 就给出了怎样使用清单 16-1 中所创建出的那个 `JoinHandle`,来确保该生成线程在 `main` 退出之前执行完毕:
文件名:`src/main.rs`
```rust
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println! ("\t- 你好,这是来自生成线程的数字 {} !", i);
thread::sleep(Duration::from_millis(20));
}
});
for i in 1..5 {
println! ("- 你好,这是来自主线程的数字 {} !", i);
thread::sleep(Duration::from_millis(20));
}
handle.join().unwrap();
}
```
*清单 16-2保存一个来自 `thread::spawn` 的 `JoinHandle` 来确保该线程运行完毕*
> **注**:结合第 9 章中 [因错误而中止的快捷方式:`unwrap` 与 `expect`](Ch09_Error_Handling.md#因错误而中止的快捷方式unwrap-与-expect),表明 `join` 返回的是个 `Result<T, E>` 类型的枚举值。
在这个把手上调用 `join`,就会阻塞那个当前运行的线程,直到由该把手所表示的该线程终止。所谓 *阻塞blocking* 某个线程,是指那个线程被阻止执行工作或退出,*blocking* a thread means that thread is prevented from performing work or exiting。由于咱们已将到 `join` 的调用,放在了那个主线程的 `for` 循环之后,因此运行清单 16-2 中的代码,应产生出如下类似的输出(注:但每次运行的输出仍然不同):
```console
- 你好,这是来自主线程的数字 1 !
- 你好,这是来自生成线程的数字 1 !
- 你好,这是来自主线程的数字 2 !
- 你好,这是来自生成线程的数字 2 !
- 你好,这是来自主线程的数字 3 !
- 你好,这是来自生成线程的数字 3 !
- 你好,这是来自主线程的数字 4 !
- 你好,这是来自生成线程的数字 4 !
- 你好,这是来自生成线程的数字 5 !
- 你好,这是来自生成线程的数字 6 !
- 你好,这是来自生成线程的数字 7 !
- 你好,这是来自生成线程的数字 8 !
- 你好,这是来自生成线程的数字 9 !
```
两个线程依旧交替运行,但因为这个到 `handle.join()` 的调用,主线程就会等待,而在生成线程完毕之前不会结束。
不过来看看像下面这样,当咱们把 `handle.join()` 移至 `main` 中那个 `for` 循环前面时,会发生什么:
文件名:`src/main.rs`
```rust
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println! ("\t- 你好,这是来自生成线程的数字 {} !", i);
thread::sleep(Duration::from_millis(20));
}
});
handle.join().unwrap();
for i in 1..5 {
println! ("- 你好,这是来自主线程的数字 {} !", i);
thread::sleep(Duration::from_millis(20));
}
}
```
主线程将等待生成线程运行完毕,并于随后运行他的 `for` 循环,因此输出将不再交错,如下所示:
```console
- 你好,这是来自生成线程的数字 1 !
- 你好,这是来自生成线程的数字 2 !
- 你好,这是来自生成线程的数字 3 !
- 你好,这是来自生成线程的数字 4 !
- 你好,这是来自生成线程的数字 5 !
- 你好,这是来自生成线程的数字 6 !
- 你好,这是来自生成线程的数字 7 !
- 你好,这是来自生成线程的数字 8 !
- 你好,这是来自生成线程的数字 9 !
- 你好,这是来自主线程的数字 1 !
- 你好,这是来自主线程的数字 2 !
- 你好,这是来自主线程的数字 3 !
- 你好,这是来自主线程的数字 4 !
```
诸如 `join` 于何处被调用这样的细节,均会影响到咱们的线程,是否在同一时间运行。
### 在线程上使用 `move` 闭包
**Using `move` Closures with Threads**
由于传递给 `thread::spawn` 的闭包随后将取得其用到的环境中一些值的所有权,由此就会把这些值的所有权,从一个线程转移到另一线程,因此咱们今后将经常在这些闭包上,使用 `move` 关键字。在第 13 章 [“捕获引用或迁移所有权”](Ch13_Functional_Language_Features_Iterators_and_Closures.md#捕获引用抑或迁移所有权) 小节,咱们就曾讨论过闭包语境下的 `move` 关键字。现在,咱们将更多地着重于 `move``thread::spawn` 之间的互动。
请注意在清单 16-1 中,传递给 `thread::spawn` 的那个闭包没有取任何参数:咱们没有在生成线程中,使用主线程中的任何数据。为在生成线程中使用主线程中的数据,那么生成线程的闭包就必须捕获其所需的值。下面清单 16-3 给出了在主线程中创建出一个矢量值,并在生成线程中用到这个矢量值的一种尝试。然而,正如即将看到的那样,这将尚不会运作。
文件名:`src/main.rs`
```rust
use std::thread;
fn main() {
let v = vec! [1, 2, 3];
let handle = thread::spawn(|| {
println! ("这里有个矢量值:{:?}", v);
});
handle.join().unwrap();
}
```
*清单 16-3尝试在另一线程中使用由主线程创建出的一个矢量值*
这个闭包用到了 `v`,因此他将捕获 `v` 并将其构造为该闭包环境的一部分。由于 `thread::spawn` 是在一个新线程中运行此闭包,因此咱们应能够在那个新线程内部访问 `v`。然而在编译这个示例时,咱们会得到如下报错:
```console
cargo run lennyp@vm-manjaro
Compiling concur_demo v0.1.0 (/home/lennyp/rust-lang/concur_demo)
error[E0373]: closure may outlive the current function, but it borrows `v`, which is owned by the current function
--> src/main.rs:9:32
|
9 | let handle = thread::spawn(|| {
| ^^ may outlive borrowed value `v`
10 | println! ("这里有个矢量值:{:?}", v);
| - `v` is borrowed here
|
note: function requires argument type to outlive `'static`
--> src/main.rs:9:18
|
9 | let handle = thread::spawn(|| {
| __________________^
10 | | println! ("这里有个矢量值:{:?}", v);
11 | | });
| |______^
help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
|
9 | let handle = thread::spawn(move || {
| ++++
For more information about this error, try `rustc --explain E0373`.
error: could not compile `concur_demo` due to previous error
```
Rust *推断出了infers* 怎样去捕获 `v`,并由于 `println!` 值需要到 `v` 的一个引用,因此该闭包就尝试借用 `v`。然而这里有个问题Rust 无法识别出这个生成线程将运行多久,因此他就不清楚到 `v` 的引用是否将始终有效。
下面清单 16-4 提供了更倾向于有着到 `v` 的不将有效引用的一种场景:
文件名:`src/main.rs`
```rust
#![allow(dead_code)]
#![allow(unused_variables)]
use std::thread;
fn main() {
let v = vec! [1, 2, 3];
let handle = thread::spawn(|| {
println! ("这里有个矢量值:{:?}", v);
});
drop(v); // 噢,不要啊!
handle.join().unwrap();
}
```
*清单 16-4有着尝试从弃用了 `v` 的主线程捕获到 `v` 引用的闭包的一个线程*
若 Rust 运行咱们运行此代码那么就有可能在一点也没有运行那个生成线程下其就会被立即置于后台中if Rust allowed us to run this code, there's a possibility the spawned thread would be immediately put in the background without running at all。那个生成线程内部有着一个到 `v` 的引用,而主线程则使用第 15 章中曾讨论过的 `drop` 函数,立即弃用了 `v`。随后,在生成线程开始执行时,`v` 就不再有效了,一次到他的引用也失效了。噢,不要!
要修复清单 16-3 中的编译器错误,咱们可以使用错误消息中的建议:
```console
help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
|
9 | let handle = thread::spawn(move || {
| ++++
```
经由在那个闭包前添加 `move` 关键字,咱们就强制该闭包取得其用到值的所有权,而非让 Rust 来推断出他应借用该值。下面清单 16-5 给出的对清单 16-3 的修改,将如咱们设想的那样编译和运行:
文件名:`src/main.rs`
```rust
use std::thread;
fn main() {
let v = vec! [1, 2, 3];
let handle = thread::spawn(move || {
println! ("这里有个矢量值:{:?}", v);
});
handle.join().unwrap();
}
```
*清单 16-5使用 `move` 关键字来强制闭包取得他所用到值的所有权*
或许也会尝试以同样做法,通过使用 `move` 关键字,去修复清单 16-4 中,主线程调用了 `drop` 的代码。然而,由于清单 16-4 尝试完成的事情,因为一种不同原因而不被允许,那么这样的修复就不会凑效。在咱们把 `move` 添加到闭包时,咱们就会把 `v` 迁移到该闭包的环境中,进而咱们就无法再在主线程中,于其上调用 `drop` 了。这是会得到如下的编译器错误:
```console
$ cargo run lennyp@vm-manjaro
Compiling concur_demo v0.1.0 (/home/lennyp/rust-lang/concur_demo)
error[E0382]: use of moved value: `v`
--> src/main.rs:13:10
|
7 | let v = vec! [1, 2, 3];
| - move occurs because `v` has type `Vec<i32>`, which does not implement the `Copy` trait
8 |
9 | let handle = thread::spawn(move || {
| ------- value moved into closure here
10 | println! ("这里有个矢量值:{:?}", &v);
| - variable moved due to use in closure
...
13 | drop(v);
| ^ value used here after move
For more information about this error, try `rustc --explain E0382`.
error: could not compile `concur_demo` due to previous error
```
Rust 的所有权规则,再次挽救了咱们!由于 Rust 一直以来的保守,以及只为那个线程借用了 `v`,就意味着主线程理论上可以令到生成线程的引用失效,而得到了清单 16-3 中代码的报错。通过告知 Rust 将 `v` 的所有权迁移到生成线程,咱们就向 Rust 保证了主线程不会再使用 `v`。而若咱们以同样方式修改清单 16-4那么随后在咱们于主线程中尝试使用 `v` 时,就破坏了那些所有权规则。这个 `move` 关键字,覆盖了 Rust 借用方面的保守做法;但他并无让咱们破坏所有权规则。
有了线程及线程 API 方面的基本认识,接下来就有看看用线程可以 *do* 些什么。
## 使用消息传递来在线程间传输数据
**Using Message Passing to Transfer Data Between Threads**
一种日渐流行的确保并发安全的方法,便是 *消息传递message passing*,其中线程或参与者,通过相互发送包含数据的消息进行通信。下面就是摘自 [Go 语言文档](https://golang.org/doc/effective_go.html#concurrency) 的一句口号:“勿要通过共用内存进行通信;而要经由通信来共用内存。”
为达成消息发送式的并发Rust 标准库提供了 *信道channels* 的一种实现。所谓信道,即数据被从一个线程,发送到另一线程,这种形式的一个通用编程概念。
咱们可以把编程中的信道,设想为带流向的水渠,如同一条小溪或一条河。在咱们把像是一只塑胶小黄鸭投入到一条小河中时,他就会顺流而下到达该水路的尽头。
信道有着两端:一个发送者和一个接收者。发送端即在将小黄鸭投入到河流中的上游位置,而接收端即为小黄鸭抵达的下游了。咱们代码中一个部分以打算发送的数据,调用发送者上的方法,而另一个部分则会查看接收端的抵达消息。在发射者或接收者端之一被弃用时,就算是信道被 *关闭closed* 了。
下面,咱们将完成有着一个线程生成一些值并将这些值发送到信道,同时有另一线程将接收这些值并将其打印出来的这么一个程序。咱们将在线程间使用信道发送一些简单值,来演示这项特性。一旦咱们熟悉了这项技巧,那么就可以对任何需要相互通讯的线程,比如聊天系统,或其中有许多线程执行着某项计算的各个部分,并把这些部分发送到结果汇总线程的系统等中使用信道。
首先,在下面的清单 16-6 中,咱们将创建出一个信道而不使用他来完成任何事情。请注意由于 Rust 无法分辨出咱们打算通过该信道,发送何种类型的值,该代码尚不会编译。
文件名:`src/main.rs`
```rust
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
}
```
*清单 16-6创建出一个信道并将两端赋值给 `tx` 与 `rx`*
咱们使用 `mpsc::channel` 函数,创建了一个新的信道;`mpsc` 表示的是 *多生产者单一消费者multiple producer, single consumer*。简而言之Rust 标准库实现信道的方式,表明信道可以有多个生成值的 *发送sending* 端,但只有一个消费这些值的 *接收receiving* 端。请设想有多条小溪,汇流到一条大河:那么从任何这些小溪,送下来的东西,都将在最后抵达一条河中。现在咱们将以单个的生产者开始,在令到这个示例工作起来时,就将添加多个生产者。
这个 `mpsc::channel` 函数返回的是一个元组,元组的首个元素为发送端 -- 发送器the transmitter -- 而第二个元素就是接收端 -- 接收器the receiver。两个缩写 `tx``rx`,传统上在许多领域,都相应地被用于表示 *transmitter**receiver*因此咱们就把这两个变量如此命名来表示两端。咱们使用了带有模式a pattern `(tx, rx)`)的一个 `let` 语句,对这个元组加以解构;在第 18 章中,咱们将讨论这种 `let` 遇见中模式的运用及解构问题。至于现在,请明白以这种方式使用 `let` 语句,是提取由 `mpsc::channel` 返回元组中那些部分的便捷方式。
下面就把其中的发射端,移入到一个生成线程,并让其发送一个字符串,从而生成线程就在与主线程通信了,如下清单 16-7 中所示。这就像是在河流上游投入一只小黄鸭,或是在一个线程发送了一条聊天消息给另一线程。
文件名:`src/main.rs`
```rust
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("你好");
tx.send(val).unwrap();
});
}
```
*清单 16-7把 `tx` 迁移到一个生成线程并发送 "你好"*
又一次,咱们使用了 `thread::spawn` 创建出一个新线程,并使用 `move``tx` 迁移进到那个闭包,于是这个生成的线程,便拥有了 `tx`。该生成线程需要拥有发送器,才能够经由信道发送消息。发送器有着取咱们打算发送值的一个 `send` 方法。而这个 `send` 方法返回的是个 `Result<T, E>` 类型值,那么在接收器已被弃用,而无处发送值时,那么发送操作就将返回一个错误。在此示例中,咱们调用了 `unwrap` 来在出现错误时终止运行panic in case of an error。而在真实应用中咱们应予以恰当处理请回到第 9 章,回顾那些那些适当的错误处理策略。
在下面的清单 16-8 中,咱们将自主线程中的接收器,获取到那个值。这就像是在河流尽头接收到小黄鸭,或是接收到一条聊天消息。
文件名:`src/main.rs`
```rust
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("你好");
tx.send(val).unwrap();
});
let received = rx.recv().unwrap();
println! ("收到:{}", received);
}
```
*清单 16-8在主进程中接收值 “你好” 并将其打印出来*
接收器有着两个有用方法:`recv``try_recv`。这里使用的是 `recv`,是 *receive* 的简写,该方法将阻塞主线程执行,而等待直到某个值被下发到信道。一旦某个值被发出,那么 `recv` 就会在一个 `Result<T, E>` 中将其返回。在发射器关闭时,`recv` 就会返回一个错误,表明不会再有值到来。
`try_recv` 方法则不会阻塞,而相反会立即返回一个 `Result<T, E>`:在消息可用时的一个保存着消息的 `Ok` 值,同时此刻没有任何消息时的一个 `Err` 值。在该线程在等待消息时,有其他工作要完成的情况下,使用 `try_recv` 便是有用的:咱们可以编写出每隔一段时间就调用 `try_recv` 的循环,在有消息时处理消息,再次检查是否收到消息之前的空隙,完成一些其他工作。
这里使用 `recv` 是为了简化;在主线程中,除了等待消息之外并无其他工作要做,因此阻塞主线程是恰当的。
当咱们运行清单 16-8 中的代码时,就会看到该值在主线程中被打印出来:
```console
收到:你好
```
好极了!
### 信道与所有权的转移
**Channels and Ownship Transference**
由于所有权规则帮助咱们编写出安全、并行的代码,因此其在消息发送中起着至关重要作用。在并发式编程中,于咱们 Rust 程序通篇考虑所有权的好处,就在于这样可以防止错误。接下来就要完成一项实验,来展示信道与所有权,是怎样一起运作以阻止问题发生的:咱们将在生成线程中,把一个 `val` 值送到信道 *之后after*,再尝试使用这个值。尝试编译清单 16-9 中的代码,来观察为何该代码是不被允许的:
文件名:`src/main.rs`
```rust
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("你好");
tx.send(val).unwrap();
println! ("val 为 {}", val);
});
let received = rx.recv().unwrap();
println! ("收到:{}", received);
}
```
*清单 16-9在咱们已将 `val` 发送到信道之后在尝试使用他*
在这里,咱们在经由 `tx.send` 已把 `val` 发出到信道之后,尝试打印出他。允许这样做将是个糟糕的主意:一旦该值已被发送到另一线程,那么在咱们尝试再度使用该值之前,发往的那个线程就可能修改或是弃用掉该值。而潜在地,另一线程的这些改动,就会由于不一致或不存在的数据,而造成错误或未预期结果。不过,在咱们尝试编译清单 16-9 中的代码时Rust 会给到咱们一个报错:
```console
$ cargo run  ✔  
Compiling mp_demo v0.1.0 (/home/peng/rust-lang/mp_demo)
error[E0382]: borrow of moved value: `val`
--> src/main.rs:13:31
|
11 | let val = String::from("你好");
| --- move occurs because `val` has type `String`, which does not implement the `Copy` trait
12 | tx.send(val).unwrap();
| --- value moved here
13 | println! ("val 为 {}", val);
| ^^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0382`.
error: could not compile `mp_demo` due to previous error
```
咱们犯下的并发错误就造成一个编译时报错。其中的 `send` 函数取得了其参数的所有权,进而在那个值被迁移时,接收器便取得了他的所有权。这就阻拦了咱们在发送了该值后,无意中地再度使用该值;所有权系统会检查各方面都妥当无虞。
### 发送出多个值并观察接收器的等待
**Sending Multiple Values and Seeing the Receiver Waiting**
清单 16-8 中的代码编译并运行了,不过其并未清楚地给出,两个单独线程是怎样通过信道相互交流的。在下面清单 16-10 中,咱们已做出将证实清单 16-8 中代码有在并发运行的一些修订:生成线程现在将发出多条消息,并在每条消息之间暂停一秒钟。
文件名:`src/main.rs`
```rust
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let vals = vec! [
String::from("你好"),
String::from(""),
String::from(""),
String::from("线程"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_millis(500));
}
});
for received in rx {
println! ("收到:{}", received);
}
}
```
*清单 16-10发送多条消息并在每次发送之间进行暂停*
这次,其中的生成线程,有着咱们打算发送到主线程的一个字符串矢量值。咱们对其迭代,而分别发送每条消息,同时通过使用 `500` 毫秒的一个 `Duration` 值,调用 `thread::sleep` 在每次消息发送间暂停。
在主线程中,咱们未再显式调用 `recv` 函数:相反,咱们将 `rx` 当作了迭代器。对于所接收到的每个值,咱们就将其打印出来。在信道被关闭时,迭代就结束了。
在运行清单 16-10 中的代码时,咱们就会看到下面每行之间有着 500ms 暂停的输出:
```console
收到:你好
收到:自
收到:此
收到:线程
```
由于咱们在主线程中的那个 `for` 循环中,并无任何暂停或延迟的代码,因此咱们就可以说,主线程是在等待接收来自生成线程的那些值。
### 通过克隆发射器创建出多个生产者
**Creating Multiple Producers by Cloning the Transmitter**
早前咱们曾提到,`mpsc`*multiple producer, single consumer* 的首字母缩写。接下来就要就要运用上 `mpsc`,并将清单 16-10 中的代码,扩充为创建出均将一些值发送到同一接收器的多个线程。通过克隆发射器,咱们就可以这样做,如下清单 16-11 中所示:
文件名:`src/main.rs`
```rust
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
let tx1 = tx.clone();
thread::spawn(move || {
let vals = vec! [
String::from("你好"),
String::from(""),
String::from(""),
String::from("线程"),
];
for val in vals {
tx1.send(val).unwrap();
thread::sleep(Duration::from_millis(500));
}
});
thread::spawn(move || {
let vals = vec! [
String::from(""),
String::from(""),
String::from("一些别的"),
String::from("消息"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_millis(500));
}
});
for received in rx {
println! ("收到:{}", received);
}
}
```
*清单 16-11从多个生产者发出多条消息*
这次在创建出首个生成线程之前,咱们调用了发射器上的 `clone` 方法。这样做将给到咱们可传递给那首个生成线程的一个新发射器。咱们把原先的发射器,传递给了第二个生成线程。这样就给到了咱们两个线程,二者都把不同消息,发送到那一个的接收器。
在运行此代码时,咱们的输出看起来应像下面这样:
```console
收到:你好
收到:给
收到:自
收到:你
收到:此
收到:一些别的
收到:线程
收到:消息
```
根据咱们所在系统的不同,也可能会看到另外顺序的这些值。这种消息每次出现顺序的不一致,正是令到并发有趣而又有难度的地方。而若带上 `thread::sleep` 加以实验,即在两个不同线程中给到不同睡眠值,这时的每次运行,将更具不确定性,而每次运行都造成不同输出。
既然咱们已经看到了信道的工作原理,那么接下来就要看看一种方式迥异的并发了。
## 状态共用的并发
**Shared-State Concurrency**
消息传递是处理并发的一种很好方式,但其并非唯一的一种。另一种方式将是,多个线程访问同一共用数据。请重新考虑一下摘自 Go 语言文档的那句口号的这个部分“勿要经由共用内存通信。do not communicate by sharing memory.”
那么经由共用内存的通信,又会是怎样的呢?另外,为何消息传递方式拥趸们,会警告不要使用内存共用方式呢?
在某种程度上,任何编程语言中的信道,均类似于单一所有权,因为一旦咱们把值传递到信道,那么就不应再使用那个值了。内存共用的并发,则像是多重所有权:多个线程均可在同一时间,访问同一内存位置。正如咱们在第 15 章中曾见到过的那里的灵巧之中令到多重所有权可行多重所有权会因为这些不同所有者需要管理而增加复杂度。Rust 的类型系统与所有权规则极大地助力了实现这样的管理正确无误。作为一个示例接下来咱们就要看看作为共用内存的一种更常见并发原语即所谓的互斥量for an example, let's look at mutexes, one of the more common concurrency primitives for shared memory。
### 运用互斥量实现一个时间仅允许一个线程访问数据
**Using Mutexes to Allow Access to Data from One Thread at a Time**
*互斥mutex**相互排斥mutual exclusion* 的缩写,正如互斥量在任何给定时间,都只允许一个线程访问某个数据。要访问互斥量中的数据,线程就必须首先通过询问来获取到该互斥量的 *lock*表明其打算访问。所谓锁则是保持着当前是谁哪个线程有着对该数据排他性访问的追踪作为该互斥量一部分的一种数据结构the lock is a data structure that is part of the mutex that keeps track of who currently has exclusive access to the data。因此所谓互斥量就被描述为经由这种加锁系统*守护着guarding* 其所保存着的数据。
由于咱们务必要记住以下两条规则,互斥量便有了难以运用的名声:
- 在使用数据之前,咱们必须尝试获取到锁;
- 在完成互斥量所保护数据的操作时,咱们必须解开该数据,以便其他线程能够获取到锁。
至于互斥量的真实世界比喻,请设想在仅有一只麦克风的会议上的一个小组讨论。那么在小组成员能发言之前,他们就不得不请求或表明,他们打算使用麦克风。在他们得到麦克风时,他们便可以想要讲多长时间便讲多长时间,并在随后吧麦克风,递给下一位要求发言的小组成员。在某名小组成员于用完麦克风,却忘记交出麦克风时,就没有人能发言了。在这个共用麦克风的管理出错时,这个小组就将不会如计划那样运作了!
互斥量的管理非常棘手,难以做到正确无误,这正是许多人热衷于信道的原因。但是,归功于 Rust 的类型系统与所有权规则,咱们就无法在互斥量的加锁与解锁上出错了。
### `Mutex<T>` 的 API
下面是如何使用互斥量的一个示例,接下来咱们就要如下面清单 16-12 中所给出的那样,通过在单一线程情形下使用互斥量开始:
文件名:`src/main.rs`
```rust
use std::sync::Mutex;
fn main() {
let m = Mutex::new(5);
{
let mut num = m.lock().unwrap();
*num = 6;
}
println! ("m = {:?}", m);
}
```
*清单 16-12为简化目的在单个线程情形下探讨 `Mutex<T>` 的 API*
与许多类型一样,咱们使用关联函数 `new` 创建出了一个 `Mutex<T>`。而为了访问这个互斥量内部的数据,咱们使用了 `lock` 方法来获取到锁。此调用将阻塞当前线程,从而在轮到咱们拥有锁之前,当前线程就无法完成任何工作。
若有另一持有着锁的线程已终止运行,那么到 `lock` 的调用就会失败。在那种情况下,就没人能获得锁了,因此咱们就选择了 `unwrap`,而在咱们陷入到那样的情形时,让这个线程终止运行。
在获取到锁后,咱们就可以对此示例中名为 `num` 的返回值,作为到互斥量内部数据的可变引用,而加以处理了。类型系统会确保咱们在使用 `m` 里的值前,获取到锁。`m` 的类型为 `Mutex<i32>`,而非 `i32`,因此咱们为了使用那个 `i32` 值, 就 *必须* 调用 `lock`。这是不能忘掉的;否则类型系统就不会让咱们访问那个内层的 `i32`
正如咱们可能怀疑的那样,`Mutex<T>` 是个灵巧指针。更准确地讲,到 `lock` 的调用,*返回的是* 封装在咱们曾以到 `unwrap` 调用处理的 `LockResult` 中,一个叫做 `MutexGuard` 的灵巧指针。`MutexGuard` 灵巧之中实现了 `Deref`,来指向咱们的内层数据;这个灵巧指针还有着在 `MutexGuard` 超出作用域,即清单 16-12 的示例内存作用域结束处所发生时,自动释放锁的一个 `Drop` 实现。而其结果就是,由于锁的释放是自动发生的,因此咱们就不会面临,忘记释放锁而阻塞该互斥量为其他线程使用的风险。
在弃用了该所之后,咱们就可以打印出该互斥量的值,并看到咱们是能够把那个内层的 `i32`,修改为 `6` 的。
### 在多个线程间共用 `Mutex<T>`
现在,咱们就来尝试使用 `Mutex<T>`,在多个线程见共用值。咱们将启动 10 个线程,并让他们分别都把一个计数器增加 `1`,因此那个计数器就会从 `0` 到达 `10`。接下来清单 16-13 中的示例,将有着一个编译器报错,同时咱们将使用那个报错,来掌握更多有关使用 `Mutex<T>`,以及 Rust 如何帮助咱们正确运用他的知识。
文件名:`src/main.rs`
```rust
use std::sync::Mutex;
use std::thread;
fn main() {
let counter = Mutex::new(0);
let mut handles = vec! [];
for _ in 0..10 {
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println! ("结果为:{}", *counter.lock().unwrap());
}
```
*清单 16-13各自分别对由 `Mutex<T>` 所守护计数器递增的十个线程*
与在清单 16-12 中一样,咱们创建出了一个在 `Mutex<T>` 内,保存着一个 `i32``counter` 变量。接下来,咱们通过对数字范围的迭代,创建出了 10 个线程。咱们使用了 `thread::spawn`,并给到全部线程同样闭包:把那个计数器迁移进到线程,通过调用 `lock` 方法取得那个 `Mutex<T>` 上的锁,并于随后加 `1` 到该互斥量的值的这样一个闭包。在线程完成运行其闭包时,`num` 就会超出作用域而释放那把锁,从而另一线程便可以取得该锁。
在主线程中咱们收集起了所有连接把手collect all the join handles。随后如同在清单 16-2 中所做的那样,咱们在各个把手上调用了 `join`,来确保所有现场运行完毕。在那个点位处,主线程将取得那把锁,并打印出该程序的结果。
咱们曾暗示过此示例不会编译。现在就来找出原因为何!
```console
$ cargo run  ✔  
Compiling mutex_demo v0.1.0 (/home/peng/rust-lang/mutex_demo)
error[E0382]: use of moved value: `counter`
--> src/main.rs:12:36
|
8 | let counter = Mutex::new(0);
| ------- move occurs because `counter` has type `Mutex<i32>`, which does not implement the `Copy` trait
...
12 | let handle = thread::spawn(move || {
| ^^^^^^^ value moved into closure here, in previous iteration of loop
13 | let mut num = counter.lock().unwrap();
| ------- use occurs due to use in closure
For more information about this error, try `rustc --explain E0382`.
error: could not compile `mutex_demo` due to previous error
```
这个报错消息指出,其中的 `counter` 值在循环的上一次迭代中已被迁移。Rust 正告诉咱们,不能将锁 `counter` 的所有权,迁移进到多个线程中。下面就来使用第 15 张中曾讨论过的多重所有权方式,修正这个编译器报错。
### 多线程下的多重所有权
**Multiple Ownership with Multiple Threads**
在第 15 章中,咱们曾通过使用灵巧指针 `Rc<T>`,来创建出一个引用计数的值,而将一个值赋予到多个所有者。下面就来完成那同样的操作,并看到会发生什么。咱们将在清单 16-14 中,把那个 `Mutex<T>` 封装在 `Rc<T>` 中,并在把所有权迁移到线程之前,克隆这个 `Rc<T>`
文件名:`src/main.rs`
```rust
use std::rc::Rc;
use std::sync::Mutex;
use std::thread;
fn main() {
let counter = Rc::new(Mutex::new(0));
let mut handles = vec! [];
for _ in 0..10 {
let counter = Rc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println! ("结果为:{}", *counter.lock().unwrap());
}
```
*清单 16-14尝试使用 `Rc<T>` 来实现多个线程拥有那个 `Mutex<T>`*
又一次,咱们编译并得到......一些不同的报错!编译器给了咱们很多指教。
```console
$ cargo run  ✔  
Compiling mutex_demo v0.1.0 (/home/peng/rust-lang/mutex_demo)
error[E0277]: `Rc<Mutex<i32>>` cannot be sent between threads safely
--> src/main.rs:14:36
|
14 | let handle = thread::spawn(move || {
| ------------- ^------
| | |
| ______________________|_____________within this `[closure@src/main.rs:14:36: 14:43]`
| | |
| | required by a bound introduced by this call
15 | | let mut num = counter.lock().unwrap();
16 | |
17 | | *num += 1;
18 | | });
| |_________^ `Rc<Mutex<i32>>` cannot be sent between threads safely
|
= help: within `[closure@src/main.rs:14:36: 14:43]`, the trait `Send` is not implemented for `Rc<Mutex<i32>>`
note: required because it's used within this closure
--> src/main.rs:14:36
|
14 | let handle = thread::spawn(move || {
| ^^^^^^^
note: required by a bound in `spawn`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `mutex_demo` due to previous error
```
喔,那报错消息真的非常罗嗦!而这些才是要关注的重要部分:`` `Rc<Mutex<i32>>` cannot be sent between threads safely ``。编译器还告诉咱们了其原因:`` the trait `Send` is not implemented for `Rc<Mutex<i32>>` ``。下一小节咱们就要讲到 `Send` 特质:他是确保咱们用到类型,是意图用于并发情形的特质之一。
不幸的是,`Rc<T>` 于跨线程的共用上是不安全的。在 `Rc<T>` 管理着引用计数时,他会增加每次到 `clone` 调用的计数并在每个克隆被弃用时减去计数。但其并未使用任何并发原语any concurrency primitives来确保那些对该计数的改变不被另一线程中断。这就会导致错误的计数 -- 进而会导致内存泄漏,或在咱们未完成值处理之前,该值就已被启用这样的一些微妙代码缺陷。咱们所需要的,是像极了 `Rc<T>`,但会令到引用计数以线程安全方式得以改变的一种类型。
> **注**简单地说与各种编程语言中的那些原生数据类型primitive data types 一样所谓并发原语concurrency primitives指的就是用于并发编程的一些基本设施the basic facilities for concurrent programming这样的说法某种程度上是跨越某个语言家族比如 C 语言家族)。
>
> 参考:[What-are-concurrency-primitives-"K Symbol"](https://qr.ae/prtpz6)
### `Arc<T>` 下的原子引用计数
**Atomic Reference Counting with `Arc<T>`**
幸运的是,`Arc<T>` *正是* 安全用于并发情形下的一个像是 `Rc<T>` 的类型。其中的 `a` 代表着 `原子atomic`,表示其是一种 *原子的引用计数atomically reference counted* 类型。原子类型是咱们不会在此详细讨论的一类额外并发原生类型:请参阅 [`std::sync::atomic` 的标准库文档](https://doc.rust-lang.org/std/sync/atomic/index.html),了解更多细节。此刻,咱们只需要知道这些原子类型会像那些原生类型一样运作,只不过他们对于跨线程的共用是安全的。
到这里咱们可能想知道,为何全部原生类型不是原子的,以及为何标准库的那些类型,没有默认使用 `Arc<T>` 实现。原因就是线程安全自带了性能损失,而只有在咱们真的需要线程安全时,才会打算付出。在咱们只是在单线程里于一些值上执行操作时,若咱们的代码不必强制实现原子类型所提供的那些保证,那么这些代码就可以运行得快得多。
接下来回到那个示例:`Arc<T>` 与 `Rc<T>` 有着同样的 API因此通过修改其中的 `use` 语句行、到 `new` 的调用,以及到 `clone` 的调用,咱们就可以修复那个程序。清单 16-15 中的代码最终将会编译及运行:
文件名:`src/main.rs`
```rust
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec! [];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println! ("结果为:{}", *counter.lock().unwrap());
}
```
*清单 16-15为能够跨越多线程地共用所有权而使用 `Arc<T>` 来封装那个 `Mutex<T>`*
此代码将打印出下面的内容:
```console
结果为10
```
咱们就做到了!咱们从 `0` 计数到了 `10`,这或许看起来不是非常印象深刻,但他真的教会了咱们很多有关 `Mutex<T>` 与线程安全的东西。咱们也可以运用这个程序的架构,完成相比于增加计数器,一些更为复杂的操作。运用这种策略,咱们可把某项计算,划分为一些独立部分,将这些部分拆解为多个线程,并于随后使用 `Mutex<T>` 来让各个各个线程,使用其自己部分对最终结果加以更新。
请注意若咱们是在完成一些简单的数字运算,你们就有由 [标准库的 `std::sync::atomic` 模组](https://doc.rust-lang.org/std/sync/atomic/index.html) 所提供的,相较于 `Mutex<T>` 更简单的一些类型。这些类型提供到原生类型安全、并发、原子的访问。咱们为这个示例而选择带有原生类型的 `Mutex<T>`,目的是可以着重于 `Mutex<T>` 的工作原理。
### `RefCell<T>`/`Rc<T>` 与 `Mutex<T>`/`Arc<T>` 二者之间的相似点
**Similarities Between `RefCell<T>`/`Rc<T>` and `Mutex<T>`/`Arc<T>`**
咱们或许已经留意到,其中那个 `counter` 是不可变的,但咱们却能获取到其内部值的可变引用;这意味着与 `Cell` 家族the `Cell` family 所做的一样, `Mutex<T>` 提供了内部可变性。与咱们在第 15 章中曾使用 `RefCell<T>` 来实现修改 `Rc<T>` 内部内容同样的方式,咱们使用了 `Mutex<T>` 来修改 `Arc<T>` 内部内容。
另一个需要注意的细节,便是在咱们使用 `Mutex<T>` 时Rust 无法保护咱们免于全部类别的逻辑错误。回顾在第 15 章中,`Rc<T>` 运用就伴随着创建出循环引用风险,其中两个 `Rc<T>` 值相互指向,导致内存泄漏。与此类似,`Mutex<T>` 则附带着创建出 *死锁deadlocks* 的风险。在某个操作需要锁住两项资源,同时两个线程分别均已请求获取两把锁中的一把时,就造成他们一直等待着对方释放各自所需的锁。若对死锁方面感兴趣,那么请尝试创建出有着死锁的一个 Rust 程序;随后就要研究任何一门语言中,互斥量的死锁消除策略,并试试在 Rust 中实现这些策略。`Mutex<T>` 和 `MutexGuard` 的标准库 API 文档,就提供了一些有用信息。
咱们将通过讲解 `Send` 与 `Sync` 两个特质,以及怎样与一些定制类型来运用他们来完结本章。
## `Sync` 与 `Send` 两个特质下的可扩展并发
**Extensible Concurrency with the `Sync` and `Send` Traits**
有趣的是Rust 语言并发方面的特性 *非常* 少。本章中到目前为止咱们讲到过的每种并发特性,都已是标准库而非语言本身的一部分。用于处理并发问题的选项,并不局限于这门语言或标准库;咱们可以编写自己的并发特性,或可以使用由其他人编写的并发特性。
不过,在这门语言中,是嵌入了两个并发概念的:即 `std::marker` 特质 `Sync` 与 `Send`。
### 使用 `Send` 特质实现线程间所有权转移
**Allowing Transference of Ownership Between Threads with `Send`**
这个 `Send` 标识符特质,表示实现 `Send` 类型值的所有权,可以在线程间转移。几乎全部 Rust 类型都是 `Send` 类型,但有一些例外,包括 `Rc<T>`:由于在咱们克隆了某个 `Rc<T>` 并尝试将这份克隆的所有权,转移到另一线程时,两个现场可能在同一时间更新引用计数,因此 `Rc<T>` 就不能是 `Send` 类型。由于这个原因,`Rc<T>` 正是为其间咱们不打算付出线程安全方面性能开销的那些单线程情形,而实现的。
由此Rust 的类型系统与特质边界type system and trait bounds就确保了咱们绝不会意外地将某个 `Rc<T>`,不安全地跨越线程发送。当咱们在清单 16-14 中尝试这样做时,咱们就曾得到编译器报错 `` the trait `Send` is not implemented for `Rc<Mutex<i32>>` ``。而在咱们切换到 `Arc<T>` 这种 `Send` 类型时,那段代码就编译了。
由全部 `Send` 类型所组成的类型,也会被自动标记为 `Send` 类型。除开那些原始指针raw pointers 外,那么可以说几乎全部原生类型都是 `Send` 的,咱们将在第 19 章中,讲到那些原始指针。
### 使用 `Sync` 实现来自多个线程的访问
**Allowing Access from Multiple Threads with `Sync`**
`Sync` 标识符表示实现 `Sync` 特质的类型,其被从多个线程引用是安全的。换句话说,任何类型 `T` 在 `&T` (即到 `T` 的不可变引用) 为 `Send` 的时,那么其即为 `Sync` 的,表示该引用可以安全地发送到另一线程。与 `Send` 类似,原生类型均为 `Sync` 的,且由全部都是 `Sync` 的类型所组成的类型,也都是 `Sync` 的。
灵巧指针 `Rc<T>` 因为其不是 `Send` 的同样原因,其也不是 `Sync` 的。`RefCell<T>` 类型(咱们曾在第 15 章讲过)以及相关的 `Cell<T>` 类型家族,都不是 `Sync` 的。`RefCell<T>` 在运行时所完成的借用检查实现,不是线程安全的。灵巧指针 `Mutex<T>` 是 `Sync` 的,并正如咱们在 [于多个线程间共用 `Mutex<T>`](#在多个线程间共用-mutext) 小节中看到的,其可被用于多个线程下共用访问。
### 手动实现 `Send` 与 `Sync` 是不安全的
**Implementing `Send` and `Sync` Manually Is Unsafe**
由于 `Send` 与 `Sync` 特质构成的类型自动也是 `Send` 与 `Sync` 的,因此咱们大可不必手动实现这两个特质。而作为标记性特质,二者甚至都没有任何要实现的方法。他们只是在执行与并发性有关的不变性方面很有用。
手动实现这两个特质,涉及到实现一些不安全 Rust 代码unsafe Rust code。在第 19 章咱们将讲到运用不安全 Rust 代码;至于现在,要点在于构造不是由一些 `Send` 与 `Sync` 部分组成的新并发类型,需要深思熟虑来维持那些安全保证。[The Rustonomicon](https://doc.rust-lang.org/nomicon/index.html) 有着这些保证的更多信息,以及维持这些保证的方式。
## 本章小节
这不会是你在本书中将见到并发的最后一章:第 20 张中的那个项目,就将在相比于这里所讨论过较小示例,而更具现实意义的情形下用到本章中的那些概念。
正如早先所提到的,由于只有极少量的 Rust 处理并发方式,属于这门语言的一部分,因此许多并发解决方案,都是作为代码箱实现的。这些方案相比标准库进化更为迅速,那么就要确保在线搜寻当前的、最前沿代码箱,来用于多线程情形中。
Rust 标准库提供了用于消息传递的信道,以及诸如 `Mutex<T>` 与 `Arc<T>` 等安全用于并发情景中的一些灵巧指针类型。类型系统与借用检查器,会确保应用了这些方案的代码,不会以数据竞争或无效引用结束。一旦让代码编译了,咱们就可以放下心来,代码将愉快地运行于多线程之上,而不会有在其他语言中常见的那些难于追踪的问题。并发编程自此不再是令人害怕的概念:去吧,让你的程序并发起来,无所畏惧!
接下来,咱们将讲到,随着咱们的 Rust 程序变得大了起来,建模问题与架构出方案的一些管用做法。此外,咱们将讨论 Rust 的一些习惯说法,这些说法可能与面向对象编程中所熟悉的有关。

View File

@@ -5,863 +5,6 @@
面向对象编程方法object-oriented programming, OOP, 是建模程序的一种方法。对象是在 20 世纪 60 年代,在编程语言 Simula 中所引入的一个程序化概念。正是那些对象,影响了 Alan Kay 的编程架构,其中对象会相互传递消息。为描述这种架构,他在 1967 年创造了面向对象编程这个术语。有许多互相竞争的定义,都描述了 OOP 是什么而根据其中一些定义Rust 属于面向对象的但根据另一些Rust 则不属于面向对象的。在本章中,咱们将探讨通常被看作是面向对象的一些特征,以及这些特征怎样被转译为 Rust 的习惯说法。随后咱们将给出在 Rust 怎样实现面向对象的设计模式,并讨论在这样做,与相反采用 Rust 的一些长处来实现解决方案,之间的权衡取舍。
## 面向对象语言的特征
End
**Characteristics of Object-Oriented Languages**
在编程界并无关于某门被视为面向对象的而必须具有哪些特性的共识。Rust 受了许多编程范式programming paradigms的影响其中就包括 OOP比如在第 13 章中,咱们就曾探讨过,那些来自于函数式编程的特性。可以说,那些 OOP 的语言,确实是共用了一些确切的特征的,那即是对象、封装与继承等。下面就来看看这些特征各自指的是什么,以及 Rust 是否支持他们。
### 对象包含了数据及行为
**Objects Contain Data and Behavior**
Erich Gamma、Richard Helm、Ralph Johnson 及 John Vlissides 等的合著 *Design Patterns: Elements of Reusable Object-Oriented Software* Addison-Wesley Professional, 1994又被通俗地叫做 *The Gang of Four* 书,便是面向对象设计模式的一个目录。该书像下面这样定义了 OOP
> 面向对象程序是由对象所组成的。*对象an object* 同时打包了数据与运行在那数据上的过程。这些过程一般就叫做 *方法methods* 或 *操作operations*。
运用这个定义Rust 便是面向对象的:结构体与枚举均有着数据,而 `impl` 块则提供了结构体与枚举上的那些方法。即使有着方法的那些结构体与枚举未*被称作* 对象,根据 The Gang of Four 的对象定义,他们提供了同样的功能。
### 隐藏了实现细节的封装
**Encapsulation that Hides Implementation Details**
通常与 OOP 相关的另一方面的 *封装encapsulation*,是指对于用到该对象的代码,对象实现细节是不可访问的。由此,与对象交互的唯一方式,便是经由该对象的公开 API运用对象的代码不应具备到达该对象内部而直接改变数据或行为的能力。这实现了程序员在无需修改用到对象的那些代码之下修改或重构对象的那些内部代码。
在第 7 章中,咱们曾讨论过怎样控制封装:咱们可以使用 `pub` 关键字,来决定咱们代码中,哪些模组、类型、函数与方法等应为公开的,而默认其他所有项目都是私有的。比如,咱们就可以定义有着包含 `i32` 值矢量的一个字段的 `AveragedCollection` 结构体。这个字段也可以有包含着那个矢量中值的平均数的一个字段,表示在有人需要该平均值时,不必按需计算出该平均值。换句话说,`AveragedCollection` 将为咱们缓存这个计算出的平均值。下面清单 17-1 便有着这个 `AveragedCollection` 结构体的定义:
文件名:`src/lib.rs`
```rust
pub struct AveragedCollection {
list: Vec<i32>,
average: f64,
}
```
*清单 17-1维护着一个整数清单及该集合中项目平均数的 `AveragedCollection` 结构体*
该结构体被标记为 `pub`,从而其他代码就可以使用他,而该结构体内部的那些字段保持着私有。由于咱们打算不管何时在有某个值被添加到清单,或从清单移除时,其中的平均数也要同时被更新,因此在这个示例中这样的封装就很重要。咱们是通过实现下面清单 17-2 中所给出的 `add``remove``average` 方法,做到这一点的。
文件名:`src/lib.rs`
```rust
impl AveragedCollection {
pub fn add(&mut self, value: i32) {
self.list.push(value);
self.update_average();
}
pub fn remove(&mut self) -> Option<i32> {
let result = self.list.pop();
match result {
Some(value) => {
self.update_average();
Some(value)
}
None => None,
}
}
pub fn average(&self) -> f64 {
self.average
}
fn update_average(&mut self) {
let total: i32 = self.list.iter().sum();
self.average = total as f64 / self.list.len() as f64;
}
}
```
*清单 17-2`AveragedCollection` 上公开方法 `add`、`remove` 与 `average` 的实现*
这些公开方法 `add``remove``average`,是仅有的访问或修改 `AveragedCollection` 实例中数据的方式。在使用 `add` 方法或 `remove` 方法,添加或移除某个条目时,其各自的实现,就同时会调用处理更新 `average` 字段的私有 `update_average` 方法。
咱们把 `list``average` 自动留着私有,从而外部代码就无法直接添加项目到那个 `list`,或直接从那个 `list` 移除项目;不然的话,在那个`list` 变化时,`average` 字段就可能失去同步。其中的 `average` 方法,返回的是 `average` 字段中的值,这实现了外部代码读取那个 `average` 而不会修改他。
由于咱们已封装了结构体 `AveragedCollection` 的实现细节,因此咱们就可以在将来轻易地修改各个方面,诸如数据结构等。比如,咱们可以对其中的 `list` 字段,使用 `HashSet<i32>` 而非 `Vec<i32>`。只要 `add``remove``average` 三个公开方法的签名保持不变,那些使用 `AveragedCollection` 的代码就无需改变。而相反若咱们把 `list` 构造为公开,就未必如此了:`HashSet<i32>``Vec<i32>` 有着添加和一处条目的不同方法,由此在外部代码直接修改 `list` 时,就大概率不得不修改了。
若封装是某门语言被视为面向对象的要件,你们 Rust 是满足那种要求的。对代码的不同部分,使用抑或不使用 `pub` 的选项,实现了实现细节的封装。
### 以类型系统及以代码共用的继承
**Inheritance as a Type System and as Code Sharing**
*继承inheritance*,乃籍以实现对象从另一对象继承一些元素,从而在不必再度定义这些元素之下,获得父辈对象数据与行为的一种机制。
若某们语言务必要有着继承,方能成为一门面向对象语言,那么 Rust 就不算是面向对象语言。在不使用宏a macro 之下,没有定义出继承父辈结构体字段与方法实现的结构体的方法。
然而,若在编程工具箱中惯于使用继承,那么依据咱们将继承作为头等大事的自身理由,是可以运用 Rust 中别的一些办法的。
之所以选用继承,大致有两种原因。一个是代码的重用:咱们可以对一个类型实现一些特定行为,而继承就让咱们可以对另一类型重用那些实现。咱们可以使用一些默认的特质方法实现,即咱们曾在清单 10-14 中,将 `summarize` 方法的一个默认实现,添加到 `Summary` 特质上时所见到的那样,在 Rust 代码中以一种受限方式做到这点。任何实现了这个 `Summary` 特质的类型,在无需更多代码之下,都将在其上有着这个 `summarize` 方法。这与父类有着某个方法的实现,同时集成的子类也会有着该方法的实现是类似的。在实现这个 `Summary` 特质时,咱们也可以重写 `summarize` 方法的默认实现,这与子类重新继承自父类的方法实现类似。
而使用与类型系统相关继承的另一原因:即为了实现在与父类型的同一地方,使用子类型。这又被成为 *多态polymorphism*,是指在多个对象共用了一些确切特征时,咱们可以相互替换使用他们。
> **关于多态**
>
> 对许多人来讲,多态等同于继承。但他实际上指的是代码可工作于多个类型数据之下的一个宽泛概念。而对于继承,这些类型则是通用的一些子类。
>
> Rust 则运用了泛型,来对各异的各种可能类型加以抽象,并使用特质边界来强化这些类型所必须提供的那些约束。有时这样的做法,又被叫做 *有边界的参数化多态bounded parametric polymorphism*。
由于继承通常有着共用了超出必要代码的风险,时至今日,其已在许多编程语言中,作为编程设计模式而失宠了。子类本不应共用其父类的全部特征,但在继承父类时却会这样做。这就会造成程序的设计有较低的灵活性。由于子类从父类继承的一些方法并不适用于子类,因此继承还会引入调用子类上无意义或引发错误方法的可能。此外,一些语言还只将运行单一继承(即子类只能从一个类继承),这进一步限制了程序设计的灵活度。
由于这些原因Rust 便采取了运用特质对象,而非继承的方法。接下来就要看看特质对象是如何实现 Rust 中的多态。
## 使用允许不同类型值的特质对象
**Using Trait Objects That Allow for Values of Different Types**
> **注**:这类似于 Java 语言中解决死亡钻石问题DDD的 [接口](https://java.xfoss.com/Ch08_Interfaces_and_Abstract_Classes.html#%E4%BD%BF%E7%94%A8%E6%8E%A5%E5%8F%A3%E6%9D%A5%E6%8B%AF%E6%95%91)。
在第 8 章中,咱们就提到过矢量值的一个局限,便是他们只能存储一种类型的元素。在清单 8-9 中咱们创建出了一种变通方案,其中定义了有着分别保存整数、浮点数与文本变种的 `SpreadsheetCell` 枚举。这就意味着咱们可在各个单元格中存储不同类型的数据,而仍旧有了表示这些单元格所组成行的一个矢量值。这对于在咱们的代码被编译时,就已经清楚这些可交换项目,为固定类型集的情况,这确实是一种相当不错的解决办法。
然而有时咱们会想要咱们库的用户能够扩展这个于某种特定情形下有效的类型集。为展示咱们将怎样达成这个目的接下来咱们将创建对一个条目清单加以迭代的示例性图形用户界面graphical user interfaceGUI 工具 -- 对于 GUI 工具来讲这可是一项常见技能。咱们将创建包含 GUI 库架构的名为 `gui` 的一个库代码箱。此代码箱会包含给人类使用的一些类型,比如 `Button``TextField`。此外,`gui` 的用户将希望创建出他们自己的能被绘制出来的类型:比如,某个程序员要添加一个 `Image`,而另一程序员则要添加一个 `SelectBox`
对于这个示例,咱们不会实现一个完全成熟的 GUI 库,而是会给出这些部分将怎样一起配合起来。在编写这个库时,咱们没法了解而定义出其他那些程序员可能想要创建的全部类型。但咱们肯定清楚 `gui` 需要追踪各种不同类型的许多不同值,同时他还需要调用这些不同类型值上的 `draw` 方法。其无需明白在咱们调用该 `draw` 方法时,具体会发生什么,他只需知道那个值会让那个方法可被咱们调用。
在有着继承的某门语言中要做到这点,咱们可能会定义其上有着名为 `draw` 的方法的一个名为 `Component` 类。至于其他类,比如 `Button``Image``SelectBox` 等,将从 `Component` 基础并因此继承这个 `draw` 方法。他们可以分别重写这个 `draw` 方法,来定义他们的定制行为,而框架则可以将全部这些类型,当作 `Component` 的实例对待而调用他们之上的 `draw`。但由于 Rust 并无继承,因此咱们需要另一种方法,来架构这个 `gui` 库,来允许用户以新类型来扩展他。
### 定义用于共同行为的特质
**Defining a Trait for Common Behavior**
为了实现咱们想要 `gui` 所拥有的行为,咱们将定义将有着一个名为 `draw` 方法的名为 `Draw` 特质。随后咱们就可以定义取 *特质对象a trait object* 的一个矢量。特质对象会同时指向实现了这个指定特质的某个类型,以及用于在运行时查找那个类型上特质方法的一张表。咱们是通过指定某种指针,比如某个 `&` 的引用,或某个 `Box<T>` 的灵巧指针,接着便是 `dyn` 关键字,以及随后指明相关特质,创建出特质对象。(在第 19 章的 [“动态大小类型与 `Sized` 特质”](Ch19_Advanced_Features.md#动态大小的类型与-sized-特质) 小节咱们将讲到特质对象必须使用指针的原因。在泛型或具体类型处咱们就可以使用特质对象。而不论在何处使用特质对象Rust 的类型系统都会确保在编译时,在那样的上下文中的任何值,都将实现该特质对象的特质。于是,咱们就无需掌握编译时的所有可能类型了。
咱们已经提到过,在 Rust 中,咱们避免将结构体与枚举称为 “对象”,是为了将二者与其他语言中的对象区别开来。在结构体或枚举中,结构体字段中的数据,与 `impl` 代码块中的行为是分开的,而在其他语言中,数据与行为被结合为通常被标称为对象的这么一个概念。然而,特质对象由于其结合了数据与行为,而 *真的* 更像其他语言中的对象。但从无法添加数据到特质对象上看,特质对象是不同于传统的对象的。特质对象并不如其他语言中的对象那样普遍的有用:其特定用途为实现共用行为的抽象。
下面清单 17-3 给出了怎样定义有着一个名为 `draw` 方法的一个名为 `Draw` 的特质:
文件名:`src/lib.rs`
```rust
pub trait Draw {
fn draw(&self);
}
```
*清单 17-3`Draw` 特质的定义*
这种语法应与在第 10 章中关于定义特质的方式看起来类似。接下来便有了一种新的语法:下面清单 17-4 定义了保存着一个名为 `components` 矢量的一个名为 `Screen` 的结构体。该矢量为类型 `Box<dyn Draw>` 的,而 `Box<dyn Draw>` 便是一个特质对象;`Box<dyn Draw>``Box` 里头实现了 `Draw` 特质的全部类型的代名词。
文件名:`src/lib.rs`
```rust
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
```
*清单 17-4带有保存着一个实现了 `Draw` 特质的特质对象矢量的 `components` 字段的 `Screen` 结构体的定义*
在这个 `Screen` 结构体上,咱们将定义将调用其 `components` 各条目上 `draw` 方法的一个名为 `run` 的方法,如下清单 17-5 中所示:
文件名:`src/lib.rs`
```rust
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
```
*清单 17-5`Screen` 上会调用各组件上 `draw` 方法的一个 `run` 方法*
这与定义出用到带有特质边界泛型参数的结构体,原理是不同的。泛型参数在某个时间只能用一种具体类型替换,而特质对象则允许在运行时填入多种具体类型。比如,咱们本可以像在下面清单 17-6 中那样,将这个 `Screen` 结构体定义为使用泛型与特质边界:
文件名:`src/lib.rs`
```rust
pub struct Screen<T: Draw> {
pub components: Vec<T>,
}
impl <T> Screen<T>
where
T: Draw,
{
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
```
*清单 17-6其 `run` 方法用到泛型与特质边界的 `Screen` 结构体的一种替代实现*
这种写法就会将咱们限制到有着全是类型 `Button` 或全是类型 `TextField` 组件清单的某个 `Screen` 实例。在咱们将仅有着同质集合homogeneous collections由于那些定义在编译时为使用具体类型而将被单一化那么此时使用泛型与特质边界便是更可取的做法。
另一方面,有了使用特质对象的方法,一个 `Screen` 实例便可以保存包含着 `Box<Button>` 以及 `Box<TextField>``Vec<T>` 了。下面就来看看其工作原理,并于随后讲讲运行时的性能影响。
### 实现该特质
**Implementing the Trait**
现在咱们将添加实现了这个 `Draw` 特质的一些类型。咱们将提供到这个 `Button` 类型。再次声明,具体实现一个 GUI 库超出了本书的范围,因此这个 `draw` 方法在其函数体中不会有任何有用的实现。为设想其实现可能的样子,那么 `Button` 结构体就可能有着 `width``height``label` 等字段,如下清单 17-7 中所示:
文件名:`src/lib.rs`
```rust
pub struct Button {
pub width: u32,
pub height: u32,
pub label: String,
}
impl Draw for Button {
fn draw(&self) {
// 具体绘制按钮的代码
}
}
```
*清单 17-7实现了 `Draw` 特质的 `Button` 结构体*
`Button` 上的 `width``height``label` 字段,将不同于其他组建上的字段;比如,`TextField` 类型就可能有着这些字段外加一个 `placeholder` 字段。各个咱们打算绘制在屏幕上的这些类型,都将实现这个 `Draw` 特质,但会在 `draw` 方法中使用不同代码,来定义出绘制特定类型的方式,正如这里的 `Button` 所拥有的那样(如前面提到的,并无具体代码)。而比如这个 `Button` 类型,则可能包含了在用户点击按钮时,相关方法的一个额外 `impl` 代码块。这些类别的方法,就不会应用到如同 `TextField` 的那些类型。
在使用咱们库的某人,决定要实现有着 `width``height``options` 字段的 `SelectBox` 时,他们也要在 `SelectBox` 类型上的 `Draw` 特质,如下清单 17-8 中所示:
文件名:`src/lib.rs`
```rust
use simple_gui::Draw;
pub struct SelectBox {
width: u32,
height: u32,
options: Vec<String>,
}
impl Draw for SelectBox {
fn draw(&self) {
// 具体绘制复选框的代码
}
}
```
*清单 17-8使用 `simple_gui` 并在 `SelectBox` 结构体上实现 `Draw` 特质的另一代码箱*
咱们库的用户,现在便可以编写他们的 `main` 函数,来创建出 `Screen` 实例。通过将各个 `SelectBox``Button` 放入到 `Box<T>` 中,而成为特质对象,他们便可以把这些 `SelectBox``Button` 添加到 `Screen` 实例了。随后他们便可以调用 `Screen` 实例上的 `run` 方法,而其将调用各个组件上的 `draw` 方法。下面清单 17-9 给出了这样的实现:
文件名:`src/main.rs`
```rust
use simple_gui::{Button, Screen};
pub fn main() {
let screen = Screen {
components: vec! [
Box::new(SelectBox {
width: 25,
height: 30,
options: vec! [
String::from("选项 A"),
String::from("选项 B"),
String::from("选项 C"),
],
}),
Box::new(Button {
width: 50,
height: 10,
label: String::from("OK"),
}),
],
};
screen.run();
}
```
*清单 17-9使用特质对象来存储实现了同一特质的不同类型值*
在咱们编写该库时,咱们是不知道有人会添加这个 `SelectBox` 类型的,但由于 `SelectBox` 实现了 `Draw` 特质,这就表示他实现了那个 `draw` 方法,因此咱们的 `Screen` 实现,就能运作于这个新类型之上而绘制出他来。
这一概念 -- 即尽考虑消息的所要应对的某个值,而非该值的具体类型 -- 与一些动态类型语言中 *鸭子类型duck typing* 概念类似:若某物像鸭子那样走动,并像鸭子那样呱呱叫,那么他就一定是只鸭子!在清单 17-5 中 `Screen` 上的 `run` 实现中,`run` 不需要掌握各个组件的具体类型为何。他不会检查某个组件是个 `Button` 还是 `SelectBox`,他只会调用那个组件上的 `draw` 方法。通过把 `Box<dyn Draw>` 指定为 `component` 矢量中那些值的类型,咱们就已将 `Screen` 定义为需要咱们可在其上调用 `draw` 方法的一些值了。
运用特质对象与 Rust 的类型系统,来编写出与运用了鸭子类型的代码相类似代码的优势,便是咱们再也不必检查,某个值在运行时是否实现了某个特定方法,也再也不必担心在某个值未实现某个方法,而咱们又调用了该方法时会收到报错了。若值未实现特质对象所需的那些特质,那么 Rust 就不会编译咱们的代码。
比如,下面清单 17-10 便给出了在咱们尝试以一个 `String` 作为组件,创建出一个 `Screen` 时会发生什么:
文件名:`src/main.rs`
```rust
use simple_gui::Screen;
pub fn main() {
let screen = Screen {
components: vec! [Box::new(String::from("你好"))],
};
screen.run();
}
```
*清单 17-10尝试使用未实现特质对象之特质的一个类型*
由于 `String` 为实现那个 `Draw` 特质,因此咱们将得到下面这个报错:
```console
$ cargo run  ✔  
Compiling simple_gui v0.1.0 (/home/peng/rust-lang/simple_gui)
error[E0277]: the trait bound `String: Draw` is not satisfied
--> src/main.rs:23:27
|
23 | components: vec! [Box::new(String::from("你好"))],
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Draw` is not implemented for `String`
|
= help: the following other types implement trait `Draw`:
Button
SelectBox
= note: required for the cast from `String` to the object type `dyn Draw`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `simple_gui` due to previous error
```
此报错让咱们明白,要么咱们传递给 `Screen` 了某个不是咱们想要传递的东西,那么就应传递另一个类型,要么咱们应在 `String` 上实现 `Draw`,从而 `Screen` 便可以调用其上的 `draw` 方法。
### 特质对象执行动态调遣
**Trait Object Perform Dynamic Dispatch**
回顾第 10 章中 [“运用了泛型的代码性能问题”](Ch10_Generic_Types_Traits_and_Lifetimes.md#使用泛型参数代码的性能问题) 小节中在泛型之上运用特质边界时咱们关于由编译器所完成的单一化过程the monomorphization process 的讨论:编译器会为咱们在泛型参数处,用到的各个具体类型,而产生出非通用的函数及方法实现。单一化过程所产生的代码,便是在进行 *静态调遣static dispatch*,这是编译器清楚,咱们在编译时调用的为哪个方法时的情况。这与 *动态调遣dynamic dispatch* 是相反的,动态调遣是编译器在编译时,无法区分出咱们所调用的为何方法时的情况。在动态调遣情况下,编译器产生出将在运行时,得出要调用方法的代码。
在咱们运用特质对象时Rust 就必须使用动态调遣。对于全部可能与用到特质对象代码一起使用的类型编译器并无掌握因此他就不明白要调用何种类型上的哪个方法。相反在运行时Rust 会使用特质对象内部的指针,来掌握要调用哪个方法。这种做法会导致静态调遣下所不会发生的运行时开销。动态调遣还会阻止编译器内联某个方法代码的抉择,这就相应地阻止了一些优化。然而,咱们却真切地获得了,如同咱们在清单 17-5 中所编写的代码那样的灵活性,同时才能够支持清单 17-9 中那样的情况如此其便是一种需要考量的取舍了。when we use trait objects, Rust must use dynamic dispatch. The compiler doesn't know all the types that might be used with the code that's using trait objects, so it doesn't know which method implemented on which type to call. Instead, at runtime, Rust uses the pointers inside the trait object to know which method to call. This lookup incurs a runtime cost that doesn't occur with static dispatch. Dynamic dispatch also prevents the compiler from choosing to inline a method's code, which in turn prevents some optimizations. However, we did get extra flexibility in the code that we wrote in Listing 17-5 and were able to support in Listing 17-9, so it's a trade-off to consider.
## 实现一种面向对象设计模式
**Implementing an Object-Oriented Design Pattern**
*状态模式the state pattern* 属于一种面向对象设计模式。这种模式的核心,便是咱们要定义某个值在其内部可能有的一套各种状态。这些状态是由一套 *状态对象state objects* 所表示的同时该值的行为会根据其状态而改变。接下来咱们会完成有着保存其状态字段该字段将有着“草稿draft”、“审阅review” 或“已发布published” 三种状态集合的状态对象,的一个博客帖子结构体示例。
状态对象共用着功能:当然,在 Rust 中咱们会使用结构体与特质,而非对象与继承。每个状态对象负责其自己的行为,以及在其应变换为另一状态时自身的治理。保存着状态对象的值,对这些状态的不同行为,或这些状态之间何时变换就毫不知情。
运用状态模式的优势在于,当程序的业务需求改变时,咱们将不需要修改该值保存状态的那些代码,也不需要修改用到该值的那些代码。咱们只需更新某个状态对象内部的那些代码,来改变其规则,或是添加别的一些状态对象。
首先,咱们将要以更传统的面向对象方式,实现这种状态模式,随后咱们将使用对于 Rust 中,更自然一些的方法。下面就来深入到使用状态模式,逐步实现一个博客帖子工作流。
最终功能看起来将像下面这样:
1. 博客帖子以一个空的草稿开始;
2. 在草稿写好后,该帖子就要求审阅一下;
3. 在帖子被批准后,其就会被发布;
4. 只有发布了的帖子,才会返回要打印的内容,因此那些未获批准的帖子就不会被无故发布。
所有别的在帖子上的尝试修改,都应无效。比如,在完成审阅之前,若咱们尝试批准博客帖子草稿,那么该帖子应保持为一个未发布的草稿。
下面清单 17-11 给出了代码形式的这个工作流:此为咱们将在一个名为 `simple_blog` 的库代码箱中,实现的这个 API 的一个示例用法。由于咱们尚未实现该 `simple_blog` 代码箱,因此这段代码尚不会编译。
文件名:`src/main.rs`
```rust
use simple_blog::Post;
fn main() {
let mut post = Post::new();
post.add_text("今天午饭我吃了沙拉。");
assert_eq! ("", post.content());
post.request_review();
assert_eq! ("", post.content());
post.approve();
assert_eq! ("今天午饭我吃了沙拉。", post.content());
}
```
*清单 17-11验证咱们打算这个 `simple_blog` 代码箱要有的必要功能的代码*
咱们打算运行用户使用 `Post::new` 创建出一个新的帖子草稿。咱们打算允许将一些文本添加到博客帖子。当咱们在审批之前,尝试立即获取到帖子的内容时,由于该帖子仍为一个草稿,因此咱们就不应得到任何文本。出于验证目的,咱们已在该代码中添加了 `assert_eq!`。而为此目的的良好单元测试,就应断言帖子草稿会从那个 `content` 方法返回空字符串,而咱们并未打算为此示例编写一些测试。
接下来,咱们打算开启该帖子的审阅请求,同时咱们系统在等待审阅期间,`content` 返回一个空字符串。在该帖子得到审批时,他就应得以发布了,表示在 `content` 被调用时,该帖子的文本将被返回。
请注意咱们与 `simple_blog` 代码箱交互的唯一类型,便是 `Post` 这个类型。此类型将用到状态模式,并将保存着将为表示帖子可能状态 -- 草稿、等待审阅或已发布,的三个状态对象之一的一个值。从一种状态改变为另一状态,将在该 `Post` 类型里得以内部管理。这些状态,会因应着库用户在 `Post` 实例上的方法调用而改变,但库用户们却不必直接管理这些状态改变。同样,用户们是无法在这些状态上犯下错误的,比如在帖子未审阅前发布帖子。
### 定义出 `Post` 并创建出一个草稿状态的新实例
**Defining `Post` and Creating a New Instance in the Draft State**
下面就来开始这个库的实现!咱们清楚咱们需要保存着一些内容的一个公开的 `Post` 结构体,因此咱们将以这个结构体的定义,及创建出 `Post` 实例的一个关联的公开 `new` 函数开始,如下清单 17-12 中所示。咱们还将构造出将定义 `Post`的全部状态对象,所必须有的行为的一个私有 `State` 特质。
随后 `Post` 类型将在名为 `state` 的私有字段的 `Option<T>` 值内部,保存 `Box<dyn State>` 类型的一个特质对象,来保存状态对象。过一会儿,咱们就会看到为何那个 `Option<T>` 是必要的。
文件名:`src/lib.rs`
```rust
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
}
trait State {}
struct Draft {}
impl State for Draft {}
```
*清单 17-12`Post` 结构体的定义,以及创建新 `Post` 实例的 `new` 函数、`State` 特质,以及 `Draft` 结构体*
其中的 `State` 特质会定义出由不同帖子状态共用的行为。这些状态对象分别是 `Draft``PendingReview``Published`,同时他们都将实现 `State` 特质。至于现在,这个特质并无任何方法,而由于 `Draft` 状态是咱们想要帖子开始的状态,因此咱们将以仅定义出这个状态开始。
在创建出新的 `Post` 实例时,咱们将其 `state` 自动设置为了保存着一个 `Box` 值的 `Some` 值。这个 `Box` 会只想 `Draft` 结构体的一个新实例。这会确保不能在咱们何时创建出一个 `Post` 的新实例,其都将作为一篇草稿开始。由于 `Post``state` 字段是私有的,因此就没有办法创建出其他任何状态的一个 `Post`!在 `Post::new` 函数中,咱们把 `content` 字段设置为了一个新的、空 `String`
### 存储帖子内容文本
**Storing the Text of the Post Content**
在 17-11 中,咱们曾看到咱们希望能调用一个名为 `add_text` 的方法,并传递给他随后被作为博客帖子内容而添加的一个 `&str`。咱们将这实现为一个方法,而不是把 `content` 作为 `pub` 暴露出来,如此稍后咱们就可以实现一个将控制 `content` 字段数据如何被读取的方法。这个 `add_text` 方法是相当直截了当的,那么接下就来在清单 17-13 中,添加这个实现到 `impl Post` 代码块:
文件名:`src/lib.rs`
```rust
impl Post {
// -- 跳过代码 --
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
}
```
*清单 17-13实现将文本添加到帖子 `content` 字段的 `add_text` 方法*
由于咱们是在修改咱们正于其上调用 `add_text``Post` 实例,因此这个 `add_text` 方法便取了到 `self` 的可变引用。咱们随后调用了 `content` 字段中 `String` 类型上的 `push_str`,并传递 `text` 参数来添加到那个已保存的 `content`。此行为不依赖帖子所处的状态,因此其并非状态模式的一部分。这个 `add_text` 方法完全不与 `state` 字段交互,但其为咱们打算支持行为的一部分。
### 确保帖子草稿的内容为空
**Ensuring the Content of a Draft Post Is Empty**
即使咱们已调用了 `add_text` 并把一些内容添加到了咱们的帖子,但由于该帖子仍处于草稿状态,故咱们仍想要那个 `content` 方法返回空字符串切片an empty string slice正如清单 17-11 中第 7 行所给出的那样。那么现在,就来用将满足此要求的最简单物件,实现这个 `content` 方法:即总是返回一个空字符串切片。稍后一旦咱们实现修改帖子状态的能力,从而帖子可被发布,咱们就会修改这个方法。到目前为止,贴子就只能处于草稿状态,因此帖子内容应始终为空。下面清单 17-14 给出了这种占位的实现:
文件名:`src/lib.rs`
```rust
impl Post {
// -- 跳过代码 --
pub fn content(&self) -> &str {
""
}
}
```
*清单 17-14添加始终返回空字符串切片的 `Post` 上 `content` 方法的一个占位实现a placeholder implementation*
有了添加的这个 `content` 方法,清单 17-11 中到第 7 行为止的那些代码就都将如预期那样工作了。
### 请求帖子审阅改变其状态
**Requesting a Review of the Post Changes Its State**
接下来,咱们就需要添加请求帖子审阅的功能了,帖子审阅应将其状态从 `Draft` 改变为 `PendingReview`。下面清单 17-15 给出了这样的代码:
文件名:`src/lib.rs`
```rust
impl Post {
// -- 跳过代码 --
pub fn request_review(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.request_review())
}
}
}
trait State {
fn request_review(self: Box<Self>) -> Box<dyn State>;
}
struct Draft {}
impl State for Draft {
fn request_review(self: Box<Self>) -> Box<dyn State> {
Box::new(PendingReview {})
}
}
struct PendingReview {}
impl State for PendingReview {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
}
```
*清单 17-15实现 `Post` 与 `State` 特质上的 `request_review` 方法*
咱们给了 `Post` 名为 `request_review` 的一个公开方法,其将取到 `self` 的一个可变引用。随后咱们调用了 `Post` 当前状态上的内部 `request_review` 方法,而这第二个 `request_review` 方法就会消费当前状态并返回一个新的状态。
咱们把那个 `request_review` 方法添加到了 `State` 特质;所有实现了这个特质的类型,现在都将需要实现这个 `request_review` 方法。请注意这里用的不再是 `self``&self``&mut self` 作为该方法的首个参数,这里用的是 `self: Box<Self>`。这样的语法表示,在有在某个保存了该类型的 `Box` 上调用时,这个方法才是有效的。这种语法会取得 `Box<Self>` 的所有权,令到原有状态失效,进而 `Post` 的状态值就可以转换到一种新的状态。
为了消费原有状态,这个 `request_review` 方法就需要取得该状态值的所有权。这便是 `Post` 的那个 `state` 字段中的 `Option` 进入之处:咱们调用了 `take` 方法(属于标准库的 `Option` 类型),来从 `state` 字段取出那个 `Some` 的值,并由于 Rust 不允许咱们在结构体中有无效或空字段Rust doesn't let us have unpopulated fields in structs而在 `state` 字段中留下一个 `None`。这样就允许咱们把其中的 `state` 值,迁移出 `Post`而非借用他。随后咱们将把帖子的 `state` 值,设置为此操作的结果。
为了获取到 `state` 值的所有权,咱们就需要暂时将 `state` 设置为 `None`,而非直接使用像是 `self.state = self.state.request_review();` 这样的代码设置他。这样做确保了在咱们已将 `Post` 转换为新状态后,其无法使用原先的 `state` 值。
`Draft` 上的 `request_review` 方法返回的是个新的、新加入的 `PendingReview` 装箱过的实例,其表示了帖子等待审阅时的状态。那个 `PendingReview` 结构体同样实现了 `request_review` 方法,但并未进行任何转换。而是,由于在咱们于某个已处于 `PendingReview` 状态的帖子上,请求审阅时,帖子应保持处于 `PendingReview` 状态,因此 `PendingReview``request_review` 方法调用返回的是他自己。
现在咱们就可以开始看到状态模式的优势了:不论 `Post``state` 值为何,其上的 `request_review` 方法都是一样的。每种状态都负责着其自己的那些规则。
咱们将保留 `Post` 上的 `content` 方法如其现在这样,即返回一个空字符串切片。现在咱们就可以让某个 `Post`,处于 `PendingReview` 状态抑或 `Draft` 状态了,不过咱们想要 `PendingReview` 状态中的同样行为。现在清单 17-11 到第 10 行便工作了!
### 添加 `approve` 来修改 `content` 的行为
**Adding `approve` to Change the Behavior of `content`**
`approve` 方法将与 `request_review` 方法类似:他将把 `state` 设置为在帖子状态为 “批准” 时,当前状态所应表明的值,如下清单 17-16 中所示:
文件名:`src/lib.rs`
```rust
impl Post {
// -- 跳过代码 --
pub fn approve(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.approve())
}
}
}
trait State {
fn request_review(self: Box<Self>) -> Box<dyn State>;
fn approve(self: Box<Self>) -> Box<dyn State>;
}
struct Draft {}
impl State for Draft {
// -- 跳过代码 --
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
}
struct PendingReview {}
impl State for PendingReview {
// -- 跳过代码 --
fn approve(self: Box<Self>) -> Box<dyn State> {
Box::new(Published {})
}
}
struct Published {}
impl State for Published {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
}
```
*清单 17-16实现 `Post` 与 `State` 特质上的 `approve` 方法*
咱们把这个 `approve` 方法,添加到了 `State` 特质,并添加了一个实现了 `State` 的新结构体,即 `Published` 状态。
`PendingReview` 上的 `request_review` 工作方式类似,在咱们调用 `Draft` 上的 `approve` 方法时,由于 `approve` 将返回 `self`,因此他将没有效果。当咱们在 `PendingReview` 上调用 `approve` 时,他返回的是一个新的、装箱过后的 `Published` 结构体实例。这个 `Published` 结构体实现了 `State` 特质,而由于帖子在`request_review``approve` 两个方法下,都应保持处于 `Published` 状态,因此对于这两个方法,他都会返回他本身。
现在咱们就需要更新 `Post` 上的那个 `content` 方法了。咱们希望从 `content` 返回的值,取决于 `Post` 的当前状态,因此咱们就让 `Post`,委托给定义在其 `state` 上的一个 `content` 方法,如下清单 17-17 中所示:
文件名:`src/lib.rs`
```rust
impl Post {
// -- 跳过代码 --
pub fn content(&self) -> &str {
self.state.as_ref().unwrap().content(self)
}
// -- 跳过代码 --
}
```
*清单 17-17将 `Post` 上的 `content` 方法,更新为委托给 `State` 上的 `content` 方法*
由于咱们的目标是要把所有规则,都保持于实现了 `State` 的那些结构体中,因此咱们就要调用 `state` 字段中值上的 `content` 方法,并将帖子实例(那就是 `self`)作为参数加以传递。随后咱们要返回从 `state` 值上的 `content` 方法调用所返回的值。
由于咱们要的是到 `Option<T>` 内部值的一个引用,而非该值的所有权,因此咱们调用了 `Option<T>` 上的 `as_ref` 方法。由于 `state` 是个 `Option<Box<dyn State>>`,在咱们调用 `as_ref` 时,就会返回一个 `Option<&Box<dyn State>>`。而若咱们没有调用 `as_ref`,那么由于咱们无法无法把 `state` 迁移出那个借用的函数参数 `&self`,而将得到一个报错。
咱们随后调用了 `unwrap` 方法(标准库 `Option<T>` 类型上的),由于咱们清楚,`Post` 上的那些方法,会确保 `state` 将在这些方法完成时,始终包含某个 `Some` 值,因此咱们就明白,这个`unwrap` 是绝不会终止运行的。这便是第 9 章 [相比与编译器咱们掌握着更多信息的情形](Ch09_Error_Handling.md#相比于编译器代码编写者掌握了更多信息的情形) 小节所讲到的情形之一:即咱们明白某个 `Option<T>` 不可能是个 `None` 值,尽管编译器无法掌握这一点。
`unwrap` 方法这里,当咱们在 `&Box<dyn State>` 上调用 `content` 方法时强制解引用转换deref coercion 就会在那个 `&``Box` 上发挥作用,从而 `content` 方法就将在实现了 `State` 特质的类型上,最终被调用到。而那就意味着咱们需要把 `content` 添加到 `State` 特质的定义,而那正是咱们把根据咱们所有的状态,返回什么样的内容,这种逻辑要放入的地方,如下清单 17-18 中所示:
文件名:`src/lib.rs`
```rust
trait State {
// -- 跳过代码 --
fn content<'a>(&self, post: &'a Post) -> &'a str { "" }
}
// -- 跳过代码 --
struct Published {}
impl State for Published {
// -- 跳过代码 --
fn content<'a>(&self, post: &'a Post) -> &'a str {
&post.content
}
}
```
*清单 17-18把 `content` 方法添加到 `State` 特质*
咱们添加了返回空字符串切片的 `content` 方法默认实现。那就意味着咱们无需在 `Draft``PendingRereview` 两个结构体上实现 `content` 方法。而 `Published` 结构体则将重写这个 `content` 方法,并返回 `post.content` 中的值。
> **注**:由于 `content` 默认实现返回的是 `""` 空字符串切片,是个已知大小的值,故方才可以写默认实现。而若将 `request_review` 或 `approve` 也写为默认实现,即如下面这样:
```rust
trait State {
fn request_review(self: Box<Self>) -> Box<dyn State> { self }
fn approve(self: Box<Self>) -> Box<dyn State> { self }
fn content<'a>(&self, post: &'a Post) -> &'a str { "" }
}
```
>
> 那么将报出错误:
```console
$ cargo run  ✔  
Compiling simple_blog v0.1.0 (/home/peng/rust-lang/simple_blog)
error[E0277]: the size for values of type `Self` cannot be known at compilation time
--> src/lib.rs:40:53
|
40 | fn approve(self: Box<Self>) -> Box<dyn State> { self }
| ^^^^ doesn't have a size known at compile-time
|
= note: required for the cast from `Self` to the object type `dyn State`
help: consider further restricting `Self`
|
40 | fn approve(self: Box<Self>) -> Box<dyn State> where Self: Sized { self }
| +++++++++++++++++
For more information about this error, try `rustc --explain E0277`.
error: could not compile `simple_blog` due to previous error
```
>
> 这表示 Rust 中的默认实现,需要返回值为固定大小。
请注意正如咱们曾在第 10 章中讨论过的那样,在这个方法上咱们需要生命周期注解。咱们取了到某个 `post` 的引用作为参数,并返回的是到那个 `post` 一部分的引用,因此所返回引用的生命周期,便于这个 `post` 参数生命周期相关。
而咱们就完成了 -- 清单 17-11 的全部代码现在便工作了咱们就已实现了博客帖子工作流规则the rules of the blog post workflow 下的状态模式。与那些规则相关的逻辑,是存在与这些状态对象中,而非散落于 `Post` 的各处the logic related to the rules lives in the state objects rather than being scattered throughout `Post`
> 为何不用枚举Why Not An Enum
>
> 你可能已经想到,为何咱们没有使用将不同帖子状态作为变种的一个 `enum`。那确实是一种可行的办法,请尝试并比较最后的结果,来发现你要选哪个方案!运用枚举的一个不足之处,便是在每个检查枚举值的地方,将都需有一个 `match` 表达式,或类似的东西来处理每种可能的变种。相比这里的特质对象方法,那就会有更多重复代码。
### 状态模式的取舍
**Trade-offs of the State Pattern**
咱们已经证明Rust 是能够实现这种面向对象模式,来封装处于不同状态下帖子应具备的各种不同行为。`Post` 上的方法对这些各种行为毫不知情。咱们组织代码的方式,即咱们必须仅在一处查看,而获悉某个已发布帖子可以有的那些不同行为方式:这便是 `Published` 结构体上 `State` 特质的实现。
若咱们原本打算创建另一种不使用状态模式的替代实现,那么咱们可能就会在 `Post` 上的那些方法中,使用一些检查帖子状态的 `match` 表达式,并在那些 `match` 表达式处改变行为。那就意味着咱们将不得不查看多个地方,来了解某个处于已发布状态帖子的全部影响!这样做只会徒增咱们所添加的更多一些状态:每个的这些 `match` 表达式,都将需要另一支臂。
而在状态模式下,那些 `Post` 方法以及那些咱们用到 `Post` 的各处,就不需要那些 `match` 表达式,而要添加一个新状态,咱们将只需添加一个新结构体,并在那个结构体上实现那些特质方法即可。
使用状态模式的这种实现,易于添加更多功能。为发现使用状态模式维护代码的简单性,请尝试下面几条建议:
- 请添加将帖子状态从 `PendingReview` 改回到 `Draft` 的一个 `reject` 方法;
- 在状态可被改变为 `Published` 之前,要求两次到 `approve` 的调用;
- 只有在某个帖子处于 `Draft` 状态时,才允许用户添加文本内容。提示:让状态对象负责那些可能修改内容的操作,而不负责修改 `Post` 的操作。
状态模式的一个缺点则是,由于这些状态都实现那些状态间的转换,那么其中一些状态就相互耦合了。当咱们在 `PendingReview``Published` 之间,添加另一状态,比如 `Scheduled` 时,咱们将不得不把 `PendingReview` 中的代码,修改为相应地转换到 `Scheduled`。若在新状态的添加下,`PendingReview` 无需修改,那么就会少一些事情,然而那便意味着转换到另一种涉及模式了。
至于另一个缺点,便是咱们重复了一些逻辑。为消除一些重复,咱们就可能会尝试构造 `State` 特质上,返回 `self``request_review``approve` 两个方法的默认实现;然而,由于该特质不清楚那个具体的 `self` 将为何物因此这会违反对象安全性violate object safety。咱们希望能够将 `State` 作为特质对象使用,因此咱们就需要他的那些方法是对象安全的。
其他代码重复包括了 `Post``request_review``approve` 两个方法的一些相似实现。这两个方法都委托给了那个 `Option``state` 字段中值上的同一方法,并将 `state` 字段的值,设置到方法的结果。若咱们在 `Post` 上有着大量的遵循这种模式的方法咱们就会考虑定义出一个宏defining a macro来消除这种重复请参阅第 19 章中 ["宏Macros"](Ch19_Advanced_Features.md#关于宏) 小节)。
经由这种完全按照面向对象模式下所定义的状态模式,来实现这种模式,咱们就没有利用上原本所能利用的 Rust 的全部优势。下面就来看看,咱们可对那个 `simple_blog` 能做出的,可将无效状态与无效状态转换,构造为编译时错误的一些改变。
#### 将状态与行为当作类型编码
**Encoding States and Behavior as Types**
咱们将给出如何对这种状态模式加以反思以得到一套不同的权衡取舍。不同于对状态及状态的转换进行完全地封装进而外部代码对他们一无所知咱们将把那些状态编码为不同类型。于是乎Rust 的类型检查系统,就将通过发出编译器报错,阻止在那些仅允许已发布帖子之处,使用草稿帖子的尝试。
下面来考虑一下清单 17-11 中,`main` 函数的第一部分:
文件名:`src/main.rs`
```rust
fn main() {
let mut post = Post::new();
post.add_text("今天午饭我吃了沙拉。");
assert_eq! ("", post.content());
}
```
咱们仍旧使用 `Post::new`,实现了新的处于草稿状态的那些帖子的创建,并实现了将文本添加到帖子内容的能力。但与在草稿帖子上有着返回空字符串的 `content` 方法不同,咱们将把 `Post` 构造为根本就没有那个 `content` 方法。那样的话,在咱们尝试获取某个草稿帖子的内容时,就会得到告诉咱们该方法不存在的编译器报错。由此,对于生产中咱们无意地显示出帖子内容,由于那样的代码甚至都不会编译,那么这将是不可能的了。清单 17-19 给出了 `Post` 结构体的定义,以及一个 `DraftPost` 的结构体,以及各自上的一些方法:
文件名:`src/lib.rs`
```rust
pub struct Post {
content: String,
}
pub struct DraftPost {
content: String,
}
impl Post {
pub fn new() -> DraftPost {
DraftPost {
content: String::new(),
}
}
pub fn content(&self) -> &str {
&self.content
}
}
impl DraftPost {
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
}
```
*清单 17-19有着 `content` 方法的 `Post` 与不带 `content` 方法的 `DraftPost`*
`Post``DraftPost` 两个结构体都有着存储了博客帖子文本的私有 `content` 字段。由于咱们正将状态编码,迁移到一些结构体类型,因此这两个结构体就不再有 `state` 字段了。`Post` 结构体将表示已发布帖子,而他便有着返回 `content``content` 方法。
咱们仍有一个 `Post::new` 函数,但不是返回 `Post` 实例,其返回的是 `DraftPost` 实例。由于 `content` 是私有的,而有没有任何返回 `Post` 的函数,那么此刻就不可能创建出 `Post` 实例。
`DraftPost` 结构体有着一个 `add_text` 方法,因此咱们就可以如同之前那样,把文本添加到 `content`,但请注意 `DraftPost` 并没有定义一个 `content` 方法!因此现在的程序确保了全部帖子都以草稿帖子开头,而草稿帖子并不会让他们的内容用于显示。任何绕过这些约束的尝试,都将导致编译器报错。
#### 以到不同类型的转换,实现(状态的)转换
**Implementing Transitions as Transformations into Different Types**
那么怎样来获取到某个已发布帖子呢?咱们是打算强化某个草稿帖子在其可被发布之前,必须被审阅和批准的规则。处于等待审阅状态的帖子,应仍然不显示任何内容。下面酒类通过添加另一结构体,`PendingReviewPost`、在 `DraftPost` 上定义出返回 `PendingReviewPost` 实例的 `request_review` 方法,以及在 `PendingReviewPost` 上定义出返回 `Post``approve` 方法,实现这些约束,如下清单 17-20 中所示:
文件名:`src/lib.rs`
```rust
impl DraftPost {
// -- 跳过代码 --
pub fn request_review(self) -> PendingReviewPost {
PendingReviewPost {
content: self.content,
}
}
}
pub struct PendingReviewPost {
content: String,
}
impl PendingReviewPost {
pub fn approve(self) -> Post {
Post {
content: self.content,
}
}
}
```
*清单 17-20通过在 `DraftPost` 上调用 `request_review` 而被创建出的 `PendingReviewPost` 实例,以及将 `PendingReviewPost` 转变为已发布 `Post` 的 `approve` 方法*
`request_review``approve` 两个方法,都取得了 `self` 的所有权,从而消费了 `DraftPost``PendingReviewPost` 实例,并把他们相应地转换成了 `PendingReviewPost` 与已发布的 `Post`。以这种方式,在咱们于 `DraftPost` 实例上调用了 `request_review` 之后,便不会再有任何遗存的 `DraftPost` 实例,对 `PendingReviewPost` 之类亦是如此。`PendingReviewPost` 结构体之上,并没有 `content` 方法,因此正如 `DraftPost` 一样,尝试读取其内容,会导致编译器报错。由于获取确实有定义出的 `content` 方法的已发布 `Post` 实例的唯一方式,为在某个 `PendingReviewPost` 上调用 `approve` 方法,而获取到一个 `PendingReviewPost` 的唯一方法,为在某个 `DraftPost` 上调用 `request_review` 方法,咱们现在便已将这个博客帖子工作流,编码为了类型系统。
不过咱们还必须对 `main` 做出一些小的修改。`request_review``approve` 两个方法,返回的都是一些新实例,而不再是修改他们于其上所调用的结构,因此咱们就需要添加更多 `let post = ` 遮蔽赋值语句,来保存那些返回的实例。咱们还不能断言草稿于等待审阅帖子的内容为空字符串,咱们也是不需要他们的:咱们再也不会编译,尝试使用处于这些状态下的帖子内容的代码。下面清单 17-21 中,给出了 `main` 中更新后的代码:
文件名:`src/main.rs`
```rust
use neo_simple_blog::Post;
fn main() {
let mut post = Post::new();
post.add_text("这是一个博客帖子。");
let post = post.request_review();
let post = post.approve();
assert_eq! ("这是一个博客帖子。", post.content());
}
```
*清单 17-21为使用这个博客帖子工作流的新实现而对 `main` 的一些修改*
咱们所需做出的对 `post` 重新复制的那些修改,表示这种实现,已不再那么严格遵循面向对象设计模式了:状态之间的转换,不再是整个地封装在 `Post` 实现里。不过,咱们的收获,则是由于类型系统,以及编译时所发生的类型检查,那些无效状态现在就不可能了!这样就确保了一些确切代码错误,比如未发布帖子内容的显示等,在其到达生产部署之前,就将被发现。
请在清单 17-21 之后的情况下,尝试在 `neo_simple_blog` 上实现本小节开头给出的那些任务,来发现你对此版本代码的设计模式有何看法。请注意其中一些任务,在这种模式下或许已被完成了。
咱们业已看到,即使 Rust 有能力实现面向对象的一些设计模式,而对于其他模式,比如将状态编码为类型系统等,在 Rust 中也都是可行的。这些模式都有着不同的取舍。尽管咱们可能对面向对象的那些模式非常熟悉,但对问题进行反思,而运用上 Rust 那些特性的优势,就可以提供到各种好处,比如在编译时阻止一些代码错误等。出于比如所有权这样的,面向对象语言所不具备的某些特性,那么在 Rust 中,面向对象的那些模式,将并不总是最佳方案。
## 本章小节
在读完这一章之后,不论咱们认为或是不认为 Rust 是门面向对象语言,现在都明白,咱们可以在 Rust 中使用特质对象来获得一些面向对象的特性。动态调遣dynamic dispatch 可以些许运行时性能损失换取到咱们代码一定程度的灵活性。咱们则可运用这样的灵活性来实现能有助于代码可维护性的一些面向对象模式。Rust 还有面向对象语言所没有的其他一些特性,比如所有权等。对于利用 Rust 各种长处方面的优势来讲,面向对象模式将不总是最佳方式,但其为一种可行选项。
接下来,咱们将看看各种模式,这是带来大量灵活性的 Rust 诸多特性的另一项。虽然贯穿本书,咱们已经粗略地看了看他们,但仍尚未见识到他们的完整能力。咱们就拭目以待吧!

View File

@@ -23,976 +23,6 @@
本章时与模式相关全部内容的一个参考。咱们将涵盖运用模式的那些有效位置、可证伪与不可证伪模式的区别the difference between refutable and irrefutable patterns以及可能见到的那些不同类别的模式语法。在本章最后咱们将获悉如何运用模式来清晰地表达许多概念。
## 可使用模式的全部位置
End
**All the Places Patterns Can Be Used**
模式会出现在 Rust 中的数个地方,而咱们以及见到很多的使用他们而不自知!本小节会讨论模式有效的全部位置。
### `match` 的那些支臂
**`match` Arms**
正如第 6 章中曾讨论过的,咱们是在 `match` 表达式的那些支臂中,使用模式的。形式上看,`match` 表达式是以关键字 `match`、要匹配的某个值,以及由某个模式和在该值匹配此模式时,要运行的一个表达式组所成的一个或多个支臂,这种形式而被定义出的,就像下面这样:
```rust
match VALUE {
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
}
```
比如,下面就是清单 6-5 中,在变量 `x` 中一个 `Option<i32>` 值上匹配的那个 `match` 表达式:
```rust
match x {
None => None,
Some(i) => Some(i + 1),
}
```
这个 `match` 表达式中的模式,便是各个箭头左边的 `None``Some(i)`
`match` 表达式的一项要求,便是在表达式中那个值,所必须考虑到的全部可能性方面,需要 *详尽无遗exhaustive*。而一种确保咱们已经覆盖每种可能性的方式便是将一个捕获全部的模式a catchall pattern作为最后支臂比如一个匹配任意值的变量名称就绝不会失败而因此会覆盖每种其余的情形。
特别的模式 `_`,将匹配任何东西,但他绝不会绑定到某个变量,因此他通常被用在最后的匹配支臂中。在比如咱们打算忽略任何不予指定的值时,这种 `_` 模式就会是有用的。在本章稍后的 [“忽略模式中的值”](#忽略模式中的某些值ignoring-values-in-a-pattern) 小节中,咱们将更详细地讲到这种 `_` 模式。
### 条件 `if let` 表达式
**Conditional `if let` Expressions**
在第 6 章中,咱们曾讨论过怎样将 `if let` 表达式主要用于编写,只与一种情形匹配的 `match` 表达式等价的简便方式。`if let` 可选地能有包含了在 `if let` 中模式不匹配时,要运行代码的一个相应 `else`
下面清单 18-1 显示,混用及匹配 `if let``else if``else if let` 这些表达式是可行的。相比于其中咱们只能表达出,与一些模式匹配的唯一一个值的 `match` 表达式这样做就会给到更多灵活性。并且Rust 不要求一系列 `if let``else if``else if let` 支臂中的那些条件相互有关联。
清单 18-1 中的代码,根据数个条件的一系列检查,而判断出构造绑架的何种颜色。对于这个示例,咱们已创建了有着真实程序中,本应从用户输入接收到,但这里是一些硬编码值的数个变量。
文件名:`src/main.rs`
```rust
fn main() {
let favorite_color: Option<&str> = None;
let is_tuesday = false;
let age: Result<u8, _> = "34".parse();
if let Some(color) = favorite_color {
println! ("使用你喜欢的颜色,{color},作为背景");
} else if is_tuesday {
println! ("周二是绿色的一天!");
} else if let Ok(age) = age {
if age > 30 {
println! ("使用紫色作为背景色");
} else {
println! ("使用橙色作为背景色");
}
} else {
println! ("使用蓝色作为背景色");
}
}
```
*清单 18-1混用 `if let`、`else if`、`else if let` 及 `else`*
在用户指定了喜好的颜色时,那种颜色就会被用作背景。若没有指定喜好颜色而今天是周二,背景颜色就是绿色。否则,在用户以字符串指定了他们的年龄,而咱们可以将其成功解析为数字时,根据该数字的值,颜色就会要么时紫色,抑或是橙色。而在这些条件都不适用时,背景颜色就会是蓝色。
这种条件结构,实现了对复杂要求的支持。在这里咱们所拥有的那些硬编码值下,这个示例将打印出 `使用紫色作为背景色`
咱们可以看到,`if let` 也能以 `match` 支臂所能够的同样方式引入一些遮蔽变量shadowed variables其中行 `if let Ok(age) = age` 就引入了包含着在那个 `Ok` 变种里值的一个新遮蔽 `age` 变量。这意味着咱们需要把 `if age > 30` 情形,放在该代码块里:咱们不能将这两种情形,结合进到 `if let Ok(age) = age && age > 30`。咱们打算将其与 `30` 比较的那个遮蔽 `age`,直到那个新代码块以花括号开头之前,都还是无效的。
使用 `if let` 表达式的缺点,便是编译器不会就穷尽加以检查,而在 `match` 表达式下则会。若咱们省略了其中最后的 `else` 代码块,而因此遗漏了处理某些情形,编译器也不会就可能的逻辑错误向我们发出告警。
### `while let` 条件循环
`if let` 的构造类似,`while let` 条件循环允许某个 `while` 循环,在某个模式持续匹配期间运行。下面清单 18-2 中,咱们编写了将一个矢量用作栈,并以该矢量中那些值被压入的相反顺序,将这些值打印处理的一个 `while let` 循环。
```rust
let mut stack = Vec::new();
stack.push(1);
stack.push(2);
stack.push(3);
while let Some(top) = stack.pop() {
println! ("{}", top);
}
```
*清单 18-2使用 `while let` 循环,来于 `stack.pop()` 返回 `Some` 期间打印出一些值*
此示例打印出 `3``2` 及随后的 `1``pop` 方法会取出矢量值的最后一个元素,并返回 `Some(value)`。在该矢量值为空时,`pop` 就会返回 `None`,这个循环便停止。咱们可以使用 `while let` 循环,弹出栈中的每个元素。
### `for` 循环
`for` 循环中,直接跟在关键字 `for` 之后的那个值,就是个模式。比如,在 `for x in y` 中,`x` 就是其中的模式。下面清单 18-3 演示了如何使用 `for` 循环中的模式,来结构,或者说拆分作为这个 `for` 循环一部分的某个元组。
```rust
fn main() {
let v = vec! ['a', 'b', 'c'];
for (index, value) in v.iter().enumerate() {
println! ("{} 处于索引 {}", value, index);
}
}
```
*清单 18-3使用一个 `for` 循环中的模式,来结构某个元组*
清单 18-3 中的代码,将打印出如下内容:
```console
$ cargo run lennyp@vm-manjaro
Compiling for_demo v0.1.0 (/home/lennyp/rust-lang/for_demo)
Finished dev [unoptimized + debuginfo] target(s) in 0.25s
Running `target/debug/for_demo`
a 处于索引 0 处
b 处于索引 1 处
c 处于索引 2 处
```
咱们使用 `enumerate` 方法适配了一个迭代器,如此他便会产生放入到元组中的一个值,以及那个值的索引。首个产生出的值,为元组 `(0, 'a')`。当这个值与模式 `(index, value)` 匹配时,`index` 将为 `0`,同时 `value` 将为 `'a'`,从而打印出输出的首行。
### `let` 语句
本章之前,咱们只明确讨论过 `match``if let` 下对模式的运用,但事实上,咱们也曾在别的地方用到过模式,包括在 `let` 语句中。比如,请考虑下面这个使用 `let` 的简单变量赋值:
```rust
let x = 5;
```
在咱们每次像这样使用 `let` 语句,就都用到了模式,尽管咱们可能并未意识到他!更正式地说,`let` 语句看起来是这样的:
```rust
let PATTERN = EXPRESSION;
```
在像是 `let x = 5;` 这样,于 `PATTERN` 槽处有着一个变量名的语句中那个变量名正是模式的一种特别简单形式。Rust 会将表达式与该模式比较而赋予其找到的任何名字Rust compares the expression against the pattern and assigns any names it finds。因此比如在 `let x = 5;` 中,`x` 就是一个表示 “将这里所匹配的东西,绑定到变量 `x`bind what matches here to the variable `x`。” 由于名字 `x` 为整个的模式,那么此模式便意味着 “将所有东西都绑定到变量 `x`不管值为何bind everything to the variable `x`, whatever the value is。”
要更清楚地看到 `let` 的模式匹配方面,请考虑下面清单 18-4他在 `let` 下使用了一个模式,来结构某个元组。
```rust
let (x, y, z) = (1, 2, 3);
```
*清单 18-4使用模式来结构元组并一次性创建处三个变量*
在这里咱们将一个元组与某个模式匹配。Rust 会比较把值 `(1, 2, 3)`,与模式 `(x, y, z)` 相比较,并发现该值与这种模式匹配,因此 Rust 就把 `1` 绑定到 `x``2` 绑定到 `y`,而把 `3` 绑定到 `z`。咱们可把这种元组模式,设想为在其中嵌套了三个单独的变量。
而当模式中元素个数,不与元组中元素个数匹配时,整体的类型就不会匹配,同时咱们将得到一个编译器报错。比如,下面清单 18-5 给出了三个元素解构到两个变量的尝试,这将不会工作。
```rust
let (x, y) = (1, 2, 3);
```
*清单 18-5不正确的构造模式其变量与元组中元素个数不匹配*
尝试编译此代码会导致如下这种类型报错:
```console
$ cargo run lennyp@vm-manjaro
Compiling while_let_demo v0.1.0 (/home/lennyp/rust-lang/while_let_demo)
error[E0308]: mismatched types
--> src/main.rs:2:9
|
2 | let (x, y) = (1, 2, 3);
| ^^^^^^ --------- this expression has type `({integer}, {integer}, {integer})`
| |
| expected a tuple with 3 elements, found one with 2 elements
|
= note: expected tuple `({integer}, {integer}, {integer})`
found tuple `(_, _)`
For more information about this error, try `rustc --explain E0308`.
error: could not compile `while_let_demo` due to previous error
```
要修复该错误,咱们可以如同即将在 [于模式中忽略一些值](#忽略模式中的某些值ignoring-values-in-a-pattern) 小节中所看到的那样,使用 `_``..`,忽略那个元组中的一个或更多的值。当问题是咱们在模式中有太多变量时,那么办法就是通过移除一些变量构造出类型,从而变量数目便等于元组中元素的数目了。
### 函数参数
**Function Parameters**
函数参数也可以是些模式。下面清单 18-6 中的代码,声明了取名为 `x` 类型 `i32` 的一个叫做 `foo` 的函数,现在看起来应不陌生。
```rust
fn foo(x: i32) {
// 代码出现在这里
}
```
*清单 18-6在参数中用到模式的一个函数签名*
其中那个 `x` 部分,便是个模式!与咱们曾在 `let` 下所做的那样,咱们可以在函数参数中,将某个元组和模式匹配。下面清单 18-7 就在咱们把某个元组传递给一个函数时,拆分了其中的那些值:
文件名:`src/main.rs`
```rust
`x` 便 `let` 18-7
`src/main.rs`
```rust
fn print_coordinates(&(x, y): &(i32, i32)) {
println!("当前坐标:({}, {})", x, y);
}
fn main() {
let point = (3, -5);
print_coordinates(&point);
}
```
*清单 18-7有着一些解构某个元组参数的一个函数*
此代码会打印出 `当前坐标:(3, -5)`。值 `&(3, -5)` 匹配了模式 `&(x, y)`,因此 `x` 为值 `3``y` 就是值 `-5`
由于如咱们在第 13 章中曾讨论过的,闭包与函数类似,咱们也可以函数参数清单中的这同样方式,在闭包参数清单中使用模式。
到这里咱们就已经看到了运用模式的数种方式但在咱们可使用他们的每种地方模式并非以同样方式运作。在一些地方模式必须是确凿的must be irrefutable而在别的情况下他们则可以是可证伪的can be refutable。接下来咱们就将讨论这两个概念。
## 可证伪性:某个模式有无可能匹配失败
**Refutability: Whether a Pattern Might Fail to Match**
模式有两种形式:可证伪与不可证伪的。将匹配所传递的任何可能值模式,即为 *不可证伪的irrefuatable*。一个示例即为 `let x = 5;` 语句中的 `x`;由于 `x` 会匹配任何值,而因此必定不会匹配失败。那些对某些可能的值,会匹配失败的模式,便是 *可证伪的refutable*。这样的一个示例,便是表达式 `if let Some(x) = a_value` 中的 `Some(x)`,因为若 `a_value` 中的值为 `None` 而非 `Some` 时,这个 `Some(x)` 模式就将不匹配。
函数参数、`let` 语句及 `for` 循环,就只能接受不可证伪的模式,这是由于这些情况下,当值不匹配时,程序便无法执行任何有意义的事情。`if let``while let` 表达式,接受可证伪与不可证伪的模式,但由于根据定义,他们被计划来处理可能的匹配失败:某个条件的功能,便在于其根据匹配成功或失败,而区别执行的能力,因此编译器会就不可证伪模式发出告警。
一般来说,咱们无须担心可证伪与不可证伪模式的区别;但是,咱们确实需要熟悉可证伪性的概念,这样咱们在看到报错消息时,就可以予以响应。在这些情形下,咱们将需要根据代码所预期的行为,而要么修改模式,或者修改该模式下用到的那个结构。
接下来就看看,当咱们在 Rust 要求使用不可证伪模式的地方,尝试使用某种可证伪模式,及反过来 Rust 要求使用可证伪模式,而尝试使用不可证伪模式时,会发生什么。下面清单 18-8 给出了一个 `let` 语句,不过咱们指定了一个可证伪的 `Some(x)` 模式。正如你会期待的那样,此代码将不会编译。
```rust
let Some(x) = some_option_value;
```
*清单 18-*:在 `let` 下使用可证伪模式的尝试*
`some_option_value` 是个 `None` 值,他就会与模式 `Some(x)` 匹配失败,意味着该模式为可证伪的。但是,由于此处没有可处理 `None` 值的有效代码,因此该 `let` 表达式就只能接收某个不可证伪的模式。在编译时Rust 将抱怨说咱们曾于要求不可证伪模式的某处,使用了可证伪模式:
```console
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0005]: refutable pattern in local binding: `None` not covered
--> src/main.rs:3:9
|
3 | let Some(x) = some_option_value;
| ^^^^^^^ pattern `None` not covered
|
= note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
= note: for more information, visit https://doc.rust-lang.org/book/ch18-02-refutability.html
note: `Option<i32>` defined here
= note: the matched value is of type `Option<i32>`
help: you might want to use `if let` to ignore the variant that isn't matched
|
3 | let x = if let Some(x) = some_option_value { x } else { todo!() };
| ++++++++++ ++++++++++++++++++++++
For more information about this error, try `rustc --explain E0005`.
error: could not compile `patterns` due to previous error
```
由于咱们不曾以 `Some(x)` 涵盖且无法涵盖到所以有效值Rust 便理所当然地产生了一个编译器报错。
而当咱们在需要不可证伪模式处,有着某个可证伪模式时,咱们可通过修改用到该模式的代码修复之:与其使用 `let`,咱们可以使用 `if let`。随后在该模式不匹配时,该代码就仅仅会跳过位于那花括号中代码,从而给了其有效继续的一种方式。下面清单 18-9 给出了修复清单 18-8 中代码的方式。
```rust
if let Some(x) = some_option_value {
println! ("{}", x);
}
```
*清单 18-9使用 `if let` 与带有可证伪模式代码块,而非 `let`*
咱们就已给了代码一条出路了!这段代码是完全有效的,尽管其意味着咱们在不接收到报错下,无法使用某个不可证伪模式。而在咱们给到 `if let` 某个将始终匹配的模式时,如下清单 18-10 中所示,编译器就会给出一条告警。
```rust
if let x = 5 {
println! ("{}", x);
}
```
*清单 18-10在 `if let` 下使用不可证伪模式的尝试*
Rust 会抱怨,以某个不可证伪模式使用 `if let` 没有意义:
```console
$ cargo run lennyp@vm-manjaro
Compiling refutable_demo v0.1.0 (/home/lennyp/rust-lang/refutable_demo)
warning: irrefutable `if let` pattern
--> src/main.rs:2:8
|
2 | if let x = 5 {
| ^^^^^^^^^
|
= note: this pattern will always match, so the `if let` is useless
= help: consider replacing the `if let` with a `let`
= note: `#[warn(irrefutable_let_patterns)]` on by default
warning: `refutable_demo` (bin "refutable_demo") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 0.59s
Running `target/debug/refutable_demo`
5
```
由于这个原因除了应以一个不可证伪模式匹配全部剩余值的最后支臂外其他那些匹配支臂就必须使用可证伪模式。Rust 允许咱们在仅有一个支臂的 `match` 中,使用不可证伪模式,但这种语法不是特别有用,并可以一个更简单的 `let` 语句替换。
现在,咱们就知道了哪些地方要使用模式,以及可证伪与不可证伪模式的区别,下面就来介绍所有可用于创建模式的语法。
## 模式语法
**Pattern Syntax**
在这个小节中,咱们会聚齐模式方面的全部有效语法,并讨论因何及何时会打算使用这每种的语法。
### 匹配字面值
**Matching Literals**
正如咱们在第 6 章中曾看到的那样,咱们可以直接将模式与字面值匹配。下面的代码给到了一些示例:
```rust
let x = 1;
match x {
1 => println! (""),
2 => println! (""),
3 => println! (""),
_ => println! ("万物"),
}
```
由于 `x` 里的值为 `1`,此代码会打印出 `壹`。当咱们打算代码,在其获取到某个特定具体值而采取某种动作时,这种语法就是有用的。
### 匹配命名变量
**Matching Named Variables**
命名变量属于匹配任意值的不可证伪模式,同时咱们已在本书中,用到他们许多次了。不过,当咱们在 `match` 表达式中使用命名变量时,便有着一种复杂性。由于 `match` 关键字开启了一个新的作用域,作用模式部分,而该 `match` 表达式内部声明出的那些变量,将遮蔽该 `match` 结构the `match` construct 外部那些有着同意名字的变量,这与所有变量下的情况一样。在下面清单 18-11 中,咱们以值 `Some(5)` 声明了名为 `x` 的一个变量,及有着值 `10` 的一个变量 `y`。随后咱们在值 `x` 上创建了一个 `match` 表达式。请注意那些匹配支臂中的模式与末尾处的 `println!`,并在运行此代码或阅读接下来的内容前,尝试得出该代码将打印出什么。
文件名:`src/main.rs`
```rust
let x = Some(5);
let y = 10;
match x {
Some(50) => println! ("得到了 50"),
Some(y) => println! ("已匹配y = {y}"),
_ => println! ("默认情况x = {:?}", x),
}
println! ("最后x = {:?}, y = {y}", x);
```
*清单 18-11有着引入了遮蔽变量 `y` 的一条支臂的 `match` 表达式*
下面就来走一遍,在这个 `match` 表达式运行时会发生些什么。首个匹配支臂中的模式不会匹配 `x` 所定义的值,因此代码会继续。
第二个匹配支臂中,引入了名为 `y` 新变量的那个模式,将匹配某个 `Some` 值内部的任意值。由于咱们是在这个 `match` 表达式内部的新作用域中,因此这就是个新的 `y` 变量,而不再是开头的以值 `10` 定义的 `y` 了。这个新的 `y` 绑定,将匹配某个 `Some` 内不的任意值,那便是咱们在 `x` 中所拥有的那个值了。因此,这个新 `y` 就绑定到了 `x` 中那个 `Some` 的内层值。那个值为 `5`,因此那个支臂的表达式就会执行,并打印出 `已匹配y = 5`
而若 `x` 曾为 `None` 值而非 `Some(5)`,那么头两个支臂中的模式,就都不会匹配,而该值将与其中的下划线 `_` 匹配。咱们并未以那个下划线模式,引入这个 `x` 变量,因此该表达式中的 `x` 仍为未被遮蔽的外层 `x`。而在这个假定情况中,该 `match` 将打印出 `默认情况x = None`
在这个 `match` 表达式完成是,他的作用域就结束了,而内层作用域的 `y` 也结束了。最后的 `println!` 会产生出 `最后x = Some(5), y = 10`
为创建出比较外层作用域中 `x``y` 值的一个 `match` 表达式而非引入一个遮蔽变量咱们将需要使用某种匹配卫兵条件a match guard conditional。稍后咱们将在 [“带有匹配保护的额外条件”](#使用匹配卫兵的额外条件extra-conditionals-with-match-guards) 小节,讨论到匹配保护问题。
### 多个模式
**Multiple Patterns**
`match` 表达式中,咱们可以使用 `|` 语法,即模式 *or* 运算符,匹配多个模式。比如,在下面的代码中,咱们把 `x` 的值与那些匹配支臂匹配,其中头一个支臂就有一个 *or* 选项,表示在 `x` 的值与那条支臂中两个值之一匹配时,那条支臂的代码都将运行:
```rust
let x = 1;
match x {
1 | 2 => println! ("一或二"),
3 => println! (""),
_ => println! ("万物"),
}
```
此代码会打印出 `一或二`
### 使用 `..=` 匹配值范围
**Matching Ranges of Values with `..=`**
这种 `..=` 语法允许咱们与某个包容性值范围匹配match to an inclusive range of values。下面的代码中当某个模式匹配给定范围中任何值时那条支臂便会执行
```rust
let x = 5;
match x {
1..=5 => println! ("一到五"),
_ => println! ("万物"),
}
```
`x``1, 2, 3, 4``5` 时,头一条支臂将匹配。相比于使用 `|` 运算符,对于多个匹配值,这种语法更便于表达同样的概念;若咱们使用的是 `|`,那么将不得不指明 `1 | 2 | 3 | 4 | 5`。而指明一个范围就简短多了,尤其是在打算匹配比如任何 `1``1000` 之间的数字时!
编译器会在编译时检查范围不为空,而由于 Rust 可识别出某个范围为空或不为空的类型,就只有 `char` 与数字值,因此就只运行数字或 `char` 值两种范围。
下面是使用 `char` 值范围的一个示例:
```rust
let x = 'c';
match x {
'a'..='j' => println! ("靠前 ASCII 字母"),
'k'..='z' => println! ("靠后 ASCII 字母"),
_ => println! ("其他东西"),
}
```
Rust 能分辨出 `c` 是在头一个模式的范围内,并打印出 `靠前 ASCII 字母`
### 将值拆散的解构
**Destructuring to Break Apart Values**
咱们还可以运用模式,来解构结构体、枚举及元组,从而用到这些值的不同部分。下面就来贯穿这各个的值。
**解构结构体Destructuring Stucts**
下面清单 18-12 给出了咱们可使用带有一个 `let` 语句的模式,而予以拆散的、有着两个字段,`x``y` 的一个 `Point` 结构体。
文件名:`src/main.rs`
```rust
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: -7 };
let Point { x: a, y: b } = p;
assert_eq! (0, a);
assert_eq! (-7, b);
}
```
*清单 18-12将结构体的那些字段解构为一些单独变量*
这段代码创建出匹配结构体 `p``x``y` 字段的变量 `a``b`。此示例展示了模式中变量的名字,不必匹配结构体的字段名字。但是,将变量名字与字段相匹配,以令到更易与记住哪些变量来自那个字段,则是通常做法。由于这种普遍用法,同时由于写下 `let Point { x: x, y: y } = p;`包含了很多重复Rust 便有了匹配结构体字段模式的一种简写:咱们只需列出结构体字段的名字,那么自该模式创建出的那些变量,就将有着这些同样名字。下面清单会与清单 18-12 中的代码,以同样方式行事,不过在那个 `let` 模式中创建出的变量,为 `x``x`,而不再是 `a``b` 了。
文件名:`src/main.rs`
```rust
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: -7 };
let Point { x, y } = p;
assert_eq! (0, x);
assert_eq! (-7, y);
}
```
*清单 18-12运用结构体字段简写解构结构体字段*
此代码创建了与变量 `p``x``y` 字段相匹配的变量 `x``y`。结果便是变量 `x``y` 包含着来自结构体 `p` 的那些值。
咱们也能以一些字面值,作为结构体模式的部分,而非创建出所有字段的变量,而加以解构。这样做允许咱们在创建出一些变量来解构其他字段的同时,而测试一些字段。
在下面清单 18-14 中,咱们有着一个将 `Point` 值分离到三种情形的一个 `match` 表达式:直接位于 `x` 轴上的那些点(在 `y = 0` 时此模式为真)、在 `y` 轴上的那些点,或既不在 `x` 也不在 `y` 轴上的那些点。
文件名:`src/main.rs`
```rust
let p = Point { x: 0, y: -7 };
match p {
Point { x, y: 0 } => println! ("在 x 轴的 {x}"),
Point { x: 0, y } => println! ("在 y 轴的 {y}"),
Point { x, y } => {
println! ("不在两个轴上:({x}, {y})");
}
}
```
*清单 18-14同时在一个模式中的解构与字面值匹配*
首个支臂通过指明 `y` 字段在其值与字面值 `0` 匹配时匹配,而将匹配位于 `x` 轴上任意点。该模式仍创建了咱们可在此支臂代码中用到的变量 `x`
类似地,第二条支臂通过指明 `x` 字段在其值为 `0` 时匹配,而会匹配位于 `y` 轴上的任意点,同时创建处 `y` 字段值的一个变量 `y`。第三条支臂没有指定任何字面值,因此其会匹配全部其他 `Point`,并创建出 `x``y` 字段的两个变量。
在此示例中,值 `p` 会由于 `x` 包含着一个 `0`,而匹配第二条支臂,从而此代码将打印出 `在 y 轴的 -7 处`.
请记住 `match` 表达式一旦找到第一个匹配的模式,就会停止检查支臂了,因此尽管 `Point { x: 0, y: 0 }` 是在 `x` 轴与 `y` 轴上,此代码将只打印出 `在 x 轴的 0 处`
**解构枚举Destructuring Enums**
本书中咱们已经解构过枚举(比如,第 6 章中的清单 6-5但尚未明确讨论过以与存储在枚举内部数据所定义方式的相对应方式来解构某个枚举的模式。作为一个示例在下面清单 18-15 中,咱们使用清单 6-2 中的那个 `Message` 枚举,并编写了带有将解构各个内部值的一个 `match` 表达式。
文件名:`src/main.rs`
```rust
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(u8, u8, u8),
}
fn main() {
let msg = Message::ChangeColor(0, 160, 255);
match msg {
Message::Quit => {
println! ("Quit 变种没有要解构的数据。");
}
Message::Move { x, y } => {
println! {"在 x 方向移动 {x},在 y 方向移动 {y}"};
}
Message::Write(text) => {
println! ("文本消息:{text}");
}
Message::ChangeColor(r, g, b) => {
println! ("把颜色改为 红 {r},绿 {g},和蓝 {b}");
}
}
}
```
*清单 18-15解构保存着不同类别值的枚举变种*
此代码将打印出 `把颜色改为 红 0绿 160和蓝 255`。请尝试修改 `msg` 的值,来看到该代码自其他支臂运行。
对应不带任何数据的那些枚举变种,像是 `Message::Quit`,咱们就无法进一步解构值。咱们只能匹配字面的 `Message::Quit` 值,且在那个模式中没有变量。
对于类似结构体的枚举变量,好比 `Message::Move`,咱们可以使用类似于指明用于匹配结构体的那种模式。在变种名字之后,咱们放置了一对花括号,并在随后列出有着变量的那些字段,从而咱们就拆散了要在此支臂代码中用到的各个部分。这里咱们运用了曾在清单 18-13 中曾用过的简写形式。
而对于类似元组的那些枚举变种,好比保存着有一个元素元组 `Message::Write` 与保存着有三个元素元组的 `Message::ChangeColor`,其模式便于指定用于匹配元组的模式类似。模式中的变量个数,务必要与咱们所匹配的变种中元素个数相匹配。
**嵌套结构体与枚举的解构Destructuring Nested Structs and Enums**
到目前为止,咱们的这些示例都匹配的是一层深的结构体与枚举,而匹配也是能够在嵌套项目上工作的!比如,咱们可将清单 18-15 中的代码,重构为在 `ChangeColor` 消息中,支持 RGB 与 HSV 两种颜色,如下清单 18-16 中所示:
文件名:`src/main.rs`
```rust
enum Color {
Rgb(u32, u32, u32),
Hsv(u32, u32, u32),
}
emum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(Color),
}
fn main() {
let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));
match msg {
Message::ChangeColor(Color::Rgb(r, g, b)) => {
println! ("将颜色改为红 {r}、绿 {g} 及蓝 {b}");
}
Message::ChangeColor(Color::Hsv(h, s, v)) => {
println! ("将颜色改为色调 {h}、饱和度 {s} 及颜色值 {v}");
}
_ => (),
}
}
```
*清单 18-16嵌套枚举上的匹配*
`match` 表达式中首个支臂的模式,匹配包含着 `Color::Rgb` 变种的 `Message::ChangeColor` 枚举变种;随后该模式绑定到那三个内部的 `i32` 值。第二支臂的模式,同样匹配的是 `Message::ChangeColor` 枚举变种,只不过那内部枚举匹配的是 `Color::Hsv` 了。咱们可在一个 `match` 表达式中,指定这些复杂条件,即使涉及到两个枚举。
**解构结构体与元组Destructing Structs and Tuples**
咱们甚至可以更复杂的方式,对解构模式进行混用、匹配及嵌套。下面的示例,给出了一种复杂的解构,其中在一个元组中,嵌套了结构体与元组,并讲全部原生值解构了出来:
```rust
let ((feet, inches), Point {x, y}) = ((3, 10), Point { x: 3, y: -10});
```
此代码实现将复杂类型,拆分为其各个组件部分,从而咱们可单独使用咱们所感兴趣的那些值。
以模式来解构数据,是各自独立使用诸如结构体中各个字段值,此类各部分值的一种便利方式。
### 忽略模式中的某些值Ignoring Values in a Pattern
咱们已经看到,某些时候忽略模式中的一些值是有用的,比如在为获取到不具体完成任何事情,而确实处理全部剩余可能值的捕获全部的 `match` 最后支臂中。有少数几种忽略模式中全部或部分值的方式:使用 `_` 模式the `_` pattern, 咱们已经见到过)、在另一模式中使用 `_` 模式、使用以下划线开头的名字,或使用 `..` 来忽略某个值的其余部分。下面就来探讨,怎样及为何要使用各个的这些模式。
**以 `_` 忽略整个值Ignoring an Entire Value with `_`**
咱们已把这个下划线,作为将匹配任意值,却不绑定到该值的通配符模式进行了使用。这作为 `match` 表达式中的最后支臂尤其有用,但咱们也可在任意模式中用他,包括一些函数参数中,如下清单 18-17 中所示。
文件名:`src/main.rs`
```rust
fn foo(_: i32, y: i32) {
println! ("此代码仅使用那个参数 y{}", y);
}
fn main() {
foo(3, 4);
}
```
*清单 18-17在函数签名中使用 `_`*
此代码将完全忽略作为第一个参数传递的值 `3`,并将打印 `此代码仅使用那个参数 y4`
在当不再需要某个特定函数参数的大多数情况下,咱们就会修改函数签名,从而其不会包含未用到的参数。而在比如正实现某个特质时,需要某种确切类型签名,而咱们的实现中函数体不需要某个的这些参数,这样的情形中,忽略某个函数参数就会特别有用。随后咱们便避免了收到关于未使用的函数参数的编译器告警,这样的告警在使用某个参数名字时就会收到。
**使用嵌套的 `_` 忽略某个值的部分Ignoring Parts of a Value with a Nested `_`**
在另一模式内部,咱们也可以使用 `_` 来仅忽略某个值的部分,比如当咱们打算仅测试某个值的部分,而在打算运行的相应代码中用不到其他部分时。下面清单 18-18 给出了负责管理某个设置值的代码。业务方面的要求为不应允许用户覆写某项设置的某个既有定制设置,但可以取消该项设置并在其当前未设置时给予其某个值。
```rust
let mut setting_value = Some(5);
let new_setting_value = Some(10);
match (setting_value, new_setting_value) {
(Some(_), Some(_)) => {
println! ("无法覆写既有定制值");
}
_ => {
setting_value = new_setting_value;
}
}
println! ("设置项值为 {:?}", setting_value);
```
*清单 18-18当咱们不需要用到 `Some` 中的值时,在匹配一些 `Some` 变种的模式里使用下划线*
此代码将打印 `无法覆写既有定制值`,及随后的 `设置项值为 Some(5)`。在首个匹配支臂中,咱们无需匹配或是使用两个 `Some` 变种里的那些值,但确实需要就 `setting_value``new_setting_value``Some` 变种时的情形,加以测试。在那样的情形下,咱们会打印出不修改 `setting_value`,以及其不会被修改的理由。
在由第二支臂中 `_` 模式所表示的全部其他情形下(即 `setting_value``new_setting_value``None` 时),咱们就打算允许 `new_setting_value` 成为 `setting_value`
咱们还可以在一个模式里的多处,使用下划线来忽略一些特定值。下面清单 18-19 给出了忽略某五个项目元组中,第二与第四个值的示例。
```rust
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, _, third, _, fifth) => {
println! ("一些数字为: {first}, {third}, {fifth}");
}
}
```
*清单 18-19忽略元组的多个部分*
此代码将打印 `一些数字为: 2, 8, 32`,而值 `4``16` 将被忽略。
**通过以 `_` 开头的名字忽略某个未用到的变量Ignoring an Unused Variable by Starting Its Name with `_`**
在咱们创建了某个变量,但未在任何地方用到他时,由于未使用变量可能是代码问题,因此 Rust 通常将发出一条告警。然而,有的时候创建出尚未用到的某个变量则是有用的,比如在咱们正构造程序原型,或刚开始某个项目时。在这种情况下,咱们就可以通过以一个下划线,开启该变量的名字,而告诉 Rust 不要就这个未用到变量发出告警。下面清单 18-20 中,咱们创建了两个未使用变量,但在编译此代码时,咱们应只收到他们中一个的告警。
```rust
fn main() {
let _x = 5;
let y = 10;
}
```
*清单 18-20以一个下划线开始变量名来避免收到未使用变量的告警*
这里咱们会得到有关未用到变量 `y` 的告警,但不会收到未使用的 `_x` 的告警。
请注意在仅使用 `_` 与使用以下划线开头的名字之间,有着细微差别。`_x` 的语法仍将该值绑定到变量,而 `_` 则完全没有绑定。为给出其中这种区别重要性的情形,下面清单 18-21 将给到咱们一个报错。
```rust
let s = Some(String::from("你好!"));
if let Some(_s) = s {
println! ("找到一个字符串");
}
println! ("{:?}", s);
```
*清单 18-21以下划线开头的未使用变量仍会绑定值这就会取得该值的所有权*
由于这个 `s` 值将仍被迁移到 `_s` 中,而这会阻止咱们再度使用 `s`,因此咱们将收到一个报错。然而,使用下划线本身,就绝不会绑定到值。由于下面清单 18-22 中的 `s` 不会被迁移到 `_` 中,因此该代码将不带任何错误的编译。
```rust
let s = Some(String::from("你好!"));
if let Some(_) = s {
println! ("找到一个字符串");
}
println! ("{:?}", s);
```
*清单 18-22使用下划线不会绑定值*
由于咱们绝不会把 `s` 绑定到任何变量,他就没有被迁移,进而此代码工作良好。
**使用 `..` 忽略值的剩余部分Ignoring Remaining Parts of a Value with `..`**
对于有着许多部分的值,咱们可以使用 `..` 语法来使用其特定部分而忽略剩下部分,避免列出各个忽略值那些下划线这样的需求。这种 `..` 模式,会忽略咱们在模式其余部分中,未曾显示匹配的任何部分。在下面清单 18-23 中,有着一个保存了三维空间中坐标的 `Point` 结构体。在那个 `match` 表达式中,咱们打算只在 `x` 坐标上运算,而忽略 `y``z` 两个字段中的值。
```rust
struct Point {
x: i32,
y: i32,
z: i32,
}
let origin = Point { x: 0, y: 0, z: 0 };
match origin {
Point { x, .. } => println! ("x 为 {}", x),
}
```
*清单 18-23通过使用 `..` 忽略 `Point` 中除 `x` 外的全部字段*
咱们列出了值 `x` 并在随后只包含了模式 `..`。这要比列出 `y: _``z: _` 要快一些,尤其是当咱们在处理那些有着很多字段,而其中只有一两个字段是攸关的情形下。
`..` 语法将扩展到其所需的那么多个值。下面清单 18-24 给出了怎样在元组下使用 `..`
文件名:`src/main.rs`
```rust
let numbers = (2, 4, 6, 8, 16, 32);
match numbers {
(first, .., last) => {
println! ("一些数字为: {first}, {last}");
}
}
```
*清单 18-24匹配元组中首个与最后值而忽略全部其他值*
在此代码中,首个与最后值,是以 `first``last` 匹配到的。其中的 `..` 将匹配并忽略中间的全部值。
不过,使用 `..` 必须必须要是明确的。在不明白哪些值是要匹配的哪些值应被忽略时Rust 就将给到我们一个报错。下面清单 18-25 给出了含混不清地使用 `..` 的一个示例,因此其不会编译。
文件名:`src/main.rs`
```rust
fn main() {
let numbers = (2, 4, 6, 8, 16, 32);
match numbers {
(.., second, ..) => {
println! ("一些数字为: {}", second);
}
}
}
```
*清单 18-25尝试以模棱两可方式使用 `..`*
当咱们编译此示例时,就可到下面这个报错:
```console
$ cargo run
Compiling pattern_syntax_demo v0.1.0 (/home/lenny.peng/rust-lang/pattern_syntax_demo)
error: `..` can only be used once per tuple pattern
--> src/main.rs:5:22
|
5 | (.., second, ..) => {
| -- ^^ can only be used once per tuple pattern
| |
| previously used here
error: could not compile `pattern_syntax_demo` due to previous error
```
Rust 不可能确定出在以 `second` 匹配某个值之前,元组中有多少个值要忽略,并随后在那之后又有多少个值要忽略。此代码可能是指咱们打算忽略 `2`,将 `second` 绑定到 `4`,并随后忽略 `8``16``32`;或是指咱们打算忽略 `2``4`,将 `second` 绑定到 `8`,并随后忽略 `16``32`;如此等等。名为 `second` 的变量,对于 Rust 并不表示任何特殊的东西,从而由于在两处使用 `..` 属于模棱两可的,因此咱们就收到一个编译报错。
### 使用匹配卫兵的额外条件Extra Conditionals with Match Guards
所谓 *匹配卫兵match guard*,是于 `match` 支臂之后被指定出来,对于这条支臂要被选中,而也必须匹配的一个额外 `if` 条件。对于表达相对于所允许的单独模式,更为复杂的一些概念,这样的匹配卫兵就是有用的。
该条件可使用模式中创建出的那些变量。下面清单 18-26 给出了其中首条支臂有着模式 `Some(x)`,并同时有着 `if x % 2 == 0` 的匹配卫兵(在该数为偶数时将为 `true` )的一个 `match`
```rust
let num = Some(4);
match num {
Some(x) if x % 2 == 0 => println! ("数字 {} 为偶数", x),
Some(x) => println! ("数字 {} 为奇数", x),
None => (),
}
```
*清单 18-26添加匹配卫兵到模式*
此示例将打印 `数字 4 为偶数`。在 `num` 与首个支臂中的模式相比时,由于 `Some(4)` 匹配了 `Some(x)`,因此他就匹配了。随后那个匹配卫兵就会检查 `x` 除以 `2` 的余数是否等于 `0`,而由于 `4` 除以 `2` 确实等于零,所以首个支臂便被选中了。
`num` 改作 `Some(5)`,那么由于 `5` 除以 `2` 的余数为 `1`,而不等于 `0`,那么首个支臂中的匹配卫兵将为 `false`。Rust 随后就会移步到第二支臂,由于第二支臂没有匹配卫兵,而因此会匹配任意 `Some` 变种,那么这第二支臂就会匹配到。
某个支臂里没有表达 `if x % 2 == 0` 的方式,因此这种匹配卫兵特性,便给到我们表达这种逻辑能力。这种额外表达力的缺点,便是在涉及到匹配卫兵时,编译器不会尝试检查完备性。
清单 18-11 中咱们曾提到咱们可以使用匹配卫兵来解决咱们的模式遮蔽问题pattern-shadowing problem。回顾到咱们曾在那个 `match` 表达式中的支臂里,创建了一个新变量,而不是使用 `match` 外的那个变量。那个新变量就意味着咱们无法将其与其中的外层变量进行比对测试了。下面清单 18-27 给出了咱们怎样能使用匹配卫兵,修复这个问题。
文件名:`src/main.rs`
```rust
fn main() {
let x = Some(5);
let y = 10;
match x {
Some(50) => println! ("得到了 50"),
Some(n) if n == y => println! ("匹配了n = {n}"),
_ => println! ("默认情况x = {:?}", x),
}
println! ("最后x = {:?}, y = {y}", x);
}
```
*清单 18-27使用匹配卫兵测试与外层变量是否相等*
此代码现在将打印 `默认情况x = Some(5)`。第二匹配支臂中的模式,没有引入将遮蔽外层 `y` 的新变量 `y`,意味着咱们可以在其中的匹配卫兵中使用那个外层的 `y`。与其将模式指明为将遮蔽外层 `y``Some(y)`,咱们指明的是 `Some(n)`。由于在这个 `match` 外没有变量 `n`,因此这创建了一个不会遮蔽任何东西的变量 `n`
其中的匹配卫兵 `if n == y` 不是个模式,而因此不会引入新的变量。这个 `y` *便是* 外层的 `y`,而非一个新遮蔽的 `y`,进而咱们可以通过将 `n``y` 比较,查找与这个外层的 `y` 有着同样值的一个值。
咱们还可在匹配卫兵中,使用 *or* 运算符 `|`,来指定多个模式;匹配卫兵条件将应用到全部这些模式。下面清单 18-28 展示了将使用了 `|` 的模式,与匹配卫兵结合时的优先级。这个示例的重要之处是,其中的 `if y` 匹配卫兵,会应用到 `4``5` ** `6`,即使看起来 `if y` 只应用到 `6`
```rust
let x = 4;
let y = false;
match x {
4 | 5 | 6 if y => println! (""),
_ => println! (""),
}
```
*清单 18-28将多个模式与匹配卫兵相结合*
其中的匹配条件指出,该支臂仅在 `x` 的值等于 `4``5``6` **`y``true` 时匹配。在此代码运行时,由于 `x``4`,因此首条支臂的模式会匹配,但匹配卫兵 `if y``false`,从而首条支臂未被选中。代码就移步到第二支臂,其就匹配了,而此程序就打印出 `否`。原因就是,其中的 `if` 条件会应用到整个模式 `4 | 5 | 6`,而不仅是应用到最后的值 `6`。也就是说,匹配守卫相对于模式的优先级表现如下:
```rust
(4 | 5 | 6) if y => ...
```
而非这样:
```rust
4 | 5 | (6 if y) => ...
```
在运行此代码后,这种优先级行为便是显而易见的了:若那个匹配卫兵,只被应用到使用 `|` 运算符所指定的值清单中的最后那个值,那么该支臂将匹配,而这个程序就会打印出 `是`
### `@` 绑定,`@` Bindings
*地址at* 运算符 `@` 实现了在咱们将某个值与模式匹配测试的同时,创建出保存该值的一个变量来。在下面清单 18-29 中,咱们打算测试某个 `Message::Hello``id` 是否在范围 `3..=7` 中。咱们还要将该值绑定到变量 `id_variable`,从而咱们可以在与该支臂相关的代码中使用他。咱们可将这个变量命名为 `id`,与那个字段相同,而对于这个示例,咱们将使用不同的名字。
```rust
fn main() {
enum Message {
Hello { id: u32 },
}
let msg = Message::Hello { id: 5 };
match msg {
Message::Hello {
id: id_variable @ 3..=7,
} => println! ("找到位于范围内的一个 id: {}", id_variable),
Message::Hello { id: 10..=12 } => {
println! ("找到位于另一范围的一个 id");
},
Message::Hello { id } => println! ("找到别的一个 id: {}", id),
}
}
```
*清单 18-29于模式中在测试某个值的同时使用 `@` 将其加以绑定*
这个示例将打印 `找到位于范围内的一个 id: 5`。通过在范围 `3..=7` 前指明 `id_variable @`,咱们在测试该值与这个范围匹配的同时,捕获了与该范围匹配的任何值。
在第二支臂中,那里咱们只在模式中指定了一个范围,与该支臂相关的代码,就不会有包含了这个 `id` 字段具体值的一个变量。这个 `id` 字段的值,可能是 `10``11``12`,但那个支臂下的代码却不清楚其为何。由于咱们不曾将那个 `id` 值保存在某个变量中,模式代码便无法使用 `id` 字段的值。
在最后支臂中,那里咱们指定了一个不带范围的变量,咱们确实令到了这个值,在该支臂代码中一个名为 `id` 的变量里可供使用。原因在于咱们使用了结构体字段速记语法the struct field shorthand syntax。不过咱们不曾在这个支臂中应用任何测试到这个 `id` 字段中的值,正如咱们对前两个支臂所做的那样:那么所有值都将匹配这个支臂。
运用 `@` 实现了在一个模式里,对某个值的测试,并将其保存在某个变量中。
## 本章小结
Rust 的模式,在区分不同类别数据方面非常有用。当在 `match` 表达式中用到模式时Rust 就会确保咱们的那些模式,涵盖每个可能的值,否则咱们的程序便不会编译。`let` 语句与函数参数中的模式,会令到这两种结构更为有用,在实现值解构为一些更小的部分的同时,赋值给一些变量。咱们可以创建出简单抑或复杂的模式,来适合咱们的需求。
接下来,作为本书倒数第二章,咱们将数种 Rust 特性中,一些高级的方面。

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,949 +3,6 @@
以下小节包含了在咱们的 Rust 路途中,会发现有用的一些参考资料。
## 附录 A关键字
End
以下清单包含了 Rust 语言当前或今后要用到的一些关键字。由此,他们便不能被用作标识符(除在 [“原始标识符”](#原始标识符) 小节中咱们将讨论的那些外)了。所谓标识符,是函数、变量、参数、结构体字段、模组、代码箱、常量、宏、静态值、属性、类型、特质或生命周期等的名字。
### 当前在用的关键字
下面是当前在用关键字的清单,带有其作用描述。
- `as` - 执行原生强制转换primitive casting消除包含着某个项目的特定特质歧义disambiguate the specific trait containing a item或重命名 `use` 语句中的项目;
- `async` - 返回一个 `Future` 类型值,而非阻塞当前线程;
- `await` - 在某个 `Future` 值的结果准备好前,暂停程序执行;
- `break` - 立即退出某个循环;
- `const` - 定义出常量项目或常量原始指针;
- `continue` - 继续下一循环迭代;
- `crate` - 在模组路径中,指向代码箱根;
- `dyn` - 动态调遣到某个特质对象,参考 [特质对象执行动态调遣](Ch17_Object_Oriented_Programming_Features_of_Rust.md#特质对象执行动态调遣);
- `else` - `if` 的回退,及 `if let` 控制流的构件;
- `extern` - 链接外部函数或变量;
- `false` - 布尔值假的字面值;
- `fn` - 定义出某个函数或函数指针类型;
- `for` - 对某个迭代器的项目加以迭代、实现某个特质或指明某个更高级别的生命周期a higher-ranked lifetime;
- `if` - 基于某个条件表达式结果的分支;
- `impl` - 实现固有或特质功能implement inherent or trait functionality;
- `in` - `for` 循环语法的一部分;
- `let` - 绑定某个变量;
- `loop` - 无条件地循环;
- `match` - 将某个值与模式匹配;
- `mod` - 定义出模组;
- `move` - 领导闭包取得其所有捕获值的所有权;
- `mut` - 注解出引用、原始指针或模式绑定等中的可变性;
- `pub` - 注解出结构体、`impl` 代码块或模组等中的公开可见性;
- `ref` - 按引用绑定;
- `return` - 自函数返回值;
- `Self` - 咱们正定义或实现中类型的类型别名;
- `self` - 方法主体method subject或当前模组
- `static` - 在整个程序执行过程持续有效的全局变量或生命周期;
- `struct` - 定义出某个结构体;
- `super` - 当前模组的父模组;
- `trait` - 定义出某个特质;
- `true` - 布尔值真的字面值;
- `type` - 定义出某个类型别名或关联类型;
- `union` - 定义出某个 [联合体](https://doc.rust-lang.org/reference/items/unions.html),是在联合体声明时用到的唯一关键字;
- `unsafe` - 注解非安全代码、函数、特质或一些实现;
- `use` - 将符号带入到作用域;
- `where` - 注解约束某个类型的子句;
- `while` - 基于某个表达式结果而有条件的循环。
### 为今后使用保留的关键字
以下关键字尚无任何功能,但被 Rust 为今后的潜在使用而保留。
- `abstract`
- `become`
- `box`
- `do`
- `final`
- `macro`
- `override`
- `priv`
- `try`
- `typeof`
- `unsized`
- `virtual`
- `yield`
### 原始标识符
*原始标识符raw identifiers* 属于允许实现使用一般不被允许关键字的语法。是通过在关键字前加上前缀 `r#`,使用原始标识符的。
比如,`match` 是个关键字。在咱们尝试编译下面这个使用 `match` 作其名字的函数时:
文件名:`src/main.rs`
```rust
fn match(needle: &str, haystack: &str) -> bool {
haystack.contains(needle)
}
```
咱们将得到这样的报错:
```console
error: expected identifier, found keyword `match`
--> src/main.rs:1:4
|
1 | fn match(needle: &str, haystack: &str) -> bool {
| ^^^^^ expected identifier, found keyword
```
该报错显示咱们无法将关键字 `match` 用作函数标识符。要将 `match` 用作函数名字,咱们就需要使用原始标识符语法,像下面这样:
文件名:`src/main.rs`
```rust
fn r#match(needle: &str, haystack: &str) -> bool {
haystack.contains(needle)
}
fn main() {
assert! (r#match("foo", "foobar"));
}
```
此代码将不带任何错误地编译。请注意那个函数的定义中,与 `main` 中该函数被调用处其名字上的 `r#` 前缀。
原始标识符实现了将任何咱们所选的词语用作标识符,即使那个词语碰巧是个保留的关键字。这给到咱们更自由地选择标识符名字,以及实现与一些以其中这些词语不属于关键字的语言,所编写的程序集成。此外,原始标识符实现了,对那些以不同于咱们代码箱 Rust 版本编写库加以运用。比如,在 2015 版中 `try` 就不是个关键字,但在 2018 版本中却是。若咱们依赖于一个使用 2015 版本编写的库,而该库有一个 `try` 函数,那么咱们就将需要在这种情况下,使用原始标识符 `r#try`,来从咱们的 2018 版本的代码,调用那个函数。请参阅 [附录 E](#appendix-e) 了解更多有关版本的信息。
## 附录 B运算符与符号
此附录包含了 Rust 语法的词汇表,包括运算符及别的一些,自己单独出现或出现于路径、泛型、特质边界、宏、属性、注释、元组及方括符等上下文中的符号。
### 运算符
表 B-1 包含了 Rust 中的符号、该符号将如何出现于上下文中的一个示例、简单的解释,以及该运算符是否可过载。若某个运算符可以过载,就会列出过载那个运算符要用到的相关特质。
**<small>表 B-1运算符</small>**
| 运算符 | 示例 | 说明 | 是否可以过载 |
| :--- | :--- | :--- | :--- |
| `!` | `ident! (...)` <br /> `ident! {...}` <br /> `ident! [...]` | 宏扩展 | |
| `!` | `!expr` | 按位或逻辑求补运算 | 否 |
| `!=` | `expr != expr` | 不等比较 | `PartialEq` |
| `%` | `expr % expr` | 算术求余运算 | `Rem` |
| `%=` | `var %= expr` | 算术求余并赋值 | `RemAssign` |
| `&` | `&expr`, `&mut expr` | 借用 | |
| `&` | `&type`, `&mut type`, `&'a type`, `&'a mut type` | 借用指针类型 | |
| `&` | `expr & expr` | 按位与AND运算 | `BitAnd` |
| `&=` | `var &= expr` | 按位与AND运算并赋值 | `BitAndAssign` |
| `&&` | `expr && expr` | 短路逻辑与AND运算short-circuit logical AND | |
| `*` | `expr * expr` | 算术乘法运算 | `Mul` |
| `*=` | `var *= expr` | 算术乘法运算并赋值 | `MulAssign` |
| `*` | `*expr` | 解引用运算 | `Deref` |
| `*` | `*const type`, `*mut type` | 原始指针运算 | |
| `+` | `trait + trait`, `'a + trait` | 复合类型约束运算 | |
| `+` | `expr + expr` | 算术加法运算 | `Add` |
| `+=` | `var += expr` | 算术加法运算并赋值 | `AddAssign` |
| `,` | `expr, expr` | 参数与元素分隔符 | |
| `-` | `- expr` | 算术取反运算 | `Neg` |
| `-` | `expr - expr` | 算术减法运算 | `Sub` |
| `-=` | `var -= expr` | 算术减法运算并赋值 | `SubAssign` |
| `->` | `fn(...) -> type`, <code>&vert;...&vert; -> type</code> | 函数与闭包的返回值类型 | |
| `.` | `expr.ident` | 成员访问 | |
| `..` | `..`, `expr..`, `..expr`, `expr..expr` | 排除右侧的范围语法字面值 | `PartialOrd` |
| `..=` | `..=expr`, `expr..=expr` | 包含右侧范围语法字面值 | `PartialOrd` |
| `..` | `..expr` | 结构体更新语法 | |
| `..` | `variant(x, ..)`, `struct_type { x, .. }` | “等等” 模式绑定,"And the rest" pattern binding | |
| `...` | `expr...expr` | (已弃用,请使用 `..=` 代替)在模式中:包含式范围模式 | |
| `/` | `expr / expr` | 算术除法运算 | `Div` |
| `/=` | `var /= expr` | 算术除法并赋值 | `DivAssign` |
| `:` | `pat: type`, `ident: type` | 约束 | |
| `:` | `ident: expr` | 结构体字段初始化 | |
| `:` | `'a: loop {...}` | 循环标签 | |
| `;` | `expr;` | 语句及项目的终止符 | |
| `;` | `[..., len]` | 固定大小数组语法的一部分 | |
| `<<` | `expr << expr` | 向左移位运算 | `Shl` |
| `<<=` | `var <<= expr` | 向左移位运算并赋值 | `ShlAssign` |
| `<` | `expr < expr` | 小于比较 | `PartialOrd` |
| `<=` | `expr <= expr` | 小于等于比较 | `PartialOrd` |
| `=` | `var = expr`, `ident = type` | 赋值/等价equivalence | |
| `==` | `expr == expr` | 相等比较 | `PartialEq` |
| `=>` | `pat => expr` | 匹配支臂语法的一部分 | |
| `>` | `expr > expr` | 大于比较 | `PartialOrd` |
| `>=` | `expr >= expr` | 大于等于比较 | `PartialOrd` |
| `>>` | `expr >> expr` | 向右位移运算 | `Shr` |
| `>>=` | `var >>= expr` | 向右位移运算并赋值 | `ShrAssign` |
| `@` | `ident @ pat` | 模式绑定 | |
| `^` | `var ^ expr` | 按位异或运算 | `BitXor` |
| `^=` | `var ^= expr` | 按位异或运算并赋值 | `BitXorAssign` |
| <code>&vert;</code> | <code>pat &vert; pat</code> | 模式选择pattern alternatives | |
| <code>&vert;</code> | <code>expr &vert; expr</code> | 按位或OR运算 | `BitOr` |
| <code>&vert;=</code> | <code>var &vert;= expr</code> | 按位或OR运算并赋值 | `BitOrAssign` |
| <code>&vert;&vert;</code> | <code>expr &vert;&vert; expr</code> | 短路逻辑或运算Short-circuiting logical OR | |
| `?` | `expr?` | 错误传递 | |
### 非运算符的符号
**Non-operator Symbols**
以下清单包含了不以运算符发挥作用的全部符号;那就是说,他们不会表现得像函数或方法调用。
表 B-2 给出了自己单独出现,并在多种场合有效的一些符号。
**<small>表 B-2独立语法Stand-Alone Syntax</small>**
| 符号 | 说明 |
| :--- | :--- |
| `'ident` | 命名的生命周期或循环标签 |
| `...u8`, `...i32`, `...f64`, `...usize` 等等 | 指定类型的数字字面值 |
| `"..."` | 字符串字面值 |
| `r"..."`, `r#"..."#`, `r##"..."##` 等等 | 原始字符串字面值,其中的转义字符不会被处理 |
| `b"..."` | 字节字符串字面值;构造出一个字节数组而非字符串 |
| `br"..."`, `br#"..."`, `br##"..."##` 等等 | 原始字节字符串字面值,是原始与字节字符串字面值的结合 |
| `'...'` | 字符字面值 |
| `b'...'` | ASCII 字节字面值 |
| <code>&vert;...&vert; expr</code> | 闭包 |
| `!` | 发散函数下总是空的底部类型always empty bottom type for diverging functions |
| `_` | “忽略ignored” 模式绑定还用于令到整数字面值可读also used to make integer literals readable |
表 B-3 展示了出现在模组层次结构中到某个项目路径上下文中的一些符号。
**<small>表 B-3路径相关的语法</small>**
| 符号 | 说明 |
| :--- | :--- |
| `ident::ident` | 命名空间路径 |
| `::path` | 相对于代码箱根的路径(比如,某个显式绝对路径) |
| `self::path` | 相对于当前模组的路径(比如,某个显式相对路径) |
| `super::path` | 相对于当前模组父模组的路径 |
| `type::ident`, `<type as trait>::ident` | 关联的常量、函数及类型 |
| `<type>::...` | 无法直接命名的某个类型的关联项目(比如,`<&T>::...`, `<[T]>::...` 等等) |
| `trait::method(...)` | 通过命名出定义方法的类型,消除该方法调用的歧义 |
| `<type as trait>::method(...)` | 通过命名出特质与类型,消除方法调用的歧义 |
表 B-4 展示了出现在运用泛型参数上下文中的一些符号。
**<small>表 B-4泛型</small>**
| 符号 | 说明 |
| :-- | :-- |
| `path<...>` | 指明类型中的泛型参数(比如,`Vec<u8>` |
| `path::<...>`, `method::<...>` | 指明表达式中泛型、函数或方法的参数通常这被称作涡轮鱼语法turbofish比如`"42".parse::<i32>()`,关于 Rust 的 turbofish 语法,请参考:[What is Rust's turbofish](https://techblog.tonsser.com/posts/what-is-rusts-turbofish)[RUST 中的 turbofish 语法(一)](https://www.jianshu.com/p/9107685ece03) ... |
| `fn ident<...> ...` | 定义出泛型函数 |
| `struct ident<...> ...` | 定义出泛型结构体 |
| `enum ident<...> ...` | 定义出泛型枚举 |
| `impl<...> ...` | 定义出泛型实现 |
| `for<...> type` | 高阶声明周期边界higher-ranked lifetime bounds |
| `type<ident=type>` | 其中一个或更多的关联类型有着指定赋值的某种泛型a generic type where one or more associated types have specific assignments比如`Iterator<Item=T>` |
下表 B-5 展示了出现在使用特质边界的约束性泛型参数上下文中的一些符号table B-5 shows symbols that appear in the context of constraining generic type parameters with trait bounds。
**<small>B-5特质边界约束Trait Bound Constrains</small>**
| 符号 | 说明 |
| :--- | :--- |
| `T: U` | 泛型参数 `T` 受实现了 `U` 的类型约束 |
| `T: 'a` | 泛型 `T` 必须要比生命周期 `'a` 活得更久generic type `T` must outlive lifetime `'a`(意思是该类型不能间接地包含任何生命周期短于 `'a` 的引用) |
| `T: 'static` | 泛型 `T` 不包含除 `'static` 的引用外的其他引用 |
| `'b: 'a` | 泛型生命周期 `'b` 必须要比 `'a` 存活得更久 |
| `T: ?Sized` | 允许泛型参数为动态大小类型 |
| `'a + trait`, `trait + trait` | 复合的类型约束 |
下表 B-6 展示了出现在宏调用或定义上下文中,并指明了某个项目上属性的一些符号。
**<small>B-6宏与属性</small>**
| 符号 | 说明 |
| :--- | :--- |
| `#[meta]` | 外层属性 |
| `#![meta]` | 内层熟悉 |
| `$ident` | 宏代换macro substitution |
| `$ident:kind` | 宏捕获 |
| `$(...) ...` | 宏重复macro repetition |
| `ident! (...)`, `ident! {...}`, `ident! [...]` | 宏调用macro invocation |
下表 B-7 展示了创建注释的一些符号。
**<small>表 B-7注释</small>**
| 符号 | 说明 |
| :--- | :--- |
| `//` | 注释行 |
| `//!` | 内层行文档注释inner line doc comment |
| `///` | 外层行文档注释outter line doc comment |
| `/*...*/` | 注释块 |
| `/*!...*/` | 内层块文档注释inner block doc comment |
| `/**...*/` | 外层块文档注释outter block doc comment |
下表 B-8 展示了出现于用到元组上下文中的一些符号。
**<small>元组</small>**
| 符号 | 说明 |
| :--- | :--- |
| `()` | 空元组(又叫单元值),同时属于字面值与类型 |
| `(expr)` | 元括号括起来的表达式parenthesized expression |
| `(expr,)` | 单一元素的元组表达式 |
| `(type,)` | 单一元素的元组类型single-element tuple type |
| `(expr, ...)` | 元组表达式 |
| `(type, ...)` | 元组类型tuple type |
| `expr(expr, ...)` | 函数调用表达式;还用于初始化一些元组的 `struct` 以及元组的 `enum` 变种function call expression; also used to initialize tuple `struct`s and tuple `enum` vairants |
| `expr.0`, `expr.1` 等等 | 对元组进行索引 |
下表 B-9 展示了其中用到花括号上下文中的一些符号。
**<small>表 B-9花括号</small>**
| 符号 | 说明 |
| :--- | :--- |
| `{...}` | 代码块表达式 |
| `Type {...}` | `struct` 的字面值 |
下表 B-10 展示了其中用到方括号上下文中的一些符号。
**<small>表 B-10方括号</small>**
| 符号 | 说明 |
| :--- | :--- |
| `[...]` | 数组的字面值 |
| `[expr; len]` | 包含着 `expr``len` 拷贝数组的字面值 |
| `[type; len]` | 包含着 `len``type` 的实例数组的字面值 |
| `expr[expr]` | 对集合进行索引collection indexing。是可过载的 `(Index, IndexMut)`overloadable `(Index, IndexMut)` |
| `expr[..]`, `expr[a..]`, `expr[..b]`, `expr[a..b]` | 用到了 `Range``RangeFrom``RangeTo``RangeFull` 作为 “索引”的带有集合切片集合索引collection indexing pretending to be collection slicing, using `Range`, `RangeFrom`, `RangeTo`, or `RangeFull` as the "index" |
## 附录 C派生特质
**Appendix C: Derivable Traits**
本书的多个不同地方,咱们都曾讨论过 `derive` 属性,咱们可将其应用到结构体或枚举定义。`derive` 属性会在咱们以 `derive` 语法注解的类型上,生成将以某个特质自身默认实现,而实现该特质的代码。
在这个附录中,咱们会提供到标准库中,咱们可以与 `derive` 一起使用的全部特质的参考。以下各个小节均会讲到:
- 此特质将启用那些操作符与方法;
-`derive` 所提供到的该特质实现会做些什么;
- 实现该特质对那个类型意味着什么;
- 允许及不允许实现该特质的情况;
- 需要该特质操作的示例。
若咱们想要不同于由 `derive` 属性所提供的行为,请参考 [标准库文档](https://doc.rust-lang.org/std/index.html),了解如何亲自实现各个特质的详细信息。
这里列出的这些特质,只是一些由标准库所提供的,可使用 `derive` 实现于咱们类型上的那些。定义在标准库中别的一些特质,则没有什么合理的默认行为,因此是否要以对于咱们正尝试完成的东西有意义的方式,实现他们就取决于咱们自己了。
不能派生的一个特质示例便是 `Display`其为终端用户处理格式化。咱们应始终要考虑将某个类型显示给用户的恰当方式。终端用户应被允许看到该类型的哪些部分他们会发现哪些部分是相关的数据的何种形式才是与他们最为密切相关的Rust 编译器并无这种见解,因此他就无法为咱们提供到恰当的默认行为。
这个附录中所提供到的派生特质清单并不详尽:库可以为他们自己的特质实现 `derive`,从而领导咱们可使用 `derive` 的特质清单为真正开放的。实现 `derive` 设计到使用程序性宏,这在第 19 章的 [“关于宏”](Ch19_Advanced_Features.md#关于宏) 小节讲到过。
### 输出给编程者的 `Debug`
**`Debug` for Programmer Output**
`Debug` 特质实现了格式字符串中的格式化,所谓格式字符串,即咱们通过在 `{}` 里添加 `:?` 所表示的。
`Debug` 特质允许咱们为调试目的打印某种类型的实例,如此咱们以及用到咱们类型的其他编程者,就可以在程序执行的某个特定时刻,就其某个实例加以探查。
在比如用到 `assert_eq!` 宏中等情况下,`Debug` 特质便是要求使用的。`assert_eq!` 这个宏在相等断言失败时,就会打印出作为参数所给到的两个实例值,如此编程者就可以看到为何这两个实例不相等。
### 用于相等比较的 `PartialEq` 与 `Eq`
`PartialEq` 特质允许咱们比较某种类型的两个实例,来检查他们是否相等,并实现 `==``!=` 运算符的应用。
`PartialEq` 进行派生,就会实现 `eq` 方法。当 `ParitalEq` 实在结构体上实现的时,只有在两个实例的 *全部* 字段都相等时,他们才是相等的,且在有任何字段不等时,两个实例便不相等。当在枚举上派生时,枚举的各个变种与自身相等,而不等于其他任何变种。
在使用需要能够比较某个类型的两个实例是否相等的 `assert_eq!` 宏时,就需要这个 `PartialEq` 特质。
`Eq` 特质则没有方法。他的目的是要表明,所注解的类型的每个值,其值都等于他自身。尽管并非所有实现 `PartialEq` 的类型都可以实现 `Eq`,但 `Eq` 特质却只可应用到那些同时实现了 `PartialEq` 的类型。这方面的一个示例便是浮点数类型浮点数的实现就表明两个非数字the not-a-number, `NaN`)的值,是各自不相等的。
要求 `Eq` 的一个示例,就是 `HashMap<K, V>` 中的那些键,如此 `HashMap<K, V>` 就可以区分出两个键是否一致。
### 用于排序比较的 `PartialOrd` 与 `Ord`
**`PartialOrd` and `Ord` for Ordering Comparisons**
`PartialOrd` 特质实现为排序目的,而比较某种类型的那些实例。实现了 `PartialOrd` 的类型,便可与 `<``>``<=``>=` 符号一起使用了。咱们只能对那些同时实现了 `PartialEq` 的类型,应用这个 `PartialOrd` 特质。
派生 `PartialOrd`,会实现 `partial_cmp` 方法,该方法会返回一个在所给的那些值不会产生出顺序时,将为 `None` 的一个 `Option<Ordering>`。至于即使那种类型的大多数值都可被比较,但仍不会产生出顺序的值的一个示例,便是非数字(`NaN`)浮点值。在任何浮点数和非数字浮点值下调用 `partial_cmp`,都会返回 `None`
在于结构体上派生时,`PartialOrd` 会通过字段出现在结构体定义中的顺序,比较每个字段中的值,比较两个实例。而当于枚举上派生时,枚举定义中较早声明的枚举变种,被当作是小于后面所列出的那些变种的。
在比如会产生出由范围表达式所指定范围中一个随机数的, `rand` 代码箱的 `gen_range` 方法来说,`PartialOrd` 特质便是需要的。
`Ord` 特质实现对所注解类型的任何两个值,将存在有效顺序的掌握。`Ord` 特质会实现 `cmp` 方法,由于有效排序将始终可行,因此该方法返回的是 `Ordering` 而非 `Option<Ordering>`。咱们只可对那些同时实现了 `PartialOrd``Eq` (而 `Eq` 要求 `PartialEq`) 的类型,实现这个 `Ord` 特质。当于结构体及枚举上派生 `Ord` 时,`cmp` 就会以与 `PartialOrd``partial_cmp` 的派生实现同样方式行事。
要求 `Ord` 的一个示例,即为将一些值存储在 `BTreeSet<T>` 这种根据值的排序,而存储数据的数据结构中时。
### 用于复制值的 `Clone` 与 `Copy`
**`Clone` and `Copy` for Duplicating Values**
`Clone` 特质实现了显式创建值的深拷贝而该复制过程则可能涉及运行一些任意代码arbitary code与拷贝内存堆数据。请参阅第 4 章中 [“变量与数据交互方式:克隆”](Ch04_Understanding_Ownership.md#变量与数据交互方式之二克隆) 小节,了解更多有关 `Clone` 的信息。
派生 `Clone` 会实现 `clone` 方法,当对整个类型实现了这个方法时,其就会在该类型的各个部分上调用 `clone`。这意味着类型要派生 `Clone` 其中的全部字段或值,都必须同时实现 `Clone`
需要 `Clone` 特质的一个示例,便是在切片上调用 `to_vec` 方法时。切片不持有其包含的那些类型实例,但自 `to_vec` 所返回的那个矢量值,却将需要持有他的那些实例,从而 `to_vec` 会调用各个条目上的 `clone`。因此,存储在切片中的类型,就必须实现 `Clone`
`Copy` 特质实现了只通过拷贝存储在栈上的二进制位,而复制某个值;任意代码并无必要。请参阅第 4 章中 [“唯栈数据:拷贝”](Ch04_Understanding_Ownership.md#唯栈数据拷贝stack-only-data-copy),了解更多有关 `Copy` 的信息。
`Copy` 特质没有定义阻止编程者过载那些方法,及破坏不会有任意代码运行这个假设的任何方法。那样的话,所有编程者就都可以假定,拷贝值将会非常快。
咱们可在其组成部分都实现了 `Copy` 的任何类型上派生 `Copy` 特质。由于实现 `Copy` 的类型,都有着执行与 `Copy` 同样任务的一个 `Clone` 的简单实现,因此实现 `Copy` 的类型必须同时实现 `Clone`
很少需要 `Copy` 特质;实现了 `Copy` 的类型,有着可供选择的优化方案,意味着咱们不必调用 `clone`,而调用 `clone` 会令到代码更简洁。
对于 `Copy` 下每种可能情况,咱们都可同时以 `Clone` 完成,除了代码可能更慢,或在一些地方不得不使用 `clone`
### 用于将值映射到固定大小值的 `Hash`
**`Hash` for Mapping a Value to a Value of Fixed Size**
`Hash` 特质实现了取某种任意大小类型的实例,并通过使用散列函数,将那个实例映射到固定大小的值。派生 `Hash` 会实现 `hash` 方法。`hash` 放的派生实现,会将在该类型各个组成部分上调用 `hash` 的结果结合起来,这就意味着类型要派生 `Hash`,那么其全部字段,都必须同时实现 `Hash`
要求 `Hash` 的一个示例,便是为了高效地存储数据,而在 `Hash<K, V>` 中存储那些键时。
### 用于默认值的 `Default`
**`Default` for Default Values**
`Default` 特质实现了为类型创建出一个默认值。派生 `Default` 会实现 `default` 函数。`default` 函数的派生实现,会在类型的各个部分上调用 `default` 函数,意味类型要派生 `Defualt`,其中的全部字段或值,都必须同时实现 `Default`
`Default::default` 函数,通常是与第 5 章中 [“使用结构体更新语法从其他实例创建出实例”](Ch05_Using_Structs_to_Structure_Related_Data.md#使用结构体更新语法从其他实例创建出实例) 小节里曾讨论过的结构体更新语法结合使用的。咱们可以定制结构体的几个字段,并在随后通过使用 `..Default::default()`,为其余字段设置并使用默认值。
`Option<T>` 实例上使用 `unwrap_or_default` 方法时,便是需要 `Default` 特质的一个示例。当那个 `Option<T>``None` 时,方法 `unwrap_or_default` 就将返回存储在 `Option<T>` 中,那个类型 `T``Default::default` 结果。
## 附录 D一些有用开发工具
在此附录中,咱们会讲到 Rust 项目所提供的一些有用的开发工具。咱们将看看自动格式化、应用警告修复的一些快速方法、一种代码静态分析工具a linter以及与多种 IDE 的集成。
### 使用 `rustfmt` 的自动格式化
**Automatic Formatting with `rustfmt`**
`rustfmt` 工具会依据社区编码风格,重新格式化咱们的代码。许多协作项目,都使用了 `rustfmt` 来防止有关编写 Rust 时使用何种风格方面的争论:每个人都使用这个工具来格式化他们的代码。
要安装 `rustfmt`,请键入下面的命令:
```console
$ rustup component add rustfmt
```
如同 Rust 会同时给到 `rustc``cargo` 一样,此命令会给到咱们 `rustfmt``cargo-fmt`。要格式化任何 Cargo 项目,请敲入下面的命令:
```console
$ cargo fmt
```
运行此命令,会重新格式化当前代码箱中全部的 Rust 代码。这只会改变编码风格,而不会改变代码语义。关于 `rustfmt` 的更多信息,请参阅 [其文档](https://github.com/rust-lang/rustfmt).
### 使用 `rustfix` 修复咱们的代码
**Fix Your Code with `rustfix`**
`rustfix` 工具已被 Rust 安装所包含,并可大致以咱们想要的方式,修复那些有着明确纠正问题方法的一些编译器告警。咱们之前大概率已经见到过编译器告警了。比如,设想有下面这段代码:
文件名:`src/main.rs`
```rust
fn do_something() {}
fn main() {
for i in 0..100 {
do_something();
}
}
```
此处,咱们正调用 `do_something` 函数 100 次,但咱们在 `for` 循环的代码体中,从未用到那个变量 `i`。Rust 就会就此对咱们发出告警:
```console
$ cargo build
Compiling rustfix_demo v0.1.0 (/home/lenny.peng/rust-lang-zh_CN/rustfix_demo)
warning: unused variable: `i`
--> src/main.rs:4:9
|
4 | for i in 0..100 {
| ^ help: if this is intentional, prefix it with an underscore: `_i`
|
= note: `#[warn(unused_variables)]` on by default
warning: `rustfix_demo` (bin "rustfix_demo") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 0.29s
```
这个告警建议咱们要使用 `_i` 做名字:其中的下划线表示咱们有意不使用这个变量。通过运行 `cargo fix` 命令,咱们就可以使用 `rustfix`,自动应用那项建议:
```console
$ cargo fix --allow-no-vcs
Checking rustfix_demo v0.1.0 (/home/lenny.peng/rust-lang-zh_CN/rustfix_demo)
Fixed src/main.rs (1 fix)
Finished dev [unoptimized + debuginfo] target(s) in 0.17s
```
当咱们再次看到 `src/main.rs`,就将发现 `cargo fix` 已修改了这段代码:
文件名:`src/main.rs`
```rust
fn do_something() {}
fn main() {
for _i in 0..100 {
do_something();
}
}
```
那个 `for` 循环变量,现在就被命名为了 `_i`,同时那条告警也不再出现了。
咱们还可使用 `cargo fix` 命令,将咱们的代码在不同 Rust 版本之间转换。有关这些 Rust 版本,在附录 E 中有讲到。
### 使用 Clippy 获得更多的代码静态分析
**More Lints with Clippy**
Clippy 工具是用于分析咱们代码,从而咱们可以捕获到一些常见错误,而改进咱们 Rust 代码的一套代码静态分析集合。
要安装 Clippy请输入以下命令
```console
$ rustup component add Clippy
```
在任何 Cargo 项目上要运行 Clippy 的静态分析,请输入以下命令:
```console
$ cargo clippy
```
比如说咱们编写了像下面这个程序这样,用到某个数学常量近似值,好比说 `pi`,的一个程序:
文件名:`src/main.rs`
```rust
fn main() {
let x = 3.1415;
let r = 8.0;
println!("圆的面积为 {}", x * r * r);
}
```
在这个项目上运行 `cargo clippy` 就会得到下面的报错:
```console
$ cargo clippy
Checking clippy_demo v0.1.0 (/home/lenny.peng/rust-lang-zh_CN/clippy_demo)
error: approximate value of `f{32, 64}::consts::PI` found
--> src/main.rs:2:13
|
2 | let x = 3.1415;
| ^^^^^^
|
= help: consider using the constant directly
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#approx_constant
= note: `#[deny(clippy::approx_constant)]` on by default
error: could not compile `clippy_demo` due to previous error
```
此报错让咱们明白Rust 已经定义了一个更精确的 `PI` 常量,且当咱们使用这个常量时,咱们的程序将更为正确。那么咱们随后就应修改咱们代码为使用这个 `PI` 常量。下面的代码就捕获导致 Clippy 的任何错误或告警:
文件名:`src/main.rs`
```rust
fn main() {
let x = std::f64::consts::PI;
let r = 8.0;
println!("圆的面积为 {}", x * r * r);
}
```
有关 Clippy 的更多信息,请参阅 [其文档](https://github.com/rust-lang/rust-clippy)。
### 用到 `rust-analyzer` 的 IDE 集成
**IDE Integration Using `rust-analyzer`**
为帮助 IDE 集成Rust 社区建议使用 [`rust-analyzer`](https://rust-analyzer.github.io/)。此工具是一套以编译器为中心,操 [语言服务器协议Language Server Protocol](http://langserver.org/) 的实用工具;而所谓语言服务器协议,则是用于各种 IDEs 和编程语言,二者相互之间通信的一种规格。有多种不同客户端可使用 `rust-analyzer`,比如 [Visual Studio Code 的 Rust 分析器插件](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)。
请访问 `rust-analyzer` 项目 [主页](https://rust-analyzer.github.io/),了解其安全说明,随后在咱们的特定 IDE 中安装该语言的服务器支持。咱们的 IDE 就能获得诸如自动补全、跳至定义及行内报错等能力。
## 附录 E关于版本
**Appendix E - Editions**
在第一章中,咱们曾看到 `cargo new` 会把一点有关某个版的元数据,添加到咱们的 `Cargo.toml` 文件。此附录就会讲到那意味着什么!
Rust 语言及编译器有着六周的发布周期意味着用户会得到源源不断的新功能。其他编程语言会不经常地发布较大变更Rust 则会更频繁发布较小的更新。不久之后,全部这些小修改就会堆积起来。不过这一个个发布中,回头看看而讲到,“噢,从版本 1.10 到 1.31Rust 改变了很多!”。则是不容易的。
每两三年Rust 团队都会产生一个新的 Rust *版本edition*。每个版本都会以完全更新的文档与工具,将那些业已落地到一个明确包中的特性放到一起。新版本会作为寻常的六周发布过程而交付。
这些版本服务了不同人群的不同目的:
- 对于活跃的 Rust 用户,新版本会把那些增量变更,一起放入到一个易于掌握的包中;
- 对于那些非用户,新版本释放了一些已落地的大进展信号,这会让 Rust 或许值得再看一看;
- 对于开发 Rust 的人们,新版本会提供这个项目作为整体的集结点。
在本书编写时,已有三个 Rust 版本可用Rust 2015、Rust 2018 与 Rust 2021。本书是用 Rust 2021 版本的习惯用语编写的。
`Cargo.toml` 中的 `edition`表示应对咱们的代码使用哪个版本的编译器。若该键不存在Rust 就会以向后兼容原因,而使用 `2015` 作为版本值。
每个项目都可以选择一个不同于默认 2015 的版本。这些版本可能包含了不兼容的变更,比如包含了与代码中标识符冲突的新关键字。但是,除非咱们选到这些变更,那么即使咱们更新了所使用的 Rust 编译器,咱们的代码将继续编译。
全部 Rust 编译器版本,都会支持先于那个编译器发布而存在的任何版本,且他们可将任何受支持版本的代码箱连接起来。版本变更只会影响编译器于编译初期解析代码的方式。因此,当咱们正使用着 Rust 2015而咱们的一项依赖使用了 Rust 2018 时,咱们的项目将编译,并能够使用那项依赖。与之相反,在咱们的项目使用 Rust 2018而一项依赖使用了 Rust 2015 的情形下,也会工作。
要明确的是:绝大多数特性,在所有版本上都将可用。使用任何 Rust 版本的开发者,都将在新的稳定发布构造出来时,发现一些改进。但是,在一些情况下,主要是在新曾了关键字时,一些新特性就会只在稍后版本中可用了。若咱们打算利用上这些新特性,咱们将需要切换版本。
有关更多细节,[版本指南Edition Guide](https://doc.rust-lang.org/stable/edition-guide/) 是本列举了不同版本间差异,并解释了怎样通过 `cargo fix`,而自动将咱们的代码更新到新版的一本完整的书。
## 附录 F - 本书的一些译本
<略>
## 附录 G - Rust 是怎样构造出来的与每日发布
**How Rust is Made and "Nightly Rust"**
此附录是有关 Rust 被怎样构造出来,及那会怎样作为一名 Rust 开发者的你。
### 强调稳定却并无止步不前
**Stability Without Stagnation**
作为一门语言Rust 在注重咱们代码稳定性方面 *用心良苦*。咱们希望 Rust 成为你可以在其上构建软件的稳固基础,而若那些物件都一直变动,那将是不可能实现的。而与此同时,若咱们无法实验一些新特性,那么直到这些特性发布后咱们不能在修改一些东西时,咱们也不会发现一些重大缺陷。
对于这个问题咱们Rust 团队)的解决方案就是咱们称作 “强调稳定又不要止步不前”而咱们的直到原则是这样的你永不必害怕升级到稳定的Rust 新版本。每次升级都应是无痛的,而又应带给你一些新特性、更少的程序错误,以及更快的编译时间。
### 啾,啾!发布通道与搭上快车
**Choo, Choo! Release Channels and Riding the Trains**
Rust 的开发,是运作在 *火车时刻表train schedule* 上的。那就是说,全部开发都是在 Rust 代码仓库的 `master` 分支上完成的。各个发布遵循了软件发布列车模型a software release train model该发布模型业已为 Cisco IOS 及其他软件项目所使用。Rust 有着以下三个 *发布通道release channels*
- 每日发布nightly
- Beta 发布beta
- 稳定发布stable
多数 Rust 开发者主要使用稳定通道,而那些希望尝试实验性新特性的人们,则会使用每日发布或 beta 通道。
下面是个开发与发布流程运作方式的一个示例:咱们来假定 Rust 团队正工作于 Rust 1.5 的发布上。那个发布发生于 2015 年 11 月,但其将提供到我们实际版本数字。有个新特性被添加到 Rust一次新提交落在了 `master` 分支。每天晚上,都有一个新的 Rust 每日版本被产生出来。每天都是个发布日,而这些发布是由咱们的发布基础设施自动创建的。因此随着时间流逝,咱们的发布看起来就像下面这样,每晚一次:
```text
nightly: * - - * - - *
```
每隔六周便是要准备一个新发布的时候了Rust 代码仓库的 `beta` 分支,便会从由每日发布所使用的 `master` 分支分叉开来。现在,就有了两个分支:
```text
nightly: * - - * - - *
|
beta: *
```
多数 Rust 使用者不会积极使用这些 beta 发布,但会在他们的 CI 系统中就 beta 发布加以测试,以帮助 Rust 发现可能出现的倒退。与此同时,仍有着每晚的每日发布:
```text
nightly: * - - * - - * - - * - - *
|
beta: *
```
在首个 beta 版创建出来六周后,就是稳定发布的时候了!`stable` 分支就被从 `beta` 分支创建出来:
```text
nightly: * - - * - - * - - * - - * - - * - * - *
|
beta: * - - - - - - - - *
|
stable: *
```
Rust 1.5 便完成了!不过,咱们忘了一件事:由于这六个星期以及过去,而咱们还需要 Rust *下一* 版本1.6,的一个新的 beta 发布。因此在 `stale` 分支从 `beta` 分支分叉出来后,下一版本的 `beta` 又会从 `nightly` 再度分叉出来:
```text
nightly: * - - * - - * - - * - - * - - * - * - *
| |
beta: * - - - - - - - - * *
|
stable: *
```
每六周就有一个发布 “离站”,但发布过程仍务必要在其抵达稳定发布前,经由这个 beta 通道行驶一段路程,由此这个过程便被称为 “列车模型”。
Rust 每六周发布,像时刻表一样。若咱们知道了一个 Rust 发布的日期,那么就能直到下一发布的日期:那便是六周后。每六周安排一次发布的一个好处,便是下一班列车很快就会到来。若某项特性刚好错过了某个特定发布,那么无需担心:另一发布将在不久后发生!这有助于减少在临近发布截止日期时,有可能未完善的功能偷偷潜入的压力。
归功于这个流程咱们可以始终检出check out下一构建的 Rust并自己验证到升级是容易的若 beta 发布没有如预期那样工作,咱们就可以将其报告给 Rust 团队并在下一稳定发布发生前修好他beta 发布中的损坏相对较少,但 `rustc` 仍属于一个软件,而确实存在一些错误。
### 不稳定特性
**Unstable Features**
这种发布模型下还有一个好处不稳定特性。Rust 使用了一种名为 “特性标识feature flags” 的技巧,来确定出给定发布中启用了哪些特性。若某项新特性处于活跃开发中,他就会落地在 `master` 分支上,而由此就会在每日发布中,但会有着一个 *特性标识*。而咱们作为用户希望尝试这个进展中的特性the work-in-progress feature咱们是可以尝试的但必须使用 Rust 的每日发布,并使用恰当的标识来注解咱们的代码,来选用该特性。
若咱们使用着 beta 或稳定发布的 Rust那么就不能使用任何特性标识。这是 Rust 团队在声明那些新特性永久稳定前,允许咱们实际用到他们的关键。希望选用最新特性的人们,便可这样做,而想要一种扎实体验的人,则可坚持使用稳定发布,而清楚他们的代码不会破坏。这便是稳定但并非止步不前。
由于那些工作中的特性仍在便会,且在本书写作时和他们在稳定构建中启用时,其间他们肯定将有所不同,因此本书只包含了那些稳定特性的信息。咱们可以在线上找到那些仅每日发布有的特性文档。
### Rustup 与 Rust 每日发布所扮演的角色
**Rustup and the Role of Rust Nightly**
Rust 令到易于在全局或每个项目基础上,从不同发布通道的 Rust 之间改变。默认情况下,咱们将安装稳定发布的 Rust。而比如要安装每日发布
```console
$ rustup toolchain install nightly
```
咱们也可以使用 `rustup`,查看全部的 *工具链toolchains* Rust 的各个发布与关联组件)。下面就是本书一位作者的 Windows 计算机上的示例:
```powershell
> rustup toolchain list
stable-x86_64-pc-windows-msvc (default)
beta-x86_64-pc-windows-msvc
nightly-x86_64-pc-windows-msvc
```
> 在 Linux 系统上的输出如下:
```console
$ rustup toolchain list
stable-x86_64-unknown-linux-gnu (default)
```
可以看到,稳定发布的工具链是默认的。绝大多数 Rust 用户会在多数时候使用稳定发布。咱们可能想要在多数时候使用稳定发布,又因为咱们关心某项最新特性,而会在特定项目使用每日发布。要这样做,就可以在那个项目目录下,使用 `rustup override` 来将每日发布工具链,设置为当咱们位处那个目录中时,`rustup` 使用的那个工具链:
```console
$ cd ~/projects/needs-nightly
$ rustup override set nightly
```
现在,当咱们每次在 `~/projects/needs-nightly` 目录下调用 `rustc``cargo` 时,`rustup` 都会确保咱们在使用每日发布的 Rust而非咱们默认的稳定发布 Rust 了。再有很多 Rust 项目时,这就会排上用场!
### 请求评议流程与各种团队
**The RFC Process and Teams**
那么咱们该怎么了解到这些新特性呢Rust 的开发模型,遵循了 *请求评议流程Request For Comments(RFC) process*。如你想要 Rust 的一项改进那么就可以编写一个名为请求评议RFC 的提议。
人人都可以编写请求评议来改进 Rust同时这些提议会经过由许多议题子团队所组成的 Rust 团队审阅和讨论。[在 Rust 网站上](https://www.rust-lang.org/governance) 有这些团队的完整清单,其中包括了该项目各领域:语言设计、编译器实现、基础设施、文档及其他等的团队。恰当的团队会阅读提议与评论,撰写出他们自己的一些评论,并在最后,便有了接受或拒绝该特性的共识。
若该特性被接受了,就会在 Rust 代码仓库上开出一个 issue同时某个人就可以实现他。将其实现得非常棒的那个人可能不是最早提议这项特性的那人在实现准备好时其就会落地于 `master` 分支的特性门a feature gate之后如同咱们曾在 [“不稳定特性”](#不稳定特性) 小节中曾讨论过的那样。
过了一段时间后,一旦那些用到每日发布的 Rust 开发者们,能够试用这项新特性,那么 Rust 团队成员将讨论这项特性,怎样将其编制到每日发布上,并决定其是否有那个被构造到稳定发布 Rust。而若决定是继续推进那么特性门就会被移除同时这项特性就被认为是稳定的了他就会搭上列车进到一个新的稳定发布 Rust 中。
## 附录 H - 有用笔记
此处记录学习及应用 Rust 编程软件过程中,觉得有用的一些东西。
### `cargo-binutils`
[这个项目](https://github.com/rust-embedded/cargo-binutils) 是 Embbeded-Rust 项目的,而不是 Rust 官方的,但提供了有用的功能。比如查看构建出的二进制程序文件的那些头部:
```console
$ cargo readobj --bin clippy_demo -- --file-headers
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x86D0
Start of program headers: 64 (bytes into file)
Start of section headers: 4305200 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 12
Size of section headers: 64 (bytes)
Number of section headers: 42
Section header string table index: 41
```
使用前需要进行如下安装:
```console
$ cargo install cargo-binutils
$ rustup component add llvm-tools-preview
```
## 附录 I - 术语清单
- 命令行界面
Command-Line Interface在终端里运行的应用与在 GUI 窗口中应用不同。
- 模组系统
The module system大型程序中组织代码的方式。
- 迁移所有权
在闭包参数清单前,使用 `move` 关键字,让闭包取得其用到的所在环境中的值所有权。
- 关联类型
Associated type, 是通过 `type` 关键字定义在特质下的类型。咱们知道方法即为关联函数associated function那么关联类型自然与关联函数有些类似。
- 消费适配器
Consuming adaptor, `Iterator` 特质上,会调用到迭代器 `next` 方法的一些方法,由于这些方法会耗尽迭代器,故他们被称为消费适配器。
- 迭代器适配器
Iterator adaptor`Iterator` 特质上,通过改变原迭代器某些方面而产生出另一迭代器的一些方法。
- 零成本抽象
Zero-cost abstractions相较于一般实现语言提供的高级抽象在编译后生成的代码与自己努力编写出的优化低级别代码类似。故使用高级抽象是没有运行时开销的。
- 展开优化
UnrollingRust 编译器在编译迭代器代码时,会把已知的历次迭代展开为重复代码,而实现性能优化。
- 文档注释
Documentation comment, 将产生出 HTML 的注释。
- 重导出程序项目
Re-export, 使用 `pub use` 重新导出程序项目。
- 语义版本控制规则
Semantic Versioning rules, 又大版本、小版本及补丁版本构成的,形如 `MAJOR.MINOR.PATCH` 的版本编号规则。参考:[semver.org](https://semver.org)。
- 工作区
Workspace为有着多个库代码箱的大型项目组织的一项 Cargo 特性。
- 编译出的物件
The compiled artifacts
- 路径依赖
A path dependency
- 匣子类型(数据结构)
`Box<T>`,由存储在栈上的指针,与存储在堆上的数据,实现的一种数据结构。
- 间接
Indirection, 匣子类型的变量,通过保存指向数据在内存堆上的地址,而间接保存了数据。
- 解引用强制转换
Deref coercion类似于其他语言的开箱操作。
- 元组结构体
A tuple struct, 形式为 `struct MyBox<T>(T)`,是保持着只有一个元素元组的结构体,`Box<T>` 的数据结构为元组结构体。
- 前奏
The Rust Prelude, `std::prelude` 模组。前奏是 Rust 自动导入到每个 Rust 程序中的东西的列表。他被保持在尽可能小的范围内,并且专注于几乎每个 Rust 程序都会用到的东西,特别是特质。参见:[`std::prelude`](https://doc.rust-lang.org/std/prelude/index.html)。
- 内部可变性模式
The interior mutability pattern, Rust 的一种设计模式,用于改变不可变值内部的某个值。
- 内存泄漏
Memory leak, 出现未清理内存的情况。
- 关联类型
An associated type, 通过 `type Target = t;` 这种语法声明出的类型,是声明泛型参数的一种稍微不同的方式。
- 单态化
所谓 *单态化monomorphization*,是指即通过把在编译后用到的具体类型填入到泛型位置,而将通用代码转换为具体代码的过程。参考 [使用泛型代码的性能问题](Ch10_Generic_Types_Traits_and_Lifetimes.md#使用泛型参数代码的性能问题)。
- 内聚属性
a property called *coherence*,参见 [在类型上实现某个特质](Ch10_Generic_Types_Traits_and_Lifetimes.md#在类型上实现某个特质)。
- 孤儿规则
the orphan rule, 参见 [在类型上实现某个特质](Ch10_Generic_Types_Traits_and_Lifetimes.md#在类型上实现某个特质)。
- `impl Trait` 语法
`impl Trait` syntax, 在函数参数清单中,将特质用作参数类型注解的语法。参见:[作为参数的特质](Ch10_Generic_Types_Traits_and_Lifetimes.md#作为参数的特质)
- 特质边界语法
Trait bound syntax, 参见 [特质边界语法](Ch10_Generic_Types_Traits_and_Lifetimes.md#特质边界语法)
- 语法糖
Sugar syntax, 参见 [特质边界语法](Ch10_Generic_Types_Traits_and_Lifetimes.md#特质边界语法)
- 指明多个特质边界的 `+` 语法
The `+` syntax for specifying multiple trait bounds, 参见:[使用 + 语法,指定多个特质边界](Ch10_Generic_Types_Traits_and_Lifetimes.md#使用--语法指定多个特质边界)
- `where` 子句
`where` clauses, 参见 []()
- 生命周期省略规则
Lifetime elision rules, 编程到 Rust 引用分析中的一些确定性模式。
- 输入生命周期
Input lifetimes函数或方法上的生命周期
- 输出生命周期
Output lifetimes, 返回值上的生命周期

View File

@@ -6,34 +6,69 @@
---
- [入门](Ch01_Getting_Started.md)
- [安装](getting_started/installation.md)
- [Hello, World!](getting_started/hello_world.md)
- [你好Cargo](getting_started/hello_cargo.md)
- [编写猜数游戏](Ch02_Programming_a_Guessing_Game.md)
- [常见编程概念](Ch03_Common_Programming_Concepts.md)
- [变量与可变性](programming_concepts/variables_and_mutability.md)
- [数据类型](programming_concepts/data_types.md)
- [函数](programming_concepts/functions.md)
- [注释](programming_concepts/comments.md)
- [控制流](programming_concepts/control_flow.md)
---
# 进阶
- [“掌握” 所有权](Ch04_Understanding_Ownership.md)
- [何为所有权?](ownership/about_ownership.md)
- [引用与借用](ownership/references_and_borrowing.md)
- [切片类型](ownership/the_slice_type.md)
- [使用结构体来对相关数据进行架构](Ch05_Using_Structs_to_Structure_Related_Data.md)
- [定义并初始化结构体](structs/defining_and_instantiating.md)
- [运用结构体的一个示例程序](structs/example_program.md)
- [方法语法](structs/method_syntax.md)
- [枚举与模式匹配](Ch06_Enums_and_Pattern_Matching.md)
- [定义一个枚举](enums_and_pattern_matching/defining_an_enum.md)
- [`match` 控制流结构](enums_and_pattern_matching/match_control_flow.md)
- [使用 `if let` 与 `let else` 的简明控制流](enums_and_pattern_matching/if-let_control_flow.md)
- [使用包、代码箱与模组对日趋增长的项目进行管理](Ch07_Managing_Growing_Projects_with_Packages_Crates_and_Modules.md)
- [代码包与代码箱](packages_crates_and_modules/packages_and_crates.md)
- [定义用于控制作用域及隐私性的模组](packages_crates_and_modules/defining_modules.md)
- [引用模组树中某个项目的路径](packages_crates_and_modules/paths.md)
- [使用 `use` 关键字将路径带入作用域](packages_crates_and_modules/the_use_keyword.md)
- [将模组分成不同文件](packages_crates_and_modules/separating_modules.md)
- [常用集合数据结构](Ch08_Common_Collections.md)
- [使用矢量值存储值的清单](common_collections/vectors.md)
- [使用字符串存储 UTF-8 编码的文本](common_collections/strings.md)
- [在哈希图中存储带有关联值的键](common_collections/hash_maps.md)
- [错误的处理](Ch09_Error_Handling.md)
- [使用 `panic!` 宏的不可恢复错误](error_handling/panic.md)
- [使用 `Result` 的可恢复错误](error_handling/result.md)
- [要 `panic!` 还是不要 `panic!`](error_handling/panic_or_not.md)
---
# 深入掌握
- [泛型、特质与生命周期](Ch10_Generic_Types_Traits_and_Lifetimes.md)
- [通用数据类型](generic_types_traits_and_lifetimes/generics.md)
- [特质:定义共用行为](generic_types_traits_and_lifetimes/traits.md)
- [使用生命周期验证引用](generic_types_traits_and_lifetimes/lifetimes.md)
- [编写自动化测试](Ch11_Writing_Automated_Tests.md)
- [怎样编写测试](automated_tests/howto.md)
- [控制测试运行方式](automated_tests/how_tests_are_run.md)
- [测试的组织](automated_tests/test_organization.md)
---
@@ -41,26 +76,84 @@
# 上篇总结 - 实操
- [一个文件系统 I/O 项目:构建一个命令行程序](Ch12_An_IO_Project_Building_a_Command_Line_Program.md)
- [接收命令行参数](io_project/accepting_cli_arguments.md)
- [读取文件](io_project/reading_a_file.md)
- [重构以改进模块化和错误处理](io_project/refactoring.md)
- [以测试驱动方法,开发这个库的功能](io_project/test_driven_dev.md)
- [使用环境变量](io_project/env_variables.md)
- [将错误消息写到标准错误,而非标准输出](io_project/std_err.md)
---
# 下篇
- [函数式编程语言特性:迭代器与闭包](Ch13_Functional_Language_Features_Iterators_and_Closures.md)
- [函数式语言特性:迭代器与闭包](Ch13_Functional_Language_Features_Iterators_and_Closures.md)
- [闭包:会捕获其环境的匿名函数](functional_features/closures.md)
- [使用迭代器处理条目序列](functional_features/iterators.md)
- [改进咱们的 I/O 项目](functional_features/improving_io_project.md)
- [性能比较:循环与迭代器](functional_features/performance.md)
- [Cargo 的其他方面及 Crates.io](Ch14_More_about_Cargo_and_Crates-io.md)
- [使用发布配置文件自定义构建](crates-io/release_profiles.md)
- [将代码箱发布到 Crates.io](crates-io/publishing.md)
- [Cargo 工作区](crates-io/workspace.md)
- [使用 `cargo install` 安装 Crates.io 上的二进制程序](crates-io/cargo_install.md)
- [以定制命令扩展 Cargo](crates-io/custom_commands.md)
- [灵巧指针](Ch15_Smart_Pointers.md)
- [使用 `Box<T>` 指向内存堆上的数据](smart_pointers/box-t.md)
- [使用 `Deref` 特质将灵巧指针视为常规引用](smart_pointers/deref-t.md)
- [使用 `Drop` 特质在内存清理时运行代码](smart_pointers/drop-t.md)
- [引用有计数的灵巧指针 `Rc<T>`](smart_pointers/rc-t.md)
- [`RefCell<T>` 与内部可变性模式](smart_pointers/refcell-t.md)
- [引用循环会泄露内存](smart_pointers/ref-cycles.md)
- [无惧并发](Ch16_Fearless_Concurrency.md)
- [使用线程同步运行代码](concurrency/threads.md)
- [使用消息传递再线程间传输数据](concurrency/message_passing.md)
- [共用状态的并发](concurrency/shared-state.md)
- [使用 `Sync` 与 `Send` 的可扩展并发](concurrency/extensible_concurrency.md)
- [异步编程基础:异步、等待、未来值与流](Ch16_1_async_programming.md)
- [未来值与异步语法](async/futures.md)
- [应用带有异步的并发](async/concurrency_n_async.md)
- [使用任意数量的未来值](async/multiple_futures.md)
- [流:序列中的未来值](async/streams.md)
- [近观异步相关的特质](async/async_traits.md)
- [放在一起:未来值、任务与线程](async/all_together.md)
- [Rust 的面向对象编程特性](Ch17_Object_Oriented_Programming_Features_of_Rust.md)
- [面向对象语言的特征](oop/characteristics_oop.md)
- [使用允许不同类型值的特质对象](oop/trait_objects.md)
- [实现一种面向对象设计模式](oop/implementing.md)
- [模式与匹配](Ch18_Patterns_and_Matching.md)
- [可使用模式的全部处所](patterns/all_places.md)
- [可证伪性:某个模式是否会匹配失败](patterns/refutability.md)
- [模式语法](patterns/syntax.md)
- [先进特性](Ch19_Advanced_Features.md)
- [不安全的 Rust](advanced_features/unsafe.md)
- [高级特质](advanced_features/adv_traits.md)
- [高级类型](advanced_features/adv_types.md)
- [高级函数与闭包](advanced_features/adv_fns_and_closures.md)
- [关于宏](advanced_features/macros.md)
- [最后项目:构建一个多线程的 Web 服务器](Ch20_Final_Project_Building_a_Multithreaded_Web_Server.md)
- [构建一个单线程的 Web 服务器](final_project/single-threaded.md)
- [将这个单线程服务器修改为多线程服务器](final_project/multithreaded.md)
- [优雅关机与内存清理](final_project/graceful_shutdown.md)
- [附录](Ch21_Appendix.md)
- [A - 关键字](appendix/keywords.md)
- [B - 运算符与符号](appendix/ops_and_symbols.md)
- [C - 派生特质](appendix/derivable_traits.md)
- [D - 有用开发工具](appendix/dev_tools.md)
- [E - 关于版本](appendix/editions.md)
- [F - 本书的一些译本](appendix/translations.md)
- [G - Rust 是怎样构造出来的与 “每日 Rust”](appendix/releases.md)
- [H - 有用笔记](appendix/notes.md)
- [I - 术语清单](appendix/terminology_list.md)

View File

@@ -0,0 +1,139 @@
# 高级函数与闭包
**Advanced Functions and Closures**
这个小节会探讨一些与函数和闭包有关的高级特性包括函数指针与作为返回值的闭包function pointers and returning closures。
## 函数指针
**Function Pointers**
咱们已讲到了怎样把闭包传递给函数;咱们也可以把常规函数传递给函数!在咱们打算传递一个咱们已定义的函数,而非定义出一个新闭包时,这种技巧便是有用的。这些函数会强制转换到类型 `fn` (有着小写的 `f`),而不会与那个 `Fn` 闭包特质混淆。这个 `fn` 类型,被称为 *函数指针funciton pointer*。使用函数指针的传递函数,将实现把函数作为其他函数参数而运用。
指明某个函数是个函数指针的语法,与参数是个闭包的语法类似,如下清单 19-27 中所示,其中咱们定义了一个往其参数加一的函数 `add_one`。函数 `do_twice` 则会取两个参数:到任何的取一个 `i32` 参数,并返回 `i32` 值函数的函数指针,以及一个 `i32` 值。这个 `do_twice` 函数会调用函数 `f` 两次,传递给他那个 `arg` 值,随后把这两次函数调用的结果相加在一起。`main` 函数使用了参数 `add_one``5` 调用 `do_twice`
文件名:`src/main.rs`
```rust
fn add_one(x: i32) -> i32 {
x + 1
}
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
f(arg) + f(arg)
}
fn main() {
let answer = do_twice(add_one, 5);
println! ("答案为:{}", answer);
}
```
*清单 19-27使用 `fn` 类型来以参数方式接收函数指针*
此代码会打印出 `答案为12`。咱们指明了 `do_twice` 中的参数 `f` 是取一个类型 `i32` 参数,并返回一个 `i32``fn`。最后咱们便可以在 `do_twice` 函数体中调用 `f` 了。在 `main` 中,咱们可以将名为 `add_one` 的函数,作为首个参数传递给 `do_twice`
与闭包不同,`fn` 是种类型而非一个特质,因此咱们将 `fn` 直接指定为参数类型,而非使用 `Fn` 特质之一,作为特质边界声明一个泛型参数。
函数指针实现了全部三个闭包特质(`Fn``FnMut``FnOnce`),意味着咱们可以一直将某个函数,作为期望得到一个闭包的函数的参数而加以传递。编写出使用了一个泛型及闭包特质之一的函数,是最佳做法,如此咱们的函数就既可以接收函数,也可以接收闭包了。
那就是说,一种咱们只想接收 `fn` 而不想接收闭包的情况便是与并无闭包的外部代码相交互时C 语言函数可以参数方式接收函数,但 C 语言是没有闭包的。
而作为既可以使用内联定义的闭包,又可以使用命名函数的一种情况,下面就来看看标准库中 `Iterator` 特质所提供的 `map` 函数的一种用法。要使用 `map` 函数来将某个一些数字构成的矢量值,转换为字符串的矢量,咱们可以使用一个闭包,如下面这样:
```rust
let list_of_numbers = vec! [1, 2, 3];
let list_of_strings: Vec<String> =
list_of_numbers.iter().map(|i| i.to_string()).collect();
```
或者咱们可以命名一个作为给 `map` 参数的函数,而非那个闭包,如下面这样:
```rust
let list_of_numbers = vec! [1, 2, 3];
let list_of_strings: Vec<String> =
list_of_numbers.iter().map(ToString::to_string).collect();
```
请注意由于有着多个可用的名为 `to_string` 函数,因此咱们就必须使用早先在 [“高级特质”](#高级特质) 小节中讲到的完全合格语法。这里咱们使用了那个标准库已对任何实现了 `Display` 类型,实现过了的 `ToString` 特质中的 `to_string` 函数。
自第 6 章 [“枚举取值”](Ch06_Enums_and_Pattern_Matching.md#枚举取值) 小节,回顾咱们所定义的各个枚举变种名字,也会成为一个初始化函数。咱们可以将这些初始化函数,作为实现了那些闭包特质的函数指针使用,这就意味着咱们可以把这些初始化函数,指定为取闭包的方法的参数,像下面这样:
```rust
enum Status {
Value(u32),
Stop,
}
let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect();
```
这里咱们运用了那些经由使用 `Status::Value` 的初始化函数,于其上调用了 `map` 的那个范围中各个 `u32` 值,而创建出了一些 `Status::Value` 的实例。有的人会首选这种方式,而别的人则首选闭包。他们会编译到同样的代码,因此请使用你认为更清晰的风格。
## 返回闭包
**Returning Closures**
闭包是由特质表示的,这就意味着咱们不能直接返回闭包。在多数咱们可能打算返回特质的情形中,咱们都可以转而使用实现了该特质的具体类型,作为函数的返回值。但是,由于闭包没有可返回的具体类型,因此对于闭包是不能这样做的;就好比咱们是不被允许将函数指针作为返回值类型。
下面的代码尝试直接返回一个闭包,但其不会编译:
```rust
fn returns_closure() -> dyn Fn(i32) -> i32 {
|x| x + 1
}
```
编译器报错如下:
```console
$ cargo build
Compiling returning_closure v0.1.0 (/home/lenny.peng/rust-lang/returning_closure)
error[E0746]: return type cannot have an unboxed trait object
--> src/main.rs:1:25
|
1 | fn returns_closure() -> dyn Fn(i32) -> i32 {
| ^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
|
= note: for information on `impl Trait`, see <https://doc.rust-lang.org/book/ch10-02-traits.html#returning-types-that-implement-traits>
help: use `impl Fn(i32) -> i32` as the return type, as all return paths are of type `[closure@src/main.rs:2:5: 2:8]`, which implements `Fn(i32) -> i32`
|
1 | fn returns_closure() -> impl Fn(i32) -> i32 {
| ~~~~~~~~~~~~~~~~~~~
For more information about this error, try `rustc --explain E0746`.
error: could not compile `returning_closure` due to previous error
```
这个报错再度指向了那个 `Sized` 特质Rust 不清楚他将需要多少内存空间来存储这个闭包。早先咱们就已见到了对这个问题的解决办法了。咱们可以使用一个特质对象:
```rust
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x + 1)
}
```
这段代码可以很好地编译。有关特质对象的更多内容,请参考第 17 章中的 [“使用特质对象实现不同类型值”](Ch17_Object_Oriented_Programming_Features_of_Rust.md#使用允许不同类型值的特质对象) 小节。
接下来,咱们就要看看宏了!
End

View File

@@ -0,0 +1,482 @@
# 高级特质
在第 10 章 [“特质:定义共用行为”](Ch10_Generic_Types_Traits_and_Lifetimes.md#特质定义共用行为) 小节中,咱们曾首先涉及到特质,但咱们不曾讨论更为高级的那些细节。现在咱们对 Rust 有了更多了解咱们就可以深入本质get into the nitty-gritty。
## 使用关联类型指定出特质定义中的一些占位性类型
**Specifying placeholder types in trait definitions with associated types**
*关联类型* 将类型占位符与特质加以结合,从而那些特质方法的定义,就可以在他们的签名中,使用这些占位符类型。特质的实现者,将为其特定实现,指明占位符类型所要使用的具体类型。如此一来,咱们便可以在特质被实现之前,无需准确获悉特质用到的类型下,定义出用到这些类型的特质。
在本章中,咱们已经介绍了绝大多数极少需要用到的高级特性。而关联类型则是位于这些高级特性中部的一种:其相较本书其余部分降到的那些特性,用得尤其少见,但相较这一章中讨论到的其他特性,则其要更常用一些。
带有关联类型特质的一个示例,便是标准库所提供的 `Iterator` 特质。其中的关联类型名为 `Item`,且代表着实现了这个 `Iterator` 特质的类型所迭代的那些值的类型。`Iterator` 特质的定义如下清单 19-12 中所示。
```rust
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
```
*清单 19-12有着关联类型 `Item` 的 `Iterator` 特质的定义*
其中的类型 `Item` 便是个占位符,而那个 `next` 方法的定义,则显示其将返回类型类型为 `Option<Self::Item>` 的值。`Iterator` 的实现者,将指明 `Item` 的具体类型,同时 `next` 方法将返回包含那个具体类型值的一个 `Option`
从泛型允许咱们在不指明函数可处理何种类型下,而定义出某个函数上看,关联类型可能看起来是个与泛型类似类似的概念。为检视这两个概念的不同,咱们将看看在指定了 `Item` 类型为 `u32` 的一个名为 `Counter` 的类型上的 `Iterator` 实现:
```rust
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
// --跳过代码--
```
这种语法似乎可与泛型的那种语法相比。那么为何没有只使用泛型定义 `Iterator`,如下清单 19-13 中所示的那样呢?
```rust
pub trait Iterator<T> {
fn next(&mut self) -> Option<T>;
}
```
*清单 19-13使用泛型的一种 `Iterator` 特质的定义假设*
不同之处在于,如同清单 19-13 中使用泛型时,咱们必须注解每个实现中的那些类型;由于咱们还可以实现 `Iterfator<String> for Counter` 或任何其他类型,咱们可以有对 `Counter` 的多个 `Iterator` 实现。也就是说,在特质有着泛型参数时,他就可以对某个类型被实现多次,每次都修改泛型参数的具体类型。当在 `Counter` 上使用 `next` 方法时,咱们就将不得不提供类型注解,来表明咱们想要使用哪个 `Iterator` 实现。
而在关联类型下,由于我们无法在一个类型上多次实现某个特质,因此就无需注解类型。在上面有着用到关联类型定义的清单 9-12 中,由于只能有一个 `impl Iterator for Counter`,所以咱们就只能就`Item` 为何选择一次。咱们不必在 `Counter` 上调用 `next` 的每个地方,指定咱们所要的是个 `u32` 值的迭代器。
关联类型还成了特质合约的一部分:特质的实现着必须提供一种类型,来顶替那个关联类型占位符。关联类型通常会有个描述该类型将被如何使用的名字,而在 API 文档中对关联类型编写文档,则是良好的做法。
## 默认泛型参数与运算符的重载
**Default Generic Type Parameters and Operator Overloading**
> **注**:请参考 [Difference Between Method Overloading and Method Overriding in Java](https://www.geeksforgeeks.org/difference-between-method-overloading-and-method-overriding-in-java/) 了解 Java 中的重载与重写区别。
在咱们用到泛型参数时,咱们可以给泛型指定默认具体类型。在所指定的默认类型就有效时,这样做消除了实现者指定具体类型的需求。在声明泛型时使用 `<PlaceholderType=ConcreteType>` 语法,指定出默认类型。
这种技巧有用处情形的一个了不起示例,便是 *运算符重载operator overloading*,咱们可以其在某些情形下,定制某个运算符(比如 `+`)的行为。
Rust 不允许咱们创建自己的运算符,或重载任意运算符。但咱们可以通过实现与运算符相关的特质,而重载那些运算及于 `std::ops` 中所列出的相应特质。比如,在下面清单 19-14 中,咱们就将 `+` 运算符过载为把两个 `Point` 实例加在一起。咱们是通过在 `Point` 结构体上实现 `Add` 特质完成这一点的。
文件名:`src/main.rs`
```rust
use std::ops::Add;
#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
fn main() {
assert_eq! (
Point { x: 1, y: 0 } + Point { x: 2, y: 3},
Point { x: 3, y: 3 }
);
}
```
*清单 19-14实现 `Add` 特质来为 `Point` 实例过载 `+` 运算符*
这里的 `add` 方法,将两个 `Point` 实例的 `x` 值及两个实例的 `y` 值相加,而创建出一个新的 `Point`。这里的 `Add` 特质有着一个确定自其中的 `add` 方法返回类型,名为的 `Output` 关联类型。
此代码中的默认泛型,是在 `Add` 特质里。以下便是其定义:
```rust
trait Add<Rhs=Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
```
此代码看起来应相当熟悉:有着一个方法与关联类型的特质。其中新的部分为 `Rhs=Self`:这种语法叫做 *默认类型参数default type parameters*。其中的 `Rhs` 泛型参数(是 `right hand side` 的缩写),定义了 `add` 方法中 `rhs` 参数的类型,当咱们实现这个 `Add` 特质,而没有指明 `Rhs` 的类型时,`Rhs` 的类型将默认为 `Self`,其将是咱们在其上实现 `Add` 的类型。
当咱们为 `Point` 实现 `Add` 时,由于咱们打算把两个 `Point` 实例相加,因此而使用了 `Rhs` 的默认值。接下来看看,其中咱们打算定制那个 `Rhs` 而非使用其默认值的一个 `Add` 实现示例。
咱们有着两个结构体,`Millimeters``Meters`保存着不同单位的一些值。这种将某个既有类型封装在另一结构体的瘦封装thin wrapping就叫做 *新类型模式newtype pattern*,在后面的 [“使用新型模式在外部类型上实现外部特质”](#使用新型模式在外层类型上实现外层的特质) 小节,咱们会对其进行更深入讨论。咱们打算把毫米值与以米计数的值相加,并要让 `Add` 的实现,正确完成单位转换。咱们可在将 `Meters` 作为 `Rhs` 下,对 `Millimeters` 实现 `Add`,如下清单 19-15 中所示。
```rust
#[derive(Debug, Copy, Clone, PartialEq)]
struct Millimeters(u32);
#[derive(Debug, Copy, Clone, PartialEq)]
struct Meters(u32);
impl Add<Meters> for Millimeters {
type Output = Millimeters;
fn add(self, other: Meters) -> Millimeters {
Millimeters(self.0 + (other.0 * 1000))
}
}
```
*清单 19-15在 `Millimeters` 上实现 `Add` 特质,以将 `Millimeters` 与 `Meters` 相加*
为了将 `Millimeters``Meters` 相加,咱们指明了 `impl Add<Meters>` 来设置那个 `Rhs` 类型参数,而非使用其默认的 `Self`
咱们将以如下两种主要方式,使用默认的类型参数:
- 在不破坏既有代码之下,扩展某个类型;
- 为实现绝大多数不会需要的特定情形下的定制to allow customization in specific cases most users won't need。
标准库的 `Add` 特质,便是第二种目的的一个示例:通常,咱们将把两个相似类型相加,但 `Add` 特质提供了定制超出那种情况的能力。在 `Add` 特质中使用默认类型,就意味着咱们不必在多数时候指定额外的参数。换句话说,并不需要一点点的实现样板,从而令到使用这个特质更为容易。
第一个目的与第二个类似,不过是反过来的:在咱们打算将类型参数添加到某个既有特质时,就可以给到其一个默认值,从而在不破坏既有那些实现代码下,实现该特质功能的扩展。
## 用于消除歧义的完全合格语法:以同一名字调用方法
**Fully Qualified Syntax for Disambiguation: Calling Methods with the Same Name**
Rust 中没有什么可以阻止某个特质有着与另一特质的方法同样名字的方法Rust 也不会阻止咱们在一个类型上实现这两种特质。至于直接在类型上,以来自不同特质方法的同样名字实现方法,也是可行的。
在以同一名字调用这些方法时,咱们将需要告诉 Rust 打算使用哪一个。设想下面清单 19-16 中,定义了两个特质,`Pilot``Wizard`,两个特质都有一个叫做 `fly` 的代码。咱们随后在已在其上实现了一个名为 `fly` 方法的类型 `Human` 上,实现了这两个特质。每个 `fly` 都完成不同的事情。
```rust
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println! ("机长在此发言。");
}
}
impl Wizard for Human {
fn fly(&self) {
println! ("飞起来!");
}
}
impl Human {
fn fly(&self) {
println! ("*愤怒地挥动双臂*");
}
}
```
*清单 19-16两个被定义作有 `fly` 方法的特质并都在 `Human` 类型上被实现,且在 `Human` 上直接实现了一个 `fly` 方法*
当咱们在 `Human` 实例上调用 `fly` 时,编译器默认为调用直接在该类型上实现的那个方法,如下清单 19-17 中所示。
```rust
fn main() {
let person = Human;
person.fly();
}
```
*清单 19-17调用 `Human` 实例上的 `fly`*
运行此代码将打印出 `*愤怒地挥动双臂*`,显示 Rust 调用了直接在 `Human` 上实现的那个 `fly` 方法。
为了调用 `Pilot``Wizard` 特质上的 `fly` 方法,咱们需要使用更为显式的语法,来指明我们所指的是那个 `fly` 方法。下面清单 19-18 对此语法进行了演示。
文件名:`src/main.rs`
```rust
fn main() {
let person = Human;
Pilot::fly(&person);
Wizard::fly(&person);
person.fly();
}
```
*清单 19-18指明咱们打算调用哪个特质的 `fly` 方法*
在方法名字前指明特质名字,就向 Rust 澄清了咱们打算调用 `fly` 的哪个实现。咱们本来也可以写下 `Human::fly(&person)`,这与咱们曾在清单 19-18 中所使用的 `person.fly()` 等级,但若咱们无需消除歧义,这样写起来就些许有些长了。
运行此代码会打印以下输出:
```console
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/disambiguation`
机长在此发言。
飞起来!
*愤怒地挥动双臂*
```
由于 `fly` 方法取了一个 `self` 参数,那么当咱们有着实现了一个 *特质* 的两个 *类型*Rust 就可以根据 `self` 的类型,找出要使用特质的哪个实现。
然而,不是方法的那些关联函数,是没有 `self` 参数的。当存在以同样函数名字,定义了非方法函数的类型或特质时,除非咱们使用了 *完全合格语法fully qualified syntax*,否则 Rust 就不会总是清楚咱们所指的是何种类型。比如,在下面清单 19-19 中,咱们创建了一个用于动物收容所的特质,其中打算将所有狗崽都命名为 `点点`。咱们构造了带有关联的非方法函数 `baby_name` 的一个 `Animal` 特质。对结构体 `Dog` 实现了这个 `Animal` 特质,在 `Dog` 上咱们还直接提供了一个关联的非方法函数 `baby_name`
```rust
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("点点")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("Puppy")
}
}
fn main() {
println! ("狗崽叫做 {}", Dog::baby_name());
}
```
*清单 19-19有着一个关联函数的特质以及一个有着同样函数名字关联函数、还实现了那个特质的类型*
咱们是在那个定义在 `Dog` 上的关联函数里,实现的将全部狗仔命名为点点的代码。`Dog` 类型还实现了特质 `Animal`,该特质描述了全部动物都有的特征。小狗都叫做狗崽,且这一点是在 `Dog` 上的 `Animal` 特质中,与 `Animal` 特质关联的 `baby_name` 函数中得以表达的。
`main` 函数中,咱们调用了那个 `Dog::baby_name` 函数,这就会调用直接定义在 `Dog` 上的那个关联函数。此代码会打印下面的输出:
```console
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/disambiguation`
狗崽叫做 点点
```
此输出不是咱们想要的。咱们想要调用作为咱们曾在 `Dog` 上实现过的 `Animal` 特质一部分的那个 `baby_name` 函数,从而代码会打印出 `小狗叫做 狗崽`。咱们曾在清单 19-18 中用到的指定特质名字的技巧,这里就不管用了;而若咱们将 `main` 修改为下面清单 19-20 中的代码,咱们就将收到一个编译报错。
```rust
fn main() {
println! ("小狗叫做 {}", Animal::baby_name());
}
```
*清单 19-20尝试调用 `Animal` 特质中的那个 `baby_name` 函数,但 Rust 不清楚要使用那个实现*
由于 `Animal::baby_name` 没有 `self` 参数,且这里可能有别的实现了 `Animal` 特质的类型,因此 Rust 就无法计算出咱们想要的那个 `Animal::baby_name` 实现。咱们将得到下面这个编译器错误:
```console
$ cargo run
Compiling disambiguation v0.1.0 (/home/lenny.peng/rust-lang/disambiguation)
error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
--> src/main.rs:20:26
|
2 | fn baby_name() -> String;
| ------------------------- `Animal::baby_name` defined here
...
20 | println! ("小狗叫做 {}", Animal::baby_name());
| ^^^^^^^^^^^^^^^^^ cannot call associated function of trait
|
help: use the fully-qualified path to the only available implementation
|
20 | println! ("小狗叫做 {}", <Dog as Animal>::baby_name());
| +++++++ +
For more information about this error, try `rustc --explain E0790`.
error: could not compile `disambiguation` due to previous error
```
为消除歧义并告知 Rust 咱们打算使用 `Dog` 的那个 `Animal` 实现,而非某种其他类型的 `Animal` 实现,咱们需要使用完全合格语法。下面清单 19-21 演示了怎样使用完全合格语法。
文件名:`src/main.rs`
```rust
fn main() {
println! ("小狗叫做 {}", <Dog as Animal>::baby_name());
}
```
*清单 19-21使用完全合格语法来指明咱们是要调用实现在 `Dog` 上的 `Animal` 特质中的那个 `baby_name` 函数*
通过讲出咱们希望将 `Dog` 类型,针对这个 `baby_name` 函数调用而作为 `Animal` 对待,从而表明咱们打算调用实现在 `Dog` 上的 `Animal` 特质中的 `baby_name` 方法,这样位处那尖括号中的类型注解,提供给 Rust。此代码现在将打印出咱们想要的输出
```console
$ cargo run
Compiling disambiguation v0.1.0 (/home/lenny.peng/rust-lang/disambiguation)
Finished dev [unoptimized + debuginfo] target(s) in 0.18s
Running `target/debug/disambiguation`
小狗叫做 狗崽
```
一般来讲,完全合格语法是像下面这样定义的:
```rust
<Type as Trait>::function(receiver_if_method, next_arg, ...);
```
对于那些不是方法的语法,此处就不会有 `receiver`:这里将只有其他参数的清单。在调用函数或方法的所有地方,咱们都可以使用完全合格语法。不过,在 Rust 能够从程序中另外的信息计算出(要调用哪个函数或方法)时,那么这种语法便是可以省略的。咱们只需在有着多个使用了同一名字的实现,且 Rust 需要帮助来识别出咱们打算调用哪个实现时,才需要使用这种更为冗长的语法。
## 在一个特质里运用超特质寻求另一特质的功能
**Using Supertraits to Require One Trait's Functionality Within Another Trait**
有的时候,咱们可能会编写依赖于另一特质的特质:对于要实现前一个特质的类型,咱们希望寻求那个类型也实现后一个特质。为了咱们的特质定义,可以利用后一个特质的那些关联项目,咱们就会实现这一点。咱们的特质所依赖的那个特质,被称为咱们特质的 *超特质supertrait*
比方说,咱们打算构造一个带有将所给的值格式化,从而其被星号框起来的 `outline_print` 方法,这样一个 `OutlinePrint` 特质。而那个所给的值则是,一个实现了标准库特质 `Display` 来得到 `(x, y)``Point` 结构体,即当咱们在有着 `x``1` `y``3``Point` 上调用 `outline_print` 时,其将打印以下输出:
```console
**********
* *
* (1, 3) *
* *
**********
```
`outline_print` 方法的实现中,咱们打算使用 `Display` 特质的功能。因此,咱们就需要指明,这个 `OutlinePrint` 特质将只对那些同时实现了 `Display` 生效,且提供了 `OutlinePrint` 所需的功能。咱们可以通过指明 `OutlinePrint: Display`,在该特质定义中实现那一点。这种技巧类似于给特质添加特质边界。下面清单 19-22 给出了这个 `OutlinePrint` 特质的一种实现。
```rust
use std::fmt;
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println! ("{}", "*".repeat(len + 4));
println! ("*{}*", " ".repeat(len + 2));
println! ("* {} *", output);
println! ("*{}*", " ".repeat(len + 2));
println! ("{}", "*".repeat(len + 4));
}
}
```
*清单 19-22需要 `Display` 中功能的 `OutlinePrint` 特质实现*
由于咱们已指明 `OutlinePrint` 需要 `Display` 特质,因此咱们就可以使用那个任何实现了 `Display` 类型上均已实现了的 `to_string` 函数。若咱们在没有于特质名字之后加上冒号并指明 `Display` 特质,便尝试使用 `to_string`,咱们就会得到一个声称当前作用域中的类型 `&Self` 下,未找到名为 `to_string` 的方法的报错。
下面来看看当咱们尝试在某个未实现 `Display` 的类型,比如 `Point` 结构体上,实现 `OutlinePrint` 时会发生什么:
```rust
struct Point {
x: i32,
y: i32,
}
impl OutlinePrint for Point {}
```
咱们会得到一个声称要求 `Display` 当其未实现的报错:
```console
$ cargo run
Compiling supertrait v0.1.0 (/home/lenny.peng/rust-lang/supertrait)
error[E0277]: `Point` doesn't implement `std::fmt::Display`
--> src/main.rs:21:23
|
21 | impl OutlinePrint for Point {}
| ^^^^^ `Point` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `Point`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
note: required by a bound in `OutlinePrint`
--> src/main.rs:3:21
|
3 | trait OutlinePrint: fmt::Display {
| ^^^^^^^^^^^^ required by this bound in `OutlinePrint`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `supertrait` due to previous error
```
为修复这个问题,咱们就要在 `Point` 上实现 `Display` 并满足 `OutlinePrint` 所需的约束,如下面这样:
```rust
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write! (f, "({}, {})", self.x, self.y)
}
}
```
随后在 `Point` 上实现 `OutlinePrint` 就将成功编译,而咱们就可以在 `Point` 实例上调用 `outline_print` 来将其实现在星号轮廓里了。
## 使用新型模式在外层类型上实现外层的特质
**Using the Newtype Pattern to Implement External Traits on External Types**
第 10 章中的 [“在类型上实现特质”](Ch10_Generic_Types_Traits_and_Lifetimes.md#在类型上实现某个特质) 小节咱们曾提到指明只有当特质或类型二者之一属于代码本地的时咱们才被允许在类型上实现特质的孤儿规则the orphan rule。而使用涉及到在元组结构体中创建出一个新类型的 *新型模式newtype pattern*,那么绕过这种限制便是可行的了。(咱们曾在第 5 章的 [“使用不带命名字段的元组结构体来创建不同类型”](Ch05_Using_Structs_to_Structure_Related_Data.md#使用不带命名字段的元组结构体来创建不同类型) 小节谈到过元组结构体这种元组结构体讲有一个字段且将是围绕咱们要实现某个特质的类型的一个瘦封装a thin wrapper。随后这个封装类型便是咱们代码箱的本地类型了而咱们就可以在这个封装上实现那个特质了。所谓 *新型newtype*,是源自 Haskell 编程语言的一个术语。使用这种模式没有运行时性能代码,同时那个封装类型在编译时会被略去。
作为一个示例,就说咱们打算在 `Vec<T>` 上实现 `Display`,而由于 `Display` 特质与 `Vec<T>` 类型,均被定义在咱们代码箱外部,因此孤儿规则会阻止咱们直接这样做。咱们可以构造一个保存着 `Vec<T>` 类型实例的 `Wrapper`;随后咱们就可以在 `Wrapper` 上实现 `Display`,并使用那个 `Vec<T>` 值,如下清单 19-23 中所示。
文件名:`src/main.rs`
```rust
use std::fmt;
struct Wrapper(Vec<String>);
impl fmt::Display for Wrapper {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write! (f, "[{}]", self.0.join(", "))
}
}
fn main() {
let w = Wrapper(vec! [String::from("你好"), String::from("世界")]);
println! ("w = {}", w);
}
```
*清单 19-23创建一个围绕 `Vec<String>` 的 `Wrapper` 类型来实现 `Display`*
由于 `Wrapper` 是个元组结构体,且 `Vec<T>` 是该元组中位于索引 `0` 处的项目,因此其中 `Display` 的实现,便使用了 `self.0` 来方法那个内部的 `Vec<T>`。随后咱们就可以在 `Wrapper` 上使用 `Display` 的功能了。
使用这种技巧的缺点,则是那个 `Wrapper` 是个新的类型,因此其没有他所保存值的那些方法。咱们讲必须直接在 `Wrapper` 上,实现 `Vec<T>` 的全部方法,即委托给 `self.0` 的那些方法,这就会允许咱们将 `Wrapper` 完全当作 `Vec<T>` 那样对待了。而若咱们想要这个新的类型,有着那个内部类型所有的全部方法,那么在 `Wrapper` 上实现 `Deref` 特质(曾在第 15 章的 [“运用 `Deref` 特质将灵巧指针像常规引用那样对待”](Ch15_Smart_Pointers.md#通过实现-deref-特质而像引用那样对待某个类型) 小节讨论过),来返回那个内部类型,将是一种办法。而若咱们不打算 `Wrapper` 类型有着内部类型的所有方法 -- 比如,为限制 `Wrapper` 的行为 -- 咱们就必须手动实现仅咱们想要的那些方法了。
即使不牵涉到特质,这种新型模式也是有用的。接下来就要转换一下视角,而看看与 Rust 的类型系统交互的一些高级方式。
End

View File

@@ -0,0 +1,255 @@
# 高级类型
**Advanced Types**
Rust 的类型系统有着一些到目前为止咱们曾提到过但尚未讨论过的特性。咱们将以一般意义上检视新型模式作为类型为何有用,而讨论新型模式开始。随后咱们将移步到类型别名,一项与新型模式类似,不过有着些许不同语义的特性。咱们还将讨论 `!` 类型与动态大小的类型。
## 为类型安全与抽象而运用新型模式
**Using the Newtype Pattern for Type Safety and Abstraction**
> **注意**:此小节假定你已读过早先的 [“使用新型模式来再外层类型上实现外层的特质”](#使用新型模式在外层类型上实现外层的特质") 小节。
对于那些超出到目前为止咱们曾讨论过的任务,包括静态强制要求值绝不会混淆,以及表明某个值的单位等等,新型模式同样是有用的。在清单 19-15 中,咱们就曾看到一个使用新型,表明单位的一个示例:回顾到 `Millimeters``Meters` 两个结构体,都曾将 `u32` 值封装在新型中。而若咱们编写了带有一个类型 `Millimeters` 参数的函数,那么咱们就无法编译某个偶然尝试以类型 `Meters` 或普通 `u32` 的值,调用那个函数的程序。
咱们还可以使用新型模式,来抽象出某个类型的一些实现细节:新的类型可暴露处不同意私有内部类型 API 的一个公开 API。
新类型还可以隐藏内部实现。比如,咱们可提供一个 `People` 类型,来封装一个存储着某人与其名字关联的 ID 的 `HashMap<i32, String>`。使用 `People` 的代码,只需与咱们提供的公开 API比如某个将名字字符串添加到 `People` 集合的方法交互;那些代码将不需要知悉咱们在内部分配了`i32` 的 ID 给那些名字。新型模式是达成,咱们曾在第 17 章讨论过的 [“隐藏实现细节的封装”](Ch17_Object_Oriented_Programming_Features_of_Rust.md#隐藏了实现细节的封装) 的一种轻量方式。
## 使用类型别名创建类型同义词
**Creating Type Synonyms with Type Aliases**
Rust 提供给到既有类型另一个名字的声明 *类型别名type alias* 的能力。为此,咱们要使用 `type` 关键字。比如,咱们可以像下面这样,创建到 `i32` 的别名 `Kilometers`
```rust
type Kilometers = i32;
```
现在,别名 `Kilometers` 便是 `i32` 的同义词了;与在清单 19-15 中咱们曾创建的 `Millimeters``Meters` 两个类型不同,`Kilometers` 不是个单独的、新类型。有着类型 `Kilometers` 的那些值,将与类型 `i32` 的那些值做同样对待:
```rust
type Kilometers = i32;
let x: i32 = 5;
let y: Kilometers = 5;
assert_eq! (x, y);
```
由于 `Kilometers``i32` 为同样类型,因此咱们可将这两种类型的值相加,且咱们可将 `Kilometers` 值传递给取 `i32` 参数的那些函数。但是,在使用这种方法时,咱们不会获得咱们早先所讨论的新型模式中的类型检查的那些益处。换句话说,当咱们在一些地方混淆了 `Kilometers``i32` 时,编译器将不会给到咱们一个报错。
类型同义词的一种主要用例,是为减少重复。比如,咱们可能有下面这样一个冗长的类型:
```rust
Box<dyn Fn() + Send + 'static>
```
在函数签名中,以及在全部代码中作为类型注解编写这种冗长类型,就会令人疲倦而容易出错。设想有个全部是下面清单 19-24 中代码的项目:
```rust
let f: Box<dyn Fn() + Send + 'static> = Box::new(|| println! (""));
fn takes_long_type(f: Box<dyn Fn() + Send + 'static>) {
// --跳过代码--
}
fn returns_long_type() -> Box<dyn Fn() + Send + 'static> {
// --跳过代码--
}
```
*清单 19-24在多处使用长类型*
类型别名通过降低重复,而令到这样的代码更为可管理。在下面清单 19-25 中,咱们为那个冗长类型,引入了一个名为 `Thunk` 的别名,从而便可以使用这个更简短的别名 `Thunk`,替换全部的该种类型。
```rust
type Thunk = Box<dyn Fn() + Send + 'static>;
let f: Thunk = Box::new(|| println! (""));
fn takes_long_type(f: Thunk) {
// --跳过代码--
}
fn returns_long_type() -> Thunk {
// --跳过代码--
}
```
*清单 19-25引入类型别名 `Thunk` 来减少重复*
这样的代码,阅读和编写起来要容易得多!给类型别名选择有意义的名字,也可以有助于表达咱们的意图( *形实替换thunk* 是个表示会在稍后被计算执行,因此对于会被存储的闭包,其是个恰当的名字)。
类型别名,还普遍用于 `Result<T, E>` 下的消除重复。设想标准库中的 `std::io` 模组。I/O 操作经常会返回一个 `Result<T, E>`,以处理操作失效时的情况。这个库有个表示了所有可能 I/O 错误的 `std::io::Error` 结构。`std::io` 中的许多函数,都会在那个 `E``std::io::Error` 下,返回 `Result<T, E>`,比如 `Write` 特质中的这些函数:
```rust
use std::fmt;
use std::io::Error;
pub trait Write {
fn write(&mut self, buf: &[u8]) -> Result<usize, Error>;
fn flush(&mut self) -> Result<(), Error>;
fn write_all(&mut self, buf: &[u8]) -> Result<(), Error>;
fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<(), Error>;
}
```
其中的 `Result<..., Error>` 就被重复了很多。由此,`std::io` 便有了下面这样的类型别名声明:
```rust
type Result<T> = std::result::Result<T, std::io::Error>;
```
由于这种声明是在 `std::io` 模组中,因此咱们就可以使用完全合格的别名 `std::io::Result<T>`;那即是,带有 `E` 被填充为 `std::io::Error``Result<T, E>`。那个 `Write` 特质的函数签名,最终看起来就像下面这样了:
```rust
pub trait Write {
fn write(&mut self, buf: &[u8]) -> Result<usize>;
fn flush(&mut self) -> Result<()>;
fn write_all(&mut self, buf: &[u8]) -> Result<();
fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<()>;
}
```
类型别名以这两种方式发挥作用:其令到代码更易于编写 ** 在整个 `std::io` 层面给到咱们一个一致的接口。由于其为一个别名,因此他仅是另一个 `Result<T, E>`,这意味着咱们可以与其一道使用那些全部工作于 `Result<T, E>` 上的方法,以及诸如 `?` 运算符那样的特殊语法。
## 永不返回的永不类型
**The Never Type that Never Returns**
Rust 有着一种因其没有值,而因此在类型理论术语中,叫做 *空类型empty type* 的名为 `!` 的类型。因为在某个函数绝不会返回值时,这个类型立于返回值类型处,所以咱们称其为 *永不类型never type*。下面是个示例:
```rust
fn bar() -> ! {
// --跳过代码--
}
```
此代码读作 “函数 `bar` 返回永不。” 返回永不的函数被称为 *发散函数diverging functions*。咱们无法创建出类型 `!` 的值,因此 `bar` 就永不会有可能返回值。
然而一种咱们永不能创建出值的类型,到底有什么用处呢?回顾到清单 2-5 中,作为那个猜数游戏一部分的代码;咱们已在在下面清单 19-26 中,重现了他的一点点:
```rust
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
```
*清单 19-26有着一个以 `continue` 结束支臂的 `match` 表达式*
那个时候,咱们跳过了此代码的一些细节。而在第 6 章中的 [“`match` 控制流运算符”](Ch06_Enums_and_Pattern_Matching.md#match-控制流结构) 小节,咱们曾讨论了 `match` 支臂必须全部返回同一类型。那么,比如说,下面的代码就不会工作:
```rust
let guess = match guess.trim().parse() {
Ok(_) => 5,
Err(_) => "你好",
}
```
此代码中的 `guess` 类型,将必须为整数与字符串,而 Rust 要求 `guess` 只有一种类型。那么 `continue` 到底返回的是什么呢?到底是怎样咱们才在清单 19-26 中,曾被允许从一个支臂返回一个 `u32`,并有着以 `continue` 结束另一个支臂的呢?
描述这种行为的正式方式,即类型 `!` 的表达式,可被强制转换为任何别的类型。由于 `continue` 不会返回值,因此咱们就被允许以 `continue` 结束这个 `match` 支臂;相反,这个 `match` 支臂将控制移回到该循环的顶部,因此在 `Err` 情形下,咱们就绝不会赋给 `guess` 一个值。
`panic!` 宏下,这个永不类型也是有用的。回顾到咱们在 `Option<T>` 值上调用 `unwrap` 函数来生成一个值,或在此定义下中止运行:
```rust
impl<T> Option<T> {
pub fn unwrap(self) -> {
match self {
Some(val) => val,
None => panic! ("在 `None` value 上调用了 `Option::unwrap()`"),
}
}
}
```
此代码中,与清单 19-26 中那个 `match` 同样的事情发生了Rust 会发现那个 `val` 有着类型 `T`,且 `panic!` 有着类型 `!`,因此整个 `match` 表达式的结果便是 `T`。此代码之所以有效,是由于 `panic!` 不会产生值;他会终止这个程序。在 `None` 情形下,咱们不会从 `unwrap` 返回值,所以此代码是有效的。
最后一个有着类型 `!` 的表达式,则是一个 `loop`
```rust
print! ("永永 ");
loop {
print! ("远远 ");
}
```
这里,那个循环永不会结束,因此 `!` 便是该表达式的值。但是,若咱们包含了一个 `break`,由于这个循环会在其到达 `break` 时终止,因此这就不再成立了。
## 动态大小的类型与 `Sized` 特质
**Dynamically Sized Types and the `Sized` Trait**
Rust 需要知道其类型的确切情况,比如给某种特定类型值分配多少的内存空间。在一开始这就给其类型系统的一个角落留下了一点混乱:那便是 *动态大小类型dynamically sized types* 这个概念。此概念有时被称为 DSTs 或 *未知大小类型unsized types*,这些类型让咱们编写出,使用了仅在运行时才知道其大小值的代码来。
下面来深入到名为 `str`,贯穿这本书咱们一直都在使用一个的动态大小类型细节。那正是 `str`,而非 `&str`,确实是个 DST。在运行时之前咱们是无法掌握字符串有多长就是说咱们无法创建出一个类型 `str` 的变量,也无法取类型 `str` 的参数。设想下面的这段无法工作的代码:
```rust
let s1: str = "致以问候!";
let s2: str = "最近过得怎么样?";
```
Rust 需要清楚,要给特定类型的任何值分配多少内存,且某种类型的所有值,都必须使用同样数量的内存。若 Rust 运行咱们编写此代码,那么这两个 `str` 值就将需要占据同样数量的内存空间。但他们有着不同长度:`s1` 需要 15 字节的存储,而 `s2` 需要 `24` 字节。这就是为何创建保存动态大小类型值的变量不可行的原因。
那么咱们要怎么做呢?在这种情况下,咱们就已经知道答案了:咱们要令到 `s1``s2` 的类型为 `&str` 而非 `str`。从第 4 章的 [“字符串切片”](Ch04_Understanding_Ownership.md#字符串切片) 小节,回顾到切片数据结构,只会存储其开始位置和切片的长度。因此尽管 `&T` 是存储了 `T` 所处内存地址的单个值,而一个 `&str` 则是 *两个* 值:`str` 的地址与其长度。如此,咱们就知道某个 `&str` 在编译时的大小了:其为 `uszie` 长度的两倍。那便是,咱们总是清楚 `&str` 的大小,而不管他所指向的字符串有多长。一般来说,这就是 Rust 中动态大小类型被运用的方式:他们有着存储了动态信息大小的额外的一点元数据。动态大小类型的黄金法则,就是咱们必须始终把那些动态大小类型的值,放置某种指针之后。
咱们可将 `str` 与所有类别的指针结合:比如,`Box<str>``Rc<str>`。事实上,之前咱们就已经见到过这样的,只不过是在一种不同的动态大小类型下:那便是特质。每个特质都是咱们可以通过使用特质名字而加以引用的动态大小类型。在第 17 章中的 [“使用允许不同类型值的特质对象”](Ch17_Object_Oriented_Programming_Features_of_Rust.md#使用允许不同类型值的特质对象) 小节,咱们曾提到为了将特质用作特质对象,咱们就必须将其放在指针之后,比如 `&dyn Trait``Box<dyn Trait>` `Rc<dyn Trait>` 也应生效)。
为处理 DSTs 相关问题Rust 提供了 `Sized` 特质来判断在编译时某个类型的大小是否已知。在运行时大小已知的全部物件都已自动实现了这个特质。此外Rust 会隐式地将 `Sized` 上的边界,添加到每个泛型函数。那就是说,像下面的一个泛型函数:
```rust
fn generic<T>(t: T) {
// --跳过代码--
}
```
实际上会被如咱们像下面写的这样被对待:
```rust
fn generic<T: Sized>(t: T) {
// --跳过代码--
}
```
默认情况下,泛型函数只将在那些编译时有着已知大小的类型上工作。但是,咱们可以使用下面的特殊语法来解除这种限制:
```rust
fn generic<T: ?Sized>(t: &T) {
// --跳过代码--
}
```
`?Sized` 上的特质边界,表示 “`T` 可能是也可能不是 `Sized` 的”,而这样的注解就会重写泛型在编译时务必要有已知大小的默认限制。有着这种意义的 `?Trait` 语法,只对 `Sized` 可用,对其他任何特质都是不可用的。
还要注意咱们已将那个参数 `t` 的类型,从 `T` 更换为了 `&T`。由于这个类型可能不是 `Sized`,因此咱们就需要在某种指针之后使用他。在这种情况下,咱们选择了一个引用。
接下来,咱们将谈谈函数与闭包!
End

View File

@@ -0,0 +1,371 @@
# 关于宏
**Macros**
贯穿这本书,咱们业已用到像是 `println!` 这样的宏,但咱们并未完整地探讨过何为宏,以及其工作原理。 *macro* 这个术语,指的是 Rust 中的一个特性家族:有着 `macro_rules!`*声明式declarative* 宏,与如下三种 *程序性procedural* 宏:
- 指明一些在结构体及枚举上以 `derive` 属性添加代码的 **定制 `#[derive]` 的宏**custome `#[derive]` macros that specify code added with the `derive` attribute used on structs and enums
- 定义出一些可在任何项目上使用的一些定制属性的 **类属性宏**attribute-like macros that define custom attributes usable on any item
- 看起来像函数调用,但是在一些指定为其参数的令牌上操作的 **类函数宏**function-like macros that look like function calls but operate on the tokens specified as their argument。
咱们将逐个讲到这每个的宏,但首先来看看,为何在已有函数的情况下,咱们还需要宏?
> **注**:宏似乎与 Java 及 Python 等语言中的装饰器类似?
## 宏与函数的区别
**The Difference Between Macros and Functions**
根本上讲,宏是一种编写其他代码的代码编写方式,这种方式被称作 *元编程metaprogramming*。在附录 C 中,咱们会讨论那个 `derive` 属性,其会为咱们生成各种特质的实现。遍布这本书,咱们也已用到了 `println!``vec!` 两个宏。全部这些宏,都会 *展开expand* 来产生相比于咱们手写代码更多的代码。
对于降低咱们所必须编写与维护代码量,元编程是有用的,这也是函数的角色之一。但是,宏有着函数所没有的一些额外能力。
函数签名必须要声明该函数所有的参数个数与类型。而另一方面的宏,则可以取数目不定的参数:咱们可以一个参数调用 `println! ("你好")`,或以两个参数调用 `println! ("你好 {}", name)`。同时,宏是在编译器对代码的意义加以解译之前展开的,因此宏就可以,比如在给到他的类型上实现某个特质。由于函数是在运行时被调用的,而特质需要在编译时被实现,故函数没办法做到这点。
实现宏而非函数的缺点,就是因为咱们是在编写那些编写出 Rust 代码的代码,所以宏定义要比函数定义更为复杂。由于这种间接性,相比于函数定义,宏定义一般都更难阅读、理解及维护。
宏与函数的另一重要区别,便是咱们必须于某个文件中调用宏 *之前*,定义好他们或将他们带入到作用域中,这一点与可在任何地方定义并在任何地方调用的函数相反。
## 用于通用元编程的带有 `macro_rules!` 的声明式宏
**Declarative Macros with `macro_rules!` for General Metaprogramming**
Rust 中使用最广泛的宏形式,就是 **声明式宏declarative macro**。这些宏有时也被指为 “示例性宏macros by example”`macro_rules!` 宏”,或仅被指为 “宏macros”。声明式宏的核心便是实现编写出类似于 Rust `match` 表达式的一些东西来。正如在第 6 章中曾讨论过的,`match` 表达式是取一个表达式、将该表达式计算结果值与一些模式比较,而在随后返回与匹配模式相关联代码的一些控制结构。宏也会把某个值与一些与特定代码相关的模式比较:在这种情形下,那个值便是传被递给宏的字面 Rust 源代码;一些模式就与那源代码比较;而与各个模式关联的代码,在匹配上时,就会替换传递给该宏的代码。这全部都是在编译器期间发生的。
要定义宏,就要用到 `macro_rules!` 结构体下面就通过看看 `vec!` 宏是如何定义的,来探讨一下怎样使用这个 `macro_rules!`。第 8 张曾涉及到咱们可以如何使用 `vec!` 宏,来创建出有着一些特定值的新矢量。比如,下面的红会创建出一个包含三个整数的新矢量值:
```rust
let v: Vec<u32> = vec! [1, 2, 3];
```
咱们也可以使用 `vec!` 宏,构造出两个整数的矢量值,或是五个字符串的矢量值。由于咱们预先不会知道值数目和类型,因此是无法使用函数完成这同样事情的。
下面清单 19-28 给出了稍微简化后的 `vec!` 宏的定义。
文件名:`src/lib.rs`
```rust
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
```
*清单 19-28`vec!` 宏定义的简化版本*
> 注意:标准库中 `vec!` 宏的具体定义,包含了预先分配正确数量内存的代码。在这里咱们为了令到这个示例更为简单,而并未包含那些属于优化的代码。
其中的 `#[macro_export]` 注解,表明当这个宏被定义的代码箱,被带入到作用域的时候,这个宏就应成为可用。若没有这个注解,那么该宏就无法被带入到作用域。
随后咱们以 `macro_rules!`*不带* 感叹号的咱们正定义宏的名字,开始该宏的定义。在此示例总,名字即为 `vec`其后跟着表示宏定义代码体the body of the macro definition, 的一对花括号。
`vec!` 宏代码体中的结构,与 `match` 表达式的结构类似。在这里咱们有着一个带有模式 `( $( $x:expr ),* )`,跟着 `=>` 及与这个模式关联代码块的支臂。在该模式匹配是那个关联代码块将被运行be emitted。鉴于这是这个宏中的唯一支臂那么就只有一种要匹配有效方式任何其他模式都将导致报错。那些更为复杂的宏则将有着多于一个的支臂。
由于宏的那些模式,始于 Rust 代码结构而非一些值相匹配的,因此宏定义中有效的模式语法,不同于第 18 章中所涉及的模式语法。咱们来看看,清单 19-28 中各个模式片段,分别表示什么;对于宏的完整模式语法,请参见 [Rust 参考手册](https://doc.rust-lang.org/reference/macros-by-example.html)。
首选,咱们使用了一对圆括号,把整个模式包括起来。咱们使用一个美元符号(`$`),来声明出在宏系统中的,一个将要包含与这个模式匹配的 Rust 代码的变量we use a dollar sign(`$`) to declare a variable in the macro system that will contain the Rust code matching the pattern。这个美元符号明确了这是个宏变量而非一个常规 Rust 变量。接下来是捕获用于替换代码中的与圆括号中模式匹配的那些值的一对圆括号next comes a set of parentheses that captures values that match the pattern within the parentheses for use in the replacement code。在 `$()` 里的,为 `$x:expr`,这会与任意 Rust 表达式匹配,并把那个表达式命名为 `$x`
`$()` 之后的逗号,表明在匹配 `$()` 中代码的代码之后,可选择性地出现一个字面的逗号分隔符。那个 `*` 指出了该模式会与零个或更多的 `*` 之前的东西匹配。
当咱们以 `vec! [1, 2, 3];` 调用这个宏时,`$x` 就会分别与表达式 `1``2``3` 匹配三次。
现在来看看与这个支臂关联的代码体中的模式:对于匹配了模式中 `$()` 的各个部分,根据该模式匹配的次数,`$()*` 里的 `temp_vec.push()` 会被零次或更多次生成。其中的 `$x` 会被各个匹配的表达式替换。当咱们以 `vec! [1, 2, 3];` 调用这个宏时,所生成的替换这个宏的代码,将是下面这样:
```rust
{
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
}
```
咱们就已定义了可取任意数目、任意类型参数,并能生成创建出包含这些特定元素矢量的一个宏了。
要了解更多有关如何编写宏的知识,请参考在线文档或其他资源,比如由 Daniel Keep 起头Lukas Wirth 续写的 [“Rust 宏小册子”](https://veykril.github.io/tlborm/)。
## 用于从属性生成代码的程序性宏
**Procedural Macros for Generating Code from Attributes**
宏的第二种形式,便是 *程序性宏procedural macro*其行事更像函数而是程序的一种类型a type of procedure。程序性宏接收一些代码作为输入在那些代码上加以操作并产生作为输出的一些代码而如同非声明式宏所做的那样与一些模式匹配并以别的代码替换那些代码。程序性宏的三种类别分别是定制派生宏custom derive、类属性宏attribute-like 及类函数宏function-like且这三种类别的程序性宏都以类似方式运作。
在创建程序性宏时那些定义务必要位处有着特别代码箱名字的他们自己的代码箱中。这是由于咱们Rust 开发团队)希望在今后消除的一些复杂技术原因。在下面清单 19-29 中,咱们给出了如何定义一个程序性宏的方式,其中 `some_attribute` 是为使用某个特定宏变种的一个占位符。
文件名:`src/lib.rs`
```rust
use proc_macro;
#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
```
*清单 19-29 定义某个程序性宏的示例*
这个定义了某个宏的函数,会取一个 `TokenStream` 值作为输入,并产生出一个 `TokenStream` 作为输出。`TokenStream` 类型是由 Rust 所包含的 `proc_macro` 代码箱定义且表示的是一个令牌序列a sequence of tokens。这个宏的核心如此该宏在其上操作的源代码构成了那个输入的 `TokenStream`,而该宏产生的代码,便是那个输出的 `TokenStream`。该函数还有一个附加给他的属性,指出咱们正在创建的是何种的程序性宏。在同一代码箱中,咱们可以有着多种类别的程序性宏。
下面就来看看各种不同类别的程序性宏。咱们将以一个定制的派生宏开始,并于随后探讨令到其他那些宏形式有所区别的一些小差异。
## 怎样编写出定制的 `derive` 宏
**How to Write a Custom `derive` Macro**
咱们就来创建一个名为 `hello_macro` 的宏,这个宏定义了一个名为 `HelloMacro`,有着名为 `hello_macro` 的关联函数的特质。与让咱们的用户为他们的各个类型实现这个 `HelloMacro` 特质不同,咱们将提供一个程序性宏,如此用户就可以 `[derive(HelloMacro)]` 注解他们的类型,从而得到那个 `hello_macro` 函数的默认实现。默认实现将打印出 `你好,宏!我的名字是 TypeName!`,其中的 `TypeName` 是这个特质被定义所在类型的名字。也就是说,咱们将编写一些实现其他编程者编写如下清单 19-30 中用到咱们代码箱的代码。
文件名:`src/main.rs`
```rust
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
#[derive(HelloMacro)]
struct Pancakes;
fn main() {
Pancakes::hello_macro();
}
```
当我们完成编写时,此代码将打印 `你好,宏!我的名字叫 Pancakes`。第一步是要构造一个新的库代码箱,像下面这样:
```console
$ cargo new hello_macro --lib --vcs none
```
接下来,咱们将定义那个 `HelloMacro` 特质及其关联函数:
文件名:`src/lib.rs`
```rust
pub trait HelloMacro {
fn hello_macro();
}
```
咱们就有了一个特质及其函数。到这里,咱们代码箱的用户就可以实现这个特质来达成所需功能,像下面这样:
```rust
use hello_macro::HelloMacro;
struct Pancakes;
impl HelloMacro for Pancakes {
fn hello_macro() {
println! ("你好,宏!我的名字叫 Pancakes");
}
}
fn main() {
Pancakes::hello_macro();
}
```
不过,用户们将需要为各种打算使用 `hello_macro` 特质的类型,编写那个实现的代码块;而咱们原本是要他们免于必须完成这项工作的。
此外,咱们尚不能提供,有着将打印特质被实现在其上类型名字的`hello_macro` 函数默认实现Rust 没有反射能力reflection capabilities因此他无法在运行时查找处那个类型的名字。咱们需要一个宏从而在编译时生成代码。
下一步就是要定义这个程序性宏。在编写这个小节的时候,程序性宏是需要在他们自己的代码箱中的。最终这个限制可能会被消除。代码箱的结构组织与宏代码箱方面的约定如下:对于名为 `foo` 的代码箱,那么定制派生程序性宏代码箱就会叫做 `foo_derive`。下面就在咱们的 `hello_macro` 项目内,开启一个名为 `hello_macro_derive` 的新代码箱:
```console
$ cargo new hello_macro_derive --lib --vcs none
```
咱们的这两个代码箱是密切相关的,因此咱们是在咱们的 `hello_macro` 代码箱目录下,创建的这个程序性宏代码箱。而若咱们修改了 `hello_macro` 中的特质定义,咱们就将不得不也要修改 `hello_macro_derive` 中那个程序性宏。两个代码箱将需要单独发布,且使用这两个代码箱的程序员,将需要将二者都添加为依赖,并同时把他们都带入到作用域。相反,咱们可以让 `hello_macro` 代码箱,将 `hello_macro_derive` 作为依赖使用,并重导出这些程序性宏的代码。然而,咱们阻止结构该项目的这种方式,会让那些不想要 `derive` 功能的程序员,也可以使用 `hello_macro`
咱们需要将 `hello_macro_derive` 代码箱,声明为程序性宏的代码箱。如同马上就会看到的那样,咱们还需要来自 `syn``quote` 代码箱的功能,,因此咱们就需要将他们添加为依赖。请将下面的配置,添加到 `hello_macro_derive``Cargo.toml` 文件:
```toml
[lib]
proc-macro = true
[dependencies]
syn = "1.0"
quote = "1.0"
```
要开始定义这个程序性宏,就要将下面清单 19-31 中的代码,放置于 `hello_macro_derive` 代码箱的 `src/lib.rs` 文件中。请注意在咱们添加了 `impl_hello_macro` 函数定义前,此代码不会编译。
文件名:`hello_macro_derive/src/lib.rs`
```rust
use proc_macro::TokenStream;
use quote::quote;
use syn;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// 以语法树形式,构建出咱们可操作 Rust 代码的表示
// Construct a representation of Rust code as a syntax tree
// that we can manipulate
let ast = syn::parse(input).unwrap();
// 构造出这个特质实现
impl_hello_macro(&ast)
}
```
*清单 19-31多数程序性宏为处理 Rust 代码而都需要的代码*
请注意咱们已经代码分解到 `hello_macro_derive` 函数中,由其负责解析那个 `TokenStream`,而其中的 `impl_hello_macro` 函数,则负责转换那个语法树:这样做令到编写程序性宏更为方便。对于几乎每个咱们所见到的或创建的程序性宏,外层函数(此示例中的 `hello_macro_derive`)中的代码将是一致的。而咱们在那个内层函数(此示例中的 `impl_hello_macro`)中指定的代码,将依据咱们程序性宏目的而有所不同。
咱们引入了三个新的代码箱:`proc_macro`、[`syn`](https://crates.io/crates/syn) 与 [`quote`](https://crates.io/crates/quote)。`proc_macro` 代码箱是 Rust 自带的,因此咱们无需将其添加到 `Cargo.toml` 的依赖。`proc_macro` 代码箱,是实现从咱们的代码读取及操作 Rust 代码的编译器 API。
`syn` 代码箱会从一个字符串将 Rust 代码解析为咱们可在其上执行操作的一种数据结构。而 `quote` 代码箱,则会将 `syn` 数据结构,转换回 Rust 代码。这些代码箱令到解析任何一种咱们打算处理的 Rust 代码更为容易:编写出 Rust 代码的完整解析器,并非易事。
这个 `hello_macro_derive` 函数,将在咱们的库用户,于某个类型上指明 `#[derive(HelloMacro)]` 时被调用。这样做之所以可行,是由于咱们已使用 `proc_macro_derive` 注解了这里的 `hello_macro_derive` 函数,并指定了于咱们的特质名字相符的名字 `HelloMacro`;而这正是多数程序性宏所遵循的约定。
这个 `hello_macro_derive` 函数首选会将那个 `input`,从一个 `TokenStream` 转换为咱们随后可以解读并于其上操作的一种数据结构。这正是 `syn` 发挥作用之处。`syn` 中的 `parse` 函数,会取一个 `TokenStream` 并返回一个表示解析出 Rust 代码的 `DeriveInput` 数据结构。下面清单 19-32 给出了咱们对 `struct Pancakes;` 字符串进行解析而得到的 `DeriveInput` 数据结构的有关部分:
```rust
DeriveInput {
// --跳过代码--
ident: Ident {
ident: "Pancakes",
span: #0 bytes(95..103)
},
data: Struct(
DataStruct {
struct_token: Struct,
fields: Unit,
semi_token: Some(
Semi
)
}
)
}
```
*清单 19-32在对清单 19-30 中有着该宏属性的代码进行解析时咱们所得到的 `DeriveInput` 实例*
这个结构体的那些字段显示,咱们所解析的 Rust 是个有着 `Pancakes``ident`标识符意为名字的一个单元结构体a unit struct。此结构体上还有一些用于描述 Rust 各个方面的其他字段;请参阅 [有关 `DeriveInput` 的 `syn` 文档](https://docs.rs/syn/1.0/syn/struct.DeriveInput.html) 了解更多信息。
很快咱们就将实现那个 `impl_hello_macro` 函数,其中咱们将构建出咱们所打算包含的新 Rust 代码。但在咱们实现之前,请注意咱们的派生宏输出,同样是个 `TokenStream`。这个返回的 `TokenStream` 会添加到咱们代码箱用户编写的代码,因此当他们编译他们的代码箱时,他们将获得咱们在这个修改的 `TokenStream` 中所提供的额外功能。
咱们或许已经留意到,咱们调用了 `unwrap`,来在这里的到 `syn::parse` 函数调用失败时,造成那个 `hello_macro_derive` 函数终止运行。由于 `proc_macro_derive` 函数必须返回 `TokenStream`,而非 `Result` 来顺应程序性宏的 API因此咱们的程序性宏就要在出错时终止运行。咱们已通过使用 `unwrap` 简化了这个示例;在生产代码中,咱们应通过运用 `panic!``expect`,提供有关那些东西出错的更具体的错误消息。
既然咱们有了将经注解的 Rust 代码,从一个 `TokenStream` 转换为一个 `DeriveInput` 实例的代码,那么就要生成在被注解类型上实现这个 `HelloMacro` 特质的代码,如下清单 19-33 中所示。
文件名:`hello_macro_derive/src/lib.rs`
```rust
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let gen = quote! {
impl HelloMacro for #name {
fn hello_macro() {
println! ("你好,宏!我的名字叫 {}", stringify! (#name));
}
}
};
gen.into()
}
```
*清单 19-33是要解析出的 Rust 代码,实现这个 `HelloMacro` 特质*
通过使用 `ast.ident`,咱们得到了一个包含着受注解类型名字(标识符)的 `Ident` 结构体实例。清单 19-32 中的代码结构,显示当咱们在清单 19-30 中的代码上运行这个 `impl_hello_macro` 函数时,咱们得到的这个 `ident` 就将有着值为有一个 `"Pancakes"` 值的 `ident` 字段。因此,清单 19-33 中的 `name` 变量,就将包含一个在被打印出时,将为字符串 `"Pancakes"`,即清单 19-30 中那个结构体名字的 `Ident` 结构体。
其中的 `quote!` 宏,允许咱们定义出咱们打算返回的 Rust 代码。编译器会期望得到不同于这个 `quote!` 宏直接执行结果的东西,因此咱们就要将其转换为一个 `TokenStream`。咱们是通过调用的那个消费这个中间表示,并返回所需的 `TokenStream` 类型的一个值的 `into` 方法,完成这一点的。
`quote!` 宏还提供了一些非常酷的模板机制:咱们可以敲入 `#name`,而 `quote!` 就将使用变量 `name` 中的值,替换掉他。咱们甚至可以与宏工作类似方式,完成一些重复操作。请参考 [`quote` 代码箱文档](https://docs.rs/quote) 了解完整信息。
咱们是要这个程序性宏,在用户注解的类型上,生成咱们的 `HelloMacro` 特质实现,而咱们可通过使用 `#name` 做到这点。这个特质实现,有着一个名为 `hello_macro` 的函数,其函数体包含了咱们打算提供的功能:打印 `你好,宏!我的名字叫` 以及随后的那个受注解类型的名字。
这里用到的那个 `stringify!` 宏,是内建于 Rust 中的。他会取一个 Rust 表达式,比如 `1 + 2`,并在编译时将这个表达式转换为字符串字面值,比如 `"1 + 2"`。这与 `format!``println!` 这样的会执行表达式并随后将结果转换为一个 `String` 的宏不同。由于存在着那个 `#name` 输入,为一个要打印出字面值的表达式的可能,因此咱们便使用了 `stringify!`。使用 `stringify!` 还通过在编译时将 `#name` 转换为字符串字面值,而节省了一次内存分配。
到这里,在 `hello_macro``hello_macro_derive` 中,`cargo build` 都应完全成功。让我们来将这两个代码箱,连接到清单 19-30 中的代码,来看看行动中的程序性宏!在咱们的 `projects` 目录下,使用 `cargo new derive_macro_comsumer --vcs none` 创建一个新的二进制项目。咱们需要在这个 `derive_macro_comsumer` 代码箱的 `Cargo.toml` 中,把 `hello_macro``hello_macro_derive` 添加为依赖项。若咱们把咱们版本的 `hello_macro``hello_macro_derive` 发布在了 [crates.io](https://crates.io/),那么他们将为一些常规依赖;而在没有发布时,咱们可以像下面这样,将他们指定为 `path` 的依赖:
```toml
hello_macro = { path = "../hello_macro" }
hello_macro_derive = { path = "./hello_macro/hello_macro_derive" }
```
请将清单 19-30 中的代码,放入到 `src/main.rs` 中,并运行 `cargo run`:其应打印出 `你好,宏!我的名字叫 Pancakes` 在这个 `derive_macro_comsumer` 代码箱无需实现那个程序性宏中的 `HelloMacro` 特质下,该特质的实现就已被包含了;正是 `#[derive(HelloMacro)]` 添加了这个特质实现。
接下来,咱们要探讨其他类别的程序性宏,与定制派生宏有怎样的不同。
## 类属性宏
**Attribute-like macros**
类属性宏与定制派生宏类似,不过与生成 `derive` 属性的代码不同,他们允许咱们创建出新的属性。他们还更灵活:`derive` 只对结构体和枚举生效;而属性则同时可应用到其他项目,比如函数等。下面就是一个使用类属性宏的示例:比方说咱们在运用某个 web 应用框架时,就有一个对函数加以注解的名为 `route` 的属性:
```rust
#[route(GET, "/")]
fn index() {
```
这个 `#[route]` 就将是由那个框架,定义的一个程序性宏。那个宏定义函数的签名,将看起来像下面这样:
```rust
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenSteam {
```
这里,咱们有两个类型 `TokenStream` 的参数。头一个是属性的内容:即 `GET, "/"` 部分。而第二个,则是该属性被附加到的那个项目的函数体:在这个示例中,便是 `fn index() {}` 及该函数的函数体其余部分。
除此之外,类属性宏与定制派生宏以同样方式运作:咱们要创建出一个有着 `proc-macro` 代码箱类型的代码箱,并实现一个生成咱们想要代码的函数!
## 类函数宏
**Function-link macros**
类函数宏定义了看起来像函数调用的宏。与 `macro_rules!` 宏类似,他们比函数更为灵活;比如,他们就可取未知数目的参数。然而,`macro_rules!` 宏只能使用咱们早先在 [用于通用元编程的带有 `macro_rules!` 的声明式宏](#用于通用元编程的带有-macro_rules-的声明式宏) 小节,曾讨论过的 match-like 语法。而类函数宏,则会取一个 `TokenStream` 参数,而这些宏的定义,就会使用 Rust 代码,如同另外两种程序性宏所做的那样,对那个 `TokenStream` 加以操纵。作为类函数宏的一个例子,便是将如下面调用的一个 `sql!` 宏:
```rust
let sql = sql! (SELECT * FROM posts WHERE id=1);
```
这个宏会解析其内部的 SQL 语句,并就其语法方面的正确性加以检查,相比 `macro_rules!` 宏所能完成的处理,这就要复杂多了。这个 `sql!` 宏将像下面这样定义:
```rust
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
```
此定义与定制派生宏的签名类似:咱们会接收圆括号内部的那些令牌,并返回咱们所要生成的代码。
# 本章小结
咦!现在咱们在工具箱中,便有了大概率不会经常用到的一些 Rust 特性,不过咱们会明白,在一些极为特别的情况下他们会是可用的。咱们业已引入几个复杂的主题,因此在咱们于一些错误消息建议,或其他人的代码中遇到他们时,咱们就能识别出这些概念和语法。请将这一章,当作引导咱们得到解决办法的一个参考。
接下来,咱们将把这正本书中曾讨论过的所有内容,投入到实践中,而完成另一个项目!
End

View File

@@ -0,0 +1,456 @@
# 不安全的 Rust
**Unsafe Rust**
到目前为止,本书所讨论的全部代码,都曾在编译时,将 Rust 的内存安全保证进行了强制执行。然而Rust 内部有着另一种不强制进行这些内存安全保证的语言:他被叫做 *不安全的 Rustunsafe rust*,而其与常规 Rust 工作类似,只不过赋予了咱们额外的超能力。
不安全 Rust 之所以存在是因为静态分析static analysis 天生是保守的。在编译器尝试判断出代码是否维持了那些保证时,相比接受一些无效程序,则退回一些有效程序会更佳。尽管代码 *可能* 没有问题,在 Rust 编译器没有足够信息对代码有信心时,他就会退回该代码。在这些情况下,咱们就可以使用不安全代码特性,来告诉编译器,“请相信我,我明白我在做什么。”但请当心,使用不安全 Rust 要风险自担:若不当使用非安全代码,那么由内存不安全而导致的问题就会发生,比如空指针的解引用。
Rust 有着一个非安全的另外自我an unsafe alter ego的另一原因便是所采行的计算机硬件本质上是不安全的。若 Rust 不允许咱们执行非安全操作那么咱们就无法完成一些特定任务。Rust 需要允许咱们完成一些底层系统编程,诸如直接与操作系统交互,或甚至编写咱们自己的操作系统。而进行底层编程工作,是这门语言的目标之一。下面就来探讨,咱们可以使用非安全 Rust 做些什么,以及怎样使用非安全 Rust。
## 不安全的超能力
**Unsafe Superpowers**
要切换到非安全 Rust就要使用 `unsafe` 关键字,并于随后开启一个驻留着非安全代码的新代码块。在非安全 Rust 中,可以进行安全 Rust 所不能进行的五种行为,咱们把这些行为叫做 *不安全的超能力unsafe superpowers*。这些超能力包括了实现下面这些的能力:
- 解引用某个原始指针dereference a raw pointer;
- 调用某个非安全的函数或方法;
- 访问或修改某个可变静态变量;
- 实现某个非安全特质;
- 访问 `union` 类型的那些字段。
明白 `unsafe` 关键字,并不会关闭借用检查器或停用任何其他的 Rust 安全性检查,是重要的:当咱们在非安全代码中用到某个引用时,其仍将受检查。`unsafe` 关键字只给到咱们访问随后不受编译器内存检查的这五种特性访问。在非安全代码块内部,咱们仍将获得一定程度的安全性。
此外,`unsafe` 并不意味着其代码块内的代码就必然是危险的,或是明显将有着内存安全问题:其意图是作为编程者的咱们,将确保 `unsafe` 代码块内部的代码将以有效的方式访问内存。
人是容易犯错误的,而错误就会发生,但通过要求将这五种非安全操作,置于以 `unsafe` 做标记出的代码块中,咱们就将清楚,任何与内存安全相关的错误,都必须在某个 `unsafe` 代码块里。请保持那些 `unsafe` 代码块较小;当咱们在调查内存错误时,就会对这种做法感激不尽。
为尽量隔离非安全代码,最佳做法即把非安全代码,封闭在安全抽象里,而提供一个安全的 API在本章检视到非安全函数及方法时咱们将讨论这个问题to isolate unsafe code as much as possible, it's best to enclose unsafe code within a safe abstraction and provide a safe API, which we'll discuss later in the chapter when we examing unsafe functions and methods。标准库的一些部分即是作为已审核过的非安全代码的安全抽象而实现的。由于运用安全抽象是安全的因此将非安全代码封装在安全抽象中就阻止了 `unsafe` 的运用,溢出到可能会用到以 `unsafe` 代码实现功能的全部处所。
下面就来依次看看,每个的这五种超能力。咱们还将看看一些提供了到非安全代码的安全接口的一些抽象。
## 解引用原始指针
**Dereferencing a Raw Pointer**
在第 4 章的 [悬空引用](Ch04_Understanding_Ownership.md#悬空引用dangling-references) 小节,咱们曾提到编译器会确保引用始终有效。不安全的 Rust 则有着与引用类似的, 叫做 *原始指针raw pointers* 的两种新类型。与引用一样,原始指针可以是不可变或可变的,并被相应地写作 `*const T``*mut T`。其中的星号 `*` 并非是解引用运算符;他是这种类型名字的一部分。在原始指针语境下,*不可变immutable* 意指该指针在被解引用之后,不能被直接赋值。
与引用及灵巧指针不同,原始指针有着以下特征:
- 通过同一内存位置上的可变及不可变指针,或多个到内存同一位置上的可变指针,原始指针允许忽略借用规则;
- 原始指针不保证指向有效的内存;
- 原始指针允许为空 `null`
- 原始指针不会实现任何的自动清理。
经由选择不让 Rust 强制执行这些保证,咱们就可以放弃(编译器)保证的安全性,而换得更佳的性能,或与其他语言或与硬件交互的能力,二者都是在 Rust 的保证中没有实现的。
下面清单 19-1 给出了怎样从引用创建出不可变与可变原始指针的方式:
```rust
let mut num = 5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
println! ("{:?}, {:?}", r1, r2);
```
*清单 19-1自引用创建原始指针*
> 运行结果如下:
```console
$ cargo run
Compiling raw_pointers v0.1.0 (/home/lenny.peng/rust-lang/raw_pointers)
Finished dev [unoptimized + debuginfo] target(s) in 0.59s
Running `target/debug/raw_pointers`
0x7ffc2c28eb84, 0x7ffc2c28eb84
```
> 注:这里的两个内存地址一样,但每次运行会显示不同的内存地址。
请注意在此代码中,咱们并未包含 `unsafe` 关键字。咱们可在安全代码中,创建原始指针;只是咱们无法在非安全代码块外部,解引用原始指针,后面马上就将看到这一点。
为验证这一点,接下来咱们将创建咱们不能那么确定其有效性的一个原始指针。下面清单 19-2 给出了怎么创建到内存中任意位置的一个原始指针。尝试使用任意内存属于不明确行为在那个地址处可能有数据或可能没有编译器就可能优化该代码如此就没有了内存访问或是该程序可能以段错误a segmentation fault而出错。通常像下面这样编写代码并无好的理由但这样写是可能的。
```rust
let address = 0x012345usize;
let r = address as *const i32;
println! ("{:?}", r);
```
*清单 19-2创建到任意内存地址的一个原始指针*
回顾到咱们可在安全代码中创建原始指针,但咱们不能 *解引用deference* 原始指针及读取所指向的数据。下面清单 19-3 中,咱们在要求 `unsafe` 代码块的一个原始指针上,使用了解引用运算符 `*`
```rust
let mut num = 5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
unsafe {
println! ("r1 为:{}", *r1);
println! ("r2 为:{}", *r2);
}
```
*清单 19-3在 `unsafe` 代码块里解引用原始指针*
> 运行结果如下:
```console
$ cargo run
Compiling raw_pointers v0.1.0 (/home/lenny.peng/rust-lang/raw_pointers)
Finished dev [unoptimized + debuginfo] target(s) in 0.15s
Running `target/debug/raw_pointers`
r1 为5
r2 为5
```
创建指针没有什么害处;只有在咱们尝试访问其所指向的值可能遇到无效值时,才会造成危害。
还要注意在清单 19-1 与 19-3 中,咱们创建的 `*const i32``*mut i32` 两个原始指针,都指向了同一内存地址,及 `num` 所存储之处。相反若咱们尝试创建到这个 `num` 的一个不可变与可变的引用,那么由于 Rust 的所有权规则在有任何不可变引用的同时,允许可变引用,该代码就不会被编译。有了原始指针,咱们就可以创建到同一内存地址的可变指针与不可变指针,而经由那个可变指针修改数据,就会潜在的造成数据竞争。所以请当心!
在全部的这些危险之下,咱们为何还要使用原始指针呢?一个主要的原因就是在与 C 代码交互时,正如将在下一小节,[”调用非安全函数或方法“](#调用不安全函数或方法),中将看到的。另一中情况,便是在构建借用检查器不清楚的一些安全抽象时。咱们将介绍非安全函数,并在随后看看一个用到不安全代码的安全抽象。
## 调用不安全函数或方法
**Calling an Unsafe Function or Method**
在非安全代码块中咱们所能进行的第二种操作,便是调用不安全函数了。不安全函数与方法看起来就像是常规函数与方法,但他们在其余定义之前,有个额外的 `unsafe` 关键字。由于 Rust (编译器)无法保证咱们在调用该函数时,业已满足一些要求,而因此这个 `unsafe` 关键字,就表明了其本身就有着这些要求。通过在 `unsafe` 代码块中调用某个不安全函数,就是说咱们为遵守该函数的合约,而已经阅读了这个函数的文档。
下面即为一个未在其函数体中实现任何东西的名为 `dangerous` 的不安全函数:
```rust
unsafe fn dangerous() {}
unsafe {
dangerous();
}
```
咱们必须在一个单独的 `unsafe` 代码块里调用这个 `dangerous` 函数。若咱们尝试在那个 `unsafe` 代码块外部调用 `dangerous`,就将得到一个报错:
```console
$ cargo run
Compiling unsafe_functions v0.1.0 (/home/lenny.peng/rust-lang/unsafe_functions)
error[E0133]: call to unsafe function is unsafe and requires unsafe function or block
--> src/main.rs:6:5
|
6 | dangerous();
| ^^^^^^^^^^^ call to unsafe function
|
= note: consult the function's documentation for information on how to avoid undefined behavior
For more information about this error, try `rustc --explain E0133`.
error: could not compile `unsafe_functions` due to previous error
```
而在 `unsafe` 代码块下,咱们便是在对 Rust 声称,咱们已经阅读了该函数的文档,明白如何恰当地使用他,以及咱们已经检查过咱们履行了这个函数合约。
不安全函数的函数体,都是有效的一些 `unsafe` 代码块,因此就可以在不安全函数里执行其他一些不安全操作,而无需添加别的 `unsafe` 代码块。
### 创建非安全代码的安全抽象
**Creating a Safe Abstraction over Unsafe Code**
仅仅因为某个函数包含了不安全代码,并不意味着咱们就需要将这整个函数标记为 `unsafe`。事实上,将不安全代码封装在安全函数中,就是一种常见的抽象。作为一个示例,下面咱们就来研究一下标准库中的 `split_at_mut` 函数,其就需要一些不安全代码。咱们将探讨咱们该怎样实现他。这个安全方法是定义在可变切片上的:他会取得一个切片,并通过于作为参数给定的索引处分割这个切片,而将其构造为两个切片。下面清单 19-4 给出了使用 `split_at_mut` 函数的方式:
```rust
let mut v = vec! [1, 2, 3, 4, 5, 6];
let r = &mut v[..];
let (a, b) = r.split_at_mut(3);
assert_eq! (a, &mut [1, 2, 3]);
assert_eq! (b, &mut [4, 5, 6]);
```
*清单 19-4使用安全的 `split_at_mut` 函数*
仅使用安全的 Rust咱们是没法实现这个函数的。一种尝试可能看起来像清单 19-5 那样,其不会编译。为简化起见,咱们将把 `split_at_mut` 实现为一个函数而非方法,并只对 `i32` 的值而非泛型 `T` 实现。
```rust
fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = values.len();
assert! (mid <= len);
(&mut values[..mid], &mut values[mid..])
}
```
*清单 19-5仅使用安全的 Rust 的`split_at_mut` 的一种实现尝试*
这个函数首先得到的是那个切片的总长度。随后其通过检查作为参数所给到的索引小于等于这个总长度,而断言了该索引是在切片里的。这个断言意味着在咱们传入了大于要分割切片长度的一个索引时,该函数将在他尝试使用那个索引前终止运行。
随后咱们返回了在一个元组中的两个可变切片:一个来自原始切片开头到 `mid` 索引处,而另一个则是来自从 `mid` 处到那个切片的末尾。
当咱们尝试编译清单 19-5 中的代码时,就将得到一个报错:
```rust
$ cargo run
Compiling safe_abstraction v0.1.0 (/home/lenny.peng/rust-lang/safe_abstraction)
error[E0499]: cannot borrow `*values` as mutable more than once at a time
--> src/main.rs:8:31
|
3 | fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
| - let's call the lifetime of this reference `'1`
...
8 | (&mut values[..mid], &mut values[mid..])
| --------------------------^^^^^^--------
| | | |
| | | second mutable borrow occurs here
| | first mutable borrow occurs here
| returning this value requires that `*values` is borrowed for `'1`
For more information about this error, try `rustc --explain E0499`.
error: could not compile `safe_abstraction` due to previous error
```
Rust 的借用检查器无法搞清楚,咱们是在借用那个切片的不同部分;他只知道咱们借用了同一切片两次。由于借用切片的两个不同部分没有重叠,因此这样做从根本上讲是可以的,但 Rust 没有足够聪明到明白这点。在咱们清楚代码是没有问题的,而 Rust 并不清楚时,你们就是要用到不安全代码的时候了。
清单 19-6 给出了如何使用一个 `unsafe` 代码块、一个原始指针,以及一些到非安全函数的调用,来领到这个 `split_at_mut` 实现工作的方式。
```rust
use std::slice;
fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = values.len();
let ptr = values.as_mut_ptr();
assert! (mid <= len);
unsafe {
(
slice::from_raw_parts_mut(ptr, mid),
slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
```
*清单 19-6 在 `split_at_mut` 函数实现中使用不安全代码*
回顾第 4 章中的 [“切片类型”](Ch04_Understanding_Ownership.md#切片类型the-slice-type) 小节,切片即为到一些数据的指针,与切片的长度。咱们使用了 `len` 方法,来获取切片的长度,并使用 `as_mut_ptr` 方法来访问切片的原始指针。在这个示例中,由于咱们有着一个到一些 `i32` 值的可变切片,`as_mut_prr` 就会返回类型 `*mut i32` 的原始指针,其已被咱们存储在变量 `ptr` 中。
咱们保留了那个 `mid` 索引是在切片里的断言。随后咱们就到了那不安全代码处:`slice::from_raw_parts_mut` 函数会取一个原始指针及长度,并创建出一个切片。咱们使用这个函数,来创建自 `ptr` 开始,且长度为 `mid` 的一个切片。随后咱们以 `mid` 作为参数,调用 `ptr` 上的 `add` 方法,来得到于 `mid` 处开始的一个原始指针,而咱们创建出使用那个指针,且以 `mid` 之后项目数量为长度的一个切片。
由于函数 `slice::from_raw_parts_mut` 取了一个原始指针,且必须相信这个指针是有效的,因此该函数是不安全的。由于原始指针上的 `add` 方法必须相信那个偏移地址亦为有效指针,故其也是不安全的。因此,咱们就不得不在这些到 `slice::from_raw_parts_mut``add` 的调用周围,放置一个 `unsafe` 代码块,从而才可以调用他们。通过查阅代码,及添加上 `mid` 务必小于等于 `len` 的断言,咱们就可以讲,在那个 `unsafe` 代码块里用到的全部原始指针,都将是到那个切片里数据的有效指针。这便是 `unsafe` 可接受及合理的使用。
请注意咱们无需将所得的 `split_at_mut` 函数标记为 `unsafe`,且咱们可以从安全的 Rust 调用这个函数。由于这个函数实现只会创建出其所访问数据的有效指针,因此他是以安全方式使用的 `unsafe` 代码,而咱们则以这个函数实现,就已经创建到非安全代码的安全抽象了。
作为对照,下面清单 19-7 中 `slice::from_raw_parts_mut` 的使用,于那个切片被用到时,大致就会崩溃。此代码取的是一个任意内存地址,并创建了有着 10,000 个条目长的切片。
```rust
use std::slice;
let address = 0x01234usize;
let r = address as *mut i32;
let values: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) };
```
*清单 19-7自任意内存地址创建切片*
> *注*:上面的代码运行结果:
```console
$ cargo run
Compiling safe_abstraction v0.1.0 (/home/lenny.peng/rust-lang/safe_abstraction)
Finished dev [unoptimized + debuginfo] target(s) in 0.15s
Running `target/debug/safe_abstraction`
```
> 可见并无报错,但若加上 `println! (":?", values);` 语句,运行结果将如下:
```console
$ cargo run
Compiling safe_abstraction v0.1.0 (/home/lenny.peng/rust-lang/safe_abstraction)
Finished dev [unoptimized + debuginfo] target(s) in 0.17s
Running `target/debug/safe_abstraction`
Segmentation fault (core dumped)
```
> 报出了段错误。
咱们并不拥有位于此任意地址处的内存,且没有此代码所创建出的切片,包含着一些有效 `i32` 值方面的保证。那么尝试将 `values` 当作其为有效切片使用就会导致未定义行为undefined behavior。
### 使用 `extern` 的函数调用外部代码
**Using `extern` Functions to Call External Code**
有的时候,咱们的 Rust 代码可能需要跟以其他语言编写的代码交互。为这个目的Rust 有着一个推动 *异种函数接口Foreign Function Interface, FFI* 的创建与运用的 `extern` 关键字。所谓 FFI是某门编程语言用于定义出一些函数并实现一门别的异种编程语言来调用这些函数的方式。
下面清单 19-8 演示了怎样建立与来自 C 语言标准库 `abs` 函数的集成。从 Rust 代码调用 `extern` 代码块中声明的函数,总是不安全的。原因在于其他语言没有强制执行 Rust 的规则与保证,同时 Rust 无法对他们加以检查,因此确保安全性的责任,就落在编程者身上。
文件名:`src/main.rs`
```rust
extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println! ("C 语言中 -3 的绝对值为:{}", abs(-3));
}
}
```
*清单 19-8声明并调用定义在别的语言中的 `extern` 函数*
> 上面代码运行结果为:
```console
$ cargo run
Compiling extern_code v0.1.0 (/home/lenny.peng/rust-lang/extern_code)
Finished dev [unoptimized + debuginfo] target(s) in 0.37s
Running `target/debug/extern_code`
C 语言中 -3 的绝对值为3
```
在那个 `extern "C"` 代码块里头,咱们列出了咱们打算调用的,来自另一语言的函数名字与签名。其中的 `"C"` 部分,定义了外部函数用到的何种 *应用二进制接口application binary interface, ABI*:正是 ABI定义了在汇编层面at the assembly level调用该函数的方式。而这个 `"C"` ABI便是最常用的且其遵循着 C 编程语言的 ABI。
> **自其他语言调用 Rust 的函数calling Rust functions from other languages**
>
> 咱们还可以使用 `extern` 关键字,来创建允许其他语言调用 Rust 函数的接口。与创建出整个 `extern` 代码块不同,咱们只是要在相关函数的 `fn` 关键字前,添加 `extern` 关键字,并指定出要使用的 ABI。咱们还需添加一个 `#[no_mangle]` 注解,来告诉 Rust 编译器不要修饰这个函数的名字mangle the name of this function。所谓 *名字修饰Mangling*,是在编译器将咱们给到某个函数的名字,修改为别的包含了给到编译过程其他部分消费的更多信息,但对人类更难于阅读名字的做法。各种编程语言的编译器,对名字的修饰会略有不同,因此为了 Rust 函数可被其他语言命名,咱们就必须关闭 Rust 编译器的名字装饰。
>
> 在下面的示例中咱们构造了一个其被编译到共享库a shared library并从 C 代码链接后,便可从 C 语言代码访问的 `call_from_c` 函数:
```rust
#[no_mangle]
pub extern "C" fn call_from_c() {
println! ("刚从 C 调用了一个 Rust 函数!");
}
```
> `extern` 的这种用法,不需要 `unsafe` 关键字。
## 访问或修改可变静态变量
**Accessing or Modifying a Mutable Static Variable**
在本书中,咱们还不曾讲到过 *全局变量global variables*,其不受 Rust 不支持而会与 Rust 的所有权规则发生问题。在两个线程都访问同一可变全局变量时,就会引起数据竞争。
在 Rust 中,全局变量被称为 *静态static* 变量。下面清单 19-9 给出了有着字符串切片作为值的,一个静态变量的示例声明与运用。
文件名:`src/main.rs`
```rust
static HELLO_WORLD: &str = "你好,世界!";
fn main() {
println! ("名字为:{}", HELLO_WORLD);
}
```
*清单 19-9定义并使用不可变静态变量*
静态变量与咱们曾在第三章中 [“变量与常量区别”](Ch03_Common_Programming_Concepts.md#常量) 小节讨论过的常量类似。静态变量的名字,依约定都是 `SCREAMING_SNAKE_CASE` 形式。静态变量只能存储有着 `'static` 声明周期的引用,这意味着 Rust 编译器可以计算出声明周期,而不要求咱们显式地对其加以注解。访问不可变的静态变量是安全的。
常量与不可变静态变量的细微差别在于,静态变量里的值在内存中有着固定地址。用到该值就总是将访问同一数据。而另一方面的常量,则凡是在用到他们时,都是允许复制他们数据的。另一不同便是,静态变量可以是可变的。访问与修改可变静态变量是 *不安全的*。下面清单 19-10 给出了如何声明、访问及修改名为 `COUNT` 的可变静态变量方式。
文件名:`src/main.rs`
```rust
static mut COUNTER: u32 = 0;
fn add_to_count(inc: u32) {
unsafe {
COUNTER += inc;
}
}
fn main() {
add_to_count(3);
unsafe {
println! ("COUNTER: {}", COUNTER);
}
}
```
*清单 19-10读取自或写入到可变静态变量均为不安全的*
与常规变量一样,咱们使用 `mut` 关键字指明可变性。任何读或写 `COUNTER` 的代码,都必须是在 `unsafe` 代码块里。由于这段代码是单线程的,因此其会如咱们预期的那样,编译并打印 `COUNTER: 3`。让多个线程访问 `COUNTER`,就可能会导致数据竞争。
在全局可访问的可变数据之下,就难于确保没有数据竞争,这就是 Rust 为何将可变静态变量视为不安全的原因。在可行条件下,就要首选运用并发技巧,以及在第 16 章中曾讨论过的线程安全的灵巧指针,从而编译器将就自不同线程访问数据以安全方式完成,而加以检查。
## 实现不安全的特质
**Implementing an Unsafe Trait**
咱们可以使用 `unsafe`来实现不安全的特质。在至少有一个特质的方法有着编译器无法验证的一些定数some invariant that the compiler can't verify那么这个特质便是不安全的。通过在 `trait` 关键字前加上 `unsafe` 关键字,并将特质的实现也标记为 `unsafe`,咱们就把这个特质声明为了 `unsafe`,如下清单 19-11 中所示。
```rust
unsafe trait Foo {
// 这里是些方法
}
unsafe impl Foo for i32 {
// 方法实现在这里
}
fn main() {}
```
*清单 19-11定义并实现不安全的特质*
通过使用 `unsafe impl`咱们就承诺咱们将坚守那些编译器无法验证的定数we'll uphold the invariants that the compiler can't verify。
作为示例,请回顾第 16 章中 [“`Sync` 与 `Send` 特质下的可扩展并发”](Ch16_Fearless_Concurrency.md#sync-与-send-两个特质下的可扩展并发) 小节中,曾讨论过的 `Sync``Send` 两个标记性特质:在咱们的类型完全是由 `Send``Sync` 两种类型构成时,编译器就会自动实现这些特质。而在咱们实现某个包含了非 `Send``Sync` 的类型,比如原始指针,同时咱们打算将那个类型标记为 `Send``Sync` 时,咱们就必须使用 `unsafe`。Rust 无法验证咱们的类型坚守了其可被跨线程安全发送,或自多个线程安全访问的那些保证;因此,咱们就需要手动完成这些检查,并以 `unsafe` 照这样加以表明。
## 访问联合体的字段
**Accessing fields of a union**
使用 `unsafe` 的就只剩下最后的用法了,那便是访问 *联合体union* 的字段。`union``struct` 类似,但一次只会用到特定实例中一个声明的字段。联合体主要用于与 C 语言代码中的联合体交互。由于 Rust 无法保证在联合体示例当前所存储的数据类型,因此访问联合体字段是不安全的。在 [Rust 参考手册](https://doc.rust-lang.org/reference/items/unions.html) 中,可了解更多有关联合体的知识。
## 何时使用不安全代码
**When to use unsafe code**
运用 `unsafe` 来采取上述五种做法(超能力)没有什么过错,或者不受欢迎。但由于编译器无法助力于保持内存安全,因此要让 `unsafe` 代码正确就更为棘手一些。在有使用 `unsafe` 代码的某种理由时,就可以这样做,而在问题出现时,显式的 `unsafe` 注解,就会令到排查问题原因更为容易。
End

View File

@@ -0,0 +1,121 @@
# 附录 C派生特质
**Appendix C: Derivable Traits**
本书的多个不同地方,咱们都曾讨论过 `derive` 属性,咱们可将其应用到结构体或枚举定义。`derive` 属性会在咱们以 `derive` 语法注解的类型上,生成将以某个特质自身默认实现,而实现该特质的代码。
在这个附录中,咱们会提供到标准库中,咱们可以与 `derive` 一起使用的全部特质的参考。以下各个小节均会讲到:
- 此特质将启用那些操作符与方法;
-`derive` 所提供到的该特质实现会做些什么;
- 实现该特质对那个类型意味着什么;
- 允许及不允许实现该特质的情况;
- 需要该特质操作的示例。
若咱们想要不同于由 `derive` 属性所提供的行为,请参考 [标准库文档](https://doc.rust-lang.org/std/index.html),了解如何亲自实现各个特质的详细信息。
这里列出的这些特质,只是一些由标准库所提供的,可使用 `derive` 实现于咱们类型上的那些。定义在标准库中别的一些特质,则没有什么合理的默认行为,因此是否要以对于咱们正尝试完成的东西有意义的方式,实现他们就取决于咱们自己了。
不能派生的一个特质示例便是 `Display`其为终端用户处理格式化。咱们应始终要考虑将某个类型显示给用户的恰当方式。终端用户应被允许看到该类型的哪些部分他们会发现哪些部分是相关的数据的何种形式才是与他们最为密切相关的Rust 编译器并无这种见解,因此他就无法为咱们提供到恰当的默认行为。
这个附录中所提供到的派生特质清单并不详尽:库可以为他们自己的特质实现 `derive`,从而领导咱们可使用 `derive` 的特质清单为真正开放的。实现 `derive` 设计到使用程序性宏,这在第 19 章的 [“关于宏”](Ch19_Advanced_Features.md#关于宏) 小节讲到过。
## 输出给编程者的 `Debug`
**`Debug` for Programmer Output**
`Debug` 特质实现了格式字符串中的格式化,所谓格式字符串,即咱们通过在 `{}` 里添加 `:?` 所表示的。
`Debug` 特质允许咱们为调试目的打印某种类型的实例,如此咱们以及用到咱们类型的其他编程者,就可以在程序执行的某个特定时刻,就其某个实例加以探查。
在比如用到 `assert_eq!` 宏中等情况下,`Debug` 特质便是要求使用的。`assert_eq!` 这个宏在相等断言失败时,就会打印出作为参数所给到的两个实例值,如此编程者就可以看到为何这两个实例不相等。
## 用于相等比较的 `PartialEq` 与 `Eq`
`PartialEq` 特质允许咱们比较某种类型的两个实例,来检查他们是否相等,并实现 `==``!=` 运算符的应用。
`PartialEq` 进行派生,就会实现 `eq` 方法。当 `ParitalEq` 实在结构体上实现的时,只有在两个实例的 *全部* 字段都相等时,他们才是相等的,且在有任何字段不等时,两个实例便不相等。当在枚举上派生时,枚举的各个变种与自身相等,而不等于其他任何变种。
在使用需要能够比较某个类型的两个实例是否相等的 `assert_eq!` 宏时,就需要这个 `PartialEq` 特质。
`Eq` 特质则没有方法。他的目的是要表明,所注解的类型的每个值,其值都等于他自身。尽管并非所有实现 `PartialEq` 的类型都可以实现 `Eq`,但 `Eq` 特质却只可应用到那些同时实现了 `PartialEq` 的类型。这方面的一个示例便是浮点数类型浮点数的实现就表明两个非数字the not-a-number, `NaN`)的值,是各自不相等的。
要求 `Eq` 的一个示例,就是 `HashMap<K, V>` 中的那些键,如此 `HashMap<K, V>` 就可以区分出两个键是否一致。
## 用于排序比较的 `PartialOrd` 与 `Ord`
**`PartialOrd` and `Ord` for Ordering Comparisons**
`PartialOrd` 特质实现为排序目的,而比较某种类型的那些实例。实现了 `PartialOrd` 的类型,便可与 `<``>``<=``>=` 符号一起使用了。咱们只能对那些同时实现了 `PartialEq` 的类型,应用这个 `PartialOrd` 特质。
派生 `PartialOrd`,会实现 `partial_cmp` 方法,该方法会返回一个在所给的那些值不会产生出顺序时,将为 `None` 的一个 `Option<Ordering>`。至于即使那种类型的大多数值都可被比较,但仍不会产生出顺序的值的一个示例,便是非数字(`NaN`)浮点值。在任何浮点数和非数字浮点值下调用 `partial_cmp`,都会返回 `None`
在于结构体上派生时,`PartialOrd` 会通过字段出现在结构体定义中的顺序,比较每个字段中的值,比较两个实例。而当于枚举上派生时,枚举定义中较早声明的枚举变种,被当作是小于后面所列出的那些变种的。
在比如会产生出由范围表达式所指定范围中一个随机数的, `rand` 代码箱的 `gen_range` 方法来说,`PartialOrd` 特质便是需要的。
`Ord` 特质实现对所注解类型的任何两个值,将存在有效顺序的掌握。`Ord` 特质会实现 `cmp` 方法,由于有效排序将始终可行,因此该方法返回的是 `Ordering` 而非 `Option<Ordering>`。咱们只可对那些同时实现了 `PartialOrd``Eq` (而 `Eq` 要求 `PartialEq`) 的类型,实现这个 `Ord` 特质。当于结构体及枚举上派生 `Ord` 时,`cmp` 就会以与 `PartialOrd``partial_cmp` 的派生实现同样方式行事。
要求 `Ord` 的一个示例,即为将一些值存储在 `BTreeSet<T>` 这种根据值的排序,而存储数据的数据结构中时。
## 用于复制值的 `Clone` 与 `Copy`
**`Clone` and `Copy` for Duplicating Values**
`Clone` 特质实现了显式创建值的深拷贝而该复制过程则可能涉及运行一些任意代码arbitary code与拷贝内存堆数据。请参阅第 4 章中 [“变量与数据交互方式:克隆”](Ch04_Understanding_Ownership.md#变量与数据交互方式之二克隆) 小节,了解更多有关 `Clone` 的信息。
派生 `Clone` 会实现 `clone` 方法,当对整个类型实现了这个方法时,其就会在该类型的各个部分上调用 `clone`。这意味着类型要派生 `Clone` 其中的全部字段或值,都必须同时实现 `Clone`
需要 `Clone` 特质的一个示例,便是在切片上调用 `to_vec` 方法时。切片不持有其包含的那些类型实例,但自 `to_vec` 所返回的那个矢量值,却将需要持有他的那些实例,从而 `to_vec` 会调用各个条目上的 `clone`。因此,存储在切片中的类型,就必须实现 `Clone`
`Copy` 特质实现了只通过拷贝存储在栈上的二进制位,而复制某个值;任意代码并无必要。请参阅第 4 章中 [“唯栈数据:拷贝”](Ch04_Understanding_Ownership.md#唯栈数据拷贝stack-only-data-copy),了解更多有关 `Copy` 的信息。
`Copy` 特质没有定义阻止编程者过载那些方法,及破坏不会有任意代码运行这个假设的任何方法。那样的话,所有编程者就都可以假定,拷贝值将会非常快。
咱们可在其组成部分都实现了 `Copy` 的任何类型上派生 `Copy` 特质。由于实现 `Copy` 的类型,都有着执行与 `Copy` 同样任务的一个 `Clone` 的简单实现,因此实现 `Copy` 的类型必须同时实现 `Clone`
很少需要 `Copy` 特质;实现了 `Copy` 的类型,有着可供选择的优化方案,意味着咱们不必调用 `clone`,而调用 `clone` 会令到代码更简洁。
对于 `Copy` 下每种可能情况,咱们都可同时以 `Clone` 完成,除了代码可能更慢,或在一些地方不得不使用 `clone`
## 用于将值映射到固定大小值的 `Hash`
**`Hash` for Mapping a Value to a Value of Fixed Size**
`Hash` 特质实现了取某种任意大小类型的实例,并通过使用散列函数,将那个实例映射到固定大小的值。派生 `Hash` 会实现 `hash` 方法。`hash` 放的派生实现,会将在该类型各个组成部分上调用 `hash` 的结果结合起来,这就意味着类型要派生 `Hash`,那么其全部字段,都必须同时实现 `Hash`
要求 `Hash` 的一个示例,便是为了高效地存储数据,而在 `Hash<K, V>` 中存储那些键时。
## 用于默认值的 `Default`
**`Default` for Default Values**
`Default` 特质实现了为类型创建出一个默认值。派生 `Default` 会实现 `default` 函数。`default` 函数的派生实现,会在类型的各个部分上调用 `default` 函数,意味类型要派生 `Defualt`,其中的全部字段或值,都必须同时实现 `Default`
`Default::default` 函数,通常是与第 5 章中 [“使用结构体更新语法从其他实例创建出实例”](Ch05_Using_Structs_to_Structure_Related_Data.md#使用结构体更新语法从其他实例创建出实例) 小节里曾讨论过的结构体更新语法结合使用的。咱们可以定制结构体的几个字段,并在随后通过使用 `..Default::default()`,为其余字段设置并使用默认值。
`Option<T>` 实例上使用 `unwrap_or_default` 方法时,便是需要 `Default` 特质的一个示例。当那个 `Option<T>``None` 时,方法 `unwrap_or_default` 就将返回存储在 `Option<T>` 中,那个类型 `T``Default::default` 结果。
End

167
src/appendix/dev_tools.md Normal file
View File

@@ -0,0 +1,167 @@
# 附录 D一些有用开发工具
在此附录中,咱们会讲到 Rust 项目所提供的一些有用的开发工具。咱们将看看自动格式化、应用警告修复的一些快速方法、一种代码静态分析工具a linter以及与多种 IDE 的集成。
## 使用 `rustfmt` 的自动格式化
**Automatic Formatting with `rustfmt`**
`rustfmt` 工具会依据社区编码风格,重新格式化咱们的代码。许多协作项目,都使用了 `rustfmt` 来防止有关编写 Rust 时使用何种风格方面的争论:每个人都使用这个工具来格式化他们的代码。
要安装 `rustfmt`,请键入下面的命令:
```console
$ rustup component add rustfmt
```
如同 Rust 会同时给到 `rustc``cargo` 一样,此命令会给到咱们 `rustfmt``cargo-fmt`。要格式化任何 Cargo 项目,请敲入下面的命令:
```console
$ cargo fmt
```
运行此命令,会重新格式化当前代码箱中全部的 Rust 代码。这只会改变编码风格,而不会改变代码语义。关于 `rustfmt` 的更多信息,请参阅 [其文档](https://github.com/rust-lang/rustfmt).
## 使用 `rustfix` 修复咱们的代码
**Fix Your Code with `rustfix`**
`rustfix` 工具已被 Rust 安装所包含,并可大致以咱们想要的方式,修复那些有着明确纠正问题方法的一些编译器告警。咱们之前大概率已经见到过编译器告警了。比如,设想有下面这段代码:
文件名:`src/main.rs`
```rust
fn do_something() {}
fn main() {
for i in 0..100 {
do_something();
}
}
```
此处,咱们正调用 `do_something` 函数 100 次,但咱们在 `for` 循环的代码体中,从未用到那个变量 `i`。Rust 就会就此对咱们发出告警:
```console
$ cargo build
Compiling rustfix_demo v0.1.0 (/home/lenny.peng/rust-lang-zh_CN/rustfix_demo)
warning: unused variable: `i`
--> src/main.rs:4:9
|
4 | for i in 0..100 {
| ^ help: if this is intentional, prefix it with an underscore: `_i`
|
= note: `#[warn(unused_variables)]` on by default
warning: `rustfix_demo` (bin "rustfix_demo") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 0.29s
```
这个告警建议咱们要使用 `_i` 做名字:其中的下划线表示咱们有意不使用这个变量。通过运行 `cargo fix` 命令,咱们就可以使用 `rustfix`,自动应用那项建议:
```console
$ cargo fix --allow-no-vcs
Checking rustfix_demo v0.1.0 (/home/lenny.peng/rust-lang-zh_CN/rustfix_demo)
Fixed src/main.rs (1 fix)
Finished dev [unoptimized + debuginfo] target(s) in 0.17s
```
当咱们再次看到 `src/main.rs`,就将发现 `cargo fix` 已修改了这段代码:
文件名:`src/main.rs`
```rust
fn do_something() {}
fn main() {
for _i in 0..100 {
do_something();
}
}
```
那个 `for` 循环变量,现在就被命名为了 `_i`,同时那条告警也不再出现了。
咱们还可使用 `cargo fix` 命令,将咱们的代码在不同 Rust 版本之间转换。有关这些 Rust 版本,在附录 E 中有讲到。
## 使用 Clippy 获得更多的代码静态分析
**More Lints with Clippy**
Clippy 工具是用于分析咱们代码,从而咱们可以捕获到一些常见错误,而改进咱们 Rust 代码的一套代码静态分析集合。
要安装 Clippy请输入以下命令
```console
$ rustup component add Clippy
```
在任何 Cargo 项目上要运行 Clippy 的静态分析,请输入以下命令:
```console
$ cargo clippy
```
比如说咱们编写了像下面这个程序这样,用到某个数学常量近似值,好比说 `pi`,的一个程序:
文件名:`src/main.rs`
```rust
fn main() {
let x = 3.1415;
let r = 8.0;
println!("圆的面积为 {}", x * r * r);
}
```
在这个项目上运行 `cargo clippy` 就会得到下面的报错:
```console
$ cargo clippy
Checking clippy_demo v0.1.0 (/home/lenny.peng/rust-lang-zh_CN/clippy_demo)
error: approximate value of `f{32, 64}::consts::PI` found
--> src/main.rs:2:13
|
2 | let x = 3.1415;
| ^^^^^^
|
= help: consider using the constant directly
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#approx_constant
= note: `#[deny(clippy::approx_constant)]` on by default
error: could not compile `clippy_demo` due to previous error
```
此报错让咱们明白Rust 已经定义了一个更精确的 `PI` 常量,且当咱们使用这个常量时,咱们的程序将更为正确。那么咱们随后就应修改咱们代码为使用这个 `PI` 常量。下面的代码就捕获导致 Clippy 的任何错误或告警:
文件名:`src/main.rs`
```rust
fn main() {
let x = std::f64::consts::PI;
let r = 8.0;
println!("圆的面积为 {}", x * r * r);
}
```
有关 Clippy 的更多信息,请参阅 [其文档](https://github.com/rust-lang/rust-clippy)。
## 用到 `rust-analyzer` 的 IDE 集成
**IDE Integration Using `rust-analyzer`**
为帮助 IDE 集成Rust 社区建议使用 [`rust-analyzer`](https://rust-analyzer.github.io/)。此工具是一套以编译器为中心,操 [语言服务器协议Language Server Protocol](http://langserver.org/) 的实用工具;而所谓语言服务器协议,则是用于各种 IDEs 和编程语言,二者相互之间通信的一种规格。有多种不同客户端可使用 `rust-analyzer`,比如 [Visual Studio Code 的 Rust 分析器插件](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)。
请访问 `rust-analyzer` 项目 [主页](https://rust-analyzer.github.io/),了解其安全说明,随后在咱们的特定 IDE 中安装该语言的服务器支持。咱们的 IDE 就能获得诸如自动补全、跳至定义及行内报错等能力。
End

32
src/appendix/editions.md Normal file
View File

@@ -0,0 +1,32 @@
# 附录 E关于版本
**Appendix E - Editions**
在第一章中,咱们曾看到 `cargo new` 会把一点有关某个版的元数据,添加到咱们的 `Cargo.toml` 文件。此附录就会讲到那意味着什么!
Rust 语言及编译器有着六周的发布周期意味着用户会得到源源不断的新功能。其他编程语言会不经常地发布较大变更Rust 则会更频繁发布较小的更新。不久之后,全部这些小修改就会堆积起来。不过这一个个发布中,回头看看而讲到,“噢,从版本 1.10 到 1.31Rust 改变了很多!”。则是不容易的。
每两三年Rust 团队都会产生一个新的 Rust *版本edition*。每个版本都会以完全更新的文档与工具,将那些业已落地到一个明确包中的特性放到一起。新版本会作为寻常的六周发布过程而交付。
这些版本服务了不同人群的不同目的:
- 对于活跃的 Rust 用户,新版本会把那些增量变更,一起放入到一个易于掌握的包中;
- 对于那些非用户,新版本释放了一些已落地的大进展信号,这会让 Rust 或许值得再看一看;
- 对于开发 Rust 的人们,新版本会提供这个项目作为整体的集结点。
在本书编写时,已有三个 Rust 版本可用Rust 2015、Rust 2018 与 Rust 2021。本书是用 Rust 2021 版本的习惯用语编写的。
`Cargo.toml` 中的 `edition`表示应对咱们的代码使用哪个版本的编译器。若该键不存在Rust 就会以向后兼容原因,而使用 `2015` 作为版本值。
每个项目都可以选择一个不同于默认 2015 的版本。这些版本可能包含了不兼容的变更,比如包含了与代码中标识符冲突的新关键字。但是,除非咱们选到这些变更,那么即使咱们更新了所使用的 Rust 编译器,咱们的代码将继续编译。
全部 Rust 编译器版本,都会支持先于那个编译器发布而存在的任何版本,且他们可将任何受支持版本的代码箱连接起来。版本变更只会影响编译器于编译初期解析代码的方式。因此,当咱们正使用着 Rust 2015而咱们的一项依赖使用了 Rust 2018 时,咱们的项目将编译,并能够使用那项依赖。与之相反,在咱们的项目使用 Rust 2018而一项依赖使用了 Rust 2015 的情形下,也会工作。
要明确的是:绝大多数特性,在所有版本上都将可用。使用任何 Rust 版本的开发者,都将在新的稳定发布构造出来时,发现一些改进。但是,在一些情况下,主要是在新曾了关键字时,一些新特性就会只在稍后版本中可用了。若咱们打算利用上这些新特性,咱们将需要切换版本。
有关更多细节,[版本指南Edition Guide](https://doc.rust-lang.org/stable/edition-guide/) 是本列举了不同版本间差异,并解释了怎样通过 `cargo fix`,而自动将咱们的代码更新到新版的一本完整的书。
End

123
src/appendix/keywords.md Normal file
View File

@@ -0,0 +1,123 @@
# 附录 A关键字
以下清单包含了 Rust 语言当前或今后要用到的一些关键字。由此,他们便不能被用作标识符(除在 [“原始标识符”](#原始标识符) 小节中咱们将讨论的那些外)了。所谓标识符,是函数、变量、参数、结构体字段、模组、代码箱、常量、宏、静态值、属性、类型、特质或生命周期等的名字。
## 当前在用的关键字
**Keywords Currently in Use**
下面是当前在用关键字的清单,带有其作用描述。
- `as` - 执行原生强制转换primitive casting消除包含着某个项目的特定特质歧义disambiguate the specific trait containing a item或重命名 `use` 语句中的项目;
- `async` - 返回一个 `Future` 类型值,而非阻塞当前线程;
- `await` - 在某个 `Future` 值的结果准备好前,暂停程序执行;
- `break` - 立即退出某个循环;
- `const` - 定义出常量项目或常量原始指针;
- `continue` - 继续下一循环迭代;
- `crate` - 在模组路径中,指向代码箱根;
- `dyn` - 动态调遣到某个特质对象,参考 [特质对象执行动态调遣](Ch17_Object_Oriented_Programming_Features_of_Rust.md#特质对象执行动态调遣);
- `else` - `if` 的回退,及 `if let` 控制流的构件;
- `extern` - 链接外部函数或变量;
- `false` - 布尔值假的字面值;
- `fn` - 定义出某个函数或函数指针类型;
- `for` - 对某个迭代器的项目加以迭代、实现某个特质或指明某个更高级别的生命周期a higher-ranked lifetime;
- `if` - 基于某个条件表达式结果的分支;
- `impl` - 实现固有或特质功能implement inherent or trait functionality;
- `in` - `for` 循环语法的一部分;
- `let` - 绑定某个变量;
- `loop` - 无条件地循环;
- `match` - 将某个值与模式匹配;
- `mod` - 定义出模组;
- `move` - 领导闭包取得其所有捕获值的所有权;
- `mut` - 注解出引用、原始指针或模式绑定等中的可变性;
- `pub` - 注解出结构体、`impl` 代码块或模组等中的公开可见性;
- `ref` - 按引用绑定;
- `return` - 自函数返回值;
- `Self` - 咱们正定义或实现中类型的类型别名;
- `self` - 方法主体method subject或当前模组
- `static` - 在整个程序执行过程持续有效的全局变量或生命周期;
- `struct` - 定义出某个结构体;
- `super` - 当前模组的父模组;
- `trait` - 定义出某个特质;
- `true` - 布尔值真的字面值;
- `type` - 定义出某个类型别名或关联类型;
- `union` - 定义出某个 [联合体](https://doc.rust-lang.org/reference/items/unions.html),是在联合体声明时用到的唯一关键字;
- `unsafe` - 注解非安全代码、函数、特质或一些实现;
- `use` - 将符号带入到作用域;
- `where` - 注解约束某个类型的子句;
- `while` - 基于某个表达式结果而有条件的循环。
## 为今后使用保留的关键字
**Keywords Reserved for Future Use**
以下关键字尚无任何功能,但被 Rust 为今后的潜在使用而保留。
- `abstract`
- `become`
- `box`
- `do`
- `final`
- `macro`
- `override`
- `priv`
- `try`
- `typeof`
- `unsized`
- `virtual`
- `yield`
## 原始标识符
**Raw Identifiers**
*原始标识符raw identifiers* 属于允许实现使用一般不被允许关键字的语法。是通过在关键字前加上前缀 `r#`,使用原始标识符的。
比如,`match` 是个关键字。在咱们尝试编译下面这个使用 `match` 作其名字的函数时:
文件名:`src/main.rs`
```rust
fn match(needle: &str, haystack: &str) -> bool {
haystack.contains(needle)
}
```
咱们将得到这样的报错:
```console
error: expected identifier, found keyword `match`
--> src/main.rs:1:4
|
1 | fn match(needle: &str, haystack: &str) -> bool {
| ^^^^^ expected identifier, found keyword
```
该报错显示咱们无法将关键字 `match` 用作函数标识符。要将 `match` 用作函数名字,咱们就需要使用原始标识符语法,像下面这样:
文件名:`src/main.rs`
```rust
fn r#match(needle: &str, haystack: &str) -> bool {
haystack.contains(needle)
}
fn main() {
assert! (r#match("foo", "foobar"));
}
```
此代码将不带任何错误地编译。请注意那个函数的定义中,与 `main` 中该函数被调用处其名字上的 `r#` 前缀。
原始标识符实现了将任何咱们所选的词语用作标识符,即使那个词语碰巧是个保留的关键字。这给到咱们更自由地选择标识符名字,以及实现与一些以其中这些词语不属于关键字的语言,所编写的程序集成。此外,原始标识符实现了,对那些以不同于咱们代码箱 Rust 版本编写库加以运用。比如,在 2015 版中 `try` 就不是个关键字,但在 2018 版本中却是。若咱们依赖于一个使用 2015 版本编写的库,而该库有一个 `try` 函数,那么咱们就将需要在这种情况下,使用原始标识符 `r#try`,来从咱们的 2018 版本的代码,调用那个函数。请参阅 [附录 E](#appendix-e) 了解更多有关版本的信息。
End

48
src/appendix/notes.md Normal file
View File

@@ -0,0 +1,48 @@
# 附录 H - 有用笔记
此处记录学习及应用 Rust 编程软件过程中,觉得有用的一些东西。
## `cargo-binutils`
[这个项目](https://github.com/rust-embedded/cargo-binutils) 是 Embbeded-Rust 项目的,而不是 Rust 官方的,但提供了有用的功能。比如查看构建出的二进制程序文件的那些头部:
```console
$ cargo readobj --bin clippy_demo -- --file-headers
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x86D0
Start of program headers: 64 (bytes into file)
Start of section headers: 4305200 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 12
Size of section headers: 64 (bytes)
Number of section headers: 42
Section header string table index: 41
```
使用前需要进行如下安装:
```console
$ cargo install cargo-binutils
$ rustup component add llvm-tools-preview
```
End

View File

@@ -0,0 +1,207 @@
# 附录 B运算符与符号
此附录包含了 Rust 语法的词汇表,包括运算符及别的一些,自己单独出现或出现于路径、泛型、特质边界、宏、属性、注释、元组及方括符等上下文中的符号。
## 运算符
**Operators**
表 B-1 包含了 Rust 中的符号、该符号将如何出现于上下文中的一个示例、简单的解释,以及该运算符是否可过载。若某个运算符可以过载,就会列出过载那个运算符要用到的相关特质。
**<small>表 B-1运算符</small>**
| 运算符 | 示例 | 说明 | 是否可以过载 |
| :--- | :--- | :--- | :--- |
| `!` | `ident! (...)` <br /> `ident! {...}` <br /> `ident! [...]` | 宏扩展 | |
| `!` | `!expr` | 按位或逻辑求补运算 | 否 |
| `!=` | `expr != expr` | 不等比较 | `PartialEq` |
| `%` | `expr % expr` | 算术求余运算 | `Rem` |
| `%=` | `var %= expr` | 算术求余并赋值 | `RemAssign` |
| `&` | `&expr`, `&mut expr` | 借用 | |
| `&` | `&type`, `&mut type`, `&'a type`, `&'a mut type` | 借用指针类型 | |
| `&` | `expr & expr` | 按位与AND运算 | `BitAnd` |
| `&=` | `var &= expr` | 按位与AND运算并赋值 | `BitAndAssign` |
| `&&` | `expr && expr` | 短路逻辑与AND运算short-circuit logical AND | |
| `*` | `expr * expr` | 算术乘法运算 | `Mul` |
| `*=` | `var *= expr` | 算术乘法运算并赋值 | `MulAssign` |
| `*` | `*expr` | 解引用运算 | `Deref` |
| `*` | `*const type`, `*mut type` | 原始指针运算 | |
| `+` | `trait + trait`, `'a + trait` | 复合类型约束运算 | |
| `+` | `expr + expr` | 算术加法运算 | `Add` |
| `+=` | `var += expr` | 算术加法运算并赋值 | `AddAssign` |
| `,` | `expr, expr` | 参数与元素分隔符 | |
| `-` | `- expr` | 算术取反运算 | `Neg` |
| `-` | `expr - expr` | 算术减法运算 | `Sub` |
| `-=` | `var -= expr` | 算术减法运算并赋值 | `SubAssign` |
| `->` | `fn(...) -> type`, <code>&vert;...&vert; -> type</code> | 函数与闭包的返回值类型 | |
| `.` | `expr.ident` | 成员访问 | |
| `..` | `..`, `expr..`, `..expr`, `expr..expr` | 排除右侧的范围语法字面值 | `PartialOrd` |
| `..=` | `..=expr`, `expr..=expr` | 包含右侧范围语法字面值 | `PartialOrd` |
| `..` | `..expr` | 结构体更新语法 | |
| `..` | `variant(x, ..)`, `struct_type { x, .. }` | “等等” 模式绑定,"And the rest" pattern binding | |
| `...` | `expr...expr` | (已弃用,请使用 `..=` 代替)在模式中:包含式范围模式 | |
| `/` | `expr / expr` | 算术除法运算 | `Div` |
| `/=` | `var /= expr` | 算术除法并赋值 | `DivAssign` |
| `:` | `pat: type`, `ident: type` | 约束 | |
| `:` | `ident: expr` | 结构体字段初始化 | |
| `:` | `'a: loop {...}` | 循环标签 | |
| `;` | `expr;` | 语句及项目的终止符 | |
| `;` | `[..., len]` | 固定大小数组语法的一部分 | |
| `<<` | `expr << expr` | 向左移位运算 | `Shl` |
| `<<=` | `var <<= expr` | 向左移位运算并赋值 | `ShlAssign` |
| `<` | `expr < expr` | 小于比较 | `PartialOrd` |
| `<=` | `expr <= expr` | 小于等于比较 | `PartialOrd` |
| `=` | `var = expr`, `ident = type` | 赋值/等价equivalence | |
| `==` | `expr == expr` | 相等比较 | `PartialEq` |
| `=>` | `pat => expr` | 匹配支臂语法的一部分 | |
| `>` | `expr > expr` | 大于比较 | `PartialOrd` |
| `>=` | `expr >= expr` | 大于等于比较 | `PartialOrd` |
| `>>` | `expr >> expr` | 向右位移运算 | `Shr` |
| `>>=` | `var >>= expr` | 向右位移运算并赋值 | `ShrAssign` |
| `@` | `ident @ pat` | 模式绑定 | |
| `^` | `var ^ expr` | 按位异或运算 | `BitXor` |
| `^=` | `var ^= expr` | 按位异或运算并赋值 | `BitXorAssign` |
| <code>&vert;</code> | <code>pat &vert; pat</code> | 模式选择pattern alternatives | |
| <code>&vert;</code> | <code>expr &vert; expr</code> | 按位或OR运算 | `BitOr` |
| <code>&vert;=</code> | <code>var &vert;= expr</code> | 按位或OR运算并赋值 | `BitOrAssign` |
| <code>&vert;&vert;</code> | <code>expr &vert;&vert; expr</code> | 短路逻辑或运算Short-circuiting logical OR | |
| `?` | `expr?` | 错误传递 | |
## 非运算符的符号
**Non-operator Symbols**
以下清单包含了不以运算符发挥作用的全部符号;那就是说,他们不会表现得像函数或方法调用。
表 B-2 给出了自己单独出现,并在多种场合有效的一些符号。
**<small>表 B-2独立语法Stand-Alone Syntax</small>**
| 符号 | 说明 |
| :--- | :--- |
| `'ident` | 命名的生命周期或循环标签 |
| `...u8`, `...i32`, `...f64`, `...usize` 等等 | 指定类型的数字字面值 |
| `"..."` | 字符串字面值 |
| `r"..."`, `r#"..."#`, `r##"..."##` 等等 | 原始字符串字面值,其中的转义字符不会被处理 |
| `b"..."` | 字节字符串字面值;构造出一个字节数组而非字符串 |
| `br"..."`, `br#"..."`, `br##"..."##` 等等 | 原始字节字符串字面值,是原始与字节字符串字面值的结合 |
| `'...'` | 字符字面值 |
| `b'...'` | ASCII 字节字面值 |
| <code>&vert;...&vert; expr</code> | 闭包 |
| `!` | 发散函数下总是空的底部类型always empty bottom type for diverging functions |
| `_` | “忽略ignored” 模式绑定还用于令到整数字面值可读also used to make integer literals readable |
表 B-3 展示了出现在模组层次结构中到某个项目路径上下文中的一些符号。
**<small>表 B-3路径相关的语法</small>**
| 符号 | 说明 |
| :--- | :--- |
| `ident::ident` | 命名空间路径 |
| `::path` | 相对于代码箱根的路径(比如,某个显式绝对路径) |
| `self::path` | 相对于当前模组的路径(比如,某个显式相对路径) |
| `super::path` | 相对于当前模组父模组的路径 |
| `type::ident`, `<type as trait>::ident` | 关联的常量、函数及类型 |
| `<type>::...` | 无法直接命名的某个类型的关联项目(比如,`<&T>::...`, `<[T]>::...` 等等) |
| `trait::method(...)` | 通过命名出定义方法的类型,消除该方法调用的歧义 |
| `<type as trait>::method(...)` | 通过命名出特质与类型,消除方法调用的歧义 |
表 B-4 展示了出现在运用泛型参数上下文中的一些符号。
**<small>表 B-4泛型</small>**
| 符号 | 说明 |
| :-- | :-- |
| `path<...>` | 指明类型中的泛型参数(比如,`Vec<u8>` |
| `path::<...>`, `method::<...>` | 指明表达式中泛型、函数或方法的参数通常这被称作涡轮鱼语法turbofish比如`"42".parse::<i32>()`,关于 Rust 的 turbofish 语法,请参考:[What is Rust's turbofish](https://techblog.tonsser.com/posts/what-is-rusts-turbofish)[RUST 中的 turbofish 语法(一)](https://www.jianshu.com/p/9107685ece03) ... |
| `fn ident<...> ...` | 定义出泛型函数 |
| `struct ident<...> ...` | 定义出泛型结构体 |
| `enum ident<...> ...` | 定义出泛型枚举 |
| `impl<...> ...` | 定义出泛型实现 |
| `for<...> type` | 高阶声明周期边界higher-ranked lifetime bounds |
| `type<ident=type>` | 其中一个或更多的关联类型有着指定赋值的某种泛型a generic type where one or more associated types have specific assignments比如`Iterator<Item=T>` |
下表 B-5 展示了出现在使用特质边界的约束性泛型参数上下文中的一些符号table B-5 shows symbols that appear in the context of constraining generic type parameters with trait bounds。
**<small>B-5特质边界约束Trait Bound Constrains</small>**
| 符号 | 说明 |
| :--- | :--- |
| `T: U` | 泛型参数 `T` 受实现了 `U` 的类型约束 |
| `T: 'a` | 泛型 `T` 必须要比生命周期 `'a` 活得更久generic type `T` must outlive lifetime `'a`(意思是该类型不能间接地包含任何生命周期短于 `'a` 的引用) |
| `T: 'static` | 泛型 `T` 不包含除 `'static` 的引用外的其他引用 |
| `'b: 'a` | 泛型生命周期 `'b` 必须要比 `'a` 存活得更久 |
| `T: ?Sized` | 允许泛型参数为动态大小类型 |
| `'a + trait`, `trait + trait` | 复合的类型约束 |
下表 B-6 展示了出现在宏调用或定义上下文中,并指明了某个项目上属性的一些符号。
**<small>B-6宏与属性</small>**
| 符号 | 说明 |
| :--- | :--- |
| `#[meta]` | 外层属性 |
| `#![meta]` | 内层熟悉 |
| `$ident` | 宏代换macro substitution |
| `$ident:kind` | 宏捕获 |
| `$(...) ...` | 宏重复macro repetition |
| `ident! (...)`, `ident! {...}`, `ident! [...]` | 宏调用macro invocation |
下表 B-7 展示了创建注释的一些符号。
**<small>表 B-7注释</small>**
| 符号 | 说明 |
| :--- | :--- |
| `//` | 注释行 |
| `//!` | 内层行文档注释inner line doc comment |
| `///` | 外层行文档注释outter line doc comment |
| `/*...*/` | 注释块 |
| `/*!...*/` | 内层块文档注释inner block doc comment |
| `/**...*/` | 外层块文档注释outter block doc comment |
下表 B-8 展示了出现于用到元组上下文中的一些符号。
**<small>元组</small>**
| 符号 | 说明 |
| :--- | :--- |
| `()` | 空元组(又叫单元值),同时属于字面值与类型 |
| `(expr)` | 元括号括起来的表达式parenthesized expression |
| `(expr,)` | 单一元素的元组表达式 |
| `(type,)` | 单一元素的元组类型single-element tuple type |
| `(expr, ...)` | 元组表达式 |
| `(type, ...)` | 元组类型tuple type |
| `expr(expr, ...)` | 函数调用表达式;还用于初始化一些元组的 `struct` 以及元组的 `enum` 变种function call expression; also used to initialize tuple `struct`s and tuple `enum` vairants |
| `expr.0`, `expr.1` 等等 | 对元组进行索引 |
下表 B-9 展示了其中用到花括号上下文中的一些符号。
**<small>表 B-9花括号</small>**
| 符号 | 说明 |
| :--- | :--- |
| `{...}` | 代码块表达式 |
| `Type {...}` | `struct` 的字面值 |
下表 B-10 展示了其中用到方括号上下文中的一些符号。
**<small>表 B-10方括号</small>**
| 符号 | 说明 |
| :--- | :--- |
| `[...]` | 数组的字面值 |
| `[expr; len]` | 包含着 `expr``len` 拷贝数组的字面值 |
| `[type; len]` | 包含着 `len``type` 的实例数组的字面值 |
| `expr[expr]` | 对集合进行索引collection indexing。是可过载的 `(Index, IndexMut)`overloadable `(Index, IndexMut)` |
| `expr[..]`, `expr[a..]`, `expr[..b]`, `expr[a..b]` | 用到了 `Range``RangeFrom``RangeTo``RangeFull` 作为 “索引”的带有集合切片集合索引collection indexing pretending to be collection slicing, using `Range`, `RangeFrom`, `RangeTo`, or `RangeFull` as the "index" |
End

146
src/appendix/releases.md Normal file
View File

@@ -0,0 +1,146 @@
# 附录 G - Rust 是怎样构造出来的与每日发布
**How Rust is Made and "Nightly Rust"**
此附录是有关 Rust 被怎样构造出来,及那会怎样作为一名 Rust 开发者的你。
## 强调稳定却并无止步不前
**Stability Without Stagnation**
作为一门语言Rust 在注重咱们代码稳定性方面 *用心良苦*。咱们希望 Rust 成为你可以在其上构建软件的稳固基础,而若那些物件都一直变动,那将是不可能实现的。而与此同时,若咱们无法实验一些新特性,那么直到这些特性发布后咱们不能在修改一些东西时,咱们也不会发现一些重大缺陷。
对于这个问题咱们Rust 团队)的解决方案就是咱们称作 “强调稳定又不要止步不前”而咱们的直到原则是这样的你永不必害怕升级到稳定的Rust 新版本。每次升级都应是无痛的,而又应带给你一些新特性、更少的程序错误,以及更快的编译时间。
## 啾,啾!发布通道与搭上快车
**Choo, Choo! Release Channels and Riding the Trains**
Rust 的开发,是运作在 *火车时刻表train schedule* 上的。那就是说,全部开发都是在 Rust 代码仓库的 `master` 分支上完成的。各个发布遵循了软件发布列车模型a software release train model该发布模型业已为 Cisco IOS 及其他软件项目所使用。Rust 有着以下三个 *发布通道release channels*
- 每日发布nightly
- Beta 发布beta
- 稳定发布stable
多数 Rust 开发者主要使用稳定通道,而那些希望尝试实验性新特性的人们,则会使用每日发布或 beta 通道。
下面是个开发与发布流程运作方式的一个示例:咱们来假定 Rust 团队正工作于 Rust 1.5 的发布上。那个发布发生于 2015 年 11 月,但其将提供到我们实际版本数字。有个新特性被添加到 Rust一次新提交落在了 `master` 分支。每天晚上,都有一个新的 Rust 每日版本被产生出来。每天都是个发布日,而这些发布是由咱们的发布基础设施自动创建的。因此随着时间流逝,咱们的发布看起来就像下面这样,每晚一次:
```text
nightly: * - - * - - *
```
每隔六周便是要准备一个新发布的时候了Rust 代码仓库的 `beta` 分支,便会从由每日发布所使用的 `master` 分支分叉开来。现在,就有了两个分支:
```text
nightly: * - - * - - *
|
beta: *
```
多数 Rust 使用者不会积极使用这些 beta 发布,但会在他们的 CI 系统中就 beta 发布加以测试,以帮助 Rust 发现可能出现的倒退。与此同时,仍有着每晚的每日发布:
```text
nightly: * - - * - - * - - * - - *
|
beta: *
```
在首个 beta 版创建出来六周后,就是稳定发布的时候了!`stable` 分支就被从 `beta` 分支创建出来:
```text
nightly: * - - * - - * - - * - - * - - * - * - *
|
beta: * - - - - - - - - *
|
stable: *
```
Rust 1.5 便完成了!不过,咱们忘了一件事:由于这六个星期以及过去,而咱们还需要 Rust *下一* 版本1.6,的一个新的 beta 发布。因此在 `stale` 分支从 `beta` 分支分叉出来后,下一版本的 `beta` 又会从 `nightly` 再度分叉出来:
```text
nightly: * - - * - - * - - * - - * - - * - * - *
| |
beta: * - - - - - - - - * *
|
stable: *
```
每六周就有一个发布 “离站”,但发布过程仍务必要在其抵达稳定发布前,经由这个 beta 通道行驶一段路程,由此这个过程便被称为 “列车模型”。
Rust 每六周发布,像时刻表一样。若咱们知道了一个 Rust 发布的日期,那么就能直到下一发布的日期:那便是六周后。每六周安排一次发布的一个好处,便是下一班列车很快就会到来。若某项特性刚好错过了某个特定发布,那么无需担心:另一发布将在不久后发生!这有助于减少在临近发布截止日期时,有可能未完善的功能偷偷潜入的压力。
归功于这个流程咱们可以始终检出check out下一构建的 Rust并自己验证到升级是容易的若 beta 发布没有如预期那样工作,咱们就可以将其报告给 Rust 团队并在下一稳定发布发生前修好他beta 发布中的损坏相对较少,但 `rustc` 仍属于一个软件,而确实存在一些错误。
## 不稳定特性
**Unstable Features**
这种发布模型下还有一个好处不稳定特性。Rust 使用了一种名为 “特性标识feature flags” 的技巧,来确定出给定发布中启用了哪些特性。若某项新特性处于活跃开发中,他就会落地在 `master` 分支上,而由此就会在每日发布中,但会有着一个 *特性标识*。而咱们作为用户希望尝试这个进展中的特性the work-in-progress feature咱们是可以尝试的但必须使用 Rust 的每日发布,并使用恰当的标识来注解咱们的代码,来选用该特性。
若咱们使用着 beta 或稳定发布的 Rust那么就不能使用任何特性标识。这是 Rust 团队在声明那些新特性永久稳定前,允许咱们实际用到他们的关键。希望选用最新特性的人们,便可这样做,而想要一种扎实体验的人,则可坚持使用稳定发布,而清楚他们的代码不会破坏。这便是稳定但并非止步不前。
由于那些工作中的特性仍在便会,且在本书写作时和他们在稳定构建中启用时,其间他们肯定将有所不同,因此本书只包含了那些稳定特性的信息。咱们可以在线上找到那些仅每日发布有的特性文档。
## Rustup 与 Rust 每日发布所扮演的角色
**Rustup and the Role of Rust Nightly**
Rust 令到易于在全局或每个项目基础上,从不同发布通道的 Rust 之间改变。默认情况下,咱们将安装稳定发布的 Rust。而比如要安装每日发布
```console
$ rustup toolchain install nightly
```
咱们也可以使用 `rustup`,查看全部的 *工具链toolchains* Rust 的各个发布与关联组件)。下面就是本书一位作者的 Windows 计算机上的示例:
```powershell
> rustup toolchain list
stable-x86_64-pc-windows-msvc (default)
beta-x86_64-pc-windows-msvc
nightly-x86_64-pc-windows-msvc
```
> 在 Linux 系统上的输出如下:
```console
$ rustup toolchain list
stable-x86_64-unknown-linux-gnu (default)
```
可以看到,稳定发布的工具链是默认的。绝大多数 Rust 用户会在多数时候使用稳定发布。咱们可能想要在多数时候使用稳定发布,又因为咱们关心某项最新特性,而会在特定项目使用每日发布。要这样做,就可以在那个项目目录下,使用 `rustup override` 来将每日发布工具链,设置为当咱们位处那个目录中时,`rustup` 使用的那个工具链:
```console
$ cd ~/projects/needs-nightly
$ rustup override set nightly
```
现在,当咱们每次在 `~/projects/needs-nightly` 目录下调用 `rustc``cargo` 时,`rustup` 都会确保咱们在使用每日发布的 Rust而非咱们默认的稳定发布 Rust 了。再有很多 Rust 项目时,这就会排上用场!
## 请求评议流程与各种团队
**The RFC Process and Teams**
那么咱们该怎么了解到这些新特性呢Rust 的开发模型,遵循了 *请求评议流程Request For Comments(RFC) process*。如你想要 Rust 的一项改进那么就可以编写一个名为请求评议RFC 的提议。
人人都可以编写请求评议来改进 Rust同时这些提议会经过由许多议题子团队所组成的 Rust 团队审阅和讨论。[在 Rust 网站上](https://www.rust-lang.org/governance) 有这些团队的完整清单,其中包括了该项目各领域:语言设计、编译器实现、基础设施、文档及其他等的团队。恰当的团队会阅读提议与评论,撰写出他们自己的一些评论,并在最后,便有了接受或拒绝该特性的共识。
若该特性被接受了,就会在 Rust 代码仓库上开出一个 issue同时某个人就可以实现他。将其实现得非常棒的那个人可能不是最早提议这项特性的那人在实现准备好时其就会落地于 `master` 分支的特性门a feature gate之后如同咱们曾在 [“不稳定特性”](#不稳定特性) 小节中曾讨论过的那样。
过了一段时间后,一旦那些用到每日发布的 Rust 开发者们,能够试用这项新特性,那么 Rust 团队成员将讨论这项特性,怎样将其编制到每日发布上,并决定其是否有那个被构造到稳定发布 Rust。而若决定是继续推进那么特性门就会被移除同时这项特性就被认为是稳定的了他就会搭上列车进到一个新的稳定发布 Rust 中。
End

View File

@@ -0,0 +1,169 @@
# 附录 I - 术语清单
- 命令行界面
Command-Line Interface在终端里运行的应用与在 GUI 窗口中应用不同。
- 模组系统
The module system大型程序中组织代码的方式。
- 迁移所有权
在闭包参数清单前,使用 `move` 关键字,让闭包取得其用到的所在环境中的值所有权。
- 关联类型
Associated type, 是通过 `type` 关键字定义在特质下的类型。咱们知道方法即为关联函数associated function那么关联类型自然与关联函数有些类似。
- 消费适配器
Consuming adaptor, `Iterator` 特质上,会调用到迭代器 `next` 方法的一些方法,由于这些方法会耗尽迭代器,故他们被称为消费适配器。
- 迭代器适配器
Iterator adaptor`Iterator` 特质上,通过改变原迭代器某些方面而产生出另一迭代器的一些方法。
- 零成本抽象
Zero-cost abstractions相较于一般实现语言提供的高级抽象在编译后生成的代码与自己努力编写出的优化低级别代码类似。故使用高级抽象是没有运行时开销的。
- 展开优化
UnrollingRust 编译器在编译迭代器代码时,会把已知的历次迭代展开为重复代码,而实现性能优化。
- 文档注释
Documentation comment, 将产生出 HTML 的注释。
- 重导出程序项目
Re-export, 使用 `pub use` 重新导出程序项目。
- 语义版本控制规则
Semantic Versioning rules, 又大版本、小版本及补丁版本构成的,形如 `MAJOR.MINOR.PATCH` 的版本编号规则。参考:[semver.org](https://semver.org)。
- 工作区
Workspace为有着多个库代码箱的大型项目组织的一项 Cargo 特性。
- 编译出的物件
The compiled artifacts
- 路径依赖
A path dependency
- 匣子类型(数据结构)
`Box<T>`,由存储在栈上的指针,与存储在堆上的数据,实现的一种数据结构。
- 间接
Indirection, 匣子类型的变量,通过保存指向数据在内存堆上的地址,而间接保存了数据。
- 解引用强制转换
Deref coercion类似于其他语言的开箱操作。
- 元组结构体
A tuple struct, 形式为 `struct MyBox<T>(T)`,是保持着只有一个元素元组的结构体,`Box<T>` 的数据结构为元组结构体。
- 前奏
The Rust Prelude, `std::prelude` 模组。前奏是 Rust 自动导入到每个 Rust 程序中的东西的列表。他被保持在尽可能小的范围内,并且专注于几乎每个 Rust 程序都会用到的东西,特别是特质。参见:[`std::prelude`](https://doc.rust-lang.org/std/prelude/index.html)。
- 内部可变性模式
The interior mutability pattern, Rust 的一种设计模式,用于改变不可变值内部的某个值。
- 内存泄漏
Memory leak, 出现未清理内存的情况。
- 关联类型
An associated type, 通过 `type Target = t;` 这种语法声明出的类型,是声明泛型参数的一种稍微不同的方式。
- 单态化
所谓 *单态化monomorphization*,是指即通过把在编译后用到的具体类型填入到泛型位置,而将通用代码转换为具体代码的过程。参考 [使用泛型代码的性能问题](Ch10_Generic_Types_Traits_and_Lifetimes.md#使用泛型参数代码的性能问题)。
- 内聚属性
a property called *coherence*,参见 [在类型上实现某个特质](Ch10_Generic_Types_Traits_and_Lifetimes.md#在类型上实现某个特质)。
- 孤儿规则
the orphan rule, 参见 [在类型上实现某个特质](Ch10_Generic_Types_Traits_and_Lifetimes.md#在类型上实现某个特质)。
- `impl Trait` 语法
`impl Trait` syntax, 在函数参数清单中,将特质用作参数类型注解的语法。参见:[作为参数的特质](Ch10_Generic_Types_Traits_and_Lifetimes.md#作为参数的特质)
- 特质边界语法
Trait bound syntax, 参见 [特质边界语法](Ch10_Generic_Types_Traits_and_Lifetimes.md#特质边界语法)
- 语法糖
Sugar syntax, 参见 [特质边界语法](Ch10_Generic_Types_Traits_and_Lifetimes.md#特质边界语法)
- 指明多个特质边界的 `+` 语法
The `+` syntax for specifying multiple trait bounds, 参见:[使用 + 语法,指定多个特质边界](Ch10_Generic_Types_Traits_and_Lifetimes.md#使用--语法指定多个特质边界)
- `where` 子句
`where` clauses, 参见 []()
- 生命周期省略规则
Lifetime elision rules, 编程到 Rust 引用分析中的一些确定性模式。
- 输入生命周期
Input lifetimes函数或方法上的生命周期
- 输出生命周期
Output lifetimes, 返回值上的生命周期
End

View File

@@ -0,0 +1,9 @@
# 附录 F - 本书的一些译本
<略>
End

121
src/async/all_together.md Normal file
View File

@@ -0,0 +1,121 @@
# 放在一起:未来值、任务与线程
正如我们在 [第 16 章](../Ch16_Fearless_Concurrency.md) 中所看到的,线程提供了一种并发的方法。我们在本章中看到了另一种方法:使用未来值与流的异步。如果咱们想要知道,何时该选择另一种方法,答案是:视情况而定!在很多情况下,我们需要选择的不是线程 ** 异步,而是线程 ** 异步。
数十年来,许多操作系统都提供了基于线程的并发模型,而许多编程语言也因此支持这些模型。不过,这些模型也并非没有代价。在许多操作系统上,每个线程都会占用相当多的内存,而且启动和关闭线程都会产生一些开销。也只有在操作系统和硬件支持的情况下,线程才可用。与主流台式机和便携电脑不同,一些嵌入式系统根本没有操作系统,因此他们也没有线程。
异步模型提供了一套不同的权衡机制,而成为一种终极补充。在异步模型中,并发操作不需要其各自的线程。相反,他们可运行于任务之上,就像我们在流小节中,使用 `trpl::spawn_task` 启动某个同步函数的工作一样。任务类似于线程,但他不是由操作系统管理,而是由库级别的代码(即运行时)管理。
在上一小节中,我们看到了可通过使用一个异步通道,及生成一个我们可从同步代码中调用的异步任务,而构建出一个流。我们也可使用线程,完成这完全一样的事情。在下面清单 17-40 中,我们使用标准库中的 `trpl::spawn_task``trpl::sleep` 两个 APIs替换了 `get_intervals` 中的异步通道与异步任务。
文件名:`src/main.rs`
```rust
fn get_intervals() -> impl Stream<Item = u32> {
let (tx, rx) = trpl::channel();
// This is *not* `trpl::spawn` but `std::thread::spawn`!
thread::spawn(move || {
let mut count = 0;
loop {
// Likewise, this is *not* `trpl::sleep` but `std::thread::sleep`!
thread::sleep(Duration::from_millis(1));
count += 1;
if let Err(send_error) = tx.send(count) {
eprintln!("Could not send interval {count}: {send_error}");
break;
};
}
});
ReceiverStream::new(rx)
}
```
*清单 17-41在 `get_intervals` 中使用 `std::thread` 而非异步的 `trpl` APIs*
若咱们运行这段代码,输出结果会与清单 17-40 相同。请注意,从调用代码的角度来看,这里的变化微乎其微。更重要的是,尽管我们的一个函数在运行时上生成了个异步任务,而另一函数生成了个操作系统的线程,但得到的两个流,并没有受到这些差异的影响。
尽管这两种方法有相似之处,但他们的行为却大相径庭,尽管我们可能很难在这个非常简单的例子中测量出来。我们可以在任何现代个人电脑上,生成数百万个异步任务。但若我们试图用线程来做这件事,内存真的会用完!
然而,这些 API 如此相似是有原因的。线程充当了一些同步操作集的边界;线程 *之间* 可以并发。任务则充当了一些异步操作集的边界;任务 *之间* 和任务 *内部* 都可以并发,因为任务可以在其主体中的未来值之间切换。最后,未来值是 Rust 最细粒度的并发单元,每个未来值都可以代表一棵由其他未来值组成的树。运行时 -- 具体来说是运行时的执行器 -- 管理着任务,而任务管理着未来值。在这方面,任务类似于由运行时管理着的轻量级线程,同时由于是由运行时而不是操作系统管理,因此任务还是具有更多功能的轻量级线程。
这并不意味着异步任务总是要比线程更好(反之亦然)。在某些方面,相比于使用 `async` 的并发,使用线程的并发是一种更简单的编程模型。这可以是优点,也可以是缺点。线程在某种程度上是 “触发并遗忘” 的;他们没有与未来值相对应的原生对等体,因此除非被操作系统本身打断,他们运行即可完成。也就是说,线程并不像未来值那样,支持 *任务内的并发*。Rust 中的线程也没有取消机制 -- 我们在本章中没有明确涉及这一主题,但每当我们结束某个未来值时,其状态就会被正确清理,这一事实暗示了任务的取消机制。
这些限制也使得线程比期货更难于组装。例如,使用线程构建 `timeout``throttle` 方法等辅助工具,就比我们在本章前面所构建的要困难得多。正如我们所看到的,未来值是一种更丰富的数据结构,这意味着他们可以更自然地组合在一起。
因此,任务给到我们对未来值的 *额外* 控制,允许我们选择在何处以及如何对他们分组。事实证明,线程和任务往往能配合得很好,因为任务可以(至少在某些运行时下)在线程间迁移。事实上,我们一直在使用的运行时,包括 `spawn_blocking``spawn_task` 两个函数,默认情况下都是多线程的!许多运行时都使用了一种名为 *工作偷取work stealing* 的方法,根据线程当前的使用情况,在线程间透明地迁移任务,以提高系统的整体性能。这种方法实际上需要线程 ** 任务,因此也需要未来值。
在考虑何时使用哪种方法时,请考虑以下经验法则:
- 如果工作的 *并行性很强*,比如处理每个部分都可以单独处理的大量数据时,线程是更好的选择;
- 如果工作的 *并发性很高*,例如处理来自不同来源,可能以不同时间间隔或不同速度发送的消息时,那么异步是更好的选择。
如果咱们同时需要并行性和并发性,咱们就不必在线程和异步之间做出选择。咱们可自由地将他们结合在一起使用,让他们各自发挥其最擅长的部分。例如,下面清单 17-42 展示了,实际 Rust 代码中这种混合使用的一个相当常见的示例。
文件名:`src/main.rs`
```rust
use std::{thread, time::Duration};
fn main() {
let (tx, mut rx) = trpl::channel();
thread::spawn(move || {
for i in 1..11 {
tx.send(i).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
trpl::run(async {
while let Some(message) = rx.recv().await {
println!("{message}");
}
});
}
```
*清单 17-42在一个线程中以阻塞代码发送消息并在一个异步代码块中等待消息*
我们以创建一个异步通道开始,然后生成一个取得通道发送侧所有权的线程。在该线程中,我们发送数字 1 到 10每个数字之间休眠一秒钟。最后就像本章所做的那样我们运行了一个以传递给 `trpl::run` 的异步代码块创建出的未来值。在这个未来值中,我们等待这些消息,就像在我们曾看到的其他消息传递示例中一样。
回到本章开头的场景,设想使用一个专门线程运行一组视频编码任务(因为视频编码是计算密集的),而以一个异步通道,通知用户界面这些操作已完成。在真实世界用例中,这类组合的例子数不胜数。
## 本章小结
这并不是咱们在本书中最后一次看到并发。[第 21 章](../Ch20_Final_Project_Building_a_Multithreaded_Web_Server.md) 中的项目,将在比这里讨论的简单示例更现实的情况下,应用这些概念,并更直接地比较使用线程和任务解决问题的方法。
无论咱们选择这些方法的哪种Rust 都能为咱们提供编写安全、快速、并发代码所需的工具,无论是用于高吞吐量 web 服务器,还是某种嵌入式操作系统。
接下来,我们将讨论在咱们的 Rust 程序变大时,问题建模和构建解决方案的一些惯用方法。此外,我们还将讨论 Rust 的惯用语,与咱们在面向对象编程中熟悉的惯用语之间的关系。
End

306
src/async/async_traits.md Normal file
View File

@@ -0,0 +1,306 @@
# 近观异步有关的特质
在本章中,我们以各种方式用到了 `Future``Pin``Unpin``Stream``StreamExt` 等特质。不过,到目前为止,我们还没有深入探讨他们的工作原理,或他们相互配合的细节,这对咱们日常的 Rust 工作来说,在大多数情况下是没有问题的。但有时,咱们将遇到需要了解更多细节的情况。在这个小节中,我们将涉足这些细节,以便在那些情形下有所帮助,而 *真正的* 深入探讨,则留给其他文档。
## `Future` 特质
我们来先仔细看看 `Future` 这个特质是如何工作的。下面是 Rust 对他的定义:
```rust
use std::pin::Pin;
use std::task::{Context, Poll};
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
```
该特质定义包含了许多新类型,以及一些我们以前从未见过的语法,因此我们来逐一了解一下该定义。
首先,`Future` 关联的类型 `Output`,指出了该未来值会解析为什么。这类似于 `Iterator` 特质关联的类型 `Item`。其次,`Future` 还有着 `poll` 方法,该方会取一个特殊的 `Pin` 引用,作为其 `slef` 参数,以及一个到 `Context` 类型的可变引用,并返回一个 `Poll<Self::Output>`。稍后我们将进一步讨论 `Pin``Context`。现在,我们着重于该方法会返回什么,即那个 `Poll` 类型:
```rust
enum Poll<T> {
Ready(T),
Pending,
}
```
这个 `Poll` 类型类似于 `Option`。他有个有值的变种,即 `Ready(T)`,还有个没有值的变种 `Pending`。不过,`Poll` 所指的内容与 `Option` 完全不同!`Pending` 变种表示这个未来值仍有工作要做,所以调用者将需要稍后再检查。`Ready` 变种表示该未来值已完成其工作,`T` 值可用。
> **注意**:对于大多数未来值,调用者都不应在该未来值返回 `Ready` 后,再次调用 `poll`。若在就绪后再次轮询,许多未来值都将出现不会恢复错误。再次轮询安全的未来值,会在其文档中明确说明。这与 `Iterator::next` 的行事方式类似。
当咱们看到使用了 `await` 的代码时Rust 会在背后将其编译为调用 `poll` 的代码。回顾 [清单 17-4](futures.md#listing-17-4),其中我们曾打印出单个 URL 解析后的页面标题Rust 就会将其编译成类似(但不完全)下面这样的代码:
```rust
match page_title(url).poll() {
Ready(page_title) => match page_title {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
Pending => {
// But what goes here?
}
}
```
在未来值仍为 `Pending` 时,我们该怎么办?我们需要某种一次又一次尝试,直到这个未来值最终就绪的方法。换句话说,我们需要一个循环:
```rust
let mut page_title_fut = page_title(url);
loop {
match page_title_fut.poll() {
Ready(value) => match page_title {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
Pending => {
// continue
}
}
}
```
如果 Rust 将其编译成这样的代码,那么每个 `await` 都会阻塞 -- 这与我们的目标正好背道而驰相反Rust 会确保这个循环可将控制权交给某个,可暂停这个未来值上的工作,转而处理其他未来值,并在稍后再次检查这个未来值的东西。正如我们所见,这个东西就是异步运行时,而这种调度和协调工作,正是他的主要工作之一。
在本章早些时候,我们介绍了 `rx.recv` 上的等待。`recv` 调用会返回一个未来值,并等待这个未来值轮询他。我们曾注意到,直到在通道关闭,未来值以 `Some(message)``None` 就绪前,运行时都将暂停该未来值。随着我们对 `Future` 这个特质,特别是 `Future::poll` 这个方法的深入理解,我们就能明白其工作原理。当未来值返回 `Poll::Pending` 时,运行时就知道这个未来值未就绪。反之,当 `poll` 返回 `Poll::Ready(Some(message))``Poll::Ready(None)` 时,运行时就知道该未来值 ** 就绪,并将其向前推进。
运行时如何做到这点的具体细节,超出了本书的范围,但关键是要了解未来值的基本机制:运行时会 *轮询* 他所负责的每个未来值,当未来值未就绪时,运行时会让该未来值重新进入休眠状态。
## `Pin` 与 `Unpin` 特质
当我们在 [清单 17-16](./multiple_futures.md#listing-17-16) 中引入 “固定” 这个概念时,我们遇到了一个非常棘手的错误消息。下面是其中的相关部分:
```console
error[E0277]: `{async block@src/main.rs:10:23: 10:33}` cannot be unpinned
--> src/main.rs:48:33
|
48 | trpl::join_all(futures).await;
| ^^^^^ the trait `Unpin` is not implemented for `{async block@src/main.rs:10:23: 10:33}`
|
= note: consider using the `pin!` macro
consider using `Box::pin` if you need to access the pinned value outside of the current scope
= note: required for `Box<{async block@src/main.rs:10:23: 10:33}>` to implement `Future`
note: required by a bound in `futures_util::future::join_all::JoinAll`
--> file:///home/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-util-0.3.30/src/future/join_all.rs:29:8
|
27 | pub struct JoinAll<F>
| ------- required by a bound in this struct
28 | where
29 | F: Future,
| ^^^^^^ required by this bound in `JoinAll`
```
这条错误消息不仅告诉我们需要固定这些值,还告诉我们为什么需要固定。`trpl::join_all` 这个函数会返回一个名为 `JoinAll` 的结构体。该结构体是个 `F` 类型的泛型,而 `F` 类型被约束到实现 `Future` 这个特质。以 `await` 直接等待某个未来值,就会隐式地固定这个未来值。这就是我们不需要在所有我们需要等待未来值的地方,使用 `pin!` 的原因。
不过,我们并没有在这里直接等待某个未来。相反,通过向 `join_all` 函数传递一个未来值的集合,我们构建了一个新未来值,即 `JoinAll``join_all` 的函数签名,要求该集合中所有项目的类型,都要实现了 `Future` 特质,而只有当取封装的 `T` 是个实现了 `Unpin` 特质的未来值时,`Box<T>` 才实现了 `Future`
要理解的东西还真不少!为了真正理解他,我们来进一步深入了解 `Future` 这个特质的具体工作原理,尤其是与 *固定* 有关的部分。
再看看这个 `Future` 特质的定义:
```rust
use std::pin::Pin;
use std::task::{Context, Poll};
pub trait Future {
type Output;
// Required method
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
```
其中的 `cx` 参数及其 `Context`类型,是运行时在保持懒惰的同时,如何具体知道何时检查任何给定未来值的关键。同样,如何工作的细节,超出了本章的讨论范围,咱们通常只有在编写某个定制的 `Future` 实现时,才需要考虑这个问题。我们将重点关注 `self` 的类型,因为这是我们第一次看到某个方法中 `self` 有着类型注解。`self` 的类型注解,与其他函数参数的类型注解一样,但有两个关键区别:
- 他告诉 Rust要调用方法的 `self` 必须是何类型;
- 他不能是任何类型。他被限制在该方法所实现的类型、指向该类型的引用或灵巧指针,或封装了指向该类型引用的一个 `Pin`
我们将在 [第 18 章](../Ch17_Object_Oriented_Programming_Features_of_Rust.md) 详细介绍这种语法。现在,我们只需知道,在咱们打算轮询某个未来值,检查他是 `Pending` 还是 `Ready(Output)` 时,我们需要一个指向该类型的 `Pin` 封装的可变引用。
`Pin` 是个对诸如 `&``&mut``Box``Rc` 等指针类型的封装器。(技术上讲,`Pin` 可以与实现了 `Deref``DerefMut` 特质的类型一起工作,不过这实际上等同于只与指针一起工作。)`Pin` 本身并非指针,也不像带有引用计数的 `Rc``Arc` 那样,有任何其自身的行为;他纯粹是个编译器可用于强制约束指针使用的工具。
回顾 `await` 是通过到 `poll` 的调用实现的,这就可以解释我们早先看到的错误消息,但那是指 `Unpin` 而不是 `Pin`。那么 `Pin``Unpin` 到底有什么关系,为什么 `Future` 需要 `self` 位于某个 `Pin` 类型中,才能调用 `poll` 呢?
还记得在本章前面的内容中,在某个未来值中的一系列等待点,都会被编译到一个状态机中,且编译器会确保该状态机遵循 Rust 有关安全性的所有一般规则包括借用及所有权。为实现这一点Rust 会查看从一个等待点,与下一等待点或异步代码块结束处间,需要哪些数据。然后他会在编译后的状态机中,创建相应变种。无论是通过获取该数据的所有权,还是通过获取数据的可变或不可变引用,每个变种都会获得,其所需的对将在源代码该部分中用到数据的访问权。
到目前为止,一切都很好:当我们在某个给定异步代码块中的所有权或引用上有任何错误时,借用检查器就将告诉我们。当我们要绕过与该代码块对应的未来值 -- 比如将其迁移到某个 `Vec` 中,以传递给 `join_all` -- 事情就变得棘手了。
当我们迁移某个未来值 -- 无论是将其压入某个数据结构,以 `join_all` 用作迭代器,还是从函数中返回他 -- 这实际上意味着迁移 Rust 为我们创建的那个状态机。与 Rust 中的大多数其他类型不同Rust 为异步代码块创建的未来值,最终会在任何给定变种的字段中,以到他们自身的引用结束,如图 17-4 中的简化插图所示。
![一种自引用的数据类型](../images/trpl17-04.svg)
*图 17-4一种自引用的数据类型*
但在默认情况下,任何有着对自身引用的对象,在迁移时都不安全,因为引用总是指向其所引用对象的具体内存地址(见图 17-5。若咱们迁移该数据结构本身这些内部引用将仍旧指向旧位置。但是该内存位置现在是无效的。首先当咱们更改该数据结构时他的值不会更新。更重要的是计算机现在可以将该内存重新用于其他目的咱们可能会在最后读到完全无关的数据。
![迁移某个自引用数据类型的不安全结果](../images/trpl17-05.svg)
*图 17-5迁移某个自引用数据类型的不安全结果*
理论上Rust 编译器可以尝试在对象被迁移时,更新该对象的每个引用,但这会增加大量的性能开销,尤其是当整个引用网络需要更新时。如果我们能确保相关数据结构 *不会在内存中迁移*,我们就不必更新任何引用。这正是 Rust 的借用检查器所要求的:在安全的代码中,他会阻止咱们迁移任何有活动引用的项目。
`Pin` 就是建立在此基础上,为我们提供了我们所需的确切保证。当我们通过将某个到该值的指针,封装在 `Pin` 中,而 *固定* 了某个值时,该值就不再可迁移了。因此,若咱们有着 `Pin<Box<SomeType>>` 时,咱们实际上固定了这个 `SomeType` 的值,而 *不是* 那个 `Box` 指针。图 17-6 演示了这一过程。
![固定某个指向一个自引用未来值类型的 `Box`](../images/trpl17-06.svg)
*图 17-6固定某个指向一个自引用未来值类型的 `Box`*
事实上,这个 `Box` 指针仍然可以自由迁移。请记住:我们关心的是,确保那个最终被引用的数据保持在原处。如果某个指针四处迁移,*但其指向的数据仍在原处*,如图 17-7 所示,就不会有潜在问题。(作为一个独立练习,请查看这些类型及 `std::pin` 这个模块的文档,并尝试解决咱们应如何以封装了某个 `Box``Pin`,实现这点。)关键在于,自引用类型本身不能迁移,因为他仍被固定着。
![迁移指向某个自引用未来值类型的 `Box`](../images/trpl17-07.svg)
*图 17-7迁移指向某个自引用未来值类型的 `Box`*
不过,大多数类型都可以安全地迁移,即使他们碰巧位于某个 `Pin` 封装器之下。只有当项目有着内部引用时,我们才需要考虑固定。诸如数字及布尔值的原生值是安全的,因为他们显然没有任何的内部引用。咱们在 Rust 中用到的大多数类型也是如此。例如,咱们可随意迁移某个 `Vec`,而不必担心。仅就我们目前所见,如果咱们有个 `Pin<Vec<String>>`,咱们就必须通过 `Pin` 提供的安全但限制性的 API完成所有操作尽管在没有到其的其他引用下移动 `Vec<String>` 总是安全的。我们需要一种告诉编译器,在这种情况下移动项目是安全的方式 -- 这就是 `Unpin` 发挥作用的地方。
`Unpin` 是个标记性特质,类似于我们在第 16 章中看到的 [`Send` 与 `Sync` 特质](../concurrency/extensible_concurrency.md),因此本身没有任何功能。标记性特质的存在,只是为了告诉编译器,在特定上下文中使用实现了某个给定特质的类型是安全的。`Unpin` 告知编译器,某个给定类型 ** 需要对相关值是否可以安全移动做出任何保证。
`Send``Sync` 一样,编译器会为所有能证明其安全的类型,自动实现 `Unpin`。与 `Send``Sync` 类似的一种特殊情况是,`Unpin` ** 对某一类型而被实现。这种情况的记法为 `impl !Unpin for SomeType`,其中,`SomeType` 是某个指向该类型的指针被用于某个 `Pin` 中时,*确实* 需要保证安全的类型名字。
换句话说,关于 `Pin``Unpin` 之间的关系,有两点需要注意。首先,`Unpin` 属于 “正常” 情况,而 `!Unpin` 是特殊情况。其次,只有在使用像是 `Pin<&mut SomeType>` 等指向某个类型的固定指针时,该类型实现 `Unpin` 还是 `!Unpin` ** 有意义。
为具体说明这点,请设想某个 `String`:他有着一个长度,以及组成他的一些 Unicode 字符。我们可将某个 `String` 封装在某个 `Pin` 中,如图 17-8 所示。然而,与 Rust 中的大多数其他类型一样,`String` 会自动实现 `Unpin`
![固定某个 `String`;虚线表示这个 `String` 实现了 `Unpin` 特质,因此其未被固定](../images/trpl17-08.svg)
*图 17-8固定某个 `String`;虚线表示这个 `String` 实现了 `Unpin` 特质,因此其未被固定*
因此,在 `String` 实现了 `!Unpin` 时,我们就可以执行原本非法的一些事情,例如以一个字符串,替换图 17-9 中内存里完全同一位置的另一字符串。这并不违反 `Pin` 的合约,因为 `String` 没有令其迁移不安全的内部引用!这正是他实现 `Unpin` 而不是 `!Unpin` 的原因。
![在内存中以一个完全不同的 `String` 替换 `String`](../images/trpl17-09.svg)
*图 17-9在内存中以一个完全不同的 `String` 替换 `String`*
现在我们知道了理解 [清单 17-17](./multiple_futures.md#listing-17-17) 中,`join_all` 调用所报出错误的足够信息。我们最初尝试将异步代码块产生的未来值,迁移到某个 `Vec<Box<dyn Future<Output = ()>>>` 中,但正如我们所见,这些未来值可能有内部引用,因此他们无法实现 `Unpin`。他们需要被固定下来,然后我们就可以将 `Pin` 类型,传递到 `Vec` 中,确保这些未来值中的底层数据,不会被迁移。
`Pin``Unpin` 对于构建底层库或,或在咱们构建运行时本身很重要,而不是对日常的 Rust 代码很重要。不过,当咱们在错误消息中看到这些特质时,现在咱们就能更好地知道,如何修复咱们的代码了!
> **注意**`Pin` 和 `Unpin` 的结合,使得在 Rust 中安全地实现一整类复杂类型成为可能,否则这些复杂类型就会因为自引用而具有挑战性。目前,需要 `Pin` 的类型最常见于异步的 Rust 中,但偶尔咱们也会在其他上下文中看到他们。
>
> `std::pin` 的 API 文档中,详细介绍了 `Pin` 和 `Unpin` 的工作原理,以及他们需要遵守的规则,因此如果咱们有兴趣了解更多,可以从这里开始。
>
> 如果咱们想更详细地了解表象之下的工作原理,请参阅 [《Rust 中的异步编程》](https://rust-lang.github.io/async-book/) 的 [第 2 章](https://rust-lang.github.io/async-book/02_execution/01_chapter.html) 和 [第 4 章](https://rust-lang.github.io/async-book/04_pinning/01_chapter.html)。
## `Stream` 特质
现在咱们已经对 `Future``Pin``Unpin` 三个特质有了更深入了解,我们可以将注意力转向 `Stream` 这个特质。正如咱们在本章前面所了解的,流类似于异步的迭代器。然而,与 `Iterator``Future` 不同的是,在本文档编写时,`Stream` 在标准库中还没有定义,但在整个生态系统中用到的 `futures` 代码箱中,*有* 个非常普遍的定义。
我们来先回顾一下 `Iterator``Future` 两个特质的定义,然后再看 `Stream` 特质如何将二者合并在一起。从 `Iterator` 中,我们获得了序列的概念:他的 `next` 方法,提供了一个 `Option<Self::Item>` 选项。而从 `Future`我们获得了随时间变化准备状态的概念the idea of readiness over time他的 `poll` 方法提供了一个 `Poll<Self::Output>` 。为了表示随时间变化而就绪的一个项目序列,我们定义了一个这两个特质组合在一起的 `Stream` 特质:
```rust
use std::pin::Pin;
use std::task::{Context, Poll};
trait Stream {
type Item;
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>
) -> Poll<Option<Self::Item>>;
}
```
`Stream` 特质定义了一个名为 `Item` 的关联类型,用于表示由流所产生项目的类型。这与 `Iterator` 特质类似,其中可能有零到多个项目,而与 `Future` 特质不同,其中总是只有一个 `Output`,即使他是单元值类型 `()`
`Stream` 还定义了个获取这些项目的方法。我们称其为 `poll_next`,以清楚地表明他会以 `Future::poll` 相同方式轮询,并以 `Iterator::next` 相同方式产生项目序列。其返回类型结合了 `Poll``Option`。外层类型为 `Poll`,因为与未来值一样,他必须检查就绪状态。内层类型为 `Option`,因为与迭代器一样,他需要发出是否有更多消息的信号。
与这种定义非常接近的定义,很可能最终会成为 Rust 标准库的一部分。在此期间,他是大多数运行时工具套件的一部分,所以咱们可以信赖他,我们接下来要介绍的所有内容,一般都会适用!
但是,我们在有关流的小节中,所看到示例里,我们并没有使用 `poll_next``Stream`,而使用了 `next``StreamExt`。当然,我们 ** 通过手写我们自己的 `Stream` 状态机,而直接使用 `poll_next` 这个 API就像我们可经由未来值的 `poll` 方法,直接使用未来值一样。不过,使用 `await` 更好,同时 `StreamExt` 特质提供了 `next` 方法,因此我们才可以这样做:
```rust
trait StreamExt: Stream {
async fn next(&mut self) -> Option<Self::Item>
where
Self: Unpin;
// other methods...
}
```
> **注意**:我们在本章早先用到的具体定义,与此略有不同,因为他支持了那些还不支持在特质中使用异步函数的 Rust 版本。由此,他看起来是这样的:
```rust
fn next(&mut self) -> Next<'_, Self> where Self: Unpin;
```
> 其中的 `Next` 类型是个实现了 `Future` 的结构体,允许我们将到 `self` 引用的生命周期,命名为 `Next<'_, Self>`,这样 `await` 就可用于这个方法了。
`StreamExt` 特质也是所有可用于流的有趣方法的发源地。每个实现了 `Stream` 的类型,都会自动实现 `StreamExt`,但这两个特质是单独定义的,以便社区在不影响基础特质下,迭代这些方便的 API。
`trpl` 这个代码箱中用到的 `StreamExt` 版本中,该特质不仅定义了 `next` 方法,还提供了 `next` 的一种可正确处理调用 `Stream::poll_next` 细节的默认实现。这意味着,即使咱们需要编写咱们自己的流数据类型,咱们也 ** 需实现 `Stream`,然后用到咱们数据类型的任何人,都可以自动使用 `StreamExt` 及其方法。
以上就是我们要介绍的关于这些特质的底层细节。最后,我们来看看未来值(包括流)、任务及线程,是如何结合在一起的!
End

View File

@@ -0,0 +1,438 @@
# 应用带有异步的并发
**Applying Concurrency with Async**
在本节中,我们将把异步应用到第 16 章中,咱们在线程上所面临的一些并发挑战。由于我们已经在第 16 章中,讨论过很多关键思想,因此本节我们将重点讨论线程与未来值之间的不同之处。
在许多情形下,使用异步处理并发的 API与使用线程处理并发的 API 非常相似。而在其他情形下,二者最终会截然不同。即使线程与异步的 API *看起来* 很相似,他们也往往有着不同行为 -- 而且他们几乎总是有着不同的性能特征。
## 使用 `spawn_task` 创建新任务
在 [使用 Spawn 创建新线程](../concurrency/threads.md#使用-spawn-函数创建新线程) 小节中,我们解决的首项操作,是在两个独立线程上计数。现在我们来使用异步,完成同样的事情。`trpl` 代码箱提供了个与 `thread::spawn` 这个 API 非常相似的 `spawn_task` 函数,以及一个 `thread::sleep` API 异步版本的 `sleep` 函数。我们可以一并使用这两个函数,实现那个计数示例,如清单 17-6 所示。
文件名:`src/main.rs`
```rust
use std::time::Duration;
fn main() {
trpl::run( async {
trpl::spawn_task( async {
for i in 1..10 {
println!("hi number {i} from the first task!");
trpl::sleep(Duration::from_millis(500)).await;
}
});
for i in 1..5 {
println!("hi number {i} from the second task!");
trpl::sleep(Duration::from_millis(500)).await;
}
});
}
```
*清单 17-6创建出在主任务打印其他内容时打印一个东西的任务*
作为咱们的起点,我们以 `trpl::run` 设置了咱们的主函数,这样我们的顶层函数就可以是异步的了。
> **注意**:从本章的这里开始,每个示例都将在 `main` 中,包含以 `trpl::run` 封装的完全相同代码,因此我们通常将跳过 `trpl::run`,就像跳过 `main` 一样。请不要忘记在咱们的代码中加入他!
然后我们在该代码块中写了两个循环,每个循环都包含了个 `trpl::sleep` 调用这会在发送下一条消息前等待半秒500 毫秒)。我们将一个循环放在 `trpl::spawn_task` 的主体中,另一个放在一个顶层的 `for` 循环中。在 `sleep` 调用后,我们还添加了个 `await`
这段代码的行为与基于线程的实现类似 -- 包括当咱们运行这段代码时,在咱们自己终端中可能看到消息以不同顺序出现这一情况:
```console
hi number 1 from the second task!
hi number 1 from the first task!
hi number 2 from the first task!
hi number 2 from the second task!
hi number 3 from the first task!
hi number 3 from the second task!
hi number 4 from the first task!
hi number 4 from the second task!
hi number 5 from the first task!
```
这个版本会在主异步代码块主体中的 `for` 循环结束时立即停止,因为由 `spawn_task` 生成的任务,在 `main` 函数结束时会被关闭。若咱们想要他一直运行到任务完成就将需要使用一个联合句柄a join handle等待第一个任务完成。在线程下我们曾使用 `join` 方法,在线程运行完毕前予以 “阻塞”。在下面的清单 17-7 中,我们可使用 `await` 完成同样的事情因为任务句柄the task handle本身就是个未来值。他的 `Output` 类型是个 `Result`,因此我们也可以在等待他后,对其解封装。
文件名:`src/main.rs`
```rust
let handle = trpl::spawn_task( async {
for i in 1..10 {
println!("hi number {i} from the first task!");
trpl::sleep(Duration::from_millis(500)).await;
}
});
for i in 1..5 {
println!("hi number {i} from the second task!");
trpl::sleep(Duration::from_millis(500)).await;
}
handle.await.unwrap();
```
*清单 17-7使用 `await` 与联合句柄,运行任务到完成*
这个更新后的版本,会运行到 *两个循环* 都结束为止。
```console
hi number 1 from the second task!
hi number 1 from the first task!
hi number 2 from the first task!
hi number 2 from the second task!
hi number 3 from the first task!
hi number 3 from the second task!
hi number 4 from the first task!
hi number 4 from the second task!
hi number 5 from the first task!
hi number 6 from the first task!
hi number 7 from the first task!
hi number 8 from the first task!
hi number 9 from the first task!
```
目前看来,异步与线程给到了我们同样的基本结果,只是语法不同:使用 `await` 而不是在联合句柄上调用 `join`,以及等待那个 `sleep` 调用。
更大的区别在于,我们无需启动另一个操作系统线程,来完成这项工作。事实上,我们甚至不需要在这里生成一个任务。由于异步代码块会编译为匿名的未来值,因此我们可将各个循环,放在一个异步代码块中,然后使用 `trpl::join` 函数,让运行时将他们运行完成。
在 [“使用 `join` 句柄等待所有线程结束”](../concurrency/threads.md#使用-join-句柄等待全部线程结束) 小节中,我们展示了在调用 `std::thread::spawn` 时返回的 `JoinHandle` 类型上,如何使用 `join` 方法。这个 `trpl::join` 函数与之类似,不过用于未来值。当咱们给到他两个未来值时,他会产生出一个其输出为元组的新未来值,该元组中包含着咱们所传入的各个未来值完成后的输出。因此,在清单 17-8 中,我们使用了 `trpl::join`,等待 `fut1``fut2` 结束。我们等待的 *不是* `fut1``fut2`,而是由 `trpl::join` 生成的那个新未来值。我们会忽略输出,因为他只是个包含了两个单元值的元组。
文件名:`src/main.rs`
```rust
let fut1 = async {
for i in 1..10 {
println!("hi number {i} from the first task!");
trpl::sleep(Duration::from_millis(500)).await;
}
};
let fut2 = async {
for i in 1..5 {
println!("hi number {i} from the second task!");
trpl::sleep(Duration::from_millis(500)).await;
}
};
trpl::join(fut1, fut2).await;
```
<a name="listing-17-8"></a> *清单 17-8使用 `trpl::join` 等待两个匿名未来值*
在运行此代码时,我们会看到两个未来值都会运行至完成:
```console
hi number 1 from the first task!
hi number 1 from the second task!
hi number 2 from the first task!
hi number 2 from the second task!
hi number 3 from the first task!
hi number 3 from the second task!
hi number 4 from the first task!
hi number 4 from the second task!
hi number 5 from the first task!
hi number 6 from the first task!
hi number 7 from the first task!
hi number 8 from the first task!
hi number 9 from the first task!
```
现在,咱们将看到每次都完全相同的顺序,这与我们在线程下看到的情况截然不同。这是因为 `trpl::join` 函数是 *公平的*,意味着他会以相同频率检查各个未来值,在二者之间交替进行,并绝不会在一个已就绪时,让另一个超前。在线程下,是由操作系统决定要检查哪个线程,以及让他运行多久。而在异步的 Rust 下,是由运行时决定要检查哪个任务。(在实践中,细节会变得复杂,因为异步运行时可能会在表象之下,使用操作系统的线程,作为其管理并发性的一部分,所以对运行时来说,保证公平性可能会更费事 -- 但这仍然是可行的!)运行时不必保证对任何给定操作的公平性,他们通常提供不同的 API让咱们选择是否需要公平性。
请在等待未来值上,尝试以下这些变化,看看他们能做些什么:
- 移除任一循环,或同时两个循环的异步代码块;
- 在定义出各个异步代码块后,立即等待他们;
- 只将第一个循环封装在异步代码块中,并在第二个循环的主体后,等待所得到的未来值。
作为额外挑战,请在运行代码 **,看看咱们能否得出每种情况下的输出结果!
## 使用消息传递在两个任务上计数
在未来值间共用数据也将很常见:再次我们将使用消息传递,但这次将使用异步版本的类型与函数。我们将采用与咱们在 [使用消息传递在线程间传输数据](../concurrency/message_passing.md) 小节中略微不同的路径,说明基于线程与基于未来值的并发间的一些关键区别。在清单 17-9 中,我们将从单个的异步代码块开始 -- 而 *不是* 像咱们曾生成单个线程时,生成单个任务。
文件名:`src/main.rs`
```rust
let (tx, mut rx) = trpl::channel();
let val = String::from("hi");
tx.send(val).unwrap();
let received = rx.recv().await.unwrap();
println!("Got: {received}");
```
*请单 17-9创建出一个异步通道并将其中两半分别赋值给 `tx` 与 `rx`*
这里,我们使用了我们在第 16 章中,与线程一起使用的多生产者、单消费者通道 API 的一个异步版本 `trpl::channel`。该 API 的异步版本,与基于线程的版本只有一点不同:他使用了可变的接收器 `rx`,而不是不可变接收器 `rx`,而且他的 `recv` 方法会产生一个我们需要等待的未来值,而不是直接产生值。现在,我们可以从发送方往接收方发送消息了。请注意,我们不必生成单独线程,甚至不需要生成任务;我们只需等待这个 `rx.recv` 调用。
`std::mpsc::channel` 中的同步 `Receiver::recv` 方法,会在收到消息前一直阻塞。而 `trpl::Receiver::recv` 这个方法不会,因为他是异步的。他不会阻塞,而是在消息被接收或通道的发送侧关闭前,会将控制权交还给运行时。相比之下,我们不会等待 `send` 调用,因为他不会阻塞。他之所以无需等待,是因为我们将消息发入的通道,是不受限的 <sup>1</sup>。
> **译注**
>
> <sup>1</sup>the channel we're sending it into is unbounded.一个有效的、不发散的程序所能占用的空间和时间并没有先验的固定限制。
>
> 参考:[Unbounded nondeterminism](https://en.wikipedia.org/wiki/Unbounded_nondeterminism)
> **注意**:由于所有这些异步代码,都在一个 `trpl::run` 调用的异步代码块中运行,因此其中的所有代码,都可避免阻塞。但是,当 `run` 函数返回时,异步代码块 *外部* 的代码会阻塞。这正是 `trpl::run` 函数的意义所在:他可以让咱们 *选择*,在哪些地方阻塞某些异步代码,从而在哪些地方切换同步代码和异步代码。在大多数异步运行时中,`run` 其实被命名为 `block_on`,正是出于这个原因。
请注意这个例子的两点。首先,消息将立即到达。其次,虽然我们在这里使用了一个未来值,但这里还没有并发。该列表中的所有事情,都是按顺序发生的,就像没有涉及到未来值一样。
我们来通过发送一系列消息并在中间休眠,解决第一部分问题,如清单 17-10 所示。
文件名:`src/main.rs`
```rust
let (tx, mut rx) = trpl::channel();
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_millis(500)).await;
}
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
```
*请单 17-10通过异步通道发送及接收多条消息并在每条消息之间对 `await` 休眠*
除发送消息外,我们还需要接收他们。在本例中,由于我们知道有多少条消息进来,因此我们可通过调用 `rx.recv().await` 四次,手动完成接收。但在真实世界中,我们一般会等待 *未知* 数量的消息,因此我们需要一直等待,直到确定没有更多信息为止。
在 [清单 16-10](../concurrency/message_passing.md#listing-16-10) 中,我们使用了个 `for` 循环处理从同步通道接收到的所有项目。然而Rust 还没有一种,为 *异步的* 序列项目编写 `for` 循环的方法,因此我们需要使用一种以前从未见过的循环:`while let` 条件循环。这是我们曾在 [“使用 `if let` 与 `let else` 的简明控制流”](../enums_and_pattern_matching/if-let_control_flow.md) 小节中,看到的 `if let` 结构的循环版本。只要循环所指定的模式继续匹配值,其就会继续执行。
其中的 `rx.recv` 调用,会产生一个我们所等待的未来值。在其准备好前,运行时将暂停该未来值。一旦有消息到达,这个未来值将解析为 `Some(message)`,解析次数与消息到达次数相同。在通道关闭时,无论 *有多少* 消息到达,该未来值都会解析为表示不再有值的 `None`,并因此我们就应停止轮询 -- 即停止等待。
那个 `while let` 循环,将所有这一切联系在了一起。在调用 `rx.recv().await` 的结果为 `Some(message)` 时,我们就可以访问到消息,并在循环体中使用他,就跟使用 `if...let` 一样。在结果为 `None` 时,则该循环结束。每次循环完毕时,其都会再次遇到等待点,因此运行时会再度将其暂停,直到另一条消息到达。
代码现在可以成功发送并接收所有信息。遗憾的是,其间仍然存在一些问题。首先,消息不是以半秒为间隔到达的。他们会在我们启动程序 2 秒2000 毫秒)后,一次性到达。另外,这个程序永远不会退出!相反,他会一直等待新信息。咱们需要以 `Ctrl-c` 关闭他。
我们先来看看,为什么消息会在全部延迟后一次性发送,而不是每条消息之间都有延迟。在某个给定异步代码块中,`await` 关键字在代码中出现的顺序,就是他们在程序运行时,被执行的顺序。
清单 17-10 中只有一个异步代码块,因此其中的所有代码都是线性运行的。其中仍然没有并发。所有的 `tx.send` 调用,都是在所有 `trpl::sleep` 调用及其关联的等待点之间发生的。在那之后,`while let` 循环才进入 `recv` 调用上的任何等待点。
要实现我们所期望的行为,即在每条消息之间发生睡眠延迟,我们需要将 `tx``rx` 操作,放在他们各自的异步代码块中,如下清单 17-11 所示。然后,运行时可使用 `trpl::join`,分别执行这两个操作,就像 [计数示例](#listing-17-8) 中那样。再一次,我们等待的是调用 `trpl::join` 的结果,而不是各个未来值。如果我们按顺序等待单个未来值,我们就会回到一种顺序流程中 -- 这正是我们 ** 想做的。
文件名:`src/main.rs`
```rust
let (tx, mut rx) = trpl::channel();
let tx_fut = async {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_millis(500)).await;
}
};
let rx_fut = async {
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
};
trpl::join(tx_fut, rx_fut).await;
```
*清单 17-11通过异步通道发送及接收多条消息并在每条消息之间以一个 `await` 休眠*
有了清单 17-11 中更新后的代码,消息将以 500 毫秒的间隔打印,而不是在 2 秒后匆忙全部打印出来。
不过,由于 `while let` 循环与 `trpl::join` 交互方式的原因,该程序仍然不会退出:
-`trpl::join` 返回的未来值,只有在传给他的两个未来值 ** 完成后才会完成;
- `tx` 这个未来值,在其发送完 `vals` 中最后一条消息,结束休眠后会立即完成;
-`while let` 循环结束后,`rx` 这个未来值才会完成;
- 在等待 `rx.recv` 产生 `None` 前,`while let` 这个循环不会结束;
- 只有在通道另一端关闭后,等待 `rx.recv` 才会返回 `None`
- 只有当我们调用 `rx.close` 时,或发送端 `tx` 被弃用(译注:超出作用域被内存回收)时,通道才会关闭;
- 我们未在任何地方调用 `rx.close`,同时在传递给 `trpl::run` 的外层异步代码块结束时,才会弃用 `tx`
- 该代码块无法结束,因为他被阻塞于 `trpl::join` 的完成中,这让我们回到了该代码清单的顶部。
我们可通过在某处调用 `rx.close` 手动关闭 `rx`,但这样做意义不大。在处理了任意数量的消息后停止,会使这个程序关闭,但我们可能会错过消息。我们需要其他方法,确保 `tx` 在该函数结束 ** 被弃用。
现在,我们于其中发送消息的异步代码块,只借用了 `tx`,因为发送消息不需要所有权,但如果我们能将 `tx` 迁移到该异步代码块中,那么一旦那个代码块结束,他就会被弃用。在第 13 章 [“捕获引用抑或迁移所有权”](../functional_features/closures.md#捕获引用抑或迁移所有权) 小节中,我们学习了如何在闭包中使用 `move` 关键字,而正如第 16 章 [“对线程使用 `move` 闭包”](../concurrency/threads.md#对线程使用-move-闭包) 小节中所讨论的,在使用线程时,我们经常需要将数据迁移到闭包中。同样的动因,也适用于异步代码块,因此 `move` 这个关键字在异步代码块中的作用,与在闭包中一样。
在下面的清单 17-12 中,我们将用于发送消息的代码块,从 `async` 改为了 `async move`。当我们运行 *这个* 版本的代码时,他就会在发送及接收完最后一条消息后,优雅地关闭。
文件名:`src/main.rs`
```rust
let (tx, mut rx) = trpl::channel();
let tx_fut = async move {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_millis(500)).await;
}
};
let rx_fut = async {
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
};
trpl::join(tx_fut, rx_fut).await;
```
*清单 17-12完成后正确关闭的清单 17-11 中代码的修订版本*
这个异步通道还是个多生产者的通道,因此在打算自多个未来值发送消息时,我们可以调用 `tx` 上的 `clone`,如下清单 17-13 所示。
文件名:`src/main.rs`
```rust
let (tx, mut rx) = trpl::channel();
let tx1 = tx.clone();
let tx1_fut = async move {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx1.send(val).unwrap();
trpl::sleep(Duration::from_millis(500)).await;
}
};
let rx_fut = async {
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
};
let tx_fut = async move {
let vals = vec![
String::from("more"),
String::from("messages"),
String::from("for"),
String::from("you"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_millis(1500)).await;
}
};
trpl::join3(tx1_fut, tx_fut, rx_fut).await;
```
*清单 17-13对异步代码块使用多生产者*
首先,我们克隆了 `tx`,在第一个异步代码块外创建出 `tx1`。我们将 `tx1` 迁移到该代码块中,就跟之前对 `tx` 所做的那样。然后,我们将原始的 `tx` 迁移到一个 *新的* 异步代码块中,并在其中以稍慢的延迟发送更多消息。我们碰巧把这个新的异步代码块,放在接收消息的异步代码块后,但他也可以在接收信息的异步代码块前。关键在于等待未来值的顺序,而不是他们被创建出的顺序。
发送信息的两个异步代码块,都需要是 `async move` 的代码块,这样当这两个代码块完成时,`tx``tx1` 都会被弃用。否则,我们就会回到一开始的无限循环中。最后,我们将 `trpl::join` 切换为了 `trpl::join3`,以处理额外的未来值。
现在,我们可以看到来自两个发送未来值的所有消息,由于两个发送未来值在发送后,使用了略微不同的延迟,消息也是在这些不同的间隔内收到的。
```console
received 'hi'
received 'more'
received 'from'
received 'the'
received 'messages'
received 'future'
received 'for'
received 'you'
```
这是一个良好开端,但他将我们限制在少数几个未来值中:两个的 `join` 或三个的 `join3`。我们来看看,如何使用更多的未来值。
End

317
src/async/futures.md Normal file
View File

@@ -0,0 +1,317 @@
# 未来值与异步语法
**Futures and the Async Syntax**
Rust 中异步编程的关键要素,是未来值与 Rust 的 `async``await` 关键字。
所谓未来值,是个现在可能还没有准备好,但会在未来某个时刻准备好的值。(在许多语言中,都有这同样的概念,有时会以其他名称如 `task``promise` 出现。Rust 提供了作为一种构建块的 `Future` 特质,这样不同异步操作,就可以有不同数据结构,但却以通用接口实现。在 Rust 中,未来值是一些实现了 `Future` 特质的类型。每个未来值都保存着其自己已完成进度的信息,以及何谓 “就绪” 的含义。
咱们可将 `async` 关键字,应用于代码块及函数,指定出他们可被中断与恢复。在某个异步代码块或异步函数中,咱们可使用 `await` 关键字,*等待* 某个未来值(即等待其就绪)。在某个异步代码块或函数中,咱们等待某个未来值的任何位置,都是该异步代码块或函数暂停及恢复的潜在位置。检查某个未来值值是否可用的过程,称为 *轮询polling*
一些别的语言,如 C# 与 JavaScript也将 `async``await` 关键字用于异步编程。如果咱们熟悉这些语言,就可能会注意到 Rust 在行事方式,包括如何处理语法上的一些显著差异。这是有原因的,我们将会看到!
在编写异步的 Rust 代码时,我们通常会使用 `async``await` 关键字。就像使用 `Iterator` 特质将 `for` 循环编译成等价代码一样Rust 会使用 `Future` 特质将这两个关键字编译成等价代码。不过,由于 Rust 提供了 `Future` 这个特质,在需要时咱们也可以给咱们自己的数据类型,实现该特质。在本章中,我们将看到许多函数,都会返回有着他们自己 `Future` 实现的类型。我们将在本章末尾,回到该特质的定义,并深入探讨其工作原理,但这些细节已经足够让我们继续前进了。
这可能会让人感觉有点抽象,所以我们来编写咱们的首个异步程序:一个小的 web 爬虫。我们将在命令行上传入两个 URL并发地获取这两个 URL 的内容,并返回其中一个先完成的结果。这个示例将使用大量新语法,不过不用担心 -- 我们会在进行过程中,为咱们解释所有需要知道的内容。
## 咱们的首个异步程序
为将本章的重点放在学习异步,而不琢磨生态的各个部分,我们创建了 `trpl` 这个代码箱(`trpl` 是 “The Rust Programming Language” 的缩写)。他会重导出了咱们所需的所有类型、特质及函数,主要来自 `futures``tokio` 两个代码箱。`futures` 代码箱是 Rust 异步代码实验的正式场所,且实际上也是 `Future` 特质的最初设计地。[Tokio](https://tokio.rs/) 是如今 Rust 中使用最广泛的异步运行时,尤其适用于 web 应用。还有其他一些很棒的运行时,而他们则可能更适合咱们的用途。我们在 `trpl` 表象之下,使用了 `tokio` 这个代码箱,因为他经过了良好测试,而且被广泛使用。
在某些情况下,`trpl` 还重命名或封装了一些原始 API以便让咱们专注于本章相关的细节。如果咱们想了解这个代码箱的功能我们建议查看 [其源代码](https://github.com/rust-lang/book/tree/main/packages/trpl)。咱们将能看到每个重导出分别来自哪个代码箱,我们还留下了大量解释该板块作用的注释。
请创建一个名为 `hello-async` 的新二进制项目,并将 `trpl` 代码箱添加为依赖项:
```console
cargo new hello-async
cd hello-async
cargo add trpl
```
现在,我们就可以使用 `trpl` 提供的各种组件,编写咱们的首个异步程序了。我们将构建出一个获取两个网页,从每个网页中提取 `<title>` 元素,并打印出最先完成整个过程网页标题的小命令行工具。
### 定义 `page_title` 函数
我们以编写将某个页面 URL 作为参数,向其发出请求,并返回标题元素文本的函数(见清单 17-1开始。
文件名:`src/main.rs`
```rust
use trpl::Html;
async fn page_title(url: &str) -> Option<String> {
let response = trpl::get(url).await;
let response_text = response.text().await;
Html::parse(&response_text)
.select_first("title")
.map(|title_element| title_element.inner_html())
}
```
*清单 17-1定义从某个 HTML 页面获取标题元素的异步函数*
首先,我们定义了一个名为 `page_title` 的函数,并以 `async` 关键字标记了他。然后,我们使用了 `trpl::get` 函数,获取传入的任何 URL并添加了 `await` 关键字等待响应。要获取到响应的文本,我们调用了其 `text` 方法,并再次以 `await` 关键字等待。这两个步骤都是异步的。对于 `get` 函数,我们必须等待服务器,发回其响应的第一部分,其中将包括 HTTP 头信息、cookie 等等,同时其可与响应正文分开发送。特别是在正文非常大时,全部送达可能需要一些时间。因为我们必须等待响应的 *全部内容* 到达,所以这个 `text` 方法也是异步的。
我们就必须显式地等待这两个未来值,因为 Rust 中未来值是 *懒惰的lazy*:在咱们使用 `await` 关键字请求他们前他们不会做任何事情事实上若咱们未使用某个未来值Rust 将给出一条编译器告警。)这可能会让咱们想起,第 13 章 [“使用迭代器处理系列条目”](../functional_features/iterators.md#使用迭代器处理条目系列) 小节中对迭代器的讨论。除非咱们调用迭代器的 `next` 方法,否则迭代器什么也不会做 -- 无论是直接调用,还是通过使用 `for` 循环或在其表象之下用到 `next` 的比如 `map` 方法。同样,除非咱们明确要求他们,这些未来值也会什么都不做。这种 “懒惰” 让 Rust 可以避免运行异步代码,除非真的需要。
> **注意**:这不同于我们在上一章 [“使用 `spawn` 创建新线程”](../concurrency/threads.md##使用-spawn-函数创建新线程) 小节中,使用 `thread::spawn` 时看到的行为,当时我们传递给另一线程的闭包立即开始了运行。这也不同于许多其他语言处理异步的方式。但这对 Rust 很重要,我们稍后将看到原因。
有了 `response_text`,我们就可以使用 `Html::parse`,将其解析为 `Html` 类型的实例。现在,我们有了一种可将其作为更丰富数据结构 HTML 处理的数据类型,而不是原始字符串了。特别是,我们可以使用 `select_first` 方法,查找给定 CSS 选择器的首个实例。通过传递字符串 `"title"`,我们将获取到文档中的第一个 `<title>` 元素,如果有的话。由于可能没有匹配的元素,`select_first` 会返回一个 `Option<ElementRef>`。最后,我们使用 `Option::map` 方法,在 `Option` 中的项目存在时,我们就可以使用他,若不存在,就什么也不做。(这里我们也可以使用一个 `match` 表达式,但 `map` 更为惯用。)在我们提供给 `map` 的函数主体中,我们调用了 `title_element` 上的 `inner_html`,获取为一个 `String` 的其内容。最后,我们得到了一个 `Option<String>`
请注意Rust 的 `await` 关键字,是在咱们正等待的表达式 *之后*,而不是之前。也就是说,他是个 *后缀* 关键字a *postfix* keyword。如果咱们在别的语言中使用过 `async`,这可能与咱们所习惯的不同,但在 Rust 中他使得方法链chains of methods更易于使用。因此我们可以将 `page_url_for` 的主体,修改为与 `trpl::get``text` 函数两个调用链接起来,并在他们之间加上 `await`,如清单 17-2 所示。
文件名:`src/main.rs`
```rust
let response_text = trpl::get(url).await.text().await;
```
*清单 17-2使用 `await` 关键字的链接*
就这样,我们已成功编写了咱们的首个异步函数!在我们于 `main` 中添加一些代码调用他前,我们先来了解一下我们已编写的内容及其含义。
当 Rust 看到某个以 `async` 关键字标记的代码块时,他会将其编译为一种实现了 `Future` 特质的唯一、匿名数据类型。当 Rust 看到某个以 `async` 标记的函数时,他会将其编译成一个主体为异步代码块的非异步函数。异步函数的返回值类型,就是编译器为该异步代码块所创建的匿名数据类型。
因此,写下 `async fn`,就相当于编写了某个返回值类型为 *未来值* 的函数。对于编译器来说,诸如清单 17-1 中的 `async fn page_title` 的函数定义,就等同于如下定义的一个非异步函数:
```rust
use std::future::Future;
use trpl::Html;
fn page_title(url: &str) -> impl Future<Output = Option<String>> + '_ {
async move {
let text = trpl::get(url).await.text().await;
Html::parse(&text)
.select_first("title")
.map(|title| title.inner_html())
}
}
```
我们来逐一了解,转换后版本的各个部分:
- 他使用了我们在第 10 章 [“作为参数的特质”](../generic_types_traits_and_lifetimes/traits.md#作为参数的特质) 小节中,讨论过的 `impl Trait` 语法;
- 返回的特质是有着 `Output` 关联类型的 `Future`。请注意,`Output` 类型为 `Option<String>`,这与 `async fn` 版本 `page_title` 的原始返回值类型相同;
- 原始函数主体中调用的所有代码,都被封装在一个 `async move` 代码块中。请记住,代码块都是一些表达式。整个代码块就是该函数所返回的表达式;
- 如上所述,该异步代码块会产生一个 `Option<String>` 类型的值。该值与返回值类型中的 `Output` 类型匹配。这与咱们曾见过的其他代码块一样;
- 新的函数体是个 `async move` 代码块,因为他使用 `url` 参数的方式;(我们将在本章后面,详细讨论 `async``async move`。)
- 该函数的新版本,在输出类型中有种我们以前从未见过的生命周期:`'_`。由于该函数返回了一个指向某个引用的未来值 -- 本例中,引用来自 `url` 参数 -- 因此我们就需要告诉 Rust我们希望该引用要被包含。在这里我们不必命名这个生命周期因为 Rust 足够聪明,知道只有一个可能涉及到的引用,但我们 *确实* 必须明确指出,得到的那个未来值受该生命周期的约束。
现在我们可以在 `main` 中调用 `page_title` 了。
## 确定单个页面的标题
首先,我们将获取到单个页面的标题。在清单 17-3 中,我们沿用了第 12 章 [“接收命令行参数”](../io_project/accepting_cli_arguments.md) 小节中,获取命令行参数的相同模式。然后,我们传递首个 URL 给 `page_title`,并等待结果。由于有未来值产生的值是个 `Option<String>`,因此我们使用一个 `match` 表达式,打印不同的信息,以反映该页面是否有着 `<title>`
文件名:`src/main.rs`
```rust
// 此代码不会工作
async fn main() {
let args: Vec<String> = std::env::args().collect();
let url = &args[1];
match page_title(url).await {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
}
```
*清单 17-3以一个用户提供的参数在 `main` 中调用 `page_title` 函数*
不幸的是,这段代码不会编译。我们只能在异步函数或代码块中,使用 `await` 关键字,同时 Rust 不会让我们将这个特殊的 `main` 函数标记为 `aysnc`
```console
error[E0752]: `main` function is not allowed to be `async`
--> src/main.rs:12:1
|
12 | async fn main() {
| ^^^^^^^^^^^^^^^ `main` function is not allowed to be `async`
```
`main` 不能标记为 `async` 的原因,是异步代码需要一个 *运行时*:一个管理执行异步代码细节的 Rust 代码箱。某个程序的 `main` 函数可以 *初始化* 某个运行时,但 *他本身* 并不是个运行时。(我们将更多地了解为什么会有这种情况。)每个执行异步代码的 Rust 程序,都至少有一个其设置了某个运行时,并执行未来值之处。
支持异步的大多数语言,都捆绑了某个运行时,但 Rust 并没有。相反,有许多不同异步运行时可用,每一种都根据其针对的用例,做出了不同取舍。例如,有着众多 CPU 核心及大量 RAM 的高吞吐量 web 服务器,与仅有一个核心、少量 RAM 且不具备内存堆分配能力的微控制器,就有着截然不同的需求。提供这些运行时的代码箱,通常还提供了诸如文件或网络 I/O 等常用功能的异步版本。
在这里,以及在本章的其余部分,我们将使用 `trpl` 代码中的 `run` 函数,他会取一个未来值作为参数,并将其运行完成。在幕后,调用 `run` 会设置一个用于运行传入未来值的运行时。一旦该未来值运行完成,`run` 就会返回该未来值所产生的任何值。
我们可将由 `page_title` 返回的未来值,直接传递给 `run`,一旦其运行完成,我们就可以匹配得到的 `Option<String>`,就像咱们在清单 17-3 中所尝试的那样。但是,在本章的大多数示例中(以及现实世界中大多数异步代码中),我们将执行不止一个异步函数调用,因此我们将传递一个 `async` 代码块,并显式等待 `page_title` 调用的结果,如清单 17-4 所示。
文件名:`src/main.rs`
```rust
fn main() {
let args: Vec<String> = std::env::args().collect();
trpl::run(async {
let url = &args[1];
match page_title(url).await {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
})
}
```
<a name="listing-17-4"></a> *清单 17-4使用 `trpl:run` 等待某个异步代码块*
当我们运行这段代码时,我们会得到最初咱们预期的行为:
```console
$ cargo run -- https://news.163.com
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.09s
Running `target/debug/hello-async 'https://news.163.com'`
The title for https://news.163.com was 网易新闻
```
呼 -- 我们终于有了一些可以工作的异步代码!不过,在我们添加让两个网站互相竞赛的代码前,我们来简单回顾一下,这些未来值的工作原理。
每个 *等待点await point* -- 即代码用到 `await` 关键字的各处 -- 都表示了一个控制权被交还给运行时之处。为实现这一点Rust 需要跟踪涉及到异步代码块的状态以便运行时可以启动其他工作然后在准备好再次尝试推进第一个工作时再返回。这是个不可见的状态机an invisible state machine就像咱们写了个保存每个等待点当前状态的枚举一样
```rust
enum PageTitleFuture<'a> {
Initial { url: &'a str },
GetAwaitPoint { url: &'a str },
TextAwaitPoint { response: trpl::Response },
}
```
然而亲自编写在各个状态间转换的代码既繁琐又容易出错尤其是当咱们需要随后添加更多功能及更多状态时。幸运的是Rust 编译器会自动创建及管理异步代码的状态机数据结构。有关数据结构的正常借用和所有权规则仍然适用,同时令人高兴的是,编译器还会为我们检查这些规则,并提供有用的错误消息。我们将在本章稍后部分讨论这些问题。
最终,必须有某种东西来执行这个状态机,而这个东西就是运行时(这就是为什么在研究运行时时,可能会遇到 *执行器executors* 的概念:所谓执行器,是某个运行时中负责执行异步代码的部分。)
现在咱们就明白,为什么编译器阻止了我们在清单 17-3 中,将 `main` 本身作为一个异步函数了吧。如果 `main` 是个异步函数,那么无论 `main` 返回什么样的未来值,都需要其他东西来管理状态机,但 `main` 是程序的起点!相反,我们在 `main` 中调用了 `trpl::run` 函数设置运行时,并在那个 `async` 返回 `Ready` 时,运行由其返回的未来值。
> **注意**:有些运行时提供以便咱们编写异步 `main` 函数的宏。这些宏会重写 `async fn main() { ... }` 为普通的 `fn main`,这与我们在清单 17-5 中,手工编写的相同:调用某个像 `trpl::run` 那样,运行某个未来值至完成。
现在,我们来将这些代码片段组合在一起,看看咱们如何编写并发代码。
## 让我们的两个 URL 相互竞赛
在下面的清单 17-5 中,我们以命令行上传入的两个不同 URL 调用 `page_title`,并进行比赛。
文件名:`src/main.rs`
```rust
use trpl::{Either, Html};
fn main() {
let args: Vec<String> = std::env::args().collect();
trpl::run(async {
let title_fut_1 = page_title(&args[1]);
let title_fut_2 = page_title(&args[2]);
let (url, maybe_title) =
match trpl::race(title_fut_1, title_fut_2).await {
Either::Left(left) => left,
Either::Right(right) => right,
};
println!("{url} returned first");
match maybe_title {
Some(title) => println!("Its page title is: '{title}'"),
None => println!("Its title could not be parsed."),
}
})
}
async fn page_title(url: &str) -> (&str, Option<String>) {
let text = trpl::get(url).await.text().await;
let title = Html::parse(&text)
.select_first("title")
.map(|title| title.inner_html());
(url, title)
}
```
*清单 17-5*
我们以对用户提供的每个 URL调用 `page_title` 开始。我们将得到的未来值,保存为 `title_fut_1``title_fut_2`。请记住,这些未来值还不会做任何事情,因为未来值是懒惰的,且我们还没有等待他们。然后,我们将这两个未来值传递给 `trpl::race`,他会返回一个表明传递给他的未来值中,首先完成那个的值。
> **注意**:表象之下,`race` 是建立在一个更通用的函数 `select` 基础上的,在真实世界 Rust 代码中,咱们将更经常遇到。`select` 函数可以完成很多 `trpl::race` 函数无法完成的事情,但他有一些我们现在可以跳过的额外复杂度。
两个未来值都可以合法地 “获胜”,因此返回 `Result` 是没有意义的。取而代之的是,`race` 会返回一种我们以前从未见过的类型 `trpl::Either``Either` 类型有点类似于 `Result`,因为他有两种情况。但与 `Result` 不同的是,`Either` 中没有成功或失败的概念。相反,他使用了 `Left``Right`,表示 “非此即彼”:
```Rust
enum Either<A, B> {
Left(A),
Right(B),
}
```
在第一个参数获胜时,则 `run` 函数就会返回 `Left`,以及该未来值的输出结果;在第二个未来值参数获胜时,则返回 `Right`,以及该参数的输出结果。这与调用该函数时,参数出现的顺序一致:第一个参数是在第二个参数的左边。
我们还更新了 `page_title`,使其返回所传入的同一 URL。这样在最先返回的页面没有我们可解析的 `<title>` 时,我们仍然可以打印出一条有意义的消息。有了这些信息,我们就可以通过更新 `println!` 的输出,表明哪个 URL 最先完成,并在该 URL 的网页有 `<title>` 元素时,该元素为何。
现在,咱们已经构建了一个可工作的小型 web 爬虫!请选择几个 URL 并运行这个命令行工具。咱们可能会发现,一些网站的速度始终比其他网站快,而在其他情况下,速度较快的网站每次运行会有所不同。更重要的是,咱们已经掌握了使用未来值的基础知识,现在我们可以更深入研究,使用异步咱们可以做些什么了。
End

View File

@@ -0,0 +1,662 @@
# 使用任意数量的未来值
在上一小节中,当我们从使用两个未来值,转换为使用三个未来值时,我们也不得不从使用 `join` 转换为使用 `join3`。如果每次改变我们要连接的未来值数量时,都要调用不同函数,那就太麻烦了。幸运的是,我们有个宏形式的 `join`,使用他我们就可以传递任意数量的参数。他还能自己处理等待未来值。因此,我们可以将清单 17-13 中的代码,重写为使用 `join!` 而非 `join3`,如下清单 17-14 中所示。
文件名:`src/main.rs`
```rust
trpl::join! (tx1_fut, tx_fut, rx_fut);
```
*清单 17-14使用 `trpl::join!` 等待多个未来值*
`join``join3``join4` 等之间的切换相比,这无疑是一种进步!不过,即使是这种宏形式,也只有在我们提前知道未来值数量的情况下才有效。但在真实世界的 Rust 中,将未来值压入某个集合,然后等待其中的部分或全部未来值完成是种常见的模式。
要检查某个集合中的全部未来值,我们将需要遍历并连接 *全部* 的未来值。`trpl::join_all` 函数接受任何实现了 `Iterator` 特质的类型,我们在第 13 章 [`Iterator` 特质与 `next` 方法](../functional_features/iterators.md#iterator-特质与-next-方法) 小节中,已经了解过迭代器特质,因此他似乎正是我们需要的。我们来尝试将我们的未来值放入一个矢量中,并用 `join_all` 代替 `join`,如下清单 17-15 所示。
```rust
let futures = vec! [tx1_fut, tx_fut, rx_fut];
trpl::join_all(futures).await ;
```
*清单 17-15将匿名未来值存储在一个矢量值中并调用 `join_all`*
不幸的是,这段代码不会编译。相反,我们会得到如下报错:
```console
error[E0308]: mismatched types
--> src/main.rs:42:38
|
8 | let tx1_fut = async move {
| ---------- the expected `async` block
...
28 | let tx_fut = async move {
| ---------- the found `async` block
...
42 | let futures = vec! [tx1_fut, tx_fut, rx_fut];
| ^^^^^^ expected `async` block, found a different `async` block
|
= note: expected `async` block `{async block@src/main.rs:8:23: 8:33}`
found `async` block `{async block@src/main.rs:28:22: 28:32}`
= note: no two async blocks, even if identical, have the same type
= help: consider pinning your async block and casting it to a trait object
```
这可能令人惊讶。毕竟,这个异步代码块没有一个会返回任何的内容,所以每个都会产生一个 `Future<Output = ()>`。但请记住,`Future` 是个特质,编译器会为这每个异步代码块,都创建一个唯一的枚举。咱们不能在一个 `Vec` 中,放入两个不同的手写结构体,同样的规则也适用于由编译器生成的不同枚举。
要令到这一点生效,我们就需要使用 *特质对象*,就像我们在第 12 章的 [“从 `run` 函数返回错误”](../io_project/refactoring.md#返回-run-函数中的错误) 小节中所做的那样。(我们将在第 18 章详细介绍特质对象)。使用特质对象,我们就可以将由这些类型产生的匿名未来值,视为同一类型,因为他们都实现了 `Future` 这个特质。
> **注意**:在第 8 章 [“使用枚举存储多个值”](../common_collections/vectors.md#运用枚举存储多种类型) 小节中,我们曾讨论了在 `Vec` 中包含多种类型的另一种方法:使用枚举来表示矢量值中可能出现的每种类型。但在这里我们不能这样做。首先,我们无法命名这些不同类型,因为他们都是匿名的。另外,我们之所以使用矢量和 `join_all`,首要考虑的时为了能够处理未来值的动态集合,我们只关心他们是否有着同一输出类型。
我们首先将 `vec!` 中的每个未来值,都封装在一个 `Box::new` 中,如下清单 17-16 中所示。
文件名:`src/main.rs`
```rust
let futures =
vec![Box::new(tx1_fut), Box::new(rx_fut), Box::new(tx_fut)];
trpl::join_all(futures).await;
```
<a name="listing-17-16"></a> *清单 17-16使用 `Box::new` 对齐某个 `Vec` 中的未来值类型*
不幸的是,这段代码仍不会编译。事实上,我们在第二和第三个 `Box::new` 调用处,都遇到了与之前同样的基本报错,同时还出现了指向 `Unpin` 特质的新报错。我们稍后再来看 `Unpin` 的报错。首先,我们来通过显式地注解 `futures` 这个变量的类型,修复 `Box::new` 调用上的类型错误(见清单 17-17
文件名:`src/main.rs`
```rust
let futures: Vec<Box<dyn Future<Output = ()>>> =
vec![Box::new(tx1_fut), Box::new(rx_fut), Box::new(tx_fut)];
```
<a name="listing-17-17"></a> *清单 17-17通过显式类型生命修复类型不匹配报错的其余部分*
这个类型声明有些重要,我们先来了解一下:
1. 最内层的类型就是那个未来值本身。通过写下 `Future<Output = ()>`,我们显式地指出,这个未来值的输出是单元值类型 `()`
2. 然后,我们以 `dyn` 关键字注解了该特质,将其标记为动态特质;
3. 整个特质引用被封装在一个 `Box` 中;
4. 最后,我们显式地指明 `futures` 是个包含这些项目的 `Vec`
这已经带来了很大的不同。现在,当我们运行编译器时,我们就只会得到提及 `Unpin` 的那些报错了。虽然有三个报错,但他们的内容非常相似。
```console
error[E0277]: `dyn Future<Output = ()>` cannot be unpinned
--> src/main.rs:46:24
|
46 | trpl::join_all(futures).await;
| -------------- ^^^^^^^ the trait `Unpin` is not implemented for `dyn Future<Output = ()>`, which is required by `Box<dyn Future<Output = ()>>: F
uture`
| |
| required by a bound introduced by this call
|
= note: consider using the `pin!` macro
consider using `Box::pin` if you need to access the pinned value outside of the current scope
= note: required for `Box<dyn Future<Output = ()>>` to implement `Future`
note: required by a bound in `join_all`
--> /home/hector/.cargo/registry/src/rsproxy.cn-8f6827c7555bfaf8/futures-util-0.3.31/src/future/join_all.rs:105:14
|
102 | pub fn join_all<I>(iter: I) -> JoinAll<I::Item>
| -------- required by a bound in this function
...
105 | I::Item: Future,
| ^^^^^^ required by this bound in `join_all`
error[E0277]: `dyn Future<Output = ()>` cannot be unpinned
--> src/main.rs:46:9
|
46 | trpl::join_all(futures).await;
| ^^^^^^^^^^^^^^^^^^^^^^^ the trait `Unpin` is not implemented for `dyn Future<Output = ()>`, which is required by `Box<dyn Future<Output = ()>>: F
uture`
|
= note: consider using the `pin!` macro
consider using `Box::pin` if you need to access the pinned value outside of the current scope
= note: required for `Box<dyn Future<Output = ()>>` to implement `Future`
note: required by a bound in `futures_util::future::join_all::JoinAll`
--> /home/hector/.cargo/registry/src/rsproxy.cn-8f6827c7555bfaf8/futures-util-0.3.31/src/future/join_all.rs:29:8
|
27 | pub struct JoinAll<F>
| ------- required by a bound in this struct
28 | where
29 | F: Future,
| ^^^^^^ required by this bound in `JoinAll`
error[E0277]: `dyn Future<Output = ()>` cannot be unpinned
--> src/main.rs:46:33
|
46 | trpl::join_all(futures).await;
| ^^^^^ the trait `Unpin` is not implemented for `dyn Future<Output = ()>`, which is required by `Box<dyn Future<Output = (
)>>: Future`
|
= note: consider using the `pin!` macro
consider using `Box::pin` if you need to access the pinned value outside of the current scope
= note: required for `Box<dyn Future<Output = ()>>` to implement `Future`
note: required by a bound in `futures_util::future::join_all::JoinAll`
--> /home/hector/.cargo/registry/src/rsproxy.cn-8f6827c7555bfaf8/futures-util-0.3.31/src/future/join_all.rs:29:8
|
27 | pub struct JoinAll<F>
| ------- required by a bound in this struct
28 | where
29 | F: Future,
| ^^^^^^ required by this bound in `JoinAll`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `hello-async` (bin "hello-async" test) due to 3 previous errors
```
这些报错要消化的东西 *太多* 了,我们来将其拆开看看。报错消息的第一部分告诉我们,第一个异步代码块(`src/main.rs:8:23: 20:10`)未实现 `Unpin` 这个,并建议使用 `pin!``Box::pin` 解决这个问题。本章稍后,我们将深入探讨有关 `Pin``Unpin` 的更多细节。不过现在,我们可按照编译器的建议解决这个问题。在清单 17-18 中,我们首先从 `std::pin` 导入 `Pin`。接下来,我们更新 `futures` 的类型注解,用 `Pin` 封装每个 `Box`。最后,我们使用 `Box::pin` 固定各个未来值。
文件名:`src/main.rs`
```rust
use std::pin::Pin;
// -- snip --
let futures: Vec<Pin<Box<dyn Future<Output = ()>>>> =
vec![Box::pin(tx1_fut), Box::pin(rx_fut), Box::pin(tx_fut)];
```
*清单 17-18使用 `Pin` 及 `Box::pin` 令到 `Vec` 的类型检查通过*
在我们编译并运行这段代码时,我们最终得到了我们所希望的输出:
```console
received 'hi'
received 'more'
received 'from'
received 'the'
received 'messages'
received 'future'
received 'for'
received 'you'
```
哈!
这里还有一些要探讨的问题。其一,使用 `Pin<Box<T>>` 会增加因为我们需要将这些未来值,与 Box 一起放在内存堆上而带来的少量开销 -- 而这样做只是为了使类型保持一致(对齐)。毕竟,我们实际上 *不需要* 内存堆分配:这些未来值对于这个特定函数,属于一些局部的未来值。正如前面所提到的,`Pin` 本身就是一个封装类型,因此在无需进行内存堆分配下,我们就能获得在 `Vec` 中保存单一类型的好处 -- 这也是我们使用 `Box` 的初衷。使用 `std::pin::pin` 这个宏,我们可对各个未来值直接使用 `Pin`
但是我们仍然必须明确被固定引用的类型否则Rust 仍无法将这些引用解释为动态的特质对象,而这正是我们在 `Vec` 中所需要的。因此,我们将 `pin` 添加到 `std::pin` 导入的列表中。然后,在定义各个未来值时对其 `pin`,并将 `futures` 定义为包含到动态未来值类型的固定可变引用的一个 `Vec`,如下清单 17-19 所示。
文件名:`src/main.rs`
```rust
use std::pin::{Pin, pin};
// -- snip --
let tx1_fut = pin!(async move {
// --snip--
});
let rx_fut = pin!(async {
// --snip--
});
let tx_fut = pin!(async move {
// --snip--
});
let futures: Vec<Pin<&mut dyn Future<Output = ()>>> =
vec![tx1_fut, rx_fut, tx_fut];
```
*清单 17-19以 `pin!` 这个宏直接使用 `Pin`,避免一些不必要的内存堆分配*
我们之所以能做到这一点,是因为我们忽略了可能存在不同 `Output` 类型这一事实。例如,在下面的清单 17-20 中,`a` 的匿名未来值,实现了 `Future<Output = u32>``b` 的匿名未来值,则实现了 `Future<Output = &str>`,而 `c` 的匿名未来值,却实现了 `Future<Output = bool>`
文件名:`src/main.rs`
```rust
let a = async { 1u32 };
let b = async { "Hello!" };
let c = async { true };
let (a_result, b_result, c_result) = trpl::join! (a, b, c);
println!("{a_result}, {b_result}, {c_result}");
```
*清单 17-20有着不同类型的三个未来值*
我们可以使用 `trpl::join!` 等待他们,因为他允许我们传入多个未来值类型,并生成这些类型的元组。我们 *不能* 使用 `trpl::join_all`,因为他要求传入的所有未来值,都要有同一类型。请记住,正是这个报错,让我们开始了 `Pin` 下的冒险之旅!
这是种基本的权衡:我们可以 `join_all` 处理动态数量的未来值,只要他们都具有同一类型;或者使用 `join` 函数或 `join!` 宏处理固定数量的未来值,即使他们有着不同类型。这与我们处理 Rust 种其他类型时,所面临的情况是一样的。未来值并不特殊,即便如此我们也有一些处理他们的良好语法,这是件好事。
## 未来值之间的竞争
**Racing Futures**
当我们以 `join` 系列函数和宏,“连接” 一些未来值时,我们要求 *全部* 未来值都完成后才能继续。但有时,我们只需要集合中的 *某个* 未来值完成,然后就可以继续 -- 这有点类似于一个未来值与另一个未来值赛跑。
在下面的清单 17-21 中,我们再次使用 `trpl::race`,运行 `slow``fast` 两个未来值。
文件名:`src/main.rs`
```rust
let slow = async {
println!("'slow' started.");
trpl::sleep(Duration::from_millis(100)).await;
println!("'slow' finished.");
};
let fast = async {
println!("'fast' started.");
trpl::sleep(Duration::from_millis(50)).await;
println!("'fast' finished.");
};
trpl::race(slow, fast).await;
```
*清单 17-21使用 `race` 获得率先完成未来值的结果*
在其开始运行时,两个未来值都会打印一条消息,通过调用及等待 `sleep` 暂停一段时间,并在其结束时打印另一条消息。然后,我们将 `slow``fast` 都传递给 `trpl::race`,并等待其中一个完成。(这里的结果并不奇怪:`fast` 会赢得竞争。)与我们在 [“咱们的首个异步程序”](../async/futures.md#咱们的首个异步程序) 中,曾用到 `race` 时不同,这里我们简单地忽略了其返回的 `Either` 实例,因为所有感兴趣行为都发生在异步代码块的主体中。
请注意,若咱们将 `race` 的参数顺序颠倒一下,则尽管那个 `fast` 未来值总是先完成,“开始” 的信息顺序会改变。这是因为这个特殊的 `race` 函数实现并不公平。他总是按照参数传递的顺序,运行作为参数传入的未来值。别的实现 ** 公平的,他们将随机选择要先轮询哪个未来值。不管我们使用的竞赛函数实现是否公平,在另一任务开始前,未来值 *之一* 都会运行到该竞赛函数主体中的首个 `await` 时刻。
回顾 [“咱们的首个异步程序”](../async/futures.md#咱们的首个异步程序)在每个等待点处Rust 都会给到运行时一个暂停任务的机会,并在正等待的未来值尚未准备好时,就会切换到另一任务。反之亦然: Rust *只会* 暂停异步代码块,并在等待点处将控制权交还给运行时。等待点之间的一切,都是同步的。
这意味着,若咱们在某个没有等待点的异步代码块中,执行大量工作,那么这个未来值将阻塞全部别的未来值取得进展。有时咱们可能会听到这样的说法:一个未来值会让其他未来值 *饿死*。在某些情况下,这可能不是什么大问题。但是,若咱们正在进行某种昂贵的设置,或执行长期运行的工作,或者在咱们有个将无限期地执行某项特定任务的未来值时,咱们就需要考虑,何时何处将控制权交还给运行时。
以同样说法,若咱们有些长期运行的阻塞操作,那么异步就会是种为程序的不同部分,提供相互关联方法的有用工具。
但在这种情况下,咱们要 *如何* 将控制权,交还给运行时呢?
## 将控制权交给运行时
**Yielding Control to the Runtime**
我们来模拟一项长时间运行的操作。下面清单 17-22 引入了一个 `slow` 函数。
文件名:`src/main.rs`
```rust
fn slow(name: &str, ms: u64) {
thread::sleep(Duration::from_millis(ms));
println!("'{name}' ran for {ms}ms");
}
```
*清单 17-22使用 `thread::sleep` 模拟慢速操作*
这段代码使用了 `std::thread::sleep` 而非 `trpl::sleep`,因此调用 `slow` 会阻塞当前线程若干毫秒。我们可以使用 `slow` 代替现实世界中,那些既要长时间运行又会阻塞的操作。
在下面的清单 17-23 中,我们使用了 `slow`,模拟两个未来值中的这种 CPU 密集的工作。
文件名:`src/main.rs`
```rust
let a = async {
println!("'a' started.");
slow("a", 30);
slow("a", 10);
slow("a", 20);
trpl::sleep(Duration::from_millis(50)).await;
println!("'a' finished.");
};
let b = async {
println!("'b' started.");
slow("b", 75);
slow("b", 10);
slow("b", 15);
slow("b", 350);
trpl::sleep(Duration::from_millis(50)).await;
println!("'b' finished.");
};
trpl::race(a, b).await;
```
*清单 17-23使用 `thread:sleep` 模拟慢速操作*
首先,两个未来值都只会在执行了一系列慢速操作 **,才会将控制权交还给运行时。若咱们运行这段代码,咱们会看到这样的输出:
```console
'a' started.
'a' ran for 30ms
'a' ran for 10ms
'a' ran for 20ms
'b' started.
'b' ran for 75ms
'b' ran for 10ms
'b' ran for 15ms
'b' ran for 350ms
'a' finished.
```
正如咱们前面的示例那样,`race` 仍会在 `a` 完成后立即结束。不过,两个未来值间没有交错。在 `trpl::sleep` 调用被等待前,`a` 这个未来值会完成他的所有工作,然后在 `b` 自己的 `trpl::sleep` 调用被等待前,`b` 会完成他的所有工作,最后 `a` 这个未来值完成。为允许两个未来值在他们的慢速任务间都取得进展,我们就需要一些等待点,以便咱们将控制权交还给运行时。这意味着我们需要一些咱们可以等待的东西!
在清单 17-23 中,我们已经可以看到这种切换:若我们去掉 `a` 未来值结尾处的 `trpl::sleep`,他就会在 `b` 未来值 *完全* 未运行的情况下完成。我们来试着以 `sleep` 函数为起点,让两个这些操作在取得进展间切换,如下清单 17-24 中所示。
文件名:`src/main.rs`
```rust
let one_ms = Duration::from_millis(1);
let a = async {
println!("'a' started.");
slow("a", 30);
trpl::sleep(one_ms).await;
slow("a", 10);
trpl::sleep(one_ms).await;
slow("a", 20);
trpl::sleep(one_ms).await;
println!("'a' finished.");
};
let b = async {
println!("'b' started.");
slow("b", 75);
trpl::sleep(one_ms).await;
slow("b", 10);
trpl::sleep(one_ms).await;
slow("b", 15);
trpl::sleep(one_ms).await;
slow("b", 350);
trpl::sleep(one_ms).await;
println!("'b' finished.");
};
```
*清单 17-24使用 `sleep` 让操作在取得进展间切换*
在清单 17-24 中,我们在每次调用 `slow` 间,以等待点添加了一些 `trpl::sleep` 调用。现在,两个未来值的工作是交替的了:
```console
'a' started.
'a' ran for 30ms
'b' started.
'b' ran for 75ms
'a' ran for 10ms
'b' ran for 10ms
'a' ran for 20ms
'b' ran for 15ms
'a' finished.
```
在将控制权移交给 `b` 前,`a` 未来值仍会运行一段时间,因为他在调用 `trpl::sleep` 前调用了 `slow`,但在此之后,每当其中一个未来值到达等待点时,他们就会来回交换。在这个示例中,我们在每次调用 `slow` 后都这样做了,但我们也可以任何对咱们最合理的方式,分解两个未来值的工作。
不过,我们并不真的打算在这里 *睡眠*:我们要的是尽快取得进展。我们只需将控制权交还给运行时。使用 `yield_now` 函数,我们就可以直接做到这点。在下面的清单 17-25 中,我们就以 `yield_now`,取代了所有的 `sleep` 调用。
文件名:`src/main.rs`
```rust
let a = async {
println!("'a' started.");
slow("a", 30);
trpl::yield_now().await;
slow("a", 10);
trpl::yield_now().await;
slow("a", 20);
trpl::yield_now().await;
println!("'a' finished.");
};
let b = async {
println!("'b' started.");
slow("b", 75);
trpl::yield_now().await;
slow("b", 10);
trpl::yield_now().await;
slow("b", 15);
trpl::yield_now().await;
slow("b", 350);
trpl::yield_now().await;
println!("'b' finished.");
};
```
*清单 17-25使用 `yield_now` 让操作在取得进展间切换*
这段代码既能更清楚地表达实际意图,又能比使用 `sleep` 快很多,因为 `sleep` 用到的这种定时器,通常有着他们所能及的粒度限制。例如,我们正使用的 `sleep` 版本,将总是会至少睡眠一毫秒,即便我们给他的 `Duration` 是一纳秒。同样,现代计算机的速度 *很快*:他们可在一毫秒中完成很多事情!
通过设置一个小的基准测试,诸如清单 17-26 中的那个,咱们就可以亲自发现这点。(这并不是一种特别严格的性能测试方法,但其足以在此显示差异。)
文件名:`src/main.rs`
```rust
let one_ns = Duration::from_nanos(1);
let start = Instant::now();
async {
for _ in 1..1000 {
trpl::sleep(one_ns).await;
}
}
.await;
let time = Instant::now() - start;
println!(
"'sleep' version finished after {} seconds.",
time.as_secs_f32()
);
let start = Instant::now();
async {
for _ in 1..1000 {
trpl::yield_now().await;
}
}
.await;
let time = Instant::now() - start;
println!(
"'yield' version finished after {} seconds.",
time.as_secs_f32()
);
```
*清单 17-26比较 `sleep` 与 `yield_now` 的性能*
这里,我们跳过了所有状态打印,将一个一纳秒的 `Duration` 传递给 `trpl::sleep`,并让每个未来值自行运行,两个未来值间没有切换。然后我们运行 `1000` 次迭代,看看使用 `trpl::sleep` 的未来值,与使用 `trpl::yield_now` 的未来值相比时间长了多少。
`yield_now` 的版本,是更快的 *方式*
```console
'sleep' version finished after 1.0830415 seconds.
'yield' version finished after 0.000244098 seconds.
```
这意味着异步对那些计算密集任务也是有用的,这取决于咱们的程序在做什么,因为异步为构建程序不同部分之间的关系,提供了有用工具。这是一种 *合作式多任务处理*,其中每个未来值都有权决定,何时通过等待点移交控制权。因此,每个未来值也有责任避免阻塞过长时间。在一些基于 Rust 的嵌入式操作系统中,这是 *唯一* 的多任务处理方式!
当然,在实际代码中,你通常不会在每行代码上,以等待点在函数调用间交替。虽然以这种方式让渡控制权的代价相对较低,但并不免费。在很多情况下,试图中断某个计算密集的任务,可能令其明显变慢,因此有时让某项操作短暂阻塞,会更有利于 *整体* 性能。一定要测量咱们代码的具体性能瓶颈。不过,在咱们发现有很多工作是以串行方式进行的,而咱们预期的是以并发方式进行时,那么就必须注意底层动态了!
## 构建咱们自己的异步抽象
我们还可以将未来值组合在一起,创建处新的模式。例如,以咱们已有的异步构件,咱们可构建出一个 `timeout` 函数。当我们完成后,结果将是另一个我们可以用于创建出更多异步抽象的构建块(译注:异步构件)。
清单 17-27 显示了,这个 `timeout` 与某个慢速未来值一起使用时,咱们所期望的其工作方式。
文件名:`src/main.rs`
```rust
let slow = async {
trpl::sleep(Duration::from_millis(100)).await;
"I finished!"
};
match timeout(slow, Duration::from_millis(10)).await {
Ok(message) => println!("Succeeded with '{message}'"),
Err(duration) => {
println!("Failed after {} seconds", duration.as_secs())
}
}
```
*清单 17-27使用咱们设想的 `timeout`,在有限时间下运行某个慢速操作*
我们来实现这个异步构件!首先,我们来考虑一下 `timeout` 的 API
- 他本身需要是个异步函数,这样我们才能等待他;
- 他的第一个参数应是个要运行的未来值。我们可将其构造为可与任何未来值一起使用的通用函数;
- 其第二个参数将是要等待的最长时间。若我们使用一个 `Duration`,就将很容易将其传递给 `trpl::sleep`
- 他应返回一个 `Result`。在未来值成功完成时,这个 `Result` 将为带有该未来值所产生值的 `Ok`。在超时在先时,`Result` 将是带有超时等待时长的 `Err`
下面清单 17-28 展示了这一声明。
文件名:`src/main.rs`
```rust
async fn timeout<F: Future> (
future_to_try: F,
max_time: Duration,
) -> Result<F::Output, Duration> {
// Here is where our implementation will go!
}
```
*清单 17-28定义出 `timeout` 的签名*
这就满足了我们的类型目标。现在,我们来考虑一下我们需要的 *行为*:我们是要让传入的未来值,与传入的持续时间赛跑。我们可使用 `trpl::sleep`,从传入的持续时间构造出一个定时器的未来值,并使用 `trpl::race` 与调用者所传入的未来值一起,运行这个定时器。
我们还指导这个 `race` 是不公平的,他会按照参数传递的顺序轮询参数。因此,我们先将 `future_to_try` 传递给 `race`,这样即使 `max_time` 很短,他也有机会完成。在 `future_to_try` 首先完成时,`race` 将返回 `Left``future_to_try` 的输出。在 `timer` 首先完成时,`race` 将返回 `Right` 与定时器的输出 `()`
在下面的清单 17-29 中,我们匹配了等待 `trpl::race` 的结果。
文件名:`src/main.rs`
```rust
use trpl::Either;
// --snip--
fn main() {
trpl::run(async {
let slow = async {
trpl::sleep(Duration::from_secs(5)).await;
"Finally finished"
};
match timeout(slow, Duration::from_secs(2)).await {
Ok(message) => println!("Succeeded with '{message}'"),
Err(duration) => {
println!("Failed after {} seconds", duration.as_secs())
}
}
});
}
async fn timeout<F: Future>(
future_to_try: F,
max_time: Duration,
) -> Result<F::Output, Duration> {
match trpl::race(future_to_try, trpl::sleep(max_time)).await {
Either::Left(output) => Ok(output),
Either::Right(_) => Err(max_time),
}
```
*清单 17-29使用 `race` 与 `sleep` 定义 `timeout`*
`future_to_try` 成功时,我们会得到 `Left(output)`,我们就会返回 `Ok(output)`。若那个睡眠定时器超时,我们就得到 `Right(())`,我们以 `_` 忽略那个 `()` 并返回 `Err(max_time)`
这样,我们就从另外两个异步助手函数 `trpl::sleep``trpl::race`,创建出了个可工作的 `timeout`。在我们运行咱们的代码时,他会在超时后打印那个失败模式:
```console
Failed after 2 seconds
```
由于未来值可与其他未来值组合,因此咱们可以使用一些较小的异步构件,构建出真正强大的工具。例如,咱们可使用同样的方法,将超时与重试结合起来,进而将其用于诸如网络调用等的操作(本章开头的示例之一)。
在实践中,咱们通常会直接使用 `async``await`,其次是诸如 `join``join_all``race` 等函数与宏。现在咱们将只需偶尔用到 `pin`,就能在这些 API 中运用未来值。
现在,我们已经看到了同时处理多个未来值的数种方法。接下来,我们将了解如何使用 **,按时间顺序处理多个未来值。不过,咱们可能需要先考虑几件事:
- 我们使用了与 `join_all` 一起的一个 `Vec`,等待某个组中的所有未来值完成。那么咱们要如何使用一个 `Vec`,依次处理一组未来值呢?这样做有什么好处?
- 请查看 `futures` 代码箱中的 `futures::stream::FuturesUnordered` 类型。使用他与使用一个 `Vec` 有何不同? (不用担心该类型是来自于这个代码箱板中的 `stream` 部分;他对任何的未来值集合都能正常工作。)
End

494
src/async/streams.md Normal file
View File

@@ -0,0 +1,494 @@
# 流:序列中的未来值
**StreamsFutures in Sequence**
本章到目前为止,我们主要着重于单个的未来值。唯一例外是我们曾用到的异步通道。回顾在本章前面的 [“消息传递”](concurrency_n_async.md#使用消息传递在两个任务上计数) 小节,我们使用咱们异步通道接收器的方式。那个异步 `recv` 方法,会随着时间推移产生一个条目序列。这是种所谓 ** 的更通用模式的一个实例。
早在第 13 章 [“`Iterator` 特质与 `next` 方法”](../functional_features/iterators.md#iterator-特质与-next-方法) 小节,介绍 `Iterator` 这个特质时,我们就见到过条目序列,但迭代器与那个异步通道接收器,有两个不同点。第一个区别是时间:迭代器是同步的,而这个通道接收器是异步的。第二个区别是 API。在直接使用 `Iterator` 时,我们会调用其同步的 `next` 方法。而在这个特定的 `trpl::Receiver` 流下,我们调用了一个异步的 `recv` 方法。除此之外,这两个 API 感觉非常相似,而这种相似并非巧合。所谓流,就像迭代的一种异步形式。不过,尽管 `trpl::Receiver` 专门等待接收消息,单通用的流 API 则有着更广泛的范围:他以 `Iterator` 同样方式,提供下一项目,不过是异步地提供。
Rust 中迭代器与流之间的相似性,意味着我们实际上可以从任何迭代器,创建出一个流。与某个迭代器一样,通过调用某个流的 `next` 方法然后等待输出,我们就可以使用这个流,如下清单 17-30 所示。
文件名:`src/main.rs`
```rust
let values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let iter = values.iter().map(|n| n * 2);
let mut stream = trpl::stream_from_iter(iter);
while let Some(value) = stream.next().await {
println!("The value was: {value}");
}
```
*清单 17-30从某个迭代器创建出一个流并打印其值*
我们从一个数字的数组开始,将其转换为一个迭代器,然后调用 `map` 将所有值加倍。然后我们使用 `trpl::stream_from_iter` 函数,将这个迭代器转换为一个流。接下来,我们以那个 `while let` 循环,在该流中项目到达时遍历他们。
不幸的是,当我们尝试运行该代码时,他不会编译,而是报告没有 `next` 方法可用:
```console
error[E0599]: no method named `next` found for struct `tokio_stream::iter::Iter` in the current scope
--> src/main.rs:7:40
|
7 | while let Some(value) = stream.next().await {
| ^^^^
|
= help: items from traits can only be used if the trait is in scope
help: the following traits which provide `next` are implemented but not in scope; perhaps you want to import one of them
|
1 + use futures_util::stream::stream::StreamExt;
|
1 + use std::iter::Iterator;
|
1 + use std::str::pattern::Searcher;
|
1 + use trpl::StreamExt;
|
help: there is a method `try_next` with a similar name
|
7 | while let Some(value) = stream.try_next().await {
| ~~~~~~~~
```
正如此输出所解释的,这条编译器报错的原因,是我们需要在作用域中的正确特质,才能使用这个 `next` 方法。根据我们到目前为止的讨论,咱们可能会合理地认为,这个特质是 `Stream`,但他实际上是 `StreamExt``Ext``extension` 的缩写,是 Rust 社区中用于表示以一个特质,扩展另一特质的常见模式。
在本章末尾,我们将对 `Stream``StreamExt` 两个特质进行更详细的解释,但现在咱们只需知道,`Stream` 特质定义了个可有效地结合 `Iterator``Future` 特质的底层接口。而 `StreamExt` 则在 `Stream` 之上,提供了一组更高级别的 API包括 `next` 方法以及与由 `Iterator` 特质所提供的类似其他工具方法。`Stream``StreamExt` 还不是 Rust 标准库的一部分,但大多数生态代码箱,都使用了这同样的定义。
修复这个编译器报错的方法,就是添加一条 `trpl::StreamExt``use` 语句,如下清单 17-31 所示。
文件名:`src/main.rs`
```rust
use trpl::StreamExt;
fn main() {
trpl::run( async {
let values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let iter = values.iter().map(|n| n * 2);
let mut stream = trpl::stream_from_iter(iter);
while let Some(value) = stream.next().await {
println!("The value was: {value}");
}
});
}
```
*清单 17-31成功将某个迭代器用作一个流的基础*
将所有这些部分放在一起,这段代码就可以按照我们想要的方式运行了!此外,既然咱们在作用域中有了 `StreamExt`,我们就可以像使用迭代器一样,使用 `StreamExt` 的所有工具方法。例如,在下面的清单 17-32 中,我们就使用了 `filter` 方法,过滤掉了除 3 与 5 倍数之外的所有内容。
文件名:`src/main.rs`
```rust
use trpl::StreamExt;
fn main() {
trpl::run( async {
let values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let iter = values.iter().map(|n| n * 2);
let mut stream = trpl::stream_from_iter(iter);
let mut filtered =
stream.filter(|value| value % 3 == 0 || value % 5 == 0);
while let Some(value) = filtered.next().await {
println!("The value was: {value}");
}
});
}
```
*清单 17-32以 `StreamExt:filter` 方法过滤某个流*
当然,这并不是很有趣,因为我们可以用一般迭代器完成同样的事情,而根本不需要任何的异步。我们来看看我们能做些什么 ** 特有的事情。
## 合成流
**Composing Streams**
许多概念都可很自然地表示为流:某个队列中的项目成为可用;当整个数据集相对于内存来说太大时,从文件系统增量地拉取数据块;或者随着时间推移,数据经由网络到达等等。由于流是一些未来值,我们可以将他们与任何的其他类型未来值一起使用,并以有趣方式将他们结合。例如,我们可以批量处理事件,避免触发过多网络调用;对长期运行的操作序列设置超时;或为用户界面事件设置阈值,避免执行不必要的工作。
我们以创建一个小的消息流,作为咱们可能在 WebSocket 或其他实时通信协议中,所见到的数据流替身,如下清单 17-33 所示。
文件名:`src/main.rs`
```rust
use trpl::{ReceiverStream, Stream, StreamExt};
fn main() {
trpl::run(async {
let mut messages = get_messages();
while let Some(message) = messages.next().await {
println!("{message}");
}
});
}
fn get_messages() -> impl Stream<Item = String> {
let (tx, rx) = trpl::channel();
let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"];
for message in messages {
tx.send(format!("Message: '{message}'")).unwrap();
}
ReceiverStream::new(rx)
}
```
*清单 17-33将 `rx` 接收器,用作一个 `ReceiverStream`*
首先,我们创建了个返回 `impl Stream<Item = String>`,名为 `get_messages` 的函数。对于其实现,我们创建了个异步通通道,遍历英语字母表的前 10 个字母,并将他们发送到该通道上。
我们还使用了一种将 `trpl::channel` 中的 `rx` 接收器,转换为有着 `next` 方法流的新类型 `ReceiverStream`。回到 `main`,我们使用一个 `while let` 循环,打印出该流中的所有消息。
当我们运行这段代码时,我们会得到正是我们所期望的结果:
```console
Message: 'a'
Message: 'b'
Message: 'c'
Message: 'd'
Message: 'e'
Message: 'f'
Message: 'g'
Message: 'h'
Message: 'i'
Message: 'j'
```
同样,我们可以使用常规的 `Receiver` API或甚至常规的 `Iterator` API 完成这一点,不过,我们来添加一个需要流的功能:添加一个应用到流上每个项目的超时,以及一个我们发出项目上的延迟,如下清单 17-34 所示。
文件名:`src/main.rs`
```rust
use std::{pin::pin, time::Duration};
use trpl::{ReceiverStream, Stream, StreamExt};
fn main() {
trpl::run(async {
let mut messages =
pin!(get_messages().timeout(Duration::from_millis(200)));
while let Some(result) = messages.next().await {
match result {
Ok(message) => println!("{message}"),
Err(reason) => eprintln!("Problem: {reason:?}"),
}
}
})
}
```
*清单 17-34使用 `StreamExt::timeout` 方法,在流中项目上设置一个时间限制*
我们以使用来自 `StreamExt` 特质的 `timeout` 方法,为流添加超时开始。然后,我们更新那个 `while let` 循环的主体,因为该流现在会返回一个 `Result`。其中的 `Ok` 变种表示有消息及时到达了;`Err` 变种表示在有消息到达前超时了。我们对该结果进行 `match`,要么在成功接收消息时打印出该消息,否则打印一条超时的通知。最后,请注意在对消息应用超时后,我们会对其进行了固定,因为这个超时助手会产生一个需要被固定才能轮询的流。
不过,由于消息之间没有延迟,这个超时没有改变该程序的行为。我们来为咱们发送的信息,添加一个可变延迟,如下清单 17-35 所示。
文件名:`src/main.rs`
```rust
fn get_messages() -> impl Stream<Item = String> {
let (tx, rx) = trpl::channel();
trpl::spawn_task(async move {
let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"];
for (index, message) in messages.into_iter().enumerate() {
let time_to_sleep = if index % 2 == 0 { 100 } else { 300 };
trpl::sleep(Duration::from_millis(time_to_sleep)).await;
tx.send(format!("Message: '{message}'")).unwrap();
}
});
ReceiverStream::new(rx)
}
```
*清单 17-35在不将 `get_messages` 函数构造为异步函数下,以一个异步的延迟通过 `tx` 发送消息*
`get_messages` 中,我们对 `messages` 这个数组,使用了迭代器方法 `enumerate`,这样咱们就能获取到咱们正发送的每个项目索引及该项目本身。然后,我们对偶数索引的项目应用 100 毫秒的延迟,对奇数索引的项目应用 300 毫秒的延迟,模拟现实世界中我们可能见到的消息流不同延迟。由于我们的超时为 200 毫秒,这应该会影响到一半的消息。
要在 `get_messages` 函数中的消息间无阻塞地睡眠,我们需要用到异步。但是,我们不能让 `get_messages` 本身构造为一个异步函数,因为这样我们就会返回一个 `Future<Output = Stream<Item = String>>` 而不是 `Stream<Item=String>>`。调用者也将必须等待 `get_messages` 本身才能访问到该流。但请记住:给定未来值中的所有项目,都是线性发生的;并发会发生在未来值 *之间*。等待 `get_messages` 就将要求他,在返回这个接收器流前发送所有消息,包括每条消息间的睡眠延迟。因此,其中的超时将毫无用处。在该流本身中,不会有延迟;所有延迟都应在该流可用前发生。
相反,我们将 `get_messages` 作为一个返回流的常规函数,且咱们生成了个任务,处理其中异步的 `sleep` 调用。
> **注意**:以这种方式调用 `spawn_task` 会工作是因为我们已经设置了咱们的运行时若咱们未设置运行时其将造成一个运行时不可恢复错误a panic。其他译注异步运行时实现选择了不同折衷方法他们可能会生成一个新的运行时而避免这种不可恢复错误但最终会带来一些额外的开销或者他们会在没有到运行时的引用时简单地不提供生成任务的独立方法。请确保咱们清楚咱们的运行时选择了哪种折衷方法并据此编写代码
现在,我们的代码有了更有趣的结果。在每对信息之间,都有个 `Problem: Elapsed(())` 报错。
```console
Message: 'a'
Problem: Elapsed(())
Message: 'b'
Message: 'c'
Problem: Elapsed(())
Message: 'd'
Message: 'e'
Problem: Elapsed(())
Message: 'f'
Message: 'g'
Problem: Elapsed(())
Message: 'h'
Message: 'i'
Problem: Elapsed(())
Message: 'j'
```
超时并未阻止信息最终到达。我们仍能收到全部原始消息,因为我们的通道 *不受限制*:内存能容纳多少消息,通道就能容纳多少消息。若超时前消息没有到达,我们的流处理程序将考虑到这点,但当他再次轮询该流时,信息就可能已经到达了。
在需要时,咱们可通过使用更通用的其他类型通道或其他类型流,获得不同行为。我们来通过将一个时间间隔流与这个消息流结合,看看其中的一种实际应用。
## 合并流
**Merging Streams**
首先,我们来创建另一个,在咱们让他直接运行时,他将每毫秒发出一个条目的流。为简单起见,我们可使用 `sleep` 函数,以一定延迟发送一条消息,并将其与咱们在 `get_messages` 中使用的自通道创建出流的同样方法结合。不同的是,这次我们将发回已经历的时间间隔计数,因此返回类型将是 `impl Stream<Item = u32>`,我们可调用函数 `get_intervals`(参见清单 17-36
文件名:`src/main.rs`
```rust
fn get_intervals() -> impl Stream<Item = u32> {
let (tx, rx) = trpl::channel();
trpl::spawn_task(async move {
let mut count = 0;
loop {
trpl::sleep(Duration::from_millis(1)).await;
count += 1;
tx.send(count).unwrap();
}
});
ReceiverStream::new(rx)
}
```
*清单 17-36以一个每毫秒都将被发射的计数器创建出一个流*
我们以在该任务中定义一个 `count` 开始。(我们也可在该任务外定义它,但限制任何给定变量的作用域会更加清晰。)然后,我们创建了个无限循环。该循环的每次迭代,都会异步地休眠一毫秒,递增那个计数,然后将其发送到通道上。由于这都封装在由 `spawn_task` 创建的任务中,因此包括无限循环在内的所有内容,都将与运行时一起被清理。
在异步的 Rust 中,这种只会在整个运行时被破坏时,才结束的无限循环相当常见:许多程序都需要无限期地运行下去。在异步下,只要在该循环的每次迭代中,至少有一个等待点,就不会阻塞其他任何事情。
现在,回到咱们主函数的异步代码块,我们可以尝试合并 `messages``intervals` 两个流,如下清单 17-37 所示。
文件名:`src/main.rs`
```rust
let messages = get_messages().timeout(Duration::from_millis(200));
let intervals = get_intervals();
let merged = messages.merge(intervals);
```
*清单 17-37尝试合并 `messages` 与 `intervals` 两个流*
我们以调用 `get_intervals` 开始。然后,我们以 `merge` 方法,合并 `messages``intervals` 两个流,该方法会将多个流合并为一个,在任一来源流中项目可用时,就会立即产生项目,而不会强加任何特定顺序。最后,我们会对合并后的流,而不再对 `messages` 循环。
此刻,`messages``intervals` 二者都不需要固定或可变,因为二者都将被合并为单一的 `merged` 流。然而,这个到 `merge` 的调用不会编译!(`while let` 循环中的 `next` 调用也不会编译,不过我们会再讨论这个问题。)这是因为两个流的类型不同。`messages` 流的类型是 `Timeout<impl Stream<Item = String>>`,其中 `Timeout` 是实现了 `Stream` 的某个 `timeout` 调用的类型。`intervals` 流的类型为 `impl Stream<Item = u32>`。要合并这两个流,我们需要转换其中一个,以匹配另一个。我们将重新设计间隔流,因为消息流已经是我们想要的基本格式,并且必须要处理超时错误(请参阅清单 17-38
文件名:`src/main.rs`
```rust
let messages = get_messages().timeout(Duration::from_millis(200));
let intervals = get_intervals()
.map(|count| format!("Interval: {count}"))
.timeout(Duration::from_secs(10));
let merged = messages.merge(intervals);
let mut stream = pin!(merged);
```
*清单 17-38将 `intervals` 流的类型与 `messages` 流的类型对齐*
首先,我们可使用 `map` 这个辅助方法,将 `intervals` 转换为字符串。其次,我们需要与 `messages` 中的 `Timeout` 匹配。不过,由于我们实际上并不 *想要* `intervals` 的某个超时,因此我们可以创建一个比我们所使用的其他持续时间,更长的一个超时。在这里,我们使用 `Duration::from_secs(10)` 创建了一个 10 秒的超时。最后,我们需要将 `stream` 可变,这样 `while let` 循环的 `next` 调用,就可以遍历这个流,并将其固定,确保这样做是安全的。这样就 *差不多* 达到了我们需要的效果。其中一切都通过了类型检查。在咱们运行这个程序时,会有两个问题。首先,他永远不会停止!咱们需要用 `ctrl-c` 停止他。其次,英文字母的消息,会埋没在全部间隔计数器消息中间。
```console
--snip--
Interval: 38
Interval: 39
Interval: 40
Interval: 41
Interval: 42
Interval: 43
Interval: 44
Interval: 45
Interval: 46
Interval: 47
Message: 'a'
Interval: 48
Interval: 49
Interval: 50
--snip--
```
下面清单 17-39 给出了解决这最后两个问题的一种方式。
文件名:`src/main.rs`
```rust
let messages = get_messages().timeout(Duration::from_millis(200));
let intervals = get_intervals()
.map(|count| format!("Interval: {count}"))
.throttle(Duration::from_millis(100))
.timeout(Duration::from_secs(10));
let merged = messages.merge(intervals).take(20);
let mut stream = pin!(merged);
```
*清单 17-39使用 `throttle` 与 `take` 管理合并的两个流*
首先,我们在 `intervals` 上使用了 `throttle` 方法,这样他就不会淹没 `messages` 流。所谓 *节流*,是种限制函数调用速率的方式 -- 或者说,在本例中,就是限制对该流轮询的频率。每 100 毫秒轮询一次就可以了,因为我们的消息大概就是以这个频率到达的。
要限制我们从某个流中接受项目的数量,我们对 `merged` 流应用了 `take` 方法,因为我们要限制的是最终输出,而不仅仅是其中一个流或另一个流。
现在,当我们运行这个程序时,他会在从该流中拉取 20 个条目后停止,且间隔时间不会淹没消息。我们也不会得到 `Interval: 100``Interval: 200` 等消息,而是得到 `Interval: 1``Interval: 2` 等消息 -- 尽管我们的源流,可以每毫秒产生一个事件。这是因为其中的 `throttle` 调用,会产生一个封装了原始流的新流,这样原始流就只能以节流速率,而不是其自己 “原生” 速率被轮询。我们没有了一堆的我们选择忽略的那些未处理间隔消息。相反,我们从一开始,就不会产生出这些间隔消息!这是 Rust 未来值固有的 “懒惰” 再次发挥了作用,允许我们选择一些性能特性。
```console
Interval: 1
Message: 'a'
Interval: 2
Interval: 3
Problem: Elapsed(())
Interval: 4
Message: 'b'
Interval: 5
Message: 'c'
Interval: 6
Interval: 7
Problem: Elapsed(())
Interval: 8
Message: 'd'
Interval: 9
Message: 'e'
Interval: 10
Interval: 11
Problem: Elapsed(())
Interval: 12
```
我们还需要处理最后一件事:出错!对于这两个基于通道的流,当通道的一侧关闭时,`send` 调用可能会失败 -- 这只是运行时执行组成流的未来值方式的问题。到目前为止,通过调用 `unwrap`,我们忽略了这种可能性,但在行为完备的应用中,我们应该显式地处理这个出错,至少要结束循环,从而咱们不再尝试发送任何消息。下面清单 17-40 展示了一种简单的出错策略:打印出问题,然后从循环中 `break`
```rust
fn get_messages() -> impl Stream<Item = String> {
let (tx, rx) = trpl::channel();
trpl::spawn_task(async move {
let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"];
for (index, message) in messages.into_iter().enumerate() {
let time_to_sleep = if index % 2 == 0 { 100 } else { 300 };
trpl::sleep(Duration::from_millis(time_to_sleep)).await;
if let Err(send_error) = tx.send(format!("Message: '{message}'")) {
eprintln!("Cannot send message '{message}': {send_error}");
break;
}
}
});
ReceiverStream::new(rx)
}
fn get_intervals() -> impl Stream<Item = u32> {
let (tx, rx) = trpl::channel();
trpl::spawn_task(async move {
let mut count = 0;
loop {
trpl::sleep(Duration::from_millis(1)).await;
count += 1;
if let Err(send_error) = tx.send(count) {
eprintln!("Could not send interval {count}: {send_error}");
break;
};
}
});
ReceiverStream::new(rx)
}
```
*清单 17-40处理出错并关闭循环*
与往常一样,处理消息发送出错的正确方法各有不同;只要确保咱们有种策略就可以了。
既然我们已经看到了实践种的大量异步,那么我们来退后一步,深入探讨一下 Rust 用于实现异步的 `Future``Stream` 及其他关键特质的一些细节。
End

View File

@@ -0,0 +1,384 @@
# 控制测试以何种方式运行
就跟 `cargo run` 会编译代码并于随后运行得出的二进制程序一样,`cargo test` 也会以测试模式编译所编写的代码,并会运行得到的测试二进制程序。而由 `cargo test` 产生出的二进制程序默认行为即是以并行方式运行全部测试并在测试运行期间捕获输出阻止输出被显示出来以及令到与测试结果相关的输出更加易于阅读the default behavior of the binary produced by `cargo test` is to run all the tests in parallel and capture output generated during test runs, preventing the output from being displayed and making it easier to read the output related to the test results。不过这里是可以指定一些命令行选项来改变这种默认行为的。
一些命令行选项是介入到 `cargo test`,而一些则是介入所得到的测试二进制程序。在介入到 `cargo test` 的命令行参数之后,跟上分隔符 `--`,随后才是那些进到测试二进制程序的参数,以这样的方式把这两种类型的命令行参数区分开。运行 `cargo test --help`,就会显示出可在 `cargo test` 下使用的选项,而运行 `cargo test -- --help` 则会显示出可在分隔符之后使用的那些选项。
## 并行抑或连续地运行测试
**Running Tests in Parallel or Consecutively**
在运行多个测试时,这些测试默认使用线程以并行方式运行,意味着他们会运行得更快,而咱们也会迅速地得到反馈。由于这些测试是在同时运行的,因此就必须确保所编写的测试不会各自依赖,并依赖于任何共用的状态,包括某种共用环境,诸如当前工作目录或环境变量。
比如说,所编写的每个测试,都会运行一些在磁盘上创建名为 `test-output.txt` 的文件,并将某些数据写到那个文件的代码。随后各个测试就会读取那个文件中的数据,并就那个包含了某个特定值进行断言,这个断言的特定值在各个测试中是不同的。由于这些测试是在同一时间运行,某个测试就可能会在另一测试写入与读取这个文件期间,对该文件进行覆写。那么第二个测试随后就将并非由于代码不正确,而因为这些测试在并行运行期间,相互之间造成了影响而失败。一种解决办法,是确保各个测试写入到不同文件;另一种办法,就是以一次运行一个的方式,运行这些测试。
在不打算并行运行这些测试,或要对所用到线程数有更细粒度掌控时,就可以将 `--test-threads` 这个命令行标志,与打算使用的线程数目,发送给那个测试二进制程序。请看看下面这个示例:
```console
$ cargo test -- --test-threads=1
```
这里把测试线程数设置为了 `1`,这就告诉了该程序不要使用任何并行机制。使用一个线程运行这些测试,相比以并行方式运行他们,将耗费更长时间,但在这些测试共用了状态时,他们之间不会相互影响。
## 展示函数的输出
**Showing Function Output**
默认情况下在某个测试通过时Rust 的测试库会对任何打印到标准输出的内容加以捕获。比如,当在某个测试中调用 `println!` 且该测试通过时,就不会在终端中看到那个 `println!` 的输出;而只将看到表示该测试通过的那行。而在某个测试失败时,则会与失败消息的其余部分一起,看到任何打印到标准输出的内容,。
作为一个示例,下面清单 11-10 有着一个打印其参数值并返回 `10` 的弱智函数,以及一个会通过的测试与一个会失败的测试。
文件名:`src/lib.rs`
```rust
fn prints_and_returns_10(a: i32) -> i32 {
println! ("我得到了一个值 {}", a);
10
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn this_test_will_pass() {
let value = prints_and_returns_10(4);
assert_eq! (10, value);
}
#[test]
fn this_test_will_fail() {
let value = prints_and_returns_10(8);
assert_eq! (5, value);
}
}
```
*清单 11-10对一个调用了 `println!` 宏的函数的两个测试*
在以 `cargo test` 运行这两个测试时,就会看到以下的输出:
```console
$ cargo test lennyp@vm-manjaro
Compiling assert_demo v0.1.0 (/home/lennyp/rust-lang/assert_demo)
Finished test [unoptimized + debuginfo] target(s) in 0.38s
Running unittests src/lib.rs (target/debug/deps/assert_demo-504fa58455de23e3)
running 2 tests
test tests::this_test_will_pass ... ok
test tests::this_test_will_fail ... FAILED
failures:
---- tests::this_test_will_fail stdout ----
我得到了一个值 8
thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left == right)`
left: `5`,
right: `10`', src/lib.rs:19:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::this_test_will_fail
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass '--lib'
```
请留意此输出中没有在哪里看到 `我得到了一个值 4`,这正是在通过的那个测试运行时所打印出的内容。那个输出就已被捕获了。而来自失败了的那个测试的输出,`我得到了一个值 8`,出现在了该测试的总结输出小节中,这个测试总结输出小节,还给出了该测试失败的原因。
在想要同样看到已通过测试的那些打印值时,就可以使用 `--show-output` 命令行开关,告诉 Rust 还要显示成功测试的输出。
```console
$ cargo test -- --show-output
```
在使用 `--show-output` 命令行开关再次运行清单 11-10 中的那些测试时,就会看到下面的输出:
```console
$ cargo test -- --show-output lennyp@vm-manjaro
Compiling assert_demo v0.1.0 (/home/lennyp/rust-lang/assert_demo)
Finished test [unoptimized + debuginfo] target(s) in 0.41s
Running unittests src/lib.rs (target/debug/deps/assert_demo-504fa58455de23e3)
running 2 tests
test tests::this_test_will_fail ... FAILED
test tests::this_test_will_pass ... ok
successes:
---- tests::this_test_will_pass stdout ----
我得到了一个值 4
successes:
tests::this_test_will_pass
failures:
---- tests::this_test_will_fail stdout ----
我得到了一个值 8
thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left == right)`
left: `5`,
right: `10`', src/lib.rs:19:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::this_test_will_fail
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass '--lib'
```
## 依据名字运行测试的某个子集
**Running a Subset of Tests by Name**
在有的时候,运行一整个的测试套件可能要用很长时间。而当在某个特定方面编写代码时,就会想要只运行与正在编写代码有关的那些测试。通过将想要运行的某个或某些测试的名字,作为参数传递给 `cargo test`,就可以对想要运行哪些测试加以选择。
为了演示怎样运行测试子集,这里将首先为所编写的 `add_two` 函数,创建三个测试,如下清单 11-11 中所示,并会选择要运行哪些测试。
文件名:`src/lib.rs`
```rust
pub fn add_two(a: i32) -> i32 {
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_two_and_two() {
assert_eq! (4, add_two(2));
}
#[test]
fn add_three_and_two() {
assert_eq! (5, add_two(3));
}
#[test]
fn one_hundred() {
assert_eq! (102, add_two(100));
}
}
```
*清单 11-11有着不同名字的三个测试*
如同早先所看到的那样,在不带传递任何参数运行这些测试时,全部这些测试将以并行方式运行:
```console
$ cargo test lennyp@vm-manjaro
Compiling assert_demo v0.1.0 (/home/lennyp/rust-lang/assert_demo)
Finished test [unoptimized + debuginfo] target(s) in 0.43s
Running unittests src/lib.rs (target/debug/deps/assert_demo-504fa58455de23e3)
running 3 tests
test tests::add_three_and_two ... ok
test tests::add_two_and_two ... ok
test tests::one_hundred ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests assert_demo
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
```
### 运行单个测试
**Running Single Tests**
可将任何测试函数的名字,传递给 `cargo test` 来只运行那个测试:
```console
$ cargo test one_hundred lennyp@vm-manjaro
Compiling assert_demo v0.1.0 (/home/lennyp/rust-lang/assert_demo)
Finished test [unoptimized + debuginfo] target(s) in 0.37s
Running unittests src/lib.rs (target/debug/deps/assert_demo-504fa58455de23e3)
running 1 test
test tests::one_hundred ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
```
上面就只有那个名字为 `one_hundred` 的测试运行了;其他两个测试并不与那个指定的名字匹配。而这个测试输出,通过末尾出显示出的 `2 filtered out`,而让咱们获悉有更多测试并未运行。
以这种方式是没法指定多个测试的名字的;只有给到 `cargo test` 的第一个值,才会被用到。不过是有方法来运行多个测试的。
### 使用过滤来运行多个测试
**Filtering to Run Multiple Tests**
这里可指定某个测试函数名字的一部分,那么名字与所指定值匹配的全部测试,就都会被运行。比如,由于上面的那些测试中有两个测试的名字包含了 `add`,因此这里就可以通过运行 `cargo test add`,运行这两个测试:
```console
$ cargo test add lennyp@vm-manjaro
Finished test [unoptimized + debuginfo] target(s) in 0.00s
Running unittests src/lib.rs (target/debug/deps/assert_demo-9c28057969510af5)
running 2 tests
test tests::add_three_and_two ... ok
test tests::add_two_and_two ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
```
此命令运行了名字中有 `add` 字样的全部测试,并将那个名为 `one_hundred` 的测试给过滤掉了。还要留意到,测试所出现在的模组,成为了该测试名字的一部分,因此就可通过以模组名字来过滤,而运行某个模组中的全部测试。
## 除非有特别要求,否则忽略某些测试
**Ignoring Some Tests Unless Specifically Requested**
有的时候少数几个特定测试,执行起来可能非常耗费时间,那么就会打算在绝大多数 `cargo test` 运行期间,将这些测试排除掉。与将全部想要运行的测试列为参数不同,这里是可以将那些耗费时间的测试,使用 `ignore` 属性进行注解,而将他们排除,如下所示:
文件名:`src/lib.rs`
```rust
pub fn add_two(a: i32) -> i32 {
a + 2
}
pub fn nth_fibonacci(n: u64) -> u64 {
if n == 0 || n == 1 {
return n;
} else {
return nth_fibonacci(n - 1) + nth_fibonacci(n - 2);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_two_and_two() {
assert_eq! (4, add_two(2));
}
#[test]
fn add_three_and_two() {
assert_eq! (5, add_two(3));
}
#[test]
fn one_hundred() {
assert_eq! (102, add_two(100));
}
#[test]
fn it_works() {
assert_eq! (2 + 2, 4);
}
#[test]
#[ignore]
fn expensive_test() {
assert_ne! (100, nth_fibonacci(50));
}
}
```
这里在 `#[test]` 之后,把那行 `#[ignore]` 添加到了打算排除的那个测试之上。此时再运行这些测试时,原来的三个测试会运行,但 `expensive_test` 就不会运行:
```console
$ cargo test lennyp@vm-manjaro
Compiling assert_demo v0.1.0 (/home/lennyp/rust-lang/assert_demo)
Finished test [unoptimized + debuginfo] target(s) in 0.46s
Running unittests src/lib.rs (target/debug/deps/assert_demo-9c28057969510af5)
running 5 tests
test tests::expensive_test ... ignored
test tests::add_two_and_two ... ok
test tests::one_hundred ... ok
test tests::it_works ... ok
test tests::add_three_and_two ... ok
test result: ok. 4 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests assert_demo
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
```
那个 `expensive_test` 函数就被列为了 `ignored`。而再打算只运行那些忽略的测试时,则可以使用 `cargo test -- --ignored`
```console
$ cargo test -- --ignored lennyp@vm-manjaro
Finished test [unoptimized + debuginfo] target(s) in 0.00s
Running unittests src/lib.rs (target/debug/deps/assert_demo-9c28057969510af5)
running 1 test
test tests::expensive_test has been running for over 60 seconds
test tests::expensive_test ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 4 filtered out; finished in 124.65s
Doc-tests assert_demo
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
```
经由控制哪些测试运行,就可以确保 `cargo test` 快速得出结果。在对那些 `ignored` 测试结果进行检查是有意义的,且有时间等待他们的结果出来时,那么就可以运行 `cargo test -- --ignored`。在打算运行全部测试,而不管他们有没有被注解为 `ignored`,那么就可以运行 `cargo test -- --include-ignored`
```console
$ cargo test -- --include-ignored lennyp@vm-manjaro
Finished test [unoptimized + debuginfo] target(s) in 0.00s
Running unittests src/lib.rs (target/debug/deps/assert_demo-9c28057969510af5)
running 5 tests
test tests::add_two_and_two ... ok
test tests::add_three_and_two ... ok
test tests::it_works ... ok
test tests::one_hundred ... ok
test tests::expensive_test has been running for over 60 seconds
test tests::expensive_test ... ok
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 130.68s
Doc-tests assert_demo
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
```
End

View File

@@ -0,0 +1,797 @@
# 怎样编写测试
所谓测试是指一些验证非测试代码the non-test code以预期方式发挥作用的函数tests are Rust functions that verify that the non-test code is functioning in the expected manner。测试函数的函数体通常执行以下三种操作
1. 建立起全部所需的数据或状态;
2. 运行打算测试的代码;
3. 就运行结果是所期望的结果进行断言assert the results are what you expect
下面就来看看Rust 专为编写进行这些操作的测试,而提供到一些特性,包括 `test` 属性the `test` attribute、几个宏以及 `should_panic` 属性the `should_panic` attribute
## 测试函数剖析
**The Anatomy of a Test Function**
Rust 最简单形态的测试,就是以 `test` 属性注解的一个函数。所谓属性,是指有关 Rust 代码片段的元数据attributes are metadata about pieces of Rust code在第 5 章中,[用在结构体上的 `derive` 属性](Ch05_Using_Structs_to_Structure_Related_Data.md#使用派生特质加入有用功能),就是一个属性的例子。要将某个函数修改为测试函数,就要把 `#[test]` 添加在 `fn` 之前的行上。在以 `cargo test` 命令运行编写的测试时Rust 就会构建一个运行这些注解过的函数并就各个测试函数是否通过或失败进行汇报的测试运行器二进制文件a test runner binary
每当用 Cargo 构造了一个新的库项目时,就会自动生成有着一个测试函数的测试模组。该模组给到了编写测试的模板,如此以来,就不必在每次开始新项目时,去找寻确切的测试结构及语法了。而至于要添加多少个额外测试函数与测试模组,则取决于咱们自己!
在对代码进行具体测试之前这里将通过进行模板测试the template test下的试验来探索测试工作原理的一些方面。随后就会编写一些对之前曾编写的代码进行调用并就这些代码有着正确行为进行断言的、真实世界中的测试。
先来创建一个名为 `adder`、把两个数字相加的新库项目:
```console
$ cargo new adder --lib lennyp@vm-manjaro
Created library `adder` package
$ cd adder
```
`adder` 库中 `src/lib.rs` 文件的内容,应看起来如清单 11-1 所示。
文件名:`src/lib.rs`
```rust
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
```
*清单 11-1由 `cargo new` 自动生成的测试模组与函数*
至于现在,就要忽略顶部的两行,并着重于那个函数。请注意那个 `#[test]` 注解:此属性表示这是个测试函数,由此测试运行器就知道将这个函数,当作一个测试对待。在那个 `tests` 模组中,可能也会有一些非测试函数,来帮助建立一些常见场景或执行一些常见操作,因此就需要表明哪些函数是测试。
这个示例函数的函数体,使用了 `assert_eq!` 宏,来对包含了 `2``2` 的结果 `result` 等于 `4` 进行断言。该断言是作为一个典型测试的格式示例,而提供的。下面就来运行他,来看到该测试会通过。
`cargo test` 命令会运行此项目中的全部测试,如下清单 11-2 所示。
```console
$ cargo test 1m 48s lennyp@vm-manjaro
Compiling adder v0.1.0 (/home/lennyp/rust-lang/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.58s
Running unittests src/lib.rs (target/debug/deps/adder-3985394b39347736)
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
```
*清单 11-2运作这个自动生成测试的输出*
Cargo 编译并运行了这个测试。这里看到那行 `running 1 test`。接下来的行就给出了那个自动生成测试函数的名字,名为 `it_works`,以及运行那个测试的结果为 `ok`。整体结论 `test result: ok.` 就表示全部测试都通过了,而后面的 `1 passed; 0 failed` 的部分,则对通过与未通过的测试数据,做了合计。
将某个测试标记为忽略,进而其在特定实例中不运行,是可能的;在本章后面的 ["忽视某些在特别要求下才运行的测试Ignoring Some Tests Unless Specifically Requested"](#在未作特别要求时忽略某些测试) 小节,就会讲到这个问题。由于这里尚未完成这个问题,因此这里的测试总结,就给出了 `0 ignored`。这里还可以把一个参数,传递给这个 `cargo test` 命令,来只测试那些名字与某个字符串匹配的测试;此特性叫做 *过滤filtering*,在 [“通过指定测试名字运行测试子集Running a Subset of Tests](#依据测试名称来运行测试的某个子集) 小节,就会讲到这个问题。而这里也没有对所运行的测试加以过滤,因此在该测试小结的最后,显示了 `0 filtered out`
其中属于基准测试的 `0 measured` 统计值对性能进行了测量。所谓基准测试benchmark tests就跟其字面意思一样只在每日构建版的 Rust 中可用。请参阅 [基准测试相关文档](https://doc.rust-lang.org/unstable-book/library-features/test.html) 了解更多信息。
测试输出接下来的部分,是以 `Doc-tests adder` 开始的,在有文档测试时,这便是文档测试的输出。虽然目前尚无文档测试,当 Rust 是可以编译在 API 文档中的全部代码示例的。此特性有助于将文档与代码保持同步!在第 14 章的 [“作为测试的文档注释Documentation Comments as Tests](Ch14_More_about_Cargo_and_Crates_io.md#作为测试的文档注释) 小节,就会讨论怎样编写文档测试。至于现在,就会这个 `Doc-tests` 的输出加以忽略。
接下来开始将该测试,定制为咱们自己所需的样子。首先将其中的 `it_works` 函数的名字,修改到某个别的名字,比如 `exploration`,像下面这样:
文件名:`src/lib.rs`
```rust
#[cfg(test)]
mod tests {
#[test]
fn exploration() {
assert_eq! (2 + 2, 4);
}
}
```
随后再度运行 `cargo test`。其输出此时就给出了 `exploration` 而非 `it_works`
```console
$ cargo test lennyp@vm-manjaro
Compiling adder v0.1.0 (/home/lennyp/rust-lang/adder)
Finished test [unoptimized + debuginfo] target(s) in 1.64s
Running unittests src/lib.rs (target/debug/deps/adder-3985394b39347736)
running 1 test
test tests::exploration ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
```
现在就要添加另一测试,但这次将构造一个会失败的测试!测试是在测试函数中的某个东西发生终止运行时,才失败的。每个测试都是运行在一个新线程中,并在主线程发现某个测试线程死去时,该测试就被标记为失败了。在第 9 章中,就讲到引发代码终止运行的最简单方式,即为调用 `panic!` 这个宏。请敲入一个名为 `another` 函数的新测试,那么这个 `src/lib.rs` 看起来就如同下面清单 11-3 这样。
文件名:`src/lib.rs`
```rust
#[cfg(test)]
mod tests {
#[test]
fn exploration() {
assert_eq! (2 + 2, 4);
}
#[test]
fn another() {
panic! ("令该测试失败");
}
}
```
使用 `cargo test` 再度运行这些测试。其输出看起来应如同清单 11-4 那样,显示这里的 `exploration` 测试通过而 `another` 失败了。
```console
$ cargo test lennyp@vm-manjaro
Compiling adder v0.1.0 (/home/lennyp/rust-lang/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.42s
Running unittests src/lib.rs (target/debug/deps/adder-3985394b39347736)
running 2 tests
test tests::exploration ... ok
test tests::another ... FAILED
failures:
---- tests::another stdout ----
thread 'tests::another' panicked at '令该测试失败', src/lib.rs:15:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::another
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass '--lib'
```
*清单 11-4在一项测试通过而一项测试失败时的测试输出*
这里不再是 `ok` 了,`test tests::another` 那行给出了 `FAILED`。在这单独结果与测试小结直接,出现了两个新的部分:第一部分显示各个测试失败的具体原因。在此示例中,就得到 `another` 失败详情,是由于该测试函数在 `src/lib.rs` 文件第 15 行处 `panicked at '令该测试失败'`。接下来的部分,则列出了仅所有失败测试的名字,这在有很多测试,进而有很多详细失败测试输出时,是有用的。随后就可以使用某个失败测试的名字,来只运行该项测试而更容易地对其加以调试;在 [“对测试运行方式进行控制Controlling How Tests Are Run](#控制测试以何种方式运行) 小节,将对运行测试方式,进行深入讲解。
显示在最后的测试小节行:总体上看,这个测试的结果为 `FAILED`。这里有一个测试通过,以及一个测试失败了。
既然现在已经见识了不同场景下测试结果的样子,那么就来看看在测试中,除 `panic!` 之外其他一些有用的宏。
## 使用 `assert!` 宏,检查测试结果
**Checing Results with the `assert!` Macro**
这个由标准库提供的 `assert!` 宏,在想要确保测试中某些情形求值为 `true` 时,是有用的。要给到这个 `assert!` 宏,一个求值为布尔值的参数。在求得的值为 `true` 时,就什么也不会发生,同时该测试通过。而在求得的值为 `false` 时,那么这个 `assert!` 宏就会调用 `panic!` 来造成该测试失败。使用这个 `assert!` 宏,有助于检查所编写代码,是以所计划方式运作。
在第 5 章的清单 5-15 中,用到了一个 `Rectangle` 结构体,以及一个 `can_hold` 方法,下面清单 11-5 中重复了那段代码。下面就将这段代码放在 `src/lib.rs` 文件,随后就要使用 `assert!` 宏为其编写一些测试。
文件名:`src/lib.rs`
```rust
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
(self.width > other.width && self.height > other.height) || (self.width > other.height && self.height > other.width)
}
}
```
*清单 11-5使用第 5 章的 `Rectangle` 结构体及其 `can_hold` 方法*
这个 `can_hold` 方法返回的是个布尔值,这就表示他是个 `assert!` 宏的绝佳用例。在下面清单 11-6 中,这里经由创建一个有着宽为 `8` 高为 `7``Rectangle` 实例,并断言其可装下另一个宽为 `5` 高为 `1``Rectangle` 实例,而编写了一个对该 `can_hold` 方法进行检查的测试。
文件名:`src/lib.rs`
```rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn larger_can_hold_smaller() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert! (larger.can_hold(&smaller));
}
}
```
*清单 11-6`can_hold` 的一个检查较大矩形是否能够真正包含较小矩形的测试*
请注意这里在 `tests` 模组里头添加了个新行:`use super::*;`。这个 `tests` 模组是个遵循第 7 章中,[“用于指向模组树中某个项目的路径”](Ch07_Managing_Growing_Projects_with_Packages_Crates_and_Modules.md#用于引用目录树中项目的路径)小节中曾讲到一般可见性规则的常规模组。由于这个 `tests` 模组是个内部模组,因此这里就需要将外层模组中的受测试代码,带入到这个 `tests` 内部模组的作用域。而由于这里使用了一个全局通配符a glob, `*`),因此所有在外层模组中定义的内容,就对这个 `tests` 模组可用了。
这里已将这个测试命名为了 `larger_can_hold_smaller`,并创建除了所需的两个 `Rectanble` 实例。随后就调用了 `assert!` 宏,并将调用 `larger.can_hold(&smaller)` 的结果传递给了他。这个表达式应返回 `true`,因此这个测试将通过。那么就来试试看吧!
```console
$ cargo test lennyp@vm-manjaro
Compiling assert_demo v0.1.0 (/home/lennyp/rust-lang/assert_demo)
Finished test [unoptimized + debuginfo] target(s) in 0.37s
Running unittests src/lib.rs (target/debug/deps/assert_demo-504fa58455de23e3)
running 1 test
test tests::larger_can_hold_smaller ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests assert_demo
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
```
这个测试真的通过了!接下来添加另一个测试,这次就断言某个较小矩形,无法装下一个较大矩形:
文件名:`src/lib.rs`
```rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn larger_can_hold_smaller() {
// --跳过代码--
}
#[test]
fn smaller_cannot_hold_larger() {
let larger = Rectangle {
width: 4,
height: 9,
};
let smaller = Rectangle {
width: 8,
height: 3,
};
assert! (!smaller.can_hold(&larger));
}
}
```
由于此情形下的 `can_hold` 正确结果为 `false`,因此就需要在将该结果传递给 `assert!` 宏之前,对其取反。而作为测试结果,在 `can_hold` 返回 `false` 时,这个测试就会通过:
```console
$ cargo test lennyp@vm-manjaro
Compiling assert_demo v0.1.0 (/home/lennyp/rust-lang/assert_demo)
Finished test [unoptimized + debuginfo] target(s) in 0.37s
Running unittests src/lib.rs (target/debug/deps/assert_demo-504fa58455de23e3)
running 2 tests
test tests::smaller_cannot_hold_larger ... ok
test tests::larger_can_hold_smaller ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests assert_demo
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
```
两个测试均通过了现在来看看在将一个代码错误a bug引入这里的代码时这里的测试结果将发生什么。这里会通过在比较两个矩形宽时将大于符号替换为小于符号而对 `can_hold` 方法的实现加以修改:
```rust
// --跳过代码--
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
(self.width < other.width && self.height > other.height) ||
(self.width < other.height && self.height > other.width)
}
}
```
现在运行这些测试,就会生成下面的输出:
```console
$ cargo test lennyp@vm-manjaro
Compiling assert_demo v0.1.0 (/home/lennyp/rust-lang/assert_demo)
Finished test [unoptimized + debuginfo] target(s) in 0.37s
Running unittests src/lib.rs (target/debug/deps/assert_demo-504fa58455de23e3)
running 2 tests
test tests::larger_can_hold_smaller ... FAILED
test tests::smaller_cannot_hold_larger ... ok
failures:
---- tests::larger_can_hold_smaller stdout ----
thread 'tests::larger_can_hold_smaller' panicked at 'assertion failed: larger.can_hold(&smaller)', src/lib.rs:29:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::larger_can_hold_smaller
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass '--lib'
```
这些测试就捕获到了代码错误the bug由于 `larger.width``8``smaller.width``5`,那么在 `can_hold` 方法中宽的比较现在就会返回 `false`: `8` 不比 `5` 小。
## 使用 `assert_eq!` 与 `assert_ne!` 两个宏,测试相等性
**Testing Equality with the `assert_eq!` and `assert_ne!` Macros**
对功能进行验证的一种常见方式,便是对测试之前代码的输出结果,与所期望的代码返回值之间是否相等进行测试。使用 `assert!` 宏并将一个使用了 `==` 运算符的表达式传递给他,就可完成这样的测试。然而由于这是一个如此常见的测试,以致标准库提供了一对宏 -- `assert_eq!``assert_ne!` -- 来更方便地执行这样的测试。这两个宏分别比较两个参数的相等与不相等。在断言失败时,他们还会打印出那两个值,这就令到发现 *为何* 测试失败,更为容易了;与之相反,`assert!` 宏则只表明他收到了那个 `==` 表达式的 `false` 值,而没有将导致那个 `false` 值的两个值打印出来的功能。
在下面清单 11-7 中,就编写了一个名为 `add_two`、将 `2` 加到其参数的函数,随后使用 `asset_eq!` 宏对这个函数进行了测试。
文件名:`src/lib.rs`
```rust
pub fn add_two(a: i32) -> i32 {
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_adds_two() {
assert_eq! (4, add_two(2));
}
}
```
*清单 11-7使用 `assert_eq!` 宏对函数 `add_two` 进行测试*
下面就来看看,他通过了测试!
```console
$ cargo test lennyp@vm-manjaro
Compiling assert_demo v0.1.0 (/home/lennyp/rust-lang/assert_demo)
Finished test [unoptimized + debuginfo] target(s) in 0.56s
Running unittests src/lib.rs (target/debug/deps/assert_demo-504fa58455de23e3)
running 1 test
test tests::it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests assert_demo
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
```
这里将 `4` 作为参数传递给了 `assert_eq!`,这与调用 `add_two(2)` 的结果相等。该测试的那一行就是 `tests::it_adds_two ... ok`,而文本 `ok` 就表明这个测试通过了!
接下来将一个 bug 引入到这里的代码,看看在 `assert_eq!` 失败时,会是什么样子。将这个 `add_two` 函数的实现修改为加 `3`
```rust
pub fn add_two(a: i32) -> i32 {
a + 3
}
```
在此运行这些测试the tests
```console
$ cargo test lennyp@vm-manjaro
Compiling assert_demo v0.1.0 (/home/lennyp/rust-lang/assert_demo)
Finished test [unoptimized + debuginfo] target(s) in 0.54s
Running unittests src/lib.rs (target/debug/deps/assert_demo-504fa58455de23e3)
running 1 test
test tests::it_adds_two ... FAILED
failures:
---- tests::it_adds_two stdout ----
thread 'tests::it_adds_two' panicked at 'assertion failed: `(left == right)`
left: `4`,
right: `5`', src/lib.rs:11:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::it_adds_two
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass '--lib'
```
这里的测试捕获到了那个 bug其中的 `it_adds_two` 测试就失败了,同时这些消息讲到,失败的断言为 ``assert failed: `(left == right)` ``,以及其中 `left` 与 `right` 的值分别为何。该消息有助于发起调试:那个 `left` 参数为 `4`,而那个 `right` 参数,即放上 `add_two(2)` 的那个,为 `5`。那么这里就可以联想到,当有很多测试在进行时,这一点就会尤其有帮助了。
请注意在某些语言与测试框架中,相等断言函数的那两个参数,分别叫做 `expected` 与 `actual`,且指定这两个参数的顺序是至关重要的。不过在 Rust 中,他们则分别叫做 `left` 与 `right`,且在指定所期望值与代码产生值的顺序,并不重要。这里可将该断言写作 `assert_eq! (add_two(2), 4)`,这仍会导致这个显示出 ``assertion failed: `(left == right)` `` 的同样失败消息。
而 `assert_ne!` 宏则将在给到其两个不相等值时通过测试,在两个值相等时测试失败。对于在不确定某个值是什么,但却清楚该值明显不会为何时的各种情形,这个宏就是最有用的。比如,在对某个确切会以某种方式修改其输入的函数进行测试,而修改方式会根据具体每周的哪一天运行该测试发生改变时,那么加以断言的最佳事物,就会是该函数的输出,与其输入不相等。
表象之下,`assert_eq!` 与 `assert_ne!` 两个宏,分别使用了运算符 `==` 与 `!=`。在他们的断言失败时这两个宏就会使用调试格式化debug formatting将他们的参数打印出来这就意味着正被比较的两个值必须实现了 `PartialEq` 与 `Debug` 特质。全部原生值与绝大多数的标准库类型,都实现了这两个特质。而对于咱们自己定义的结构体与枚举,就需要实现 `PartialEq` 来对这些类型的相等与否进行断言。同样还需要实现 `Debug`,来在断言失败时打印比较的两个值。由于这两个特质都正如第 5 章清单 5-12 中所提到的派生特质derivable traits这样就跟将 `#[derive(PartialEq, Debug)]` 注解,添加到所编写的结构体或枚举定义一样直接了。请参阅附录 C[“可派生特质derivable traits”](Ch21_Appdendix.md#附录-c派生特质) 了解更多有关这两个及其他派生特质的详细信息。
## 添加定制的失败消息
**Adding Custom Failure Message**
还可将与失败消息一同打印的定制消息,作为 `assert!`、`assert_eq!` 及 `assert_ne!` 宏的可选参数加入进来。在必须的两个参数之后指定的全部参数,都被传递给他们中的 `format!` 宏(第 8 章中 [“以 `+` 操作符或 `format!` 宏的字符串连接Concatenation with the `+` Operator or the `format!` macro”](Ch08_Common_Collections.md#使用--运算符或-format-宏的字符串连接) 小节曾讲到),因此就可以传递一个包含了 `{}` 占位符的格式化字符串,以及进到这些占位符的值。对于给某个断言表示什么的文档编制,这些定制消息就是有用的;在某个测试失败时,就会有着该代码下那个问题的较好理解。
比如说,这里有个按照名字来打招呼的函数,并打算就传入到该函数的名字有出现在输出中进行测试:
文件名:`src/lib.rs`
```rust
pub fn greeting(name: &str) -> String {
format! ("你好,{}", name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn greeting_contains_name() {
let result = greeting("Lenny");
assert! (result.contains("Lenny"));
}
}
```
该程序的各项要求尚未达成一致,同时这里十分肯定问候开始处的 `你好` 文字将会改变。这里已经确定不打算在各项要求改变时,必定要对这个测试加以更新,因此这里将只就输出包含输出参数的文本进行断言,而非对自 `greeting` 函数返回的值,进行精确的相等检查。
下面就来通过把 `greeting` 修改未排除 `name`,而将一个 bug 引入到这段代码,来看看这个默认测试失败的样子:
```rust
pub fn greeting(name: &str) -> String {
String::from("你好!")
}
```
运行这个测试,就会产生以下输出:
```console
$ cargo test lennyp@vm-manjaro
Compiling assert_demo v0.1.0 (/home/lennyp/rust-lang/assert_demo)
Finished test [unoptimized + debuginfo] target(s) in 0.48s
Running unittests src/lib.rs (target/debug/deps/assert_demo-504fa58455de23e3)
running 1 test
test tests::greeting_contains_name ... FAILED
failures:
---- tests::greeting_contains_name stdout ----
thread 'tests::greeting_contains_name' panicked at 'assertion failed: result.contains(\"Lenny\")', src/lib.rs:12:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::greeting_contains_name
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass '--lib'
```
这样的结果,正好表明了该断言失败了,以及这个失败断言所在的行。而更有用的失败消息,应会打印出那个 `greeting` 函数的值来。下面就来添加一个,由带有以获取自 `greeting` 函数的具体值所填充的占位符的格式字符串,所构成的定制失败消息:
```rust
#[test]
fn greeting_contains_name() {
let result = greeting("Lenny");
assert! (
result.contains("Lenny"),
"问候语未包含名字,问候语的值为 `{}`",
result
);
}
```
现在运行这个测试,就会得到内容更为的错误消息:
```console
$ cargo test lennyp@vm-manjaro
Compiling assert_demo v0.1.0 (/home/lennyp/rust-lang/assert_demo)
Finished test [unoptimized + debuginfo] target(s) in 0.42s
Running unittests src/lib.rs (target/debug/deps/assert_demo-504fa58455de23e3)
running 1 test
test tests::greeting_contains_name ... FAILED
failures:
---- tests::greeting_contains_name stdout ----
thread 'tests::greeting_contains_name' panicked at '问候语未包含名字,问候语的值为 `你好!`', src/lib.rs:12:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::greeting_contains_name
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass '--lib'
```
现在就可以在测试输出中看到具体得到的值了这将有助于对发生的事情而非期望发生的事情进行调试有所帮助we can see the value we actually got in the test output, which would help us debug what happened instead of what we were expecting to happen
## 使用 `should_panic`,检查中止运行
**Checking for Panics with `should_panic`**
除了检查返回值外,重要的是检查所编写代码有如预期那样,对各种错误情形进行处理。比如,请考虑在第 9 章清单 9-13 中所创建的那个 `Guess` 类型。使用了 `Guess` 的其他代码,就仰赖于 `Guess` 实例,将包含仅在 `1` 与 `100` 之间的值这一保证。这里就可以编写一个,确保在尝试创建带有那个范围之外值的 `Guess` 实例时,会中止运行的测试。
这里是通过将属性 `should_panic` 添加到此处的测试函数,来完成这一点的。在函数内部代码中止运行时,该测试便会通过;若函数中代码没有中止运行,那么该测试就会失败。
下面清单 11-8就给出了一个在预期 `Guess::new` 的各种错误情形发生时,对这些错误情形进行检查的测试。
文件名:`src/lib/rs`
```rust
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic! ("Guess 值必须在 1 与 100 之间,得到的是 {}。", value);
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn greater_than_100() {
Guess::new(200);
}
}
```
*清单 11就某个将引发 `panic!` 的情形进行测试*
这里将那个 `#[should_panic]` 属性,放在了 `#[test]` 属性之后,且在其应用到的函数之前。下面来看看在该测试通过时的样子:
```console
$ cargo test lennyp@vm-manjaro
Compiling assert_demo v0.1.0 (/home/lennyp/rust-lang/assert_demo)
Finished test [unoptimized + debuginfo] target(s) in 0.64s
Running unittests src/lib.rs (target/debug/deps/assert_demo-504fa58455de23e3)
running 1 test
test tests::greater_than_100 - should panic ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests assert_demo
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
```
看起来不错!现在就来通过移出当其中的值大于 `100` 时,这个 `new` 函数将中止运行的条件,而将一个 bug 引入到这里的代码:
```rust
// --跳过代码--
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 {
panic! ("Guess 值必须在 1 与 100 之间,得到的是 {}。", value);
}
Guess { value }
}
}
```
此时在运行清单 11-8 中的测试,他就会失败了:
```console
$ cargo test lennyp@vm-manjaro
Compiling assert_demo v0.1.0 (/home/lennyp/rust-lang/assert_demo)
Finished test [unoptimized + debuginfo] target(s) in 0.42s
Running unittests src/lib.rs (target/debug/deps/assert_demo-504fa58455de23e3)
running 1 test
test tests::greater_than_100 - should panic ... FAILED
failures:
---- tests::greater_than_100 stdout ----
note: test did not panic as expected
failures:
tests::greater_than_100
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass '--lib'
```
在这个示例中,并未获得非常有用的消息,不过在查看那个测试函数时,就会看到其被 `#[should_panic]` 给注解过。这里收到了失败,就表示在这个测试函数中的代码,并未引发运行中止。
用到 `should_panic` 的测试,可并不那么精确。即便在该测试由于某个不同于咱们所预期的原因而中止运行了,这个 `should_panic` 测试仍会通过。要令到 `should_panic` 测试更加精确,则可以将某个可选的 `expected` 参数,传递给那个 `should_panic` 属性。这种测试工具将确保失败消息包含了所提供的文本the test harneess will make sure that the failure message contains the provided text。比如请考虑下面清单 11-9 中修改过的 `Guess` 代码,其中 `new` 函数会根据该值是否过小或过大,而以不同消息中止运行。
文件名:`src/lib.rs`
```rust
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 {
panic! (
"Guess 值必须大于或等于 1, 得到的是 {}。",
value
);
} else if value > 100 {
panic! (
"Guess 值必须小于或等于 100, 得到的是 {}。",
value
);
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "小于或等于 100")]
fn greater_than_100() {
Guess::new(200);
}
}
```
*清单 11-9对有着包含指定 _子字符串_ 的中止运行消息,的某个 `panic!` 进行测试*
由于这里放在那个 `should_panic` 属性的 `expected` 参数中的值,正是其中 `Guess::new` 函数中止运行消息的一个子字符串,因此这个测试将通过。这里本可将所预期的整个中止运行消息给指定出来,在此示例中即为 `Guess 值必须小于或等于 100得到的是 200。` 选择指明什么,是根据中止运行消息,具有何种程度的独特性或动态变化,以及打算要整个测试具有何种级别的准确度。在此示例中,那个中止运行消息的某个子字符串,就足够用于确保该测试函数中代码,执行了 `else if value > 100` 的条件。
为看到在某个 `should_panic` 以一个 `expected` 消息失败时,会发生什么,下面就来通过调换 `if value < 1` 与 `else if value > 100` 代码块的代码体,而引入一个 bug 到这里的代码中:
```rust
if value < 1 {
panic! (
"Guess 值必须小于或等于 100, 得到的是 {}。",
value
);
} else if value > 100 {
panic! (
"Guess 值必须大于或等于 1, 得到的是 {}。",
value
);
}
```
这次在运行这个 `should_panic` 测试时,便会失败了:
```console
$ cargo test lennyp@vm-manjaro
Compiling assert_demo v0.1.0 (/home/lennyp/rust-lang/assert_demo)
Finished test [unoptimized + debuginfo] target(s) in 0.41s
Running unittests src/lib.rs (target/debug/deps/assert_demo-504fa58455de23e3)
running 1 test
test tests::greater_than_100 - should panic ... FAILED
failures:
---- tests::greater_than_100 stdout ----
thread 'tests::greater_than_100' panicked at 'Guess 值必须大于或等于 1, 得到的是 200。', src/lib.rs:13:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
note: panic did not contain expected string
panic message: `"Guess 值必须大于或等于 1, 得到的是 200。"`,
expected substring: `"小于或等于 100"`
failures:
tests::greater_than_100
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass '--lib'
```
这样的失败消息就表示,这个测试确实如预期那样中止运行了,但中止运行消息并未包含预期的字符串 `小于或等于 100`。在此示例中,真正得到中止运行消息,为 `Guess 值必须大于或等于 1, 得到的是 200。` 现在就可以开始找出,这里的 bug 在哪了!
## 在测试中使用 `Result<T, E>`
**Using `Result<T, E>` in Tests**
到目前为止,这里全部的测试在失败时,都会中止运行。这里通用可以编写用到 `Result<T, E>` 的测试!下面就是清单 11-1 的那个测试,只是被重写为了使用 `Result<T, E>`,并返回一个 `Err` 而非中止运行:
```rust
#[cfg(test)]
mod tests {
#[test]
fn it_works() -> Result<(), String> {
if 2 + 2 == 4 {
Ok(())
} else {
Err(String::from("二加二不等于四"))
}
}
}
```
这个 `it_works` 函数现在有了 `Result<T, E>` 的返回值类型。而在该函数的函数体中,此时在那个 `if` 测试通过时,返回了 `Ok(())`,在测试失败时返回一个带有 `String` 的 `Err`,而不再调用那个 `assert_eq!` 宏了。
编写这样的返回某个 `Return<T, E>` 的测试就令到在各个测试的函数体中使用问号运算符the question mark operator, `?`)可行了,而在测试函数体中使用 `?`,则可以是编写那些,在其内部返回某个 `Err` 变种时将会失败测试的便利方式。
在那些用到 `Result<T, E>` 的测试上,是不可以使用 `#[should_panic]` 注解的。而要断言某个操作返回的是一个`Result<T, E>` 枚举的 `Err` 变种,就不要在返回的 `Result<T, E>` 值上,使用问号操作符。相反,要使用 `assert!(value.is_err())` 这种方式。
既然咱们已经了解了编写测试的几种方式,那么就来看一下,在运行这些编写的测试时会发生什么,并探索一下可与 `cargo test` 一起使用的不同选项。
End

View File

@@ -0,0 +1,278 @@
# 测试的组织
如同在本章开头提到的测试是门复杂的学问而不同人群会使用不同术语及组织方式testing is a complex discipline, and different people use different terminology and organization。Rust 社群认为测试是由两个大类组成单元测试与集成测试unit tests and integration tests。*单元测试* 是一些小而更为专注的测试,他们一次单独测试一个模组,并可对私有接口进行测试(*unit tests* are small and more focused, testing one module in isolation at a time, and can test private interfaces。*集成测试* 则是完全在所编写库外部进行,并会像其他外部代码那样,对咱们的代码加以使用,因此就只会对公开接口进行使用,且潜在每个测试会检查多个模组。
这两种类型的测试编写,对于确保代码库的各个部分有在单独及共同完成所预期的事项,都是重要的。
## 单元测试
单元测试的目的,是要将各个代码单元孤立于其余代码进行测试,从而快速定位出何处代码有如预期那样工作,以及何处代码未如预期那样工作。应将单元测试,放在 `src` 目录之下,在那些有着他们要测试代码的各个文件中。约定即为要在各个文件中,创建包含那些测试函数的一个名为 `tests` 的模组,并使用 `cfg(test)` 对该模组加以注解。
### 测试模组与 `#[cfg(test)]`
在测试模组上的那个 `#[cfg(test)]` 注解,告诉 Rust 仅在运行 `cargo test`,而非运行 `cargo build`时,才编译和运行测试代码。在只打算构建该库时,这样就节省了编译时间,并由于在得到的已编译工件中不会包含测试,而在其中节省了空间。后面就会看到,由于集成测试会进到不同目录,他们就不需要这个 `#[cfg(test)]` 注解。不过由于单元测试是在与代码同样的文件中,因此就会使用 `#[cfg(test)]`,来指明他们不应被包含在编译结果中。
回顾在本章第一小节中,当那里生成那个新的 `adder` 项目时Cargo 就为咱们生成了下面的代码:
```console
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
let result = 2 + 2;
assert_eq!(result, 4);
}
}
```
这段代码就是自动生成的测试模组。其中的属性 `cfg` 是指 *配置configuration*,而告诉 Rust 接下来的项目只应在给定的某个配置选项下才被包含。在此示例中,那个配置选项便是 `test`Rust 提供的这个配置选项,用于测试的编译及运行。通过使用这个 `cfg` 属性Cargo 就会只在咱们以 `cargo test`,明确表示要运行这些测试时,才对这里的测试代码进行编译。而这些测试代码,包含了可能位于此模组内部的全部辅助函数,以及使用 `#[test]` 注解过的那些函数。
### 测试私有函数
**Testing Private Functions**
在测试社区有着私有函数是否应被直接测试的争论而别的语言让对私有函数的测试成为困难或不可行的事情。不论所才行的是何种测试理念Rust 的私有规则,真的实现了对私有函数的测试。请考虑下面清单中,有着私有函数 `internal_adder` 的代码。
文件名:`src/lib.rs`
```rust
pub fn add_two(a: i32) -> i32 {
internal_add(a, 2)
}
fn internal_add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn internal() {
assert_eq! (4, internal_add(2, 2));
}
}
```
*清单 11-12对私有函数进行测试*
请注意这个 `internal_adder` 函数,未被标记为 `pub`。其中的那些测试,都只是些 Rust 代码,同时那个 `tests` 模组,只是另一个模组。如同在前面的 [“用于对模组树中某个项目进行引用的路径”](Ch07_Managing_Growing_Projects_with_Packages_Crates_and_Modules.md#用于引用目录树中项目的路径) 小节中所讨论的,子模组中的那些项目,可以使用其祖辈模组中的项目。在这个测试中,就以 `use super::*` 语句,将那个 `tests` 模组父辈的那些项目,带入到了作用域,进而该测试随后就可以调用 `internal_adder` 了。而在不认为私有函数应被测试时,那么 Rust 就没有什么可以迫使你对他们进行测试了if you don't think private functions should be tested, there's nothing in Rust that will compel you to do so
## 集成测试
**Integration Tests**
在 Rust 中,集成测试整个都是属于所编写库外部的。他们会以与其他代码同样方式,对咱们编写的库加以使用,这就意味着集成测试只能调用属于库公开 API 一部分的那些函数。集成测试的目的,是要就所编写库的多个部分,是否有正确地一起运作进行测试。这些各自正常工作的代码单元,在被集成在一起时,就可能有问题,因此集成后代码的测试面,也是重要的。要创建集成测试,首先就需要一个 `tests` 目录。
### `tests` 目录
**The `tests` Directory**
这里时在项目目录的顶层,挨着那个 `src` 目录,创建一个 `tests` 目录的。Cargo 就明白要在整个目录下查找那些集成测试的文件。至于可以构造多少个测试文件则是想要多少都可以Cargo 将把这些各个文件,编译为单独的代码箱。
下面就来创建一个集成测试。使用清单 11-12 中仍在 `src/lib.rs` 文件中的代码,构造一个 `tests` 目录,并创建一个名为 `tests/integration_test.rs` 的文件。那么现在的目录结构,应像下面这样:
```console
adder
├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
└── tests
└── integration_test.rs
```
请将下面清单 11-13 中的代码,输入到那个 `tests/integration_test.rs` 文件中:
文件名:`tests/integration_test.rs`
```rust
use adder;
#[test]
fn it_adds_two() {
assert_eq! (4, adder::add_two(2));
}
```
*清单 11-13一个 `adder` 代码箱中函数的集成测试*
`tests` 目录中的每个文件,都是个单独代码箱,因此这里就需要将所编写的库,带入到各个测试代码箱的作用域。由于这个原因,这里就要在该代码的顶部,添加 `use adder` 语句,这在之前的单元测试中就不需要。
这里不需要以 `#[cfg(test)]``tests/integration_test.rs` 中的任何代码进行注解。Cargo 会特别对待 `tests` 目录,而只在运行 `cargo test` 时,才编译此目录中的文件。现在运行 `cargo test`
```console
$ cargo test lennyp@vm-manjaro
Compiling adder v0.1.0 (/home/lennyp/rust-lang/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.52s
Running unittests src/lib.rs (target/debug/deps/adder-7763e46d5dd299a3)
running 1 test
test tests::internal ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/integration_test.rs (target/debug/deps/integration_test-d0d0eaf0bad2a59f)
running 1 test
test it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
```
输出的三个部分,包含了单元测试、集成测试与文档测试。请留意在某个部分的任何测试失败时,接下来的部分就不会运行了。比如,在某个单元测试失败时,由于集成测试与文档测试只会在全部单元测试通过时才运行,因此就不再会有集成与文档测试的任何输出了。
其中单元测试的第一部分,与之前曾见到过的一样:每个单元测试一行(那行就是在清单 11-12 中所添加的名为 `internal` 的测试),并随后有个这些单元测试的小结。
集成测试部分是以那行 `Running tests/integration_test.rs` 开始的。接下来,集成测试中的每个测试函数都有一行,且在紧接着 `Doc-tests adder` 部分开始之前,就是集成测试的那些结果的一个小结。
每个集成测试都有其自己的部分,那么在把更多文件添加到那个 `tests` 目录中时,就会有更多的集成测试部分了。
通过将测试函数的名字,指明为 `cargo test` 的命令行参数,这里仍可运行某个特定集成测试函数。而要运行某个特定集成测试文件中的全部测试,则要使用跟上了该文件名字的 `cargo test``--test` 参数:
```console
$ cargo test --test integration_test lennyp@vm-manjaro
Compiling adder v0.1.0 (/home/lennyp/rust-lang/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.23s
Running tests/integration_test.rs (target/debug/deps/integration_test-d0d0eaf0bad2a59f)
running 1 test
test it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
```
此命令只运行在 `test/integration_test.rs` 文件中的那些测试。
### 集成测试中的子模组
**Submodules in Integration Tests**
随着更多集成测试的添加,就会想要在那个 `tests` 目录下,构造更多文件,来帮助组织这些文件;比如就可以将那些测试函数,按照他们所测试的功能而进行分组。如同早先所提到的,在 `tests` 目录下的各个文件,都作为其自己单独的代码箱而被编译,这一点对于创建独立作用域,来对最终用户将要使用所编写代码箱的方式,进行更紧密模拟是有用的。不过,这将意味着在 `tests` 目录中的那些文件,不会如同在第 7 章中,有关 [如何将代码分离为模组与文件](Ch07_Managing_Growing_Projects_with_Packages_Crates_and_Modules.md#将模组拆分为不同文件) 部分,所掌握的 `src` 中的那些文件那样,共用同样的行为。
在有着一套在多个集成测试文件中使用的辅助函数,并尝试遵循第 7 章 [将模组分离为不同文件](Ch07_Managing_Growing_Projects_with_Packages_Crates_and_Modules.md#将模组拆分为不同文件) 中的步骤,把这些辅助函数提取到某个通用模组中时,`tests` 目录的那些文件的不同行为就最为明显了。比如说,在创建出 `tests/common.rs` 并将一个名为 `setup` 的函数放在其中时,就可以将一些要在多个测试文件的多个测试函数调用的代码,添加到 `setup`
文件名:`tests/common.rs`
```rust
pub fn setup() {
// 特定于库测试的一些设置代码,将放在这里
}
```
当再度运行这些测试时,即使这个 `common.rs` 文件未包含任何测试函数,也没有从任何地方调用这个 `setup` 函数,仍会在测试输出中,发现这个 `common.rs` 文件的一个新部分:
```console
$ cargo test lennyp@vm-manjaro
Compiling adder v0.1.0 (/home/lennyp/rust-lang/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.47s
Running unittests src/lib.rs (target/debug/deps/adder-7763e46d5dd299a3)
running 1 test
test tests::internal ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/common.rs (target/debug/deps/common-82aa4aac16d81562)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/integration_test.rs (target/debug/deps/integration_test-d0d0eaf0bad2a59f)
running 1 test
test it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
```
以显示出他的 `running 0 tests` 方式,让 `common` 出现在测试结果中,并非咱们想要的。这里只是打算在其他集成测试文字之下,共用一些代码。
要避开让 `common` 出现在测试输出中,就要创建出 `tests/common/mod.rs`,而非创建出 `tests/common.rs`。该项目目录现在看起来像下面这样:
```console
adder
├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
└── tests
├── common
│   └── mod.rs
└── integration_test.rs
```
这是曾在第 7 章 ["替代文件路径"](Ch07_Managing_Growing_Projects_with_Packages_Crates_and_Modules.md#备用文件路径) 小节所提到的Rust 同样明白的较早命名约定。以这种方式命名该文件,就告诉 Rust 不要将那个 `common` 模组,作为一个集成测试文件对待。在将这个 `setup` 函数移入到 `tests/common/mod.rs` 里头,并删除了那个 `tests/common.rs` 文件时,在测试输出中的该部分就不再出现了。`tests` 目录子目录中的那些文件,不会作为单独代码箱而被编译,也不会在测试输出中拥有自己的部分。
在创建出 `tests/common/mod.rs` 之后,就可以从任意的集成测试文件,将其作为模组而加以使用。下面就是一个从 `tests/integration_test.rs` 中的 `it_adds_two` 测试,对这个 `setup` 函数进行调用的示例:
文件名:`tests/integration_test.rs`
```rust
use adder;
mod common;
#[test]
fn it_adds_two() {
common::setup();
assert_eq! (6, adder::add_two(4));
}
```
请留意其中的 `mod common;` 声明,与曾在清单 7-21 中演示过的模组声明相同。随后在那个测试函数中,这里既可以调用那个 `common::setup()` 函数了。
### 二进制代码箱的集成测试
**Integration Tests for Binary Crates**
在所编写详细是个仅包含 `src/main.rs` 文件的二进制代码箱,而没有 `src/lib.rs` 文件时,就无法在 `tests` 目录中创建集成测试,以及使用 `use` 语句,将定义在 `src/main.rs` 中的函数带入到作用域。唯有库代码箱将其他代码箱可以使用的函数给暴露出来二进制代码箱本来就是由他们自己来运行的binary crates are meant to be run on their own
这是那些提供到二进制程序的 Rust 项目,有着一个直接了当的、对存在于 `src/lib.rs` 逻辑进行调用的 `src/main.rs` 文件的原因之一。运用那样的结构,集成测试就 *可以* 使用 `use` 对库代码箱进行测试,从而令到重要功能可用。当重要功能运作时,那么在那个 `src/main.rs` 文件中的少量代码,也将同样工作,同时那少量代码就不需要被测试了。
# 本章小结
Rust 的这些测试特性,提供到一种指明代码应如何生效,从而确保即使在进行了修改时,其仍继续如预期那样工作的方式。单元测试对库的各个部分进行单独检查,而可对一些私有实现细节进行测试。集成测试则对库的多个部分一起正确运作进行检查,同时他们会使用库的公开 API以与外部代码使用库的同样方式对代码进行测试。即使 Rust 的类型系统与所有权规则有助于防止某些种类的代码错误,对于消除与所编写代码预期表现方式有关的逻辑错误,测试仍是必不可少的。
下面就来将本章以及前面那些章中所掌握的知识结合起来,在一个项目上练手一下了!
End

View File

@@ -0,0 +1,289 @@
# 在哈希图中存储关联的键与值
咱们最后一个常用集合,就是 *哈希图hash map*`HashMap<K, V>` 这种类型,使用 *哈希函数hashing function*,存储类型为 `K` 的键,到类型为 `V` 的值的映射,哈希函数确定了其将这些值放置于内存中的方式。许多编程语言都支持这种数据结构,不过他们通常使用别的名称,比如 *哈希*、*映射*、*对象*、*哈希表*、*字典*,或者 *关联数组* 等,这里仅举几例。
在咱们不像在矢量值中,以索引查找数据,而以可是任何类型的键查找数据时,哈希图就非常有用。例如,在某场比赛中,咱们可在一个哈希图中记录每支球队的得分,其中每个键都是某支球队的名称,而值则是每支球队的得分。在给定某个队名时,咱们就可以获取到其得分。
我们将在本节中介绍哈希图的基本 API但标准库为 `HashMap<K, V>` 定义的函数中,还隐藏了更多精彩内容。请一如既往地查看标准库文档,了解更多信息。
## 创建一个新哈希图
创建空哈希图的一种方法是使用 `new`,并以 `insert` 添加元素。在下面清单 8-20 中,我们记录了名为 `Blue``Yellow` 两支球队的得分。蓝队从 10 分开始,黄队从 50 分开始。
```rust
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("蓝队"), 10);
scores.insert(String::from("红队"), 50);
```
*清单 8-20创建一个新的哈希图并插入一些键与值*
请注意,我们需要首先 `use` 标准库中集合部分的 `HashMap`。在我们常用的三种集合中,哈希图是最不常用的,因此他并未包含在前奏中自动带入作用域的那些特性里。哈希图在标准库中的支持也较少;例如,就没有构造哈希图的内置宏。
与矢量一样,哈希图也会将其数据存储在堆上。上面这个 `HashMap` 的键为 `String` 类型,值为 `i32` 类型。与矢量一样,哈希图也是同构的 <sup>1</sup>:所有键的类型必须相同,且所有值的类型也必须相同。
> <sup>1</sup>Homogeneous, 参见:[Difference Between Heterogeneous and Homogeneous Data Structures](https://codeskiller.codingblocks.com/library/articles/difference-between-heterogeneous-and-homogeneous-data-structures)
另一种构造哈希图的方式为通过使用迭代器,及元组矢量上的 `collect` 方法,而元组矢量中各个元组由某个键与其值组成。在 [第 13 章的 “使用迭代器处理一系列的条目” 小节](Ch13_Functional_Language_Features_Iterators_and_Closures.md#使用迭代器对条目系列进行处理),就会深入到迭代器的细节及其相关方法。`collect` 方法会将数据收集到包括 `HashMap` 在内的各种集合类型中。比如,在将球队名字与初始得分放在两个单独矢量中时,就可使用 `zip` 方法,创建出一个元组的迭代器,其中 `Blue` 会与 `10` 结对,并以此类推。随后就可使用 `collect` 方法,将那个元组迭代器转换为一个哈希图,如下清单 8-21 中所示。
```rust
use std::collections::HashMap;
let teams = vec! [String::from("蓝队"), String::from("红队")];
let initial_scores = vec! [10, 50];
let mut scores: HashMap<_, _> = teams
.into_iter()
.zip(initial_scores.into_iter())
.collect();
```
*清单 8-21从球队清单与得分清单创建出一个哈希图*
由于有可能 `collect` 到许多不同数据结构,因此除非有指明,那么 Rust 就不清楚其所想要的是何种数据结构,因此这里的类型注解 `HashMap<_, _>` 是需要的。对于键与值类型的泛型参数,这里使用了下划线(`_`),而 Rust 可基于那两个矢量中数据的类型,推断出该哈希图的类型。在上面清单 8-21 中,键的类型将是 `String`,值类型将是 `i32`,就跟清单 8-20 中的一样。
## 访问哈希图中的值
通过将某个值的键提供给 `get` 方法,咱们可从哈希图中提取该值,如下清单 8-23 中所示。
```rust
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
let team_name = String::from("Blue");
let score = scores.get(&team_name).copied().unwrap_or(0);
```
*清单 8-23访问该哈希图中蓝队的得分*
这里,`score` 将具有与蓝队关联的值,结果将是 `10``get` 方法会返回一个 `Option<&V>`在哈希图中没有该键的值时g`et` 将返回 `None`。这个程序处理 `Option` 的方法,是调用 `copied` 获得 `Option<i32>` 而不是 `Option<&i32>`,然后调用 `unwrap_or`,在 `scores` 中没有该键的条目时,将 `score` 设为 `0`
就像处理向量一样,我们可使用 `for` 循环的方式,遍历哈希图中的每个键值对:
```rust
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("蓝队"), 10);
scores.insert(String::from("红队"), 50);
for (key, value) in &scores {
println! ("{}, {}", key, value);
}
```
这段代码将以任意顺序打印出各键值对:
```console
蓝队, 10
红队, 50
```
## 哈希图与所有权
对于像是 `i32` 这样实现了 `Copy` 特质的类型值,会被复制到哈希图中。对于 `String` 这样的自有值,则会被迁移,同时哈希图将成为这些值的所有者,如下清单 8-22 所示。
```rust
use std::collections::HashMap;
let field_name = String::from("喜好颜色");
let field_value = String::from("蓝色");
let mut map = HashMap::new();
map.insert(field_name, field_value);
println! ("{}, {}", field_name, field_value);
// 到这里 field_name 与 field_value 就无效了,请尝试对
// 他们进行使用,并看看会收到什么样的编译器错误!
```
*清单 8-25显示一旦被插入到哈希图键与值就被哈希图所有*
```console
$ cargo run  ✔
Compiling hashmap_demo v0.1.0 (/home/peng/rust-lang/hashmap_demo)
error[E0382]: borrow of moved value: `field_name`
--> src/main.rs:10:25
|
4 | let field_name = String::from("喜好颜色");
| ---------- move occurs because `field_name` has type `String`, which does not implement the `Copy` trait
...
8 | map.insert(field_name, field_value);
| ---------- value moved here
9 |
10 | println! ("{}, {}", field_name, field_value);
| ^^^^^^^^^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0382]: borrow of moved value: `field_value`
--> src/main.rs:10:37
|
5 | let field_value = String::from("蓝色");
| ----------- move occurs because `field_value` has type `String`, which does not implement the `Copy` trait
...
8 | map.insert(field_name, field_value);
| ----------- value moved here
9 |
10 | println! ("{}, {}", field_name, field_value);
| ^^^^^^^^^^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0382`.
error: could not compile `hashmap_demo` due to 2 previous errors
```
在调用 `insert``field_name``field_value` 迁移到哈希图中后,我们就无法使用这两个变量了。
若我们将到这两个值的引用,插入到哈希图中,那么这两个值就不会被迁移到哈希图中。引用所指向的值,必须至少在哈希图有效期内有效。我们将在第 10 章中的 [“使用生命周期验证引用”](../generic_types_traits_and_lifetimes/lifetimes.md) 中,详细讨论这些问题。
## 更新哈希图
虽然键值对的数量可以增长,但同一时间每个唯一键只能有一个与之关联的值(反之则不然:例如,蓝队和黄队都可以在 `scores` 这个哈希图中 `10`)。
当咱们打算更改某个哈希图中的数据时,咱们必须决定如何处理键已赋值的情形。
- 咱们可在完全忽略原有值下,用新值替换旧值;
- 咱们可以保留旧值而忽略新值,只有在键还 *没有* 值的情况下才添加新值;
- 或者,咱们也可以将旧值和新值结合。
我们来分别看看怎样完成这些操作!
### 重写某个值
**Overwriting a Value**
若我们将一个键与值插入到某个哈希图中,然后以不同值插入这个相同键,那么与该键关联的值就会被替换。即使下面清单 8-23 中的代码调用了两次 `insert`,该哈希图也只将包含一个键值对,因为我们两次插入的都是蓝队键的值。
```rust
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("蓝队"), 10);
scores.insert(String::from("蓝队"), 25);
println! ("{:?}", scores);
```
*清单 8-24以特定键替换某个存储的值*
这段代码将打印 `{"蓝队": 25}`。原先的值 `10` 已被重写。
### 仅在键不存在时添加键与值
**Adding a Key and Value Only If a Key Isn't Present**
以一个值检查哈希图中某个特定键是否已存在,并随后采取以下措施:若该键确实存在于哈希图中,则现有值应不变;若该键不存在,则插入该键及他的一个值,这种做法很常见。
哈希图为此提供了一个名为 `entry` 的特殊 API他会取咱们打算检查的键作为参数。这个 `entry` 方法的返回值,是个名为 `Entry` 的枚举,表示某个可能存在也可能不存在的值。比方说,我们想要检查黄队这个键是否有个与其关联的值。在没有时,我们就插入值 `50`,对于蓝队也一样。使用这个 `entry` API代码看起来如同下面清单 8-24。
```rust
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("蓝队"), 10);
scores.entry(String::from("黄队")).or_insert(50);
scores.entry(String::from("蓝队")).or_insert(50);
println! ("{:?}", scores);
```
*清单 8-25使用 `entry` 方法,只有在键还没有值时插入*
`Entry` 上的 `or_insert` 方法,被定义为在键存在时,返回一个到这个对应 `Entry` 键值的可变引用;而若不存在,他就会将参数作为该键的新值插入,并返回一个到这个新值的可变引用。这种技巧比我们自己编写逻辑要简洁得多,而且与借用检查器的配合也更好。
运行清单 8-24 中的代码将打印出 `{"黄色" 50, "Blue": 10}`。到 `entry` 的第一次调用,将插入黄队的键与值 `50`,因为黄队还没有值。到 `entry` 的第二次调用不会更改这个哈希图,因为蓝队已有了值 `10`
### 根据原有值更新某个值
**Updating a Value Based on the Old Value**
哈希图的另一个常见用例,是查找某个键的值,然后根据其原有值更新他。例如,下面清单 8-25 给出了计算某个文本中每个单词出现次数的代码。我们使用了一个以各个单词为键的哈希图,并递增值来记录我们看到该单词的次数。在我们首次看到某个单词时,我们将首先插入值 `0`
```rust
use std::collections::HashMap;
let text = "hello world wonderful world";
let mut map = HashMap::new();
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0);
*count += 1;
}
println! ("{:?}", map);
```
*清单 8-26使用存储了单词与计数的哈希图统计单词出现次数*
这段代码将打印 `{"world": 2, "hello": 1, "wonderful": 1}`。咱们可能会看到这些同样的键值对,以不同的顺序打印出来:回顾在 [“访问哈希图中的值”](#访问哈希图中的值) 中,对哈希图的迭代就是以任意顺序出现的。
`split_whitespace` 这个方法会返回 `text` 中值以空白分隔的子切片的迭代器。`or_insert` 方法会返回指定键值的可变引用(`&mut V`)。这里,我们将该可变引用存储在 `count` 变量中,因此为了赋值给该值,我们必须首先使用星号(`*`),解引用 `count`。在这个 `for` 循环结束时,这个可变引用就会超出作用域,因此所有这些更改都是安全的,也是借用规则所允许的。
## 哈希函数
**Hashing Functions**
默认情况下,`HashMap` 使用了一个名为 `SipHash`可抵抗涉及哈希表拒绝服务DoS攻击的散列函数 <sup>2</sup>。虽然这不是最快的散列算法,但以降低性能来换取更好的安全性是值得的。如果咱们在分析咱们的代码时,发现这个默认散列函数的速度对于咱们目的太慢,那么咱们可通过指定不同的散列器,切换到其他函数。所谓 *散列器hasher*,是某种实现了 `BuildHasher` 特质的类型。我们将在 [第 10 章](../generic_types_traits_and_lifetimes/traits.md) 讨论特质以及如何实现特质。咱们不一定要从头实现咱们自己的散列器;[`crates.io`](https://crates.io/) 上有一些由其他 Rust 用户共享,提供实现了许多常见散列算法的散列器库。
> <sup>2</sup>:参见:[https://en.wikipedia.org/wiki/SipHash](https://en.wikipedia.org/wiki/SipHash)。
# 本章小结
在咱们需要存储、访问及修改数据时,矢量值、字符串于哈希图,提供了程序中大量必要的功能。下面是一些咱们现在应有能力解决的练习:
1. 给定一个整数列表,请使用一个矢量并返回该列表的中位数(排序时,位于中间位置的值)与模式(出现频率最高的值;哈希图在这里会很有用);
2. 将字符串(英语)转换为拉丁文的结尾。每个单词的第一个辅音,要被移到该单词词末尾并加上 *ay*,因此 *first* 就会变成 *first-fay*。以元音开头的单词,则要在该词末尾加上 *hay* *apple* 就会变成 *apple-hay* )。请记住有关 UTF-8 编码的细节!
3. 使用哈希图和矢量值,创建一个允许用户将员工姓名,添加到公司的某个部门的界面;例如,“将 Sally 添加到工程部” 或 “将 Amir 添加到销售部”。然后允许用户按字母顺序,获取某个部门所有人的请单,或按部门获取公司所有人的名单。
标准库 API 文档介绍了矢量值、字符串于哈希图的方法,这些方法对这些练习很有帮助!
我们正进入到一些更复杂程序中,其中的操作可能会失败,所以现在是讨论错误处理的最佳时机。接下来我们将讨论这个问题!
End

View File

@@ -0,0 +1,390 @@
# 使用 `String` 存储 UTF-8 编码的文本
在第 4 章中,我们曾讨论过字符串,现在我们将对其进行更深入的研究。新的 Rust 公民通常卡在字符串上,原因有三:
- Rust 有暴露可能错误的倾向;
- 字符串是种比许多程序员认为的更复杂的数据结构;
- 以及 UTF-8。
当咱们从其他编程语言转至 Rust 时,这些因素就会以看起来很难的方式,纠缠在一起。
我们之所以会在集合语境下讨论字符串,是因为字符串是作为字节集合,加上一些在这些字节被解释为文本时,提供有用功能的方法实现的。在本节中,我们将讨论那些 `String` 上的、每种集合类型都有的操作,比如创建、更新及读取等。我们还将讨论 `String` 与其他集合的不同之处,即由于人与计算机在解析 `String` 数据时的差异,而对 `String` 的索引会变得如何复杂。
## 何为字符串?
我们将首先定义出所谓 *字符串* 的含义。Rust 的核心语言中,只有一种字符串类型,即通常以其借用形式 `&str` 出现的字符串切片 `str`。在第 4 章我们曾谈到过 [*字符串片切片*](../ownership/the_slice_type.md#字符串切片),他们是对存储在别处的 UTF-8 编码字符串数据的一些引用。例如,字符串的字面值,就存储在程序的二进制文件中,而因此属于一些字符串的切片。
所谓 `String` 类型,则是由 Rust 的标准库提供而未被编码到核心语言是种可增长、可变、有所有权、UTF-8 编码的字符串类型。当 Rust 公民提及 Rust 中的 “字符串” 时,他们可能指的是 `String` 或字符串切片的 `&str` 类型,而不单是其中一种类型。虽然本节主要讨论 `String`,但这两种类型在 Rust 的标准库中都有大量使用,同时 `String` 和字符串切片均为 UTF-8 编码的。
## 创建一个新的 `String`
`Vec<T>` 的许多相同操作对 `String` 也可使用,因为 `String` 实际上是作为字节矢量的封装器实现的,不过带有一些额外的保证、限制及功能。`Vec<T>``String` 下以同一方式工作的函数示例,便是创建出一个实例的 `new` 函数,如下清单 8-11 所示。
```rust
let mut s = String::new();
```
*清单 8-11创建一个新的空 `String`*
这行代码会创建出一个名为 `s` 的新空字符串,然后我们就可以向其中加载数据。通常情况下,我们会使用一些咱们开始该字符串的初始数据。为此,我们会使用 `to_string` 方法,该方法适用于任何实现了 `Display` 特质的类型,正如字符串字面量那样。下面清单 8-12 展示了两个示例。
```rust
let data = "初始内容";
let s = data.to_string();
// 该方法同样直接工作于字面值之上
let s = "初始内容".to_string();
```
*清单 8-12使用 `to_string` 方法从某个字符串字面值创建出 `String`*
这段代码会创建出一个包含 `初始内容` 的字符串。
我们还可以使用函数 `String::from`,从某个字符串字面量创建出字符串。下面清单 8-13 中的代码,等同于清单 8-12 中使用 `to_string` 的代码。
```rust
let s = String::from("初始内容");
```
*清单 8-13使用 `String::from` 函数,从某个字符串字面值创建出 `String`*
由于字符串的用途非常广泛,我们可以使用许多不同的字符串通用 API从而为我们提供了大量选择。其中有些看起来是多余的但他们都有自己的用武之地在这个示例中`String::from``to_string` 做的是同一件事,所以咱们选择哪个只是风格与可读性的问题。
请记住,字符串是 UTF-8 编码的,因此我们可以在其中包含任何正确编码的数据,如下清单 8-14 所示。
```rust
let hello = String::from("السلام عليكم");
let hello = String::from("Dobrý den");
let hello = String::from("Hello");
let hello = String::from("שָׁלוֹם");
let hello = String::from("नमस्ते");
let hello = String::from("こんにちは");
let hello = String::from("안녕하세요");
let hello = String::from("你好");
let hello = String::from("Olá");
let hello = String::from("Здравствуйте");
let hello = String::from("Hola");
let hello = String::from("👋");
```
*清单 8-14以字符串存储不同语言的问候语*
所有这些都是有效的 `String` 值。
## 更新字符串
在咱们向某个 `String` 中压入更多数据时,其大小和内容都会发生变化,就像 `Vec<T>` 的内容一样。此外,咱们还可以方便地使用 `+` 运算符或 `format!` 宏,连接多个 `String` 值。
### 使用 `push_str` 与 `push` 追加内容到某个 `String`
如下清单 8-15 所示,通过使用 `push_str` 方法追加一个字符串切片,我们可以增长某个 `String`
```rust
let mut s = String::from("foo");
s.push_str("bar");
println! ("{}", s);
```
*清单 8-15使用 `push_str` 方法将字符串切片追加到某个 `String`*
在这两行后,`s` 将包含 `foobar``push_str` 方法会取个字符串切片,因为我们并不会想要取得那个参数的所有权。例如,在下面清单 8-16 中的代码中,我们打算在将 `s2` 的内容追加到 `s1` 后,能够使用 `s2`
```rust
let mut s1 = String::from("foo");
let s2 = "bar";
s1.push_str(s2);
println! ("{}", s2);
```
*清单 8-16在将其内容追加到某个 `String` 后,使用该字符串切片*
如果 `push_str` 方法取得了 `s2` 的所有权,我们就无法在最后那行打印出他的值。然而,这段代码会如我们与其那样工作!
`push` 方法会取单个字符作为参数,并将其添加到 `String`。下面清单 8-17 使用 `push` 方法将字母 `l` 追加到某个 `String`
```rust
let mut s = String::from("lo");
s.push('l');
```
*清单 8-17使用 `push` 将一个字符添加到 `String`*
作为结果,`s` 将包含 `lol`
### 使用 `+` 运算符或 `format!` 宏的字符串连接
**Concatenation with the `+` Operator or the `format!` Macro**
通常情况下,咱们会想要合并两个现有字符串。一种方法是使用 `+` 运算符,如下清单 8-18 所示。
```rust
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // 请注意这里 s1 已被迁移,而不能再被使用
```
*清单 8-18使用 `+` 运算符合并两个 `String` 值为新的 `String` 值*
字符串 `s3` 将包含 `Hello, world!`。加法运算后,字符串 `s1` 不再有效的原因,以及我们使用到 `s2` 引用的原因,都与我们使用 `+` 运算符时调用的方法签名有关。`+` 运算符使用了 `add` 方法,该方法的签名如下所示:
```rust
fn add(self, s: &str) -> String
```
在标准库中,咱们会看到使用了泛型及关联类型定义的 `add`。在这里,我们代入了具体类型,这就是我们对 `String` 值调用此方法时的情况。我们将在第 10 章讨论 [泛型](../Ch10_Generic_Types_Traits_and_Lifetimes.md)。这个签名为我们提供了理解 `+` 这个运算符中,棘手部分所需的线索。
首先,`s2` 带有一个 `&`,这意味着我们是将第二个字符串的 *引用*,加到第一个字符串。这是因为 `add` 函数中的那个 `s` 参数:我们只能将一个 `&str` 加到某个 `String`;我们不能将两个 `String` 值加到一起。但是等等,`&s2` 的类型为 `&String`,而不是 `&str`,正如 `add` 的第二个参数所指定的那样。那么为什么清单 8-18 会编译呢?
我们能够在到 `add` 调用中使用 `&s2` 的原因是,编译器可以将 `&String` 参数,强制转换为 `&str`。当我们调用 `add` 方法时Rust 会使用 *解引用强制转换*,在这里其会将 `&s2` 转换为 `&s2[..]`。我们将在第 15 章更详细地讨论 [解引用强制转换](../smart_pointers/deref-t.md)。由于 `add` 方法不会取得参数 `s` 的所有权,因此在这个操作后,`s2` 仍将是个有效的 `String` 值。
其次,我们可以在这个签名中看到,`add` 函数会取得 `self` 的所有权,因为 `self` *没有* `&`。这意味着列表 8-18 中的 `s1`,将被迁移到 `add` 这个调用中,并且在调用之后将不再有效。因此,尽管 `let s3 = s1 + &s2;` 看起来像是会拷贝两个字符串,并创建出一个新字符串,但实际上该语句会取得 `s1` 的所有权,将 `s2` 内容的一份副本追加到 `s1` 上,然后返回结果的所有权。换句话说,虽然看起来像是进行了多次拷贝,但实际上并非如此;该实现比拷贝更高效。
在我们需要连接多个字符串时,`+` 运算符的行为会变得笨重:
```rust
let s1 = String::from("tic");
let s2 = String::from("toc");
let s3 = String::from("toe");
let s = s1 + "-" + &s2 + "-" + &s3;
```
此时,`s` 将是 `tic-tac-toe`。由于包含大量 `+``"` 字符,很难看清具体情况。对于更复杂的字符串合并,我们可以改用 `format!` 这个宏:
```rust
let s1 = String::from("tic");
let s2 = String::from("toc");
let s3 = String::from("toe");
let s = format! ("{}-{}-{}", s1, s2, s3);
```
这段代码也会将 `s` 设置为 `tic-toc-toe``format!` 宏与 `println!` 类似,但他不会将输出打印到屏幕上,而是返回一个包含内容的 `String`。使用 `format!` 的代码版本更加易于阅读,且由 `format!` 宏生成的代码使用了引用,因此这个调用不会取得其任何参数的所有权。
## 字符串的索引
**Indexing into Strings**
在许多其他编程语言中,通过索引引用字符串中的单个字符,是种有效且常见的操作。然而,如果咱们尝试在 Rust 中使用索引语法,访问某个 `String` 的一些部分,咱们将得到一个报错。请设想下面列表 8-19 中的无效代码。
```rust
let s1 = String::from("hello");
let h = s1[0];
```
*清单 8-19尝试对某个 `String` 使用索引语法*
这段代码将导致一些报错:
```console
$ cargo run
Compiling string_demo v0.1.0 (/home/peng/rust-lang/projects/string_demo)
error[E0277]: the type `String` cannot be indexed by `{integer}`
--> src/main.rs:3:13
|
3 | let h = s1[0];
| ^^^^^ `String` cannot be indexed by `{integer}`
|
= help: the trait `Index<{integer}>` is not implemented for `String`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `string_demo` due to previous error
```
该报错与注释揭示了问题所在Rust 字符串不支持索引操作。但为什么不支持呢?要回答这个问题,我们需要讨论 Rust 是如何在内存中存储字符串的。
### 内部表示
**Internal Representation**
`String` 是对 `Vec<u8>` 的封装。我们来看看列表 8-14 中,咱们的那些正确编码的 UTF-8 示例字符串。首先,这个:
```rust
let hello = String::from("Hola");
```
在这种情况下,`len` 值将为 `4`,这意味着存储字符串 `"Hola"` 的矢量值长度为 4 字节。在以 UTF-8 编码时,这些字母每个占用一字节。然而,下面这行代码可能会让咱们感到意外(请注意,这个字符串以大写的西里尔字母 *Ze* 开头,而非数字 3
```rust
let hello = String::from("Здравствуйте");
```
在咱们被问及该字符串的长度时,你可能会说 12。事实上Rust 的答案是 24这是以 UTF-8 编码 “Здравствуйте” 所需的字节数,因为该字符串中的每个 Unicode 标量值,都需要 2 个字节的存储空间。因此,对字符串字节的索引,并不总是对应于有效的 Unicode 标量值。为了演示,请考虑下面这段无效的 Rust 代码:
```rust
let hello = String::from("Здравствуйте");
let answer = &hello[0];
```
咱们已经知道 `answer` 不会是 `З`,即第一个字母。当以 UTF-8 编码时,`З` 的第一个字节是 `208`,第二个字节是 `151`,因此 `answer` 似乎应是 `208`,但 `208` 本身并不是个有效的字符。在用户询问该字符串的首字母时,返回 `208` 可能并非用户所期望的结果;然而,这是 Rust 在字节索引 `0` 处唯一拥有的数据。用户通常不想要返回的字节值,即使该字符串仅包含拉丁字母:若 `&"hi"[0]` 是返回字节值的有效代码,他将返回 `104`,而非 `h`
因此答案是为避免返回非预期值而引发可能不会立即被发现的错误Rust 根本不会编译这段代码,从而在开发过程早期就防止误解。
### 字节、标量值与字素簇!我的天!
**Bytes and Scalar Values and Grapheme Clusters! Oh My!**
关于 UTF-8 的另一要点,是从 Rust 角度来看,实际上存在三种相关的字符串表示方式:
- 字节;
- 标量值
- 以及字素簇(与我们所说的 *字母* 最接近的概念)。
在我们看到以梵文Devanagari书写的印地语单词 “नमस्ते” 时,他会被存储为一个 `u8` 值的矢量,看起来像这样:
```rust
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164, 224, 165, 135]
```
这是 18 个字节,也是计算机最终存储这个数据的方式。若我们将其视为 Unicode 的标量值,即 Rust 的 `char` 类型,那么这些字节看起来如下:
```rust
['न', 'म', 'स', '्', 'त', 'े']
```
这里有六个 `char` 值,但第四和第六个不属于字母:他们是独立存在时没有意义的音标符号。最后,在我们将其视为字素簇是,我们就会得到人类所说的,构成印地语单词的四个字母:
```rust
["", "", "स्", "ते"]
```
Rust 提供了解析计算机所存储原始字符串数据的多种方法,以便每个程序都能根据需要选择合适的解析方式,而无论数据使用的何种人类语言。
Rust 不允许我们通过索引访问字符串中某个字符的最后一个原因,是索引操作被认为始终会消耗常数时间的复杂度(即 `O(1)` )。但对于 `String` 而言,无法保证这种性能,因为 Rust 必须从开头遍历到索引位置的内容,以确定其间有效字符的数量。
## 字符串切片
**Slicing Strings**
字符串索引操作通常是个糟糕的主意因为字符串索引操作的返回类型并不明确是字节值、字符、字素簇还是个字符串切片。因此在咱们确实需要使用索引创建字符串切片时Rust 会要求咱们更加明确。
与其将 `[]` 与单个数字一起使用,咱们可将 `[]` 与某个范围一起使用,创建出包含特定字节的字符串切片:
```rust
let hello = String::from("Здравствуйте");
let s = &hello[0..4];
```
这里,`s` 将是个包含着该字符串前四个字节的 `&str`。早先我们提到,这些字符是两个字节,这意味着 `s` 将是 `Зд`
若我们尝试以类似 `&hello[0..1]` 的方式只切取该某个字符字节部分时Rust 将以访问某个矢量值中无效索引时的同一方式终止运行:
```console
$ cargo run
Compiling string_demo v0.1.0 (/home/hector/rust-lang-zh_CN/projects/string_demo)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.10s
Running `target/debug/string_demo`
thread 'main' panicked at src/main.rs:3:19:
byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `Здравствуйте`
```
在以范围创建字符串切片时,咱们应谨慎操作,因为此类操作可能导致咱们的程序崩溃。
## 迭代字符串的方法
**Methods for Iterating Over Strings**
对字符串片段进行操作的最佳方式,是明确指定咱们是要字符还是字节。对于单个的 Unicode 标量值,就要使用 `chars` 方法。对 “Зд” 调用 `chars` 方法,会将其拆分并返回两个 `char` 类型的值,咱们就可以遍历结果,访问每个元素:
```rust
for c in "नमस्ते".chars() {
println!("{}", c);
}
```
这段代码将打印以下内容:
```rust
```
或者,`bytes` 方法会返回可能适合咱们领域的各个原始字节:
```rust
for b in "Зд".bytes() {
println!("{b}");
}
```
该代码将打印出构成这个 `String` 的 18 个字节来:
```console
208
151
208
180
```
但请务必记住,有效的 Unicode 标量值可能由多个字节组成。
从字符串中获取字素簇,如上面对梵文那样,较为复杂,因此标准库未提供此功能。若咱们需要此功能,可在 [crates.io](https://crates.io/) 上找到相关代码箱。
## 字符串并不简单
**Strings Are Not So Simple**
总而言之字符串是复杂的。不同编程语言在向程序员呈现这种复杂性方面会做出不同的选择。Rust 选择将正确处理 `String` 数据,作为所有 Rust 程序的默认行为,这意味着程序员在开发初期,必须更多地考虑如何处理 UTF-8 数据。这种权衡虽然比其他编程语言暴露了更多字符串的复杂性,但他防止了咱们在开发生命周期的后期,不得不处理那些涉及非 ASCII 字符的错误。
好消息是,标准库提供了大量基于 `String``&str` 类型的功能,帮助正确处理这些复杂情况。建议查阅文档,了解一些有用方法,例如用于在字符串中检索的 `contains` 方法,以及用于将字符串的一部分替换为另一字符串的 `replace` 方法等。
让我们转向一个稍微简单一点的主题:哈希图!
End

View File

@@ -0,0 +1,249 @@
# 使用矢量值存储值的清单
**Storing Lists of Values with Vectors**
我们将介绍的首个集合类型为 `Vec<T>`,也称为 *矢量*。矢量值允许咱们将多个值,存储在会将全部这些值,依次存放于内存里的单个数据结构中。矢量只能存储相同类型的值 <sup>1</sup>。当咱们需要存储某个项目清单,例如某个文件中的文本行或购物车中的商品价格时,矢量值就非常有用。
> **译注**:与后面的 [哈希图](./hash_maps.md) 一样,矢量值也是同构的。
## 创建一个新矢量值
要创建一个新的空矢量值,我们要调用 `Vec::new` 函数,如下清单 8-1 所示。
```rust
let v: Vec<i32> = Vec::new();
```
*清单 8-1创建一个新的、空矢量值存储类型 `i32` 的一些值*
请注意我们在这里添加了类型注解。由于我们并未向该矢量中插入任何值Rust 就不清楚我们打算存储何种类型元素。这一点非常重要。矢量值是运用泛型实现的;我们将在第 10 章中,涉及如何对咱们自己的类型使用泛型。目前只需了解,标准库提供的 `Vec<T>` 类型可以存储任何类型。当我们要创建某个存储特定类型的矢量值时,我们可在尖括号里指定出该类型。在列表 8-1 中,我们就已告知 Rust`v` 中的 `Vec<T>` 将存储 `i32` 类型的元素。
大多数情况下,咱们将创建出某个带有一些初始值的 `Vec<T>`,同时 Rust 将推断出咱们想要存储的值类型因此咱们很少需要这种类型的注解。Rust 善解人意地提供了 `vec!` 这个宏,其将创建出一个新矢量值,并存储咱们给到他的那些值。下面清单 8-2 就会创建一个存储了值 `1``2``3` 的新 `Vec<i32>`。其中整数类型为 `i32`,因为这是默认的整数类型,正如我们在第 3 章 [“数据类型”](../programming_concepts/data_types.md) 小节所讨论的。
```rust
let v = vec! [1, 2, 3];
```
*清单 8-2创建一个包含了一些值的新矢量*
由于我们已给出了那些初始 `i32`Rust 便可推断出 `v` 的类型为 `Vec<i32>`,因此类型注解就不必要了。接下来,我们将探讨如何修改某个矢量。
## 更新矢量值
要创建某个矢量并随后向其添加元素,我们可使用 `push` 方法,如下清单 8-3 所示。
```rust
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
```
*清单 8-3使用 `push` 方法把一些值添加到某个矢量*
正如在第 3 章中曾讨论过的,与任何变量一样,在我们想要能修改其值时,我们就需要使用 `mut` 关键字令其成为可变的。我们放入其中的数字均为 `i32` 类型,而 Rust 会从该数据推断出这点,因此我们无需添加 `Vec<i32>` 的注解。
## 读取矢量的元素
**Reading Elements of Vectors**
引用存储在矢量中某个值的方式有两种:
- 经由索引;
- 或使用 `get` 方法。
在下面的示例中,我们已注解了这些函数所返回值的类型,以增加辨识度。
下面列表 8-4 展示了访问矢量中某个值的两种方法,即通过索引语法与使用 `get` 方法。
```rust
let v = vec! [1, 2, 3, 4];
let third: &i32 = &v[2];
println! ("第三个元素为 {}", third);
match v.get(2) {
Some(third) => println! ("第三个元素为 {}", third),
None => println! ("没有第三个元素。"),
}
```
*清单 8-4使用索引语法与 `get` 方法,访问矢量中的某个项目*
> **译注**:与 `get` 一样,`[]` 这个运算符同样是个方法。
请留意以下几点。我们使用索引值 `2` 获取第三个元素,因为矢量是以数字索引的,从零开始。使用 `&``[]` 给到我们到索引值处元素的引用。当我们以作为参数传递的索引,使用 `get` 方法时,我们会得到一个可与 `match` 使用的 `Option<&T>`
Rust 提供了这两种引用某个元素的方式,这样在尝试使用超出现有元素范围的索引值时,咱们就可以选择程序的行事方式。例如,我们来看看在我们有个五元素的矢量,然后尝试使用两种方式分别访问索引为 `100` 的元素时会发生什么,如下清单 8-5 所示。
```rust
let v = vec! [1, 2, 3, 4, 5];
let does_not_exist = &v[100];
let does_not_exist = v.get(100);
```
*清单 8-5在包含五个元素的某个矢量中尝试访问索引 `100` 处的元素*
当我们运行这段代码时,由于其中第一个 `[]` 方法引用了某个不存在的元素,因此其将造成该程序死机。在咱们希望咱们的程序,在尝试访问矢量末尾之后某个元素时要崩溃的情况下,就最好使用这个方法。
而当传递给 `get` 方法的是个超出该矢量范围的索引时,他会在不死机下返回 `None`。在正常情况偶尔需要访问矢量范围之外的某个元素时,咱们就要使用这个方法。此时咱们的代码,将有着处理 `Some(&element)``None` 情形代码,正如曾在第 6 章中讨论过的。例如,索引可能来自用户输入的数字。在用户不小心输入了某个过大的数字,程序得到一个 `None` 值时,咱们就可以告知用户,当前矢量中有多少元素,并给他们另一次输入有效值的机会。这比因输入错误而导致程序崩溃要用户友好得多!
当程序有了某个有效引用时,借用检查器会强制执行所有权及借用规则检查(第 4 章中讲到的),确保该引用以及到矢量内容的任何其他引用保持有效。回顾一下那条规则:在同一作用域内,咱们不能同时有着可变和不可变引用。这条规则适用于下面示例 8-6其中我们有个到某矢量中首个元素的不可变引用并尝试添加一个元素到其末尾。若我们在函数的稍后还尝试引用该元素那么该程序将不会工作。
```rust
let mut v = vec! [1, 2, 3, 4, 5];
let first: &i32 = &v[0];
v.push(6);
println! ("首个元素为:{}", first);
```
*清单 8-6在保留到矢量某个条目的引用同时尝试将一个元素添加到该矢量*
编译这段代码将得到如下报错:
```console
$ cargo run
Compiling vec_demo v0.1.0 (/home/peng/rust-lang/projects/vec_demo)
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> src/main.rs:6:5
|
4 | let first: &i32 = &v[0];
| - immutable borrow occurs here
5 |
6 | v.push(6);
| ^^^^^^^^^ mutable borrow occurs here
7 |
8 | println! ("首个元素为:{}", first);
| ----- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `vec_demo` due to previous error
```
清单 8-6 中的代码看起来似乎应该工作:为什么到首个元素的引用,会关心该矢量末尾的更改?这个报错是由于矢量的工作方式造成的:因为矢量会把值连续地存储在内存中,而向矢量末尾添加新元素就可能需要分配新的内存,在该矢量当前存储的位置没有连续存放全部元素的足够空间时,那些原有元素就会被拷贝到新的内存空间。在这种情况下,到首个元素的引用,就会指向已释放内存。借用规则阻止了程序陷入这种情况。
> **请注意**:更多有关 `Vec<T>` 类型的实现细节,请参考 [Rust 专论The Rustonomicon](https://doc.rust-lang.org/nomicon/vec/vec.html)。
## 迭代矢量中的值
**Iterating over the Values in a Vector**
要依次访问矢量中的每个元素,我们就要遍历所有元素,而不是使用索引一次访问一个元素。下面清单 8-7 展示了如何使用 `for` 循环,获取到某个 `i32` 值矢量中,各个元素的不可变引用并打印出来。
```rust
let v = vec! [100, 32, 57];
for i in &v {
println! ("{}", i);
}
```
*清单 8-7使用 `for` 循迭代元素,打印某矢量中的各个元素*
我们还可以遍历到可变矢量中,各个元素的可变引用,对所有元素进行更改。下面清单 8-8 中的 `for` 循环,将把 `50` 加到每个元素。
```rust
let mut v = vec! [100, 32, 57];
for i in &mut v {
*i += 50;
}
```
*清单 8-9对某个矢量中元素的可变引用进行迭代*
要修改可变引用所指向的值,我们必须先使用 `*` 这个解引用运算符the `*` dereference operator获取 `i` 中的值,然后才能使用 `+=` 这个运算符。我们将在第 15 章 [“顺着指针找到值”](../smart_pointers/deref-t.md#顺着指针找到值) 小节,详细介绍这个解引用运算符。
无论是可变还是不可变地遍历矢量,他们都是安全的,因为借用检查器的规则确保了这点。若我们在列表 8-7 与列表 8-8 的 `for` 循环体内,尝试插入或移除元素,我们就会得到与列表 8-6 中代码类似的编译器报错。到 `for` 循环所持有矢量的引用会阻止对整个矢量的同时修改the reference to the vector that the `for` loop holds prevents simultaneous modification of the whole vector。
## 使用枚举存储多种类型
**Using an Enum to Store Multiple Types**
矢量值只能存储同一类型的值。这可能会造成不便;在某些情况下,我们肯定需要存储一些不同类型的条目列表。幸运的是,枚举变种是在同一枚举类型下定义的,因此当我们需要表示不同类型的元素一中类型时,我们可以定义并使用一个枚举!
例如,我们要从电子表格的某行中获取到一些值,该行中某些列包含了整数、浮点数与字符串。我们可以定义一个其变种包含不同值类型的枚举,而所有枚举变种将被视为同一类型:即该枚举的类型。然后,我们可以创建一个保存该枚举的矢量值,并最终保存了不同的类型。我们已在下面清单 8-9 中,演示了这点。
```rust
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec! [
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
```
*清单 8-9定义在一个矢量值中存储不同类型值的 `enum`*
Rust 需要在编译时就知道,矢量值中将包含哪些类型,这样他才能准确地知道,堆上需要多少内存来存储各个元素。我们还必须显式指出,该向量容许哪些类型。若 Rust 允许某个矢量值存储任何类型,那么在对该矢量值的元素进行操作时,其中一种或多种类型就有可能导致错误。使用枚举与 `match` 表达式,意味着 Rust 将在编译时,确保每一种可能情形都得以处理,正如第 6 章中曾讨论的。
在咱们不直到在运行时程序将获得的要存储在某个矢量中的所有类型集时,那么这种枚举技巧就没有用。相反,咱们可以使用 [特质对象](../oop/trait_objects.md),我们将在第 18 章中介绍。
现在我们已经讨论了矢量值的一些最常见用法,请务必查看 [API 文档](https://doc.rust-lang.org/std/vec/struct.Vec.html),了解标准库为 `Vec<T>` 定义的所有实用方法。例如,除 `push` 方法外,`pop` 方法会移除并返回最后的元素。
## 弃用矢量会弃用其元素
**Dropping a Vector Drops Its Elements**
与其他 `struct` 一样,在矢量值超出作用域时,他也会被释放,如同下面清单 8-10 中所注释的那样。
```rust
{
let v = vec! [1, 2, 3, 4];
// 对 v 执行一些操作
} // 这里 v 就超出了作用域,而被释放掉
```
*清单 8-10展示矢量值及其元素在何处被弃用*
当该矢量值被弃用时,其所有内容也会被弃用,这意味着他所保存的那些整数将被清理掉。借用检查器会确保到某个矢量值内容的全部引用,只有在这个矢量值本身有效期间才会被用到。
让我们移步下一集合类型: `String`
End

View File

@@ -0,0 +1,55 @@
# `Sync` 与 `Send` 两个特质下的可扩展并发
**Extensible Concurrency with the `Sync` and `Send` Traits**
有趣的是Rust 语言并发方面的特性 *非常* 少。本章中到目前为止咱们讲到过的每种并发特性,都已是标准库而非语言本身的一部分。用于处理并发问题的选项,并不局限于这门语言或标准库;咱们可以编写自己的并发特性,或可以使用由其他人编写的并发特性。
不过,在这门语言中,是嵌入了两个并发概念的:即 `std::marker` 特质 `Sync``Send`
## 使用 `Send` 特质实现线程间所有权转移
**Allowing Transference of Ownership Between Threads with `Send`**
这个 `Send` 标识符特质,表示实现 `Send` 类型值的所有权,可以在线程间转移。几乎全部 Rust 类型都是 `Send` 类型,但有一些例外,包括 `Rc<T>`:由于在咱们克隆了某个 `Rc<T>` 并尝试将这份克隆的所有权,转移到另一线程时,两个现场可能在同一时间更新引用计数,因此 `Rc<T>` 就不能是 `Send` 类型。由于这个原因,`Rc<T>` 正是为其间咱们不打算付出线程安全方面性能开销的那些单线程情形,而实现的。
由此Rust 的类型系统与特质边界type system and trait bounds就确保了咱们绝不会意外地将某个 `Rc<T>`,不安全地跨越线程发送。当咱们在清单 16-14 中尝试这样做时,咱们就曾得到编译器报错 `` the trait `Send` is not implemented for `Rc<Mutex<i32>>` ``。而在咱们切换到 `Arc<T>` 这种 `Send` 类型时,那段代码就编译了。
由全部 `Send` 类型所组成的类型,也会被自动标记为 `Send` 类型。除开那些原始指针raw pointers 外,那么可以说几乎全部原生类型都是 `Send` 的,咱们将在第 19 章中,讲到那些原始指针。
## 使用 `Sync` 实现来自多个线程的访问
**Allowing Access from Multiple Threads with `Sync`**
`Sync` 标识符表示实现 `Sync` 特质的类型,其被从多个线程引用是安全的。换句话说,任何类型 `T` 在 `&T` (即到 `T` 的不可变引用) 为 `Send` 的时,那么其即为 `Sync` 的,表示该引用可以安全地发送到另一线程。与 `Send` 类似,原生类型均为 `Sync` 的,且由全部都是 `Sync` 的类型所组成的类型,也都是 `Sync` 的。
灵巧指针 `Rc<T>` 因为其不是 `Send` 的同样原因,其也不是 `Sync` 的。`RefCell<T>` 类型(咱们曾在第 15 章讲过)以及相关的 `Cell<T>` 类型家族,都不是 `Sync` 的。`RefCell<T>` 在运行时所完成的借用检查实现,不是线程安全的。灵巧指针 `Mutex<T>` 是 `Sync` 的,并正如咱们在 [于多个线程间共用 `Mutex<T>`](#在多个线程间共用-mutext) 小节中看到的,其可被用于多个线程下共用访问。
## 手动实现 `Send` 与 `Sync` 是不安全的
**Implementing `Send` and `Sync` Manually Is Unsafe**
由于 `Send` 与 `Sync` 特质构成的类型自动也是 `Send` 与 `Sync` 的,因此咱们大可不必手动实现这两个特质。而作为标记性特质,二者甚至都没有任何要实现的方法。他们只是在执行与并发性有关的不变性方面很有用。
手动实现这两个特质,涉及到实现一些不安全 Rust 代码unsafe Rust code。在第 19 章咱们将讲到运用不安全 Rust 代码;至于现在,要点在于构造不是由一些 `Send` 与 `Sync` 部分组成的新并发类型,需要深思熟虑来维持那些安全保证。[The Rustonomicon](https://doc.rust-lang.org/nomicon/index.html) 有着这些保证的更多信息,以及维持这些保证的方式。
# 本章小节
这不会是你在本书中将见到并发的最后一章:第 20 张中的那个项目,就将在相比于这里所讨论过较小示例,而更具现实意义的情形下用到本章中的那些概念。
正如早先所提到的,由于只有极少量的 Rust 处理并发方式,属于这门语言的一部分,因此许多并发解决方案,都是作为代码箱实现的。这些方案相比标准库进化更为迅速,那么就要确保在线搜寻当前的、最前沿代码箱,来用于多线程情形中。
Rust 标准库提供了用于消息传递的信道,以及诸如 `Mutex<T>` 与 `Arc<T>` 等安全用于并发情景中的一些灵巧指针类型。类型系统与借用检查器,会确保应用了这些方案的代码,不会以数据竞争或无效引用结束。一旦让代码编译了,咱们就可以放下心来,代码将愉快地运行于多线程之上,而不会有在其他语言中常见的那些难于追踪的问题。并发编程自此不再是令人害怕的概念:去吧,让你的程序并发起来,无所畏惧!
接下来,咱们将讲到,随着咱们的 Rust 程序变得大了起来,建模问题与架构出方案的一些管用做法。此外,咱们将讨论 Rust 的一些习惯说法,这些说法可能与面向对象编程中所熟悉的有关。
End

View File

@@ -0,0 +1,278 @@
# 使用消息传递来在线程间传输数据
**Using Message Passing to Transfer Data Between Threads**
一种日渐流行的确保并发安全的方法,便是 *消息传递message passing*,其中线程或参与者,通过相互发送包含数据的消息进行通信。下面就是摘自 [Go 语言文档](https://golang.org/doc/effective_go.html#concurrency) 的一句口号:“勿要通过共用内存进行通信;而要经由通信来共用内存。”
为达成消息发送式的并发Rust 标准库提供了 *信道channels* 的一种实现。所谓信道,即数据被从一个线程,发送到另一线程,这种形式的一个通用编程概念。
咱们可以把编程中的信道,设想为带流向的水渠,如同一条小溪或一条河。在咱们把像是一只塑胶小黄鸭投入到一条小河中时,他就会顺流而下到达该水路的尽头。
信道有着两端:一个发送者和一个接收者。发送端即在将小黄鸭投入到河流中的上游位置,而接收端即为小黄鸭抵达的下游了。咱们代码中一个部分以打算发送的数据,调用发送者上的方法,而另一个部分则会查看接收端的抵达消息。在发射者或接收者端之一被弃用时,就算是信道被 *关闭closed* 了。
下面,咱们将完成有着一个线程生成一些值并将这些值发送到信道,同时有另一线程将接收这些值并将其打印出来的这么一个程序。咱们将在线程间使用信道发送一些简单值,来演示这项特性。一旦咱们熟悉了这项技巧,那么就可以对任何需要相互通讯的线程,比如聊天系统,或其中有许多线程执行着某项计算的各个部分,并把这些部分发送到结果汇总线程的系统等中使用信道。
首先,在下面的清单 16-6 中,咱们将创建出一个信道而不使用他来完成任何事情。请注意由于 Rust 无法分辨出咱们打算通过该信道,发送何种类型的值,该代码尚不会编译。
文件名:`src/main.rs`
```rust
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
}
```
*清单 16-6创建出一个信道并将两端赋值给 `tx` 与 `rx`*
咱们使用 `mpsc::channel` 函数,创建了一个新的信道;`mpsc` 表示的是 *多生产者单一消费者multiple producer, single consumer*。简而言之Rust 标准库实现信道的方式,表明信道可以有多个生成值的 *发送sending* 端,但只有一个消费这些值的 *接收receiving* 端。请设想有多条小溪,汇流到一条大河:那么从任何这些小溪,送下来的东西,都将在最后抵达一条河中。现在咱们将以单个的生产者开始,在令到这个示例工作起来时,就将添加多个生产者。
这个 `mpsc::channel` 函数返回的是一个元组,元组的首个元素为发送端 -- 发送器the transmitter -- 而第二个元素就是接收端 -- 接收器the receiver。两个缩写 `tx``rx`,传统上在许多领域,都相应地被用于表示 *transmitter**receiver*因此咱们就把这两个变量如此命名来表示两端。咱们使用了带有模式a pattern `(tx, rx)`)的一个 `let` 语句,对这个元组加以解构;在第 18 章中,咱们将讨论这种 `let` 遇见中模式的运用及解构问题。至于现在,请明白以这种方式使用 `let` 语句,是提取由 `mpsc::channel` 返回元组中那些部分的便捷方式。
下面就把其中的发射端,移入到一个生成线程,并让其发送一个字符串,从而生成线程就在与主线程通信了,如下清单 16-7 中所示。这就像是在河流上游投入一只小黄鸭,或是在一个线程发送了一条聊天消息给另一线程。
文件名:`src/main.rs`
```rust
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("你好");
tx.send(val).unwrap();
});
}
```
*清单 16-7把 `tx` 迁移到一个生成线程并发送 "你好"*
又一次,咱们使用了 `thread::spawn` 创建出一个新线程,并使用 `move``tx` 迁移进到那个闭包,于是这个生成的线程,便拥有了 `tx`。该生成线程需要拥有发送器,才能够经由信道发送消息。发送器有着取咱们打算发送值的一个 `send` 方法。而这个 `send` 方法返回的是个 `Result<T, E>` 类型值,那么在接收器已被弃用,而无处发送值时,那么发送操作就将返回一个错误。在此示例中,咱们调用了 `unwrap` 来在出现错误时终止运行panic in case of an error。而在真实应用中咱们应予以恰当处理请回到第 9 章,回顾那些那些适当的错误处理策略。
在下面的清单 16-8 中,咱们将自主线程中的接收器,获取到那个值。这就像是在河流尽头接收到小黄鸭,或是接收到一条聊天消息。
文件名:`src/main.rs`
```rust
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("你好");
tx.send(val).unwrap();
});
let received = rx.recv().unwrap();
println! ("收到:{}", received);
}
```
*清单 16-8在主进程中接收值 “你好” 并将其打印出来*
接收器有着两个有用方法:`recv``try_recv`。这里使用的是 `recv`,是 *receive* 的简写,该方法将阻塞主线程执行,而等待直到某个值被下发到信道。一旦某个值被发出,那么 `recv` 就会在一个 `Result<T, E>` 中将其返回。在发射器关闭时,`recv` 就会返回一个错误,表明不会再有值到来。
`try_recv` 方法则不会阻塞,而相反会立即返回一个 `Result<T, E>`:在消息可用时的一个保存着消息的 `Ok` 值,同时此刻没有任何消息时的一个 `Err` 值。在该线程在等待消息时,有其他工作要完成的情况下,使用 `try_recv` 便是有用的:咱们可以编写出每隔一段时间就调用 `try_recv` 的循环,在有消息时处理消息,再次检查是否收到消息之前的空隙,完成一些其他工作。
这里使用 `recv` 是为了简化;在主线程中,除了等待消息之外并无其他工作要做,因此阻塞主线程是恰当的。
当咱们运行清单 16-8 中的代码时,就会看到该值在主线程中被打印出来:
```console
收到:你好
```
好极了!
## 信道与所有权的转移
**Channels and Ownship Transference**
由于所有权规则帮助咱们编写出安全、并行的代码,因此其在消息发送中起着至关重要作用。在并发式编程中,于咱们 Rust 程序通篇考虑所有权的好处,就在于这样可以防止错误。接下来就要完成一项实验,来展示信道与所有权,是怎样一起运作以阻止问题发生的:咱们将在生成线程中,把一个 `val` 值送到信道 *之后after*,再尝试使用这个值。尝试编译清单 16-9 中的代码,来观察为何该代码是不被允许的:
文件名:`src/main.rs`
```rust
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("你好");
tx.send(val).unwrap();
println! ("val 为 {}", val);
});
let received = rx.recv().unwrap();
println! ("收到:{}", received);
}
```
*清单 16-9在咱们已将 `val` 发送到信道之后在尝试使用他*
在这里,咱们在经由 `tx.send` 已把 `val` 发出到信道之后,尝试打印出他。允许这样做将是个糟糕的主意:一旦该值已被发送到另一线程,那么在咱们尝试再度使用该值之前,发往的那个线程就可能修改或是弃用掉该值。而潜在地,另一线程的这些改动,就会由于不一致或不存在的数据,而造成错误或未预期结果。不过,在咱们尝试编译清单 16-9 中的代码时Rust 会给到咱们一个报错:
```console
$ cargo run  ✔  
Compiling mp_demo v0.1.0 (/home/peng/rust-lang/mp_demo)
error[E0382]: borrow of moved value: `val`
--> src/main.rs:13:31
|
11 | let val = String::from("你好");
| --- move occurs because `val` has type `String`, which does not implement the `Copy` trait
12 | tx.send(val).unwrap();
| --- value moved here
13 | println! ("val 为 {}", val);
| ^^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0382`.
error: could not compile `mp_demo` due to previous error
```
咱们犯下的并发错误就造成一个编译时报错。其中的 `send` 函数取得了其参数的所有权,进而在那个值被迁移时,接收器便取得了他的所有权。这就阻拦了咱们在发送了该值后,无意中地再度使用该值;所有权系统会检查各方面都妥当无虞。
## 发送出多个值并观察接收器的等待
**Sending Multiple Values and Seeing the Receiver Waiting**
清单 16-8 中的代码编译并运行了,不过其并未清楚地给出,两个单独线程是怎样通过信道相互交流的。在下面清单 16-10 中,咱们已做出将证实清单 16-8 中代码有在并发运行的一些修订:生成线程现在将发出多条消息,并在每条消息之间暂停一秒钟。
文件名:`src/main.rs`
```rust
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let vals = vec! [
String::from("你好"),
String::from(""),
String::from(""),
String::from("线程"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_millis(500));
}
});
for received in rx {
println! ("收到:{}", received);
}
}
```
<a name="listing-16-10"></a> *清单 16-10发送多条消息并在每次发送之间进行暂停*
这次,其中的生成线程,有着咱们打算发送到主线程的一个字符串矢量值。咱们对其迭代,而分别发送每条消息,同时通过使用 `500` 毫秒的一个 `Duration` 值,调用 `thread::sleep` 在每次消息发送间暂停。
在主线程中,咱们未再显式调用 `recv` 函数:相反,咱们将 `rx` 当作了迭代器。对于所接收到的每个值,咱们就将其打印出来。在信道被关闭时,迭代就结束了。
在运行清单 16-10 中的代码时,咱们就会看到下面每行之间有着 500ms 暂停的输出:
```console
收到:你好
收到:自
收到:此
收到:线程
```
由于咱们在主线程中的那个 `for` 循环中,并无任何暂停或延迟的代码,因此咱们就可以说,主线程是在等待接收来自生成线程的那些值。
## 通过克隆发射器创建出多个生产者
**Creating Multiple Producers by Cloning the Transmitter**
早前咱们曾提到,`mpsc`*multiple producer, single consumer* 的首字母缩写。接下来就要就要运用上 `mpsc`,并将清单 16-10 中的代码,扩充为创建出均将一些值发送到同一接收器的多个线程。通过克隆发射器,咱们就可以这样做,如下清单 16-11 中所示:
文件名:`src/main.rs`
```rust
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
let tx1 = tx.clone();
thread::spawn(move || {
let vals = vec! [
String::from("你好"),
String::from(""),
String::from(""),
String::from("线程"),
];
for val in vals {
tx1.send(val).unwrap();
thread::sleep(Duration::from_millis(500));
}
});
thread::spawn(move || {
let vals = vec! [
String::from(""),
String::from(""),
String::from("一些别的"),
String::from("消息"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_millis(500));
}
});
for received in rx {
println! ("收到:{}", received);
}
}
```
*清单 16-11从多个生产者发出多条消息*
这次在创建出首个生成线程之前,咱们调用了发射器上的 `clone` 方法。这样做将给到咱们可传递给那首个生成线程的一个新发射器。咱们把原先的发射器,传递给了第二个生成线程。这样就给到了咱们两个线程,二者都把不同消息,发送到那一个的接收器。
在运行此代码时,咱们的输出看起来应像下面这样:
```console
收到:你好
收到:给
收到:自
收到:你
收到:此
收到:一些别的
收到:线程
收到:消息
```
根据咱们所在系统的不同,也可能会看到另外顺序的这些值。这种消息每次出现顺序的不一致,正是令到并发有趣而又有难度的地方。而若带上 `thread::sleep` 加以实验,即在两个不同线程中给到不同睡眠值,这时的每次运行,将更具不确定性,而每次运行都造成不同输出。
既然咱们已经看到了信道的工作原理,那么接下来就要看看一种方式迥异的并发了。
End

View File

@@ -0,0 +1,273 @@
# 状态共用的并发
**Shared-State Concurrency**
消息传递是处理并发的一种很好方式,但其并非唯一的一种。另一种方式将是,多个线程访问同一共用数据。请重新考虑一下摘自 Go 语言文档的那句口号的这个部分“勿要经由共用内存通信。do not communicate by sharing memory.”
那么经由共用内存的通信,又会是怎样的呢?另外,为何消息传递方式拥趸们,会警告不要使用内存共用方式呢?
在某种程度上,任何编程语言中的信道,均类似于单一所有权,因为一旦咱们把值传递到信道,那么就不应再使用那个值了。内存共用的并发,则像是多重所有权:多个线程均可在同一时间,访问同一内存位置。正如咱们在第 15 章中曾见到过的那里的灵巧之中令到多重所有权可行多重所有权会因为这些不同所有者需要管理而增加复杂度。Rust 的类型系统与所有权规则极大地助力了实现这样的管理正确无误。作为一个示例接下来咱们就要看看作为共用内存的一种更常见并发原语即所谓的互斥量for an example, let's look at mutexes, one of the more common concurrency primitives for shared memory。
## 运用互斥量实现一个时间仅允许一个线程访问数据
**Using Mutexes to Allow Access to Data from One Thread at a Time**
*互斥mutex**相互排斥mutual exclusion* 的缩写,正如互斥量在任何给定时间,都只允许一个线程访问某个数据。要访问互斥量中的数据,线程就必须首先通过询问来获取到该互斥量的 *lock*表明其打算访问。所谓锁则是保持着当前是谁哪个线程有着对该数据排他性访问的追踪作为该互斥量一部分的一种数据结构the lock is a data structure that is part of the mutex that keeps track of who currently has exclusive access to the data。因此所谓互斥量就被描述为经由这种加锁系统*守护着guarding* 其所保存着的数据。
由于咱们务必要记住以下两条规则,互斥量便有了难以运用的名声:
- 在使用数据之前,咱们必须尝试获取到锁;
- 在完成互斥量所保护数据的操作时,咱们必须解开该数据,以便其他线程能够获取到锁。
至于互斥量的真实世界比喻,请设想在仅有一只麦克风的会议上的一个小组讨论。那么在小组成员能发言之前,他们就不得不请求或表明,他们打算使用麦克风。在他们得到麦克风时,他们便可以想要讲多长时间便讲多长时间,并在随后吧麦克风,递给下一位要求发言的小组成员。在某名小组成员于用完麦克风,却忘记交出麦克风时,就没有人能发言了。在这个共用麦克风的管理出错时,这个小组就将不会如计划那样运作了!
互斥量的管理非常棘手,难以做到正确无误,这正是许多人热衷于信道的原因。但是,归功于 Rust 的类型系统与所有权规则,咱们就无法在互斥量的加锁与解锁上出错了。
## `Mutex<T>` 的 API
下面是如何使用互斥量的一个示例,接下来咱们就要如下面清单 16-12 中所给出的那样,通过在单一线程情形下使用互斥量开始:
文件名:`src/main.rs`
```rust
use std::sync::Mutex;
fn main() {
let m = Mutex::new(5);
{
let mut num = m.lock().unwrap();
*num = 6;
}
println! ("m = {:?}", m);
}
```
*清单 16-12为简化目的在单个线程情形下探讨 `Mutex<T>` 的 API*
与许多类型一样,咱们使用关联函数 `new` 创建出了一个 `Mutex<T>`。而为了访问这个互斥量内部的数据,咱们使用了 `lock` 方法来获取到锁。此调用将阻塞当前线程,从而在轮到咱们拥有锁之前,当前线程就无法完成任何工作。
若有另一持有着锁的线程已终止运行,那么到 `lock` 的调用就会失败。在那种情况下,就没人能获得锁了,因此咱们就选择了 `unwrap`,而在咱们陷入到那样的情形时,让这个线程终止运行。
在获取到锁后,咱们就可以对此示例中名为 `num` 的返回值,作为到互斥量内部数据的可变引用,而加以处理了。类型系统会确保咱们在使用 `m` 里的值前,获取到锁。`m` 的类型为 `Mutex<i32>`,而非 `i32`,因此咱们为了使用那个 `i32` 值, 就 *必须* 调用 `lock`。这是不能忘掉的;否则类型系统就不会让咱们访问那个内层的 `i32`
正如咱们可能怀疑的那样,`Mutex<T>` 是个灵巧指针。更准确地讲,到 `lock` 的调用,*返回的是* 封装在咱们曾以到 `unwrap` 调用处理的 `LockResult` 中,一个叫做 `MutexGuard` 的灵巧指针。`MutexGuard` 灵巧之中实现了 `Deref`,来指向咱们的内层数据;这个灵巧指针还有着在 `MutexGuard` 超出作用域,即清单 16-12 的示例内存作用域结束处所发生时,自动释放锁的一个 `Drop` 实现。而其结果就是,由于锁的释放是自动发生的,因此咱们就不会面临,忘记释放锁而阻塞该互斥量为其他线程使用的风险。
在弃用了该所之后,咱们就可以打印出该互斥量的值,并看到咱们是能够把那个内层的 `i32`,修改为 `6` 的。
## 在多个线程间共用 `Mutex<T>`
现在,咱们就来尝试使用 `Mutex<T>`,在多个线程见共用值。咱们将启动 10 个线程,并让他们分别都把一个计数器增加 `1`,因此那个计数器就会从 `0` 到达 `10`。接下来清单 16-13 中的示例,将有着一个编译器报错,同时咱们将使用那个报错,来掌握更多有关使用 `Mutex<T>`,以及 Rust 如何帮助咱们正确运用他的知识。
文件名:`src/main.rs`
```rust
use std::sync::Mutex;
use std::thread;
fn main() {
let counter = Mutex::new(0);
let mut handles = vec! [];
for _ in 0..10 {
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println! ("结果为:{}", *counter.lock().unwrap());
}
```
*清单 16-13各自分别对由 `Mutex<T>` 所守护计数器递增的十个线程*
与在清单 16-12 中一样,咱们创建出了一个在 `Mutex<T>` 内,保存着一个 `i32``counter` 变量。接下来,咱们通过对数字范围的迭代,创建出了 10 个线程。咱们使用了 `thread::spawn`,并给到全部线程同样闭包:把那个计数器迁移进到线程,通过调用 `lock` 方法取得那个 `Mutex<T>` 上的锁,并于随后加 `1` 到该互斥量的值的这样一个闭包。在线程完成运行其闭包时,`num` 就会超出作用域而释放那把锁,从而另一线程便可以取得该锁。
在主线程中咱们收集起了所有连接把手collect all the join handles。随后如同在清单 16-2 中所做的那样,咱们在各个把手上调用了 `join`,来确保所有现场运行完毕。在那个点位处,主线程将取得那把锁,并打印出该程序的结果。
咱们曾暗示过此示例不会编译。现在就来找出原因为何!
```console
$ cargo run  ✔  
Compiling mutex_demo v0.1.0 (/home/peng/rust-lang/mutex_demo)
error[E0382]: use of moved value: `counter`
--> src/main.rs:12:36
|
8 | let counter = Mutex::new(0);
| ------- move occurs because `counter` has type `Mutex<i32>`, which does not implement the `Copy` trait
...
12 | let handle = thread::spawn(move || {
| ^^^^^^^ value moved into closure here, in previous iteration of loop
13 | let mut num = counter.lock().unwrap();
| ------- use occurs due to use in closure
For more information about this error, try `rustc --explain E0382`.
error: could not compile `mutex_demo` due to previous error
```
这个报错消息指出,其中的 `counter` 值在循环的上一次迭代中已被迁移。Rust 正告诉咱们,不能将锁 `counter` 的所有权,迁移进到多个线程中。下面就来使用第 15 张中曾讨论过的多重所有权方式,修正这个编译器报错。
## 多线程下的多重所有权
**Multiple Ownership with Multiple Threads**
在第 15 章中,咱们曾通过使用灵巧指针 `Rc<T>`,来创建出一个引用计数的值,而将一个值赋予到多个所有者。下面就来完成那同样的操作,并看到会发生什么。咱们将在清单 16-14 中,把那个 `Mutex<T>` 封装在 `Rc<T>` 中,并在把所有权迁移到线程之前,克隆这个 `Rc<T>`
文件名:`src/main.rs`
```rust
use std::rc::Rc;
use std::sync::Mutex;
use std::thread;
fn main() {
let counter = Rc::new(Mutex::new(0));
let mut handles = vec! [];
for _ in 0..10 {
let counter = Rc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println! ("结果为:{}", *counter.lock().unwrap());
}
```
*清单 16-14尝试使用 `Rc<T>` 来实现多个线程拥有那个 `Mutex<T>`*
又一次,咱们编译并得到......一些不同的报错!编译器给了咱们很多指教。
```console
$ cargo run  ✔  
Compiling mutex_demo v0.1.0 (/home/peng/rust-lang/mutex_demo)
error[E0277]: `Rc<Mutex<i32>>` cannot be sent between threads safely
--> src/main.rs:14:36
|
14 | let handle = thread::spawn(move || {
| ------------- ^------
| | |
| ______________________|_____________within this `[closure@src/main.rs:14:36: 14:43]`
| | |
| | required by a bound introduced by this call
15 | | let mut num = counter.lock().unwrap();
16 | |
17 | | *num += 1;
18 | | });
| |_________^ `Rc<Mutex<i32>>` cannot be sent between threads safely
|
= help: within `[closure@src/main.rs:14:36: 14:43]`, the trait `Send` is not implemented for `Rc<Mutex<i32>>`
note: required because it's used within this closure
--> src/main.rs:14:36
|
14 | let handle = thread::spawn(move || {
| ^^^^^^^
note: required by a bound in `spawn`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `mutex_demo` due to previous error
```
喔,那报错消息真的非常罗嗦!而这些才是要关注的重要部分:`` `Rc<Mutex<i32>>` cannot be sent between threads safely ``。编译器还告诉咱们了其原因:`` the trait `Send` is not implemented for `Rc<Mutex<i32>>` ``。下一小节咱们就要讲到 `Send` 特质:他是确保咱们用到类型,是意图用于并发情形的特质之一。
不幸的是,`Rc<T>` 于跨线程的共用上是不安全的。在 `Rc<T>` 管理着引用计数时,他会增加每次到 `clone` 调用的计数并在每个克隆被弃用时减去计数。但其并未使用任何并发原语any concurrency primitives来确保那些对该计数的改变不被另一线程中断。这就会导致错误的计数 -- 进而会导致内存泄漏,或在咱们未完成值处理之前,该值就已被启用这样的一些微妙代码缺陷。咱们所需要的,是像极了 `Rc<T>`,但会令到引用计数以线程安全方式得以改变的一种类型。
> **注**简单地说与各种编程语言中的那些原生数据类型primitive data types 一样所谓并发原语concurrency primitives指的就是用于并发编程的一些基本设施the basic facilities for concurrent programming这样的说法某种程度上是跨越某个语言家族比如 C 语言家族)。
>
> 参考:[What-are-concurrency-primitives-"K Symbol"](https://qr.ae/prtpz6)
## `Arc<T>` 下的原子引用计数
**Atomic Reference Counting with `Arc<T>`**
幸运的是,`Arc<T>` *正是* 安全用于并发情形下的一个像是 `Rc<T>` 的类型。其中的 `a` 代表着 `原子atomic`,表示其是一种 *原子的引用计数atomically reference counted* 类型。原子类型是咱们不会在此详细讨论的一类额外并发原生类型:请参阅 [`std::sync::atomic` 的标准库文档](https://doc.rust-lang.org/std/sync/atomic/index.html),了解更多细节。此刻,咱们只需要知道这些原子类型会像那些原生类型一样运作,只不过他们对于跨线程的共用是安全的。
到这里咱们可能想知道,为何全部原生类型不是原子的,以及为何标准库的那些类型,没有默认使用 `Arc<T>` 实现。原因就是线程安全自带了性能损失,而只有在咱们真的需要线程安全时,才会打算付出。在咱们只是在单线程里于一些值上执行操作时,若咱们的代码不必强制实现原子类型所提供的那些保证,那么这些代码就可以运行得快得多。
接下来回到那个示例:`Arc<T>` 与 `Rc<T>` 有着同样的 API因此通过修改其中的 `use` 语句行、到 `new` 的调用,以及到 `clone` 的调用,咱们就可以修复那个程序。清单 16-15 中的代码最终将会编译及运行:
文件名:`src/main.rs`
```rust
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec! [];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println! ("结果为:{}", *counter.lock().unwrap());
}
```
*清单 16-15为能够跨越多线程地共用所有权而使用 `Arc<T>` 来封装那个 `Mutex<T>`*
此代码将打印出下面的内容:
```console
结果为10
```
咱们就做到了!咱们从 `0` 计数到了 `10`,这或许看起来不是非常印象深刻,但他真的教会了咱们很多有关 `Mutex<T>` 与线程安全的东西。咱们也可以运用这个程序的架构,完成相比于增加计数器,一些更为复杂的操作。运用这种策略,咱们可把某项计算,划分为一些独立部分,将这些部分拆解为多个线程,并于随后使用 `Mutex<T>` 来让各个各个线程,使用其自己部分对最终结果加以更新。
请注意若咱们是在完成一些简单的数字运算,你们就有由 [标准库的 `std::sync::atomic` 模组](https://doc.rust-lang.org/std/sync/atomic/index.html) 所提供的,相较于 `Mutex<T>` 更简单的一些类型。这些类型提供到原生类型安全、并发、原子的访问。咱们为这个示例而选择带有原生类型的 `Mutex<T>`,目的是可以着重于 `Mutex<T>` 的工作原理。
### `RefCell<T>`/`Rc<T>` 与 `Mutex<T>`/`Arc<T>` 二者之间的相似点
**Similarities Between `RefCell<T>`/`Rc<T>` and `Mutex<T>`/`Arc<T>`**
咱们或许已经留意到,其中那个 `counter` 是不可变的,但咱们却能获取到其内部值的可变引用;这意味着与 `Cell` 家族the `Cell` family 所做的一样, `Mutex<T>` 提供了内部可变性。与咱们在第 15 章中曾使用 `RefCell<T>` 来实现修改 `Rc<T>` 内部内容同样的方式,咱们使用了 `Mutex<T>` 来修改 `Arc<T>` 内部内容。
另一个需要注意的细节,便是在咱们使用 `Mutex<T>` 时Rust 无法保护咱们免于全部类别的逻辑错误。回顾在第 15 章中,`Rc<T>` 运用就伴随着创建出循环引用风险,其中两个 `Rc<T>` 值相互指向,导致内存泄漏。与此类似,`Mutex<T>` 则附带着创建出 *死锁deadlocks* 的风险。在某个操作需要锁住两项资源,同时两个线程分别均已请求获取两把锁中的一把时,就造成他们一直等待着对方释放各自所需的锁。若对死锁方面感兴趣,那么请尝试创建出有着死锁的一个 Rust 程序;随后就要研究任何一门语言中,互斥量的死锁消除策略,并试试在 Rust 中实现这些策略。`Mutex<T>` 和 `MutexGuard` 的标准库 API 文档,就提供了一些有用信息。
咱们将通过讲解 `Send` 与 `Sync` 两个特质,以及怎样与一些定制类型来运用他们来完结本章。
End

327
src/concurrency/threads.md Normal file
View File

@@ -0,0 +1,327 @@
# 运用线程来同步运行代码
**Using Threads to Run Code Simutaneously**
在绝大多数当前的操作系统中,被执行的程序代码,都是运行于 *进程a process* 中的,而所在的操作系统,则会同时管理多个进程。在程序内部,咱们同样可以有着同步运行的一些独立部分。运行这些独立部分的特性,便被称作 *线程threads*。比如web 服务器就可以有多个线程,如此他就可以在同一时间,响应多于一个的请求。
将咱们程序的运算拆分为多个线程来在同一时间运行多个任务可以提升性能但这样也增加了复杂度。由于线程能够同步运行因此在于不同线程上将要运行代码哪个部分的顺序方面就没有了某种固有保证because threads can run simultaneously, there's no inherent guarantee about the order in which parts of your code on different threads will run。这就会导致一些问题诸如
- 竞争局面,其中线程正以不一致顺序,访问着一些数据或资源;
- 死锁问题,其中两个线程正相互等待,而阻止了他们继续运行下去;
- 只在一些确切情形下才发生,而难于重现并可靠修复的代码错误。
Rust 试图消除这些运用线程方面的负面影响,但在多线程情景下的编程,仍要深思熟虑,并要求与运行在单线程下程序,截然不同的代码架构。
诸多编程语言,都是以少数几种不同途径,实现的线程,且多数操作系统,均提供了编程语言为可以创建出线程而调用的 API。Rust 标准库使用的是线程实现的 1:1 模型,由此程序就会以一个语言线程,对应使用一个操作系统线程。也有实现了别的线程操作模型的代码箱,对这种 1:1 模型做出了取舍。
## 使用 `spawn` 函数创建新线程
要创建出一个新的线程,咱们就要调用 `thread::spawn` 函数,并传递给他一个包含了打算在这个新线程中运行代码的闭包(在第 13 章中曾谈到过闭包)。下面清单 16-1 中的示例,会打印出来自主线程的一些文本,以及来自新线程的一些文本:
文件名:`src/main.rs`
```rust
use std::thread;
use std::time::Duration;
fn main() {
thread::spawn(|| {
for i in 1..10 {
println! ("\t- 你好,这是来自生成线程的数字 {} !", i);
thread::sleep(Duration::from_millis(20));
}
});
for i in 1..5 {
println! ("- 你好,这是来自主线程的数字 {} !", i);
thread::sleep(Duration::from_millis(20));
}
}
```
*清单 16-1创建出一个新线程来打印某物件与此同时主线程也在打印着其他东西*
请注意在 Rust 程序主线程完毕时,全部生成的线程就被关闭了,而不论他们是否已结束运行。该程序的输出每次都会有些许不同,但其看起来将如下所示:
```console
- 你好,这是来自主线程的数字 1 !
- 你好,这是来自生成线程的数字 1 !
- 你好,这是来自主线程的数字 2 !
- 你好,这是来自生成线程的数字 2 !
- 你好,这是来自主线程的数字 3 !
- 你好,这是来自生成线程的数字 3 !
- 你好,这是来自主线程的数字 4 !
- 你好,这是来自生成线程的数字 4 !
- 你好,这是来自生成线程的数字 5 !
```
`thread::sleep` 的调用,强制线程停止其执行短暂的时间,而允许别的线程运行。这些线程可能会轮流运行,但那并无保证:这取决于咱们的操作系统调度线程的方式。在此运行中,主线程就先行打印了,即便生成的线程中的打印语句,首先出现在代码中。而即便这里告诉了生成的线程,打印直到 `i``9` 的时候,但 `i` 在主线程关闭之前,仍只到了 `5`
若在运行此代码时,只看到主线程的输出,或未看到任何重叠部分,那么就要尝试增加其中那个范围(`1..10`, `1..5`)的数字,来给操作系统创造出,更多的与线程之间切换的机会。
## 使用 `join` 句柄等待全部线程结束
**Waiting for All Threads to Finish Using `join` Handles**
清单 16-1 中的代码,不仅会由于主线程的结束而提前停止生成线程,并因为在线程运行的顺序上没有保证,咱们还根本无法确保其中的生成线程将得到完整运行!
> **注**:在 `thred::sleep` 为 `1ms` 时,将偶发出现下面的运行结果:
```console
- 你好,这是来自主线程的数字 1 !
- 你好,这是来自生成线程的数字 1 !
- 你好,这是来自主线程的数字 2 !
- 你好,这是来自生成线程的数字 2 !
- 你好,这是来自生成线程的数字 3 !
- 你好,这是来自主线程的数字 3 !
- 你好,这是来自主线程的数字 4 !
- 你好,这是来自生成线程的数字 4 !
- 你好,这是来自生成线程的数字 %
```
咱们可以通过将 `thread::spawn` 的返回值,保存在一个变量中,来修复该生成线程不运行或提前结束的问题。`thread::spawn` 的返回值类型为 `JoinHandle`。而 `JoinHandle` 值则是一个自有值,在咱们于其上调用 `join` 方法时,他将等待其线程执行完毕。下面清单 16-2 就给出了怎样使用清单 16-1 中所创建出的那个 `JoinHandle`,来确保该生成线程在 `main` 退出之前执行完毕:
文件名:`src/main.rs`
```rust
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println! ("\t- 你好,这是来自生成线程的数字 {} !", i);
thread::sleep(Duration::from_millis(20));
}
});
for i in 1..5 {
println! ("- 你好,这是来自主线程的数字 {} !", i);
thread::sleep(Duration::from_millis(20));
}
handle.join().unwrap();
}
```
*清单 16-2保存一个来自 `thread::spawn` 的 `JoinHandle` 来确保该线程运行完毕*
> **注**:结合第 9 章中 [因错误而中止的快捷方式:`unwrap` 与 `expect`](Ch09_Error_Handling.md#因错误而中止的快捷方式unwrap-与-expect),表明 `join` 返回的是个 `Result<T, E>` 类型的枚举值。
在这个把手上调用 `join`,就会阻塞那个当前运行的线程,直到由该把手所表示的该线程终止。所谓 *阻塞blocking* 某个线程,是指那个线程被阻止执行工作或退出,*blocking* a thread means that thread is prevented from performing work or exiting。由于咱们已将到 `join` 的调用,放在了那个主线程的 `for` 循环之后,因此运行清单 16-2 中的代码,应产生出如下类似的输出(注:但每次运行的输出仍然不同):
```console
- 你好,这是来自主线程的数字 1 !
- 你好,这是来自生成线程的数字 1 !
- 你好,这是来自主线程的数字 2 !
- 你好,这是来自生成线程的数字 2 !
- 你好,这是来自主线程的数字 3 !
- 你好,这是来自生成线程的数字 3 !
- 你好,这是来自主线程的数字 4 !
- 你好,这是来自生成线程的数字 4 !
- 你好,这是来自生成线程的数字 5 !
- 你好,这是来自生成线程的数字 6 !
- 你好,这是来自生成线程的数字 7 !
- 你好,这是来自生成线程的数字 8 !
- 你好,这是来自生成线程的数字 9 !
```
两个线程依旧交替运行,但因为这个到 `handle.join()` 的调用,主线程就会等待,而在生成线程完毕之前不会结束。
不过来看看像下面这样,当咱们把 `handle.join()` 移至 `main` 中那个 `for` 循环前面时,会发生什么:
文件名:`src/main.rs`
```rust
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println! ("\t- 你好,这是来自生成线程的数字 {} !", i);
thread::sleep(Duration::from_millis(20));
}
});
handle.join().unwrap();
for i in 1..5 {
println! ("- 你好,这是来自主线程的数字 {} !", i);
thread::sleep(Duration::from_millis(20));
}
}
```
主线程将等待生成线程运行完毕,并于随后运行他的 `for` 循环,因此输出将不再交错,如下所示:
```console
- 你好,这是来自生成线程的数字 1 !
- 你好,这是来自生成线程的数字 2 !
- 你好,这是来自生成线程的数字 3 !
- 你好,这是来自生成线程的数字 4 !
- 你好,这是来自生成线程的数字 5 !
- 你好,这是来自生成线程的数字 6 !
- 你好,这是来自生成线程的数字 7 !
- 你好,这是来自生成线程的数字 8 !
- 你好,这是来自生成线程的数字 9 !
- 你好,这是来自主线程的数字 1 !
- 你好,这是来自主线程的数字 2 !
- 你好,这是来自主线程的数字 3 !
- 你好,这是来自主线程的数字 4 !
```
诸如 `join` 于何处被调用这样的细节,均会影响到咱们的线程,是否在同一时间运行。
## 对线程使用 `move` 闭包
**Using `move` Closures with Threads**
由于传递给 `thread::spawn` 的闭包随后将取得其用到的环境中一些值的所有权,由此就会把这些值的所有权,从一个线程转移到另一线程,因此咱们今后将经常在这些闭包上,使用 `move` 关键字。在第 13 章 [“捕获引用或迁移所有权”](Ch13_Functional_Language_Features_Iterators_and_Closures.md#捕获引用抑或迁移所有权) 小节,咱们就曾讨论过闭包语境下的 `move` 关键字。现在,咱们将更多地着重于 `move``thread::spawn` 之间的互动。
请注意在清单 16-1 中,传递给 `thread::spawn` 的那个闭包没有取任何参数:咱们没有在生成线程中,使用主线程中的任何数据。为在生成线程中使用主线程中的数据,那么生成线程的闭包就必须捕获其所需的值。下面清单 16-3 给出了在主线程中创建出一个矢量值,并在生成线程中用到这个矢量值的一种尝试。然而,正如即将看到的那样,这将尚不会运作。
文件名:`src/main.rs`
```rust
use std::thread;
fn main() {
let v = vec! [1, 2, 3];
let handle = thread::spawn(|| {
println! ("这里有个矢量值:{:?}", v);
});
handle.join().unwrap();
}
```
*清单 16-3尝试在另一线程中使用由主线程创建出的一个矢量值*
这个闭包用到了 `v`,因此他将捕获 `v` 并将其构造为该闭包环境的一部分。由于 `thread::spawn` 是在一个新线程中运行此闭包,因此咱们应能够在那个新线程内部访问 `v`。然而在编译这个示例时,咱们会得到如下报错:
```console
cargo run lennyp@vm-manjaro
Compiling concur_demo v0.1.0 (/home/lennyp/rust-lang/concur_demo)
error[E0373]: closure may outlive the current function, but it borrows `v`, which is owned by the current function
--> src/main.rs:9:32
|
9 | let handle = thread::spawn(|| {
| ^^ may outlive borrowed value `v`
10 | println! ("这里有个矢量值:{:?}", v);
| - `v` is borrowed here
|
note: function requires argument type to outlive `'static`
--> src/main.rs:9:18
|
9 | let handle = thread::spawn(|| {
| __________________^
10 | | println! ("这里有个矢量值:{:?}", v);
11 | | });
| |______^
help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
|
9 | let handle = thread::spawn(move || {
| ++++
For more information about this error, try `rustc --explain E0373`.
error: could not compile `concur_demo` due to previous error
```
Rust *推断出了infers* 怎样去捕获 `v`,并由于 `println!` 值需要到 `v` 的一个引用,因此该闭包就尝试借用 `v`。然而这里有个问题Rust 无法识别出这个生成线程将运行多久,因此他就不清楚到 `v` 的引用是否将始终有效。
下面清单 16-4 提供了更倾向于有着到 `v` 的不将有效引用的一种场景:
文件名:`src/main.rs`
```rust
#![allow(dead_code)]
#![allow(unused_variables)]
use std::thread;
fn main() {
let v = vec! [1, 2, 3];
let handle = thread::spawn(|| {
println! ("这里有个矢量值:{:?}", v);
});
drop(v); // 噢,不要啊!
handle.join().unwrap();
}
```
*清单 16-4有着尝试从弃用了 `v` 的主线程捕获到 `v` 引用的闭包的一个线程*
若 Rust 运行咱们运行此代码那么就有可能在一点也没有运行那个生成线程下其就会被立即置于后台中if Rust allowed us to run this code, there's a possibility the spawned thread would be immediately put in the background without running at all。那个生成线程内部有着一个到 `v` 的引用,而主线程则使用第 15 章中曾讨论过的 `drop` 函数,立即弃用了 `v`。随后,在生成线程开始执行时,`v` 就不再有效了,一次到他的引用也失效了。噢,不要!
要修复清单 16-3 中的编译器错误,咱们可以使用错误消息中的建议:
```console
help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
|
9 | let handle = thread::spawn(move || {
| ++++
```
经由在那个闭包前添加 `move` 关键字,咱们就强制该闭包取得其用到值的所有权,而非让 Rust 来推断出他应借用该值。下面清单 16-5 给出的对清单 16-3 的修改,将如咱们设想的那样编译和运行:
文件名:`src/main.rs`
```rust
use std::thread;
fn main() {
let v = vec! [1, 2, 3];
let handle = thread::spawn(move || {
println! ("这里有个矢量值:{:?}", v);
});
handle.join().unwrap();
}
```
*清单 16-5使用 `move` 关键字来强制闭包取得他所用到值的所有权*
或许也会尝试以同样做法,通过使用 `move` 关键字,去修复清单 16-4 中,主线程调用了 `drop` 的代码。然而,由于清单 16-4 尝试完成的事情,因为一种不同原因而不被允许,那么这样的修复就不会凑效。在咱们把 `move` 添加到闭包时,咱们就会把 `v` 迁移到该闭包的环境中,进而咱们就无法再在主线程中,于其上调用 `drop` 了。这是会得到如下的编译器错误:
```console
$ cargo run lennyp@vm-manjaro
Compiling concur_demo v0.1.0 (/home/lennyp/rust-lang/concur_demo)
error[E0382]: use of moved value: `v`
--> src/main.rs:13:10
|
7 | let v = vec! [1, 2, 3];
| - move occurs because `v` has type `Vec<i32>`, which does not implement the `Copy` trait
8 |
9 | let handle = thread::spawn(move || {
| ------- value moved into closure here
10 | println! ("这里有个矢量值:{:?}", &v);
| - variable moved due to use in closure
...
13 | drop(v);
| ^ value used here after move
For more information about this error, try `rustc --explain E0382`.
error: could not compile `concur_demo` due to previous error
```
Rust 的所有权规则,再次挽救了咱们!由于 Rust 一直以来的保守,以及只为那个线程借用了 `v`,就意味着主线程理论上可以令到生成线程的引用失效,而得到了清单 16-3 中代码的报错。通过告知 Rust 将 `v` 的所有权迁移到生成线程,咱们就向 Rust 保证了主线程不会再使用 `v`。而若咱们以同样方式修改清单 16-4那么随后在咱们于主线程中尝试使用 `v` 时,就破坏了那些所有权规则。这个 `move` 关键字,覆盖了 Rust 借用方面的保守做法;但他并无让咱们破坏所有权规则。
有了线程及线程 API 方面的基本认识,接下来就有看看用线程可以 *do* 些什么。
End

View File

@@ -0,0 +1,81 @@
# 使用 `cargo install` 安装二进制代码箱
**Installing Binaries with `cargo install`**
`cargo install` 命令允许咱们在本地安装和使用二进制的代码箱。这并不是要取代系统包system packages它的目的是为 Rust 开发者提供一种方便的方式来安装别人在 [crates.io](https://crates.io) 上分享的工具。请注意咱们只能安装有二进制目标的包packages that have binary targets。所谓 *二进制目标binary target*即与本身为非可运行而适合于在其他程序中包含的库目标a libary target相反因为代码箱有着一个 `src/main.rs` 文件,或有着被指定为二进制的另一文件时,而创建出的可以运行的程序。通常,代码箱会在 `README` 文件中,有着关于其是否为库代码箱,还是有着二进制目标,或二者皆具方面的信息。
使用 `cargo install` 安装的全部二进制程序文件,都被存储在安装根的 `bin` 文件中in the installation root's `bin` folder。在使用 `rustup.rs` 安装 Rust且没做任何定制配置时这个目录将是 `$HOME/.cargo/bin`。为能运行咱们使用 `cargo install` 安装的程序,就要确保那个目录在 `$PATH` 中。
> 注:可在任意位置运行 `cargo install` 命令,来安装 Crates.io 上的 Rust 二进制程序,这些程序都将被安装在 `$HOME/.cargo/bin` 下。若已安装了某个 Rust 程序后再安装他,那么就会有如下输出:
```console
$ cargo install ripgrep 1m 4s lennyp@vm-manjaro
Updating crates.io index
Ignored package `ripgrep v13.0.0` is already installed, use --force to override
```
比如,咱们曾在第 12 章中提到,有个用于搜索文件,`grep` 工具的 Rust 实现 `ripgrep`。要安装 `ripgrep`,咱们可运行如下命令:
```console
$ cargo install ripgrep
Updating crates.io index
Installing ripgrep v13.0.0
Compiling memchr v2.5.0
Compiling cfg-if v1.0.0
Compiling libc v0.2.137
Compiling log v0.4.17
Compiling proc-macro2 v1.0.47
Compiling lazy_static v1.4.0
Compiling regex-automata v0.1.10
Compiling quote v1.0.21
Compiling unicode-ident v1.0.5
Compiling bstr v0.2.17
Compiling syn v1.0.103
Compiling aho-corasick v0.7.20
Compiling regex-syntax v0.6.28
Compiling serde_derive v1.0.147
Compiling encoding_rs v0.8.31
Compiling serde v1.0.147
Compiling regex v1.7.0
Compiling grep-matcher v0.1.5
Compiling serde_json v1.0.89
Compiling unicode-width v0.1.10
Compiling fnv v1.0.7
Compiling same-file v1.0.6
Compiling once_cell v1.16.0
Compiling thread_local v1.1.4
Compiling globset v0.4.9
Compiling textwrap v0.11.0
Compiling encoding_rs_io v0.1.7
Compiling memmap2 v0.5.8
Compiling bitflags v1.3.2
Compiling crossbeam-utils v0.8.14
Compiling bytecount v0.6.3
Compiling itoa v1.0.4
Compiling ryu v1.0.11
Compiling strsim v0.8.0
Compiling termcolor v1.1.3
Compiling clap v2.34.0
Compiling grep-searcher v0.1.10
Compiling atty v0.2.14
Compiling base64 v0.13.1
Compiling grep-printer v0.1.6
Compiling grep-cli v0.1.6
Compiling grep-regex v0.1.10
Compiling ripgrep v13.0.0
Compiling walkdir v2.3.2
Compiling ignore v0.4.18
Compiling grep v0.2.10
Compiling num_cpus v1.14.0
Finished release [optimized + debuginfo] target(s) in 1m 09s
Installing /home/lennyp/.cargo/bin/rg
Installed package `ripgrep v13.0.0` (executable `rg`)
```
输出的倒数第二行显示出已安装二进制程序的位置与名字,在这个示例中名字便是 `rg`。正如前面提到的,只要安装目录是在 `$PATH` 中,随后咱们就可以运行 `rg --help`,并启动一个用于检索文件的更快、更具 Rust 风格的工具了!
End

View File

@@ -0,0 +1,15 @@
# 使用定制命令扩展 Cargo
**Extending Cargo with Custom Commands**
Cargo 被设计为在无需修改 Cargo 下,咱们就可以使用新的子命令,对其加以扩展。若咱们的 `$PATH` 中有名为 `cargo-something` 的二进制程序,咱们便可通过运行 `cargo something`,将其作为 Cargo 的子命令运行。像这样的定制命令,还会在咱们运行 `cargo --list` 时给列出来。使用 `cargo install` 安装扩展,并随后跟运行内建的 Cargo 工具一样运行他们的这种能力,正是 Cargo 设计的一项超级便利的好处!
# 本章小结
运用 Cargo 与 [crates.io](https://crates.io) 分享代码,是令到 Rust 生态对于许多不同任务都有用的一个方面。Rust 的标准库是小型且稳定的,但在不同于语言本身的时间线上,代码箱则易于共享、运用以及改进。请不要羞于在 [crates.io](https://crates.io) 上分享对自己有用的代码;那些代码或许对其他人也同样有用!
End

Some files were not shown because too many files have changed in this diff Show More