From 84f74aa28731004c9a9f35badbe27fa0487f8983 Mon Sep 17 00:00:00 2001 From: Allan Downey Date: Wed, 2 Mar 2022 19:56:14 +0800 Subject: [PATCH] Update: unified format and fix wrong link --- src/SUMMARY.md | 428 ++++++++++---------- src/about-book.md | 11 +- src/basic/base-type/char-bool.md | 8 +- src/basic/base-type/function.md | 27 +- src/basic/base-type/index.md | 7 +- src/basic/base-type/numbers.md | 74 ++-- src/basic/base-type/statement-expression.md | 15 +- src/basic/collections/hashmap.md | 54 ++- src/basic/collections/intro.md | 4 +- src/basic/collections/vector.md | 39 +- src/basic/comment.md | 95 +++-- src/basic/compound-type/array.md | 37 +- src/basic/compound-type/enum.md | 31 +- src/basic/compound-type/intro.md | 8 +- src/basic/compound-type/string-slice.md | 74 +++- src/basic/compound-type/struct.md | 57 ++- src/basic/compound-type/tuple.md | 4 + src/basic/converse.md | 42 +- src/basic/crate-module/crate.md | 16 +- src/basic/crate-module/intro.md | 1 + src/basic/crate-module/module.md | 35 +- src/basic/crate-module/use.md | 31 +- src/basic/flow-control.md | 67 ++- src/basic/formatted-output.md | 64 ++- src/basic/intro.md | 8 +- src/basic/match-pattern/all-patterns.md | 32 +- src/basic/match-pattern/intro.md | 1 - src/basic/match-pattern/match-if-let.md | 38 +- src/basic/match-pattern/option.md | 15 +- src/basic/match-pattern/pattern-match.md | 37 +- src/basic/method.md | 20 +- src/basic/ownership/borrowing.md | 36 +- src/basic/ownership/ownership.md | 53 +-- src/basic/result-error/intro.md | 4 +- src/basic/result-error/panic.md | 59 ++- src/basic/result-error/result.md | 61 ++- src/basic/trait/advance-trait.md | 52 ++- src/basic/trait/generic.md | 51 ++- src/basic/trait/trait-object.md | 50 ++- src/basic/trait/trait.md | 83 +++- src/basic/variable.md | 24 +- src/first-try/cargo.md | 2 +- src/first-try/editor.md | 5 +- src/first-try/hello-world.md | 14 +- src/first-try/installation.md | 6 +- src/index-list.md | 5 +- src/into-rust.md | 2 +- src/some-thoughts.md | 2 +- src/sth-you-should-not-do.md | 16 +- 49 files changed, 1289 insertions(+), 616 deletions(-) diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 0d1364ea..fea211c5 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -1,230 +1,238 @@ -# Rust语言圣经 +# Rust 语言圣经 -[进入Rust编程世界](into-rust.md) -[AWS为何这么喜欢Rust?](usecases/aws-rust.md) +[进入 Rust 编程世界](into-rust.md) +[AWS 为何这么喜欢 Rust?](usecases/aws-rust.md) [避免从入门到放弃](sth-you-should-not-do.md) [关于本书](about-book.md) [快速查询入口](index-list.md) ## Getting started + - [寻找牛刀,以便小试](first-try/intro.md) - - [安装Rust环境](first-try/installation.md) - - [墙推VSCode!](first-try/editor.md) - - [认识Cargo](first-try/cargo.md) - - [不仅仅是Hello world](first-try/hello-world.md) + - [安装 Rust 环境](first-try/installation.md) + - [墙推 VSCode!](first-try/editor.md) + - [认识 Cargo](first-try/cargo.md) + - [不仅仅是 Hello world](first-try/hello-world.md) + +## Rust 学习三部曲 -## Rust学习三部曲 +- [Rust 基础入门](basic/intro.md) + - [变量绑定与解构](basic/variable.md) + - [基本类型](basic/base-type/index.md) + - [数值类型](basic/base-type/numbers.md) + - [字符、布尔、单元类型](basic/base-type/char-bool.md) + - [语句与表达式](basic/base-type/statement-expression.md) + - [函数](basic/base-type/function.md) + - [所有权和借用](basic/ownership/index.md) + - [所有权](basic/ownership/ownership.md) + - [引用与借用](basic/ownership/borrowing.md) + - [复合类型](basic/compound-type/intro.md) + - [字符串与切片](basic/compound-type/string-slice.md) + - [元组](basic/compound-type/tuple.md) + - [结构体](basic/compound-type/struct.md) + - [枚举](basic/compound-type/enum.md) + - [数组](basic/compound-type/array.md) + - [流程控制](basic/flow-control.md) + - [模式匹配](basic/match-pattern/intro.md) + - [match 和 if let](basic/match-pattern/match-if-let.md) + - [解构 Option](basic/match-pattern/option.md) + - [模式适用场景](basic/match-pattern/pattern-match.md) + - [全模式列表](basic/match-pattern/all-patterns.md) + - [方法 Method](basic/method.md) + - [泛型和特征](basic/trait/intro.md) + - [泛型 Generics](basic/trait/generic.md) + - [特征 Trait](basic/trait/trait.md) + - [特征对象](basic/trait/trait-object.md) + - [进一步深入特征](basic/trait/advance-trait.md) + - [集合类型](basic/collections/intro.md) + - [动态数组 Vector](basic/collections/vector.md) + - [KV 存储 HashMap](basic/collections/hashmap.md) + - [类型转换](basic/converse.md) + - [返回值和错误处理](basic/result-error/intro.md) + - [panic 深入剖析!](basic/result-error/panic.md) + - [返回值 Result 和?](basic/result-error/result.md) + - [包和模块](basic/crate-module/intro.md) + - [包 Crate](basic/crate-module/crate.md) + - [模块 Module](basic/crate-module/module.md) + - [使用 use 引入模块及受限可见性](basic/crate-module/use.md) + - [注释和文档](basic/comment.md) + - [格式化输出](basic/formatted-output.md) +- [Rust 高级进阶](advance/intro.md) + - [生命周期](advance/lifetime/intro.md) + - [认识生命周期](advance/lifetime/basic.md) + - [深入生命周期](advance/lifetime/advance.md) + - [&'static 和 T: 'static](advance/lifetime/static.md) + + - [函数式编程: 闭包、迭代器](advance/functional-programing/intro.md) + - [闭包 Closure](advance/functional-programing/closure.md) + - [迭代器 Iterator](advance/functional-programing/iterator.md) + - [深入类型](advance/into-types/intro.md) + - [newtype 和 类型别名](advance/into-types/custom-type.md) + - [Sized 和不定长类型 DST](advance/into-types/sized.md) + - [枚举和整数](advance/into-types/enum-int.md) + - [智能指针](advance/smart-pointer/intro.md) + - [Box堆对象分配](advance/smart-pointer/box.md) + - [Deref 解引用](advance/smart-pointer/deref.md) + - [Drop 释放资源](advance/smart-pointer/drop.md) + - [Rc 与 Arc 实现 1vN 所有权机制](advance/smart-pointer/rc-arc.md) + - [Cell 与 RefCell 内部可变性](advance/smart-pointer/cell-refcell.md) + - [循环引用与自引用](advance/circle-self-ref/intro.md) + - [Weak 与循环引用](advance/circle-self-ref/circle-reference.md) + - [结构体中的自引用](advance/circle-self-ref/self-referential.md)) + - [多线程并发编程](advance/concurrency-with-threads/intro.md) + - [并发和并行](advance/concurrency-with-threads/concurrency-parallelism.md) + - [使用多线程](advance/concurrency-with-threads/thread.md) + - [线程同步:消息传递](advance/concurrency-with-threads/message-passing.md) + - [线程同步:锁、Condvar 和信号量](advance/concurrency-with-threads/sync1.md) + - [线程同步:Atomic 原子操作与内存顺序](advance/concurrency-with-threads/sync2.md) + - [基于 Send 和 Sync 的线程安全](advance/concurrency-with-threads/send-sync.md) + - [实践应用:多线程 Web 服务器 todo](advance/concurrency-with-threads/web-server.md) + - [全局变量](advance/global-variable.md) + - [错误处理](advance/errors.md) -- [Rust基础入门](basic/intro.md) - - [变量绑定与解构](basic/variable.md) - - [基本类型](basic/base-type/index.md) - - [数值类型](basic/base-type/numbers.md) - - [字符、布尔、单元类型](basic/base-type/char-bool.md) - - [语句与表达式](basic/base-type/statement-expression.md) - - [函数](basic/base-type/function.md) - - [所有权和借用](basic/ownership/index.md) - - [所有权](basic/ownership/ownership.md) - - [引用与借用](basic/ownership/borrowing.md) - - [复合类型](basic/compound-type/intro.md) - - [字符串与切片](basic/compound-type/string-slice.md) - - [元组](basic/compound-type/tuple.md) - - [结构体](basic/compound-type/struct.md) - - [枚举](basic/compound-type/enum.md) - - [数组](basic/compound-type/array.md) - - [流程控制](basic/flow-control.md) - - [模式匹配](basic/match-pattern/intro.md) - - [match和if let](basic/match-pattern/match-if-let.md) - - [解构Option](basic/match-pattern/option.md) - - [模式适用场景](basic/match-pattern/pattern-match.md) - - [全模式列表](basic/match-pattern/all-patterns.md) - - [方法Method](basic/method.md) - - [泛型和特征](basic/trait/intro.md) - - [泛型Generics](basic/trait/generic.md) - - [特征Trait](basic/trait/trait.md) - - [特征对象](basic/trait/trait-object.md) - - [进一步深入特征](basic/trait/advance-trait.md) - - [集合类型](basic/collections/intro.md) - - [动态数组Vector](basic/collections/vector.md) - - [KV存储HashMap](basic/collections/hashmap.md) - - [类型转换](basic/converse.md) - - [返回值和错误处理](basic/result-error/intro.md) - - [panic深入剖析!](basic/result-error/panic.md) - - [返回值Result和?](basic/result-error/result.md) - - [包和模块](basic/crate-module/intro.md) - - [包Crate](basic/crate-module/crate.md) - - [模块Module](basic/crate-module/module.md) - - [使用use引入模块及受限可见性](basic/crate-module/use.md) - - [注释和文档](basic/comment.md) - - [格式化输出](basic/formatted-output.md) -- [Rust高级进阶](advance/intro.md) - - [生命周期](advance/lifetime/intro.md) - - [认识生命周期](advance/lifetime/basic.md) - - [深入生命周期](advance/lifetime/advance.md) - - [&'static 和 T: 'static](advance/lifetime/static.md) - - - [函数式编程: 闭包、迭代器](advance/functional-programing/intro.md) - - [闭包Closure](advance/functional-programing/closure.md) - - [迭代器Iterator](advance/functional-programing/iterator.md) - - [深入类型](advance/into-types/intro.md) - - [newtype 和 类型别名](advance/into-types/custom-type.md) - - [Sized 和不定长类型 DST](advance/into-types/sized.md) - - [枚举和整数](advance/into-types/enum-int.md) - - [智能指针](advance/smart-pointer/intro.md) - - [Box堆对象分配](advance/smart-pointer/box.md) - - [Deref解引用](advance/smart-pointer/deref.md) - - [Drop释放资源](advance/smart-pointer/drop.md) - - [Rc与Arc实现1vN所有权机制](advance/smart-pointer/rc-arc.md) - - [Cell与RefCell内部可变性](advance/smart-pointer/cell-refcell.md) - - [循环引用与自引用](advance/circle-self-ref/intro.md) - - [Weak与循环引用](advance/circle-self-ref/circle-reference.md) - - [结构体中的自引用](advance/circle-self-ref/self-referential.md)) - - [多线程并发编程](advance/concurrency-with-threads/intro.md) - - [并发和并行](advance/concurrency-with-threads/concurrency-parallelism.md) - - [使用多线程](advance/concurrency-with-threads/thread.md) - - [线程同步:消息传递](advance/concurrency-with-threads/message-passing.md) - - [线程同步:锁、Condvar和信号量](advance/concurrency-with-threads/sync1.md) - - [线程同步:Atomic原子操作与内存顺序](advance/concurrency-with-threads/sync2.md) - - [基于Send和Sync的线程安全](advance/concurrency-with-threads/send-sync.md) - - [实践应用:多线程Web服务器 todo](advance/concurrency-with-threads/web-server.md) - - [全局变量](advance/global-variable.md) - - [错误处理](advance/errors.md) - - - [Unsafe Rust](advance/unsafe/intro.md) - - [五种兵器](advance/unsafe/superpowers.md) - - [内联汇编 todo](advance/unsafe/inline-asm.md) - - [Macro宏编程](advance/macro.md) + - [Unsafe Rust](advance/unsafe/intro.md) + - [五种兵器](advance/unsafe/superpowers.md) + - [内联汇编 todo](advance/unsafe/inline-asm.md) + - [Macro 宏编程](advance/macro.md) -## 专题内容,每个专题都配套一个小型项目进行实践 +## 专题内容,每个专题都配套一个小型项目进行实践 + - [自动化测试](test/intro.md) - - [编写测试及控制执行](test/write-tests.md) - - [单元测试和集成测试](test/unit-integration-test.md) - - [断言assertion](test/assertion.md) - - [用Github Actions进行持续集成](test/ci.md) - - [基准测试benchmark](test/benchmark.md) -- [async/await异步编程](async/intro.md) - - [async编程入门](async/getting-started.md) - - [底层探秘: Future执行与任务调度](async/future-excuting.md) - - [定海神针Pin和Unpin](async/pin-unpin.md) - - [async/await和Stream流处理](async/async-await.md) - - [同时运行多个Future](async/multi-futures-simultaneous.md) + - [编写测试及控制执行](test/write-tests.md) + - [单元测试和集成测试](test/unit-integration-test.md) + - [断言 assertion](test/assertion.md) + - [用 Github Actions 进行持续集成](test/ci.md) + - [基准测试 benchmark](test/benchmark.md) + +- [async/await 异步编程](async/intro.md) + - [async 编程入门](async/getting-started.md) + - [底层探秘: Future 执行与任务调度](async/future-excuting.md) + - [定海神针 Pin 和 Unpin](async/pin-unpin.md) + - [async/await 和 Stream 流处理](async/async-await.md) + - [同时运行多个 Future](async/multi-futures-simultaneous.md) - [一些疑难问题的解决办法](async/pain-points-and-workarounds.md) - - [实践应用:Async Web服务器](async/web-server.md) - -- [Tokio使用指南](tokio/intro.md) - - [tokio概览](tokio/overview.md) - - [使用初印象](tokio/getting-startted.md) - - [创建异步任务](tokio/spawning.md) - - [共享状态](tokio/shared-state.md) - - [消息传递](tokio/channels.md) - - [I/O](tokio/io.md) - - [解析数据帧](tokio/frame.md) - - [深入async](tokio/async.md) - - [select](tokio/select.md) - - [类似迭代器的Stream](tokio/stream.md)) - - [优雅的关闭](tokio/graceful-shutdown.md) - - [异步跟同步共存](tokio/bridging-with-sync.md) - -- [Cargo使用指南](cargo/intro.md) - - [上手使用](cargo/getting-started.md) - - [基础指南](cargo/guide/intro.md) - - [为何会有Cargo](cargo/guide/why-exist.md) - - [下载并构建Package](cargo/guide/download-package.md) - - [添加依赖](cargo/guide/dependencies.md) - - [Package目录结构](cargo/guide/package-layout.md) - - [Cargo.toml vs Cargo.lock](cargo/guide/cargo-toml-lock.md) - - [测试和CI](cargo/guide/tests-ci.md) - - [Cargo缓存](cargo/guide/cargo-cache.md) - - [Build缓存](cargo/guide/build-cache.md) - - [进阶指南](cargo/reference/intro.md) - - [指定依赖项](cargo/reference/specify-deps.md) - - [依赖覆盖](cargo/reference/deps-overriding.md) - - [Cargo.toml清单详解](cargo/reference/manifest.md) - - [Cargo Target](cargo/reference/cargo-target.md) - - [工作空间Workspace](cargo/reference/workspaces.md) - - [条件编译Features](cargo/reference/features/intro.md) - - [Features示例](cargo/reference/features/examples.md) - - [发布配置Profile](cargo/reference/profiles.md) - - [通过config.toml对Cargo进行配置](cargo/reference/configuration.md) - - [发布到crates.io](cargo/reference/publishing-on-crates.io.md) - - [构建脚本 build.rs](cargo/reference/build-script/intro.md) - - [构建脚本示例 todo](cargo/reference/build-script/examples.md) + - [实践应用:Async Web 服务器](async/web-server.md) +- [Tokio 使用指南](tokio/intro.md) + + - [tokio 概览](tokio/overview.md) + - [使用初印象](tokio/getting-startted.md) + - [创建异步任务](tokio/spawning.md) + - [共享状态](tokio/shared-state.md) + - [消息传递](tokio/channels.md) + - [I/O](tokio/io.md) + - [解析数据帧](tokio/frame.md) + - [深入 async](tokio/async.md) + - [select](tokio/select.md) + - [类似迭代器的 Stream](tokio/stream.md)) + - [优雅的关闭](tokio/graceful-shutdown.md) + - [异步跟同步共存](tokio/bridging-with-sync.md) + +- [Cargo 使用指南](cargo/intro.md) + + - [上手使用](cargo/getting-started.md) + - [基础指南](cargo/guide/intro.md) + - [为何会有 Cargo](cargo/guide/why-exist.md) + - [下载并构建 Package](cargo/guide/download-package.md) + - [添加依赖](cargo/guide/dependencies.md) + - [Package 目录结构](cargo/guide/package-layout.md) + - [Cargo.toml vs Cargo.lock](cargo/guide/cargo-toml-lock.md) + - [测试和 CI](cargo/guide/tests-ci.md) + - [Cargo 缓存](cargo/guide/cargo-cache.md) + - [Build 缓存](cargo/guide/build-cache.md) + - [进阶指南](cargo/reference/intro.md) + - [指定依赖项](cargo/reference/specify-deps.md) + - [依赖覆盖](cargo/reference/deps-overriding.md) + - [Cargo.toml 清单详解](cargo/reference/manifest.md) + - [Cargo Target](cargo/reference/cargo-target.md) + - [工作空间 Workspace](cargo/reference/workspaces.md) + - [条件编译 Features](cargo/reference/features/intro.md) + - [Features 示例](cargo/reference/features/examples.md) + - [发布配置 Profile](cargo/reference/profiles.md) + - [通过 config.toml 对 Cargo 进行配置](cargo/reference/configuration.md) + - [发布到 crates.io](cargo/reference/publishing-on-crates.io.md) + - [构建脚本 build.rs](cargo/reference/build-script/intro.md) + - [构建脚本示例 todo](cargo/reference/build-script/examples.md) - [易混淆概念解析](confonding/intro.md) - - [切片和切片引用](confonding/slice.md) - - [String、&str 和 str](confonding/string.md) - - [原生指针、引用和智能指针 todo](confonding/pointer.md) - - [作用域、生命周期和 NLL todo](confonding/lifetime.md) - - [move、Copy和Clone todo](confonding/move-copy.md) + + - [切片和切片引用](confonding/slice.md) + - [String、&str 和 str](confonding/string.md) + - [原生指针、引用和智能指针 todo](confonding/pointer.md) + - [作用域、生命周期和 NLL todo](confonding/lifetime.md) + - [move、Copy 和 Clone todo](confonding/move-copy.md) - [对抗编译检查 doing](fight-with-compiler/intro.md) - - [幽灵数据(todo)](fight-with-compiler/phantom-data.md) - - [生命周期](fight-with-compiler/lifetime/intro.md) - - [生命周期过大-01](fight-with-compiler/lifetime/too-long1.md) - - [生命周期过大-02](fight-with-compiler/lifetime/too-long2.md) - - [循环中的生命周期](fight-with-compiler/lifetime/loop.md) - - [闭包碰到特征对象-01](fight-with-compiler/lifetime/closure-with-static.md) - - [重复借用](fight-with-compiler/borrowing/intro.md) - - [同时在函数内外使用引用](fight-with-compiler/borrowing/ref-exist-in-out-fn.md) - - [智能指针引起的重复借用错误](fight-with-compiler/borrowing/borrow-distinct-fields-of-struct.md) - - [类型未限制(todo)](fight-with-compiler/unconstrained.md) + - [幽灵数据(todo)](fight-with-compiler/phantom-data.md) + - [生命周期](fight-with-compiler/lifetime/intro.md) + - [生命周期过大-01](fight-with-compiler/lifetime/too-long1.md) + - [生命周期过大-02](fight-with-compiler/lifetime/too-long2.md) + - [循环中的生命周期](fight-with-compiler/lifetime/loop.md) + - [闭包碰到特征对象-01](fight-with-compiler/lifetime/closure-with-static.md) + - [重复借用](fight-with-compiler/borrowing/intro.md) + - [同时在函数内外使用引用](fight-with-compiler/borrowing/ref-exist-in-out-fn.md) + - [智能指针引起的重复借用错误](fight-with-compiler/borrowing/borrow-distinct-fields-of-struct.md) + - [类型未限制(todo)](fight-with-compiler/unconstrained.md) -- [Rust常见陷阱](pitfalls/index.md) - - [for循环中使用外部数组](pitfalls/use-vec-in-for.md) - - [线程类型导致的栈溢出](pitfalls/stack-overflow.md) - - [算术溢出导致的panic](pitfalls/arithmetic-overflow.md) - - [闭包中奇怪的生命周期](pitfalls/closure-with-lifetime.md) - - [可变变量不可变?](pitfalls/the-disabled-mutability.md) - - [可变借用失败引发的深入思考](pitfalls/multiple-mutable-references.md) - - [不太勤快的迭代器](pitfalls/lazy-iterators.md) - - [奇怪的序列x..y](pitfalls/weird-ranges.md) - - [无处不在的迭代器](pitfalls/iterator-everywhere.md) - - [线程间传递消息导致主线程无法结束](pitfalls/main-with-channel-blocked.md) +- [Rust 常见陷阱](pitfalls/index.md) + + - [for 循环中使用外部数组](pitfalls/use-vec-in-for.md) + - [线程类型导致的栈溢出](pitfalls/stack-overflow.md) + - [算术溢出导致的 panic](pitfalls/arithmetic-overflow.md) + - [闭包中奇怪的生命周期](pitfalls/closure-with-lifetime.md) + - [可变变量不可变?](pitfalls/the-disabled-mutability.md) + - [可变借用失败引发的深入思考](pitfalls/multiple-mutable-references.md) + - [不太勤快的迭代器](pitfalls/lazy-iterators.md) + - [奇怪的序列 x..y](pitfalls/weird-ranges.md) + - [无处不在的迭代器](pitfalls/iterator-everywhere.md) + - [线程间传递消息导致主线程无法结束](pitfalls/main-with-channel-blocked.md) + +- [Rust 最佳实践 doing](practice/intro.md) + - [日常开发三方库精选](practice/third-party-libs.md) + - [命名规范](practice/naming.md) + - [代码开发实践 todo](practice/best-pratice.md) + - [日志记录 todo](practice/logs.md) + - [可观测性监控 todo](practice/observability.md) -- [Rust最佳实践 doing](practice/intro.md) - - [日常开发三方库精选](practice/third-party-libs.md) - - [命名规范](practice/naming.md) - - [代码开发实践 todo](practice/best-pratice.md) - - [日志记录 todo](practice/logs.md) - - [可观测性监控 todo](practice/observability.md) - - -- [Rust性能剖析 todo](profiling/intro.md) + +- [Rust 性能剖析 todo](profiling/intro.md) + - [深入内存 todo](profiling/memory/intro.md) - - [指针和引用 todo](profiling/memory/pointer-ref.md) - - [未初始化内存 todo](profiling/memory/uninit.md) - - [内存分配 todo](profiling/memory/allocation.md) - - [内存布局 todo](profiling/memory/layout.md) - - [虚拟内存 todo](profiling/memory/virtual.md) + - [指针和引用 todo](profiling/memory/pointer-ref.md) + - [未初始化内存 todo](profiling/memory/uninit.md) + - [内存分配 todo](profiling/memory/allocation.md) + - [内存布局 todo](profiling/memory/layout.md) + - [虚拟内存 todo](profiling/memory/virtual.md) - [性能调优 doing](profiling/performance/intro.md) - - [字符串操作性能](profiling/performance/string.md) - - [深入理解move](profiling/performance/deep-into-move.md) - - [糟糕的提前优化 todo](profiling/performance/early-optimise.md) - - [Clone和Copy todo](profiling/performance/clone-copy.md) - - [减少Runtime check(todo)](profiling/performance/runtime-check.md) - - [CPU缓存性能优化 todo](profiling/performance/cpu-cache.md) - - [计算性能优化 todo](profiling/performance/calculate.md) - - [堆和栈 todo](profiling/performance/heap-stack.md) - - [内存allocator todo](profiling/performance/allocator.md) - - [常用性能测试工具 todo](profiling/performance/tools.md) - - [Enum内存优化 todo](profiling/performance/enum.md) + - [字符串操作性能](profiling/performance/string.md) + - [深入理解 move](profiling/performance/deep-into-move.md) + - [糟糕的提前优化 todo](profiling/performance/early-optimise.md) + - [Clone 和 Copy todo](profiling/performance/clone-copy.md) + - [减少 Runtime check(todo)](profiling/performance/runtime-check.md) + - [CPU 缓存性能优化 todo](profiling/performance/cpu-cache.md) + - [计算性能优化 todo](profiling/performance/calculate.md) + - [堆和栈 todo](profiling/performance/heap-stack.md) + - [内存 allocator todo](profiling/performance/allocator.md) + - [常用性能测试工具 todo](profiling/performance/tools.md) + - [Enum 内存优化 todo](profiling/performance/enum.md) - [编译优化 todo](profiling/compiler/intro.md) - - [LLVM todo](profiling/compiler/llvm.md) - - [常见属性标记 todo](profiling/compiler/attributes.md) - - [提升编译速度 todo](profiling/compiler/speed-up.md) - - [编译器优化 todo](profiling/compiler/optimization/intro.md) - - [Option枚举 todo](profiling/compiler/optimization/option.md) + - [LLVM todo](profiling/compiler/llvm.md) + - [常见属性标记 todo](profiling/compiler/attributes.md) + - [提升编译速度 todo](profiling/compiler/speed-up.md) + - [编译器优化 todo](profiling/compiler/optimization/intro.md) + - [Option 枚举 todo](profiling/compiler/optimization/option.md) - [标准库解析 todo](std/intro.md) - - [标准库使用最佳时间 todo](std/search.md) - - [Vector常用方法 todo](std/vector.md) - - [HashMap todo](std/hashmap.md) - - [Iterator常用方法 todo](std/iterator.md) + + - [标准库使用最佳时间 todo](std/search.md) + - [Vector 常用方法 todo](std/vector.md) + - [HashMap todo](std/hashmap.md) + - [Iterator 常用方法 todo](std/iterator.md) - [Ctrl-C/V: 编程常用代码片段 todo](cases/intro.md) - [命令行解析 todo](cases/cmd.md) @@ -248,22 +256,22 @@ - [开发调试 todo](cases/dev/intro.md) - [日志](cases/dev/logs.md) - [性能分析](cases/dev/profile.md) - - ## 附录 -- [附录](appendix/intro.md) - - [A-关键字](appendix/keywords.md) - - [B-运算符与符号](appendix/operators.md) - - [C-表达式](appendix/expressions.md) - - [D-派生特征 trait](appendix/derive.md) - - [E-prelude 模块 todo](appendix/prelude.md) - - [F-Rust 版本说明](appendix/rust-version.md) - - [G-Rust 更新版本列表](appendix/rust-versions/intro.md) - - [1.58](appendix/rust-versions/1.58.md) - - [1.59](appendix/rust-versions/1.59.md) +- [附录](appendix/intro.md) + - [A-关键字](appendix/keywords.md) + - [B-运算符与符号](appendix/operators.md) + - [C-表达式](appendix/expressions.md) + - [D-派生特征 trait](appendix/derive.md) + - [E-prelude 模块 todo](appendix/prelude.md) + - [F-Rust 版本说明](appendix/rust-version.md) + - [G-Rust 更新版本列表](appendix/rust-versions/intro.md) + - [1.58](appendix/rust-versions/1.58.md) + - [1.59](appendix/rust-versions/1.59.md) diff --git a/src/about-book.md b/src/about-book.md index 00ea5c4b..dee453da 100644 --- a/src/about-book.md +++ b/src/about-book.md @@ -1,14 +1,13 @@ -# Rust语言圣经 (The Course) +# Rust 语言圣经 (The Course) - 在线阅读 - 官方: [https://course.rs](https://course.rs) - 知乎: [支持章节内目录跳转,很好用!](https://www.zhihu.com/column/c_1452781034895446017) > 学了 Rust 语法后迷茫了?来试试 [Rust By Practice](https://github.com/sunface/rust-by-practice),它是本书的配套练习题和实践,覆盖了 easy to hard 各个难度,满足你的所有需求。 -> +> > [Rust 语言周刊](https://github.com/sunface/rust-weekly),每周一发布,这里有精选的开源项目、新闻、技术文章和 Rust 语言更新等内容栏目。 - ### 教程简介 **`Rust语言圣经`**涵盖从**入门到精通**所需的 Rust 知识,目录及内容都经过深思熟虑的设计,同时语言生动幽默,行文流畅自如,摆脱技术书籍常有的机器味和晦涩感。 @@ -27,11 +26,10 @@ 在开源版权上,我们选择了 [No License](https://www.google.com.hk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&ved=2ahUKEwigkv-KtMT0AhXFdXAKHdI4BCcQFnoECAQQAw&url=https%3A%2F%2Fchoosealicense.com%2Fno-permission%2F&usg=AOvVaw3M2Q4IbdhnpJ2K71TF7SPB),这意味着读者可以随意的 fork 和阅读,但是**不能私下修改后再包装分发**,如果有这方面的需求,请联系我们,望理解。 -Rust语言圣经是**完全开源**的电子书,每个章节都至少用时 4-6 个小时才能初步完稿,牺牲了大量休闲娱乐、陪伴家人的时间,还没有任何钱赚。 +Rust 语言圣经是**完全开源**的电子书,每个章节都至少用时 4-6 个小时才能初步完稿,牺牲了大量休闲娱乐、陪伴家人的时间,还没有任何钱赚。 **如果大家觉得这本书作者真的用心了,希望你能帮我们点一个 🌟 `star`。感激不尽!:)** - ### 借鉴的书籍 站在巨人的肩膀上,能帮我们看的更远,特此感谢以下巨人: @@ -54,4 +52,5 @@ Rust语言圣经是**完全开源**的电子书,每个章节都至少用时 4- - [@AllanDowney](https://github.com/AllanDowney) - [@Mintnoii](https://github.com/Mintnoii) -尤其感谢这些主要贡献者,谢谢你们花费大量时间贡献了多处`fix`和高质量的内容优化。非常感动,再次感谢~~ \ No newline at end of file +尤其感谢这些主要贡献者,谢谢你们花费大量时间贡献了多处`fix`和高质量的内容优化。非常感动,再次感谢~~ + diff --git a/src/basic/base-type/char-bool.md b/src/basic/base-type/char-bool.md index f2f6ecf8..47a47310 100644 --- a/src/basic/base-type/char-bool.md +++ b/src/basic/base-type/char-bool.md @@ -7,6 +7,7 @@ 字符,对于没有其它编程经验的新手来说可能不太好理解(没有编程经验敢来学 Rust 的绝对是好汉),但是你可以把它理解为英文中的字母,中文中的汉字。 下面的代码展示了几个颇具异域风情的字符: + ``` fn main() { let c = 'z'; @@ -16,9 +17,10 @@ fn main() { } ``` -如果大家是从有年代感的编程语言过来,可能会大喊一声:这XX叫字符?是的,在 Rust 语言中这些都是字符,Rust 的字符不仅仅是 `ASCII`,所有的 `Unicode` 值都可以作为 Rust 字符,包括单个的中文、日文、韩文、emoji表情符号等等,都是合法的字符类型。`Unicode` 值的范围从 `U+0000~U+D7FF` 和 `U+E000~U+10FFFF`。不过“字符”并不是 `Unicode` 中的一个概念,所以人在直觉上对“字符”的理解和 Rust 的字符概念并不一致。 +如果大家是从有年代感的编程语言过来,可能会大喊一声:这 XX 叫字符?是的,在 Rust 语言中这些都是字符,Rust 的字符不仅仅是 `ASCII`,所有的 `Unicode` 值都可以作为 Rust 字符,包括单个的中文、日文、韩文、emoji 表情符号等等,都是合法的字符类型。`Unicode` 值的范围从 `U+0000 ~ U+D7FF` 和 `U+E000 ~ U+10FFFF`。不过“字符”并不是 `Unicode` 中的一个概念,所以人在直觉上对“字符”的理解和 Rust 的字符概念并不一致。 由于 `Unicode` 都是 4 个字节编码,因此字符类型也是占用 4 个字节: + ```rust fn main() { let x = '中'; @@ -31,7 +33,7 @@ fn main() { ```console $ cargo run Compiling ... - + 字符'中'占用了4字节的内存大小 ``` @@ -65,4 +67,4 @@ fn main() { 例如常见的 `println!()` 的返回值也是单元类型 `()`。 -再比如,你可以用 `()` 作为 `map` 的值,表示我们不关注具体的值,只关注 `key`。 这种用法和 Go 语言的 ***struct{}*** 类似,可以作为一个值用来占位,但是完全**不占用**任何内存。 +再比如,你可以用 `()` 作为 `map` 的值,表示我们不关注具体的值,只关注 `key`。 这种用法和 Go 语言的 **_struct{}_** 类似,可以作为一个值用来占位,但是完全**不占用**任何内存。 diff --git a/src/basic/base-type/function.md b/src/basic/base-type/function.md index c5c01478..dacb9e4a 100644 --- a/src/basic/base-type/function.md +++ b/src/basic/base-type/function.md @@ -17,6 +17,7 @@ fn add(i: i32, j: i32) -> i32 { 当你看懂了这张图,其实就等于差不多完成了函数章节的学习,但是这么短的章节显然对不起读者老爷们的厚爱,所以我们来展开下。 ## 函数要点 + - 函数名和变量名使用[蛇形命名法(snake case)](https://course.rs/practice/naming.html),例如 `fn add_two() -> {}` - 函数的位置可以随便放,Rust 不关心我们在哪里定义了函数,只要有定义即可 - 每个函数参数都需要标注类型 @@ -24,6 +25,7 @@ fn add(i: i32, j: i32) -> i32 { ## 函数参数 Rust 是强类型语言,因此需要你为每一个函数参数都标识出它的具体类型,例如: + ```rust fn main() { another_function(5, 6.1); @@ -36,6 +38,7 @@ fn another_function(x: i32, y: f32) { ``` `another_function` 函数有两个参数,其中 `x` 是 `i32` 类型,`y` 是 `f32` 类型,然后在该函数内部,打印出这两个值。这里去掉 `x` 或者 `y` 的任何一个的类型,都会报错: + ```rust fn main() { another_function(5, 6.1); @@ -48,12 +51,13 @@ fn another_function(x: i32, y) { ``` 错误如下: + ```console error: expected one of `:`, `@`, or `|`, found `)` --> src/main.rs:5:30 | 5 | fn another_function(x: i32, y) { - | ^ expected one of `:`, `@`, or `|` // 期待以下符号之一 `:`, `@`, or `|` + | ^ expected one of `:`, `@`, or `|` // 期待以下符号之一 `:`, `@`, or `|` | = note: anonymous parameters are removed in the 2018 edition (see RFC 1685) // 匿名参数在 Rust 2018 edition 中就已经移除 @@ -68,9 +72,11 @@ help: if this is a type, explicitly ignore the parameter name // 如果y是一 ``` ## 函数返回 + 在上一章节语句和表达式中,我们有提到,在 Rust 中函数就是表达式,因此我们可以把函数的返回值直接赋给调用者。 函数的返回值就是函数体最后一条表达式的返回值,当然我们也可以使用 `return` 提前返回,下面的函数使用最后一条表达式来返回一个值: + ```rust fn plus_five(x:i32) -> i32 { x + 5 @@ -86,10 +92,12 @@ fn main() { `x + 5` 是一条表达式,求值后,返回一个值,因为它是函数的最后一行,因此该表达式的值也是函数的返回值。 再来看两个重点: + 1. `let x = plus_five(5)`,说明我们用一个函数的返回值来初始化 `x` 变量,因此侧面说明了在 Rust 中函数也是表达式,这种写法等同于 `let x = 5 + 5;` 2. `x + 5` 没有分号,因为它是一条表达式,这个在上一节中我们也有详细介绍 再来看一段代码,同时使用 `return` 和表达式作为返回值: + ```rust fn plus_or_minus(x:i32) -> i32 { if x > 5 { @@ -108,25 +116,25 @@ fn main() { `plus_or_minus` 函数根据传入 `x` 的大小来决定是做加法还是减法,若 `x > 5` 则通过 `return` 提前返回 `x - 5` 的值,否则返回 `x + 5` 的值。 - -#### Rust中的特殊返回类型 +#### Rust 中的特殊返回类型 ##### 无返回值`()` 对于 Rust 新手来说,有些返回类型很难理解,而且如果你想通过百度或者谷歌去搜索,都不好查询,因为这些符号太常见了,根本难以精确搜索到。 例如单元类型 `()`,是一个零长度的元组。它没啥作用,但是可以用来表达一个函数没有返回值: + - 函数没有返回值,那么返回一个 `()` - 通过 `;` 结尾的表达式返回一个 `()` - 例如下面的 `report` 函数会隐式返回一个 `()`: + ```rust use std::fmt::Debug; - + fn report(item: T) { println!("{:?}", item); - + } ``` @@ -139,6 +147,7 @@ fn clear(text: &mut String) -> () { ``` 在实际编程中,你会经常在错误提示中看到该 `()` 的身影出没,假如你的函数需要返回一个 `u32` 值,但是如果你不幸的以 `表达式;` 的方式作为函数的最后一行代码,就会报错: + ```rust fn add(x:u32,y:u32) -> u32 { x + y; @@ -146,6 +155,7 @@ fn add(x:u32,y:u32) -> u32 { ``` 错误如下: + ```console error[E0308]: mismatched types // 类型不匹配 --> src/main.rs:6:24 @@ -153,15 +163,15 @@ error[E0308]: mismatched types // 类型不匹配 6 | fn add(x:u32,y:u32) -> u32 { | --- ^^^ expected `u32`, found `()` // 期望返回u32,却返回() | | - | implicitly returns `()` as its body has no tail or `return` expression + | implicitly returns `()` as its body has no tail or `return` expression 7 | x + y; | - help: consider removing this semicolon ``` 还记得我们在[语句与表达式](./statement-expression.md)中讲过的吗?只有表达式能返回值,而 `;` 结尾的是语句,在 Rust 中,一定要严格区分**表达式**和**语句**的区别,这个在其它语言中往往是被忽视的点。 - ##### 永不返回的函数`!` + 当用 `!` 作函数返回类型的时候,表示该函数永不返回,特别的,这种语法往往用做会导致程序崩溃的函数: ```rust @@ -169,6 +179,7 @@ fn dead_end() -> ! { panic!("你已经到了穷途末路,崩溃吧!"); } ``` + 下面的函数创建了一个无限循环,该循环永不跳出,因此函数也永不返回: ```rust diff --git a/src/basic/base-type/index.md b/src/basic/base-type/index.md index d07f2d36..8cc6881b 100644 --- a/src/basic/base-type/index.md +++ b/src/basic/base-type/index.md @@ -1,4 +1,5 @@ # 基本类型 + 当一门语言不谈类型时,你得小心,这大概率是动态语言(别拍我,我承认是废话)。但是把类型大张旗鼓的用多个章节去讲的,Rust 是其中之一。 Rust 每个值都有其确切的数据类型,总的来说可以分为两类:基本类型和复合类型。 基本类型意味着它们往往是一个最小化原子类型,无法解构为其它类型(一般意义上来说),由以下组成: @@ -6,20 +7,21 @@ Rust 每个值都有其确切的数据类型,总的来说可以分为两类: - 数值类型: 有符号整数 (`i8`, `i16`, `i32`, `i64`, `isize`)、 无符号整数 (`u8`, `u16`, `u32`, `u64`, `usize`) 、浮点数 (`f32`, `f64`)、以及有理数、复数 - 字符串:字符串字面量和字符串切片 `&str` - 布尔类型: `true`和`false` -- 字符类型: 表示单个Unicode字符,存储为4个字节 +- 字符类型: 表示单个 Unicode 字符,存储为 4 个字节 - 单元类型: 即 `()` ,其唯一的值也是 `()` - ## 类型推导与标注 与 Python、Javascript 等动态语言不同,Rust 是一门静态类型语言,也就是编译器必须在编译期知道我们所有变量的类型,但这不意味着你需要为每个变量指定类型,因为 **Rust 编译器很聪明,它可以根据变量的值和上下文中的使用方式来自动推导出变量的类型**,同时编译器也不够聪明,在某些情况下,它无法推导出变量类型,需要手动去给予一个类型标注,关于这一点在 [Rust 语言初印象](https://course.rs/first-try/hello-world.html#rust-语言初印象)中有过展示。 来看段代码: + ```rust let guess = "42".parse().expect("Not a number!"); ``` 先忽略 `.parse().expect..` 部分,这段代码的目的是将字符串 `"42"` 进行解析,而编译器在这里无法推导出我们想要的类型:整数?浮点数?字符串?因此编译器会报错: + ```console $ cargo build Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations) @@ -31,4 +33,3 @@ error[E0282]: type annotations needed ``` 因此我们需要提供给编译器更多的信息,例如给 `guess` 变量一个**显式的类型标注**:`let guess: i32 = ...` 或者 `"42".parse::()`。 - diff --git a/src/basic/base-type/numbers.md b/src/basic/base-type/numbers.md index 7faadcba..68b0353c 100644 --- a/src/basic/base-type/numbers.md +++ b/src/basic/base-type/numbers.md @@ -1,4 +1,5 @@ # 数值类型 + 我朋友有一个领导(读者:你朋友?黑人问号)说过一句话:所有代码就是 0 和 1 ,简单的很。咱不评价这句话的正确性,但是计算机底层由 01 组成倒是真的。 计算机和数值关联在一起的时间,远比我们想象的要长,因此数值类型可以说是有计算机以来就有的类型,下面内容将深入讨论 Rust 的数值类型以及相关的运算符。 @@ -11,44 +12,41 @@ Rust 使用一个相对传统的语法来创建整数(`1`,`2`,...)和浮 #### 整数类型 -**整数**是没有小数部分的数字。之前使用过的 `i32` 类型,表示有符号的 32位 整数( `i` 是英文单词 *integer* 的首字母,与之相反的是 `u`,代表无符号 `unsigned` 类型)。下表显示了 Rust 中的内置的整数类型: - +**整数**是没有小数部分的数字。之前使用过的 `i32` 类型,表示有符号的 32 位整数( `i` 是英文单词 _integer_ 的首字母,与之相反的是 `u`,代表无符号 `unsigned` 类型)。下表显示了 Rust 中的内置的整数类型: | 长度 | 有符号类型 | 无符号类型 | -|------------|---------|---------| -| 8 位 | `i8` | `u8` | -| 16 位 | `i16` | `u16` | -| 32 位 | `i32` | `u32` | -| 64 位 | `i64` | `u64` | -| 128-位 | `i128` | `u128` | -| 视架构而定 | `isize` | `usize` | +| ---------- | ---------- | ---------- | +| 8 位 | `i8` | `u8` | +| 16 位 | `i16` | `u16` | +| 32 位 | `i32` | `u32` | +| 64 位 | `i64` | `u64` | +| 128-位 | `i128` | `u128` | +| 视架构而定 | `isize` | `usize` | 类型定义的形式统一为:`有无符号 + 类型大小(位数)`。**无符号数**表示数字只能取正数,而**有符号**则表示数字即可以取正数又可以取负数。就像在纸上写数字一样:当要强调符号时,数字前面可以带上正号或负号;然而,当很明显确定数字为正数时,就不需要加上正号了。有符号数字以[补码](https://en.wikipedia.org/wiki/Two%27s_complement)形式存储。 -每个有符号类型规定的数字范围是 -(2n - 1) ~ 2n - +每个有符号类型规定的数字范围是 -(2n - 1) ~ 2n - 1 - 1,其中 `n` 是该定义形式的位长度。因此 `i8` 可存储数字范围是 -(27) ~ 27 - 1,即 -128 ~ 127。无符号类型可以存储的数字范围是 0 ~ 2n - 1,所以 `u8` 能够存储的数字为 0 ~ 28 - 1,即 0 ~ 255。 此外,`isize` 和 `usize` 类型取决于程序运行的计算机 CPU 类型: 若 CPU 是 32 位的,则这两个类型是 32 位的,同理,若 CPU 是 64 位,那么它们则是 64 位。 整形字面量可以用下表的形式书写: - | 数字字面量 | 示例 | -|------------------|---------------| -| 十进制 | `98_222` | -| 十六进制 | `0xff` | -| 八进制 | `0o77` | -| 二进制 | `0b1111_0000` | +| ------------------ | ------------- | +| 十进制 | `98_222` | +| 十六进制 | `0xff` | +| 八进制 | `0o77` | +| 二进制 | `0b1111_0000` | | 字节 (仅限于 `u8`) | `b'A'` | - 这么多类型,有没有一个简单的使用准则?答案是肯定的, Rust 整形默认使用 `i32`,例如 `let i = 1`,那 `i` 就是 `i32` 类型,因此你可以首选它,同时该类型也往往是性能最好的。`isize` 和 `usize` 的主要应用场景是用作集合的索引。 > ##### 整型溢出 > -> 比方说有一个 `u8` ,它可以存放从 0 到 255 的值。那么当你将其修改为范围之外的值,比如 256,则会发生**整型溢出**。关于这一行为 Rust 有一些有趣的规则:当在 debug 模式编译时,Rust 会检查整型溢出,若存在这些问题,则使程序在编译时 *panic*(崩溃,Rust 使用这个术语来表明程序因错误而退出)。 +> 比方说有一个 `u8` ,它可以存放从 0 到 255 的值。那么当你将其修改为范围之外的值,比如 256,则会发生**整型溢出**。关于这一行为 Rust 有一些有趣的规则:当在 debug 模式编译时,Rust 会检查整型溢出,若存在这些问题,则使程序在编译时 _panic_(崩溃,Rust 使用这个术语来表明程序因错误而退出)。 > -> 在当使用 `--release` 参数进行 release 模式构建时,Rust **不**检测溢出。相反,当检测到整型溢出时,Rust 会按照补码循环溢出(*two’s complement wrapping*)的规则处理。简而言之,大于该类型最大值的数值会被补码转换成该类型能够支持的对应数字的最小值。比如在 `u8` 的情况下,256 变成 0,257 变成 1,依此类推。程序不会 *panic*,但是该变量的值可能不是你期望的值。依赖这种默认行为的代码都应该被认为是错误的代码。 +> 在当使用 `--release` 参数进行 release 模式构建时,Rust **不**检测溢出。相反,当检测到整型溢出时,Rust 会按照补码循环溢出(_two’s complement wrapping_)的规则处理。简而言之,大于该类型最大值的数值会被补码转换成该类型能够支持的对应数字的最小值。比如在 `u8` 的情况下,256 变成 0,257 变成 1,依此类推。程序不会 _panic_,但是该变量的值可能不是你期望的值。依赖这种默认行为的代码都应该被认为是错误的代码。 > > 要显式处理可能的溢出,可以使用标准库针对原始数字类型提供的这些方法: > @@ -77,7 +75,6 @@ fn main() { Rust 支持所有数字类型的基本数学运算:加法、减法、乘法、除法和取模运算。下面代码各使用一条 `let` 语句来说明相应运算的用法: - ```rust fn main() { // 加法 @@ -99,7 +96,6 @@ fn main() { 这些语句中的每个表达式都使用了数学运算符,并且计算结果为一个值,然后绑定到一个变量上。[附录 B](https://course.rs/appendix/operators.html#运算符) 中给出了 Rust 提供的所有运算符的列表。 - 再来看一个综合性的示例: ```rust @@ -110,15 +106,15 @@ fn main() { let twenty_one: i32 = 21; // 通过类型后缀的方式进行类型标注:22是i32类型 let twenty_two = 22i32; - + // 只有同样类型,才能运算 let addition = twenty + twenty_one + twenty_two; println!("{} + {} + {} = {}", twenty, twenty_one, twenty_two, addition); - + // 对于较长的数字,可以用_进行分割,提升可读性 let one_million: i64 = 1_000_000; println!("{}", one_million.pow(2)); - + // 定义一个f32数组,其中42.0会自动被推导为f32类型 let forty_twos = [ 42.0, @@ -129,24 +125,27 @@ fn main() { // 打印数组中第一个值,并控制小数位为2位 println!("{:.2}", forty_twos[0]); } - ``` +``` #### 浮点数陷阱 浮点数由于底层格式的特殊性,导致了如果在使用浮点数时不够谨慎,就可能造成危险,有两个原因: + 1. **浮点数往往是你想要数字的近似表达** -浮点数类型是基于二进制实现的,但是我们想要计算的数字往往是基于十进制,例如 `0.1` 在二进制上并不存在精确的表达形式,但是在十进制上就存在。这种不匹配性导致一定的歧义性,更多的,虽然浮点数能代表真实的数值,但是由于底层格式问题,它往往受限于定长的浮点数精度,如果你想要表达完全精准的真实数字,只有使用无限精度的浮点数才行 + 浮点数类型是基于二进制实现的,但是我们想要计算的数字往往是基于十进制,例如 `0.1` 在二进制上并不存在精确的表达形式,但是在十进制上就存在。这种不匹配性导致一定的歧义性,更多的,虽然浮点数能代表真实的数值,但是由于底层格式问题,它往往受限于定长的浮点数精度,如果你想要表达完全精准的真实数字,只有使用无限精度的浮点数才行 2. **浮点数在某些特性上是反直觉的** -例如大家都会觉得浮点数可以进行比较,对吧?是的,它们确实可以使用 `>` , `>=` 等进行比较,但是在某些场景下,这种直觉上的比较特性反而会害了你。因为 `f32` , `f64` 上的比较运算实现的是 `std::cmp::PartialEq` 特征(类似其他语言的接口),但是并没有实现 `std::cmp::Eq` 特征,但是后者在其它数值类型上都有定义,说了这么多,可能大家还是云里雾里,用一个例子来举例: + 例如大家都会觉得浮点数可以进行比较,对吧?是的,它们确实可以使用 `>`,`>=` 等进行比较,但是在某些场景下,这种直觉上的比较特性反而会害了你。因为 `f32` , `f64` 上的比较运算实现的是 `std::cmp::PartialEq` 特征(类似其他语言的接口),但是并没有实现 `std::cmp::Eq` 特征,但是后者在其它数值类型上都有定义,说了这么多,可能大家还是云里雾里,用一个例子来举例: -Rust 的 `HashMap` 数据结构,是一个 KV 类型的 Hash Map 实现,它对于 `K` 没有特定类型的限制,但是要求能用作 `K` 的类型必须实现了 `std::cmp::Eq` 特征,因此这意味着你无法使用浮点数作为 `HashMap` 的 `Key`,来存储键值对,但是作为对比,Rust的整数类型、字符串类型、布尔类型都实现了该特征,因此可以作为 `HashMap` 的 `Key`。 +Rust 的 `HashMap` 数据结构,是一个 KV 类型的 Hash Map 实现,它对于 `K` 没有特定类型的限制,但是要求能用作 `K` 的类型必须实现了 `std::cmp::Eq` 特征,因此这意味着你无法使用浮点数作为 `HashMap` 的 `Key`,来存储键值对,但是作为对比,Rust 的整数类型、字符串类型、布尔类型都实现了该特征,因此可以作为 `HashMap` 的 `Key`。 为了避免上面说的两个陷阱,你需要遵守以下准则: + - 避免在浮点数上测试相等性 - 当结果在数学上可能存在未定义时,需要格外的小心 来看个小例子: + ```rust fn main() { // 断言0.1 + 0.2与0.3相等 @@ -154,7 +153,7 @@ fn main() { } ``` -你可能以为,这段代码没啥问题吧,实际上它会 *panic*(程序崩溃,抛出异常),因为二进制精度问题,导致了 0.1 + 0.2 并不严格等于 0.3,它们可能在小数点 N 位后存在误差。 +你可能以为,这段代码没啥问题吧,实际上它会 _panic_(程序崩溃,抛出异常),因为二进制精度问题,导致了 0.1 + 0.2 并不严格等于 0.3,它们可能在小数点 N 位后存在误差。 那如果非要进行比较呢?可以考虑用这种方式 `(0.1_f64 + 0.2 - 0.3).abs() < 0.00001` ,具体小于多少,取决于你对精度的需求。 @@ -186,11 +185,11 @@ fn main() { abc (f32) 0.1 + 0.2: 3e99999a 0.3: 3e99999a - + xyz (f64) 0.1 + 0.2: 3fd3333333333334 0.3: 3fd3333333333333 - + thread 'main' panicked at 'assertion failed: xyz.0 + xyz.1 == xyz.2', ➥ch2-add-floats.rs.rs:14:5 note: run with `RUST_BACKTRACE=1` environment variable to display @@ -236,6 +235,7 @@ for i in 1..=5 { ``` 最终程序输出: + ```console 1 2 @@ -255,6 +255,7 @@ for i in 'a'..='z' { ## 有理数和复数 Rust 的标准库相比其它语言,准入门槛较高,因此有理数和复数并未包含在标准库中: + - 有理数和复数 - 任意大小的整数和任意精度的浮点数 - 固定精度的十进制小数,常用于货币相关的场景 @@ -262,6 +263,7 @@ Rust 的标准库相比其它语言,准入门槛较高,因此有理数和复 好在社区已经开发出高质量的 Rust 数值库:[num](https://crates.io/crates/num)。 按照以下步骤来引入 `num` 库: + 1. 创建新工程 `cargo new complex-num && cd complex-num` 2. 在 `Cargo.toml` 中的 `[dependencies]` 下添加一行 `num = "0.4.0"` 3. 将 `src/main.rs` 文件中的 `main` 函数替换为下面的代码 @@ -269,17 +271,16 @@ Rust 的标准库相比其它语言,准入门槛较高,因此有理数和复 ```rust use num::complex::Complex; - + fn main() { let a = Complex { re: 2.1, im: -1.2 }; let b = Complex::new(11.1, 22.2); let result = a + b; - + println!("{} + {}i", result.re, result.im) } ``` - ## 总结 之前提到了过 Rust 的数值类型和运算跟其他语言较为相似,但是实际上,除了语法上的不同之外,还是存在一些差异点: @@ -289,8 +290,3 @@ use num::complex::Complex; - **Rust 的数值上可以使用方法**. 例如你可以用以下方法来将 `13.14` 取整:`13.14_f32.round()`,在这里我们使用了类型后缀,因为编译器需要知道 `13.14` 的具体类型 数值类型的讲解已经基本结束,接下来,来看看字符和布尔类型。 - - - - - diff --git a/src/basic/base-type/statement-expression.md b/src/basic/base-type/statement-expression.md index 53f6bbd5..49c0c0f5 100644 --- a/src/basic/base-type/statement-expression.md +++ b/src/basic/base-type/statement-expression.md @@ -1,6 +1,7 @@ # 语句和表达式 Rust 的函数体是由一系列语句组成,最后由一个表达式来返回值,例如: + ```rust fn add_with_extra(x: i32, y: i32) -> i32 { let x = x + 1; // 语句 @@ -8,6 +9,7 @@ fn add_with_extra(x: i32, y: i32) -> i32 { x + y // 表达式 } ``` + 语句会执行一些操作但是不会返回一个值,而表达式会在求值后返回一个值,因此在上述函数体的三行代码中,前两行是语句,最后一行是表达式。 对于 Rust 语言而言,**这种基于语句和表达式的方式是非常重要的,你需要能明确的区分这两个概念**, 但是对于很多其它语言而言,这两个往往无需区分。基于表达式是函数式语言的重要特征,**表达式总要返回值**。 @@ -30,7 +32,8 @@ let (a, c) = ("hi", false); let b = (let a = 8); ``` -错误如下: +错误如下: + ```console error: expected expression, found statement (`let`) // 期望表达式,却发现`let`语句 --> src/main.rs:2:13 @@ -52,13 +55,13 @@ error[E0658]: `let` expressions in this position are experimental ``` -以上的错误告诉我们 `let` 是语句,不是表达式,因此它不返回值,也就不能给其它变量赋值。但是该错误还透漏了一个重要的信息, `let` 作为表达式已经是试验功能了,也许不久的将来,我们在[`stable rust`](../../appendix/rust-version.md)下可以这样使用。 - +以上的错误告诉我们 `let` 是语句,不是表达式,因此它不返回值,也就不能给其它变量赋值。但是该错误还透漏了一个重要的信息, `let` 作为表达式已经是试验功能了,也许不久的将来,我们在 [`stable rust`](../../appendix/rust-version.md) 下可以这样使用。 ## 表达式 + 表达式会进行求值,然后返回一个值。例如 `5 + 6`,在求值后,返回值 `11`,因此它就是一条表达式。 -表达式可以成为语句的一部分,例如 `let y = 6` 中, `6` 就是一个表达式,它在求值后返回一个值 `6` (有些反直觉,但是确实是表达式). +表达式可以成为语句的一部分,例如 `let y = 6` 中,`6` 就是一个表达式,它在求值后返回一个值 `6`(有些反直觉,但是确实是表达式)。 调用一个函数是表达式,因为会返回一个值,调用宏也是表达式,用花括号包裹最终返回一个值的语句块也是表达式,总之,能返回值,它就是表达式: @@ -74,6 +77,7 @@ fn main() { ``` 上面使用一个语句块表达式将值赋给 `y` 变量,语句块长这样: + ```rust { let x = 3; @@ -81,5 +85,4 @@ fn main() { } ``` -该语句块是表达式的原因是:它的最后一行是表达式,返回了 `x + 1` 的值,注意 `x + 1` 不能以分号结尾,否则就会从表达式变成语句, **表达式不能包含分号**。这一点非常重要,一旦你在表达式后加上分号,它就会变成一条语句,再也**不会**返回一个值,请牢记! - +该语句块是表达式的原因是:它的最后一行是表达式,返回了 `x + 1` 的值,注意 `x + 1` 不能以分号结尾,否则就会从表达式变成语句,**表达式不能包含分号**。这一点非常重要,一旦你在表达式后加上分号,它就会变成一条语句,再也**不会**返回一个值,请牢记! diff --git a/src/basic/collections/hashmap.md b/src/basic/collections/hashmap.md index 3f010b0a..639e6152 100644 --- a/src/basic/collections/hashmap.md +++ b/src/basic/collections/hashmap.md @@ -1,14 +1,15 @@ -# KV存储HashMap +# KV 存储 HashMap 和动态数组一样,`HashMap` 也是 Rust 标准库中提供的集合类型,但是又与动态数组不同,`HashMap` 中存储的是一一映射的 `KV `键值对,并提供了平均复杂度为 `O(1)` 的查询方法,当我们希望通过一个 `Key` 去查询值时,该类型非常有用,以致于 Go 语言将该类型设置成了语言级别的内置特性。 Rust 中哈希类型(哈希映射)为 `HashMap`,在其它语言中,也有类似的数据结构,例如 `hash map`,`map`,`object`,`hash table`,`字典` 等等,引用小品演员孙涛的一句台词:大家都是本地狐狸,别搁那装貂 :)。 -## 创建HashMap +## 创建 HashMap 跟创建动态数组 `Vec` 的方法类似,可以使用 `new` 方法来创建` HashMap`,然后通过` insert` 方法插入键值对。 -#### 使用new方法创建 +#### 使用 new 方法创建 + ```rust use std::collections::HashMap; @@ -23,13 +24,14 @@ my_gems.insert("河边捡的误以为是宝石的破石头", 18); 很简单对吧?跟其它语言没有区别,聪明的同学甚至能够猜到该 `HashMap` 的类型:`HashMap<&str,i32>`。 -但是还有一点,你可能没有注意,那就是使用 `HashMap` 需要手动通过 `use ...` 从标准库中引入到我们当前的作用域中来,仔细回忆下,之前使用另外两个集合类型 `String` 和` Vec` 时,我们是否有手动引用过?答案是 `No`,因为 `HashMap` 并没有包含在 Rust 的 [`prelude`](../../appendix/prelude.md) 中(Rust为了简化用户使用,提前将最常用的类型自动引入到作用域中)。 +但是还有一点,你可能没有注意,那就是使用 `HashMap` 需要手动通过 `use ...` 从标准库中引入到我们当前的作用域中来,仔细回忆下,之前使用另外两个集合类型 `String` 和` Vec` 时,我们是否有手动引用过?答案是 `No`,因为 `HashMap` 并没有包含在 Rust 的 [`prelude`](../../appendix/prelude.md) 中(Rust 为了简化用户使用,提前将最常用的类型自动引入到作用域中)。 所有的集合类型都是动态的,意味着它们没有固定的内存大小,因此它们底层的数据都存储在内存堆上,然后通过一个存储在栈中的引用类型来访问。同时,跟其它集合类型一致,`HashMap` 也是内聚性的,即所有的 `K` 必须拥有同样的类型,`V` 也是如此。 > 跟 `Vec` 一样,如果预先知道要存储的 `KV` 对个数,可以使用 `HashMap::with_capacity(capacity)` 创建指定大小的 `HashMap`,避免频繁的内存分配和拷贝,提升性能 -#### 使用迭代器和collect方法创建 +#### 使用迭代器和 collect 方法创建 + 在实际使用中,不是所有的场景都能 `new` 一个哈希表后,然后悠哉悠哉的依次插入对应的键值对,而是可能会从另外一个数据结构中,获取到对应的数据,最终生成 `HashMap`。 例如考虑一个场景,有一张表格中记录了足球联赛中各队伍名称和积分的信息,这张表如果被导入到 Rust 项目中,一个合理的数据结构是 `Vec<(String, u32)>` 类型,该数组中的元素是一个个元组,该数据结构跟表格数据非常契合:表格中的数据都是逐行存储,每一个行都存有一个 `(队伍名称, 积分)` 的信息。 @@ -37,6 +39,7 @@ my_gems.insert("河边捡的误以为是宝石的破石头", 18); 但是在很多时候,又需要通过队伍名称来查询对应的积分,此时动态数组就不适用了,因此可以用 `HashMap` 来保存相关的**队伍名称 -> 积分**映射关系。 理想很骨感,现实很丰满,如何将 `Vec<(String, u32)>` 中的数据快速写入到 `HashMap` 中? 一个动动脚趾头就能想到的笨方法如下: + ```rust fn main() { use std::collections::HashMap; @@ -51,7 +54,7 @@ fn main() { for team in &teams_list { teams_map.insert(&team.0, team.1); } - + println!("{:?}",teams_map) } ``` @@ -59,6 +62,7 @@ fn main() { 遍历列表,将每一个元组作为一对 `KV `插入到 `HashMap` 中,很简单,但是……也不太聪明的样子,换个词说就是 —— 不够 rusty。 好在,Rust 为我们提供了一个非常精妙的解决办法:先将 `Vec` 转为迭代器,接着通过 `collect` 方法,将迭代器中的元素收集后,转成 `HashMap`: + ```rust fn main() { use std::collections::HashMap; @@ -70,7 +74,7 @@ fn main() { ]; let teams_map: HashMap<_,_> = teams_list.into_iter().collect(); - + println!("{:?}",teams_map) } ``` @@ -78,21 +82,24 @@ fn main() { 代码很简单,`into_iter` 方法将列表转为迭代器,接着通过 `collect` 进行收集,不过需要注意的是,`collect` 方法在内部实际上支持生成多种类型的目标集合,因为我们需要通过类型标注 `HashMap<_,_>` 来告诉编译器:请帮我们收集为 `HashMap` 集合类型,具体的 `KV` 类型,麻烦编译器您老人家帮我们推导。 由此可见,Rust 中的编译器时而小聪明,时而大聪明,不过好在,它大聪明的时候,会自家人知道自己事,总归会通知你一声: + ```console error[E0282]: type annotations needed // 需要类型标注 --> src/main.rs:10:9 | -10 | let teams_map = teams_list.into_iter().collect(); +10 | let teams_map = teams_list.into_iter().collect(); | ^^^^^^^^^ consider giving `teams_map` a type // 给予 `teams_map` 一个具体的类型 ``` ## 所有权转移 `HashMap` 的所有权规则与其它 Rust 类型没有区别: + - 若类型实现 `Copy` 特征,该类型会被复制进 `HashMap`,因此无所谓所有权 - 若没实现 `Copy` 特征,所有权将被转移给 `HashMap` 中 例如我参选帅气男孩时的场景再现: + ```rust fn main() { use std::collections::HashMap; @@ -109,6 +116,7 @@ fn main() { ``` 运行代码,报错如下: + ```console error[E0382]: borrow of moved value: `name` --> src/main.rs:10:32 @@ -118,15 +126,15 @@ error[E0382]: borrow of moved value: `name` ... 8 | handsome_boys.insert(name, age); | ---- value moved here -9 | +9 | 10 | println!("因为过于无耻,{}已经被除名", name); | ^^^^ value borrowed here after move ``` 提示很清晰,`name` 是 `String` 类型,因此它受到所有权的限制,在 `insert` 时,它的所有权被转移给 `handsome_boys`,所以最后在使用时,会遇到这个无情但是意料之中的报错。 +**如果你使用引用类型放入 HashMap 中**,请确保该引用的生命周期至少跟 `HashMap` 活得一样久: -**如果你使用引用类型放入HashMap中**,请确保该引用的生命周期至少跟 `HashMap` 活得一样久: ```rust fn main() { use std::collections::HashMap; @@ -144,10 +152,11 @@ fn main() { ``` 上面代码,我们借用 `name` 获取了它的引用,然后插入到 `handsome_boys` 中,至此一切都很完美。但是紧接着,就通过 `drop` 函数手动将 `name` 字符串从内存中移除,再然后就报错了: + ```console handsome_boys.insert(&name, age); | ----- borrow of `name` occurs here // name借用发生在此处 -9 | +9 | 10 | std::mem::drop(name); | ^^^^ move out of `name` occurs here // name的所有权被转移走 11 | println!("因为过于无耻,{:?}已经被除名", handsome_boys); @@ -156,8 +165,10 @@ fn main() { 最终,某人因为过于无耻,真正的被除名了 :) -## 查询HashMap +## 查询 HashMap + 通过 `get` 方法可以获取元素: + ```rust use std::collections::HashMap; @@ -171,10 +182,12 @@ let score: Option<&i32> = scores.get(&team_name); ``` 上面有几点需要注意: + - `get` 方法返回一个 `Option<&i32> `类型:当查询不到时,会返回一个 `None`,查询到时返回 `Some(&i32)` - `&i32` 是对 `HashMap` 中值的借用,如果不使用借用,可能会发生所有权的转移 还可以通过循环的方式依次遍历 `KV` 对: + ```rust use std::collections::HashMap; @@ -187,14 +200,18 @@ for (key, value) in &scores { println!("{}: {}", key, value); } ``` + 最终输出: + ```console Yellow: 50 Blue: 10 ``` -## 更新HashMap中的值 +## 更新 HashMap 中的值 + 更新值的时候,涉及多种情况,咱们在代码中一一进行说明: + ```rust fn main() { use std::collections::HashMap; @@ -210,7 +227,7 @@ fn main() { // 查询新插入的值 let new = scores.get("Blue"); assert_eq!(new, Some(&20)); - + // 查询Yellow对应的值,若不存在则插入新值 let v = scores.entry("Yellow").or_insert(5); assert_eq!(*v, 5); // 不存在,插入5 @@ -224,7 +241,9 @@ fn main() { 具体的解释在代码注释中已有,这里不再进行赘述。 #### 在已有值的基础上更新 + 另一个常用场景如下:查询某个 `key` 对应的值,若不存在则插入新值,若存在则对已有的值进行更新,例如在文本中统计词语出现的次数: + ```rust use std::collections::HashMap; @@ -243,11 +262,12 @@ println!("{:?}", map); 上面代码中,新建一个 `map` 用于保存词语出现的次数,插入一个词语时会进行判断:若之前没有插入过,则使用该词语作 `Key`,插入次数 0 作为 `Value`,若之前插入过则取出之前统计的该词语出现的次数,对其加一。 有两点值得注意: + - `or_insert` 返回了 `&mut v` 引用,因此可以通过该可变引用直接修改 `map` 中对应的值 - 使用 `count` 引用时,需要先进行解引用 `*count`,否则会出现类型不匹配 - ## 哈希函数 + 你肯定比较好奇,为何叫哈希表,到底什么是哈希。 先来设想下,如果要实现 `Key` 与 `Value` 的一一对应,是不是意味着我们要能比较两个 `Key` 的相等性?例如 "a" 和 "b",1 和 2,当这些类型做 `Key` 且能比较时,可以很容易知道 `1` 对应的值不会错误的映射到 `2` 上,因为 `1` 不等于 `2`。因此,一个类型能否作为 `Key` 的关键就是是否能进行相等比较,或者说该类型是否实现了 `std::cmp::Eq` 特征。 @@ -262,7 +282,9 @@ println!("{:?}", map); 若要追求安全,尽可能减少冲突,同时防止拒绝服务(Denial of Service, DoS)攻击,就要使用密码学安全的哈希函数,`HashMap` 就是使用了这样的哈希函数。反之若要追求性能,就需要使用没有那么安全的算法。 #### 高性能三方库 + 因此若性能测试显示当前标准库默认的哈希函数不能满足你的性能需求,就需要去 [`crates.io`](https://crates.io) 上寻找其它的哈希函数实现,使用方法很简单: + ```rust use std::hash::BuildHasherDefault; use std::collections::HashMap; @@ -277,4 +299,4 @@ assert_eq!(hash.get(&42), Some(&"the answer")); > 目前,`HashMap` 使用的哈希函数是 `SipHash`,它的性能不是很高,但是安全性很高。`SipHash` 在中等大小的 `Key` 上,性能相当不错,但是对于小型的 `Key` (例如整数)或者大型 `Key` (例如字符串)来说,性能还是不够好。若你需要极致性能,例如实现算法,可以考虑这个库:[ahash](https://github.com/tkaitchuck/ahash) -最后,如果你想要了解 `HashMap` 更多的用法,请参见本书的标准库解析章节:[HashMap常用方法](../../std/hashmap.md) +最后,如果你想要了解 `HashMap` 更多的用法,请参见本书的标准库解析章节:[HashMap 常用方法](../../std/hashmap.md) diff --git a/src/basic/collections/intro.md b/src/basic/collections/intro.md index be8ae8e0..43564b59 100644 --- a/src/basic/collections/intro.md +++ b/src/basic/collections/intro.md @@ -8,6 +8,6 @@ 紧接着,第二个集合在全场的嘘声和羡慕眼光中闪亮登场,只见里面的元素排成一对一对的,彼此都手牵着手,非对方莫属,这种情深深雨蒙蒙的样子真是...挺欠扁的。 它就是 `HashMap` 类型,该类型允许你在里面存储 `KV` 对,每一个 `K` 都有唯一的 `V` 与之配对。 -最后,请用热烈的掌声迎接我们的 `String `集合,哦,抱歉,`String` 集合天生低调,见不得前两个那样,因此被气走了,你可以去[这里](../compound-type/string-slice)找它。 +最后,请用热烈的掌声迎接我们的 `String` 集合,哦,抱歉,`String` 集合天生低调,见不得前两个那样,因此被气走了,你可以去[这里](../compound-type/string-slice)找它。 -言归正传,本章所讲的 `Vector`、`HashMap `再加上之前的 `String` 类型,是标准库中最最常用的集合类型,可以说,几乎任何一段代码中都可以找到它们的身影,那么先来看看`Vector`。 +言归正传,本章所讲的 `Vector`、`HashMap `再加上之前的 `String` 类型,是标准库中最最常用的集合类型,可以说,几乎任何一段代码中都可以找到它们的身影,那么先来看看 `Vector`。 diff --git a/src/basic/collections/vector.md b/src/basic/collections/vector.md index ecb4c0ea..b7d904d9 100644 --- a/src/basic/collections/vector.md +++ b/src/basic/collections/vector.md @@ -1,4 +1,4 @@ -# 动态数组Vector +# 动态数组 Vector 动态数组类型用`Vec`表示,事实上,在之前的章节,它的身影多次出现,我们一直没有细讲,只是简单的把它当作数组处理。 @@ -7,15 +7,19 @@ 总之,当我们想拥有一个列表,里面都是相同类型的数据时,动态数组将会非常有用。 ## 创建动态数组 + 在 Rust 中,有多种方式可以创建动态数组。 #### Vec::new + 使用 `Vec::new` 创建动态数组是最 rusty 的方式,它调用了 `Vec` 中的 `new` 关联函数: + ```rust let v: Vec = Vec::new(); ``` 这里,`v` 被显式地声明了类型`Vec`,这是因为 Rust 编译器无法从 `Vec::new()` 中得到任何关于类型的暗示信息,因此也无法推导出 `v` 的具体类型,但是当你向里面增加一个元素后,一切又不同了: + ```rust let mut v = Vec::new(); v.push(1); @@ -26,15 +30,19 @@ v.push(1); > 如果预先知道要存储的元素个数,可以使用 `Vec::with_capacity(capacity)` 创建动态数组,这样可以避免因为插入大量新数据导致频繁的内存分配和拷贝,提升性能 #### vec![] + 还可以使用宏 `vec!` 来创建数组,与 `Vec::new` 有所不同,前者能在创建同时给予初始化值: + ```rust let v = vec![1, 2, 3]; -``` +``` 同样,此处的 `v` 也无需标注类型,编译器只需检查它内部的元素即可自动推导出 `v` 的类型是 `Vec` (Rust 中,整数默认类型是 `i32`,在[数值类型](../base-type/numbers.md#整数类型)中有详细介绍)。 -## 更新Vector +## 更新 Vector + 向数组尾部添加元素,可以使用 `push` 方法: + ```rust let mut v = Vec::new(); v.push(1); @@ -42,9 +50,10 @@ v.push(1); 与其它类型一样,必须将 `v` 声明为 `mut` 后,才能进行修改。 +## Vector 与其元素共存亡 -## Vector与其元素共存亡 跟结构体一样,`Vector` 类型在超出作用域范围后,会被自动删除: + ```rust { let v = vec![1, 2, 3]; @@ -55,8 +64,10 @@ v.push(1); 当 `Vector` 被删除后,它内部存储的所有内容也会随之被删除。目前来看,这种解决方案简单直白,但是当 `Vector` 中的元素被引用后,事情可能会没那么简单。 -## 从Vector中读取元素 +## 从 Vector 中读取元素 + 读取指定位置的元素有两种方式可选:通过下标索引访问或者使用 `get` 方法: + ```rust let v = vec![1, 2, 3, 4, 5]; @@ -72,7 +83,9 @@ match v.get(2) { 和其它语言一样,集合类型的索引下标都是从 `0` 开始,`&v[2]` 表示借用 `v` 中的第三个元素,最终会获得该元素的引用。而 `v.get(2)` 也是访问第三个元素,但是有所不同的是,它返回了 `Option<&T>`,因此还需要额外的 `match` 来匹配解构出具体的值。 #### 下标索引与 `.get` 的区别 + 这两种方式都能成功的读取到指定的数组元素,既然如此为什么会存在两种方法?何况 `.get` 还会增加使用复杂度,让我们通过示例说明: + ```rust let v = vec![1, 2, 3, 4, 5]; @@ -82,12 +95,14 @@ let does_not_exist = v.get(100); 运行以上代码,`&v[100]` 的访问方式会导致程序无情报错退出,因为发生了数组越界访问。 但是 `v.get` 就不会,它在内部做了处理,有值的时候返回 `Some(T)`,无值的时候返回 `None`,因此 `v.get` 的使用方式非常安全。 -既然如此,为何不统一使用 `v.get` 的形式?因为实在是有些啰嗦,Rust语言的设计者和使用者在审美这方面还是相当统一的:简洁即正义,何况性能上也会有轻微的损耗。 +既然如此,为何不统一使用 `v.get` 的形式?因为实在是有些啰嗦,Rust 语言的设计者和使用者在审美这方面还是相当统一的:简洁即正义,何况性能上也会有轻微的损耗。 既然有两个选择,肯定就有如何选择的问题,答案很简单,当你确保索引不会越界的时候,就用索引访问,否则用 `.get`。例如,访问第几个数组元素并不取决于我们,而是取决于用户的输入时,用 `.get` 会非常适合,天知道那些可爱的用户会输入一个什么样的数字进来! ##### 同时借用多个数组元素 + 既然涉及到借用数组元素,那么很可能会遇到同时借用多个数组元素的情况,还记得在[所有权和借用](../ownership/borrowing.md#借用规则总结)章节咱们讲过的借用规则嘛?如果记得,就来看看下面的代码:) + ```rust let mut v = vec![1, 2, 3, 4, 5]; @@ -101,6 +116,7 @@ println!("The first element is: {}", first); 先不运行,来推断下结果,首先 `first = &v[0]` 进行了不可变借用,`v.push` 进行了可变借用,如果 `first` 在 `v.push` 之后不再使用,那么该段代码可以成功编译(原因见[引用的作用域](../ownership/borrowing.md#可变引用与不可变引用不能同时存在))。 可是上面的代码中,`first` 这个不可变借用在可变借用 `v.push` 后被使用了,那么妥妥的,编译器就会报错: + ```console $ cargo run Compiling collections v0.1.0 (file:///projects/collections) @@ -109,10 +125,10 @@ error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immuta | 4 | let first = &v[0]; | - immutable borrow occurs here // 不可变借用发生在此处 -5 | +5 | 6 | v.push(6); | ^^^^^^^^^ mutable borrow occurs here // 可变借用发生在此处 -7 | +7 | 8 | println!("The first element is: {}", first); | ----- immutable borrow later used here // 不可变借用在这里被使用 @@ -128,9 +144,10 @@ error: could not compile `collections` due to previous error > 若读者想要更深入的了解`Vec`,可以看看[Rustonomicon](https://nomicon.purewhite.io/vec/vec.html),其中从零手撸一个动态数组,非常适合深入学习 -## 迭代遍历Vector中的元素 +## 迭代遍历 Vector 中的元素 如果想要依次访问数组中的元素,可以使用迭代的方式去遍历数组,这种方式比用下标的方式去遍历数组更安全也更高效(每次下标访问都会触发数组边界检查): + ```rust let v = vec![1, 2, 3]; for i in &v { @@ -139,6 +156,7 @@ for i in &v { ``` 也可以在迭代过程中,修改 `Vector` 中的元素: + ```rust let mut v = vec![1, 2, 3]; for i in &mut v { @@ -147,7 +165,9 @@ for i in &mut v { ``` ## 存储不同类型的元素 + 在本节开头,有讲到数组的元素必需类型相同,但是也提到了解决方案:那就是通过使用枚举类型和特征对象来实现不同类型元素的存储。先来看看通过枚举如何实现: + ```rust #[derive(Debug)] enum IpAddr { @@ -173,6 +193,7 @@ fn show_addr(ip: IpAddr) { 数组 `v` 中存储了两种不同的 `ip` 地址,但是这两种都属于 `IpAddr` 枚举类型的成员,因此可以存储在数组中。 再来看看特征对象的实现: + ```rust trait IpAddr { fn display(&self); diff --git a/src/basic/comment.md b/src/basic/comment.md index c6182ef5..3d70f2ff 100644 --- a/src/basic/comment.md +++ b/src/basic/comment.md @@ -1,9 +1,11 @@ # 注释和文档 + 好的代码会说话,好的程序员不写注释,这些都是烂大街的“编程界俚语”。但是,如果你真的遇到一个不写注释的项目或程序员,那一定会对它/他“刮目相看”。 在之前的章节我们学习了包和模块如何使用,在此章节将进一步学习如何书写文档注释,以及如何使用 `cargo doc` 生成项目的文档,最后将以一个包、模块和文档的综合性例子,来将这些知识融会贯通。 ## 注释的种类 + 在 Rust 中,注释分为三类: - 代码注释,用于说明某一块代码的功能,读者往往是同一个项目的协作开发者 @@ -13,16 +15,18 @@ 通过这些注释,实现了 Rust 极其优秀的文档化支持,甚至你还能在文档注释中写测试用例,省去了单独写测试用例的环节,我直接好家伙! ## 代码注释 + 显然之前的刮目相看是打了引号的,想要去掉引号,该写注释的时候,就老老实实的,不过写时需要遵循八字原则:**围绕目标,言简意赅**,记住,洋洋洒洒那是用来形容文章的,不是形容注释! 代码注释方式有两种: #### 行注释 `//` + ```rust fn main() { // 我是Sun... // face - let name = "sunface"; + let name = "sunface"; let age = 18; // 今年好像是18岁 } ``` @@ -31,7 +35,8 @@ fn main() { 当注释行数较多时,你还可以使用**块注释** -#### 块注释`/* ..... */` +#### 块注释`/* ..... */` + ```rust fn main() { /* @@ -40,9 +45,9 @@ fn main() { S u n - ... 淦,好长! + ... 淦,好长! */ - let name = "sunface"; + let name = "sunface"; let age = "???"; // 今年其实。。。挺大了 } ``` @@ -52,13 +57,16 @@ fn main() { 你会发现,Rust 的代码注释跟其它语言并没有区别,主要区别其实在于文档注释这一块,也是本章节内容的重点。 ## 文档注释 + 当查看一个 `crates.io` 上的包时,往往需要通过它提供的文档来浏览相关的功能特性、使用方式,这种文档就是通过文档注释实现的。 Rust 提供了 `cargo doc` 的命令,可以用于把这些文档注释转换成 `HTML` 网页文件,最终展示给用户浏览,这样用户就知道这个包是做什么的以及该如何使用。 #### 文档行注释 `///` + 本书的一大特点就是废话不多,因此我们开门见山: -```rust + +````rust /// `add_one` 将指定值加1 /// /// # Examples @@ -72,7 +80,7 @@ Rust 提供了 `cargo doc` 的命令,可以用于把这些文档注释转换 pub fn add_one(x: i32) -> i32 { x + 1 } -``` +```` 以上代码有几点需要注意: @@ -83,7 +91,9 @@ pub fn add_one(x: i32) -> i32 { 咦?文档注释中的例子,为什看上去像是能运行的样子?竟然还是有 `assert_eq` 这种常用于测试目的的宏。 嗯,你的感觉没错,详细内容会在本章后面讲解,容我先卖个关子。 #### 文档块注释 `/** ... */` + 与代码注释一样,文档也有块注释,当注释内容多时,使用块注释可以减少 `///` 的使用: + ````rust /** `add_two` 将指定值加2 @@ -97,11 +107,12 @@ assert_eq!(7, answer); ``` */ pub fn add_two(x: i32) -> i32 { - x + 2 + x + 2 } ```` #### 查看文档 cargo doc + 锦衣不夜行,这是中国人的传统美德。我们写了这么漂亮的文档注释,当然要看看网页中是什么效果咯。 很简单,运行 `cargo doc` 可以直接生成 `HTML` 文件,放入*target/doc*目录下。 @@ -113,6 +124,7 @@ pub fn add_two(x: i32) -> i32 { 非常棒,而且非常简单,这就是 Rust 工具链的强大之处。 #### 常用文档标题 + 之前我们见到了在文档注释中该如何使用 `markdown`,其中包括 `# Examples` 标题。除了这个标题,还有一些常用的,你可以在项目中酌情使用: - **Panics**:函数可能会出现的异常状况,这样调用函数的人就可以提前规避 @@ -122,11 +134,13 @@ pub fn add_two(x: i32) -> i32 { 话说回来,这些标题更多的是一种惯例,如果你非要用中文标题也没问题,但是最好在团队中保持同样的风格 :) ## 包和模块级别的注释 + 除了函数、结构体等 Rust 项的注释,你还可以给包和模块添加注释,需要注意的是,**这些注释要添加到包、模块的最上方**! 与之前的任何注释一样,包级别的注释也分为两种:行注释 `//!` 和块注释 `/*! ... */`。 现在,为我们的包增加注释,在 `src/lib.rs` 包根的最上方,添加: + ```rust /*! lib包是world_hello二进制包的依赖包, 里面包含了compute等有用模块 */ @@ -135,6 +149,7 @@ pub mod compute; ``` 然后再为该包根的子模块 `src/compute.rs` 添加注释: + ```rust //! 计算一些你口算算不出来的复杂算术题 @@ -152,10 +167,12 @@ pub mod compute; 至此,关于如何注释的内容,就结束了,那么注释还能用来做什么?可以玩出花来吗?答案是`Yes`. ## 文档测试(Doc Test) + 相信读者之前都写过单元测试用例,其中一个很蛋疼的问题就是,随着代码的进化,单元测试用例经常会失效,过段时间后(为何是过段时间?应该这么问,有几个开发喜欢写测试用例 =,=),你发现需要连续修改不少处代码,才能让测试重新工作起来。然而,在 Rust 中,大可不必。 在之前的 `add_one` 中,我们写的示例代码非常像是一个单元测试的用例,这是偶然吗?并不是。因为 Rust 允许我们在文档注释中写单元测试用例!方法就如同之前做的: -```rust + +````rust /// `add_one` 将指定值加1 /// /// # Examples11 @@ -169,9 +186,10 @@ pub mod compute; pub fn add_one(x: i32) -> i32 { x + 1 } -``` +```` 以上的注释不仅仅是文档,还可以作为单元测试的用例运行,使用 `cargo test` 运行测试: + ```console Doc-tests world_hello @@ -187,8 +205,10 @@ test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; fini > 需要注意的是,你可能需要使用类如 `world_hello::compute::add_one(arg)` 的完整路径来调用函数,因为测试是在另外一个独立的线程中运行的 #### 造成 panic 的文档测试 + 文档测试中的用例还可以造成 `panic`: -```rust + +````rust /// # Panics /// /// The function panics if the second argument is zero. @@ -204,8 +224,10 @@ pub fn div(a: i32, b: i32) -> i32 { a / b } -``` +```` + 以上测试运行后会 `panic`: + ```console ---- src/compute.rs - compute::div (line 38) stdout ---- Test executable failed (exit code 101). @@ -216,7 +238,8 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace ``` 如果想要通过这种测试,可以添加 `should_panic`: -```rust + +````rust /// # Panics /// /// The function panics if the second argument is zero. @@ -225,22 +248,24 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace /// // panics on division by zero /// world_hello::compute::div(10, 0); /// ``` -``` +```` 通过 `should_panic`,告诉 Rust 我们这个用例会导致 `panic`,这样测试用例就能顺利通过。 #### 保留测试,隐藏文档 + 在某些时候,我们希望保留文档测试的功能,但是又要将某些测试用例的内容从文档中隐藏起来: -```rust + +````rust /// ``` /// # // 使用#开头的行会在文档中被隐藏起来,但是依然会在文档测试中运行 -/// # fn try_main() -> Result<(), String> { +/// # fn try_main() -> Result<(), String> { /// let res = world_hello::compute::try_div(10, 0)?; /// # Ok(()) // returning from try_main /// # } -/// # fn main() { -/// # try_main().unwrap(); -/// # +/// # fn main() { +/// # try_main().unwrap(); +/// # /// # } /// ``` pub fn try_div(a: i32, b: i32) -> Result { @@ -250,16 +275,18 @@ pub fn try_div(a: i32, b: i32) -> Result { Ok(a / b) } } -``` +```` 以上文档注释中,我们使用 `#` 将不想让用户看到的内容隐藏起来,但是又不影响测试用例的运行,最终用户将只能看到那行没有隐藏的 `let res = world_hello::compute::try_div(10, 0)?;`: ## 文档注释中的代码跳转 + Rust 在文档注释中还提供了一个非常强大的功能,那就是可以实现对外部项的链接: #### 跳转到标准库 + ```rust /// `add_one` 返回一个[`Option`]类型 pub fn add_one(x: i32) -> Option { @@ -268,10 +295,12 @@ pub fn add_one(x: i32) -> Option { ``` 此处的 **[`Option`]** 就是一个链接,指向了标准库中的 `Option` 枚举类型,有两种方式可以进行跳转: -- 在IDE中,使用 `Command + 鼠标左键`(mac系统下),`CTRL + 鼠标左键`(win系统下) + +- 在 IDE 中,使用 `Command + 鼠标左键`(macOS),`CTRL + 鼠标左键`(Windows) - 在文档中直接点击链接 再比如,还可以使用路径的方式跳转: + ```rust use std::sync::mpsc::Receiver; @@ -290,7 +319,9 @@ impl AsyncReceiver { ``` #### 使用完整路径跳转到指定项 + 除了跳转到标准库,你还可以通过指定具体的路径跳转到自己代码或者其它库的指定项,例如在 `lib.rs` 中添加以下代码: + ```rust mod a { /// `add_one` 返回一个[`Option`]类型 @@ -306,7 +337,9 @@ struct MySpecialFormatter; 使用 `crate::MySpecialFormatter` 这种路径就可以实现跳转到 `lib.rs` 中定义的结构体上。 #### 同名项的跳转 + 如果遇到同名项,可以使用标示类型的方式进行跳转: + ```rust /// 跳转到结构体 [`Foo`](struct@Foo) pub struct Bar; @@ -324,7 +357,9 @@ macro_rules! foo { ``` ## 文档搜索别名 + Rust 文档支持搜索功能,我们可以为自己的类型定义几个别名,以实现更好的搜索展现,当别名命中时,搜索结果会被放在第一位: + ```rust #[doc(alias = "x")] #[doc(alias = "big")] @@ -337,16 +372,17 @@ pub struct BigY; 结果如下图所示: - ## 一个综合例子 + 这个例子我们将重点应用几个知识点: - 文档注释 -- 一个项目可以包含两个包:二进制可执行包和 `lib` 包(库包),它们的包根分别是 `src/main.rs` 和 `src/lib.rs` +- 一个项目可以包含两个包:二进制可执行包和 `lib` 包(库包),它们的包根分别是 `src/main.rs` 和 `src/lib.rs` - 在二进制包中引用 `lib` 包 - 使用 `pub use` 再导出 API,并观察文档 -首先,使用 `cargo new art` 创建一个Package `art`: +首先,使用 `cargo new art` 创建一个 Package `art`: + ```console Created binary (application) `art` package ``` @@ -356,7 +392,8 @@ Created binary (application) `art` package 现在,在 `src` 目录下创建一个 `lib.rs` 文件,同样,根据之前学习的知识,创建该文件等于又创建了一个库类型的包,包名也是 `art`,包根为 `src/lib.rs`,该包是是库类型的,因此往往作为依赖库被引入。 将以下内容添加到 `src/lib.rs` 中: -```rust + +````rust //! # Art //! //! 未来的艺术建模库,现在的调色库 @@ -367,7 +404,7 @@ pub use self::utils::mix; pub mod kinds { //! 定义颜色的类型 - + /// 主色 pub enum PrimaryColor { Red, @@ -398,11 +435,12 @@ pub mod utils { SecondaryColor::Green } } -``` +```` 在库包的包根 `src/lib.rs` 下,我们又定义了几个子模块,同时将子模块中的三个项通过 `pub use` 进行了再导出。 接着,将下面内容添加到 `src/main.rs` 中: + ```rust use art::kinds::PrimaryColor; use art::utils::mix; @@ -415,18 +453,19 @@ fn main() { ``` 在二进制可执行包的包根 `src/main.rs` 下,我们引入了库包 `art` 中的模块项,同时使用 `main` 函数作为程序的入口,该二进制包可以使用 `cargo run` 运行: + ```console Green ``` -至此,库包完美提供了用于调色的 API,二进制包引入这些API完美的实现了调色并打印输出。 +至此,库包完美提供了用于调色的 API,二进制包引入这些 API 完美的实现了调色并打印输出。 最后,再来看看文档长啥样: ## 总结 + 在 Rust 中,注释分为三个主要类型:代码注释、文档注释、包和模块注释,每个注释类型都拥有两种形式:行注释和块注释,熟练掌握包模块和注释的知识,非常有助于我们创建工程性更强的项目。 如果读者看到这里对于包模块还是有些模糊,强烈建议回头看看相关的章节以及本章节的最后一个综合例子。 - diff --git a/src/basic/compound-type/array.md b/src/basic/compound-type/array.md index c78932dd..750c4928 100644 --- a/src/basic/compound-type/array.md +++ b/src/basic/compound-type/array.md @@ -5,6 +5,7 @@ 不知道你们发现没,这两个数组的关系跟 `&str` 与 `String` 的关系很像,前者是长度固定的字符串切片,后者是可动态增长的字符串。其实,在 Rust 中无论是 `String` 还是 `Vector`,它们都是 Rust 的高级类型:集合类型,在后面章节会有详细介绍。 对于本章节,我们的重点还是放在数组 `array` 上。数组的具体定义很简单:将多个类型相同的元素依次组合在一起,就是一个数组。结合上面的内容,可以得出数组的三要素: + - 长度固定 - 元素必须有相同的类型 - 依次线性排列 @@ -12,38 +13,46 @@ 这里再啰嗦一句,**我们这里说的数组是 Rust 的基本类型,是固定长度的,这点与其他编程语言不同,其它编程语言的数组往往是可变长度的,与 Rust 中的动态数组 `Vector` 类似**,希望读者大大牢记此点。 ### 创建数组 + 在 Rust 中,数组是这样定义的: + ```rust fn main() { let a = [1, 2, 3, 4, 5]; } ``` -数组语法跟 JavaScript 很像,也跟大多数编程语言很像。由于它的元素类型大小固定,且长度也是固定,因此**数组 `array` 是存储在栈上**,性能也会非常优秀。与此对应,**动态数组 `Vector` 是存储在堆上**,因此长度可以动态改变。当你不确定是使用数组还是动态数组时,那就应该使用后者,具体见[动态数组Vector](../collections/vector.md)。 +数组语法跟 JavaScript 很像,也跟大多数编程语言很像。由于它的元素类型大小固定,且长度也是固定,因此**数组 `array` 是存储在栈上**,性能也会非常优秀。与此对应,**动态数组 `Vector` 是存储在堆上**,因此长度可以动态改变。当你不确定是使用数组还是动态数组时,那就应该使用后者,具体见[动态数组 Vector](../collections/vector.md)。 举个例子,在需要知道一年中各个月份名称的程序中,你很可能希望使用的是数组而不是动态数组。因为月份是固定的,它总是只包含 12 个元素: + ```rust let months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; ``` 在一些时候,还需要为**数组声明类型**,如下所示: + ```rust let a: [i32; 5] = [1, 2, 3, 4, 5]; ``` + 这里,数组类型是通过方括号语法声明,`i32` 是元素类型,分号后面的数字 `5` 是数组长度,数组类型也从侧面说明了**数组的元素类型要统一,长度要固定**。 -还可以使用下面的语法初始化一个**某个值重复出现N次的数组**: +还可以使用下面的语法初始化一个**某个值重复出现 N 次的数组**: + ```rust let a = [3; 5]; ``` -`a` 数组包含 `5` 个元素,这些元素的初始化值为 `3`,聪明的读者已经发现,这种语法跟数组类型的声明语法其实是保持一致的:`[3;5]` 和 `[类型;长度]`。 + +`a` 数组包含 `5` 个元素,这些元素的初始化值为 `3`,聪明的读者已经发现,这种语法跟数组类型的声明语法其实是保持一致的:`[3; 5]` 和 `[类型; 长度]`。 在元素重复的场景,这种写法要简单的多,否则你就得疯狂敲击键盘:`let a = [3, 3, 3, 3, 3];`,不过老板可能很喜欢你的这种疯狂编程的状态。 ### 访问数组元素 因为数组是连续存放元素的,因此可以通过索引的方式来访问存放其中的元素: + ```rust fn main() { let a = [9, 8, 7, 6, 5]; @@ -52,10 +61,13 @@ fn main() { let second = a[1]; // 获取第二个元素 } ``` -与许多语言类似,数组的索引下标是从0开始的。此处,`first` 获取到的值是 `9`,`second` 是 `8`。 + +与许多语言类似,数组的索引下标是从 0 开始的。此处,`first` 获取到的值是 `9`,`second` 是 `8`。 #### 越界访问 + 如果使用超出数组范围的索引访问数组元素,会怎么样?下面是一个接收用户的控制台输入,然后将其作为索引访问数组元素的例子: + ```rust use std::io; @@ -85,6 +97,7 @@ fn main() { ``` 使用 `cargo run` 来运行代码,因为数组只有 5 个元素,如果我们试图输入 `5` 去访问第 6 个元素,则会访问到不存在的数组元素,最终程序会崩溃退出: + ```console Please enter an array index. 5 @@ -94,13 +107,13 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace 这就是数组访问越界,访问了数组中不存在的元素,导致 Rust 运行时错误。程序因此退出并显示错误消息,未执行最后的 `println!` 语句。 -当你尝试使用索引访问元素时,Rust 将检查你指定的索引是否小于数组长度。如果索引大于或等于数组长度,Rust会出现 ***panic***。这种检查只能在运行时进行,比如在上面这种情况下,编译器无法在编译期知道用户运行代码时将输入什么值。 +当你尝试使用索引访问元素时,Rust 将检查你指定的索引是否小于数组长度。如果索引大于或等于数组长度,Rust 会出现 **_panic_**。这种检查只能在运行时进行,比如在上面这种情况下,编译器无法在编译期知道用户运行代码时将输入什么值。 这种就是 Rust 的安全特性之一。在很多系统编程语言中,并不会检查数组越界问题,你会访问到无效的内存地址获取到一个风马牛不相及的值,最终导致在程序逻辑上出现大问题,而且这种问题会非常难以检查。 ## 数组切片 -在之前的[章节](./string-slice.md#切片(slice)),我们有讲到`切片`这个概念,它允许你引用集合中的部分连续片段,而不是整个集合,对于数组也是,数组切片允许我们引用数组的一部分: +在之前的[章节](./string-slice.md#切片slice),我们有讲到 `切片` 这个概念,它允许你引用集合中的部分连续片段,而不是整个集合,对于数组也是,数组切片允许我们引用数组的一部分: ```rust let a: [i32; 5] = [1, 2, 3, 4, 5]; @@ -109,13 +122,17 @@ let slice: &[i32] = &a[1..3]; assert_eq!(slice, &[2, 3]); ``` + 上面的数组切片 `slice` 的类型是`&[i32]`,与之对比,数组的类型是`[i32;5]`,简单总结下切片的特点: + - 切片的长度可以与数组不同,并不是固定的,而是取决于你使用时指定的起始和结束位置 - 创建切片的代价非常小,因为切片只是针对底层数组的一个引用 - 切片类型[T]拥有不固定的大小,而切片引用类型&[T]则具有固定的大小,因为 Rust 很多时候都需要固定大小数据类型,因此&[T]更有用,`&str`字符串切片也同理 ## 总结 + 最后,让我们以一个综合性使用数组的例子,来结束本章节的学习: + ```rust fn main() { // 编译器自动推导出one的类型 @@ -124,10 +141,10 @@ fn main() { let two: [u8; 3] = [1, 2, 3]; let blank1 = [0; 3]; let blank2: [u8; 3] = [0; 3]; - + // arrays是一个二维数组,其中每一个元素都是一个数组,元素类型是[u8; 3] let arrays: [[u8; 3]; 4] = [one, two, blank1, blank2]; - + // 借用arrays的元素用作循环中 for a in &arrays { print!("{:?}: ", a); @@ -136,7 +153,7 @@ fn main() { for n in a.iter() { print!("\t{} + 10 = {}", n, n+10); } - + let mut sum = 0; // 0..a.len,是一个 Rust 的语法糖,其实就等于一个数组,元素是从0,1,2一直增加到到a.len-1 for i in 0..a.len() { @@ -148,9 +165,9 @@ fn main() { ``` 做个总结,数组虽然很简单,但是其实还是存在几个要注意的点: + - **数组类型容易跟数组切片混淆**,[T;n]描述了一个数组的类型,而[T]描述了切片的类型, 因为切片是运行期的数据结构,它的长度无法在编译器得知,因此不能用[T;n]的形式去描述 - `[u8; 3]`和`[u8; 4]`是不同的类型,数组的长度也是类型的一部分 - **在实际开发中,使用最多的是数组切片[T]**,我们往往通过引用的方式去使用`&[T]`,因为后者有固定的类型大小 - 至此,关于数据类型部分,我们已经全部学完了,对于 Rust 学习而言,我们也迈出了坚定的第一步,后面将开始更高级特性的学习。未来如果大家有疑惑需要检索知识,一样可以继续回顾过往的章节,因为本书不仅仅是一门 Rust 的教程,还是一本厚重的 Rust 工具书。 diff --git a/src/basic/compound-type/enum.md b/src/basic/compound-type/enum.md index fa2c6315..85420980 100644 --- a/src/basic/compound-type/enum.md +++ b/src/basic/compound-type/enum.md @@ -1,6 +1,7 @@ # 枚举 枚举(enum 或 enumeration)允许你通过列举可能的成员来定义一个**枚举类型**,例如扑克牌花色: + ```rust enum PokerSuit { Clubs, @@ -20,13 +21,16 @@ enum PokerSuit { **枚举类型是一个类型,它会包含所有可能的枚举成员, 而枚举值是该类型中的具体某个成员的实例。** ## 枚举值 + 现在来创建 `PokerSuit` 枚举类型的两个成员实例: + ```rust let heart = PokerSuit::Hearts; let diamond = PokerSuit::Diamonds; ``` 我们通过 `::` 操作符来访问 `PokerSuit` 下的具体成员,从代码可以清晰看出,`heart` 和 `diamond` 都是 `PokerSuit` 枚举类型的,接着可以定义一个函数来使用它们: + ```rust fn main() { let heart = PokerSuit::Hearts; @@ -43,9 +47,10 @@ fn print_suit(card: PokerSuit) { `print_suit` 函数的参数类型是 `PokerSuit`,因此我们可以把 `heart` 和 `diamond` 传给它,虽然 `heart` 是基于 `PokerSuit` 下的 `Hearts` 成员实例化的,但是它是货真价实的 `PokerSuit` 枚举类型。 -接下来,我们想让扑克牌变得更加实用,那么需要给每张牌赋予一个值:`A`(1)-`K`(13),这样再加上花色,就是一张真实的扑克牌了,例如红心A。 +接下来,我们想让扑克牌变得更加实用,那么需要给每张牌赋予一个值:`A`(1)-`K`(13),这样再加上花色,就是一张真实的扑克牌了,例如红心 A。 目前来说,枚举值还不能带有值,因此先用结构体来实现: + ```rust enum PokerSuit { Clubs, @@ -70,9 +75,11 @@ fn main() { }; } ``` + 这段代码很好的完成了它的使命,通过结构体 `PokerCard` 来代表一张牌,结构体的 `suit` 字段表示牌的花色,类型是 `PokerSuit` 枚举类型,`value` 字段代表扑克牌的数值。 可以吗?可以!好吗?说实话,不咋地,因为还有简洁得多的方式来实现: + ```rust enum PokerCard { Clubs(u8), @@ -90,6 +97,7 @@ fn main() { 直接将数据信息关联到枚举成员上,省去近一半的代码,这种实现是不是更优雅? 不仅如此,同一个枚举类型下的不同成员还能持有不同的数据类型,例如让某些花色打印 `1-13` 的字样,另外的花色打印上 `A-K` 的字样: + ```rust enum PokerCard { Clubs(u8), @@ -106,8 +114,8 @@ fn main() { 回想一下,遇到这种不同类型的情况,再用我们之前的结构体实现方式,可行吗?也许可行,但是会复杂很多。 - 再来看一个来自标准库中的例子: + ```rust struct Ipv4Addr { // --snip-- @@ -122,11 +130,13 @@ enum IpAddr { V6(Ipv6Addr), } ``` -这个例子跟我们之前的扑克牌很像,只不过枚举成员包含的类型更复杂了,变成了结构体:分别通过 `Ipv4Addr` 和 `Ipv6Addr` 来定义两种不同的IP数据。 + +这个例子跟我们之前的扑克牌很像,只不过枚举成员包含的类型更复杂了,变成了结构体:分别通过 `Ipv4Addr` 和 `Ipv6Addr` 来定义两种不同的 IP 数据。 从这些例子可以看出,**任何类型的数据都可以放入枚举成员中**: 例如字符串、数值、结构体甚至另一个枚举。 增加一些挑战?先看以下代码: + ```rust enum Message { Quit, @@ -143,12 +153,14 @@ fn main() { ``` 该枚举类型代表一条消息,它包含四个不同的成员: + - `Quit` 没有任何关联数据 - `Move` 包含一个匿名结构体 - `Write` 包含一个 `String` 字符串 - `ChangeColor` 包含三个 `i32` 当然,我们也可以用结构体的方式来定义这些消息: + ```rust struct QuitMessage; // 单元结构体 struct MoveMessage { @@ -168,13 +180,14 @@ struct ChangeColorMessage(i32, i32, i32); // 元组结构体 最后,再用一个实际项目中的简化片段,来结束枚举类型的语法学习。 例如我们有一个 WEB 服务,需要接受用户的长连接,假设连接有两种:`TcpStream` 和 `TlsStream`,但是我们希望对这两个连接的处理流程相同,也就是用同一个函数来处理这两个连接,代码如下: + ```rust fn new (stream: TcpStream) { let mut s = stream; if tls { s = negotiate_tls(stream) } - + // websocket是一个WebSocket或者 // WebSocket>类型 websocket = WebSocket::from_raw_socket( @@ -183,6 +196,7 @@ fn new (stream: TcpStream) { ``` 此时,枚举类型就能帮上大忙: + ```rust enum Websocket { Tcp(Websocket), @@ -190,8 +204,9 @@ enum Websocket { } ``` -## Option枚举用于处理空值 -在其它编程语言中,往往都有一个 `null` 关键字,该关键字用于表明一个变量当前的值为空(不是零值,例如整形的零值是 0),也就是不存在值。当你对这些 `null` 进行操作时,例如调用一个方法,就会直接抛出**null异常**,导致程序的崩溃,因此我们在编程时需要格外的小心去处理这些 `null` 空值。 +## Option 枚举用于处理空值 + +在其它编程语言中,往往都有一个 `null` 关键字,该关键字用于表明一个变量当前的值为空(不是零值,例如整形的零值是 0),也就是不存在值。当你对这些 `null` 进行操作时,例如调用一个方法,就会直接抛出**null 异常**,导致程序的崩溃,因此我们在编程时需要格外的小心去处理这些 `null` 空值。 > Tony Hoare, `null` 的发明者,曾经说过一段非常有名的话 > @@ -200,6 +215,7 @@ enum Websocket { 尽管如此,空值的表达依然非常有意义,因为空值表示当前时刻变量的值是缺失的。有鉴于此,Rust 吸取了众多教训,决定抛弃 `null`,而改为使用 `Option` 枚举变量来表述这种结果。 `Option` 枚举包含两个成员,一个成员表示含有值:`Some(T)`, 另一个表示没有值:`None`,定义如下: + ```rust enum Option { Some(T), @@ -212,6 +228,7 @@ enum Option { `Option` 枚举是如此有用以至于它被包含在了 [`prelude`](../../appendix/prelude.md)(prelude 属于 Rust 标准库,Rust 会将最常用的类型、函数等提前引入其中,省得我们再手动引入)之中,你不需要将其显式引入作用域。另外,它的成员 `Some` 和 `None` 也是如此,无需使用 `Option::` 前缀就可直接使用 `Some` 和 `None`。总之,不能因为 `Some(T)` 和 `None` 中没有 `Option::` 的身影,就否认它们是 `Option` 下的卧龙凤雏。 再来看以下代码: + ```rust let some_number = Some(5); let some_string = Some("a string"); @@ -219,7 +236,6 @@ let some_string = Some("a string"); let absent_number: Option = None; ``` - 如果使用 `None` 而不是 `Some`,需要告诉 Rust `Option` 是什么类型的,因为编译器只通过 `None` 值无法推断出 `Some` 成员保存的值的类型。 当有一个 `Some` 值时,我们就知道存在一个值,而这个值保存在 `Some` 中。当有个 `None` 值时,在某种意义上,它跟空值具有相同的意义:并没有一个有效的值。那么,`Option` 为什么就比空值要好呢? @@ -253,7 +269,6 @@ not satisfied 那么当有一个 `Option` 的值时,如何从 `Some` 成员中取出 `T` 的值来使用它呢?`Option` 枚举拥有大量用于各种情况的方法:你可以查看[它的文档](https://doc.rust-lang.org/std/option/enum.Option.html)。熟悉 `Option` 的方法将对你的 Rust 之旅非常有用。 - 总的来说,为了使用 `Option` 值,需要编写处理每个成员的代码。你想要一些代码只当拥有 `Some(T)` 值时运行,允许这些代码使用其中的 `T`。也希望一些代码在值为 `None` 时运行,这些代码并没有一个可用的 `T` 值。`match` 表达式就是这么一个处理枚举的控制流结构:它会根据枚举的成员运行不同的代码,这些代码可以使用匹配到的值中的数据。 这里先简单看一下 `match` 的大致模样,在[模式匹配](../match-pattern/intro.md)中,我们会详细讲解: diff --git a/src/basic/compound-type/intro.md b/src/basic/compound-type/intro.md index db3547c8..c65724d7 100644 --- a/src/basic/compound-type/intro.md +++ b/src/basic/compound-type/intro.md @@ -5,6 +5,7 @@ 本章的重点在复合类型上,顾名思义,复合类型是由其它类型组合而成的,最典型的就是结构体 `struct` 和枚举 `enum`。例如平面上的一个点 `point(x,y)`,它由两个数值类型的值 `x` 和 `y` 组合而来。我们无法单独去维护这两个数值,因为单独一个 `x` 或者 `y` 是含义不完整的,无法标识平面上的一个点,应该把它们看作一个整体去理解和处理。 来看一段代码,它使用我们之前学过的内容来构建文件操作: + ```rust #![allow(unused_variables)] type File = String; @@ -27,13 +28,12 @@ fn main() { //read(&mut f1, &mut vec![]); close(&mut f1); } - ``` +``` 接下来我们的学习非常类似原型设计:有的方法只提供 API 接口,但是不提供具体实现。此外,有的变量在声明之后并未使用,因此在这个阶段我们需要排除一些编译器噪音(Rust 在编译的时候会扫描代码,变量声明后未使用会以 `warning` 警告的形式进行提示),引入 `#![allow(unused_variables)]` 属性标记,该标记会告诉编译器忽略未使用的变量,不要抛出 `warning` 警告,具体的常见编译器属性你可以在这里查阅:[编译器属性标记](https://course.rs/profiling/compiler/attributes.html)。 - `read` 函数也非常有趣,它返回一个 `!` 类型,这个表明该函数是一个发散函数,不会返回任何值,包括 `()`。`unimplemented!()` 告诉编译器该函数尚未实现,`unimplemented!()` 标记通常意味着我们期望快速完成主要代码,回头再通过搜索这些标记来完成次要代码,类似的标记还有 `todo!()`,当代码执行到这种未实现的地方时,程序会直接报错。你可以反注释 `read(&mut f1, &mut vec![]);` 这行,然后再观察下结果。 - - 同时,从代码设计角度来看,关于文件操作的类型和函数应该组织在一起,散落得到处都是,是难以管理和使用的。而且通过 `open(&mut f1)` 进行调用,也远没有使用 `f1.open()` 来调用好,这就体现出了只使用基本类型的局限性:**无法从更高的抽象层次去简化代码**。 +`read` 函数也非常有趣,它返回一个 `!` 类型,这个表明该函数是一个发散函数,不会返回任何值,包括 `()`。`unimplemented!()` 告诉编译器该函数尚未实现,`unimplemented!()` 标记通常意味着我们期望快速完成主要代码,回头再通过搜索这些标记来完成次要代码,类似的标记还有 `todo!()`,当代码执行到这种未实现的地方时,程序会直接报错。你可以反注释 `read(&mut f1, &mut vec![]);` 这行,然后再观察下结果。 +同时,从代码设计角度来看,关于文件操作的类型和函数应该组织在一起,散落得到处都是,是难以管理和使用的。而且通过 `open(&mut f1)` 进行调用,也远没有使用 `f1.open()` 来调用好,这就体现出了只使用基本类型的局限性:**无法从更高的抽象层次去简化代码**。 接下来,我们将引入一个高级数据结构 —— 结构体 `struct`,来看看复合类型是怎样更好的解决这类问题。 开始之前,先来看看 Rust 的重点也是难点:字符串 `String` 和 `&str`。 diff --git a/src/basic/compound-type/string-slice.md b/src/basic/compound-type/string-slice.md index 1607336d..9fd04373 100644 --- a/src/basic/compound-type/string-slice.md +++ b/src/basic/compound-type/string-slice.md @@ -3,6 +3,7 @@ 在其他语言,字符串往往是送分题,因为实在是太简单了,例如 `"hello, world"` 就是字符串章节的几乎全部内容了,但是如果你带着同样的想法来学 Rust,我保证,绝对会栽跟头, **因此这一章大家一定要重视,仔细阅读,这里有很多其它 Rust 书籍中没有的内容**。 首先来看段很简单的代码: + ```rust fn main() { let my_name = "Pascal"; @@ -38,6 +39,7 @@ Bingo,果然报错了,编译器提示 `greet` 函数需要一个 `String` 切片并不是 Rust 独有的概念,在 Go 语言中就非常流行,它允许你引用集合中部分连续的元素序列,而不是引用整个集合。 对于字符串而言,切片就是对 `String` 类型中某一部分的引用,它看起来像这样: + ```rust let s = String::from("hello world"); @@ -53,7 +55,8 @@ let world = &s[6..11]; -在使用 Rust 的 `..` [range序列](https://course.rs/base-type/numbers.html#序列range)语法时,如果你想从索引 0 开始,可以使用如下的方式,这两个是等效的: +在使用 Rust 的 `..` [range 序列](https://course.rs/base-type/numbers.html#序列range)语法时,如果你想从索引 0 开始,可以使用如下的方式,这两个是等效的: + ```rust let s = String::from("hello"); @@ -62,6 +65,7 @@ let slice = &s[..2]; ``` 同样的,如果你的切片想要包含 `String` 的最后一个字节,则可以这样使用: + ```rust let s = String::from("hello"); @@ -72,6 +76,7 @@ let slice = &s[4..]; ``` 你也可以截取完整的 `String` 切片: + ```rust let s = String::from("hello"); @@ -81,18 +86,21 @@ let slice = &s[0..len]; let slice = &s[..]; ``` ->在对字符串使用切片语法时需要格外小心,切片的索引必须落在字符之间的边界位置,也就是UTF-8字符的边界,例如中文在UTF-8中占用三个字节,下面的代码就会崩溃: ->```rust +> 在对字符串使用切片语法时需要格外小心,切片的索引必须落在字符之间的边界位置,也就是 UTF-8 字符的边界,例如中文在 UTF-8 中占用三个字节,下面的代码就会崩溃: +> +> ```rust > let s = "中国人"; > let a = &s[0..2]; > println!("{}",a); ->``` ->因为我们只取 `s` 字符串的前两个字节,但是本例中每个汉字占用三个字节,因此没有落在边界处,也就是连 `中` 字都取不完整,此时程序会直接崩溃退出,如果改成 `&s[0..3]`,则可以正常通过编译。 +> ``` +> +> 因为我们只取 `s` 字符串的前两个字节,但是本例中每个汉字占用三个字节,因此没有落在边界处,也就是连 `中` 字都取不完整,此时程序会直接崩溃退出,如果改成 `&s[0..3]`,则可以正常通过编译。 > 因此,当你需要对字符串做切片索引操作时,需要格外小心这一点, 关于该如何操作 UTF-8 字符串,参见[这里](#操作-UTF8-字符串) 字符串切片的类型标识是 `&str`,因此我们可以这样声明一个函数,输入 `String` 类型,返回它的切片: `fn first_word(s: &String) -> &str `。 有了切片就可以写出这样的安全代码: + ```rust fn main() { let mut s = String::from("hello world"); @@ -107,17 +115,19 @@ fn first_word(s: &String) -> &str { &s[..1] } ``` + 编译器报错如下: + ```console error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable --> src/main.rs:18:5 | 16 | let word = first_word(&s); | -- immutable borrow occurs here -17 | +17 | 18 | s.clear(); // error! | ^^^^^^^^^ mutable borrow occurs here -19 | +19 | 20 | println!("the first word is: {}", word); | ---- immutable borrow later used here ``` @@ -127,7 +137,9 @@ error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immuta 从上述代码可以看出,Rust 不仅让我们的 `API` 更加容易使用,而且也在编译期就消除了大量错误! #### 其它切片 + 因为切片是对集合的部分引用,因此不仅仅字符串有切片,其它集合类型也有,例如数组: + ```rust let a = [1, 2, 3, 4, 5]; @@ -135,20 +147,23 @@ let slice = &a[1..3]; assert_eq!(slice, &[2, 3]); ``` -该数组切片的类型是 `&[i32]`,数组切片和字符串切片的工作方式是一样的,例如持有一个引用指向原始数组的某个元素和长度。 +该数组切片的类型是 `&[i32]`,数组切片和字符串切片的工作方式是一样的,例如持有一个引用指向原始数组的某个元素和长度。 ## 字符串字面量是切片 之前提到过字符串字面量,但是没有提到它的类型: + ```rust let s = "Hello, world!"; ``` 实际上,`s` 的类型是 `&str`,因此你也可以这样声明: + ```rust let s: &str = "Hello, world!"; ``` + 该切片指向了程序可执行文件中的某个点,这也是为什么字符串字面量是不可变的,因为 `&str` 是一个不可变引用。 了解完切片,可以进入本节的正题了。 @@ -166,6 +181,7 @@ Rust 在语言级别,只有一种字符串类型: `str`,它通常是以引 #### 操作字符串 由于 `String` 是可变字符串,因此我们可以对它进行创建、增删操作,下面的代码汇总了相关的操作方式: + ```rust fn main() { // 创建一个空String @@ -203,6 +219,7 @@ fn main() { ``` 在上面代码中,有一处需要解释的地方,就是使用 `+` 来对字符串进行相加操作, 这里之所以使用 `s1 + &s2` 的形式,是因为 `+` 使用了 `add` 方法,该方法的定义类似: + ```rust fn add(self, s: &str) -> String { ``` @@ -210,6 +227,7 @@ fn add(self, s: &str) -> String { 因为该方法涉及到更复杂的特征功能,因此我们这里简单说明下, `self` 是 `String` 类型的字符串 `s1`,该函数说明,只能将 `&str` 类型的字符串切片添加到 `String` 类型的 `s1` 上,然后返回一个新的 `String` 类型,所以 `let s3 = s1 + &s2;` 就很好解释了,将 `String` 类型的 `s1` 与 `&str` 类型的 `s2` 进行相加,最终得到 `String` 类型的 `s3`。 由此可推,以下代码也是合法的: + ```rust let s1 = String::from("tic"); let s2 = String::from("tac"); @@ -223,13 +241,15 @@ let s = s1 + "-" + &s2 + "-" + &s3; 在上面代码中,我们做了一个有些难以理解的 `&String` 操作,下面来展开讲讲。 -## String与&str的转换 +## String 与 &str 的转换 在之前的代码中,已经见到好几种从 `&str` 类型生成 `String` 类型的操作: + - `String::from("hello,world")` - `"hello,world".to_string()` 那么如何将 `String` 类型转为 `&str` 类型呢?答案很简单,取引用即可: + ```rust fn main() { let s = String::from("hello,world!"); @@ -248,12 +268,14 @@ fn say_hello(s: &str) { ## 字符串索引 在其它语言中,使用索引的方式访问字符串的某个字符或者子串是很正常的行为,但是在 Rust 中就会报错: + ```rust let s1 = String::from("hello"); let h = s1[0]; ``` 该代码会产生如下错误: + ```console 3 | let h = s1[0]; | ^^^^^ `String` cannot be indexed by `{integer}` @@ -262,23 +284,32 @@ fn say_hello(s: &str) { ``` #### 深入字符串内部 + 字符串的底层的数据存储格式实际上是[ `u8` ],一个字节数组。对于 `let hello = String::from("Hola");` 这行代码来说, `hello` 的长度是 `4` 个字节,因为 `"hola"` 中的每个字母在 UTF-8 编码中仅占用 1 个字节,但是对于下面的代码呢? + ```rust let hello = String::from("中国人"); ``` + 如果问你该字符串多长,你可能会说 `3`,但是实际上是 `9` 个字节的长度,因为大部分常用汉字在 UTF-8 中的长度是 `3` 个字节,因此这种情况下对 `hello` 进行索引,访问 `&hello[0]` 没有任何意义,因为你取不到 `中` 这个字符,而是取到了这个字符三个字节中的第一个字节,这是一个非常奇怪而且难以理解的返回值。 #### 字符串的不同表现形式 + 现在看一下用梵文写的字符串 `“नमस्ते”`, 它底层的字节数组如下形式: + ```rust [224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164, 224, 165, 135] ``` -长度是18个字节,这也是计算机最终存储该字符串的形式。如果从字符的形式去看,则是: + +长度是 18 个字节,这也是计算机最终存储该字符串的形式。如果从字符的形式去看,则是: + ```rust ['न', 'म', 'स', '्', 'त', 'े'] ``` + 但是这种形式下,第四和六两个字母根本就不存在,没有任何意义,接着再从字母串的形式去看: + ```rust ["न", "म", "स्", "ते"] ``` @@ -288,32 +319,42 @@ let hello = String::from("中国人"); 还有一个原因导致了 Rust 不允许去索引字符串:因为索引操作,我们总是期望它的性能表现是 O(1),然而对于 `String` 类型来说,无法保证这一点,因为 Rust 可能需要从 0 开始去遍历字符串来定位合法的字符。 ## 字符串切片 + 前文提到过,字符串切片是非常危险的操作,因为切片的索引是通过字节来进行,但是字符串又是 UTF-8 编码,因此你无法保证索引的字节刚好落在字符的边界上,例如: + ```rust let hello = "中国人"; let s = &hello[0..2]; ``` + 运行上面的程序,会直接造成崩溃: + ```console thread 'main' panicked at 'byte index 2 is not a char boundary; it is inside '中' (bytes 0..3) of `中国人`', src/main.rs:4:14 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace ``` + 这里提示的很清楚,我们索引的字节落在了 `中` 字符的内部,这种返回没有任何意义。 因此在通过索引区间来访问字符串时,**需要格外的小心**,一不注意,就会导致你程序的崩溃! ## 操作 UTF8 字符串 + 前文提到了几种使用 UTF-8 字符串的方式,下面来一一说明。 #### 字符 + 如果你想要以 Unicode 字符的方式遍历字符串,最好的办法是使用 `chars` 方法,例如: + ```rust for c in "中国人".chars() { println!("{}", c); } ``` + 输出如下 + ```console 中 国 @@ -321,13 +362,17 @@ for c in "中国人".chars() { ``` #### 字节 + 这种方式是返回字符串的底层字节数组表现形式: + ```rust for b in "中国人".bytes() { println!("{}", b); } ``` + 输出如下: + ```console 228 184 @@ -341,15 +386,14 @@ for b in "中国人".bytes() { ``` #### 获取子串 -想要准确的从UTF-8字符串中获取子串是较为复杂的事情,例如想要从 `holla中国人नमस्ते` 这种变长的字符串中取出某一个子串,使用标准库你是做不到的。 + +想要准确的从 UTF-8 字符串中获取子串是较为复杂的事情,例如想要从 `holla中国人नमस्ते` 这种变长的字符串中取出某一个子串,使用标准库你是做不到的。 你需要在 `crates.io` 上搜索 `utf8` 来寻找想要的功能。 可以考虑尝试下这个库:[utf8_slice](https://crates.io/crates/utf8_slice)。 - - - ## 字符串深度剖析 + 那么问题来了,为啥 `String` 可变,而字符串字面值 `str` 却不可以? 就字符串字面值来说,我们在编译时就知道其内容,最终字面值文本被直接硬编码进可执行文件中,这使得字符串字面值快速且高效,这主要得益于字符串字面值的不可变性。不幸的是,我们不能为了获得这种性能,而把每一个在编译时大小未知的文本都放进内存中(你也做不到!),因为有的字符串是在程序运行得过程中动态生成的。 @@ -376,6 +420,6 @@ for b in "中国人".bytes() { 与其它系统编程语言的 `free` 函数相同,Rust 也提供了一个释放内存的函数: `drop`,但是不同的是,其它语言要手动调用 `free` 来释放每一个变量占用的内存,而 Rust 则在变量离开作用域时,自动调用 `drop` 函数: 上面代码中,Rust 在结尾的 `}` 处自动调用 `drop`。 -> 其实,在 C++ 中,也有这种概念: *Resource Acquisition Is Initialization (RAII)*。如果你使用过 RAII 模式的话应该对 Rust 的 `drop` 函数并不陌生 +> 其实,在 C++ 中,也有这种概念: _Resource Acquisition Is Initialization (RAII)_。如果你使用过 RAII 模式的话应该对 Rust 的 `drop` 函数并不陌生 这个模式对编写 Rust 代码的方式有着深远的影响,在后面章节我们会进行更深入的介绍。 diff --git a/src/basic/compound-type/struct.md b/src/basic/compound-type/struct.md index 029ff3c7..5a6dfc63 100644 --- a/src/basic/compound-type/struct.md +++ b/src/basic/compound-type/struct.md @@ -5,9 +5,11 @@ 结构体跟之前讲过的[元组](./tuple.md)有些相像:都是由多种类型组合而成。但是与元组不同的是,结构体可以为内部的每个字段起一个富有含义的名称。因此结构体更加灵活更加强大,你无需依赖这些字段的顺序来访问和解析它们。 ## 结构体语法 + 天下无敌的剑士往往也因为他有一柄无双之剑,既然结构体这么强大,那么我们就需要给它配套一套强大的语法,让用户能更好的驾驭。 #### 定义结构体 + 一个结构体有几部分组成: - 通过关键字 `struct` 定义 @@ -15,6 +17,7 @@ - 几个有名字的结构体 `字段` 例如以下结构体定义了某网站的用户: + ```rust struct User { active: bool, @@ -23,10 +26,13 @@ struct User { sign_in_count: u64, } ``` + 该结构体名称是 `User`,拥有 4 个字段,且每个字段都有对应的字段名及类型声明,例如 `username` 代表了用户名,是一个可变的 `String` 类型。 #### 创建结构体实例 + 为了使用上述结构体,我们需要创建 `User` 结构体的**实例**: + ```rust let user1 = User { email: String::from("someone@example.com"), @@ -37,11 +43,14 @@ struct User { ``` 有几点值得注意: + 1. 初始化实例时,**每个字段**都需要进行初始化 2. 初始化时的字段顺序**不需要**和结构体定义时的顺序一致 #### 访问结构体字段 + 通过 `.` 操作符即可访问结构体实例内部的字段值,也可以修改它们: + ```rust let mut user1 = User { email: String::from("someone@example.com"), @@ -52,10 +61,13 @@ struct User { user1.email = String::from("anotheremail@example.com"); ``` + 需要注意的是,必须要将结构体实例声明为可变的,才能修改其中的字段,Rust 不支持将某个结构体某个字段标记为可变。 #### 简化结构体创建 + 下面的函数类似一个构建函数,返回了 `User` 结构体的实例: + ```rust fn build_user(email: String, username: String) -> User { User { @@ -66,7 +78,9 @@ fn build_user(email: String, username: String) -> User { } } ``` + 它接收两个字符串参数: `email` 和 `username`,然后使用它们来创建一个 `User` 结构体,并且返回。可以注意到这两行: `email: email` 和 `username: username`,非常的扎眼,因为实在有些啰嗦,如果你从 TypeScript 过来,肯定会鄙视 Rust 一番,不过好在,它也不是无可救药: + ```rust fn build_user(email: String, username: String) -> User { User { @@ -77,11 +91,13 @@ fn build_user(email: String, username: String) -> User { } } ``` -如上所示,当函数参数和结构体字段同名时,可以直接使用缩略的方式进行初始化,跟 TypeScript 中一模一样。 +如上所示,当函数参数和结构体字段同名时,可以直接使用缩略的方式进行初始化,跟 TypeScript 中一模一样。 #### 结构体更新语法 + 在实际场景中,有一种情况很常见:根据已有的结构体实例,创建新的结构体实例,例如根据已有的 `user1` 实例来构建 `user2`: + ```rust let user2 = User { active: user1.active, @@ -92,12 +108,14 @@ fn build_user(email: String, username: String) -> User { ``` 老话重提,如果你从 TypeScript 过来,肯定觉得啰嗦爆了:竟然手动把 `user1` 的三个字段逐个赋值给 `user2`,好在 Rust 为我们提供了 `结构体更新语法`: + ```rust let user2 = User { email: String::from("another@example.com"), ..user1 }; ``` + 因为 `user2` 仅仅在 `email` 上与 `user1` 不同,因此我们只需要对 `email` 进行赋值,剩下的通过结构体更新语法 `..user1` 即可完成。 `..` 语法表明凡是我们没有显示声明的字段,全部从 `user1` 中自动获取。需要注意的是 `..user1` 必须在结构体的尾部使用。 @@ -141,30 +159,31 @@ println!("{:?}", user1); ## 结构体的内存排列 先来看以下代码: + ```rust #[derive(Debug)] struct File { name: String, data: Vec, } - + fn main() { let f1 = File { name: String::from("f1.txt"), data: Vec::new(), }; - + let f1_name = &f1.name; let f1_length = &f1.data.len(); - + println!("{:?}", f1); println!("{} is {} bytes long", f1_name, f1_length); } -``` +``` 上面定义的 `File` 结构体在内存中的排列如下图所示: - + 从图中可以清晰的看出 `File` 结构体两个字段 `name` 和 `data` 分别拥有底层两个 `[u8]` 数组的所有权(`String` 类型的底层也是 `[u8]` 数组),通过 `ptr` 指针指向底层数组的内存地址,这里你可以把 `ptr` 指针理解为 Rust 中的引用类型。 该图片也侧面印证了:**把结构体中具有所有权的字段转移出去后,将无法再访问该字段,但是可以正常访问其它的字段**。 @@ -172,6 +191,7 @@ println!("{:?}", user1); ## 元组结构体(Tuple Struct) 结构体必须要有名称,但是结构体的字段可以没有名称,这种结构体长得很像元组,因此被称为元组结构体,例如: + ```rust struct Color(i32, i32, i32); struct Point(i32, i32, i32); @@ -180,12 +200,14 @@ println!("{:?}", user1); let origin = Point(0, 0, 0); ``` -元组结构体在你希望有一个整体名称,但是又不关心里面字段的名称时将非常有用。例如上面的 `Point` 元组结构体,众所周知3D点是 `(x, y, z)` 形式的坐标点,因此我们无需再为内部的字段逐一命名为:`x`, `y`, `z`。 +元组结构体在你希望有一个整体名称,但是又不关心里面字段的名称时将非常有用。例如上面的 `Point` 元组结构体,众所周知 3D 点是 `(x, y, z)` 形式的坐标点,因此我们无需再为内部的字段逐一命名为:`x`, `y`, `z`。 ## 单元结构体(Unit-like Struct) + 还记得之前讲过的基本没啥用的[单元类型](../base-type/char-bool.md#单元类型)吧?单元结构体就跟它很像,没有任何字段和属性,但是好在,它还挺有用。 如果你定义一个类型,但是不关心该类型的内容, 只关心它的行为时,就可以使用 `单元结构体`: + ```rust struct AlwaysEqual; @@ -193,11 +215,10 @@ let subject = AlwaysEqual; // 我们不关心 AlwaysEqual 的字段数据,只关心它的行为,因此将它声明为单元结构体,然后再为它实现某个特征 impl SomeTrait for AlwaysEqual { - + } ``` - ## 结构体数据的所有权 在之前的 `User` 结构体的定义中,有一处细节:我们使用了自身拥有所有权的 `String` 类型而不是基于引用的 `&str` 字符串切片类型。这是一个有意而为之的选择:因为我们想要这个结构体拥有它所有的数据,而不是从其它地方借用数据。 @@ -253,11 +274,12 @@ help: consider introducing a named lifetime parameter | ``` - 未来在[生命周期](../../advance/lifetime/basic.md)中会讲到如何修复这个问题以便在结构体中存储引用,不过在那之前,我们会避免在结构体中使用引用类型。 ## 使用 `#[derive(Debug)]` 来打印结构体的信息 + 在前面的代码中我们使用 `#[derive(Debug)]` 对结构体进行了标记,这样才能使用 `println("{:?}", s)` 的方式对其进行打印输出,如果不加,看看会发生什么: + ```rust struct Rectangle { width: u32, @@ -275,39 +297,45 @@ fn main() { ``` 首先可以观察到,上面使用了 `{}` 而不是之前的 `{:?}`,运行后报错: + ```shell error[E0277]: `Rectangle` doesn't implement `std::fmt::Display` ``` 提示我们结构体 `Rectangle` 没有实现 `Display` 特征,这是因为如果我们使用 `{}` 来格式化输出,那对应的类型就必须实现 `Display` 特征,以前学习的基本类型,都默认实现了该特征: + ```rust fn main() { let v = 1; let b = true; println!("{}, {}", v, b); -} +} ``` 上面代码不会报错,那么结构体为什么不默认实现 `Display` 特征呢?原因在于结构体较为复杂,例如考虑以下问题:你想要逗号对字段进行分割吗?需要括号吗?加在什么地方?所有的字段都应该显示?类似的还有很多,由于这种复杂性,Rust 不希望猜测我们想要的是什么,而是把选择权交给我们自己来实现:如果要用 `{}` 的方式打印结构体,那就自己实现 `Display` 特征。 接下来继续阅读报错: + ```shell = 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 ``` 上面提示我们使用 `{:?}` 来试试,这个方式我们在本文的前面也见过,下面来试试: + ```rust println!("rect1 is {:?}", rect1); ``` -可是依然无情报错了: +可是依然无情报错了: + ```shell error[E0277]: `Rectangle` doesn't implement `Debug` ``` 好在,聪明的编译器又一次给出了提示: + ```shell = help: the trait `Debug` is not implemented for `Rectangle` = note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle` @@ -321,6 +349,7 @@ error[E0277]: `Rectangle` doesn't implement `Debug` - 使用 `derive` 派生实现 后者简单的多,但是也有限制,具体见[附录 D](https://course.rs/appendix/derive.html),这里我们就不再深入讲解,来看看该如何使用: + ```rust #[derive(Debug)] struct Rectangle { @@ -339,6 +368,7 @@ fn main() { ``` 此时运行程序,就不再有错误,输出如下: + ```shell $ cargo run rect1 is Rectangle { width: 30, height: 50 } @@ -347,6 +377,7 @@ rect1 is Rectangle { width: 30, height: 50 } 这个输出格式看上去也不赖嘛,虽然未必是最好的。这种格式是 Rust 自动为我们提供的实现,看上基本就跟结构体的定义形式一样。 当结构体较大时,我们可能希望能够有更好的输出表现,此时可以使用 `{:#?}` 来替代 `{:?}`,输出如下: + ```shell rect1 is Rectangle { width: 30, @@ -361,6 +392,7 @@ rect1 is Rectangle { > `dbg!` 输出到标准错误输出 `stderr`,而 `println!` 输出到标准输出 `stdout` 下面的例子中清晰的展示了 `dbg!` 如何在打印出信息的同时,还把表达式的值赋给了 `width`: + ```rust #[derive(Debug)] struct Rectangle { @@ -380,6 +412,7 @@ fn main() { ``` 最终的 debug 输出如下: + ```shell $ cargo run [src/main.rs:10] 30 * scale = 60 diff --git a/src/basic/compound-type/tuple.md b/src/basic/compound-type/tuple.md index 2a623f9c..c9c75373 100644 --- a/src/basic/compound-type/tuple.md +++ b/src/basic/compound-type/tuple.md @@ -3,6 +3,7 @@ 元组是由多种类型组合到一起形成的,因此它是复合类型,元组的长度是固定的,元组中元素的顺序也是固定的。 可以通过以下语法创建一个元组: + ```rust fn main() { let tup: (i32, f64, u8) = (500, 6.4, 1); @@ -30,6 +31,7 @@ fn main() { ### 用 `.` 来访问元组 模式匹配可以让我们一次性把元组中的值全部或者部分获取出来,如果只想要访问某个特定元素,那模式匹配就略显繁琐,对此,Rust 提供了 `.` 的访问方式: + ```rust fn main() { let x: (i32, f64, u8) = (500, 6.4, 1); @@ -41,9 +43,11 @@ fn main() { let one = x.2; } ``` + 和其它语言的数组、字符串一样,元组的索引从 0 开始。 ### 元组的使用示例 + 元组在函数返回值场景很常用,例如下面的代码,可以使用元组返回多个值: ```rust diff --git a/src/basic/converse.md b/src/basic/converse.md index 5f0096c4..4ebde9c5 100644 --- a/src/basic/converse.md +++ b/src/basic/converse.md @@ -3,7 +3,9 @@ Rust 是类型安全的语言,因此在 Rust 中做类型转换不是一件简单的事,这一章节我们将对 Rust 中的类型转换进行详尽讲解。 ## `as`转换 + 先来看一段代码: + ```rust fn main() { let a: i32 = 10; @@ -22,12 +24,14 @@ fn main() { 因为每个类型能表达的数据范围不同,如果把范围较大的类型转换成较小的类型,会造成错误,因此我们需要把范围较小的类型转换成较大的类型,来避免这些问题的发生。 > 使用类型转换需要小心,因为如果执行以下操作 `300_i32 as i8`,你将获得 `44` 这个值,而不是 `300`,因为 `i8` 类型能表达的的最大值为 `2^7 - 1`,使用以下代码可以查看 `i8` 的最大值: + ```rust let a = i8::MAX; println!("{}",a); ``` 下面列出了常用的转换形式: + ```rust fn main() { let a = 3.1 as i8; @@ -39,6 +43,7 @@ fn main() { ``` #### 内存地址转换为指针 + ```rust let mut values: [i32; 2] = [1, 2]; let p1: *mut i32 = values.as_mut_ptr(); @@ -52,7 +57,9 @@ assert_eq!(values[1], 3); ``` #### 强制类型转换的边角知识 + 1. 数组切片原生指针之间的转换,不会改变数组占用的内存字节数,尽管数组元素的类型发生了改变: + ```rust fn main() { let a: *const [u16] = &[1, 2, 3, 4, 5]; @@ -62,20 +69,21 @@ fn main() { ``` 2. 转换不具有传递性 -就算 `e as U1 as U2` 是合法的,也不能说明 `e as U2` 是合法的(`e` 不能直接转换成 `U2`)。 + 就算 `e as U1 as U2` 是合法的,也不能说明 `e as U2` 是合法的(`e` 不能直接转换成 `U2`)。 + +## TryInto 转换 -## TryInto转换 在一些场景中,使用 `as` 关键字会有比较大的限制。如果你想要在类型转换上拥有完全的控制而不依赖内置的转换,例如处理转换错误,那么可以使用 `TryInto` : ```rust use std::convert::TryInto; - + fn main() { let a: u8 = 10; let b: u16 = 1500; - + let b_: u8 = b.try_into().unwrap(); - + if a < b_ { println!("Ten is less than one hundred."); } @@ -87,6 +95,7 @@ fn main() { `try_into` 会尝试进行一次转换,并返回一个 `Result`,此时就可以对其进行相应的错误处理。由于我们的例子只是为了快速测试,因此使用了 `unwrap` 方法,该方法在发现错误时,会直接调用 `panic` 导致程序的崩溃退出,在实际项目中,请不要这么使用,具体见[panic](./exception-error.md#panic)部分。 最主要的是 `try_into` 转换会捕获大类型向小类型转换时导致的溢出错误: + ```rust fn main() { let b: i16 = 1500; @@ -100,11 +109,13 @@ fn main() { }; } ``` + 运行后输出如下 `"out of range integral type conversion attempted"`,在这里我们程序捕获了错误,编译器告诉我们类型范围超出的转换是不被允许的,因为我们试图把 `1500_i16` 转换为 `u8` 类型,后者明显不足以承载这么大的值。 ## 通用类型转换 虽然 `as` 和 `TryInto` 很强大,但是只能应用在数值类型上,可是 Rust 有如此多的类型,想要为这些类型实现转换,我们需要另谋出路,先来看看在一个笨办法,将一个结构体转换为另外一个结构体: + ```rust struct Foo { x: u32, @@ -125,9 +136,11 @@ fn reinterpret(foo: Foo) -> Bar { 简单粗暴,但是从另外一个角度来看,也挺啰嗦的,好在 Rust 为我们提供了更通用的方式来完成这个目的。 #### 强制类型转换 + 在某些情况下,类型是可以进行隐式强制转换的,虽然这些转换弱化了 Rust 的类型系统,但是它们的存在是为了让 Rust 在大多数场景可以工作(说白了,帮助用户省事),而不是报各种类型上的编译错误。 首先,在匹配特征时,不会做任何强制转换(除了方法)。一个类型 `T` 可以强制转换为 `U`,不代表 `impl T` 可以强制转换为 `impl U`,例如下面的代码就无法通过编译检查: + ```rust trait Trait {} @@ -142,6 +155,7 @@ fn main() { ``` 报错如下: + ```console error[E0277]: the trait bound `&mut i32: Trait` is not satisfied --> src/main.rs:9:9 @@ -157,6 +171,7 @@ error[E0277]: the trait bound `&mut i32: Trait` is not satisfied `&i32` 实现了特征 `Trait`, `&mut i32` 可以转换为 `&i32`,但是 `&mut i32` 依然无法作为 `Trait` 来使用。 #### 点操作符 + 方法调用的点操作符看起来简单,实际上非常不简单,它在调用时,会发生很多魔法般的类型转换,例如:自动引用、自动解引用,强制类型转换直到类型能匹配等。 假设有一个方法 `foo`,它有一个接收器(接收器就是 `self`、`&self`、`&mut self` 参数)。如果调用 `value.foo()`,编译器在调用 `foo` 之前,需要决定到底使用哪个 `Self` 类型来调用。现在假设 `value` 拥有类型 `T`。 @@ -170,6 +185,7 @@ error[E0277]: the trait bound `&mut i32: Trait` is not satisfied 5. 若还是不行,那...没有那了,最后编译器大喊一声:汝欺我甚,不干了! 下面我们来用一个例子来解释上面的方法查找算法: + ```rust let array: Rc> = ...; let first_entry = array[0]; @@ -186,14 +202,17 @@ let first_entry = array[0]; 过程看起来很复杂,但是也还好,挺好理解,如果你现在不能彻底理解,也不要紧,等以后对 Rust 理解更深了,同时需要深入理解类型转换时,再来细细品读本章。 再来看看以下更复杂的例子: + ```rust fn do_stuff(value: &T) { let cloned = value.clone(); } ``` + 上面例子中 `cloned` 的类型是什么?首先编译器检查能不能进行**值方法调用**, `value` 的类型是 `&T`,同时 `clone` 方法的签名也是 `&T` : `fn clone(&T) -> T`,因此可以进行值方法调用,再加上编译器知道了 `T` 实现了 `Clone`,因此 `cloned` 的类型是 `T`。 如果 `T: Clone` 的特征约束被移除呢? + ```rust fn do_stuff(value: &T) { let cloned = value.clone(); @@ -207,6 +226,7 @@ fn do_stuff(value: &T) { 最终,我们复制出一份引用指针,这很合理,因为值类型 `T` 没有实现 `Clone`,只能去复制一个指针了。 下面的例子也是自动引用生效的地方: + ```rust #[derive(Clone)] struct Container(Arc); @@ -224,6 +244,7 @@ fn clone_containers(foo: &Container, bar: &Container) { 上面代码中,`Container` 实现了 `Clone` 特征,因此编译器可以直接进行值方法调用,此时相当于直接调用 `foo.clone`,其中 `clone` 的函数签名是 `fn clone(&T) -> T`,由此可以看出 `foo_cloned` 的类型是 `Container`。 然而,`bar_cloned` 的类型却是 `&Container`,这个不合理啊,明明我们为 `Container` 派生了 `Clone` 特征,因此它也应该是 `Container` 类型才对。万事皆有因,我们先来看下 `derive` 宏最终生成的代码大概是啥样的: + ```rust impl Clone for Container where T: Clone { fn clone(&self) -> Self { @@ -237,6 +258,7 @@ impl Clone for Container where T: Clone { 编译器接着会去尝试引用方法调用,此时 `&Container` 引用实现了 `Clone`,最终可以得出 `bar_cloned` 的类型是 `&Container`。 当然,也可以为 `Container` 手动实现 `Clone` 特征: + ```rust impl Clone for Container { fn clone(&self) -> Self { @@ -260,16 +282,14 @@ impl Clone for Container { 1. 首先也是最重要的,转换后创建一个任意类型的实例会造成无法想象的混乱,而且根本无法预测。不要把 `3` 转换成 `bool` 类型,就算你根本不会去使用该 `bool` 类型,也不要去这样转换 2. 变形后会有一个重载的返回类型,即使你没有指定返回类型,为了满足类型推导的需求,依然会产生千奇百怪的类型 3. 将 `&` 变形为 `&mut` 是未定义的行为 - - 这种转换永远都是未定义的 - - 不,你不能这么做 - - 不要多想,你没有那种幸运 + - 这种转换永远都是未定义的 + - 不,你不能这么做 + - 不要多想,你没有那种幸运 4. 变形为一个未指定生命周期的引用会导致[无界生命周期](../advance/lifetime/advance.md) 5. 在复合类型之间互相变换时,你需要保证它们的排列布局是一模一样的!一旦不一样,那么字段就会得到不可预期的值,这也是未定义的行为,至于你会不会因此愤怒, **WHO CARES** ,你都用了变形了,老兄! -对于第5条,你该如何知道内存的排列布局是一样的呢?对于 `repr(C)` 类型和 `repr(transparent)` 类型来说,它们的布局是有着精确定义的。但是对于你自己的"普通却自信"的 Rust 类型 `repr(Rust)` 来说,它可不是有着精确定义的。甚至同一个泛型类型的不同实例都可以有不同的内存布局。 `Vec` 和 `Vec` 它们的字段可能有着相同的顺序,也可能没有。对于数据排列布局来说,**什么能保证,什么不能保证**目前还在 Rust 开发组的[工作任务](https://rust-lang.github.io/unsafe-code-guidelines/layout.html)中呢。 +对于第 5 条,你该如何知道内存的排列布局是一样的呢?对于 `repr(C)` 类型和 `repr(transparent)` 类型来说,它们的布局是有着精确定义的。但是对于你自己的"普通却自信"的 Rust 类型 `repr(Rust)` 来说,它可不是有着精确定义的。甚至同一个泛型类型的不同实例都可以有不同的内存布局。 `Vec` 和 `Vec` 它们的字段可能有着相同的顺序,也可能没有。对于数据排列布局来说,**什么能保证,什么不能保证**目前还在 Rust 开发组的[工作任务](https://rust-lang.github.io/unsafe-code-guidelines/layout.html)中呢。 你以为你之前凝视的是深渊吗?不,你凝视的只是深渊的大门。 `mem::transmute_copy` 才是真正的深渊,它比之前的还要更加危险和不安全。它从 `T` 类型中拷贝出 `U` 类型所需的字节数,然后转换成 `U`。 `mem::transmute` 尚有大小检查,能保证两个数据的内存大小一致,现在这哥们干脆连这个也丢了,只不过 `U` 的尺寸若是比 `T` 大,会是一个未定义行为。 当然,你也可以通过原生指针转换和 `unions` (todo!)获得所有的这些功能,但是你将无法获得任何编译提示或者检查。原生指针转换和 `unions` 也不是魔法,无法逃避上面说的规则。 - - diff --git a/src/basic/crate-module/crate.md b/src/basic/crate-module/crate.md index 09b8bfc8..3f3815ce 100644 --- a/src/basic/crate-module/crate.md +++ b/src/basic/crate-module/crate.md @@ -1,4 +1,5 @@ # 包和 Package + 当读者按照章节顺序读到本章时,意味着你已经几乎具备了参与真实项目开发的能力。但是真实项目远比我们之前的 `cargo new` 的默认目录结构要复杂,好在,Rust 为我们提供了强大的包管理工具: - **项目(Package)**:可以用来构建、测试和分享包 @@ -7,23 +8,27 @@ - **模块(Module)**:可以一个文件多个模块,也可以一个文件一个模块,模块可以被认为是真实项目中的代码组织单元 ## 定义 + 其实项目 `Package` 和包 `Crate` 很容易被搞混,甚至在很多书中,这两者都是不分的,但是由于官方对此做了明确的区分,因此我们会在本章节中试图(挣扎着)理清这个概念。 #### 包 Crate + 对于 Rust 而言,包是一个独立的可编译单元,它编译后会生成一个可执行文件或者一个库。 一个包会将相关联的功能打包在一起,使得该功能可以很方便的在多个项目中分享。例如标准库中没有提供但是在三方库中提供的 `rand` 包,它提供了随机数生成的功能,我们只需要将该包通过 `use rand;` 引入到当前项目的作用域中,就可以在项目中使用 `rand` 的功能:`rand::XXX`。 同一个包中不能有同名的类型,但是在不同包中就可以。例如,虽然 `rand` 包中,有一个 `Rng` 特征,可是我们依然可以在自己的项目中定义一个 `Rng`,前者通过 `rand::Rng` 访问,后者通过 `Rng` 访问,对于编译器而言,这两者的边界非常清晰,不会存在引用歧义。 - ## 项目 Package + 鉴于 Rust 团队标新立异的起名传统,以及包的名称被 `crate` 占用,库的名称被 `library` 占用,经过斟酌, 我们决定将 `Package` 翻译成项目,你也可以理解为工程、软件包。 由于 `Package` 就是一个项目,因此它包含有独立的 `Cargo.toml` 文件,以及因为功能性被组织在一起的一个或多个包。一个 `Package` 只能包含**一个**库(library)类型的包,但是可以包含**多个**二进制可执行类型的包。 #### 二进制 Package + 让我们来创建一个二进制 `Package`: + ```console $ cargo new my-project Created binary (application) `my-project` package @@ -39,7 +44,9 @@ main.rs 使用 `cargo run` 可以运行该项目,输出:`Hello, world!`。 #### 库 Package + 再来创建一个库类型的 `Package`: + ```console $ cargo new my-lib --lib Created library `my-lib` package @@ -51,24 +58,28 @@ lib.rs ``` 首先,如果你试图运行 `my-lib`,会报错: + ```console $ cargo run error: a bin target must be available for `cargo run` ``` + 原因是库类型的 `Package` 只能作为三方库被其它项目引用,而不能独立运行,只有之前的二进制 `Package` 才可以运行。 与 `src/main.rs` 一样,Cargo 知道,如果一个 `Package` 包含有 `src/lib.rs`,意味它包含有一个库类型的同名包 `my-lib`,该包的根文件是 `src/lib.rs`。 #### 易混淆的 Package 和包 + 看完上面,相信大家看出来为何 `Package` 和包容易被混淆了吧?因为你用 `cargo new` 创建的 `Package` 和它其中包含的包是同名的! 不过,只要你牢记 `Package` 是一个项目工程,而包只是一个编译单元,基本上也就不会混淆这个两个概念了:`src/main.rs` 和 `src/lib.rs` 都是编译单元,因此它们都是包。 - #### 典型的 `Package` 结构 + 上面创建的 `Package` 中仅包含 `src/main.rs` 文件,意味着它仅包含一个二进制同名包 `my-project`。如果一个 `Package` 同时拥有 `src/main.rs` 和 `src/lib.rs`,那就意味着它包含两个包:库包和二进制包,这两个包名也都是 `my-project` —— 都与 `Package` 同名。 一个真实项目中典型的 `Package`,会包含多个二进制包,这些包文件被放在 `src/bin` 目录下,每一个文件都是独立的二进制包,同时也会包含一个库包,该包只能存在一个 `src/lib.rs`: + ```css . ├── Cargo.toml @@ -94,7 +105,6 @@ error: a bin target must be available for `cargo run` - 基准性能测试 `benchmark` 文件:`benches` 目录下 - 项目示例:`examples` 目录下 - 这种目录结构基本上是 Rust 的标准目录结构,在 `github` 的大多数项目上,你都将看到它的身影。 理解了包的概念,我们再来看看构成包的基本单元:模块。 diff --git a/src/basic/crate-module/intro.md b/src/basic/crate-module/intro.md index 89277a57..3b96f5a2 100644 --- a/src/basic/crate-module/intro.md +++ b/src/basic/crate-module/intro.md @@ -1,6 +1,7 @@ # 包和模块 当工程规模变大时,把代码写到一个甚至几个文件中,都是不太聪明的做法,可能存在以下问题: + 1. 单个文件过大,导致打开、翻页速度大幅变慢 2. 查询和定位效率大幅降低,类比下,你会把所有知识内容放在一个几十万字的文档中吗? 3. 只有一个代码层次:函数,难以维护和协作,想象一下你的操作系统只有一个根目录,剩下的都是单层子目录会如何:`disaster` diff --git a/src/basic/crate-module/module.md b/src/basic/crate-module/module.md index 22cbf9b9..c39e5c7d 100644 --- a/src/basic/crate-module/module.md +++ b/src/basic/crate-module/module.md @@ -1,10 +1,13 @@ # 模块 Module + 在本章节,我们将深入讲讲 Rust 的代码构成单元:模块。使用模块可以将包中的代码按照功能性进行重组,最终实现更好的可读性及易用性。同时,我们还能非常灵活地去控制代码的可见性,进一步强化 Rust 的安全性。 ## 创建嵌套模块 + 小旅馆,sorry,是小餐馆,相信大家都挺熟悉的,学校外的估计也没少去,那么咱就用小餐馆为例,来看看 Rust 的模块该如何使用。 使用 `cargo new --lib restaurant` 创建一个小餐馆,注意,这里创建的是一个库类型的 `Package`,然后将以下代码放入 `src/lib.rs` 中: + ```rust // 餐厅前厅,用于吃饭 mod front_of_house { @@ -34,7 +37,9 @@ mod front_of_house { 类似上述代码中所做的,使用模块,我们就能将功能相关的代码组织到一起,然后通过一个模块名称来说明这些代码为何被组织在一起。这样其它程序员在使用你的模块时,就可以更快地理解和上手。 ## 模块树 + 在[上一节](./crate.md)中,我们提到过 `src/main.rs` 和 `src/lib.rs` 被称为包根(crate root),这个奇葩名称的来源(我不想承认是自己翻译水平太烂-,-)是由于这两个文件的内容形成了一个模块 `crate`,该模块位于包的树形结构(由模块组成的树形结构)的根部: + ```console crate └── front_of_house @@ -50,11 +55,13 @@ crate 这颗树展示了模块之间**彼此的嵌套**关系,因此被称为**模块树**。其中 `crate` 包根是 `src/lib.rs` 文件,包根文件中的三个模块分别形成了模块树的剩余部分。 #### 父子模块 + 如果模块 `A` 包含模块 `B`,那么 `A` 是 `B` 的父模块,`B` 是 `A` 的子模块。在上例中,`front_of_house` 是 `hosting` 和 `serving` 的父模块,反之,后两者是前者的子模块。 聪明的读者,应该能联想到,模块树跟计算机上文件系统目录树的相似之处。不仅仅是组织结构上的相似,就连使用方式都很相似:每个文件都有自己的路径,用户可以通过这些路径使用它们,在 Rust 中,我们也通过路径的方式来引用模块。 ## 用路径引用模块 + 想要调用一个函数,就需要知道它的路径,在 Rust 中,这种路径有两种形式: - **绝对路径**,从包根开始,路径名以包名或者 `crate` 作为开头 @@ -82,12 +89,15 @@ pub fn eat_at_restaurant() { 上面的代码为了简化实现,省去了其余模块和函数,这样可以把关注点放在函数调用上。`eat_at_restaurant` 是一个定义在包根中的函数,在该函数中使用了两种方式对 `add_to_waitlist` 进行调用。 #### 绝对路径引用 + 因为 `eat_at_restaurant` 和 `add_to_waitlist` 都定义在一个包中,因此在绝对路径引用时,可以直接以 `crate` 开头,然后逐层引用,每一层之间使用 `::` 分隔: + ```rust crate::front_of_house::hosting::add_to_waitlist(); ``` 对比下之前的模块树: + ```console crate └── eat_at_restaurant @@ -104,16 +114,21 @@ crate 可以看出,绝对路径的调用,完全符合了模块树的层级递进,非常符合直觉,如果类比文件系统,就跟使用绝对路径调用可执行程序差不多:`/front_of_house/hosting/add_to_waitlist`,使用 `crate` 作为开始就和使用 `/` 作为开始一样。 #### 相对路径引用 + 再回到模块树中,因为 `eat_at_restaurant` 和 `front_of_house` 都处于包根 `crate` 中,因此相对路径可以使用 `front_of_house` 作为开头: + ```rust front_of_house::hosting::add_to_waitlist(); ``` + 如果类比文件系统,那么它类似于调用同一个目录下的程序,你可以这么做:`front_of_house/hosting/add_to_waitlist`,嗯也很符合直觉。 #### 绝对还是相对? + 如果只是为了引用到指定模块中的对象,那么两种都可以,但是在实际使用时,需要遵循一个原则:**当代码被挪动位置时,尽量减少引用路径的修改**,相信大家都遇到过,修改了某处代码,导致所有路径都要挨个替换,这显然不是好的路径选择。 回到之前的例子,如果我们把 `front_of_house` 模块和 `eat_at_restaurant` 移动到一个模块中 `customer_experience`,那么绝对路径的引用方式就必须进行修改:`crate::customer_experience::front_of_house ...`,但是假设我们使用的相对路径,那么该路径就无需修改,因为它们两的相对位置其实没有变: + ```console crate └── customer_experience @@ -127,6 +142,7 @@ crate 从新的模块树中可以很清晰的看出这一点。 再比如,其它的都不动,把 `eat_at_restaurant` 移动到模块 `dining` 中,如果使用相对路径,你需要修改该路径,但如果使用的是绝对路径,就无需修改: + ```console crate └── dining @@ -139,7 +155,9 @@ crate 不过,如果不确定哪个好,你可以考虑优先使用绝对路径,因为调用的地方和定义的地方往往是分离的,而定义的地方较少会变动。 ## 代码可见性 + 让我们运行下面(之前)的代码: + ```rust mod front_of_house { mod hosting { @@ -157,6 +175,7 @@ pub fn eat_at_restaurant() { ``` 意料之外的报错了,毕竟看上去确实很简单且没有任何问题: + ```console error[E0603]: module `hosting` is private --> src/lib.rs:9:28 @@ -172,9 +191,11 @@ error[E0603]: module `hosting` is private Rust 出于安全的考虑,默认情况下,所有的类型都是私有化的,包括函数、方法、结构体、枚举、常量,是的,就连模块本身也是私有化的。在中国,父亲往往不希望孩子拥有小秘密,但是在 Rust 中,**父模块完全无法访问子模块中的私有项,但是子模块却可以访问父模块、父父..模块的私有项**。 #### pub 关键字 + 类似其它语言的 `public` 或者 Go 语言中的首字母大写,Rust 提供了 `pub` 关键字,通过它你可以控制模块和模块中指定项的可见性。 由于之前的解释,我们知道了只需要将 `hosting` 模块标记为对外可见即可: + ```rust mod front_of_house { pub mod hosting { @@ -186,6 +207,7 @@ mod front_of_house { ``` 但是不幸的是,又报错了: + ```console error[E0603]: function `add_to_waitlist` is private --> src/lib.rs:12:30 @@ -194,11 +216,12 @@ error[E0603]: function `add_to_waitlist` is private | ^^^^^^^^^^^^^^^ private function ``` -哦?难道模块可见还不够,还需要将函数 `add_to_waitlist` 标记为可见的吗? 是的,没错,模块可见性不代表模块内部项的可见性,模块的可见性仅仅是允许其它模块去引用它,但是想要引用它内部的项,还得继续将对应的项标记为 `pub`。 +哦?难道模块可见还不够,还需要将函数 `add_to_waitlist` 标记为可见的吗? 是的,没错,模块可见性不代表模块内部项的可见性,模块的可见性仅仅是允许其它模块去引用它,但是想要引用它内部的项,还得继续将对应的项标记为 `pub`。 在实际项目中,一个模块需要对外暴露的数据和 API 往往就寥寥数个,如果将模块标记为可见代表着内部项也全部对外可见,那你是不是还得把那些不可见的,一个一个标记为 `private`?反而是更麻烦的多。 既然知道了如何解决,那么我们为函数也标记上 `pub`: + ```rust mod front_of_house { pub mod hosting { @@ -212,6 +235,7 @@ mod front_of_house { Bang,顺利通过编译,感觉自己又变强了。 ## 使用 `super` 引用模块 + 在[用路径引用模块](#用路径引用模块)中,我们提到了相对路径有三种方式开始:`self`、`super`和 `crate` 或者模块名,其中第三种在前面已经讲到过,现在来看看通过 `super` 的方式引用模块项。 `super` 代表的是父模块为开始的引用方式,非常类似于文件系统中的 `..` 语法:`../a/b` @@ -236,7 +260,9 @@ mod back_of_house { 那么你可能会问,为何不使用 `crate::serve_order` 的方式?额,其实也可以,不过如果你确定未来这种层级关系不会改变,那么 `super::serve_order` 的方式会更稳定,未来就算它们都不在包根了,依然无需修改引用路径。所以路径的选用,往往还是取决于场景,以及未来代码的可能走向。 ## 使用 `self` 引用模块 + `self` 其实就是引用自身模块中的项,也就是说和我们之前章节的代码类似,都调用同一模块中的内容,区别在于之间章节中直接通过名称调用即可,而 `self`,你得多此一举: + ```rust fn serve_order() { self::back_of_house::cook_order() @@ -254,8 +280,8 @@ mod back_of_house { 是的,多此一举,因为完全可以直接调用 `back_of_house`,但是 `self` 还有一个大用处,在下一节中我们会讲。 - ## 结构体和枚举的可见性 + 为何要把结构体和枚举的可见性单独拎出来讲呢?因为这两个家伙的成员字段拥有完全不同的可见性: - 将结构体设置为 `pub`,但它的所有字段依然是私有的 @@ -266,9 +292,11 @@ mod back_of_house { 而结构体的应用场景比较复杂,其中的字段也往往部分在 A 处被使用,部分在 B 处被使用,因此无法确定成员的可见性,那索性就设置为全部不可见,将选择权交给程序员。 ## 模块与文件分离 + 在之前的例子中,我们所有的模块都定义在 `src/lib.rs` 中,但是当模块变多或者变大时,需要将模块放入一个单独的文件中,让代码更好维护。 现在,把 `front_of_house` 前厅分离出来,放入一个单独的文件中 `src/front_of_house.rs`: + ```rust pub mod hosting { pub fn add_to_waitlist() {} @@ -276,6 +304,7 @@ pub mod hosting { ``` 然后,将以下代码留在 `src/lib.rs` 中: + ```rust mod front_of_house; @@ -289,10 +318,10 @@ pub fn eat_at_restaurant() { ``` so easy!其实跟之前在同一个文件中也没有太大的不同,但是有几点值得注意: + - `mod front_of_house;` 告诉 Rust 从另一个和模块 `front_of_house` 同名的文件中加载该模块的内容 - 使用绝对路径的方式来引用 `hosting` 模块:`crate::front_of_house::hosting;` 需要注意的是,和之前代码中 `mod front_of_house{..}` 的完整模块不同,现在的代码中,模块的声明和实现是分离的,实现是在单独的 `front_of_house.rs` 文件中,然后通过 `mod front_of_house;` 这条声明语句从该文件中把模块内容加载进来。因此我们可以认为,模块 `front_of_house` 的定义还是在 `src/lib.rs` 中,只不过模块的具体内容被移动到了 `src/front_of_house.rs` 文件中。 在这里出现了一个新的关键字 `use`,联想到其它章节我们见过的标准库引入 `use std::fmt;`,可以大致猜测,该关键字用来将外部模块中的项引入到当前作用域中来,这样无需冗长的父模块前缀即可调用:`hosting::add_to_waitlist();`,在下节中,我们将对 `use` 进行详细的讲解。 - diff --git a/src/basic/crate-module/use.md b/src/basic/crate-module/use.md index 728bee35..8430fdfd 100644 --- a/src/basic/crate-module/use.md +++ b/src/basic/crate-module/use.md @@ -1,12 +1,15 @@ # 使用 use 及受限可见性 + 如果代码中,通篇都是 `crate::front_of_house::hosting::add_to_waitlist` 这样的函数调用形式,我不知道有谁会喜欢,也许靠代码行数赚工资的人会很喜欢,但是强迫症肯定受不了,悲伤的是程序员大多都有强迫症。。。 因此我们需要一个办法来简化这种使用方式,在 Rust 中,可以使用 `use` 关键字把路径提前引入到当前作用域中,随后的调用就可以省略该路径,极大地简化了代码。 ## 基本引入方式 + 在 Rust 中,引入模块中的项有两种方式:[绝对路径和相对路径](./module.md#用路径引用模块),这两者在前面章节都有讲过,就不再赘述,先来看看使用绝对路径的引入方式。 #### 绝对路径引入模块 + ```rust mod front_of_house { pub mod hosting { @@ -26,7 +29,9 @@ pub fn eat_at_restaurant() { 这里,我们使用 `use` 和绝对路径的方式,将 `hosting` 模块引入到当前作用域中,然后只需通过 `hosting::add_to_waitlist` 的方式,即可调用目标模块中的函数,相比 `crate::front_of_house::hosting::add_to_waitlist()` 的方式要简单的多,那么还能更简单吗? #### 相对路径引入模块中的函数 + 在下面代码中,我们不仅要使用相对路径进行引入,而且与上面引入 `hosting` 模块不同,直接引入该模块中的 `add_to_waitlist` 函数: + ```rust mod front_of_house { pub mod hosting { @@ -46,6 +51,7 @@ pub fn eat_at_restaurant() { 很明显,三兄弟又变得更短了,不过,怎么觉得这句话怪怪的。。 #### 引入模块还是函数 + 从使用简洁性来说,引入函数自然是更甚一筹,但是在某些时候,引入模块会更好: - 需要引入同一个模块的多个函数 @@ -54,6 +60,7 @@ pub fn eat_at_restaurant() { 在以上两种情况中,使用 `use front_of_house::hosting` 引入模块要比 `use front_of_house::hosting::add_to_waitlist;` 引入函数更好。 例如,如果想使用 `HashMap`,那么直接引入该结构体是比引入模块更好的选择,因为在 `collections` 模块中,我们只需要使用一个 `HashMap` 结构体: + ```rust use std::collections::HashMap; @@ -66,9 +73,11 @@ fn main() { 其实严格来说,对于引用方式并没有需要遵守的惯例,主要还是取决于你的喜好,不过我们建议:**优先使用最细粒度(引入函数、结构体等)的引用方式,如果引起了某种麻烦(例如前面两种情况),再使用引入模块的方式**。 ## 避免同名引用 + 根据上一章节的内容,我们只要保证同一个模块中不存在同名项就行,模块之间、包之间的同名,谁管得着谁啊,话虽如此,一起看看,如果遇到同名的情况该如何处理。 #### 模块::函数 + ```rust use std::fmt; use std::io; @@ -87,7 +96,9 @@ fn function2() -> io::Result<()> { 可以看出,避免同名冲突的关键,就是使用**父模块的方式来调用**,除此之外,还可以给予引入的项起一个别名。 #### `as` 别名引用 + 对于同名冲突问题,还可以使用 `as` 关键字来解决,它可以赋予引入项一个全新的名称: + ```rust use std::fmt::Result; use std::io::Result as IoResult; @@ -107,7 +118,9 @@ fn function2() -> IoResult<()> { - `IoResult` 代表 `std:io::Result` ## 引入项再导出 + 当外部的模块项 `A` 被引入到当前模块中时,它的可见性自动被设置为私有的,如果你希望允许其它外部代码引用我们的模块项 `A`,那么可以对它进行再导出: + ```rust mod front_of_house { pub mod hosting { @@ -129,12 +142,14 @@ pub fn eat_at_restaurant() { 当你希望将内部的实现细节隐藏起来或者按照某个目的组织代码时,可以使用 `pub use` 再导出,例如统一使用一个模块来提供对外的 API,那该模块就可以引入其它模块中的 API,然后进行再导出,最终对于用户来说,所有的 API 都是由一个模块统一提供的。 ## 使用第三方包 + 之前我们一直在引入标准库模块或者自定义模块,现在来引入下第三方包中的模块,关于如何引入外部依赖,我们在 [Cargo 入门](../../first-try/cargo.md#package配置段落)中就有讲,这里直接给出操作步骤: 1. 修改 `Cargo.toml` 文件,在 `[dependencies]` 区域添加一行:`rand = "0.8.3"` 2. 此时,如果你用的是 `VSCode` 和 `rust-analyzer` 插件,该插件会自动拉取该库,你可能需要等它完成后,再进行下一步(VSCode 左下角有提示) 好了,此时,`rand` 包已经被我们添加到依赖中,下一步就是在代码中使用: + ```rust use rand::Rng; @@ -146,12 +161,15 @@ fn main() { 这里使用 `use` 引入了第三方包 `rand` 中的 `Rng` 特征,因为我们需要调用的 `gen_range` 方法定义在该特征中。 #### crates.io,lib.rs + Rust 社区已经为我们贡献了大量高质量的第三方包,你可以在 `crates.io` 或者 `lib.rs` 中检索和使用,从目前来说查找包更推荐 `lib.rs`,搜索功能更强大,内容展示也更加合理,但是下载依赖包还是得用`crates.io`。 你可以在网站上搜索 `rand` 包,看看它的文档使用方式是否和我们之前引入方式相一致:在网上找到想要的包,然后将你想要的包和版本信息写入到 `Cargo.toml` 中。 ## 使用 `{}` 简化引入方式 + 对于以下一行一行的引入方式: + ```rust use std::collections::HashMap; use std::collections::BTreeMap; @@ -162,18 +180,21 @@ use std::io; ``` 可以使用 `{}` 来一起引入进来,在大型项目中,使用这种方式来引入,可以减少大量 `use` 的使用: + ```rust use std::collections::{HashMap,BTreeMap,HashSet}; use std::{cmp::Ordering, io}; ``` 对于下面的同时引入模块和模块中的项: + ```rust use std::io; use std::io::Write; ``` 可以使用 `{}` 的方式进行简化: + ```rust use std::io::{self, Write}; ``` @@ -186,7 +207,9 @@ use std::io::{self, Write}; - `use xxx::{self, yyy}`,表示,加载当前路径下模块 `xxx` 本身,以及模块 `xxx` 下的 `yyy` ## 使用 `*` 引入模块下的所有项 + 对于之前一行一行引入 `std::collections` 的方式,我们还可以使用 + ```rust use std::collections::*; ``` @@ -194,6 +217,7 @@ use std::collections::*; 以上这种方式来引入 `std::collections` 模块下的所有公共项,这些公共项自然包含了 `HashMap`,`HashSet` 等想手动引入的集合类型。 当使用 `*` 来引入的时候要格外小心,因为你很难知道到底哪些被引入到了当前作用域中,有哪些会和你自己程序中的名称相冲突: + ```rust use std::collections::*; @@ -209,6 +233,7 @@ fn main() { 在实际项目中,这种引用方式往往用于快速写测试代码,它可以把所有东西一次性引入到 `tests` 模块中。 ## 受限的可见性 + 在上一节中,我们学习了[可见性](./module.md#代码可见性)这个概念,这也是模块体系中最为核心的概念,控制了模块中哪些内容可以被外部看见,但是在实际使用时,光被外面看到还不行,我们还想控制哪些人能看,这就是 Rust 提供的受限可见性。 例如,在 Rust 中,包是一个模块树,我们可以通过 `pub(crate) item;` 这种方式来实现:`item` 虽然是对外可见的,但是只在当前包内可见,外部包无法引用到该 `item`。 @@ -239,6 +264,7 @@ fn d() { ``` 以上代码充分说明了之前两种办法的使用方式,但是有时我们会遇到这两种方法都不太好用的时候。例如希望对于某些特定的模块可见,但是对于其他模块又不可见: + ```rust // 目标:`a` 导出 `I`、`bar` and `foo`,其他的不导出 pub mod a { @@ -267,6 +293,7 @@ pub mod a { 这段代码会报错,因为与父模块中的项对子模块可见相反,子模块中的项对父模块是不可见的。这里 `semisecret` 方法中,`a` -> `b` -> `c` 形成了父子模块链,那 `c` 中的 `J` 自然对 `a` 模块不可见。 如果使用之前的可见性方式,那么想保持 `J` 私有,同时让 `a` 继续使用 `semisecret` 函数的办法是将该函数移动到 `c` 模块中,然后用 `pub use` 将 `semisecret` 函数进行再导出: + ```rust pub mod a { pub const I: i32 = 3; @@ -321,6 +348,7 @@ pub mod a { 通过 `pub(in crate::a)` 的方式,我们指定了模块 `c` 和常量 `J` 的可见范围都只是 `a` 模块中,`a` 之外的模块是完全访问不到它们的。 #### 限制可见性语法 + `pub(crate)` 或 `pub(in crate::a)` 就是限制可见性语法,前者是限制在整个包内可见,后者是通过绝对路径,限制在包内的某个模块内可见,总结一下: - `pub` 意味着可见性无任何限制 @@ -329,8 +357,8 @@ pub mod a { - `pub(super)` 在父模块可见 - `pub(in )` 表示在某个路径代表的模块中可见,其中 `path` 必须是父模块或者祖先模块 - #### 一个综合例子 + ```rust // 一个名为 `my_mod` 的模块 mod my_mod { @@ -437,4 +465,3 @@ fn main() { // 试一试 ^ 取消此行的注释 } ``` - diff --git a/src/basic/flow-control.md b/src/basic/flow-control.md index 8deee601..08fd0db0 100644 --- a/src/basic/flow-control.md +++ b/src/basic/flow-control.md @@ -1,13 +1,15 @@ # 流程控制 -80后应该都对学校的小混混记忆犹新,在那个时代,小混混们往往都认为自己是地下王者,管控着地下事务的流程,在我看来,他们就像代码中的流程控制一样,无处不在,很显眼,但是又让人懒得重视。 +80 后应该都对学校的小混混记忆犹新,在那个时代,小混混们往往都认为自己是地下王者,管控着地下事务的流程,在我看来,他们就像代码中的流程控制一样,无处不在,很显眼,但是又让人懒得重视。 -言归正传,Rust程序是从上而下顺序执行的,在此过程中,我们可以通过循环、分支等流程控制方式,更好的实现相应的功能。 +言归正传,Rust 程序是从上而下顺序执行的,在此过程中,我们可以通过循环、分支等流程控制方式,更好的实现相应的功能。 -## 使用if来做分支控制 -> if else无处不在 - `鲁迅说` +## 使用 if 来做分支控制 + +> if else 无处不在 - 鲁迅 但凡你能找到一门编程语言没有 `if else`,那么一定更要反馈给鲁迅,反正不是我说的:) 总之,只要你拥有其它语言的编程经验,就一定会有以下认知:`if else` **表达式**根据条件执行不同的代码分支: + ```rust if condition == true { // A... @@ -19,6 +21,7 @@ if condition == true { 该代码读作:若 `condition` 的值为 `true`,则执行 `A` 代码,否则执行 `B` 代码。 先看下面代码: + ```rust fn main() { let condition = true; @@ -53,8 +56,10 @@ error[E0308]: if and else have incompatible types found type `&str` ``` -#### 使用else if来处理多重条件 +#### 使用 else if 来处理多重条件 + 可以将 `else if` 与 `if`、`else` 组合在一起实现更复杂的条件分支判断: + ```rust fn main() { let n = 6; @@ -70,6 +75,7 @@ fn main() { } } ``` + 程序执行时,会按照自上至下的顺序执行每一个分支判断,一旦成功,则跳出 `if` 语句块,最终本程序会匹配执行 `else if n % 3 == 0` 的分支,输出 `"number is divisible by 3"`。 有一点要注意,就算有多个分支能匹配,也只有第一个匹配的分支会被执行! @@ -82,9 +88,10 @@ fn main() { 在 Rust 语言中有三种循环方式:`for`、`while` 和 `loop`,其中 `for` 循环是 Rust 循环王冠上的明珠。 -#### for循环 +#### for 循环 `for` 循环是 Rust 的大杀器: + ```rust fn main() { for i in 1..=5 { @@ -100,9 +107,11 @@ for 元素 in 集合 { // 使用元素干一些你懂我不懂的事情 } ``` + 这个语法跟 JavaScript 还蛮像,应该挺好理解。 注意,使用 `for` 时我们往往使用集合的引用形式,除非你不想在后面的代码中继续使用该集合(比如我们这里使用了 `container` 的引用)。如果不使用引用的话,所有权会被转移(move)到 `for` 语句块中,后面就无法再使用这个集合了): + ```rust for item in &container { // ... @@ -111,8 +120,8 @@ for item in &container { > 对于实现了 `copy` 特征的数组(例如 [i32; 10] )而言, `for item in arr` 并不会把 `arr` 的所有权转移,而是直接对其进行了拷贝,因此循环之后仍然可以使用 `arr` 。 - 如果想在循环中,**修改该元素**,可以使用 `mut` 关键字: + ```rust for item in &mut collection { // ... @@ -121,13 +130,14 @@ for item in &mut collection { 总结如下: - 使用方法 | 等价使用方式 | 所有权 ----------|--------|-------- -`for item in collection` | `for item in IntoIterator::into_iter(collection)` | 转移所有权 -`for item in &collection` | `for item in collection.iter()` | 不可变借用 -`for item in &mut collection` | `for item in collection.iter_mut()` | 可变借用 +| 使用方法 | 等价使用方式 | 所有权 | +| ----------------------------- | ------------------------------------------------- | ---------- | +| `for item in collection` | `for item in IntoIterator::into_iter(collection)` | 转移所有权 | +| `for item in &collection` | `for item in collection.iter()` | 不可变借用 | +| `for item in &mut collection` | `for item in collection.iter_mut()` | 可变借用 | 如果想在循环中**获取元素的索引**: + ```rust fn main() { let a = [4,3,2,1]; @@ -139,16 +149,19 @@ fn main() { ``` 有同学可能会想到,如果我们想用 `for` 循环控制某个过程执行 10 次,但是又不想单独声明一个变量来控制这个流程,该怎么写? + ```rust for _ in 0..10 { // ... } ``` + 可以用 `_` 来替代 `i` 用于 `for` 循环中,在 Rust 中 `_` 的含义是忽略该值或者类型的意思,如果不使用 `_`,那么编译器会给你一个 `变量未使用的` 的警告。 **两种循环方式优劣对比** 以下代码,使用了两种循环方式: + ```rust // 第一种 let collection = [1, 2, 3, 4, 5]; @@ -165,13 +178,15 @@ for item in collection { 第一种方式是循环索引,然后通过索引下标去访问集合,第二种方式是直接循环集合中的元素,优劣如下: -- **性能**:第一种使用方式中 `collection[index]` 的索引访问,会因为边界检查(Bounds Checking)导致运行时的性能损耗 —— Rust会检查并确认 `index` 是否落在集合内,但是第二种直接迭代的方式就不会触发这种检查,因为编译器会在编译时就完成分析并证明这种访问是合法的 +- **性能**:第一种使用方式中 `collection[index]` 的索引访问,会因为边界检查(Bounds Checking)导致运行时的性能损耗 —— Rust 会检查并确认 `index` 是否落在集合内,但是第二种直接迭代的方式就不会触发这种检查,因为编译器会在编译时就完成分析并证明这种访问是合法的 - **安全**:第一种方式里对 `collection` 的索引访问是非连续的,存在一定可能性在两次访问之间,`collection` 发生了变化,导致脏数据产生。而第二种直接迭代的方式是连续访问,因此不存在这种风险(这里是因为所有权吗?是的话可能要强调一下) 由于 `for` 循环无需任何条件限制,也不需要通过索引来访问,因此是最安全也是最常用的,通过与下面的 `while` 的对比,我们能看到为什么 `for` 会更加安全。 #### `continue` + 使用 `continue` 可以跳过当前当次的循环,开始下次的循环: + ```rust for i in 1..4 { if i == 2 { @@ -180,13 +195,18 @@ for item in collection { println!("{}",i); } ``` + 上面代码对 1 到 3 的序列进行迭代,且跳过值为 2 时的循环,输出如下: + ```console 1 3 ``` + #### `break` + 使用 `break` 可以直接跳出当前整个循环: + ```rust for i in 1..4 { if i == 2 { @@ -195,14 +215,17 @@ for item in collection { println!("{}",i); } ``` + 上面代码对 1 到 3 的序列进行迭代,在遇到值为 2 时的跳出整个循环,后面的循环不在执行,输出如下: + ```console 1 ``` -#### while循环 +#### while 循环 如果你需要一个条件来循环,当该条件为 `true` 时,继续循环,条件为 `false`,跳出循环,那么 `while` 就非常适用: + ```rust fn main() { let mut n = 0; @@ -218,6 +241,7 @@ fn main() { ``` 该 `while` 循环,只有当 `n` 小于等于 `5` 时,才执行,否则就立刻跳出循环,因此在上述代码中,它会先从 `0` 开始,满足条件,进行循环,然后是 `1`,满足条件,进行循环,最终到 `6` 的时候,大于 5,不满足条件,跳出 `while` 循环,执行 `我出来了` 的打印,然后程序结束: + ```console 0! 1! @@ -229,6 +253,7 @@ fn main() { ``` 当然,你也可以用其它方式组合实现,例如 `loop`(无条件循环,将在下面介绍) + `if` + `break`: + ```rust fn main() { let mut n = 0; @@ -244,11 +269,13 @@ fn main() { println!("我出来了!"); } ``` + 可以看出,在这种循环场景下,`while` 要简洁的多。 **while vs for** 我们也能用 `while` 来实现 `for` 的功能: + ```rust fn main() { let a = [10, 20, 30, 40, 50]; @@ -261,7 +288,9 @@ fn main() { } } ``` + 这里,代码对数组中的元素进行计数。它从索引 `0` 开始,并接着循环直到遇到数组的最后一个索引(这时,`index < 5` 不再为真)。运行这段代码会打印出数组中的每一个元素: + ```console the value is: 10 the value is: 20 @@ -272,9 +301,10 @@ the value is: 50 数组中的所有五个元素都如期被打印出来。尽管 `index` 在某一时刻会到达值 5,不过循环在其尝试从数组获取第六个值(会越界)之前就停止了。 -但这个过程很容易出错;如果索引长度不正确会导致程序 ***panic***。这也使程序更慢,因为编译器增加了运行时代码来对每次循环的每个元素进行条件检查。 +但这个过程很容易出错;如果索引长度不正确会导致程序 **_panic_**。这也使程序更慢,因为编译器增加了运行时代码来对每次循环的每个元素进行条件检查。 `for`循环代码如下: + ```rust fn main() { let a = [10, 20, 30, 40, 50]; @@ -287,11 +317,12 @@ fn main() { 可以看出,`for` 并不会使用索引去访问数组,因此更安全也更简洁,同时避免 `运行时的边界检查`,性能更高。 +#### loop 循环 -#### loop循环 对于循环而言,`loop` 循环毋庸置疑,是适用面最高的,它可以适用于所有循环场景(虽然能用,但是在很多场景下, `for` 和 `while` 才是最优选择),因为 `loop` 就是一个简单的无限循环,你可以在内部实现逻辑通过 `break` 关键字来控制循环何时结束。 使用 `loop` 循环一定要打起精神,否则你会写出下面的跑满你一个 CPU 核心的疯子代码: + ```rust,ignore fn main() { loop { @@ -301,6 +332,7 @@ fn main() { ``` 该循环会不停的在终端打印输出,直到你使用 `Ctrl-C` 结束程序: + ```console again! again! @@ -312,6 +344,7 @@ again! **注意**,不要轻易尝试上述代码,如果你电脑配置不行,可能会死机!!! 因此,当使用 `loop` 时,必不可少的伙伴是 `break` 关键字,它能让循环在满足某个条件时跳出: + ```rust fn main() { let mut counter = 0; @@ -327,10 +360,10 @@ fn main() { println!("The result is {}", result); } ``` + 以上代码当 `counter` 递增到 `10` 时,就会通过 `break` 返回一个 `counter * 2` 的值,最后赋给 `result` 并打印出来。 这里有几点值得注意: - **break 可以单独使用,也可以带一个返回值**,有些类似 `return` - **loop 是一个表达式**,因此可以返回一个值 - diff --git a/src/basic/formatted-output.md b/src/basic/formatted-output.md index 955c8baa..047e34de 100644 --- a/src/basic/formatted-output.md +++ b/src/basic/formatted-output.md @@ -1,8 +1,11 @@ # 格式化输出 + 提到格式化输出,可能很多人立刻就想到 `"{}"`,但是 Rust 能做到的远比这个多的多,本章节我们将深入讲解格式化输出的各个方面。 ## 满分初印象 + 先来一段代码,看看格式化输出的初印象: + ```rust println!("Hello"); // => "Hello" println!("Hello, {}!", "world"); // => "Hello, world!" @@ -15,7 +18,8 @@ println!("{:04}", 42); // => "0042" with leading zeros 可以看到 `println!` 宏接受的是可变参数,第一个参数是一个字符串常量,它表示最终输出字符串的格式,包含其中形如 `{}` 的符号是**占位符**,会被 `println!` 后面的参数依次替换。 -## `print!`,`println!`,`format!` +## `print!`,`println!`,`format!` + 它们是 Rust 中用来格式化输出的三大金刚,用途如下: - `print!` 将格式化文本输出到标准输出,不带换行符 @@ -23,6 +27,7 @@ println!("{:04}", 42); // => "0042" with leading zeros - `format!` 将格式化文本输出到 `String` 字符串 在实际项目中,最常用的是 `println!` 及 `format!`,前者常用来调试输出,后者常用来生成格式化的字符串: + ```rust fn main() { let s = "hello"; @@ -34,13 +39,16 @@ fn main() { ``` 其中,`s1` 是通过 `format!` 生成的 `String` 字符串,最终输出如下: + ```console hello, wolrd hello, world! ``` #### `eprint!`,`eprintln!` + 除了三大金刚外,还有两大护法,使用方式跟 `print!`,`println!` 很像,但是它们输出到标准错误输出: + ```rust eprintln!("Error: Could not complete task") ``` @@ -48,6 +56,7 @@ eprintln!("Error: Could not complete task") 它们仅应该被用于输出错误信息和进度信息,其它场景都应该使用 `print!` 系列。 ## {} 与 {:?} + 与其它语言常用的 `%d`,`%s` 不同,Rust 特立独行地选择了 `{}` 作为格式化占位符(说到这个,有点想吐槽下,Rust 中自创的概念其实还挺多的,真不知道该夸奖还是该吐槽-,-),事实证明,这种选择非常正确,它帮助用户减少了很多使用成本,你无需再为特定的类型选择特定的占位符,统一用 `{}` 来替代即可,剩下的类型推导等细节只要交给 Rust 去做。 与 `{}` 类似,`{:?}` 也是占位符: @@ -58,7 +67,9 @@ eprintln!("Error: Could not complete task") 其实两者的选择很简单,当你在写代码需要调试时,使用 `{:?}`,剩下的场景,选择 `{}`。 #### `Debug` 特征 + 事实上,为了方便我们调试,大多数 Rust 类型都实现了 `Debug` 特征或者支持派生该特征: + ```rust #[derive(Debug)] struct Person { @@ -77,9 +88,10 @@ fn main() { 对于数值、字符串、数组,可以直接使用 `{:?}` 进行输出,但是对于结构体,需要[派生`Debug`](../appendix/derive.md)特征后,才能进行输出,总之很简单。 - #### `Display` 特征 + 与大部分类型实现了 `Debug` 不同,实现了 `Display` 特征的 Rust 类型并没有那么多,往往需要我们自定义想要的格式化方式: + ```rust let i = 3.1415926; let s = String::from("hello"); @@ -100,7 +112,9 @@ println!("{}, {}, {}, {}", i, s, v, p); 下面来一一看看这三种方式。 #### {:#?} + `{:#?}` 与 `{:?}` 几乎一样,唯一的区别在于它能更优美地输出内容: + ```console // {:?} [1, 2, 3], Person { name: "sunface", age: 18 } @@ -118,7 +132,9 @@ println!("{}, {}, {}, {}", i, s, v, p); 因此对于 `Display` 不支持的类型,可以考虑使用 `{:#?}` 进行格式化,虽然理论上它更适合进行调试输出。 #### 为自定义类型实现 `Display` 特征 + 如果你的类型是定义在当前作用域中的,那么可以为其实现 `Display` 特征,即可用于格式化输出: + ```rust struct Person { name: String, @@ -145,12 +161,15 @@ fn main() { ``` 如上所示,只要实现 `Display` 特征中的 `fmt` 方法,即可为自定义结构体 `Person` 添加自定义输出: + ```console 大佬在上,请受我一拜,小弟姓名sunface,年芳18,家里无田又无车,生活苦哈哈 ``` #### 为外部类型实现 `Display` 特征 + 在 Rust 中,无法直接为外部类型实现外部特征,但是可以使用[`newtype`](./custom-type.md#newtype)解决此问题: + ```rust struct Array(Vec); @@ -167,6 +186,7 @@ fn main() { ``` `Array` 就是我们的 `newtype`,它将想要格式化输出的 `Vec` 包裹在内,最后只要为 `Array` 实现 `Display` 特征,即可进行格式化输出: + ```console 数组是:[1, 2, 3] ``` @@ -174,7 +194,9 @@ fn main() { 至此,关于 `{}` 与 `{:?}` 的内容已介绍完毕,下面让我们正式开始格式化输出的旅程。 ## 指定位置参数 -除了按照依次顺序使用值去替换占位符之外,还能让指定位置的参数去替换某个占位符,例如 `{1}`,表示用第二个参数替换该占位符(索引从0开始): + +除了按照依次顺序使用值去替换占位符之外,还能让指定位置的参数去替换某个占位符,例如 `{1}`,表示用第二个参数替换该占位符(索引从 0 开始): + ```rust fn main() { println!("{}{}", 1, 2); // =>"12" @@ -186,7 +208,9 @@ fn main() { ``` ## 带名称的变量 + 除了像上面那样指定位置外,我们还可以为参数指定名称: + ```rust fn main() { println!("{argument}", argument = "test"); // => "test" @@ -196,6 +220,7 @@ fn main() { ``` 需要注意的是:**带名称的参数必须放在不带名称参数的后面**,例如下面代码将报错: + ```rust println!("{abc} {1}", abc = "def", 2); ``` @@ -211,7 +236,9 @@ error: positional arguments cannot follow named arguments ``` ## 格式化参数 + 格式化输出,意味着对输出格式会有更多的要求,例如只输出浮点数的小数点后两位: + ```rust fn main() { let v = 3.1415926; @@ -227,10 +254,13 @@ fn main() { 接下来,让我们一起来看看 Rust 中有哪些格式化参数。 #### 宽度 + 宽度用来指示输出目标的长度,如果长度不够,则进行填充和对齐: ##### 字符串填充 + 字符串格式化默认使用空格进行填充,并且进行左对齐。 + ```rust fn main() { //----------------------------------- @@ -250,8 +280,10 @@ fn main() { } ``` -##### 数字填充:符号和0 +##### 数字填充:符号和 0 + 数字格式化默认也是使用空格进行填充,但与字符串左对齐不同的是,数字是右对齐。 + ```rust fn main() { // 宽度是5 => Hello 5! @@ -266,6 +298,7 @@ fn main() { ``` ##### 对齐 + ```rust fn main() { // 以下全部都会补齐5个字符的长度 @@ -283,7 +316,9 @@ fn main() { ``` #### 精度 + 精度可以用于控制浮点数的精度或者字符串的长度 + ```rust fn main() { let v = 3.1415926; @@ -295,7 +330,7 @@ fn main() { println!("{:.0}", v); // 通过参数来设定精度 => 3.1416,相当于{:.4} println!("{:.1$}", v, 4); - + let s = "hi我是Sunface孙飞"; // 保留字符串前三个字符 => hi我 println!("{:.3}", s); @@ -305,6 +340,7 @@ fn main() { ``` #### 进制 + 可以使用 `#` 号来控制数字的进制输出: - `#b`, 二进制 @@ -330,11 +366,12 @@ fn main() { println!("{:x}!", 27); // 使用0填充二进制,宽度为10 => 0b00011011! - println!("{:#010b}!", 27); + println!("{:#010b}!", 27); } ``` #### 指数 + ```rust fn main() { println!("{:2e}", 1000000000); // => 1e9 @@ -343,13 +380,16 @@ fn main() { ``` #### 指针地址 + ```rust let v= vec![1, 2, 3]; println!("{:p}", v.as_ptr()) // => 0x600002324050 ``` #### 转义 + 有时需要输出 `{`和`}`,但这两个字符是特殊字符,需要进行转义: + ```rust fn main() { // {使用{转义,}使用} => Hello {} @@ -363,6 +403,7 @@ fn main() { ## 在格式化字符串时捕获环境中的值(Rust 1.58 新增) 在以前,想要输出一个函数的返回值,你需要这么做: + ```rust fn get_person() -> String { String::from("sunface") @@ -374,7 +415,9 @@ fn main() { println!("Hello, {person}!", person = p); } ``` + 问题倒也不大,但是一旦格式化字符串长了后,就会非常冗余,而在 1.58 后,我们可以这么写: + ```rust fn get_person() -> String { String::from("sunface") @@ -384,15 +427,19 @@ fn main() { println!("Hello, {person}!"); } ``` + 是不是清晰、简洁了很多?甚至还可以将环境中的值用于格式化参数: + ```rust let (width, precision) = get_format(); for (name, score) in get_scores() { println!("{name}: {score:width$.precision$}"); } ``` + 但也有局限,它只能捕获普通的变量,对于更复杂的类型(例如表达式),可以先将它赋值给一个变量或使用以前的 `name = expression` 形式的格式化参数。 目前除了 `panic!` 外,其它接收格式化参数的宏,都可以使用新的特性。对于 `panic!` 而言,如果还在使用 `2015版本` 或 `2018版本`,那 `panic!("{ident}")` 依然会被当成 正常的字符串来处理,同时编译器会给予 `warn` 提示。而对于 `2021版本` ,则可以正常使用: + ```rust fn get_person() -> String { String::from("sunface") @@ -404,15 +451,16 @@ fn main() { ``` 输出: + ```console thread 'main' panicked at 'Hello, sunface!', src/main.rs:6:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace ``` ## 总结 + 把这些格式化都牢记在脑中是不太现实的,也没必要,我们要做的就是知道 Rust 支持相应的格式化输出,在需要之时,读者再来查阅本文即可。 -还是那句话,[<>](https://github.com/sunface/rust-course)不仅仅是 Rust 学习书籍,还是一本厚重的工具书! +还是那句话,[<>](https://github.com/sunface/rust-course)不仅仅是 Rust 学习书籍,还是一本厚重的工具书! 至此,Rust 的基础内容学习已经全部完成,下面我们将学习 Rust 的高级进阶内容,正式开启你的高手之路。 - diff --git a/src/basic/intro.md b/src/basic/intro.md index 52e27e69..1ecca4c1 100644 --- a/src/basic/intro.md +++ b/src/basic/intro.md @@ -1,6 +1,7 @@ # Rust 基本概念 从现在开始,我们正式踏入了 Rust 大陆,这篇广袤而神秘的世界,在这个世界中,将接触到很多之前都没有听过的概念: + - 所有权、借用、生命周期 - 宏编程 - 模式匹配 @@ -28,13 +29,13 @@ fn main() { let d = 30_i32; // 跟其它语言一样,可以使用一个函数的返回值来作为另一个函数的参数 let e = add(add(a, b), add(c, d)); - + // println!是宏调用,看起来像是函数但是它返回的是宏定义的代码块 // 该函数将指定的格式化字符串输出到标准输出中(控制台) // {}是占位符,在具体执行过程中,会把e的值代入进来 println!("( a + b ) + ( c + d ) = {}", e); } - + // 定义一个函数,输入两个i32类型的32位有符号整数,返回它们的和 fn add(i: i32, j: i32) -> i32 { // 返回相加值,这里可以省略return @@ -43,8 +44,9 @@ fn main() { ``` > 注意 ->在上面的 `add` 函数中,不要为 `i+j` 添加 `;`,这会改变语法导致函数返回 `()` 而不是 `i32`,具体参见[语句和表达式](./base-type/statement-expression.md) +> 在上面的 `add` 函数中,不要为 `i+j` 添加 `;`,这会改变语法导致函数返回 `()` 而不是 `i32`,具体参见[语句和表达式](./base-type/statement-expression.md) 有几点可以留意下: + - 字符串使用双引号 `""` 而不是单引号 `''`,Rust 中单引号是留给单个字符类型(`char`)使用的 - Rust 使用 `{}` 来作为格式化输出占位符,其它语言可能使用的是 `%s`,`%d`,`%p` 等,由于 `println!` 会自动推导出具体的类型, 因此无需手动指定 diff --git a/src/basic/match-pattern/all-patterns.md b/src/basic/match-pattern/all-patterns.md index 786b49fd..8e3414f0 100644 --- a/src/basic/match-pattern/all-patterns.md +++ b/src/basic/match-pattern/all-patterns.md @@ -19,7 +19,7 @@ match x { ### 匹配命名变量 -在 [match 一章](./match-if-let#变量覆盖)中,我们有讲过变量覆盖的问题,这个在**匹配命名变量**时会遇到: +在 [match](https://course.rs/match-if-let.html#变量覆盖) 中,我们有讲过变量覆盖的问题,这个在**匹配命名变量**时会遇到: ```rust fn main() { @@ -64,7 +64,7 @@ match x { ### 通过序列 `..=` 匹配值的范围 -在[数值类型](../base-type/numbers#序列(Range))中我们有讲到一个序列语法,该语言不仅可以用循环中,还能用于匹配模式。 +在[数值类型](https://course.rs/basic/base-type/numbers.html#序列range)中我们有讲到一个序列语法,该语言不仅可以用循环中,还能用于匹配模式。 `..=` 语法允许你匹配一个闭区间序列内的值。在如下代码中,当模式匹配任何在此序列内的值时,该分支会执行: @@ -137,12 +137,12 @@ fn main() { } ``` - 这段代码创建了变量 `x` 和 `y`,与结构体 `p` 中的 `x` 和 `y` 字段相匹配。其结果是变量 `x` 和 `y` 包含结构体 `p` 中的值。 也可以使用字面值作为结构体模式的一部分进行进行解构,而不是为所有的字段创建变量。这允许我们测试一些字段为特定值的同时创建其他字段的变量。 下文展示了固定某个字段的匹配方式: + ```rust # struct Point { # x: i32, @@ -161,7 +161,7 @@ fn main() { ``` 首先是 `match` 第一个分支,指定匹配 `y` 为 `0` 的 `Point`; -然后第二个分支在第一个分支之后,匹配 `y` 不为`0`,`x`为 `0` 的 `Point`; +然后第二个分支在第一个分支之后,匹配 `y` 不为 `0`,`x` 为 `0` 的 `Point`; 最后一个分支匹配 `x` 不为 `0`,`y` 也不为 `0` 的 `Point`。 在这个例子中,值 `p` 因为其 `x` 包含 0 而匹配第二个分支,因此会打印出 `On the y axis at 7`。 @@ -256,6 +256,7 @@ fn main() { } } ``` + `match` 第一个分支的模式匹配一个 `Message::ChangeColor` 枚举成员,该枚举成员又包含了一个 `Color::Rgb` 的枚举成员,最终绑定了 3 个内部的 `i32` 值。第二个,就交给亲爱的读者来思考完成。 #### 解构结构体和元组 @@ -281,7 +282,6 @@ let ((feet, inches), Point {x, y}) = ((3, 10), Point { x: 3, y: -10 }); 虽然 `_` 模式作为 `match` 表达式最后的分支特别有用,但是它的作用还不限于此。例如可以将其用于函数参数中: - ```rust fn foo(_: i32, y: i32) { println!("This code only uses the y parameter: {}", y); @@ -316,13 +316,11 @@ match (setting_value, new_setting_value) { println!("setting is {:?}", setting_value); ``` - 这段代码会打印出 `Can't overwrite an existing customized value` 接着是 `setting is Some(5)`。 第一个匹配分支,我们不关心里面的值,只关心元组中两个元素的类型,因此对于 `Some` 中的值,直接进行忽略。 剩下的形如 `(Some(_),None)`,`(None, Some(_))`, `(None,None)` 形式,都由第二个分支 `_` 进行分配。 - 还可以在一个模式中的多处使用下划线来忽略特定值,如下所示,这里忽略了一个五元元组中的第二和第四个值: ```rust @@ -363,7 +361,9 @@ if let Some(_s) = s { println!("{:?}", s); ``` + `s` 是一个拥有所有权的动态字符串,在上面代码中,我们会得到一个错误,因为 `s` 的值会被转移给 `_s`,在 `println!` 中再次使用 `s` 会报错: + ```console error[E0382]: borrow of partially moved value: `s` --> src/main.rs:8:22 @@ -451,12 +451,11 @@ error: `..` can only be used once per tuple pattern // 每个元组模式只能 error: could not compile `world_hello` due to previous error ^^ ``` -Rust无法判断,`second` 应该匹配 `numbers` 中的第几个元素,因此这里使用两个 `..` 模式,是由很大歧义的! - +Rust 无法判断,`second` 应该匹配 `numbers` 中的第几个元素,因此这里使用两个 `..` 模式,是由很大歧义的! ### 匹配守卫提供的额外条件 -**匹配守卫**(*match guard*)是一个位于 `match` 分支模式之后的额外 `if` 条件,它能为分支模式提供更进一步的匹配条件。 +**匹配守卫**(_match guard_)是一个位于 `match` 分支模式之后的额外 `if` 条件,它能为分支模式提供更进一步的匹配条件。 这个条件可以使用模式中创建的变量: @@ -529,7 +528,7 @@ match x { ## @绑定 -`@`(读作at)运算符允许为一个字段绑定另外一个变量。下面例子中,我们希望测试 `Message::Hello` 的 `id` 字段是否位于 `3..=7` 范围内,同时也希望能将其值绑定到 `id_variable` 变量中以便此分支中相关的代码可以使用它。我们可以将 `id_variable` 命名为 `id`,与字段同名,不过出于示例的目的这里选择了不同的名称。 +`@`(读作 at)运算符允许为一个字段绑定另外一个变量。下面例子中,我们希望测试 `Message::Hello` 的 `id` 字段是否位于 `3..=7` 范围内,同时也希望能将其值绑定到 `id_variable` 变量中以便此分支中相关的代码可以使用它。我们可以将 `id_variable` 命名为 `id`,与字段同名,不过出于示例的目的这里选择了不同的名称。 ```rust enum Message { @@ -551,8 +550,6 @@ match msg { } ``` - - 上例会打印出 `Found an id in range: 5`。通过在 `3..=7` 之前指定 `id_variable @`,我们捕获了任何匹配此范围的值并同时将该值绑定到变量 `id_variable` 上。 第二个分支只在模式中指定了一个范围,`id` 字段的值可以是 `10、11 或 12`,不过这个模式的代码并不知情也不能使用 `id` 字段中的值,因为没有将 `id` 值保存进一个变量。 @@ -562,7 +559,9 @@ match msg { 当你既想要限定分支范围,又想要使用分支的变量时,就可以用 `@` 来绑定到一个新的变量上,实现想要的功能。 #### @前绑定后解构(Rust 1.56 新增) + 使用 `@` 还可以在绑定新变量的同时,对目标进行解构: + ```rust #[derive(Debug)] struct Point { @@ -576,7 +575,7 @@ fn main() { println!("x: {}, y: {}", px, py); println!("{:?}", p); - + let point = Point {x: 10, y: 5}; if let p @ Point {x: 10, y} = point { println!("x is 10 and y is {} in {:?}", y, p); @@ -587,7 +586,9 @@ fn main() { ``` #### @新特性(Rust 1.53 新增) + 考虑下面一段代码: + ```rust fn main() { match 1 { @@ -598,12 +599,13 @@ fn main() { } } ``` + 编译不通过,是因为 `num` 没有绑定到所有的模式上,只绑定了模式 `1`,你可能会试图通过这个方式来解决: + ```rust num @ (1 | 2) ``` 但是,如果你用的是 Rust 1.53 之前的版本,那这种写法会报错,因为编译器不支持。 - 至此,模式匹配的内容已经全部完结,复杂但是详尽,想要一次性全部记住属实不易,因此读者可以先留一个印象,等未来需要时,再来翻阅寻找具体的模式实现方式。 diff --git a/src/basic/match-pattern/intro.md b/src/basic/match-pattern/intro.md index 097053d7..5819ef83 100644 --- a/src/basic/match-pattern/intro.md +++ b/src/basic/match-pattern/intro.md @@ -3,4 +3,3 @@ 模式匹配,这个词,对于非函数语言编程来说,真的还蛮少听到,因为它经常出现在函数式编程里,用于为复杂的类型系统提供一个轻松的解构能力。 曾记否?在枚举和流程控制那章,我们遗留了两个问题,都是关于 `match` 的,第一个是如何对 `Option` 枚举进行进一步处理,另外一个就是如何用 `match` 来替代 `else if` 这种丑陋的多重分支使用方式,那么让我们先一起来揭开 `match` 的神秘面纱。 - diff --git a/src/basic/match-pattern/match-if-let.md b/src/basic/match-pattern/match-if-let.md index 929c622f..63b06e81 100644 --- a/src/basic/match-pattern/match-if-let.md +++ b/src/basic/match-pattern/match-if-let.md @@ -1,8 +1,9 @@ -# match和if let +# match 和 if let 在 Rust 中,模式匹配最常用的就是 `match` 和 `if let`,本章节将对两者及相关的概念进行详尽介绍。 先来看一个关于 `match` 的简单例子: + ```rust enum Direction { East, @@ -24,16 +25,17 @@ fn main() { ``` 这里我们想去匹配 `dire` 对应的枚举类型,因此在 `match` 中用三个匹配分支来完全覆盖枚举变量 `Direction` 的所有成员类型,有以下几点值得注意: + - `match` 的匹配必须要穷举出所有可能,因此这里用 `_` 来代表未列出的所有可能性 - `match` 的每一个分支都必须是一个表达式,且所有分支的表达式最终返回值的类型必须相同 - **X | Y**,是逻辑运算符 `或`,代表该分支可以匹配 `X` 也可以匹配 `Y`,只要满足一个即可 - 其实 `match` 跟其他语言中的 `switch` 非常像,`_` 类似于 `switch` 中的 `default`。 -## `match` 匹配 +## `match` 匹配 首先来看看 `match` 的通用形式: + ```rust match target { 模式1 => 表达式1, @@ -80,7 +82,9 @@ fn value_in_cents(coin: Coin) -> u8 { 每个分支相关联的代码是一个表达式,而表达式的结果值将作为整个 `match` 表达式的返回值。如果分支有多行代码,那么需要用 `{}` 包裹,同时最后一行代码需要是一个表达式。 #### 使用 `match` 表达式赋值 + 还有一点很重要,`match` 本身也是一个表达式,因此可以用它来赋值: + ```rust enum IpAddr { Ipv4, @@ -98,11 +102,13 @@ fn main() { println!("{}", ip_str); } ``` + 因为这里匹配到 `_` 分支,所以将 `"::1"` 赋值给了 `ip_str`。 #### 模式绑定 模式匹配的另外一个重要功能是从模式中取出绑定的值,例如: + ```rust #[derive(Debug)] enum UsState { @@ -118,27 +124,31 @@ enum Coin { Quarter(UsState), // 25美分硬币 } ``` + 其中 `Coin::Quarter` 成员还存放了一个值:美国的某个州(因为在 1999 年到 2008 年间,美国在 25 美分(Quarter)硬币的背后为 50 个州印刷了不同的标记,其它硬币都没有这样的设计)。 接下来,我们希望在模式匹配中,获取到 25 美分硬币上刻印的州的名称: + ```rust fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, - Coin::Quarter(state) => { + Coin::Quarter(state) => { println!("State quarter from {:?}!", state); 25 }, } } ``` + 上面代码中,在匹配 `Coin::Quarter(state)` 模式时,我们把它内部存储的值绑定到了 `state` 变量上,因此 `state` 变量就是对应的 `UsState` 枚举类型。 例如有一个印了阿拉斯加州标记的 25 分硬币:`Coin::Quarter(UsState::Alaska))`, 它在匹配时,`state` 变量将被绑定 `UsState::Alaska` 的枚举值。 再来看一个更复杂的例子: + ```rust enum Action { Say(String), @@ -147,7 +157,7 @@ enum Action { } fn main() { - let actions = [ + let actions = [ Action::Say("Hello Rust".to_string()), Action::MoveTo(1,2), Action::ChangeColorRGB(255,255,0), @@ -171,6 +181,7 @@ fn main() { ``` 运行后输出: + ```console $ cargo run Compiling world_hello v0.1.0 (/Users/sunfei/development/rust/world_hello) @@ -182,7 +193,9 @@ change color into '(r:255, g:255, b:0)', 'b' has been ignored ``` #### 穷尽匹配 + 在文章的开头,我们简单总结过 `match` 的匹配必须穷尽所有情况,下面来举例说明,例如: + ```rust enum Direction { East, @@ -203,6 +216,7 @@ fn main() { ``` 我们没有处理 `Direction::West` 的情况,因此会报错: + ```console error[E0004]: non-exhaustive patterns: `West` not covered // 非穷尽匹配,`West` 没有被覆盖 --> src/main.rs:10:11 @@ -245,18 +259,21 @@ match some_u8_value { 然后,在某些场景下,我们其实只关心**某一个值是否存在**,此时 `match` 就显得过于啰嗦。 ## `if let` 匹配 + 有时会遇到只有一个模式的值需要被处理,其它值直接忽略的场景,如果用 `match` 来处理就要写成下面这样: + ```rust let v = Some(3u8); match v{ Some(3) => println!("three"), _ => (), } -```` +``` 我们只想要对 `Some(3)` 模式进行匹配, 不想处理任何其他 `Some` 值或 `None` 值。但是为了满足 `match` 表达式(穷尽性)的要求,写代码时必须在处理完这唯一的成员后加上 `_ => ()`,这样会增加不少无用的代码。 杀鸡焉用牛刀,可以用 `if let` 的方式来实现: + ```rust if let Some(3) = v { println!("three"); @@ -266,9 +283,11 @@ if let Some(3) = v { 这两种匹配对于新手来说,可能有些难以抉择,但是只要记住一点就好:**当你只要匹配一个条件,且忽略其他条件时就用 `if let` ,否则都用 `match`**。 ## matches!宏 + Rust 标准库中提供了一个非常实用的宏:`matches!`,它可以将一个表达式跟模式进行匹配,然后返回匹配的结果 `true` or `false`。 例如,有一个动态数组,里面存有以下枚举: + ```rust enum MyEnum { Foo, @@ -281,16 +300,19 @@ fn main() { ``` 现在如果想对 `v` 进行过滤,只保留类型是 `MyEnum::Foo` 的元素,你可能想这么写: + ```rust v.iter().filter(|x| x == MyEnum::Foo); ``` 但是,实际上这行代码会报错,因为你无法将 `x` 直接跟一个枚举成员进行比较。好在,你可以使用 `match` 来完成,但是会导致代码更为啰嗦,是否有更简洁的方式?答案是使用 `matches!`: + ```rust v.iter().filter(|x| matches!(x, MyEnum::Foo)); ``` 很简单也很简洁,再来看看更多的例子: + ```rust let foo = 'f'; assert!(matches!(foo, 'A'..='Z' | 'a'..='z')); @@ -300,7 +322,9 @@ assert!(matches!(bar, Some(x) if x > 2)); ``` ## 变量覆盖 + 无论是是 `match` 还是 `if let`,他们都可以在模式匹配时覆盖掉老的值,绑定新的值: + ```rust fn main() { let age = Some(30); @@ -314,6 +338,7 @@ fn main() { ``` `cargo run `运行后输出如下: + ```console 在匹配前,age是Some(30) 匹配出来的age是30 @@ -323,6 +348,7 @@ fn main() { 可以看出在 `if let` 中,`=` 右边 `Some(i32)` 类型的 `age` 被左边 `i32` 类型的新 `age` 覆盖了,该覆盖一直持续到 `if let` 语句块的结束。因此第三个 `println!` 输出的 `age` 依然是 `Some(i32)` 类型。 对于 `match` 类型也是如此: + ```rust fn main() { let age = Some(30); diff --git a/src/basic/match-pattern/option.md b/src/basic/match-pattern/option.md index 22e1ca29..634a46db 100644 --- a/src/basic/match-pattern/option.md +++ b/src/basic/match-pattern/option.md @@ -1,13 +1,15 @@ -# 解构Option +# 解构 Option 在枚举那章,提到过 `Option` 枚举,它用来解决 Rust 中变量是否有值的问题,定义如下: + ```rust enum Option { Some(T), None, } ``` -简单解释就是: **一个变量要么有值:`Some(T)`, 要么为空:`None`**。 + +简单解释就是:**一个变量要么有值:`Some(T)`, 要么为空:`None`**。 那么现在的问题就是该如何去使用这个 `Option` 枚举类型,根据我们上一节的经验,可以通过 `match` 来实现。 @@ -32,13 +34,14 @@ let none = plus_one(None); `plus_one` 接受一个 `Option` 类型的参数,同时返回一个 `Option` 类型的值(这种形式的函数在标准库内随处所见),在该函数的内部处理中,如果传入的是一个 `None` ,则返回一个 `None` 且不做任何处理;如果传入的是一个 `Some(i32)`,则通过模式绑定,把其中的值绑定到变量 `i` 上,然后返回 `i+1` 的值,同时用 `Some` 进行包裹。 - -为了进一步说明,假设 `plus_one` 函数接受的参数值x是 `Some(5)`,来看看具体的分支匹配情况: +为了进一步说明,假设 `plus_one` 函数接受的参数值 x 是 `Some(5)`,来看看具体的分支匹配情况: #### 传入参数 `Some(5)` + ```rust,ignore None => None, ``` + 首先是匹配 `None` 分支,因为值 `Some(5)` 并不匹配模式 `None`,所以继续匹配下一个分支。 ```rust,ignore @@ -47,7 +50,8 @@ Some(i) => Some(i + 1), `Some(5)` 与 `Some(i)` 匹配吗?当然匹配!它们是相同的成员。`i` 绑定了 `Some` 中包含的值,因此 `i` 的值是 `5`。接着匹配分支的代码被执行,最后将 `i` 的值加一并返回一个含有值 `6` 的新 `Some`。 -#### 传入参数None +#### 传入参数 None + 接着考虑下 `plus_one` 的第二个调用,这次传入的 `x` 是 `None`, 我们进入 `match` 并与第一个分支相比较。 ```rust,ignore @@ -55,4 +59,3 @@ None => None, ``` 匹配上了!接着程序继续执行该分支后的代码:返回表达式 `None` 的值,也就是返回一个 `None`,因为第一个分支就匹配到了,其他的分支将不再比较。 - diff --git a/src/basic/match-pattern/pattern-match.md b/src/basic/match-pattern/pattern-match.md index 48068510..cf8b8088 100644 --- a/src/basic/match-pattern/pattern-match.md +++ b/src/basic/match-pattern/pattern-match.md @@ -1,17 +1,18 @@ # 模式适用场景 ## 模式 + 模式是 Rust 中的特殊语法,它用来匹配类型中的结构和数据,它往往和 `match` 表达式联用,以实现强大的模式匹配能力。模式一般由以下内容组合而成: + - 字面值 - 解构的数组、枚举、结构体或者元组 - 变量 - 通配符 - 占位符 - ### 所有可能用到模式的地方 -#### match分支 +#### match 分支 ```rust match VALUE { @@ -20,7 +21,9 @@ match VALUE { PATTERN => EXPRESSION, } ``` + 如上所示,`match` 的每个分支就是一个**模式**,因为 `match` 匹配是穷尽式的,因此我们往往需要一个特殊的模式 `_`,来匹配剩余的所有情况: + ```rust match VALUE { PATTERN => EXPRESSION, @@ -29,16 +32,20 @@ match VALUE { } ``` -#### if let分支 +#### if let 分支 + `if let` 往往用于匹配一个模式,而忽略剩下的所有模式的场景: + ```rust if let PATTERN = SOME_VALUE { } ``` -#### while let条件循环 +#### while let 条件循环 + 一个与 `if let` 类似的结构是 `while let` 条件循环,它允许只要模式匹配就一直进行 `while` 循环。下面展示了一个使用 `while let` 的例子: + ```rust // Vec是动态数组 let mut stack = Vec::new(); @@ -58,7 +65,8 @@ while let Some(top) = stack.pop() { 你也可以用 `loop` + `if let` 或者 `match` 来实现这个功能,但是会更加啰嗦。 -#### for循环 +#### for 循环 + ```rust let v = vec!['a', 'b', 'c']; @@ -69,16 +77,19 @@ for (index, value) in v.iter().enumerate() { 这里使用 `enumerate` 方法产生一个迭代器,该迭代器每次迭代会返回一个 `(索引,值)` 形式的元组,然后用 `(index,value)` 来匹配。 -#### let语句 +#### let 语句 ```rust let PATTERN = EXPRESSION; ``` + 是的, 该语句我们已经用了无数次了,它也是一种模式匹配: + ```rust let x = 5; ``` -这其中,`x` 也是一种模式绑定,代表将**匹配的值绑定到变量x上**。因此,在 Rust 中,**变量名也是一种模式**,只不过它比较朴素很不起眼罢了。 + +这其中,`x` 也是一种模式绑定,代表将**匹配的值绑定到变量 x 上**。因此,在 Rust 中,**变量名也是一种模式**,只不过它比较朴素很不起眼罢了。 ```rust let (x, y, z) = (1, 2, 3); @@ -87,9 +98,11 @@ let (x, y, z) = (1, 2, 3); 上面将一个元组与模式进行匹配(**模式和值的类型必需相同!**),然后把 `1, 2, 3` 分别绑定到 `x, y, z` 上。 模式匹配要求两边的类型必须相同,否则就会导致下面的报错: + ```rust let (x, y) = (1, 2, 3); ``` + ```rust error[E0308]: mismatched types --> src/main.rs:4:5 @@ -104,16 +117,21 @@ error[E0308]: mismatched types For more information about this error, try `rustc --explain E0308`. error: could not compile `playground` due to previous error ``` + 对于元组来说,元素个数也是类型的一部分! #### 函数参数 + 函数参数也是模式: + ```rust fn foo(x: i32) { // 代码 } ``` + 其中 `x` 就是一个模式,你还可以在参数中匹配元组: + ```rust fn print_coordinates(&(x, y): &(i32, i32)) { println!("Current location: ({}, {})", x, y); @@ -124,20 +142,23 @@ fn main() { print_coordinates(&point); } ``` -`&(3, 5)` 会匹配模式 `&(x, y)`,因此 `x` 得到了 `3`,`y` 得到了 `5`。 +`&(3, 5)` 会匹配模式 `&(x, y)`,因此 `x` 得到了 `3`,`y` 得到了 `5`。 #### if 和 if let 对于以下代码,编译器会报错: + ```rust let Some(x) = some_option_value; ``` + 因为右边的值可能不为 `Some`,而是 `None`,这种时候就不能进行匹配,也就是上面的代码遗漏了 `None` 的匹配。 类似 `let` 和 `for`、`match` 都必须要求完全覆盖匹配,才能通过编译。 但是对于 `if let`,就可以这样使用: + ```rust if let Some(x) = some_option_value { println!("{}", x); diff --git a/src/basic/method.md b/src/basic/method.md index 030cd3bd..2d756921 100644 --- a/src/basic/method.md +++ b/src/basic/method.md @@ -1,14 +1,17 @@ -# 方法Method +# 方法 Method 从面向对象语言过来的同学对于方法肯定不陌生,`class` 里面就充斥着方法的概念。在 Rust 中,方法的概念也大差不差,往往和对象成对出现: + ```rust object.method() ``` + 例如读取一个文件写入缓冲区,如果用函数的写法 `read(f,buffer)`,用方法的写法 `f.read(buffer)`。不过与其它语言 `class` 跟方法的联动使用不同(这里可能要修改下),Rust 的方法往往跟结构体、枚举、特征一起使用,特征(Trait)将在后面几章进行介绍。 ## 定义方法 Rust 使用 `impl` 来定义方法,例如以下代码: + ```rust struct Circle { x: f64, @@ -41,6 +44,7 @@ impl Circle { 可以看出,其它语言中所有定义都在 `class` 中,但是 Rust 的对象定义和方法定义是分离的,这种数据和使用分离的方式,会给予使用者极高的灵活度。 再来看一个例子: + ```rust #[derive(Debug)] struct Rectangle { @@ -66,11 +70,12 @@ fn main() { 该例子定义了一个 `Rectangle` 结构体,并且在其上定义了一个 `area` 方法,用于计算该矩形的面积。 -`impl Rectangle {}` 表示为 `Rectangle` 实现方法(`impl` 是实现 *implementation* 的缩写),这样的写法表明 `impl` 语句块中的一切都是跟 `Rectangle` 相关联的。 +`impl Rectangle {}` 表示为 `Rectangle` 实现方法(`impl` 是实现 _implementation_ 的缩写),这样的写法表明 `impl` 语句块中的一切都是跟 `Rectangle` 相关联的。 接下里的内容非常重要,请大家仔细看。在 `area` 的签名中,我们使用 `&self` 替代 `rectangle: &Rectangle`,`&self` 其实是 `self: &Self` 的简写(注意大小写)。在一个 `impl` 块内,`Self` 指代被实现方法的结构体类型,`self` 指代此类型的实例,换句话说,`self` 指代的是 `Rectangle` 结构体实例,这样的写法会让我们的代码简洁很多,而且非常便于理解:我们为哪个结构体实现方法,那么 `self` 就是指代哪个结构体的实例。 需要注意的是,`self` 依然有所有权的概念: + - `self` 表示 `Rectangle` 的所有权转移到该方法中,这种形式用的较少 - `&self` 表示该方法对 `Rectangle` 的不可变借用 - `&mut self` 表示可变借用 @@ -80,11 +85,14 @@ fn main() { 回到上面的例子中,选择 `&self` 的理由跟在函数中使用 `&Rectangle` 是相同的:我们并不想获取所有权,也无需去改变它,只是希望能够读取结构体中的数据。如果想要在方法中去改变当前的结构体,需要将第一个参数改为 `&mut self`。仅仅通过使用 `self` 作为第一个参数来使方法获取实例的所有权是很少见的,这种使用方式往往用于把当前的对象转成另外一个对象时使用,转换完后,就不再关注之前的对象,且可以防止对之前对象的误调用。 简单总结下,使用方法代替函数有以下好处: + - 不用在函数签名中重复书写 `self` 对应的类型 - 代码的组织性和内聚性更强,对于代码维护和阅读来说,好处巨大 #### 方法名跟结构体字段名相同 + 在 Rust 中,允许方法名跟结构体的字段名相同: + ```rust impl Rectangle { fn width(&self) -> bool { @@ -107,6 +115,7 @@ fn main() { 当我们使用 `rect1.width()` 时, Rust 知道我们调用的是它的方法,如果使用 `rect1.width`,则是访问它的字段。 一般来说,方法跟字段同名,往往适用于实现 `getter` 访问器,例如: + ```rust pub struct Rectangle { width: u32, @@ -163,7 +172,9 @@ fn main() { > 第一行看起来简洁的多。这种自动引用的行为之所以有效,是因为方法有一个明确的接收者———— `self` 的类型。在给出接收者和方法名的前提下,Rust 可以明确地计算出方法是仅仅读取(`&self`),做出修改(`&mut self`)或者是获取所有权(`self`)。事实上,Rust 对方法接收者的隐式借用让所有权在实践中更友好。 ## 带有多个参数的方法 + 方法和函数一样,可以使用多个参数: + ```rust impl Rectangle { fn area(&self) -> u32 { @@ -185,7 +196,6 @@ fn main() { } ``` - ## 关联函数 现在大家可以思考一个问题,如何为一个结构体定义一个构造器方法?也就是接受几个参数,然后构造并返回该结构体的实例。其实答案在开头的代码片段中就给出了,很简单,不使用 `self` 中即可。 @@ -212,8 +222,10 @@ impl Rectangle { 因为是函数,所以不能用 `.` 的方式来调用,我们需要用`::`来调用,例如 `let sq = Rectangle::new(3,3);`。这个方法位于结构体的命名空间中:`::` 语法用于关联函数和模块创建的命名空间。 -## 多个impl定义 +## 多个 impl 定义 + Rust 允许我们为一个结构体定义多个 `impl` 块,目的是提供更多的灵活性和代码组织性,例如当方法多了后,可以把相关的方法组织在同一个 `impl` 块中,那么就可以形成多个 `impl` 块,各自完成一块儿目标: + ```rust # #[derive(Debug)] # struct Rectangle { diff --git a/src/basic/ownership/borrowing.md b/src/basic/ownership/borrowing.md index dcebf233..4b219192 100644 --- a/src/basic/ownership/borrowing.md +++ b/src/basic/ownership/borrowing.md @@ -2,13 +2,12 @@ 上节中提到,如果仅仅支持通过转移所有权的方式获取一个值,那会让程序变得复杂。 Rust 能否像其它编程语言一样,使用某个变量的指针或者引用呢?答案是可以。 -Rust 通过 `借用(Borrowing)` 这个概念来达成上述的目的, **获取变量的引用,称之为借用(borrowing)**。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来,当使用完毕后,也必须要物归原主。 - - +Rust 通过 `借用(Borrowing)` 这个概念来达成上述的目的,**获取变量的引用,称之为借用(borrowing)**。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来,当使用完毕后,也必须要物归原主。 ### 引用与解引用 常规引用是一个指针类型,指向了对象存储的内存地址。在下面代码中,我们创建一个 `i32` 值的引用 `y`,然后使用解引用运算符来解出 `y` 所使用的值: + ```rust fn main() { let x = 5; @@ -19,7 +18,7 @@ fn main() { } ``` -变量 `x` 存放了一个 `i32` 值 `5`。`y` 是 `x` 的一个引用。可以断言 `x` 等于 `5`。然而,如果希望对 `y` 的值做出断言,必须使用 `*y` 来解出引用所指向的值(也就是 **解引用**)。一旦解引用了 `y`,就可以访问 `y` 所指向的整型值并可以与 `5` 做比较。 +变量 `x` 存放了一个 `i32` 值 `5`。`y` 是 `x` 的一个引用。可以断言 `x` 等于 `5`。然而,如果希望对 `y` 的值做出断言,必须使用 `*y` 来解出引用所指向的值(也就是**解引用**)。一旦解引用了 `y`,就可以访问 `y` 所指向的整型值并可以与 `5` 做比较。 相反如果尝试编写 `assert_eq!(5, y);`,则会得到如下编译错误: @@ -39,6 +38,7 @@ error[E0277]: can't compare `{integer}` with `&{integer}` ### 不可变引用 下面的代码,我们用 `s1` 的引用作为参数传递给 `calculate_length` 函数,而不是把 `s1` 的所有权转移给该函数: + ```rust fn main() { let s1 = String::from("hello"); @@ -54,15 +54,17 @@ fn calculate_length(s: &String) -> usize { ``` 能注意到两点: + 1. 无需像上章一样:先通过函数参数传入所有权,然后再通过函数返回来传出所有权,代码更加简洁 2. `calculate_length` 的参数 `s` 类型从 `String` 变为 `&String` 这里,`&` 符号即是引用,它们允许你使用值,但是不获取所有权,如图所示: &String s pointing at String s1 -通过 `&s1` 语法,我们创建了一个 **指向s1的引用**,但是并不拥有它。因为并不拥有这个值,当引用离开作用域后,其指向的值也不会被丢弃。 +通过 `&s1` 语法,我们创建了一个**指向 `s1` 的引用**,但是并不拥有它。因为并不拥有这个值,当引用离开作用域后,其指向的值也不会被丢弃。 同理,函数 `calculate_length` 使用 `&` 来表明参数 `s` 的类型是一个引用: + ```rust fn calculate_length(s: &String) -> usize { // s 是对 String 的引用 s.len() @@ -71,6 +73,7 @@ fn calculate_length(s: &String) -> usize { // s 是对 String 的引用 ``` 人总是贪心的,可以拉女孩小手了,就想着抱抱柔软的身子(读者中的某老司机表示,这个流程完全不对),因此光借用已经满足不了我们了,如果尝试修改借用的变量呢? + ```rust fn main() { let s = String::from("hello"); @@ -84,6 +87,7 @@ fn change(some_string: &String) { ``` 很不幸,妹子你没抱到,哦口误,你修改错了: + ```console error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference --> src/main.rs:8:5 @@ -101,6 +105,7 @@ error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` ref ### 可变引用 只需要一个小调整,即可修复上面代码的错误: + ```rust fn main() { let mut s = String::from("hello"); @@ -118,6 +123,7 @@ fn change(some_string: &mut String) { ##### 可变引用同时只能存在一个 不过可变引用并不是随心所欲、想用就用的,它有一个很大的限制: **同一作用域,特定数据只能有一个可变引用**: + ```rust let mut s = String::from("hello"); @@ -128,6 +134,7 @@ println!("{}, {}", r1, r2); ``` 以上代码会报错: + ```console error[E0499]: cannot borrow `s` as mutable more than once at a time 同一时间无法对 `s` 进行两次可变借用 --> src/main.rs:5:14 @@ -136,10 +143,10 @@ error[E0499]: cannot borrow `s` as mutable more than once at a time 同一时间 | ------ first mutable borrow occurs here 首个可变引用在这里借用 5 | let r2 = &mut s; | ^^^^^^ second mutable borrow occurs here 第二个可变引用在这里借用 -6 | +6 | 7 | println!("{}, {}", r1, r2); | -- first borrow later used here 第一个借用在这里使用 - ``` +``` 这段代码出错的原因在于,第一个可变借用 `r1` 必须要持续到最后一次使用的位置 `println!`,在 `r1` 创建和最后一次使用之间,我们又尝试创建第二个可变引用 `r2`。 @@ -154,6 +161,7 @@ error[E0499]: cannot borrow `s` as mutable more than once at a time 同一时间 数据竞争会导致未定义行为,这种行为很可能超出我们的预期,难以在运行时追踪,并且难以诊断和修复。而 Rust 避免了这种情况的发生,因为它甚至不会编译存在数据竞争的代码! 很多时候,大括号可以帮我们解决一些编译不通过的问题,通过手动限制变量的作用域: + ```rust let mut s = String::from("hello"); @@ -168,6 +176,7 @@ let r2 = &mut s; ##### 可变引用与不可变引用不能同时存在 下面的代码会导致一个错误: + ```rust let mut s = String::from("hello"); @@ -179,6 +188,7 @@ println!("{}, {}, and {}", r1, r2, r3); ``` 错误如下: + ```console error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable // 无法借用可变 `s` 因为它已经被借用了不可变 @@ -189,7 +199,7 @@ error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immuta 5 | let r2 = &s; // 没问题 6 | let r3 = &mut s; // 大问题 | ^^^^^^ mutable borrow occurs here 可变借用发生在这里 -7 | +7 | 8 | println!("{}, {}, and {}", r1, r2, r3); | -- immutable borrow later used here 不可变借用在这里使用 ``` @@ -199,16 +209,17 @@ error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immuta > 注意,引用的作用域 `s` 从创建开始,一直持续到它最后一次使用的地方,这个跟变量的作用域有所不同,变量的作用域从创建持续到某一个花括号 `}` Rust 的编译器一直在优化,早期的时候,引用的作用域跟变量作用域是一致的,这对日常使用带来了很大的困扰,你必须非常小心的去安排可变、不可变变量的借用,免得无法通过编译,例如以下代码: + ```rust fn main() { let mut s = String::from("hello"); - let r1 = &s; - let r2 = &s; + let r1 = &s; + let r2 = &s; println!("{} and {}", r1, r2); // 新编译器中,r1,r2作用域在这里结束 - let r3 = &mut s; + let r3 = &mut s; println!("{}", r3); } // 老编译器中,r1、r2、r3作用域在这里结束 // 新编译器中,r3作用域在这里结束 @@ -224,7 +235,6 @@ fn main() { 虽然这种借用错误有的时候会让我们很郁闷,但是你只要想想这是 Rust 提前帮你发现了潜在的 BUG,其实就开心了,虽然减慢了开发速度,但是从长期来看,大幅减少了后续开发和运维成本。 - ### 悬垂引用(Dangling References) 悬垂引用也叫做悬垂指针,意思为指针指向某个值后,这个值被释放掉了,而指针仍然存在,其指向的内存可能不存在任何值或已被其它变量重新使用。在 Rust 中编译器可以确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器可以确保数据不会在其引用之前被释放,要想释放数据,必须先停止其引用的使用。 @@ -269,7 +279,6 @@ this function's return type contains a borrowed value, but there is no value for 仔细看看 `dangle` 代码的每一步到底发生了什么: - ```rust fn dangle() -> &String { // dangle 返回一个字符串的引用 @@ -294,7 +303,6 @@ fn no_dangle() -> String { 这样就没有任何错误了,最终 `String` 的 **所有权被转移给外面的调用者**。 - ## 借用规则总结 总的来说,借用规则如下: diff --git a/src/basic/ownership/ownership.md b/src/basic/ownership/ownership.md index 83b5a79c..1b7b8c4d 100644 --- a/src/basic/ownership/ownership.md +++ b/src/basic/ownership/ownership.md @@ -12,7 +12,8 @@ ## 一段不安全的代码 -先来看看一段来自C语言的糟糕代码: +先来看看一段来自 C 语言的糟糕代码: + ```c int* foo() { int a; // 变量a的作用域开始 @@ -44,7 +45,6 @@ int* foo() { 因为上述的实现方式,栈中的所有数据都必须占用已知且固定大小的内存空间,假设数据大小是未知的,那么在取出数据时,你将无法取到你想要的数据。 - #### 堆 与栈不同,对于大小未知或者可能变化的数据,我们需要将它存储在堆上。 @@ -71,24 +71,23 @@ int* foo() { 对于其他很多编程语言,你确实无需理解堆栈的原理,但是**在 Rust 中,明白堆栈的原理,对于我们理解所有权的工作原理会有很大的帮助**。 - - ## 所有权原则 理解了堆栈,接下来看一下*关于所有权的规则*,首先请谨记以下规则: + > 1. Rust 中每一个值都 `有且只有` 一个所有者(变量) > 2. 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop) - - #### 变量作用域 作用域是一个变量在程序中有效的范围, 假如有这样一个变量: + ```rust let s = "hello" ``` 变量 `s` 绑定到了一个字符串字面值,该字符串字面值是硬编码到程序代码中的。`s` 变量从声明的点开始直到当前作用域的结束都是有效的: + ```rust { // s 在这里无效,它尚未声明 let s = "hello"; // 从此处起,s 是有效的 @@ -99,24 +98,25 @@ let s = "hello" 简而言之,`s` 从创建伊始就开始有效,然后有效期持续到它离开作用域为止,可以看出,就作用域来说,Rust 语言跟其他编程语言没有区别。 -#### 简单介绍String类型 +#### 简单介绍 String 类型 -之前提到过,本章会用 `String` 作为例子,因此这里会进行一下简单的介绍,具体的 `String` 学习请参见 [String类型](../compound-type/string-slice.md)。 +之前提到过,本章会用 `String` 作为例子,因此这里会进行一下简单的介绍,具体的 `String` 学习请参见 [String 类型](../compound-type/string-slice.md)。 我们已经见过字符串字面值 `let s ="hello"`,`s` 是被硬编码进程序里的字符串值(类型为 `&str` )。字符串字面值是很方便的,但是它并不适用于所有场景。原因有二: - **字符串字面值是不可变的**,因为被硬编码到程序代码中 - 并非所有字符串的值都能在编写代码时得知 - 例如,字符串是需要程序运行时,通过用户动态输入然后存储在内存中的,这种情况,字符串字面值就完全无用武之地。 为此,Rust 为我们提供动态字符串类型: `String`, 该类型被分配到堆上,因此可以动态伸缩,也就能存储在编译时大小未知的文本。 可以使用下面的方法基于字符串字面量来创建 `String` 类型: + ```rust let s = String::from("hello"); ``` `::` 是一种调用操作符,这里表示调用 `String` 中的 `from` 方法,因为 `String` 存储在堆上是动态的,你可以这样修改它: + ```rust let mut s = String::from("hello"); @@ -132,6 +132,7 @@ println!("{}", s); // 将打印 `hello, world!` #### 转移所有权 先来看一段代码: + ```rust let x = 5; let y = x; @@ -142,20 +143,23 @@ let y = x; 可能有同学会有疑问:这种拷贝不消耗性能吗?实际上,这种栈上的数据足够简单,而且拷贝非常非常快,只需要复制一个整数大小(`i32`,4 个字节)的内存即可,因此在这种情况下,拷贝的速度远比在堆上创建内存来得快的多。实际上,上一章我们讲到的 Rust 基本类型都是通过自动拷贝的方式来赋值的,就像上面代码一样。 然后再来看一段代码: + ```rust let s1 = String::from("hello"); let s2 = s1; ``` -此时,可能某个大聪明(善意昵称)已经想到了:嗯,把 `s1` 的内容拷贝一份赋值给 `s2`,实际上,并不是这样。之前也提到了,对于基本类型(存储在栈上),Rust 会自动拷贝,但是 `String` 不是基本类型,而且是存储在堆上的,因此不能自动拷贝。 + +此时,可能某个大聪明(善意昵称)已经想到了:嗯,把 `s1` 的内容拷贝一份赋值给 `s2`,实际上,并不是这样。之前也提到了,对于基本类型(存储在栈上),Rust 会自动拷贝,但是 `String` 不是基本类型,而且是存储在堆上的,因此不能自动拷贝。 实际上, `String` 类型是一个复杂类型,由**存储在栈中的堆指针**、**字符串长度**、**字符串容量**共同组成,其中**堆指针**是最重要的,它指向了真实存储字符串内容的堆内存,至于长度和容量,如果你有 Go 语言的经验,这里就很好理解:容量是堆内存分配空间的大小,长度是目前已经使用的大小。 总之 `String` 类型指向了一个堆上的空间,这里存储着它的真实数据, 下面对上面代码中的 `let s2 = s1` 分成两种情况讨论: + 1. 拷贝 `String` 和存储在堆上的字节数组 -如果该语句是拷贝所有数据(深拷贝),那么无论是 `String` 本身还是底层的堆上数据,都会被全部拷贝,这对于性能而言会造成非常大的影响 + 如果该语句是拷贝所有数据(深拷贝),那么无论是 `String` 本身还是底层的堆上数据,都会被全部拷贝,这对于性能而言会造成非常大的影响 2. 只拷贝 `String` 本身 -这样的拷贝非常快,因为在 64 位机器上就拷贝了 `8字节的指针`、`8字节的长度`、`8字节的容量`,总计 24 字节,但是带来了新的问题,还记得我们之前提到的所有权规则吧?其中有一条就是:**一个值只允许有一个所有者**,而现在这个值(堆上的真实字符串数据)有了两个所有者:`s1` 和 `s2`。 + 这样的拷贝非常快,因为在 64 位机器上就拷贝了 `8字节的指针`、`8字节的长度`、`8字节的容量`,总计 24 字节,但是带来了新的问题,还记得我们之前提到的所有权规则吧?其中有一条就是:**一个值只允许有一个所有者**,而现在这个值(堆上的真实字符串数据)有了两个所有者:`s1` 和 `s2`。 好吧,就假定一个值可以拥有两个所有者,会发生什么呢? @@ -164,6 +168,7 @@ let s2 = s1; 因此,Rust 这样解决问题:**当 `s1` 赋予 `s2` 后,Rust 认为 `s1` 不再有效,因此也无需在 `s1` 离开作用域后 `drop` 任何东西,这就是把所有权从 `s1` 转移给了 `s2`,`s1` 在被赋予 `s2` 后就马上失效了**。 再来看看,在所有权转移后再来使用旧的所有者,会发生什么: + ```rust let s1 = String::from("hello"); let s2 = s1; @@ -172,6 +177,7 @@ println!("{}, world!", s1); ``` 由于 Rust 禁止你使用无效的引用,你会看到以下的错误: + ```console error[E0382]: use of moved value: `s1` --> src/main.rs:5:28 @@ -197,8 +203,8 @@ error[E0382]: use of moved value: `s1` 这样就解决了我们之前的问题,`s1` 不再指向任何数据,只有 `s2` 是有效的,当 `s2` 离开作用域,它就会释放内存。 相信此刻,你应该明白了,为什么 Rust 称呼 `let a = b` 为**变量绑定**了吧? - 再来看一段代码: + ```rust fn main() { let x: &str = "hello, world"; @@ -235,6 +241,7 @@ println!("s1 = {}, s2 = {}", s1, s2); 浅拷贝只发生在栈上,因此性能很高,在日常编程中,浅拷贝无处不在。 再回到之前看过的例子: + ```rust let x = 5; let y = x; @@ -250,15 +257,17 @@ Rust 有一个叫做 `Copy` 的特征,可以用在类似整型这样在栈中 那么什么类型是可 `Copy` 的呢?可以查看给定类型的文档来确认,不过作为一个通用的规则: **任何基本类型的组合可以 `Copy` ,不需要分配内存或某种形式资源的类型是可以 `Copy` 的**。如下是一些 `Copy` 的类型: -* 所有整数类型,比如 `u32`。 -* 布尔类型,`bool`,它的值是 `true` 和 `false`。 -* 所有浮点数类型,比如 `f64`。 -* 字符类型,`char`。 -* 元组,当且仅当其包含的类型也都是 `Copy` 的时候。比如,`(i32, i32)` 是 `Copy` 的,但 `(i32, String)` 就不是。 -* 引用类型,例如[转移所有权](#转移所有权)中的最后一个例子 - +- 所有整数类型,比如 `u32`。 +- 布尔类型,`bool`,它的值是 `true` 和 `false`。 +- 所有浮点数类型,比如 `f64`。 +- 字符类型,`char`。 +- 元组,当且仅当其包含的类型也都是 `Copy` 的时候。比如,`(i32, i32)` 是 `Copy` 的,但 `(i32, String)` 就不是。 +- 引用类型,例如[转移所有权](#转移所有权)中的最后一个例子 + ## 函数传值与返回 + 将值传递给函数,一样会发生 `移动` 或者 `复制`,就跟 `let` 语句一样,下面的代码展示了所有权、作用域的规则: + ```rust fn main() { let s = String::from("hello"); // s 进入作用域 @@ -285,8 +294,8 @@ fn makes_copy(some_integer: i32) { // some_integer 进入作用域 你可以尝试在 `takes_ownership` 之后,再使用 `s`,看看如何报错?例如添加一行 `println!("在move进函数后继续使用s: {}",s);`。 - 同样的,函数返回值也有所有权,例如: + ```rust fn main() { let s1 = gives_ownership(); // gives_ownership 将返回值 @@ -315,6 +324,4 @@ fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用 } ``` - 所有权很强大,避免了内存的不安全性,但是也带来了一个新麻烦: **总是把一个值传来传去来使用它**。 传入一个函数,很可能还要从该函数传出去,结果就是语言表达变得非常啰嗦,幸运的是,Rust 提供了新功能解决这个问题。 - diff --git a/src/basic/result-error/intro.md b/src/basic/result-error/intro.md index c9de9379..c1f871b3 100644 --- a/src/basic/result-error/intro.md +++ b/src/basic/result-error/intro.md @@ -4,12 +4,14 @@ 社会演变至今,这种思想依然没变,甚至来到计算中的微观世界,也是如此。及时、准确的获知系统在发生什么,是程序设计的重中之重。因此能够准确的分辨函数返回值是正确的还是错误的、以及在发生错误时该怎么快速处理,成了程序设计语言的必备功能。 -Go 语言为人诟病的其中一点就是 ***if err != nil {}*** 的大量使用,缺乏一些程序设计的美感,不过我倒是觉得这种简单的方式也有其好处,就是阅读代码时的流畅感很强,你不需要过多的思考各种语法是什么意思。与 Go 语言不同,Rust 博采众家之长,实现了颇具自身色彩的返回值和错误处理体系,本章我们就高屋建瓴地来学习,更加深入的讲解见[错误处理](https://course.rs/advance/errors.html)。 +Go 语言为人诟病的其中一点就是 **_if err != nil {}_** 的大量使用,缺乏一些程序设计的美感,不过我倒是觉得这种简单的方式也有其好处,就是阅读代码时的流畅感很强,你不需要过多的思考各种语法是什么意思。与 Go 语言不同,Rust 博采众家之长,实现了颇具自身色彩的返回值和错误处理体系,本章我们就高屋建瓴地来学习,更加深入的讲解见[错误处理](https://course.rs/advance/errors.html)。 ## Rust 的错误哲学 + 错误对于软件来说是不可避免的,因此一门优秀的编程语言必须有其完整的错误处理哲学。在很多情况下,Rust 需要你承认自己的代码可能会出错,并提前采取行动,来处理这些错误。 Rust 中的错误主要分为两类: + - **可恢复错误**,通常用于从系统全局角度来看可以接受的错误,例如处理用户的访问、操作等错误,这些错误只会影响某个用户自身的操作进程,而不会对系统的全局稳定性产生影响 - **不可恢复错误**,刚好相反,该错误通常是全局性或者系统性的错误,例如数组越界访问,系统启动时发生了影响启动流程的错误等等,这些错误的影响往往对于系统来说是致命的 diff --git a/src/basic/result-error/panic.md b/src/basic/result-error/panic.md index 5c32a40c..4aa1fc67 100644 --- a/src/basic/result-error/panic.md +++ b/src/basic/result-error/panic.md @@ -1,16 +1,19 @@ -# panic深入剖析 +# panic 深入剖析 在正式开始之前,先来思考一个问题:假设我们想要从文件读取数据,如果失败,你有没有好的办法通知调用者为何失败?如果成功,你有没有好的办法把读取的结果返还给调用者? -## panic!与不可恢复错误 +## panic! 与不可恢复错误 + 上面的问题在真实场景会经常遇到,其实处理起来挺复杂的,让我们先做一个假设:文件读取操作发生在系统启动阶段。那么可以轻易得出一个结论,一旦文件读取失败,那么系统启动也将失败,这意味着该失败是不可恢复的错误,无论是因为文件不存在还是操作系统硬盘的问题,这些只是错误的原因不同,但是归根到底都是不可恢复的错误(梳理清楚当前场景的错误类型非常重要)。 -既然是不可恢复错误,那么一旦发生,只需让程序崩溃即可。对此,Rust 为我们提供了 `panic!` 宏,当调用执行该宏时,**程序会打印出一个错误信息,展开报错点往前的函数调用堆栈,最后退出程序**。 +既然是不可恢复错误,那么一旦发生,只需让程序崩溃即可。对此,Rust 为我们提供了 `panic!` 宏,当调用执行该宏时,**程序会打印出一个错误信息,展开报错点往前的函数调用堆栈,最后退出程序**。 + +切记,一定是不可恢复的错误,才调用 `panic!` 处理,你总不想系统仅仅因为用户随便传入一个非法参数就崩溃吧?所以,**只有当你不知道该如何处理时,再去调用 panic!**. -切记,一定是不可恢复的错误,才调用 `panic!` 处理,你总不想系统仅仅因为用户随便传入一个非法参数就崩溃吧?所以,**只有当你不知道该如何处理时,再去调用panic!**. +## 调用 panic! -## 调用panic! 首先,来调用一下 `panic!`,这里使用了最简单的代码实现,实际上你在程序的任何地方都可以这样调用: + ```rust fn main() { panic!("crash and burn"); @@ -18,6 +21,7 @@ fn main() { ``` 运行后输出: + ```console thread 'main' panicked at 'crash and burn', src/main.rs:2:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace @@ -25,15 +29,17 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace 以上信息包含了两条重要信息: -- `main` 函数所在的线程崩溃了,发生的代码位置是 `src/main.rs` 中的第2行第5个字符(去除该行前面的空字符) +- `main` 函数所在的线程崩溃了,发生的代码位置是 `src/main.rs` 中的第 2 行第 5 个字符(去除该行前面的空字符) - 在使用时加上一个环境变量可以获取更详细的栈展开信息: - - Linux/macOS 等UNIX系统: `RUST_BACKTRACE=1 cargo run` - - Windows 系统(PowerShell): `$env:RUST_BACKTRACE=1 ; cargo run` + - Linux/macOS 等 UNIX 系统: `RUST_BACKTRACE=1 cargo run` + - Windows 系统(PowerShell): `$env:RUST_BACKTRACE=1 ; cargo run` 下面让我们针对第二点进行详细展开讲解。 -## backtrace栈展开 +## backtrace 栈展开 + 在真实场景中,错误往往涉及到很长的调用链甚至会深入第三方库,如果没有栈展开技术,错误将难以跟踪处理,下面我们来看一个真实的崩溃例子: + ```rust fn main() { let v = vec![1, 2, 3]; @@ -41,19 +47,22 @@ fn main() { v[99]; } ``` + 上面的代码很简单,数组只有 `3` 个元素,我们却尝试去访问它的第 `100` 号元素(数组索引从 `0` 开始),那自然会崩溃。 我们的读者里不乏正义之士,此时肯定要质疑,一个简单的数组越界访问,为何要直接让程序崩溃?是不是有些小题大作了? -如果有过C语言的经验,即使你越界了,问题不大,我依然尝试去访问,至于这个值是不是你想要的(`100` 号内存地址也有可能有值,只不过是其它变量或者程序的!),抱歉,不归我管,我只负责取,你要负责管理好自己的索引访问范围。上面这种情况被称为**缓冲区溢出**,并可能会导致安全漏洞,例如攻击者可以通过索引来访问到数组后面不被允许的数据。 +如果有过 C 语言的经验,即使你越界了,问题不大,我依然尝试去访问,至于这个值是不是你想要的(`100` 号内存地址也有可能有值,只不过是其它变量或者程序的!),抱歉,不归我管,我只负责取,你要负责管理好自己的索引访问范围。上面这种情况被称为**缓冲区溢出**,并可能会导致安全漏洞,例如攻击者可以通过索引来访问到数组后面不被允许的数据。 + +说实话,我宁愿程序崩溃,为什么?当你取到了一个不属于你的值,这在很多时候会导致程序上的逻辑 BUG! 有编程经验的人都知道这种逻辑上的 BUG 是多么难被发现和修复!因此程序直接崩溃,然后告诉我们问题发生的位置,最后我们对此进行修复,这才是最合理的软件开发流程,而不是把问题藏着掖着: -说实话,我宁愿程序崩溃,为什么?当你取到了一个不属于你的值,这在很多时候会导致程序上的逻辑bug! 有编程经验的人都知道这种逻辑上的 BUG 是多么难被发现和修复!因此程序直接崩溃,然后告诉我们问题发生的位置,最后我们对此进行修复,这才是最合理的软件开发流程,而不是把问题藏着掖着: ```console 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 ``` 好的,现在成功知道问题发生的位置,但是如果我们想知道该问题之前经过了哪些调用环节,该怎么办?那就按照提示使用 `RUST_BACKTRACE=1 cargo run` 或 `$env:RUST_BACKTRACE=1 ; cargo run` 来再一次运行程序: + ```console thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5 stack backtrace: @@ -80,50 +89,59 @@ note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose bac 要获取到栈回溯信息,你还需要开启 `debug` 标志,该标志在使用 `cargo run` 或者 `cargo build` 时自动开启(这两个操作默认是 `Debug` 运行方式)。同时,栈展开信息在不同操作系统或者 Rust 版本上也所有不同。 -## panic时的两种终止方式 -当出现 `panic!` 时,程序提供了两种方式来处理终止流程: **栈展开** 和 **直接终止**。 +## panic 时的两种终止方式 + +当出现 `panic!` 时,程序提供了两种方式来处理终止流程:**栈展开**和**直接终止**。 其中,默认的方式就是 `栈展开`,这意味着 Rust 会回溯栈上数据和函数调用,因此也意味着更多的善后工作,好处是可以给出充分的报错信息和栈调用信息,便于事后的问题复盘。`直接终止`,顾名思义,不清理数据就直接退出程序,善后工作交与操作系统来负责。 对于绝大多数用户,使用默认选择是最好的,但是当你关心最终编译出的二进制可执行文件大小时,那么可以尝试去使用直接终止的方式,例如下面的配置修改 `Cargo.toml` 文件,实现在 [`release`](../first-try/cargo.md#手动编译和运行项目) 模式下遇到 `panic` 直接终止: + ```rust [profile.release] panic = 'abort' ``` ## 线程 `panic` 后,程序是否会终止? + 长话短说,如果是 `main` 线程,则程序会终止,如果是其它子线程,该线程会终止,但是不会影响 `main` 线程。因此,尽量不要在 `main` 线程中做太多任务,将这些任务交由子线程去做,就算子线程 `panic` 也不会导致整个程序的结束。 -具体解析见 [panic原理剖析](#panic原理剖析)。 +具体解析见 [panic 原理剖析](#panic-原理剖析)。 -## 何时该使用panic! +## 何时该使用 panic! 下面让我们大概罗列下何时适合使用 `panic`,也许经过之前的学习,你已经能够对 `panic` 的使用有了自己的看法,但是我们还是会罗列一些常见的用法来加深你的理解。 先来一点背景知识,在前面章节我们粗略讲过 `Result` 这个枚举类型,它是用来表示函数的返回结果: + ```rust enum Result { Ok(T), Err(E), } ``` + 当没有错误发生时,函数返回一个用 `Result` 类型包裹的值 `Ok(T)`,当错误时,返回一个 `Err(E)`。对于 `Result` 返回我们有很多处理方法,最简单粗暴的就是 `unwrap` 和 `expect`,这两个函数非常类似,我们以 `unwrap` 举例: + ```rust use std::net::IpAddr; let home: IpAddr = "127.0.0.1".parse().unwrap(); ``` -上面的 `parse` 方法试图将字符串 `"127.0.0.1" `解析为一个IP地址类型 `IpAddr`,它返回一个 `Result` 类型,如果解析成功,则把 `Ok(IpAddr)` 中的值赋给 `home`,如果失败,则不处理 `Err(E)`,而是直接 `panic`。 +上面的 `parse` 方法试图将字符串 `"127.0.0.1" `解析为一个 IP 地址类型 `IpAddr`,它返回一个 `Result` 类型,如果解析成功,则把 `Ok(IpAddr)` 中的值赋给 `home`,如果失败,则不处理 `Err(E)`,而是直接 `panic`。 因此 `unwrap` 简而言之:成功则返回值,失败则 `panic`,总之不进行任何错误处理。 #### 示例、原型、测试 + 这几个场景下,需要快速地搭建代码,错误处理会拖慢编码的速度,也不是特别有必要,因此通过 `unwrap`、`expect` 等方法来处理是最快的。 同时,当我们回头准备做错误处理时,可以全局搜索这些方法,不遗漏地进行替换。 -#### 你确切的知道你的程序是正确时,可以使用panic +#### 你确切的知道你的程序是正确时,可以使用 panic + 因为 `panic` 的触发方式比错误处理要简单,因此可以让代码更清晰,可读性也更加好,当我们的代码注定是正确时,你可以用 `unwrap` 等方法直接进行处理,反正也不可能 `panic` : + ```rust use std::net::IpAddr; let home: IpAddr = "127.0.0.1".parse().unwrap(); @@ -134,20 +152,20 @@ let home: IpAddr = "127.0.0.1".parse().unwrap(); 当然,如果该字符串是来自于用户输入,那在实际项目中,就必须用错误处理的方式,而不是 `unwrap`,否则你的程序一天要崩溃几十万次吧! #### 可能导致全局有害状态时 + 有害状态大概分为几类: - 非预期的错误 - 后续代码的运行会受到显著影响 - 内存安全的问题 -当错误预期会出现时,返回一个错误较为合适,例如解析器接收到格式错误的数据,HTTP请求接收到错误的参数甚至该请求内的任何错误(不会导致整个程序有问题,只影响该次请求)。 **因为错误是可预期的,因此也是可以处理的**。 +当错误预期会出现时,返回一个错误较为合适,例如解析器接收到格式错误的数据,HTTP 请求接收到错误的参数甚至该请求内的任何错误(不会导致整个程序有问题,只影响该次请求)。**因为错误是可预期的,因此也是可以处理的**。 当启动时某个流程发生了错误,对后续代码的运行造成了影响,那么就应该使用 `panic`,而不是处理错误后继续运行,当然你可以通过重试的方式来继续。 上面提到过,数组访问越界,就要 `panic` 的原因,这个就是属于内存安全的范畴,一旦内存访问不安全,那么我们就无法保证自己的程序会怎么运行下去,也无法保证逻辑和数据的正确性。 - -## panic原理剖析 +## panic 原理剖析 本来不想写这块儿内容,因为真的难写,但是转念一想,既然号称圣经,那么本书就得与众不同,避重就轻显然不是该有的态度。 @@ -161,4 +179,3 @@ let home: IpAddr = "127.0.0.1".parse().unwrap(); 还有一种情况,在展开过程中,如果展开本身 `panic` 了,那展开线程会终止,展开也随之停止。 一旦线程展开被终止或者完成,最终的输出结果是取决于哪个线程 `panic`:对于 `main` 线程,操作系统提供的终止功能 `core::intrinsics::abort()` 会被调用,最终结束当前的 `panic` 进程;如果是其它子线程,那么子线程就会简单的终止,同时信息会在稍后通过 `std::thread::join()` 进行收集。 - diff --git a/src/basic/result-error/result.md b/src/basic/result-error/result.md index 640fe03b..73db59e1 100644 --- a/src/basic/result-error/result.md +++ b/src/basic/result-error/result.md @@ -1,16 +1,20 @@ -# 可恢复的错误Result +# 可恢复的错误 Result + 还记得上一节中,提到的关于文件读取的思考题吧?当时我们解决了读取文件时遇到不可恢复错误该怎么处理的问题,现在来看看,读取过程中,正常返回和遇到可以恢复的错误时该如何处理。 假设,我们有一台消息服务器,每个用户都通过 websocket 连接到该服务器来接收和发送消息,该过程就涉及到 socket 文件的读写,那么此时,如果一个用户的读写发生了错误,显然不能直接 `panic`,否则服务器会直接崩溃,所有用户都会断开连接,因此我们需要一种更温和的错误处理方式:`Result`。 之前章节有提到过,`Result` 是一个枚举类型,定义如下: + ```rust enum Result { Ok(T), Err(E), } ``` + 泛型参数 `T` 代表成功时存入的正确值的类型,存放方式是 `Ok(T)`,`E` 代表错误是存入的错误值,存放方式是 `Err(E)`,枯燥的讲解永远不及代码生动准确,因此先来看下打开文件的例子: + ```rust use std::fs::File; @@ -18,19 +22,23 @@ fn main() { let f = File::open("hello.txt"); } ``` + 以上 `File::open` 返回一个 `Result` 类型,那么问题来了: > #### 如何获知变量类型或者函数的返回类型 -> +> > 有几种常用的方式,此处更推荐第二种方法: -> +> > - 第一种是查询标准库或者三方库文档,搜索 `File`,然后找到它的 `open` 方法 > - 在 [Rust IDE](../../first-try/editor.md) 章节,我们推荐了 `VSCode` IDE 和 `rust-analyzer` 插件,如果你成功安装的话,那么就可以在 `VSCode` 中很方便的通过代码跳转的方式查看代码,同时 `rust-analyzer` 插件还会对代码中的类型进行标注,非常方便好用! > - 你还可以尝试故意标记一个错误的类型,然后让编译器告诉你: + ```rust let f: u32 = File::open("hello.txt"); ``` + 错误提示如下: + ```console error[E0308]: mismatched types --> src/main.rs:4:18 @@ -48,6 +56,7 @@ error[E0308]: mismatched types 别慌,其实很简单,首先 `Result` 本身是定义在 `std::result` 中的,但是因为 `Result` 很常用,所以就被包含在了 [`prelude`](../../appendix/prelude.md) 中(将常用的东东提前引入到当前作用域内),因此无需手动引入 `std::result::Result`,那么返回类型可以简化为 `Result`,你看看是不是很像标准的 `Result` 枚举定义?只不过 `T` 被替换成了具体的类型 `std::fs::File`,是一个文件句柄类型,`E` 被替换成 `std::io::Error`,是一个 IO 错误类型. 这个返回值类型说明 `File::open` 调用如果成功则返回一个可以进行读写的文件句柄,如果失败,则返回一个 IO 错误:文件不存在或者没有访问文件的权限等。总之 `File::open` 需要一个方式告知调用者是成功还是失败,并同时返回具体的文件句柄(成功)或错误信息(失败),万幸的是,这些信息可以通过 `Result` 枚举提供: + ```rust use std::fs::File; @@ -65,11 +74,12 @@ fn main() { 代码很清晰,对打开文件后的 `Result` 类型进行匹配取值,如果是成功,则将 `Ok(file)` 中存放的的文件句柄 `file` 赋值给 `f`,如果失败,则将 `Err(error)` 中存放的错误信息 `error` 使用 `panic` 抛出来,进而结束程序,这非常符合上文提到过的 `panic` 使用场景。 - 好吧,也没有那么合理 :) ## 对返回的错误进行处理 + 直接 `panic` 还是过于粗暴,因为实际上 IO 的错误有很多种,我们需要对部分错误进行特殊处理,而不是所有错误都直接崩溃: + ```rust use std::fs::File; use std::io::ErrorKind; @@ -98,11 +108,12 @@ fn main() { 虽然很清晰,但是代码还是有些啰嗦,我们会在[简化错误处理](../../advance/errors.md)一章重点讲述如何写出更优雅的错误。 ## 失败就 panic: unwrap 和 expect + 上一节中,已经看到过这两兄弟的简单介绍,这里再来回顾下。 在不需要处理错误的场景,例如写原型、示例时,我们不想使用 `match` 去匹配 `Result ` 以获取其中的 `T` 值,因为 `match` 的穷尽匹配特性,你总要去处理下 `Err` 分支。那么有没有办法简化这个过程?有,答案就是 `unwrap` 和 `expect`。 -它们的作用就是,如果返回成功,就将 `Ok(T)` 中的值取出来,如果失败,就直接 `panic`,真的勇士绝不多BB,直接崩溃。 +它们的作用就是,如果返回成功,就将 `Ok(T)` 中的值取出来,如果失败,就直接 `panic`,真的勇士绝不多 BB,直接崩溃。 ```rust use std::fs::File; @@ -112,7 +123,7 @@ fn main() { } ``` -如果调用这段代码时 *hello.txt* 文件不存在,那么 `unwrap` 就将直接 `panic`: +如果调用这段代码时 _hello.txt_ 文件不存在,那么 `unwrap` 就将直接 `panic`: ```console 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 @@ -120,6 +131,7 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace ``` `expect` 跟 `unwrap` 很像,也是遇到错误直接 `panic`, 但是会带上自定义的错误提示信息,相当于重载了错误打印的函数: + ```rust use std::fs::File; @@ -127,6 +139,7 @@ fn main() { let f = File::open("hello.txt").expect("Failed to open hello.txt"); } ``` + 报错如下: ```console @@ -137,9 +150,11 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace 可以看出,`expect` 相比 `unwrap` 能提供更精确的错误信息,在有些场景也会更加实用。 ## 传播错误 + 咱们的程序几乎不太可能只有 `A->B` 形式的函数调用,一个设计良好的程序,一个功能涉及十几层的函数调用都有可能。而错误处理也往往不是哪里调用出错,就在哪里处理,实际应用中,大概率会把错误层层上传然后交给调用链的上游函数进行处理,错误传播将极为常见。 例如以下函数从文件中读取用户名,然后将结果进行返回: + ```rust use std::fs::File; use std::io::{self, Read}; @@ -176,7 +191,9 @@ fn read_username_from_file() -> Result { 但是上面的代码也有自己的问题,那就是太长了(优秀的程序员身上的优点极多,其中最大的优点就是*懒*),我自认为也有那么一点点优秀,因此见不到这么啰嗦的代码,下面咱们来讲讲如何简化它。 ### 传播界的大明星: ? + 大明星出场,必需得有排面,来看看 `?` 的排面: + ```rust use std::fs::File; use std::io; @@ -193,6 +210,7 @@ fn read_username_from_file() -> Result { 看到没,这就是排面,相比前面的 `match` 处理错误的函数,代码直接减少了一半不止,但是,一山更比一山难,看不懂啊! 其实 `?` 就是一个宏,它的作用跟上面的 `match` 几乎一模一样: + ```rust let mut f = match f { // 打开文件成功,将file句柄赋值给f @@ -201,6 +219,7 @@ let mut f = match f { Err(e) => return Err(e), }; ``` + 如果结果是 `Ok(T)`,则把 `T` 赋值给 `f`,如果结果是 `Err(E)`,则返回该错误,所以 `?` 特别适合用来传播错误。 虽然 `?` 和 `match` 功能一致,但是事实上 `?` 会更胜一筹。何解? @@ -208,12 +227,14 @@ let mut f = match f { 想象一下,一个设计良好的系统中,肯定有自定义的错误特征,错误之间很可能会存在上下级关系,例如标准库中的 `std::io::Error `和 `std::error::Error`,前者是 IO 相关的错误结构体,后者是一个最最通用的标准错误特征,同时前者实现了后者,因此 `std::io::Error` 可以转换为 `std:error::Error`。 明白了以上的错误转换,`?` 的更胜一筹就很好理解了,它可以自动进行类型提升(转换): + ```rust fn open_file() -> Result> { let mut f = File::open("hello.txt")?; Ok(f) } ``` + 上面代码中 `File::open` 报错时返回的错误是 `std::io::Error` 类型,但是 `open_file` 函数返回的错误类型是 `std::error::Error` 的特征对象,可以看到一个错误类型通过 `?` 返回后,变成了另一个错误类型,这就是 `?` 的神奇之处。 根本原因是在于标准库中定义的 `From` 特征,该特征有一个方法 `from`,用于把一个类型转成另外一个类型,`?` 可以自动调用该方法,然后进行隐式类型转换。因此只要函数返回的错误 `ReturnError` 实现了 `From` 特征,那么 `?` 就会自动把 `OtherError` 转换为 `ReturnError`。 @@ -221,6 +242,7 @@ fn open_file() -> Result> { 这种转换非常好用,意味着你可以用一个大而全的 `ReturnError` 来覆盖所有错误类型,只需要为各种子错误类型实现这种转换即可。 强中自有强中手,一码更比一码短: + ```rust use std::fs::File; use std::io; @@ -234,9 +256,11 @@ fn read_username_from_file() -> Result { Ok(s) } ``` + 瞧见没? `?` 还能实现链式调用,`File::open` 遇到错误就返回,没有错误就将 `Ok` 中的值取出来用于下一个方法调用,简直太精妙了,从 Go 语言过来的我,内心狂喜(其实学 Rust 的苦和痛我才不会告诉你们)。 不仅有更强,还要有最强,我不信还有人比我更短(不要误解): + ```rust use std::fs; use std::io; @@ -249,8 +273,10 @@ fn read_username_from_file() -> Result { 从文件读取数据到字符串中,是比较常见的操作,因此 Rust 标准库为我们提供了 `fs::read_to_string` 函数,该函数内部会打开一个文件、创建 `String`、读取文件内容最后写入字符串并返回,因为该函数其实与本章讲的内容关系不大,因此放在最后来讲,其实只是我想震你们一下 :) -#### ? 用于Option的返回 +#### ? 用于 Option 的返回 + `?` 不仅仅可以用于 `Result` 的传播,还能用于 `Option` 的传播,再来回忆下 `Option` 的定义: + ```rust pub enum Option { Some(T), @@ -259,42 +285,53 @@ pub enum Option { ``` `Result` 通过 `?` 返回错误,那么 `Option` 就通过 `?` 返回 `None`: + ```rust fn first(arr: &[i32]) -> Option<&i32> { let v = arr.get(0)?; Some(v) } ``` + 上面的函数中,`arr.get` 返回一个 `Option<&i32>` 类型,因为 `?` 的使用,如果 `get` 的结果是 `None`,则直接返回 `None`,如果是 `Some(&i32)`,则把里面的值赋给 `v`。 其实这个函数有些画蛇添足,我们完全可以写出更简单的版本: + ```rust fn first(arr: &[i32]) -> Option<&i32> { arr.get(0) } ``` + 有一句话怎么说?没有需求,制造需求也要上……大家别跟我学习,这是软件开发大忌。只能用代码洗洗眼了: + ```rust fn last_char_of_first_line(text: &str) -> Option { text.lines().next()?.chars().last() } ``` + 上面代码展示了在链式调用中使用 `?` 提前返回 `None` 的用法, `.next` 方法返回的是 `Option` 类型:如果返回 `Some(&str)`,那么继续调用 `chars` 方法,如果返回 `None`,则直接从整个函数中返回 `None`,不再继续进行链式调用。 #### 新手用 ? 常会犯的错误 + 初学者在用 `?` 时,老是会犯错,例如写出这样的代码: + ```rust fn first(arr: &[i32]) -> Option<&i32> { arr.get(0)? } ``` + 这段代码无法通过编译,切记:`?` 操作符需要一个变量来承载正确的值,这个函数只会返回 `Some(&i32)` 或者 `None`,只有错误值能直接返回,正确的值不行,所以如果数组中存在 0 号元素,那么函数第二行使用 `?` 后的返回类型为 `&i32` 而不是 `Some(&i32)`。因此 `?` 只能用于以下形式: - `let v = xxx()?;` - `xxx()?.yyy()?;` -#### 带返回值的main函数 +#### 带返回值的 main 函数 + 在了解了 `?` 的使用限制后,这段代码你很容易看出它无法编译: + ```rust use std::fs::File; @@ -302,9 +339,11 @@ fn main() { let f = File::open("hello.txt")?; } ``` + 因为 `?` 要求 `Result` 形式的返回值,而 `main` 函数的返回是 `()`,因此无法满足,那是不是就无解了呢? 实际上 Rust 还支持另外一种形式的 `main` 函数: + ```rust use std::error::Error; use std::fs::File; @@ -321,7 +360,9 @@ fn main() -> Result<(), Box> { 至于 `main` 函数可以有多种返回值,那是因为实现了 [std::process::Termination](https://doc.rust-lang.org/std/process/trait.Termination.html) 特征,目前为止该特征还没进入稳定版 Rust 中,也许未来你可以为自己的类型实现该特征! #### try! + 在 `?` 横空出世之前( Rust 1.13 ),Rust 开发者还可以使用 `try!` 来处理错误,该宏的大致定义如下: + ```rust macro_rules! try { ($e:expr) => (match $e { @@ -332,6 +373,7 @@ macro_rules! try { ``` 简单看一下与 `?` 的对比: + ```rust // `?` let x = function_with_error()?; // 若返回 Err, 则立刻返回;若返回 Ok(255),则将 x 的值设置为 255 @@ -340,7 +382,6 @@ let x = function_with_error()?; // 若返回 Err, 则立刻返回;若返回 Ok let x = try!(function_with_error()); ``` -可以看出 `?` 的优势非常明显,何况 `?` 还能做链式调用。 +可以看出 `?` 的优势非常明显,何况 `?` 还能做链式调用。 总之,`try!` 作为前浪已经死在了沙滩上,**在当前版本中,我们要尽量避免使用 try!**。 - diff --git a/src/basic/trait/advance-trait.md b/src/basic/trait/advance-trait.md index 23fb89b5..03435f63 100644 --- a/src/basic/trait/advance-trait.md +++ b/src/basic/trait/advance-trait.md @@ -1,11 +1,13 @@ # 深入了解特征 -特征之于 Rust 更甚于接口之于其他语言,因此特征在 Rust 中很重要也相对较为复杂,我们决定把特征分为两篇进行介绍,[第一篇](./trait.md)在之前已经讲过,现在就是第二篇:关于特征的进阶篇,会讲述一些不常用到但是你该了解的特性。 +特征之于 Rust 更甚于接口之于其他语言,因此特征在 Rust 中很重要也相对较为复杂,我们决定把特征分为两篇进行介绍,[第一篇](https://course.rs/basic/trait/trait.html)在之前已经讲过,现在就是第二篇:关于特征的进阶篇,会讲述一些不常用到但是你该了解的特性。 ## 关联类型 -在方法一章中,我们讲到了[关联函数](../method.md#关联函数),但是实际上关联类型和关联函数并没有任何交集,虽然它们的名字有一半的交集。 + +在方法一章中,我们讲到了[关联函数](https://course.rs/basic/method#关联函数),但是实际上关联类型和关联函数并没有任何交集,虽然它们的名字有一半的交集。 关联类型是在特征定义的语句块中,申明一个自定义类型,这样就可以在特征的方法签名中使用该类型: + ```rust pub trait Iterator { type Item; @@ -18,7 +20,8 @@ pub trait Iterator { 同时,`next` 方法也返回了一个 `Item` 类型,不过使用 `Option` 枚举进行了包裹,假如迭代器中的值是 `i32` 类型,那么调用 `next` 方法就将获取一个 `Option` 的值。 -还记得 `Self` 吧?在之前的章节[提到过](./trait-object#Self与self), `Self` 用来指代当前的特征实例,那么 `Self::Item` 就用来指代特征实例中具体的 `Item` 类型: +还记得 `Self` 吧?在之前的章节[提到过](https://course.rs/basic/trait/trait-object#self与self), `Self` 用来指代当前的特征实例,那么 `Self::Item` 就用来指代特征实例中具体的 `Item` 类型: + ```rust impl Iterator for Counter { type Item = u32; @@ -32,6 +35,7 @@ impl Iterator for Counter { 在上述代码中,我们为 `Counter` 类型实现了 `Iterator` 特征,那么 `Self` 就是当前的 `Iterator` 特征对象, `Item` 就是 `u32` 类型。 聪明的读者之所以聪明,是因为你们喜欢联想和举一反三,同时你们也喜欢提问:为何不用泛型,例如如下代码: + ```rust pub trait Iterator { fn next(&mut self) -> Option; @@ -39,6 +43,7 @@ pub trait Iterator { ``` 答案其实很简单,为了代码的可读性,当你使用了泛型后,你需要在所有地方都写 `Iterator`,而使用了关联类型,你只需要写 `Iterator`,当类型定义复杂时,这种写法可以极大的增加可读性: + ```rust pub trait CacheableItem: Clone + Default + fmt::Debug + Decodable + Encodable { type Address: AsRef<[u8]> + Clone + fmt::Debug + Eq + Hash; @@ -49,6 +54,7 @@ pub trait CacheableItem: Clone + Default + fmt::Debug + Decodable + Encodable { 例如上面的代码,`Address` 的写法自然远比 `AsRef<[u8]> + Clone + fmt::Debug + Eq + Hash` 要简单的多,而且含义清晰。 再例如,如果使用泛型,你将得到以下的代码: + ```rust trait Container { fn contains(&self,a: A,b: B) -> bool; @@ -60,6 +66,7 @@ fn difference(container: &C) -> i32 ``` 可以看到,由于使用了泛型,导致函数头部也必须增加泛型的声明,而使用关联类型,将得到可读性好得多的代码: + ```rust trait Container{ type A; @@ -73,6 +80,7 @@ fn difference(container: &C) {} ## 默认泛型类型参数 当使用泛型类型参数时,可以为其指定一个默认的具体类型,例如标准库中的 `std::ops::Add` 特征: + ```rust trait Add { type Output; @@ -80,9 +88,11 @@ trait Add { fn add(self, rhs: RHS) -> Self::Output; } ``` + 它有一个泛型参数 `RHS`,但是与我们以往的用法不同,这里它给 `RHS` 一个默认值,也就是当用户不指定 `RHS` 时,默认使用两个同样类型的值进行相加,然后返回一个关联类型 `Output`。 可能上面那段不太好理解,下面我们用代码来举例: + ```rust use std::ops::Add; @@ -108,11 +118,13 @@ fn main() { Point { x: 3, y: 3 }); } ``` + 上面的代码主要干了一件事,就是为 `Point` 结构体提供 `+` 的能力,这就是**运算符重载**,不过 Rust 并不支持创建自定义运算符,你也无法为所有运算符进行重载,目前来说,只有定义在 `std::ops` 中的运算符才能进行重载。 跟 `+` 对应的特征是 `std::ops::Add`,我们在之前也看过它的定义 `trait Add`,但是上面的例子中并没有为 `Point` 实现 `Add` 特征,而是实现了 `Add` 特征(没有默认泛型类型参数),这意味着我们使用了 `RHS` 的默认类型,也就是 `Self`。换句话说,我们这里定义的是两个相同的 `Point` 类型相加,因此无需指定 `RHS`。 与上面的例子相反,下面的例子,我们来创建两个不同类型的相加: + ```rust use std::ops::Add; @@ -131,6 +143,7 @@ impl Add for Millimeters { 这里,是进行 `Millimeters + Meters` 两种数据类型的 `+` 操作,因此此时不能再使用默认的 `RHS`,否则就会变成 `Millimeters + Millimeters` 的形式。使用 `Add` 可以将 `RHS` 指定为 `Meters`,那么 `fn add(self, rhs: RHS)` 自然而言的变成了 `Millimeters` 和 `Meters` 的相加。 默认类型参数主要用于两个方面: + 1. 减少实现的样板代码 2. 扩展类型但是无需大幅修改现有的代码 @@ -141,7 +154,9 @@ impl Add for Millimeters { 归根到底,默认泛型参数,是有用的,但是大多数情况下,咱们确实用不到,当需要用到时,大家再回头来查阅本章即可,**手上有剑,心中不慌**。 ## 调用同名的方法 + 不同特征拥有同名的方法是很正常的事情,你没有任何办法阻止这一点;甚至除了特征上的同名方法外,在你的类型上,也有同名方法: + ```rust trait Pilot { fn fly(&self); @@ -177,17 +192,22 @@ impl Human { 既然代码已经不可更改,那下面我们来讲讲该如何调用这些 `fly` 方法。 #### 优先调用类型上的方法 + 当调用 `Human` 实例的 `fly` 时,编译器默认调用该类型中定义的方法: + ```rust fn main() { let person = Human; person.fly(); } ``` + 这段代码会打印 `*waving arms furiously*`,说明直接调用了类型上定义的方法。 #### 调用特征上的方法 + 为了能够调用两个特征的方法,需要使用显式调用的语法: + ```rust fn main() { let person = Human; @@ -198,6 +218,7 @@ fn main() { ``` 运行后依次输出: + ```console This is your captain speaking. Up! @@ -209,6 +230,7 @@ Up! 这个时候问题又来了,如果方法没有 `self` 参数呢?稍等,估计有读者会问:还有方法没有 `self` 参数?看到这个疑问,作者的眼泪不禁流了下来,大明湖畔的[关联函数](../method.md#关联函数),你还记得嘛? 但是成年人的世界,就算再伤心,事还得做,咱们继续: + ```rust trait Animal { fn baby_name() -> String; @@ -236,6 +258,7 @@ fn main() { 就像人类妈妈会给自己的宝宝起爱称一样,狗妈妈也会。狗妈妈称呼自己的宝宝为**Spot**,其它动物称呼狗宝宝为**puppy**,这个时候假如有动物不知道该称如何呼狗宝宝,它需要查询一下。 `Dog::baby_name()` 的调用方式显然不行,因为这只是狗妈妈对宝宝的爱称,可能你会想到通过下面的方式查询其他动物对狗狗的称呼: + ```rust fn main() { println!("A baby dog is called a {}", Animal::baby_name()); @@ -243,6 +266,7 @@ fn main() { ``` 铛铛,无情报错了: + ```rust error[E0283]: type annotations needed // 需要类型注释 --> src/main.rs:20:43 @@ -258,15 +282,19 @@ error[E0283]: type annotations needed // 需要类型注释 此时,就需要使用**完全限定语法**。 ##### 完全限定语法 + 完全限定语法是调用函数最为明确的方式: + ```rust fn main() { println!("A baby dog is called a {}", ::baby_name()); } ``` + 在尖括号中,通过 `as` 关键字,我们向 Rust 编译器提供了类型注解,也就是 `Animal` 就是 `Dog`,而不是其他动物,因此最终会调用 `impl Animal for Dog` 中的方法,获取到其它动物对狗宝宝的称呼:**puppy**。 言归正题,完全限定语法定义为: + ```rust ::function(receiver_if_method, next_arg, ...); ``` @@ -275,12 +303,12 @@ fn main() { 完全限定语法可以用于任何函数或方法调用,那么我们为何很少用到这个语法?原因是 Rust 编译器能根据上下文自动推导出调用的路径,因此大多数时候,我们都无需使用完全限定语法。只有当存在多个同名函数或方法,且 Rust 无法区分出你想调用的目标函数时,该用法才能真正有用武之地。 - - ## 特征定义中的特征约束 -有时,我们会需要让某个特征A能使用另一个特征B的功能(另一种形式的特征约束),这种情况下,不仅仅要为类型实现特征A,还要为类型实现特征B才行,这就是 `supertrait` (实在不知道该如何翻译,有大佬指导下嘛?) + +有时,我们会需要让某个特征 A 能使用另一个特征 B 的功能(另一种形式的特征约束),这种情况下,不仅仅要为类型实现特征 A,还要为类型实现特征 B 才行,这就是 `supertrait` (实在不知道该如何翻译,有大佬指导下嘛?) 例如有一个特征 `OutlinePrint`,它有一个方法,能够对当前的实现类型进行格式化输出: + ```rust use std::fmt::Display; @@ -300,6 +328,7 @@ trait OutlinePrint: Display { 等等,这里有一个眼熟的语法: `OutlinePrint: Display`,感觉很像之前讲过的**特征约束**,只不过用在了特征定义中而不是函数的参数中,是的,在某种意义上来说,这和特征约束非常类似,都用来说明一个特征需要实现另一个特征,这里就是:如果你想要实现 `OutlinePrint` 特征,首先你需要实现 `Display` 特征。 想象一下,假如没有这个特征约束,那么 `self.to_string` 还能够调用吗( `to_string` 方法会为实现 `Display` 特征的类型自动实现)?编译器肯定是不愿意的,会报错说当前作用域中找不到用于 `&Self` 类型的方法 `to_string` : + ```rust struct Point { x: i32, @@ -308,7 +337,9 @@ struct Point { impl OutlinePrint for Point {} ``` + 因为 `Point` 没有实现 `Display` 特征,会得到下面的报错: + ```console error[E0277]: the trait bound `Point: std::fmt::Display` is not satisfied --> src/main.rs:20:6 @@ -321,6 +352,7 @@ try using `:?` instead if you are using a format string ``` 既然我们有求于编译器,那只能选择满足它咯: + ```rust use std::fmt; @@ -335,9 +367,9 @@ impl fmt::Display for Point { ## 在外部类型上实现外部特征(newtype) - 在[特征](./trait#特征定义与实现的位置(孤儿规则))章节中,有提到孤儿规则,简单来说,就是特征或者类型必需至少有一个是本地的,才能在此类型上定义特征。 +在[特征](https://course.rs/basic/trait/trait#特征定义与实现的位置孤儿规则)章节中,有提到孤儿规则,简单来说,就是特征或者类型必需至少有一个是本地的,才能在此类型上定义特征。 -这里提供一个办法来绕过孤儿规则,那就是使用**newtype模式**,简而言之:就是为一个[元组结构体](../compound-type/struct.md#元组结构体)创建新类型。该元组结构体封装有一个字段,该字段就是希望实现特征的具体类型。 +这里提供一个办法来绕过孤儿规则,那就是使用**newtype 模式**,简而言之:就是为一个[元组结构体](https://course.rs/basic/compound-type/struct#元组结构体tuple-struct)创建新类型。该元组结构体封装有一个字段,该字段就是希望实现特征的具体类型。 该封装类型是本地的,因此我们可以为此类型实现外部的特征。 @@ -345,7 +377,6 @@ impl fmt::Display for Point { 下面来看一个例子,我们有一个动态数组类型: `Vec`,它定义在标准库中,还有一个特征 `Display`,它也定义在标准库中,如果没有 `newtype`,我们是无法为 `Vec` 实现 `Display` 的: - ```console error[E0117]: only traits defined in the current crate can be implemented for arbitrary types --> src/main.rs:5:1 @@ -384,7 +415,6 @@ fn main() { 类似的,任何数组上的方法,你都无法直接调用,需要先用 `self.0` 取出数组,然后再进行调用。 -当然,解决办法还是有的,要不怎么说 Rust 是极其强大灵活的编程语言!Rust 提供了一个特征叫 [`Deref`](../../advance/smart-pointer/deref.md),实现该特征后,可以自动做一层类似类型转换的操作,可以将 `Wrapper` 变成 `Vec` 来使用。这样就会像直接使用数组那样去使用 `Wrapper`,而无需为每一个操作都添加上 `self.0`。 +当然,解决办法还是有的,要不怎么说 Rust 是极其强大灵活的编程语言!Rust 提供了一个特征叫 [`Deref`](https://course.rs/advance/smart-pointer/deref.html),实现该特征后,可以自动做一层类似类型转换的操作,可以将 `Wrapper` 变成 `Vec` 来使用。这样就会像直接使用数组那样去使用 `Wrapper`,而无需为每一个操作都添加上 `self.0`。 同时,如果不想 `Wrapper` 暴漏底层数组的所有方法,我们还可以为 `Wrapper` 去重载这些方法,实现隐藏的目的。 - diff --git a/src/basic/trait/generic.md b/src/basic/trait/generic.md index 402bf426..d6d7a29d 100644 --- a/src/basic/trait/generic.md +++ b/src/basic/trait/generic.md @@ -1,9 +1,10 @@ -# 泛型Generics +# 泛型 Generics Go 语言在 2022 年,就要正式引入泛型,被视为在 1.0 版本后,语言特性发展迈出的一大步,为什么泛型这么重要?到底什么是泛型?Rust 的泛型有几种? 本章将一一为你讲解。 我们在编程中,经常有这样的需求:用同一功能的函数处理不同类型的数据,例如两个数的加法,无论是整数还是浮点数,甚至是自定义类型,都能进行支持。在不支持泛型的编程语言中,通常需要为每一种类型编写一个函数: + ```rust fn add_i8(a:i8, b:i8) -> i8 { a + b @@ -29,6 +30,7 @@ fn main() { 在编程的时候,我们经常利用多态。通俗的讲,多态就是好比坦克的炮管,既可以发射普通弹药,也可以发射制导炮弹(导弹),也可以发射贫铀穿甲弹,甚至发射子母弹,没有必要为每一种炮弹都在坦克上分别安装一个专用炮管,即使生产商愿意,炮手也不愿意,累死人啊。所以在编程开发中,我们也需要这样“通用的炮管”,这个“通用的炮管”就是多态。 实际上,泛型就是一种多态。泛型主要目的是为程序员提供编程的便利,减少代码的臃肿,同时可以极大地丰富语言本身的表达能力,为程序员提供了一个合适的炮管。想想,一个函数,可以代替几十个,甚至数百个函数,是一件多么让人兴奋的事情: + ```rust fn add(a:T, b:T) -> T { a + b @@ -40,20 +42,25 @@ fn main() { println!("add f64: {}", add(1.23, 1.23)); } ``` + 将之前的代码改成上面这样,就是 Rust 泛型的初印象,这段代码虽然很简洁,但是并不能编译通过,我们会在后面进行详细讲解,现在只要对泛型有个大概的印象即可。 ## 泛型详解 + 上面代码的 `T` 就是**泛型参数**,实际上在 Rust 中,泛型参数的名称你可以任意起,但是出于惯例,我们都用 `T` ( `T` 是 `type` 的首字母)来作为首选,这个名称越短越好,除非需要表达含义,否则一个字母是最完美的。 使用泛型参数,有一个先决条件,必需在使用前对其进行声明: + ```rust fn largest(list: &[T]) -> T { ``` -该泛型函数的作用是从列表中找出最大的值,其中列表中的元素类型为T。首先 `largest` 对泛型参数 `T` 进行了声明,然后才在函数参数中进行使用该泛型参数 `list: &[T]` (还记得 `&[T]` 类型吧?这是[数组切片](../compound-type/array#数组切片))。 + +该泛型函数的作用是从列表中找出最大的值,其中列表中的元素类型为 T。首先 `largest` 对泛型参数 `T` 进行了声明,然后才在函数参数中进行使用该泛型参数 `list: &[T]` (还记得 `&[T]` 类型吧?这是[数组切片](../compound-type/array#数组切片))。 总之,我们可以这样理解这个函数定义:函数 `largest` 有泛型类型 `T`,它有个参数 `list`,其类型是元素为 `T` 的数组切片,最后,该函数返回值的类型也是 `T`。 具体的泛型函数实现如下: + ```rust fn largest(list: &[T]) -> T { let mut largest = list[0]; @@ -81,6 +88,7 @@ fn main() { ``` 运行后报错: + ```console error[E0369]: binary operation `>` cannot be applied to type `T` // `>`操作符不能用于类型`T` --> src/main.rs:5:17 @@ -96,9 +104,10 @@ help: consider restricting type parameter `T` // 考虑对T进行类型上的限 | ++++++++++++++++++++++ ``` -因为T可以是任何类型,但不是所有的类型都能进行比较,因此上面的错误中,编译器建议我们给 `T` 添加一个类型限制:使用 `std::cmp::PartialOrd` 特征(Trait)对T进行限制,特征在下一节会详细介绍,现在你只要理解,该特征的目的就是让**类型实现可比较的功能**。 +因为 `T` 可以是任何类型,但不是所有的类型都能进行比较,因此上面的错误中,编译器建议我们给 `T` 添加一个类型限制:使用 `std::cmp::PartialOrd` 特征(Trait)对 `T` 进行限制,特征在下一节会详细介绍,现在你只要理解,该特征的目的就是让**类型实现可比较的功能**。 还记得我们一开始的 `add` 泛型函数吗?如果你运行它,会得到以下的报错: + ```console error[E0369]: cannot add `T` to `T` // 无法将 `T` 类型跟 `T` 类型进行相加 --> src/main.rs:2:7 @@ -110,20 +119,24 @@ error[E0369]: cannot add `T` to `T` // 无法将 `T` 类型跟 `T` 类型进行 | help: consider restricting type parameter `T` | -1 | fn add>(a:T, b:T) -> T { +1 | fn add>(a:T, b:T) -> T { | +++++++++++++++++++++++++++ ``` 同样的,不是所有 `T` 类型都能进行相加操作,因此我们需要用 `std::ops::Add` 对 `T` 进行限制: + ```rust fn add>(a:T, b:T) -> T { a + b } ``` + 进行如上修改后,就可以正常运行。 ## 结构体中使用泛型 + 结构体中的字段类型也可以用泛型来定义,下面代码定义了一个坐标点 `Point`,它可以存放任何类型的坐标值: + ```rust struct Point { x: T, @@ -137,10 +150,12 @@ fn main() { ``` 这里有两点需要特别的注意: + - **提前声明**,跟泛型函数定义类似,首先我们在使用泛型参数之前必需要进行声明 `Point`,接着就可以在结构体的字段类型中使用 `T` 来替代具体的类型 -- **x和y是相同的类型** +- **x 和 y 是相同的类型** 第二点非常重要,如果使用不同的类型,那么它会导致下面代码的报错: + ```rust struct Point { x: T, @@ -153,6 +168,7 @@ fn main() { ``` 错误如下: + ```console error[E0308]: mismatched types //类型不匹配 --> src/main.rs:7:28 @@ -165,6 +181,7 @@ error[E0308]: mismatched types //类型不匹配 当把 `1` 赋值给 `x` 时,变量 `p` 的 `T` 类型就被确定为整数类型,因此 `y` 也必须是整数类型,但是我们却给它赋予了浮点数,因此导致报错。 如果想让 `x` 和 `y` 即能类型相同,又能类型不同,需要使用不同的泛型参数: + ```rust struct Point { x: T, @@ -180,6 +197,7 @@ fn main() { ## 枚举中使用泛型 提到枚举类型,`Option` 永远是第一个应该被想起来的,在之前的章节中,它也多次出现: + ```rust enum Option { Some(T), @@ -190,6 +208,7 @@ enum Option { `Option` 是一个拥有泛型 `T` 的枚举类型,它第一个成员是 `Some(T)`,存放了一个类型为 `T` 的值。得益于泛型的引入,我们可以在任何一个需要返回值的函数中,去使用 `Option` 枚举类型来做为返回值,用于返回一个任意类型的值 `Some(T)`,或者没有值 `None`。 对于枚举而言,卧龙凤雏永远是绕不过去的存在:如果是 `Option` 是卧龙,那么 `Result` 就一定是凤雏,得两者可得天下: + ```rust enum Result { Ok(T), @@ -202,7 +221,9 @@ enum Result { 如果函数正常运行,则最后返回一个 `Ok(T)`,`T` 是函数具体的返回值类型,如果函数异常运行,则返回一个 `Err(E)`,`E` 是错误类型。例如打开一个文件:如果成功打开文件,则返回 `Ok(std::fs::File)`,因此 `T` 对应的是 `std::fs::File` 类型;而当打开文件时出现问题时,返回 `Err(std::io::Error)`,`E` 对应的就是 `std::io::Error` 类型。 ## 方法中使用泛型 + 上一章中,我们讲到什么是方法以及如何在结构体和枚举上定义方法。方法上也可以使用泛型: + ```rust struct Point { x: T, @@ -254,7 +275,9 @@ fn main() { 这个例子中,`T,U` 是定义在结构体 `Point` 上的泛型参数,`V,W` 是单独定义在方法 `mixup` 上的泛型参数,它们并不冲突,说白了,你可以理解为,一个是结构体泛型,一个是函数泛型。 #### 为具体的泛型类型实现方法 + 对于 `Point` 类型,你不仅能定义基于 `T` 的方法,还能针对特定的具体类型,进行方法定义: + ```rust impl Point { fn distance_from_origin(&self) -> f32 { @@ -262,16 +285,17 @@ impl Point { } } ``` + 这段代码意味着 `Point` 类型会有一个方法 `distance_from_origin`,而其他 `T` 不是 `f32` 类型的 `Point `实例则没有定义此方法。这个方法计算点实例与坐标`(0.0, 0.0)` 之间的距离,并使用了只能用于浮点型的数学运算符。 这样我们就能针对特定的泛型类型实现某个特定的方法,对于其它泛型类型则没有定义该方法。 - - ## const 泛型(Rust 1.51 版本引入的重要特性) + 在之前的泛型中,可以抽象为一句话:针对类型实现的泛型,所有的泛型都是为了抽象不同的类型,那有没有针对值的泛型?可能很多同学感觉很难理解,值怎么使用泛型?不急,我们先从数组讲起。 在[数组](../compound-type/array.md)那节,有提到过很重要的一点:`[i32; 2]` 和 `[i32; 3]` 是不同的数组类型,比如下面的代码: + ```rust fn display_array(arr: [i32; 3]) { println!("{:?}", arr); @@ -286,6 +310,7 @@ fn main() { ``` 运行后报错: + ```console error[E0308]: mismatched types // 类型不匹配 --> src/main.rs:10:19 @@ -298,6 +323,7 @@ error[E0308]: mismatched types // 类型不匹配 结合代码和报错,可以很清楚的看出,`[i32; 3]` 和 `[i32; 2]` 确实是两个完全不同的类型,因此无法用同一个函数调用。 首先,让我们修改代码,让 `display_array` 能打印任意长度的 `i32` 数组: + ```rust fn display_array(arr: &[i32]) { println!("{:?}", arr); @@ -310,6 +336,7 @@ fn main() { display_array(&arr); } ``` + 很简单,只要使用数组切片,然后传入 `arr` 的不可变引用即可。 接着,将 `i32` 改成所有类型的数组: @@ -327,11 +354,12 @@ fn main() { } ``` -也不难,唯一要注意的是需要对T加一个限制 `std::fmt::Debug`,该限制表明 `T` 可以用在 `println!("{:?}", arr)` 中,因为 `{:?}` 形式的格式化输出需要 `arr` 实现该特征。 +也不难,唯一要注意的是需要对 `T` 加一个限制 `std::fmt::Debug`,该限制表明 `T` 可以用在 `println!("{:?}", arr)` 中,因为 `{:?}` 形式的格式化输出需要 `arr` 实现该特征。 -通过引用,我们可以很轻松的解决处理任何类型数组的问题,但是如果在某些场景下引用不适宜用或者干脆不能用呢?你们知道为什么以前 Rust 的一些数组库,在使用的时候都限定长度不超过32吗?因为它们会为每个长度都单独实现一个函数,简直。。。毫无人性。难道没有什么办法可以解决这个问题吗? +通过引用,我们可以很轻松的解决处理任何类型数组的问题,但是如果在某些场景下引用不适宜用或者干脆不能用呢?你们知道为什么以前 Rust 的一些数组库,在使用的时候都限定长度不超过 32 吗?因为它们会为每个长度都单独实现一个函数,简直。。。毫无人性。难道没有什么办法可以解决这个问题吗? 好在,现在咱们有了 const 泛型,也就是针对值的泛型,正好可以用于处理数组长度的问题: + ```rust fn display_array(arr: [T; N]) { println!("{:?}", arr); @@ -352,7 +380,9 @@ fn main() { 在泛型参数之前,Rust 完全不适合复杂矩阵的运算,自从有了 const 泛型,一切即将改变。 #### const 泛型表达式 + 假设我们某段代码需要在内存很小的平台上工作,因此需要限制函数参数占用的内存大小,此时就可以使用 const 泛型表达式来实现: + ```rust // 目前只能在nightly版本下使用 #![allow(incomplete_features)] @@ -388,6 +418,7 @@ impl IsTrue for Assert { ``` #### const fn + @todo ## 泛型的性能 @@ -398,7 +429,7 @@ impl IsTrue for Assert { 具体来说: -Rust 通过在编译时进行泛型代码的 **单态化**(*monomorphization*)来保证效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。 +Rust 通过在编译时进行泛型代码的 **单态化**(_monomorphization_)来保证效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。 编译器所做的工作正好与我们创建泛型函数的步骤相反,编译器寻找所有泛型代码被调用的位置并针对具体类型生成代码。 diff --git a/src/basic/trait/trait-object.md b/src/basic/trait/trait-object.md index d566d410..a3f8c084 100644 --- a/src/basic/trait/trait-object.md +++ b/src/basic/trait/trait-object.md @@ -1,6 +1,7 @@ # 特征对象 在上一节中有一段代码无法通过编译: + ```rust fn returns_summarizable(switch: bool) -> impl Summary { if switch { @@ -14,12 +15,13 @@ fn returns_summarizable(switch: bool) -> impl Summary { } } ``` -其中 `Post` 和 `Weibo` 都实现了 `Summary` 特征,因此上面的函数试图通过返回 `impl Summary` 来返回这两个类型,但是编译器却无情地报错了,原因是 - `impl Trait` 的返回值类型并不支持多种不同的类型返回,那如果我们想返回多种类型,该怎么办? + +其中 `Post` 和 `Weibo` 都实现了 `Summary` 特征,因此上面的函数试图通过返回 `impl Summary` 来返回这两个类型,但是编译器却无情地报错了,原因是 `impl Trait` 的返回值类型并不支持多种不同的类型返回,那如果我们想返回多种类型,该怎么办? 再来考虑一个问题:现在在做一款游戏,需要将多个对象渲染在屏幕上,这些对象属于不同的类型,存储在列表中,渲染的时候,需要循环该列表并顺序渲染每个对象,在 Rust 中该怎么实现? 聪明的同学可能已经能想到一个办法,利用枚举: + ```rust #[derive(Debug)] enum UiObject { @@ -46,6 +48,7 @@ fn draw(o: UiObject) { Bingo,这个确实是一个办法,但是问题来了,如果你的对象集合并不能事先明确地知道呢?或者别人想要实现一个 UI 组件呢?此时枚举中的类型是有些缺少的,是不是还要修改你的代码增加一个枚举成员? 总之,在编写这个 UI 库时,我们无法知道所有的 UI 对象类型,只知道的是: + - UI 对象的类型不同 - 需要一个统一的类型来处理这些对象,无论是作为函数参数还是作为列表中的一员 - 需要对每一个对象调用 `draw` 方法 @@ -53,15 +56,19 @@ Bingo,这个确实是一个办法,但是问题来了,如果你的对象集 在拥有继承的语言中,可以定义一个名为 `Component` 的类,该类上有一个 `draw` 方法。其他的类比如 `Button`、`Image` 和 `SelectBox` 会从 `Component` 派生并因此继承 `draw` 方法。它们各自都可以覆盖 `draw` 方法来定义自己的行为,但是框架会把所有这些类型当作是 `Component` 的实例,并在其上调用 `draw`。不过 Rust 并没有继承,我们得另寻出路。 ## 特征对象定义 + 为了解决上面的所有问题,Rust 引入了一个概念 —— **特征对象**。 在介绍特征对象之前,先来为之前的 UI 组件定义一个特征: + ```rust pub trait Draw { fn draw(&self); } ``` + 只要组件实现了 `Draw` 特征,就可以调用 `draw` 方法来进行渲染。假设有一个 `Button` 和 `SelectBox` 组件实现了 `Draw` 特征: + ```rust pub struct Button { pub width: u32, @@ -88,32 +95,36 @@ impl Draw for SelectBox { } ``` + 此时,还需要一个动态数组来存储这些 UI 对象: + ```rust pub struct Screen { pub components: Vec, } ``` + 注意到上面代码中的 `?` 吗?它的意思是:我们应该填入什么类型,可以说就之前学过的内容里,你找不到哪个类型可以填入这里,但是因为 `Button` 和 `SelectBox` 都实现了 `Draw` 特征,那我们是不是可以把 `Draw` 特征的对象作为类型,填入到数组中呢?答案是肯定的。 **特征对象**指向实现了 `Draw` 特征的类型的实例,也就是指向了 `Button` 或者 `SelectBox` 的实例,这种映射关系是存储在一张表中,可以在运行时通过特征对象找到具体调用的类型方法。 可以通过 `&` 引用或者 `Box` 智能指针的方式来创建特征对象: + ```rust trait Draw { - fn draw(&self) -> String; + fn draw(&self) -> String; } impl Draw for u8 { fn draw(&self) -> String { - format!("u8: {}", *self) - } + format!("u8: {}", *self) + } } impl Draw for f64 { fn draw(&self) -> String { - format!("f64: {}", *self) - } + format!("f64: {}", *self) + } } fn draw1(x: Box) { @@ -137,21 +148,25 @@ fn main() { ``` 上面代码,有几个非常重要的点: + - `draw1` 函数的参数是 `Box` 形式的特征对象,该特征对象是通过 `Box::new(x)` 的方式创建的 - `draw2` 函数的参数是 `&dyn Draw` 形式的特征对象,该特征对象是通过 `&x` 的方式创建的 - `dyn` 关键字只用在特征对象的类型声明上,在创建时无需使用 `dyn` 因此,可以使用特征对象来代表泛型或具体的类型。 -继续来完善之前的UI组件代码,首先来实现 `Screen`: +继续来完善之前的 UI 组件代码,首先来实现 `Screen`: + ```rust pub struct Screen { pub components: Vec>, } ``` + 其中存储了一个动态数组,里面元素的类型是 `Draw` 特征对象:`Box`,任何实现了 `Draw` 特征的类型,都可以存放其中。 -再来为 `Screen` 定义 `run` 方法,用于将列表中的UI组件渲染在屏幕上: +再来为 `Screen` 定义 `run` 方法,用于将列表中的 UI 组件渲染在屏幕上: + ```rust impl Screen { pub fn run(&self) { @@ -161,9 +176,11 @@ impl Screen { } } ``` + 至此,我们就完成了之前的目标:在列表中存储多种不同类型的实例,然后将它们使用同一个方法逐一渲染在屏幕上! 再来看看,如果通过泛型实现,会如何: + ```rust pub struct Screen { pub components: Vec, @@ -178,11 +195,13 @@ impl Screen } } ``` + 上面的 `Screen` 的列表中,存储了类型为 `T` 的元素,然后在 `Screen` 中使用特征约束让 `T` 实现了 `Draw` 特征,进而可以调用 `draw` 方法。 但是这种写法限制了 `Screen` 实例的 `Vec` 中的每个元素必须是 `Button` 类型或者全是 `SelectBox` 类型。如果只需要同质(相同类型)集合,更倾向于这种写法:使用泛型和 特征约束,因为实现更清晰,且性能更好(特征对象,需要在运行时从 `vtable` 动态查找需要调用的方法)。 现在来运行渲染下咱们精心设计的 UI 组件列表: + ```rust fn main() { let screen = Screen { @@ -207,9 +226,10 @@ fn main() { screen.run(); } ``` + 上面使用 `Box::new(T)` 的方式来创建了两个 `Box` 特征对象,如果以后还需要增加一个 UI 组件,那么让该组件实现 `Draw` 特征,则可以很轻松的将其渲染在屏幕上,甚至用户可以引入我们的库作为三方库,然后在自己的库中为自己的类型实现 `Draw` 特征,然后进行渲染。 -在动态类型语言中,有一个很重要的概念: **鸭子类型**(*duck typing*),简单来说,就是只关心值长啥样,而不关心它实际是什么。当一个东西走起来像鸭子,叫起来像鸭子,那么它就是一只鸭子,就算它实际上是一个奥特曼,也不重要,我们就当它是鸭子。 +在动态类型语言中,有一个很重要的概念:**鸭子类型**(_duck typing_),简单来说,就是只关心值长啥样,而不关心它实际是什么。当一个东西走起来像鸭子,叫起来像鸭子,那么它就是一只鸭子,就算它实际上是一个奥特曼,也不重要,我们就当它是鸭子。 在上例中,`Screen` 在 `run` 的时候,我们并不需要知道各个组件的具体类型是什么。它也不检查组件到底是 `Button` 还是 `SelectBox` 的实例,只要它实现了 `Draw` 特征,就能通过 `Box::new` 包装成 `Box` 特征对象,然后被渲染在屏幕上。 @@ -226,9 +246,11 @@ fn main() { screen.run(); } ``` + 因为 `String` 类型没有实现 `Draw` 特征,编译器直接就会报错,不会让上述代码运行。如果想要 `String` 类型被渲染在屏幕上,那么只需要为其实现 `Draw` 特征即可,非常容易。 -#### &dyn和Box\的区别 +#### &dyn 和 Box\的区别 + 前文提到, `&dyn` 和 `Box` 都可以用于特征对象,因此在功能上 `&dyn` 和 `Box` 几乎没有区别,唯一的区别就是:`&dyn` 减少了一次指针调用。 因为 `Box` 是一个宽指针(`fat pointer`),它需要一次额外的解引用后,才能获取到指向 `vtable` 的指针,然后再通过该指针访问 `vtable` 查询到具体的函数指针,最后进行调用。 @@ -265,8 +287,10 @@ help: function arguments must have a statically known size, borrowed types alway -## Self与self +## Self 与 self + 在 Rust 中,有两个`self`,一个指代当前的实例对象,一个指代特征或者方法类型的别名: + ```rust trait Draw { fn draw(&self) -> Self; @@ -291,7 +315,9 @@ fn main() { 当理解了 `self` 与 `Self` 的区别后,我们再来看看何为对象安全。 ## 特征对象的限制 + 不是所有特征都能拥有特征对象,只有对象安全的特征才行。当一个特征的所有方法都有如下属性时,它的对象才是安全的: + - 方法的返回类型不能是 `Self` - 方法没有任何泛型参数 diff --git a/src/basic/trait/trait.md b/src/basic/trait/trait.md index ef94d72a..c7ece8f7 100644 --- a/src/basic/trait/trait.md +++ b/src/basic/trait/trait.md @@ -1,22 +1,27 @@ -# 特征Trait +# 特征 Trait + 如果我们想定义一个文件系统,那么把该系统跟底层存储解耦是很重要的。文件操作主要包含三个:`open` 、`write`、`read`,这些操作可以发生在硬盘,也可以发生在缓存,可以通过网络也可以通过(我实在编不下去了,大家来帮帮我)。总之如果你要为每一种情况都单独实现一套代码,那这种实现将过于繁杂,而且也没那个必要。 要解决上述问题,需要把这些行为抽象出来,就要使用 Rust 中的特征 `trait` 概念。可能你是第一次听说这个名词,但是不要怕,如果学过其他语言,那么大概率你听说过接口,没错,特征很类似接口。 在之前的代码中,我们也多次见过特征的使用,例如 `#[derive(Debug)]`,它在我们定义的类型(`struct`)上自动派生 `Debug` 特征,接着可以使用 `println!("{:?}", x)` 打印这个类型;再例如: + ```rust fn add>(a:T, b:T) -> T { a + b } ``` + 通过 `std::ops::Add` 特征来限制 `T`,只有 `T` 实现了 `std::ops::Add` 才能进行合法的加法操作,毕竟不是所有的类型都能进行相加。 这些都说明一个道理,特征定义了**一个可以被共享的行为,只要实现了特征,你就能使用该行为**。 ## 定义特征 + 如果不同的类型具有相同的行为,那么我们就可以定义一个特征,然后为这些类型实现该特征。**定义特征**是把一些方法组合在一起,目的是定义一个实现某些目标所必需的行为的集合。 例如,我们现在有文章 `Post` 和微博 `Weibo` 两种内容载体,而我们想对相应的内容进行总结,也就是无论是文章内容,还是微博内容,都可以在某个时间点进行总结,那么总结这个行为就是共享的,因此可以用特征来定义: + ```rust pub trait Summary { fn summarize(&self) -> String; @@ -30,9 +35,11 @@ pub trait Summary { 接下来,每一个实现这个特征的类型都需要具体实现该特征的相应方法,编译器也会确保任何实现 `Summary` 特征的类型都拥有与这个签名的定义完全一致的 `summarize` 方法。 ## 为类型实现特征 + 因为特征只定义行为看起来是什么样的,因此我们需要为类型实现具体的特征,定义行为具体是怎么样的。 首先来为 `Post` 和 `Weibo` 实现 `Summary` 特征: + ```rust pub trait Summary { fn summarize(&self) -> String; @@ -64,17 +71,19 @@ impl Summary for Weibo { 实现特征的语法与为结构体、枚举实现方法很像:`impl Summary for Post`,读作“为 `Post` 类型实现 `Summary` 特征”,然后在 `impl` 的花括号中实现该特征的具体方法。 接下来就可以在这个类型上调用特征的方法: + ```rust fn main() { let post = Post{title: "Rust语言简介".to_string(),author: "Sunface".to_string(), content: "Rust棒极了!".to_string()}; let weibo = Weibo{username: "sunface".to_string(),content: "好像微博没Tweet好用".to_string()}; - + println!("{}",post.summarize()); println!("{}",weibo.summarize()); } ``` 运行输出: + ```console 文章 Rust 语言简介, 作者是Sunface sunface发表了微博好像微博没Tweet好用 @@ -83,6 +92,7 @@ sunface发表了微博好像微博没Tweet好用 说实话,如果特征仅仅如此,你可能会觉得花里胡哨没啥用,接下来就让你见识下 `trait` 真正的威力。 #### 特征定义与实现的位置(孤儿规则) + 上面我们将 `Summary` 定义成了 `pub` 公开的。这样,如果他人想要使用我们的 `Summary` 特征,则可以引入到他们的包中,然后再进行实现。 关于特征实现与定义的位置,有一条非常重要的原则:**如果你想要为类型 `A` 实现特征 `T`,那么 `A` 或者 `T` 至少有一个是在当前作用域中定义的!**。例如我们可以为上面的 `Post` 类型实现标准库中的 `Display` 特征,这是因为 `Post` 类型定义在当前的作用域中。同时,我们也可以在当前包中为 `String` 类型实现 `Summary` 特征,因为 `Summary` 定义在当前作用域中。 @@ -92,7 +102,9 @@ sunface发表了微博好像微博没Tweet好用 该规则被称为**孤儿规则**,可以确保其它人编写的代码不会破坏你的代码,也确保了你不会莫名其妙就破坏了风马牛不相及的代码。 #### 默认实现 + 你可以在特征中定义具有**默认实现**的方法,这样其它类型无需再实现该方法,或者也可以选择重载该方法: + ```rust pub trait Summary { fn summarize(&self) -> String { @@ -102,6 +114,7 @@ pub trait Summary { ``` 上面为 `Summary` 定义了一个默认实现,下面我们编写段代码来测试下: + ```rust impl Summary for Post {} @@ -113,6 +126,7 @@ impl Summary for Weibo { ``` 可以看到,`Post` 选择了默认实现,而 `Weibo` 重载了该方法,调用和输出如下: + ```rust println!("{}",post.summarize()); println!("{}",weibo.summarize()); @@ -124,6 +138,7 @@ sunface发表了微博好像微博没Tweet好用 ``` 默认实现允许调用相同特征中的其他方法,哪怕这些方法没有默认实现。如此,特征可以提供很多有用的功能而只需要实现指定的一小部分内容。例如,我们可以定义 `Summary` 特征,使其具有一个需要实现的 `summarize_author` 方法,然后定义一个 `summarize` 方法,此方法的默认实现调用 `summarize_author` 方法: + ```rust pub trait Summary { fn summarize_author(&self) -> String; @@ -135,6 +150,7 @@ pub trait Summary { ``` 为了使用 `Summary`,只需要实现 `summarize_author` 方法即可: + ```rust impl Summary for Weibo { fn summarize_author(&self) -> String { @@ -148,9 +164,11 @@ println!("1 new weibo: {}", weibo.summarize()); `weibo.summarize()` 会先调用 `Summary` 特征默认实现的 `summarize` 方法,通过该方法进而调用 `Weibo` 为 `Summary` 实现的 `summarize_author` 方法,最终输出:`1 new weibo: (Read more from @horse_ebooks...)`。 ## 使用特征作为函数参数 + 之前提到过,特征如果仅仅是用来实现方法,那真的有些大材小用,现在我们来讲下,真正可以让特征大放光彩的地方。 现在,先定义一个函数,使用特征用做函数参数: + ```rust pub fn notify(item: &impl Summary) { println!("Breaking news! {}", item.summarize()); @@ -162,20 +180,25 @@ pub fn notify(item: &impl Summary) { 你可以使用任何实现了 `Summary` 特征的类型作为该函数的参数,同时在函数体内,还可以调用该特征的方法,例如 `summarize` 方法。具体的说,可以传递 `Post` 或 `Weibo` 的实例来作为参数,而其它类如 `String` 或者 `i32` 的类型则不能用做该函数的参数,因为它们没有实现 `Summary` 特征。 ## 特征约束(trait bound) + 虽然 `impl Trait` 这种语法非常好理解,但是实际上它只是一个语法糖: + ```rust pub fn notify(item: &T) { println!("Breaking news! {}", item.summarize()); } ``` + 真正的完整书写形式如上所述,形如 `T: Summary` 被称为**特征约束**。 在简单的场景下 `impl Trait` 的语法就足够使用,但是对于复杂的场景,特征约束可以让我们拥有更大的灵活性和语法表现能力,例如一个函数接受两个 `impl Summary` 的参数: + ```rust pub fn notify(item1: &impl Summary, item2: &impl Summary) {} ``` 如果函数两个参数是不同的类型,那么上面的方法很好,只要这两个类型都实现了 `Summary` 特征即可。但是如果我们想要强制函数的两个参数是同一类型呢?上面的语法就无法做到这种限制,此时我们只能使特征约束来实现: + ```rust pub fn notify(item1: &T, item2: &T) {} ``` @@ -183,23 +206,31 @@ pub fn notify(item1: &T, item2: &T) {} 泛型类型 `T` 说明了 `item1` 和 `item2` 必须拥有同样的类型,同时 `T: Summary` 说明了 `T` 必须实现 `Summary` 特征。 #### 多重约束 + 除了单个约束条件,我们还可以指定多个约束条件,例如除了让参数实现 `Summary` 特征外,还可以让参数实现 `Display` 特征以控制它的格式化输出: + ```rust pub fn notify(item: &(impl Summary + Display)) { ``` 除了上述的语法糖形式,还能使用特征约束的形式: + ```rust pub fn notify(item: &T) {} ``` + 通过这两个特征,就可以使用 `item.summarize` 方法,以及通过 `println!("{}", item)` 来格式化输出 `item`。 -#### Where约束 +#### Where 约束 + 当特征约束变得很多时,函数的签名将变得很复杂: + ```rust fn some_function(t: &T, u: &U) -> i32 { ``` + 严格来说,上面的例子还是不够复杂,但是我们还是能对其做一些形式上的改进,通过 `where`: + ```rust fn some_function(t: &T, u: &U) -> i32 where T: Display + Clone, @@ -208,7 +239,9 @@ fn some_function(t: &T, u: &U) -> i32 ``` #### 使用特征约束有条件地实现方法或特征 + 特征约束,可以让我们在指定类型 + 指定特征的条件下去实现方法,例如: + ```rust use std::fmt::Display; @@ -241,6 +274,7 @@ impl Pair { 该函数可读性会更好,因为泛型参数、参数、返回值都在一起,可以快速的阅读,同时每个泛型参数的特征也在新的代码行中通过**特征约束**进行了约束。 **也可以有条件地实现特征**, 例如,标准库为任何实现了 `Display` 特征的类型实现了 `ToString` 特征: + ```rust impl ToString for T { // --snip-- @@ -248,13 +282,15 @@ impl ToString for T { ``` 我们可以对任何实现了 `Display` 特征的类型调用由 `ToString` 定义的 `to_string` 方法。例如,可以将整型转换为对应的 `String` 值,因为整型实现了 `Display`: + ```rust let s = 3.to_string(); ``` - ## 函数返回中的 `impl Trait` + 可以通过 `impl Trait` 来说明一个函数返回了一个类型,该类型实现了某个特征: + ```rust fn returns_summarizable() -> impl Summary { Weibo { @@ -271,6 +307,7 @@ fn returns_summarizable() -> impl Summary { 这种 `impl Trait` 形式的返回值,在一种场景下非常非常有用,那就是返回的真实类型非常复杂,你不知道该怎么声明时(毕竟 Rust 要求你必须标出所有的类型),此时就可以用 `impl Trait` 的方式简单返回。例如,闭包和迭代器就是很复杂,只有编译器才知道那玩意的真实类型,如果让你写出来它们的具体类型,估计内心有一万只草泥马奔腾,好在你可以用 `impl Iterator` 来告诉调用者,返回了一个迭代器,因为所有迭代器都会实现 `Iterator` 特征。 但是这种返回值方式有一个很大的限制:只能有一个具体的类型,例如: + ```rust fn returns_summarizable(switch: bool) -> impl Summary { if switch { @@ -305,7 +342,9 @@ expected struct `Post`, found struct `Weibo` 报错提示我们 `if` 和 `else` 返回了不同的类型。如果想要实现返回不同的类型,需要使用下一章节中的[特征对象](./trait-object.md)。 ## 修复上一节中的 `largest` 函数 + 还记得上一节中的[例子](./generic#泛型详解)吧,当时留下一个疑问,该如何解决编译报错: + ```rust error[E0369]: binary operation `>` cannot be applied to type `T` // 无法在 `T` 类型上应用`>`运算符 --> src/main.rs:5:17 @@ -324,11 +363,13 @@ help: consider restricting type parameter `T` // 考虑使用以下的特征来 在 `largest` 函数体中我们想要使用大于运算符(`>`)比较两个 `T` 类型的值。这个运算符是标准库中特征 `std::cmp::PartialOrd` 的一个默认方法。所以需要在 `T` 的特征约束中指定 `PartialOrd`,这样 `largest` 函数可以用于内部元素类型可比较大小的数组切片。 由于 `PartialOrd` 位于 `prelude` 中所以并不需要通过 `std::cmp` 手动将其引入作用域。所以可以将 `largest` 的签名修改为如下: + ```rust fn largest(list: &[T]) -> T { ``` 但是此时编译,又会出现新的错误: + ```rust error[E0508]: cannot move out of type `[T]`, a non-copy slice --> src/main.rs:2:23 @@ -349,9 +390,10 @@ error[E0507]: cannot move out of borrowed content | cannot move out of borrowed content ``` -错误的核心是 `cannot move out of type [T], a non-copy slice`,原因是 `T` 没有[实现 `Copy` 特性](../ownership/ownership.md#拷贝(浅拷贝)),因此我们只能把所有权进行转移,毕竟只有 `i32` 等基础类型才实现了 `Copy` 特性,可以存储在栈上,而 `T` 可以指代任何类型(严格来说是实现了 `PartialOrd` 特征的所有类型)。 +错误的核心是 `cannot move out of type [T], a non-copy slice`,原因是 `T` 没有[实现 `Copy` 特性](../ownership/ownership.md#拷贝浅拷贝),因此我们只能把所有权进行转移,毕竟只有 `i32` 等基础类型才实现了 `Copy` 特性,可以存储在栈上,而 `T` 可以指代任何类型(严格来说是实现了 `PartialOrd` 特征的所有类型)。 因此,为了让 `T` 拥有 `Copy` 特性,我们可以增加特征约束: + ```rust fn largest(list: &[T]) -> T { let mut largest = list[0]; @@ -378,12 +420,12 @@ fn main() { } ``` -如果并不希望限制 `largest` 函数只能用于实现了 `Copy` 特征的类型,我们可以在 `T` 的特征约束中指定 [`Clone` 特征](../ownership/ownership.md#克隆(深拷贝)) 而不是 `Copy` 特征。并克隆 `list` 中的每一个值使得 `largest` 函数拥有其所有权。使用 `clone` 函数意味着对于类似 `String` 这样拥有堆上数据的类型,会潜在地分配更多堆上空间,而堆分配在涉及大量数据时可能会相当缓慢。 +如果并不希望限制 `largest` 函数只能用于实现了 `Copy` 特征的类型,我们可以在 `T` 的特征约束中指定 [`Clone` 特征](../ownership/ownership.md#克隆深拷贝) 而不是 `Copy` 特征。并克隆 `list` 中的每一个值使得 `largest` 函数拥有其所有权。使用 `clone` 函数意味着对于类似 `String` 这样拥有堆上数据的类型,会潜在地分配更多堆上空间,而堆分配在涉及大量数据时可能会相当缓慢。 另一种 `largest` 的实现方式是返回在 `list` 中 `T` 值的引用。如果我们将函数返回值从 `T` 改为 `&T` 并改变函数体使其能够返回一个引用,我们将不需要任何 `Clone` 或 `Copy` 的特征约束而且也不会有任何的堆分配。尝试自己实现这种替代解决方式吧! - ## 通过 `derive` 派生特征 + 在本书中,形如 `#[derive(Debug)]` 的代码已经出现了很多次,这种是一种特征派生语法,被 `derive` 标记的对象会自动实现对应的默认特征代码,继承相应的功能。 例如 `Debug` 特征,它有一套自动实现的默认代码,当你给一个结构体标记后,就可以使用 `println!("{:?}", s)` 的形式打印该结构体的对象。 @@ -395,11 +437,12 @@ fn main() { 详细的 `derive` 列表参见[附录-派生特征](../../appendix/derive.md)。 ## 调用方法需要引入特征 + 在一些场景中,使用 `as` 关键字做类型转换会有比较大的限制,因为你想要在类型转换上拥有完全的控制,例如处理转换错误,那么你将需要 `TryInto`: ```rust use std::convert::TryInto; - + fn main() { let a: i32 = 10; let b: u16 = 100; @@ -417,11 +460,11 @@ fn main() { 但是 Rust 又提供了一个非常便利的办法,即把最常用的标准库中的特征通过 [`std::prelude`](std::convert::TryInto) 模块提前引入到当前作用域中,其中包括了 `std::convert::TryInto`,你可以尝试删除第一行的代码 `use ...`,看看是否会报错。 - ## 几个综合例子 #### 为自定义类型实现 `+` 操作 -在 Rust 中除了数值类型的加法,`String` 也可以做[加法](../compound-type/string-slice.md#操作字符串),因为 Rust 为该类型实现了 `std::ops::Add` 特征,同理,如果我们为自定义类型实现了该特征,那就可以自己实现 `Point1 + Point2` 的操作: + +在 Rust 中除了数值类型的加法,`String` 也可以做[加法](../compound-type/string-slice.md#操作字符串),因为 Rust 为该类型实现了 `std::ops::Add` 特征,同理,如果我们为自定义类型实现了该特征,那就可以自己实现 `Point1 + Point2` 的操作: ```rust use std::ops::Add; @@ -460,7 +503,9 @@ fn main() { ``` #### 自定义类型的打印输出 + 在开发过程中,往往只要使用 `#[derive(Debug)]` 对我们的自定义类型进行标注,即可实现打印输出的功能: + ```rust #[derive(Debug)] struct Point{ @@ -472,26 +517,28 @@ fn main() { println!("{:?}",p); } ``` + 但是在实际项目中,往往需要对我们的自定义类型进行自定义的格式化输出,以让用户更好的阅读理解我们的类型,此时就要为自定义类型实现 `std::fmt::Display` 特征: + ```rust #![allow(dead_code)] - + use std::fmt; use std::fmt::{Display}; - + #[derive(Debug,PartialEq)] enum FileState { Open, Closed, } - + #[derive(Debug)] struct File { name: String, data: Vec, state: FileState, } - + impl Display for FileState { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { @@ -500,14 +547,14 @@ impl Display for FileState { } } } - + impl Display for File { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "<{} ({})>", self.name, self.state) } } - + impl File { fn new(name: &str) -> File { File { @@ -517,7 +564,7 @@ impl File { } } } - + fn main() { let f6 = File::new("f6.txt"); //... @@ -526,8 +573,6 @@ fn main() { } ``` - 以上两个例子较为复杂,目的是为读者展示下真实的使用场景长什么样,因此需要读者细细阅读,最终消化这些知识对于你的 Rust 之路会有莫大的帮助。 - 最后,特征和特征约束,是 Rust 中极其重要的概念,如果你还是没搞懂,强烈建议回头再看一遍,或者寻找相关的资料进行补充学习。如果已经觉得掌握了,那么就可以进入下一节的学习。 diff --git a/src/basic/variable.md b/src/basic/variable.md index 1a4b898e..6c119d18 100644 --- a/src/basic/variable.md +++ b/src/basic/variable.md @@ -1,6 +1,6 @@ # 变量绑定与解构 -鉴于本书的目标读者(别慌,来到这里就说明你就是目标读者)已经熟练掌握其它任意一门编程语言,因此这里就不再对何为变量进行赘述,让我们开门见山来谈谈,为何 Rust 选择了手动设定变量的可变性。 +鉴于本书的目标读者(别慌,来到这里就说明你就是目标读者)已经熟练掌握其它任意一门编程语言,因此这里就不再对何为变量进行赘述,让我们开门见山来谈谈,为何 Rust 选择了手动设定变量的可变性。 ## 为何要手动设置变量的可变性? @@ -14,7 +14,7 @@ 在命名方面,和其它语言没有区别,不过当给变量命名时,需要遵循 [Rust 命名规范](https://course.rs/practice/naming.html)。 -> Rust 语言有一些**关键字**(*keywords*),和其他语言一样,这些关键字都是被保留给 Rust 语言使用的,因此,它们不能被用作变量或函数的名称。在 [附录 A](../appendix/keywords) 中可找到关键字列表。 +> Rust 语言有一些**关键字**(_keywords_),和其他语言一样,这些关键字都是被保留给 Rust 语言使用的,因此,它们不能被用作变量或函数的名称。在 [附录 A](../appendix/keywords) 中可找到关键字列表。 ## 变量绑定 @@ -26,11 +26,11 @@ ## 变量可变性 - Rust 的变量在默认情况下是**不可变的**。在上文提到过,这是 Rust 团队为我们精心设计的语言特性之一,这样可以让我们编写更安全、更高性能的代码。当然你可以通过 `mut` 关键字让变量变为**可变的**,以实现更加灵活的设计。 +Rust 的变量在默认情况下是**不可变的**。在上文提到过,这是 Rust 团队为我们精心设计的语言特性之一,这样可以让我们编写更安全、更高性能的代码。当然你可以通过 `mut` 关键字让变量变为**可变的**,以实现更加灵活的设计。 -当变量不可变时,这意味着一旦一个值绑定到一个变量 `a` 后,就不能再更改 `a` 的值了。为了说明,在我们的工程目录下使用 `cargo new variables` 来创建一个名为 *variables* 的新项目。 +当变量不可变时,这意味着一旦一个值绑定到一个变量 `a` 后,就不能再更改 `a` 的值了。为了说明,在我们的工程目录下使用 `cargo new variables` 来创建一个名为 _variables_ 的新项目。 -然后在新建的 *variables* 目录下,打开 *src/main.rs* 并将代码替换为下面还未能通过编译的代码: +然后在新建的 _variables_ 目录下,打开 _src/main.rs_ 并将代码替换为下面还未能通过编译的代码: ```rust fn main() { @@ -40,7 +40,7 @@ fn main() { println!("The value of x is: {}", x); } ``` - + 保存文件,并使用 `cargo run` 运行程序,你将会收到一条错误消息,输出如下所示: ```console @@ -71,7 +71,7 @@ error: aborting due to previous error 在 Rust 中,可变性很简单,只要在变量名前加一个 `mut` 即可, 而且这种显式的声明方式还会给后来人传达这样的信息:嗯,这个变量在后面代码部分会发生改变。 -为了让变量声明为可变,将 *src/main.rs* 改为以下内容: +为了让变量声明为可变,将 _src/main.rs_ 改为以下内容: ```rust fn main() { @@ -113,7 +113,9 @@ fn main() { ``` ### 解构式赋值 + 在 [Rust 1.59](https://course.rs/appendix/rust-versions/1.59.html) 版本后,我们可以在赋值语句的左式中使用元组、切片和结构体模式了。 + ```rust struct Struct { e: i32 @@ -127,7 +129,7 @@ fn main() { Struct { e, .. } = Struct { e: 5 }; assert_eq!([1, 2, 1, 4, 5], [a, b, c, d, e]); -} +} ``` 这种使用方式跟之前的 `let` 保持了一致性,但是 `let` 会重新绑定,而这里仅仅是对之前绑定的变量进行再赋值。 @@ -136,14 +138,13 @@ fn main() { ### 变量和常量之间的差异 -变量的值不能更改可能让你想起其他另一个很多语言都有的编程概念:**常量**(*constant*)。与不可变变量一样,常量也是绑定到一个常量名且不允许更改的值,但是常量和变量之间存在一些差异: +变量的值不能更改可能让你想起其他另一个很多语言都有的编程概念:**常量**(_constant_)。与不可变变量一样,常量也是绑定到一个常量名且不允许更改的值,但是常量和变量之间存在一些差异: - 常量不允许使用 `mut`。**常量不仅仅默认不可变,而且自始至终不可变**,因为常量在编译完成后,已经确定它的值。 - 常量使用 `const` 关键字而不是 `let` 关键字来声明,并且值的类型**必须**标注。 我们将在下一节[数据类型](./base-type/index.md)中介绍,因此现在暂时无需关心细节。 - 下面是一个常量声明的例子,其常量名为 `MAX_POINTS`,值设置为 `100,000`。(Rust 常量的命名约定是全部字母都使用大写,并使用下划线分隔单词,另外对数字字面量可插入下划线以提高可读性): ```rust @@ -152,7 +153,7 @@ const MAX_POINTS: u32 = 100_000; 常量可以在任意作用域内声明,包括全局作用域,在声明的作用域内,常量在程序运行的整个过程中都有效。对于需要在多处代码共享一个不可变的值时非常有用,例如游戏中允许玩家赚取的最大点数或光速。 ->在实际使用中,最好将程序中用到的硬编码值都声明为常量,对于代码后续的维护有莫大的帮助。如果将来需要更改硬编码的值,你也只需要在代码中更改一处即可。 +> 在实际使用中,最好将程序中用到的硬编码值都声明为常量,对于代码后续的维护有莫大的帮助。如果将来需要更改硬编码的值,你也只需要在代码中更改一处即可。 ### 变量遮蔽(shadowing) @@ -190,6 +191,7 @@ The value of x is: 6 变量遮蔽的用处在于,如果你在某个作用域内无需再使用之前的变量(在被遮蔽后,无法再访问到之前的同名变量),就可以重复的使用变量名字,而不用绞尽脑汁去想更多的名字。 例如,假设有一个程序要统计一个空格字符串的空格数量: + ```rust // 字符串类型 let spaces = " "; diff --git a/src/first-try/cargo.md b/src/first-try/cargo.md index cc5bc5c4..7ddc5adf 100644 --- a/src/first-try/cargo.md +++ b/src/first-try/cargo.md @@ -128,7 +128,7 @@ $ cargo check 现在用 VSCode 打开上面创建的"世界,你好"项目,然后进入根目录的 `Cargo.toml` 文件,可以看到该文件包含不少信息: -### package配置段落 +### package 配置段落 `package` 中记录了项目的描述信息,典型的如下: diff --git a/src/first-try/editor.md b/src/first-try/editor.md index 3de60f59..9be7f8cc 100644 --- a/src/first-try/editor.md +++ b/src/first-try/editor.md @@ -15,7 +15,9 @@ ## 安装 VSCode 的 Rust 插件 在 VSCode 的左侧扩展目录里,搜索 `rust`, 你能看到两个 Rust 插件,如果没有意外,这两个应该分别排名第一和第二: + 1. 官方的 `Rust`,作者是 `The Rust Programming Language`, 官方出品,牛逼就完了,但是……我们并不推荐,这个插件有几个问题: + - 首先是在代码跳转上支持的很烂,只能在自己的代码库中跳转,一旦跳到别的三方库,那就无法继续跳转,对于查看标准库和三方库的源码带来了极大的困扰 - 其次,不支持类型自动标注,对于 Rust 语言而言,类型说明是非常重要的,特别是在你不知道给变量一个什么类型时,这种 IDE 的自动提示就变得弥足珍贵 - 代码提示不太好用,有些方法既不会提示,也不能跳转 @@ -31,12 +33,13 @@ 当插件使用默认设置时,每一次保存代码,都会出进行一次重新编译。 > 如果你的电脑慢,有一点一定要注意: -> +> > 在编译器构建代码的同时,不要在终端再运行 `cargo run` 等命令进行编译,不然会获得一个报错提示,大意是当前文件目录已经被锁定,等待其它使用者释放。如果等了很久 IDE 还是没有释放(虽然我没遇到过,但是存在这个可能性),你可以关掉 IDE,并手动 `kill` 掉 `rust-analyzer`,然后重新尝试。 ## 安装其它好用的插件 在此,再推荐大家几个好用的插件: + 1. `Better TOML`,用于更好的展示 .toml 文件 2. `Error Lens`, 更好的获得错误展示 3. `One Dark Pro`, 非常好看的 VSCode 主题 diff --git a/src/first-try/hello-world.md b/src/first-try/hello-world.md index e9c0ed8d..0d6d39a1 100644 --- a/src/first-try/hello-world.md +++ b/src/first-try/hello-world.md @@ -9,6 +9,7 @@ 现在使用 VSCode 打开 [上一节](./cargo.md) 中创建的 `world_hello` 工程,然后进入 `main.rs` 文件。(此文件是当前 Rust 工程的入口文件,和其它语言几无区别。) 接下来,对世界友人给予热切的问候: + ```rust fn greet_world() { let southern_germany = "Grüß Gott!"; @@ -19,7 +20,7 @@ fn greet_world() { println!("{}", ®ion); } } - + fn main() { greet_world(); } @@ -46,14 +47,17 @@ World, hello 对于 `println` 来说,我们没有使用其它语言惯用的 `%s`、`%d` 来做输出占位符,而是使用 `{}`,因为 Rust 在底层帮我们做了大量工作,会自动识别输出数据的类型,例如当前例子,会识别为 `String` 类型。 最后,和其它语言不同,Rust 的集合类型不能直接进行循环,需要变成迭代器(这里是通过 `.iter()` 方法),才能用于迭代循环。在目前来看,你会觉得这一点好像挺麻烦,不急,以后就知道这么做的好处所在。 + > 实际上这段代码可以简写,在 2021 edition 及以后,支持直接写 `for region in regions`,原因会在迭代器章节的开头提到,是因为 for 隐式地将 regions 转换成迭代器。 至于函数声明、调用、数组的使用,和其它语言没什么区别,So Easy! ## Rust 语言初印象 + Rust 这门语言对于 Haskell 和 Java 开发者来说,可能会觉得很熟悉,因为它们在高阶表达方面都很优秀。简而言之,就是可以很简洁的写出原本需要一大堆代码才能表达的含义。但是,Rust 又有所不同:它的性能是底层语言级别的性能,可以跟 C/C++ 相媲美。 上面的 `So Easy` 的余音仍在绕梁,我希望它能继续下去,可是… 人总是要面对现实,因此让我们来点狠活: + ```rust fn main() { let penguin_data = "\ @@ -63,14 +67,14 @@ fn main() { Fiordland penguin,60 Invalid,data "; - + let records = penguin_data.lines(); - + for (i, record) in records.enumerate() { if i == 0 || record.trim().len() == 0 { continue; } - + // 声明一个 fields 变量,类型是 Vec // Vec 是 vector 的缩写,是一个可伸缩的集合类型,可以认为是一个动态数组 // <_>表示 Vec 中的元素类型由编译器自行推断,在很多场景下,都会帮我们省却不少功夫 @@ -83,7 +87,7 @@ fn main() { eprintln!("debug: {:?} -> {:?}", record, fields); } - + let name = fields[0]; // 1. 尝试把 fields[1] 的值转换为 f32 类型的浮点数,如果成功,则把 f32 值赋给 length 变量 // 2. if let 是一个匹配表达式,用来从=右边的结果中,匹配出 length 的值: diff --git a/src/first-try/installation.md b/src/first-try/installation.md index 162ae8b1..f732c803 100644 --- a/src/first-try/installation.md +++ b/src/first-try/installation.md @@ -36,6 +36,7 @@ Rust 对运行环境的依赖和 Go 语言很像,几乎所有环境都可以 ```console $ xcode-select --install ``` + **Linux 下:** Linux 用户一般应按照相应发行版的文档来安装 `GCC` 或 `Clang`。 @@ -54,8 +55,8 @@ Windows 上安装 Rust 需要有 `C++` 环境,以下为安装的两种方式 在 [RUSTUP-INIT](https://www.rust-lang.org/learn/get-started) 下载系统相对应的 Rust 安装程序,一路默认即可。 -``` shell -PS C:\Users\Hehongyuan> rustup-init.exe +```shell +PS C:\Users\Hehongyuan> rustup-init.exe ...... Current installation options: @@ -149,6 +150,7 @@ rustc 1.56.1 (59eed8a2a 2021-11-01) $ cargo -V cargo 1.57.0 (b2e52d7ca 2021-10-21) ``` + > 注:若发现版本号不同,以您的版本号为准 恭喜,你已成功安装 Rust!如果没看到此信息,并且你使用的是 Windows,请检查 Rust 或 `%USERPROFILE%\.cargo\bin` 是否在 `%PATH%` 系统变量中。 diff --git a/src/index-list.md b/src/index-list.md index d781f75a..e7bed5f0 100644 --- a/src/index-list.md +++ b/src/index-list.md @@ -1,9 +1,10 @@ # 快速查询入口 -<> 既然自诩为 Rust 日常开发工具书,那就得有工具书的样子,如果没有了快速索引查询的功能,也就没有了灵魂。 + +<> 既然自诩为 Rust 日常开发工具书,那就得有工具书的样子,如果没有了快速索引查询的功能,也就没有了灵魂。 因此我们决定在这里提供一个对全书内容进行快速索引的途径。理论上来说,**你想查的任何东西在这里都可以快速的被找到并能进入相应的章节查看详细的介绍**。 可能大家会有疑问,不是有站内搜索功能嘛?是的,但是尴尬的是:首先它不支持中文,其次就算支持了中文,也一样不好用,我们需要的是快速精准地找到内容而不是模糊的查询内容。 +## 索引列表 todo -## 索引列表 todo \ No newline at end of file diff --git a/src/into-rust.md b/src/into-rust.md index afeedc58..bf251493 100644 --- a/src/into-rust.md +++ b/src/into-rust.md @@ -159,4 +159,4 @@ Rust 语言表达能力更强,性能更高。同时线程安全方面 Rust 也 > 本书是完全开源的,但是并不意味着质量上的妥协,这里的每一个章节都花费了大量的心血和时间才能完成,为此牺牲了陪伴家人、日常娱乐的时间,虽然我们并不后悔,但是如果能得到读者您的鼓励,我们将感激不尽。 > -> 既然是开源,那最大的鼓励不是 money,而是 star:) **如果大家觉得这本书作者真的用心了,就帮我们[点一个 🌟 ](https;//github.com/sunface/rust-course)吧,这将是我们继续前行最大的动力** +> 既然是开源,那最大的鼓励不是 money,而是 star:) **如果大家觉得这本书作者真的用心了,就帮我们[点一个 🌟 ](https;//github.com/sunface/rust-course)吧,这将是我们继续前行最大的动力** diff --git a/src/some-thoughts.md b/src/some-thoughts.md index 367171d9..d00129c7 100644 --- a/src/some-thoughts.md +++ b/src/some-thoughts.md @@ -1 +1 @@ -# Rust语言圣经 +# Rust 语言圣经 diff --git a/src/sth-you-should-not-do.md b/src/sth-you-should-not-do.md index af79e18e..f76e7ff1 100644 --- a/src/sth-you-should-not-do.md +++ b/src/sth-you-should-not-do.md @@ -1,14 +1,17 @@ # 避免从入门到放弃 + 很多人都在学 Rust ing,也有很多人在放弃 ing。想要顺利学完 Rust,大家需要谨记本文列出的内容,否则这极有可能是又又又一次从入门到放弃之旅。 Rust 是一门全新的语言,它会带给你前所未有的体验,提升你的通用编程水平,甚至于赋予你全新的编程思想。在此时此刻,大家可能还半信半疑,但是当学完它再回头看时,可能你也会认同这些貌似浮夸的赞美。 ## 避免试一试的心态 + 在学习 Go、Python 等编程语言时,你可能会一边工作,一边轻松愉快的学习它们,但是 Rust 不行。原因如文章开头所说,在学习 Rust 的同时你会收获很多语言之外的知识,因此 Rust 在入门阶段比很多编程语言要更难,但是一旦入门,你将收获一个全新的自己,成为一个更加优秀的程序员。 在学习过程中,一开始可能会轻松愉快,但是在开始 Rust 核心概念时(所有权、借用、生命周期、智能指针等),难度可能会陡然提升,此时就需要认真对待起来,否则会为后面埋下很多难以填补的坑,结果最后你可能只有两个选择:重新学一遍 or 放弃。 因此,在学习过程中,给大家三点建议: + - 要提前做好会遇到困难的准备,因为如上面所说,学习 Rust 不仅仅是在学习一门编程语言 - 不要抱着试一试的心态去试一试,否则是浪费时间和消耗学习的激情,作为连续六年全世界最受喜欢的语言,Rust 不仅仅是值得试一试 :) - 深入学习一本好书或教程 @@ -16,18 +19,20 @@ Rust 是一门全新的语言,它会带给你前所未有的体验,提升你 总之, Rust 入门难,但是在你一次次克服艰难险阻的同时,也一次次收获了与众不同的编程经验,最后历经九九八十一难,立地成大佬。 给自己一个机会,也给 Rust 一个机会 :) ## 深入学习一本好书 + Rust 跟其它语言不一样,你无法看了一遍语法,然后就能上手写代码,对,我说的就是对比 Go 语言,后者的简单易用是有目共睹的。 这些年,我遇到过太多在网上看了一遍菜鸟教程(或其它简易教程)就上手写 demo 甚至项目的同学,无一例外,都各种碰壁、趟坑,最后要么放弃,要么回炉重造,之前的时间和精力基本等同浪费。 因此,大家一定要舍得投入时间,沉下心去读一本好书,这本书会带你深入浅出地学习使用 Rust 所需的各种知识,还会带你提前趟坑,这些坑往往是需要大量的时间才能领悟的。 -在以前我可能会推荐看官方那本书的英文原版 + async book + nomicon 这几本书的组合,但是现在有了一本更适合中国用户的书籍,那就是 [<>](https://github.com/sunface/rust-course),内容好坏大家一读即知,光就文字而言,那绝对是行云流水般的阅读体验,可以极大提升你的学习效率,也不再因为反复读也读不懂一句话而烦闷不堪。 +在以前我可能会推荐看官方那本书的英文原版 + async book + nomicon 这几本书的组合,但是现在有了一本更适合中国用户的书籍,那就是 [<>](https://github.com/sunface/rust-course),内容好坏大家一读即知,光就文字而言,那绝对是行云流水般的阅读体验,可以极大提升你的学习效率,也不再因为反复读也读不懂一句话而烦闷不堪。 ## 千万别从链表或图开始练手 + CS 课程中我们会学习大量的常用数据结构和算法,因此大家都养成了一种好习惯:学习一门新语言,先用它写个链表或图试试。 -我的天,在 Rust 中**千万别这么干**,你是在扼杀自己之前的努力!因为不像其它语言,链表在 Rust 中简直是地狱一般的难度,我见过太多英雄好汉难过链表关,最终黯然退幕。我不希望正在阅读此文的你也成为其中一个 :( +我的天,在 Rust 中**千万别这么干**,你是在扼杀自己之前的努力!因为不像其它语言,链表在 Rust 中简直是地狱一般的难度,我见过太多英雄好汉难过链表关,最终黯然退幕。我不希望正在阅读此文的你也成为其中一个 :( 这些自引用类型的数据结构(包含了字段,该字段又引用了自身),它们是恶魔,它们不仅仅在蹂躏着新手,还在折磨着老手,有意思的是,它们的难恰恰是 Rust 的优点导致的:无 GC 也无手动内存管理,内存安全。 @@ -38,24 +43,25 @@ CS 课程中我们会学习大量的常用数据结构和算法,因此大家 如果想要练手,我们可以换个方向开始,当然如果你就是喜欢征服困难,那没问题,就从链表开始。但是无论选择哪个,之前提到的那本书都会给你莫大的帮助,包括如何实现一个链表! ## 仔细阅读编译错误 + 在一些编程语言中,你可能习惯了编译器给出的错误只要看前面(或后面)几行就行,大部分是不怎么用到的信息,总之编译器总感觉笨笨的。 但是 Rust 不是,它为我们提供了一个强大无比的编译器,而且会提示我们该如何修改代码以解决错误,简直就是一个优秀的老师! -因此在使用 Rust 过程中,如果错误你不知该如何解决,不妨仔细阅读下编译器或者IDE给出的错误提示,绝大多数时候,你都可以通过这些提示顺利的解决问题。 +因此在使用 Rust 过程中,如果错误你不知该如何解决,不妨仔细阅读下编译器或者 IDE 给出的错误提示,绝大多数时候,你都可以通过这些提示顺利的解决问题。 同时也不要忽略编译器给出的警告信息(warnings),因为里面包含了 `cargo clippy` 给出的 `lint` 提示,这些提示不仅仅包含代码风格,甚至包含了一些隐藏很深的错误!至于这些错误为何不是 `error` 形式出现,随着学习的深入,你将逐渐理解 Rust 的各种设计选择,包括这个问题。 ## 不要强制自己使用其它编程语言的最佳实践来写 Rust + 大多数其它编程语言适用的最佳实践在 Rust 中也可以很好的使用,但是 Rust 并不是一门专门的面向对象或者函数式语言,因此在使用自己喜欢的编程风格时,也要考虑遵循 Rust 应有的实践。 例如纯面向对象或纯函数式编程,在 Rust 中就并不是一个很好的选择。如果你有过 Go 语言的编程经验,相信能更加理解我这里想表达的含义。 不过大家也不用担心,在书中我们以专题的形式专门讲解 Rust 的最佳实践,看完后自然就明白了。 - - ## 总结 + 对于新手而言,最应该避免的就是从**链表开始练手**,最应该做的就是认真仔细地学习一本优秀的书。 总之,认真学 Rust,既然选择了,就相信自己,你的前方会是星辰大海!