Document not found (404)
+This URL is invalid, sorry. Please use the navigation bar or search to continue.
+ +diff --git a/README.md b/README.md index eea2f6b70..90185d720 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,13 @@ Github Repo | last commit | home ---- | ---- | ---- [Rust 标准库文档中文版](https://github.com/rust-lang-cn/std-cn) | [](https://github.com/rust-lang-cn/std-cn/commits) | [#home](https://jaywcjlove.github.io/rust-cn-document-for-docker/std/std/) -[Rust 程序设计语言 中文版](https://github.com/rust-lang-cn/book-cn) | [](https://github.com/rust-lang-cn/book-cn/commits) | [#home](https://jaywcjlove.github.io/rust-cn-document-for-docker/book/) -[Rust By Example 中文版](https://github.com/rust-lang-cn/rust-by-example-cn) | [](https://github.com/rust-lang-cn/rust-by-example-cn/commits) | [#home](https://jaywcjlove.github.io/rust-cn-document-for-docker/rust-by-example-cn/) +[Rust 程序设计语言中文版](https://github.com/rust-lang-cn/book-cn) | [](https://github.com/rust-lang-cn/book-cn/commits) | [#home](https://jaywcjlove.github.io/rust-cn-document-for-docker/book/) +[通过例子学 Rust 中文版](https://github.com/rust-lang-cn/rust-by-example-cn) | [](https://github.com/rust-lang-cn/rust-by-example-cn/commits) | [#home](https://jaywcjlove.github.io/rust-cn-document-for-docker/rust-by-example-cn/) [Rust 参考手册中文版](https://github.com/rust-lang-cn/reference-cn) | [](https://github.com/rust-lang-cn/reference-cn/commits) | [#home](https://jaywcjlove.github.io/rust-cn-document-for-docker/reference/) [RustDoc 手册中文版](https://github.com/rust-lang-cn/rustdoc-cn) | [](https://github.com/rust-lang-cn/rustdoc-cn/commits) | [#home](https://jaywcjlove.github.io/rust-cn-document-for-docker/rustdoc/) [Rust Cookbook 中文版](https://github.com/rust-lang-cn/rust-cookbook-cn) | [](https://github.com/rust-lang-cn/rust-cookbook-cn/commits) | [#home](https://jaywcjlove.github.io/rust-cn-document-for-docker/rust-cookbook/) [Rust 版本指南](https://github.com/rust-lang-cn/edition-guide-cn) | [](https://github.com/rust-lang-cn/edition-guide-cn/commits) | [#home](https://jaywcjlove.github.io/rust-cn-document-for-docker/edition-guide/) +[Rust 设计模式中文版](https://github.com/chuxiuhong/chuxiuhong-rust-patterns-zh) | [](https://github.com/chuxiuhong/chuxiuhong-rust-patterns-zh/commits) | [#home](https://jaywcjlove.github.io/rust-cn-document-for-docker/chuxiuhong-rust-patterns-zh/) [Cargo 手册](https://github.com/rust-lang-cn/cargo-cn) | [](https://github.com/rust-lang-cn/cargo-cn/commits) | [#home](https://jaywcjlove.github.io/rust-cn-document-for-docker/cargo/) [Rust 秘典](https://github.com/rust-lang-cn/nomicon-zh-Hans) | [](https://github.com/rust-lang-cn/nomicon-zh-Hans/commits) | [#home](https://jaywcjlove.github.io/rust-cn-document-for-docker/nomicon/) [Cargo 备忘清单(速查表)](https://github.com/jaywcjlove/reference) | [](https://github.com/jaywcjlove/reference/commits) | [#home](https://jaywcjlove.github.io/rust-cn-document-for-docker/quick-reference/docs/cargo.html) diff --git a/chuxiuhong-rust-patterns-zh/.env b/chuxiuhong-rust-patterns-zh/.env new file mode 100644 index 000000000..5cbca0062 --- /dev/null +++ b/chuxiuhong-rust-patterns-zh/.env @@ -0,0 +1 @@ +MDBOOK_VERSION=0.4.6 diff --git a/chuxiuhong-rust-patterns-zh/.gitignore b/chuxiuhong-rust-patterns-zh/.gitignore new file mode 100644 index 000000000..69d06fc11 --- /dev/null +++ b/chuxiuhong-rust-patterns-zh/.gitignore @@ -0,0 +1,4 @@ +# Generated output of mdbook +.idea +.DS_Store +book/ diff --git a/chuxiuhong-rust-patterns-zh/.markdownlint.yaml b/chuxiuhong-rust-patterns-zh/.markdownlint.yaml new file mode 100644 index 000000000..f3eb8fed1 --- /dev/null +++ b/chuxiuhong-rust-patterns-zh/.markdownlint.yaml @@ -0,0 +1,21 @@ +--- +# Use `#` for headers +MD003: + style: atx + +# Set maximum line length +MD013: + line_length: 99999 + +# Use `---` for horizontal rule +MD035: + style: --- + +# Use ``` for code blocks +MD046: + style: fenced +MD048: + style: backtick + +# See https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md for +# additional info diff --git a/chuxiuhong-rust-patterns-zh/.nojekyll b/chuxiuhong-rust-patterns-zh/.nojekyll new file mode 100644 index 000000000..f17311098 --- /dev/null +++ b/chuxiuhong-rust-patterns-zh/.nojekyll @@ -0,0 +1 @@ +This file makes sure that Github Pages doesn't process mdBook's output. diff --git a/chuxiuhong-rust-patterns-zh/404.html b/chuxiuhong-rust-patterns-zh/404.html new file mode 100644 index 000000000..dc447eb5a --- /dev/null +++ b/chuxiuhong-rust-patterns-zh/404.html @@ -0,0 +1,226 @@ + + +
+ + +This URL is invalid, sorry. Please use the navigation bar or search to continue.
+ +在一个系统中,每一个知识都必须有一个单一、明确、权威的表示。
+绝大多数系统简单时比复杂时工作的要好。因此简单性是设计中的关键目标,并且应该避免不必要的复杂性。
+一个实体应该尽可能少的与任何其他的结构或者特性(包括子组件)发生关系,符合“信息隐藏”的原则。
+软件设计者应该为软件组件定义规范、准确和可验证的接口,扩展了抽象数据类型的平凡定义,包括前置条件、后置条件和不变量。
+将数据与对该数据进行操作的方法捆绑在一起,或者限制对对象某些组件的直接访问。封装用于隐藏类中结构体对象的值或状态,防止未经授权地直接访问它们。
+函数不应该产生抽象的副作用,只允许命令(过程)产生副作用——Bertrand Meyer:《面向对象软件构造》
+系统的组件应该像人们期望的那样工作,而不应该给用户一个惊奇。
+模块必须与使用的语言单元相符合——Bertrand Meyer:《面向对象软件构造》
+一个模块的设计者应该努力使所有关于该模块的信息成为模块本身的一部分——Bertrand Meyer:《面向对象软件构造》
+一个模块提供的所有服务都应该通过一个统一的符号来提供,而这个符号并不表明它们是通过存储还是通过计算来实现的。——Bertrand Meyer:《面向对象软件构造》
+每当软件系统必须支持一组备选方案时,系统中应该只有一个模块知道它们的底细。——Bertrand Meyer:《面向对象软件构造》
+当存储一个对象时,必须将其所依赖的部分一起存储。每当检索机制检索以前存储的对象时,它还必须检索该对象的尚未检索到的所有依赖项。——Bertrand Meyer:《面向对象软件构造》
+ +补充有用内容的集合
+借用检查阻止了Rust用户开发不安全的代码,以此保证:只存在一个可变引用,或者(许多)不可变引用。如果编写的代码不符合这些条件,而开发者通过克隆变量来解决编译器错误,就会产生这种反模式。
+++#![allow(unused)] +fn main() { +// 定义任意变量 +let mut x = 5; + +// 借用 `x`(先clone) +let y = &mut (x.clone()); + +// 由于 x.clone(), x 并未被借用, 这行代码可以运行。 +println!("{}", x); + +// 用这个借用做点什么,防止因Rust优化直接砍掉这个借用 +*y += 1; +} +
用这种模式来解决借用检查令人困惑的问题是很诱人的,特别是对于初学者来说。然而,这有严重的后果。使用.clone()
会导致数据被复制。两者之间的任何变化都不会同步——因为会有两个完全独立的变量存在。
有种特殊情况—— Rc<T>
被设计为智能处理 clone
。它在内部确切管理着一份数据的副本,clone它只会clone引用。
还有Arc<T>
,它提供堆分配类型T的共享所有权。对Arc
调用.clone()
会得到新的Arc
实例,它指向和源Arc
相同的栈分配,增加引用计数。
一般来说,应该经过深思熟虑,充分了解其后果再clone。如果用clone消除借用检查器报错,很可能你使用了这种反模式。
+即使.clone()
是坏模式的预兆,有时编写低效率的代码是可以的,比如这些情况时:
如果你怀疑做了不必要的clone,在评估是否需要clone之前,先去弄懂《Rust Book》的所有权章节。
+此外要保证一直给你的项目跑cargo clippy
,它可以判断一些.clone()
调用不必要的情况,比如甲,乙,丙或者丁.
#![deny(warnings)]
一个善意的库作者想要确保他们的代码在编译时不会产生警告。因此他们在库里标注以下内容:
+++#![allow(unused)] +#![deny(warnings)] + +fn main() { +// 一切安好 +} +
它很短,如果有什么错误就停止编译。
+通过禁用编译器生成警告,库的作者放弃了Rust的稳定性。有时新的特性或者旧的不合格的特性需要被更改,因此,将会在一段宽限期内给出警告,之后变成禁用。
+举例来说,一个类型可以有两个具有相同方法的实现。这被认为是一个坏主意,但是为了顺利过渡,引入 overlapping-inherent-impls
提示来警告那些在将来版本中出现严重错误的人。
而且有时API会被弃用,所以使用它们会发出警告。
+所有的这些在改变时都可能破坏编译过程。
+此外,除非这个删除注释,否则不能再使用提供额外警告的库。(例如rust-clippy)这可以通过--cap-lints缓解。--cap-lints=warn
命令行参数将所有的deny
提示的错误转换为警告。
解决这个问题有两种方法:第一种,我们可以将编译设置与代码解耦;第二种,我们可以显式地命名要拒绝的警告。
+下面这个命令行参数将会带着所有关闭的警告进行编译:
+RUSTFLAGS="-D warnings" cargo build
任何独立开发者都可以这样做(或者设置到持续集成工具,如Travis,但是记住当某些内容发生变化时,可能会破坏编译)。
+或者,我们可以指定我们想要在代码中关闭的警告。下面是警告提示列表(Rustc 1.48.0):
+#[deny(bad-style,
+ const-err,
+ dead-code,
+ improper-ctypes,
+ non-shorthand-field-patterns,
+ no-mangle-generic-items,
+ overflowing-literals,
+ path-statements ,
+ patterns-in-fns-without-body,
+ private-in-public,
+ unconditional-recursion,
+ unused,
+ unused-allocation,
+ unused-comparisons,
+ unused-parens,
+ while-true)]
+
+此外,下面的提示是推荐关闭的:
+#[deny(missing-debug-implementations,
+ missing-docs,
+ trivial-casts,
+ trivial-numeric-casts,
+ unused-extern-crates,
+ unused-import-braces,
+ unused-qualifications,
+ unused-results)]
+
+有时可能需要增加missing-copy-implementations
到清单中。
请注意,我们没有关闭deprecated
提示,因为可以肯定的是,将来会有更多不推荐的API。
rustc -W help
for a list of lints on your system. Also type
+rustc --help
for a general list of optionsDeref
多态滥用Deref
特性,模拟结构体之间的继承,从而重用方法。
有时我们想要从诸如Java之类的面向对象语言中模拟以下常见模式:
+class Foo {
+ void m() { ... }
+}
+
+class Bar extends Foo {}
+
+public static void main(String[] args) {
+ Bar b = new Bar();
+ b.m();
+}
+
+我们可以用deref多态反模式来实现:
+use std::ops::Deref;
+
+struct Foo {}
+
+impl Foo {
+ fn m(&self) {
+ //..
+ }
+
+}
+
+struct Bar {
+ f: Foo,
+}
+
+impl Deref for Bar {
+ type Target = Foo;
+ fn deref(&self) -> &Foo {
+ &self.f
+ }
+}
+
+fn main() {
+ let b = Bar { f: Foo {} };
+ b.m();
+}
+
+Rust中没有结构体的继承。取而代之的是我们使用组合方式在Bar
内包含Foo
(因为字段是一个值,它在内部存储),因此它们都是字段,拥有和Java版本相同的内存布局。(如果你想要确保这一点,可以用#[repr(C)]
)。
为了使方法调用有效,我们为Bar
实现了Deref
特性,生成目标为Foo
(返回的是内置的Foo
字段)。这就相当于当我们对Bar
解引用的时候我们就会获取到一个Foo
对象。这是非常诡异的,解引用通常是通过一个类型的引用获取这个类型的值,然而这里却是两种不相关的类型。不过,因为点运算符是隐式的解引用,所以方法调用时也将搜索Foo
类型的方法。
节省了一些样板代码,例如:
+impl Bar {
+ fn m(&self) {
+ self.f.m()
+ }
+}
+
+最重要的是这是一个令人惊讶的习惯用法——未来的程序员在阅读这些代码时不会期望发生这种情况。这是因为我们滥用了Deref
特性,而不是按预期的那样去使用。同时也是因为这里的机制是完全隐式的。
这种模式并没有实现像Java或者C++里的继承。此外,对Foo
实现的特性也不会自动地适用于Boo
,所以这种模式对于边界检查和泛型编程来说非常差。
使用这种模式,就self
而言,给出了与大多数面向对象语言截然不同的语义。通常它仍是子类型的引用,在这种模式下它将是定义方法的“类”。
最后,这种模式仅支持单继承,并且没有接口的概念、基于类的隐私性或者其他的与继承相关的特性。因此,对于习惯于Java那种继承的程序员来说,它提供了一种“惊喜”。
+这没有好的替代方案。根据具体情况,最好用特性重新实现,或者手动编写分发给Foo
的方法。我们确实打算为Rust添加一种像这样的继承机制,
+但是可能需要一段时间才能进入稳定版本的Rust。看这些 博客、
+文章
+和这个RFC issue 来了解更多细节。
Deref
特性是被设计用来实现自定义指针类型的。它的用处是将T
的引用转变为T
的值,而不是在类型间转换。遗憾的是,这不是(或者说无法)靠特性定义来强制执行。
Rust尝试在显式和隐式机制之间做出权衡,更偏向于类型间进行显式转换。点运算符自动解引用是出于符合人体工程学的角度做的隐式设计,其目的仅限于有限的间接程度,而不是任意类型之间做隐式转换。
+Deref
trait.Rust的类型系统设计的更像函数式语言(比如Haskell),而非指令式语言如Java和C++。因此,Rust可以将许多编程问题转换成“静态类型”问题。这是选择函数式语言时最大的亮点之一,对于Rust的许多编译时保证来说是至关重要的。
+这个概念的一个关键部分正是泛型的工作方式。在C++与Java中,举个例子,泛型是编译器的一种元编程结构。C++的vector<int>
和vector<char>
只是vector
类型(叫模板
)的同一模板代码的两个不同副本,其中填充了两种不同的类型。
在Rust中,泛型参数如同函数式语言中的“类型类约束”,而最终用户填写的每个不同的参数实际上都会改变类型。换句话说,Vec<isize>
和Vec<char>
是两个不同的类型,它们被类型系统识别为不同的类型。
这被称作单态化,不同类型以多态代码创建。这种特殊行为需要用impl
块指定泛型参数:泛型的不同值会导致不同的类型,而不同的类型可以有不同的impl
块。
在面向对象语言中,类可以从父类那里继承行为。实际上,这不仅允许将额外的行为附加到类型类的特定成员上,还允许附加额外的行为。
+最接近的是Javascript和Python中的运行时多态性,新的成员可以被任何构造函数随意添加到对象中。然而,与这些语言不同,Rust的所有额外方法在使用时都可以进行类型检查,因为它们的泛型是静态定义的。这使得它们在保持安全的同时更具有实用性。
+想象你正在为实验室机器集群设计存储服务器。因为涉及的软件,有两个不同的协议需要你支持。BOOTP(用于PXE网络启动),和NFS(用于远程安装存储)。
+你的目标是一个用Rust编写的程序,它可以处理这两种请求。它将有协议handler,监听两种请求。此外,主应用逻辑要允许实验室管理员配置实际文件的存储和安全控制。
+不管来自什么协议,实验室机器对文件的请求都包含相同的基本信息:一个认证方法,和一个要检索的文件名。一个直接的实现会是这样的:
+
+enum AuthInfo {
+ Nfs(crate::nfs::AuthInfo),
+ Bootp(crate::bootp::AuthInfo),
+}
+
+struct FileDownloadRequest {
+ file_name: PathBuf,
+ authentication: AuthInfo,
+}
+
+这种设计可能工作得很好。但现在,假设你需要支持添加协议特定的元数据。例如,对于NFS,你想确定他们的挂载点是什么,以便执行额外的安全规则。
+当前结构的设计方式将协议的决定权留给了运行时。这也就是说,任何适用于一种协议而非另一种协议的方法都需要程序员进行运行时检查。
+下面是获取NFS挂载点的情况:
+struct FileDownloadRequest {
+ file_name: PathBuf,
+ authentication: AuthInfo,
+ mount_point: Option<PathBuf>,
+}
+
+impl FileDownloadRequest {
+ // ... 其他方法 ...
+
+ /// 如果有NFS请求,获取一个NFS挂载点。
+ /// 否则返回None。
+ pub fn mount_point(&self) -> Option<&Path> {
+ self.mount_point.as_ref()
+ }
+}
+
+每个mount_point()
的调用者都必须检查None
并编写代码来处理它。就算他们知道,在一个给定的代码路径中只有NFS请求被使用。
如果不同的请求类型被弄混,引起编译时错误会理想。毕竟,用户的整个代码路径,包括他们使用的库中那些函数,都会知道一个请求是NFS请求还是BOOTP请求。
+在Rust中,这是可能的!解决方案是加个泛型,分割API。
+这样子:
++use std::path::{Path, PathBuf}; + +mod nfs { + #[derive(Clone)] + pub(crate) struct AuthInfo(String); // NFS会话管理给省了 +} + +mod bootp { + pub(crate) struct AuthInfo(); // bootp没验证机制 +} + +// private module, lest outside users invent their own protocol kinds! +mod proto_trait { + use std::path::{Path, PathBuf}; + use super::{bootp, nfs}; + + pub(crate) trait ProtoKind { + type AuthInfo; + fn auth_info(&self) -> Self::AuthInfo; + } + + pub struct Nfs { + auth: nfs::AuthInfo, + mount_point: PathBuf, + } + + impl Nfs { + pub(crate) fn mount_point(&self) -> &Path { + &self.mount_point + } + } + + impl ProtoKind for Nfs { + type AuthInfo = nfs::AuthInfo; + fn auth_info(&self) -> Self::AuthInfo { + self.auth.clone() + } + } + + pub struct Bootp(); // 没有附加元数据 + + impl ProtoKind for Bootp { + type AuthInfo = bootp::AuthInfo; + fn auth_info(&self) -> Self::AuthInfo { + bootp::AuthInfo() + } + } +} + +use proto_trait::ProtoKind; // 保持内部,以防止 impl +pub use proto_trait::{Nfs, Bootp}; // 重导出,这样调用者能看到它们 + +struct FileDownloadRequest<P: ProtoKind> { + file_name: PathBuf, + protocol: P, +} + +// 把所有共同的API部分放进一个泛型实现块 +impl<P: ProtoKind> FileDownloadRequest<P> { + fn file_path(&self) -> &Path { + &self.file_name + } + + fn auth_info(&self) -> P::AuthInfo { + self.protocol.auth_info() + } +} + +// all protocol-specific impls go into their own block +impl FileDownloadRequest<Nfs> { + fn mount_point(&self) -> &Path { + self.protocol.mount_point() + } +} + +fn main() { + // 你代码扔这儿 +} +
对于这个方法,如果用户搞错了,使用了错误的类型:
+fn main() {
+ let mut socket = crate::bootp::listen()?;
+ while let Some(request) = socket.next_request()? {
+ match request.mount_point().as_ref()
+ "/secure" => socket.send("Access denied"),
+ _ => {} // 继续下去...
+ }
+ // 剩余代码部分放这里
+ }
+}
+
+会得到一个类型错误。类型FileDownloadRequest<Bootp>
没实现mount_point()
,只有类型FileDownloadRequest<Nfs>
实现了。而且说到底,那是NFS模块创建的,不是BOOTP!
首先,它可以去重多个状态下共有的字段。通过使非共享字段成为泛型字段,它们只需要实现一次。
+其次,它使impl
块更容易阅读,因为它们是按状态分解的。所有状态下通用的方法都在一个块中输入一次,而某个状态下特有的方法则在一个单独的块中。
这两种情况都意味着代码行数更少,而且更有条理。
+目前这将增加二进制文件大小,这是编译器实现单态化的方式造成的。希望这种实现方式在未来能够得到改善。
+如果一个类型由于构造或部分初始化,似乎需要一个 “切分的API”,可以考虑用Builder模式代替。
+如果类型之间的API不发生变化,只有行为发生变化,那么最好使用策略来代替。
+这种模式在整个标准库中都有应用。
+Vec<u8>
can be cast from a String, unlike every other type of Vec<T>
.1Ord
trait.2to_string
method was specialized for Cow
only of type str
.3它也被一些流行的crate使用,用以改进API灵活性:
+The embedded-hal
ecosystem used for embedded devices makes extensive use of this pattern. For example, it allows statically verifying the configuration of device registers used to control embedded pins. When a pin is put into a mode, it returns a Pin<MODE>
struct, whose generic determines the functions usable in that mode, which are not on the Pin
itself. ^4
hyper
HTTP客户端库用它为不同可插拔请求导出富API。Clients with different connectors have different methods on them as well as different trait implementations, while a core set of methods apply to any connector. ^5
The "type state" pattern -- where an object gains and loses API based on an internal state or invariant -- is implemented in Rust using the same basic concept, and a slightly different technique. ^6
+https://docs.rs/stm32f30x-hal/0.1.0/stm32f30x_hal/gpio/gpioa/struct.PA0.html
+https://docs.rs/hyper/0.14.5/hyper/client/struct.Client.html
+The Case for the Type State Pattern and Rusty Typestate Series (an extensive thesis)
+ +Rust是一种命令式语言,但是它也遵循很多函数式语言的范式。
+++ +在计算机科学中,函数式编程是一种通过应用和组合函数来编程的一种范式。它是一种声明式编程范式,其中函数的定义是每个表达式返回一个值的表达式树,而不是一系列改变程序状态的命令语句。
+
当出于一个命令式的背景时,理解函数式程序最大的障碍之一就是思维的转变。命令式程序说明了如何做,然而声明式程序说明做了什么。让我们用对1到10求和的例子来说明这一点。
+++#![allow(unused)] +fn main() { +let mut sum = 0; +for i in 1..11 { + sum += i; +} +println!("{}", sum); +} +
在命令式程序中,我们必须用编译器来查看发生了什么。这里sum
起始为0,然后我们在1到10范围内循环,每次循环中我们加上对应的值,最后输出。
i | sum |
---|---|
1 | 1 |
2 | 3 |
3 | 6 |
4 | 10 |
5 | 15 |
6 | 21 |
7 | 28 |
8 | 36 |
9 | 45 |
10 | 55 |
这就是我们大多数人开始编程的方式。我们了解到程序是一些操作步骤的集合。
+++#![allow(unused)] +fn main() { +println!("{}", (1..11).fold(0, |a, b| a + b)); +} +
哇哦!这真是不一样!这里发生了啥?记住声明式程序说明了做了什么,而不是如何去做。fold
是一个 组合函数的函数。这个名字来自于Haskell。
这里,我们组合了在1到10范围内的加法函数(闭包|a,b| a + b
)。0
是起始点,所以a
最开始是0
,b
是范围的第一个元素1
。结果是
+0 + 1 = 1
。所以现在我们再次fold
,a = 1
、b = 2
下一个结果是1 + 2 = 3
。这个过程一直持续到范围内最后一个元素10
。
a | b | result |
---|---|---|
0 | 1 | 1 |
1 | 2 | 3 |
3 | 3 | 6 |
6 | 4 | 10 |
10 | 5 | 15 |
15 | 6 | 21 |
21 | 7 | 28 |
28 | 8 | 36 |
36 | 9 | 45 |
45 | 10 | 55 |
当你为函数选择参数类型时,使用带强制隐式转换的目标会增加你代码的复杂度。在这种情况下,函数将会接受更多的输入参数类型。
+使用可切片类型或者胖指针类型没有限制。事实上,你应该总是用借用类型(borrowed type),
+而不是自有数据类型的借用(borrowing the owned type)。
+例如&str
而非 &String
, &[T]
而非 &Vec<T>
, 或者 &T
而非 &Box<T>
.
当自有数据结构(owned type)的实例已经提供了一个访问数据的间接层时,使用借用类型可以让你避免增加间接层。举例来说,String
类型有一层间接层,所以&String
将有两个间接层。我们可以用&Str
来避免这种情况,无论何时调用函数,强制&String
转换为&Str
。
在这个例子中,我们将说明使用&String
与&Str
作为函数参数的区别。这个思路用于对比&Vec<T>
和 &[T]
、 &T
和&Box<T>
也适用。
考虑一个我们想要确定一个单词是否包含3个连续的元音字母的例子。我们不需要获得字符串的所有权,所以我们将获取一个引用。
+代码如下:
++fn three_vowels(word: &String) -> bool { + let mut vowel_count = 0; + for c in word.chars() { + match c { + 'a' | 'e' | 'i' | 'o' | 'u' => { + vowel_count += 1; + if vowel_count >= 3 { + return true + } + } + _ => vowel_count = 0 + } + } + false +} + +fn main() { + let ferris = "Ferris".to_string(); + let curious = "Curious".to_string(); + println!("{}: {}", ferris, three_vowels(&ferris)); + println!("{}: {}", curious, three_vowels(&curious)); + + // 至此运行正常,但下面两行就会失败: + // println!("Ferris: {}", three_vowels("Ferris")); + // println!("Curious: {}", three_vowels("Curious")); + +} +
这里能够正常运行是因为我们传的参数是&String
类型。最后注释的两行运行失败是因为&str
类型不能强制隐式转换为&String
类型。我们靠修改参数类型即可轻松解决。
例如,如果我们把函数定义改为:
+fn three_vowels(word: &str) -> bool {
+
+那么两种版本都能编译通过并打印相同的输出。
+Ferris: false
+Curious: true
+
+等等,这并不是全部!这里还有点说道。你可能对自己说,这没啥事,我永远不会用&'static str
当输入参数(像我们刚刚输入"Ferris"
这种情况)。即使不考虑这个特殊例子,你还会发现使用&Str
类型将会比&String
类型带给你更大的灵活性。
让我们现在考虑一个例子:当给定一个句子,我们需确定句子中是否有单词包含3个连续的元音字母。我们也许应该用刚刚写好的函数来对句子中的每个单词做判断。 +An example of this could look like this:
++fn three_vowels(word: &str) -> bool { + let mut vowel_count = 0; + for c in word.chars() { + match c { + 'a' | 'e' | 'i' | 'o' | 'u' => { + vowel_count += 1; + if vowel_count >= 3 { + return true + } + } + _ => vowel_count = 0 + } + } + false +} + +fn main() { + let sentence_string = + "Once upon a time, there was a friendly curious crab named Ferris".to_string(); + for word in sentence_string.split(' ') { + if three_vowels(word) { + println!("{} has three consecutive vowels!", word); + } + } +} +
运行我们&Str
参数函数定义版本会输出:
curious has three consecutive vowels!
+
+然而,使用&String
版本的函数无法在这个例子中使用。这是因为字符串的切片是&Str
类型而非&String
类型,其转换为&String
类型不是隐性的,然而&String
转换为&Str
是低开销且隐性的。
String
and &str
see
+this blog series (2015)
+by Herman J. Radtke III