Elegant Library APIs in Rust
The existence of libraries with nice, user-friendly interfaces is one of the most important factors when choosing a programming language. Here are some tips on how to write libraries with nice APIs in Rust. (Many of the points also apply to other languages.)
具有良好用户界面的库的存在是选择编程语言时最重要的因素之一。以下是有关如何在 Rust 中编写具有良好 API 的库的一些建议。(许多观点也适用于其他语言。)
You can also watch my talk at Rustfest 2017 about this!
您还可以观看我在 2017 年 Rustfest 关于这个主题的演讲!
Update 2017-04-27: Since writing that post,
@brson of the Rust Libs Team has published a pretty comprehensive
Rust API Guidelines
document that includes my advice here and a lot more.
更新日期 2017-04-27:自从撰写该帖子以来,Rust Libs 团队的@brson 发布了一份相当全面的 Rust API 指南文档,其中包括了我在这里的建议以及更多内容。
Update 2020-06-01: This post is quite a few years old by now!
Most of the patterns are still valid and actively used in Rust today,
but the language has also evolved quite a bit and enabled new patterns
that are not discussed here.
I’ve updated some of syntax and crate recommendations
but otherwise kept the post as it was in 2016.
更新 2020-06-01:这篇文章现在已经有好几年的历史了!大部分模式在 Rust 中仍然有效,并且今天仍然被积极使用,但是语言也发展了很多,并且启用了一些新的模式,这些模式在这里没有讨论。我已经更新了一些语法和包推荐,但除此之外,这篇文章仍然保持着 2016 年的原样。
Contents 目录
- What makes an API elegant?
API 有哪些优雅之处? - Techniques 技术
- Consistent names 一致的命名
- Doc tests 文档测试
- Don’t “stringly type” your API
不要“字符串类型”您的 API - Error handling 错误处理
- Public type aliases 公共类型别名
- Use conversion traits
使用转换特性 - Laziness 懒惰
- Convenience traits 便利特性
- Custom traits for input parameters
输入参数的自定义特性 - Extension traits 扩展特性
- Builder pattern 生成器模式
- Session types 会话类型
- Use lifetimes well
善用生命周期
- Case Studies 案例研究
- Other design patterns 其他设计模式
What makes an API elegant?
API 优雅的特质是什么?
- Code using the API is easily readable thanks to obvious, self-explanatory method names.
由于明显且不言自明的方法名称,使用 API 的代码非常易读。 - Guessable method names also help when using the API, so there is less need to read documentation.
可猜测的方法名称在使用 API 时也很有帮助,因此减少了阅读文档的需要。 - Everything has at least some documentation and a small code example.
一切至少都有一些文档和一个小的代码示例。 - Users of the API need to write little boilerplate code to use it, as
API 的用户需要编写很少的样板代码来使用它,因为- methods accept a wide range of valid input types (where conversions are obvious), and
方法接受各种有效输入类型(其中转换是明显的),并 - shortcuts to get the ‘usual’ stuff done quickly are available.
可以快速完成“常规”工作的快捷方式可用。
- methods accept a wide range of valid input types (where conversions are obvious), and
- Types are cleverly used to prevent logic errors, but don’t get in your way too much.
类型被巧妙地使用以防止逻辑错误,但不会过多地妨碍你。 - Returned errors are useful, and panic cases are clearly documented.
返回的错误很有用,紧急情况有明确的文档记录。
Techniques 技术
Consistent names 一致的命名
There are a few Rust RFCs that describe the naming scheme of the standard library. You should follow them to make your library’s API feel familiar for users.
有一些 Rust RFC 描述了标准库的命名方案。您应该遵循它们,使您的库的 API 对用户感觉熟悉。
- RFC 199 explains that you should use
mut
,move
, orref
as suffixes to differentiate methods based on the mutability of their parameters.
RFC 199 解释了应该使用mut
,move
或ref
作为后缀,以区分基于参数的可变性的方法。 - RFC 344 defines some interesting conventions, like
RFC 344 定义了一些有趣的约定,比如- how to refer to types in method names (e.g.,
&mut [T]
becomesmut_slice
, and*mut T
becomesmut_ptr
),
如何在方法名称中引用类型(例如,&mut [T]
变为mut_slice
,*mut T
变为mut_ptr
), - how to call methods that return iterators,
如何调用返回迭代器的方法 - that getters methods should be called
field_name
while setter methods should beset_field_name
,
getter 方法应该被调用field_name
,而 setter 方法应该是set_field_name
, - and how to name traits: “Prefer (transitive) verbs, nouns, and then adjectives; avoid grammatical suffixes (like able)”, but also “if there is a single method that is the dominant functionality of the trait, consider using the same name for the trait itself”.
如何命名特征:“首选(及物)动词、名词,然后形容词;避免使用语法后缀(如 able)”,但也“如果有一个方法是特征的主要功能,考虑使用相同的名称作为特征本身”。
- how to refer to types in method names (e.g.,
- RFC 430 describes some general casing conventions (tl;dr
CamelCase
for type-level stuff,snake_case
for value-level stuff).
RFC 430 描述了一些一般的大小写约定(太长不看CamelCase
用于类型级别的内容,snake_case
用于值级别的内容)。 - RFC 445 wants you to add an
Ext
suffix to extension traits.
RFC 445 要求您向扩展特性添加Ext
后缀。
More method name conventions
更多方法命名约定
In addition to what RFC 199 and RFC 344 (see above) define, there are a few more conventions around what method names to choose, which seem to not be represented in RFCs (yet). These are mostly documented in the old Rust style guidelines and in @llogiq’s post Rustic Bits as well as clippy’s wrong_self_convention
lint. Here’s a summary:
除了 RFC 199 和 RFC 344(见上文)定义的内容外,还有一些关于选择方法名称的约定,似乎尚未在 RFC 中表示。这些大多数都记录在旧的 Rust 风格指南和 @llogiq 的帖子 Rustic Bits 以及 clippy 的 wrong_self_convention
lint 中。以下是摘要:
Method name 方法名称 | Parameters 参数 | Notes 笔记 | Examples 示例 |
---|---|---|---|
new |
no self, usually ≥ 11 没有自身,通常≥ 1 1 |
Constructor, also cf. Default 构造函数,另见 Default |
Box::new , std::net::Ipv4Addr::new |
with_... |
no self, ≥ 1 无自身,≥ 1 |
Alternative constructors 替代构造函数 |
Vec::with_capacity , regex::Regex::with_size_limit |
from_... |
1 | cf. conversion traits cf. 转换特性 | String::from_utf8_lossy |
as_... |
&self |
Free conversion, gives a view into data 免费转换,提供数据视图 |
str::as_bytes , uuid::Uuid::as_bytes |
to_... |
&self |
Expensive conversion 昂贵的转换 | str::to_string , std::path::Path::to_str |
into_... |
self (consumes) self (消耗) |
Potentially expensive conversion, cf. conversion traits 潜在昂贵的转换,参见转换特性 |
std::fs::File::into_raw_fd |
is_... |
&self (or none) &self (或无) |
Should probably return a bool 应该返回 bool |
slice::is_empty , Result::is_ok , std::path::Path::is_file |
has_... |
&self (or none) &self (或无) |
Should probably return a bool 应该返回 bool |
regex_syntax::Expr::has_bytes |
Doc tests 文档测试
Write documentation with example code showing how to use your API and get automatic tests for free – Two birds, one stone. You can read more about in the documentation chapter of the official book.
编写文档,其中包含示例代码,展示如何使用您的 API,并免费获得自动测试 - 一箭双雕。您可以在官方书籍的文档章节中了解更多信息。
/// Manipulate a number by magic
///
/// # Examples
///
/// ```rust
/// assert_eq!(min( 0, 14), 0);
/// assert_eq!(min( 0, -127), -127);
/// assert_eq!(min(42, 666), 42);
/// ```
fn min(lhs: i32, rhs: i32) -> i32 {
if lhs < rhs { lhs } else { rhs }
}
To enforce that every public API item is documented, use #![deny(missing_docs)]
. You might also be interested in my post suggesting conventions for formatting Rust documentation.
要强制要求每个公共 API 项都有文档,请使用 #![deny(missing_docs)]
。您可能还对我提出的有关格式化 Rust 文档的约定的帖子感兴趣。
Don’t “stringly type” your API
不要“字符串类型”您的 API
Coming from dynamically typed languages, you might be tempted to use strings with specific values in various places.
来自动态类型语言,您可能会被诱惑在各个地方使用具有特定值的字符串。
For example: You want a function to print some input text in a color, so you use a parameter color
of type &str
. That also means you expect your users to write one of the names from a limited number of color names (like ["red", "green", "blue", "light golden rod yellow"]
).
例如:您希望一个函数以一种颜色打印一些输入文本,因此您使用类型为 &str
的参数 color
。这也意味着您希望用户从有限数量的颜色名称之一中选择一个名称(例如 ["red", "green", "blue", "light golden rod yellow"]
)。
This is not what you should do in Rust! If you know all possible variants beforehand, use an enum
. This way, you don’t need to parse/pattern match the string – and deal with possible errors – but can be sure that a user of your API can only ever supply valid inputs2.
这不是你在 Rust 中应该做的事情!如果你事先知道所有可能的变体,请使用一个 enum
。这样,你就不需要解析/模式匹配字符串 - 也不需要处理可能的错误 - 而且可以确保你的 API 用户只能提供有效的输入 2 。
enum Color { Red, Green, Blue, LightGoldenRodYellow }
fn color_me(input: &str, color: Color) { /* ... */ }
fn main() {
color_me("surprised", Color::Blue);
}
A module full of constants
一个充满常量的模块
Alternatively, if you have a more complex value you want to express you can define a new struct
and then define a bunch of constants with common values. If you put the constants into a public module, your users can access them using similar syntax as when using an enum variant.
或者,如果您有更复杂的值要表达,可以定义一个新的 struct
,然后定义一堆具有常见值的常量。如果将这些常量放入公共模块中,用户可以使用类似于使用枚举变体时的语法来访问它们。
pub mod output_options {
pub struct OutputOptions { /* ... */ }
impl OutputOptions { fn new(/* ... */) -> OutputOptions { /* ... */ } }
pub const DEFAULT: OutputOptions = OutputOptions { /* ... */ };
pub const SLIM: OutputOptions = OutputOptions { /* ... */ };
pub const PRETTY: OutputOptions = OutputOptions { /* ... */ };
}
fn output(f: &Foo, opts: OutputOptions) { /* ... */ }
fn main() {
let foo = Foo::new();
output(foo, output_options::PRETTY);
}
Actually parsing a string with FromStr
实际上使用 FromStr
解析字符串
There may still be cases where users of your API actually have strings, e.g. from reading environment variables or by taking their user input – i.e., they didn’t write (static) strings themselves in their code to give to your API (which is what we try to prevent). In those cases it makes sense to have a look at what the FromStr
trait can give you, which abstracts over the concept of parsing a string into a Rust data type.
您的 API 的用户实际上可能仍然有字符串的情况,例如从读取环境变量或接受用户输入 - 也就是说,他们没有在代码中自己编写(静态)字符串来提供给您的 API(这是我们试图防止的)。在这些情况下,有必要查看 FromStr
特性可以为您提供的内容,该特性抽象了将字符串解析为 Rust 数据类型的概念。
If all you want to do is map a string with an enum variant name to the right enum variant, you can adapt this macro (from this tweet; there might also be a crate for that).
如果您只想要将字符串与枚举变体名称映射到正确的枚举变体,您可以适应这个宏(来自这个推文;可能也有一个适用于此的板条箱)。
Depending on your API, you could also decide to have your users deal with parsing the string. If you supply the right types and implementations, it shouldn’t be difficult (but needs to be documented nonetheless).
根据您的 API,您还可以决定让用户处理字符串的解析。如果提供正确的类型和实现,这不应该很困难(但仍然需要记录文档)。
// Option A: You do the parsing
fn output_a(f: &Foo, color: &str) -> Result<Bar, ParseError> {
// This shadows the `options` name with the parsed type
let color: Color = options.parse()?;
f.to_bar(&color)
}
// Option B: User does the parsing
fn output_b(f: &Foo, color: &Color) -> Bar {
f.to_bar(color)
}
fn main() {
let foo = Foo::new();
// Option A: You do the parsing, user needs to deal with API error
output_a(foo, "Green").expect("Error :(");
// Option B: User has correct type, no need to deal with error here
output_b(foo, Color::Green);
// Option B: User has string, needs to parse and deal with parse error
output_b(foo, "Green".parse().except("Parse error!"));
}
Error handling 错误处理
The official book has an awesome chapter on error handling.
官方书籍中有一章关于错误处理的精彩内容。
There are a few crates to reduce the boilerplate needed for good error handling,
e.g., anyhow (dynamic error type with methods for annotating and chaining errors),
and thiserror (makes creating custom error types easy).
有一些箱子可以减少所需的样板文件,以便进行良好的错误处理,例如,anyhow(具有用于注释和链接错误的方法的动态错误类型),以及 thiserror(使创建自定义错误类型变得容易)。
Public type aliases 公共类型别名
If your internal code uses generic types with the same type parameters over and over again, it makes sense to use a type alias. If you also expose those types to your users, you should expose (and document) the type alias as well.
如果您的内部代码一遍又一遍地使用相同的类型参数的泛型类型,那么使用类型别名是有意义的。如果您还向用户公开这些类型,您应该同时公开(并记录)类型别名。
A common case where this is used is Result<T, E>
types, where the error case (E
) is fixed. For example, std::io::Result<T>
is an alias for Result<T, std::io::Error>
, std::fmt::Result
is an alias for Result<(), std::fmt::Error>
, and serde_json::error::Result<T>
is an alias for Result<T, serde_json::error::Error>
.
常见的情况是使用 Result<T, E>
类型,其中错误情况( E
)已经修复。例如, std::io::Result<T>
是 Result<T, std::io::Error>
的别名, std::fmt::Result
是 Result<(), std::fmt::Error>
的别名, serde_json::error::Result<T>
是 Result<T, serde_json::error::Error>
的别名。
Use conversion traits 使用转换特性
It’s good practice to never have &String
or &Vec<T>
as input parameters and instead use &str
and &[T]
as they allow more types to be passed in. (Basically, everything that deref
s to a (string) slice).
最好的做法是永远不要将 &String
或 &Vec<T>
作为输入参数,而是使用 &str
和 &[T]
,因为它们允许传递更多类型。(基本上,所有 deref
都转换为(字符串)切片)。
We can apply the same idea at a more abstract level: Instead of using concrete types for input parameters, try to use generics with precise constraints. The downside of this is that the documentation will be less readable as it will be full of generics with complex constraints!
我们可以在更抽象的层面应用相同的思想:不要使用具体类型作为输入参数,尝试使用具有精确约束的泛型。这样做的缺点是文档会变得不太可读,因为它将充满具有复杂约束的泛型!
std::convert
has some goodies for that:
std::convert
有一些好东西可以用:
AsMut
: A cheap, mutable reference-to-mutable reference conversion.
AsMut
:便宜的、可变的引用到可变引用的转换。AsRef
: A cheap, reference-to-reference conversion.
AsRef
:一种廉价的引用到引用转换。From
: Construct Self via a conversion.
From
:通过转换构建自身。Into
: A conversion that consumes self, which may or may not be expensive.
Into
:消耗自身的转换,可能是昂贵的,也可能不是。TryFrom
: Attempt to construct Self via a conversion. (Unstable as of Rust 1.10)
TryFrom
:尝试通过转换构造 Self。(截至 Rust 1.10 仍不稳定)TryInto
: An attempted conversion that consumes self, which may or may not be expensive. (Unstable as of Rust 1.10)
TryInto
:尝试转换,会消耗自身,可能会很昂贵。(截至 Rust 1.10 版本仍不稳定)
You might also enjoy this article about Convenient and idiomatic conversions in Rust.
您可能也会喜欢这篇关于 Rust 中方便和惯用转换的文章。
Cow 牛
If you are dealing with a lot of things that may or may not need to be allocated, you should also look into Cow<'a, B>
which allows you to abstract over borrowed and owned data.
如果您正在处理许多可能需要分配或不需要分配的事物,您还应该查看 Cow<'a, B>
,它允许您对借用和拥有的数据进行抽象。
Example: std::convert::Into
示例: std::convert::Into
fn foo(p: PathBuf) |
fn foo<P: Into<PathBuf>>(p: P) |
---|---|
Users needs to convert their data to a PathBuf 用户需要将他们的数据转换为 PathBuf |
Library can call .into() for them库可以为它们调用 .into() |
User does allocation 用户执行分配 | Less obvious: Library might need to do allocation 不太明显:库可能需要进行分配 |
User needs to care about what a PathBuf is and how to get one用户需要关心 PathBuf 是什么以及如何获取一个 |
User can just give a String or an OsString or a PathBuf and be happy用户只需给出 String 、 OsString 或 PathBuf 就可以开心 |
Into<Option<_>>
This pull request, which landed in Rust 1.12, adds an impl<T> From<T> for Option<T>
. While only a few lines long this allows you to write APIs that can be called without typing Some(…)
all the time.
这个拉取请求已经合并到 Rust 1.12 中,添加了一个 impl<T> From<T> for Option<T>
。虽然只有几行代码,但这样可以让你编写的 API 在调用时无需一直输入 Some(…)
。
// Easy for API author, easy to read documentation
fn foo(lorem: &str, ipsum: Option<i32>, dolor: Option<i32>, sit: Option<i32>) {
println!("{}", lorem);
}
fn main() {
foo("bar", None, None, None); // That looks weird
foo("bar", Some(42), None, None); // Okay
foo("bar", Some(42), Some(1337), Some(-1)); // Halp! Too… much… Some…!
}
// A bit more typing for the API author.
// (Sadly, the parameters need to be specified individually – or Rust would
// infer the concrete type from the first parameter given. This reads not as
// nicely, and documentation might not look as pretty as before.)
fn foo<I, D, S>(lorem: &str, ipsum: I, dolor: D, sit: S) where
I: Into<Option<i32>>,
D: Into<Option<i32>>,
S: Into<Option<i32>>,
{
println!("{}", lorem);
}
fn main() {
foo("bar", None, None, None); // Still weird
foo("bar", 42, None, None); // Okay
foo("bar", 42, 1337, -1); // Wow, that's nice! Gimme more APIs like this!
}
A note on possibly long compile times
关于可能较长的编译时间的说明
If you have: 如果您有:
- a lot of type parameters (e.g. for the conversion traits)
许多类型参数(例如用于转换特性) - on a complex/large function
在一个复杂/大型函数 - which is used a lot
经常使用
then rustc
will need to compile a lot of permutations of this function (it monomorphizes generic functions), which will lead to long compile times.
那么 rustc
将需要编译此函数的许多排列组合(它使泛型函数单态化),这将导致较长的编译时间。
bluss mentioned on Reddit that you can use “de-generization” to circumvent this issue: Your (public) generic function just calls another, (private) non-generic function, which will only be compiled once.
bluss 在 Reddit 上提到,您可以使用“去泛化”来规避这个问题:您的(公共)泛型函数只调用另一个(私有)非泛型函数,这样只会编译一次。
The examples bluss gave was the implementation of std::fs::OpenOptions::open
(source from Rust 1.12) and this pull request on the image
crate, which changed its open
function to this:
bluss 给出的示例是 std::fs::OpenOptions::open
的实现(源自 Rust 1.12),以及对 image
crate 的此拉取请求,该请求将其 open
函数更改为:
pub fn open<P>(path: P) -> ImageResult<DynamicImage> where P: AsRef<Path> {
// thin wrapper function to strip generics before calling open_impl
open_impl(path.as_ref())
}
Laziness 懒惰
While Rust does not have ‘laziness’ in the sense of lazy evaluation of expressions like Haskell implements it, there are several techniques you can use to elegantly omit doing unnecessary computations and allocations.
尽管 Rust 没有类似 Haskell 实现的表达式惰性求值的“懒惰”,但有几种技术可以优雅地避免执行不必要的计算和分配。
Use Iterators 使用迭代器
One of the most amazing constructs in the standard library is Iterator
, a trait that allows generator-like iteration of values where you only need to implement a next
method3. Rust’s iterators are lazy in that you explicitly need to call a consumer to start iterating through values. Just writing "hello".chars().filter(char::is_whitespace)
won’t do anything until you call something like .collect::<String>()
on it.
标准库中最令人惊叹的构造之一是 Iterator
,这是一个特质,允许像生成器一样迭代值,您只需要实现一个 next
方法 3 。Rust 的迭代器是惰性的,您需要显式调用一个消费者来开始遍历值。仅仅编写 "hello".chars().filter(char::is_whitespace)
不会有任何作用,直到您调用类似 .collect::<String>()
的东西。
Iterators as parameters 迭代器作为参数
Using iterators as inputs may make your API harder to read (taking a T: Iterator<Item=Thingy>
vs. &[Thingy]
), but allows users to skip allocations.
使用迭代器作为输入可能会使您的 API 更难阅读(接受 T: Iterator<Item=Thingy>
而不是 &[Thingy]
),但允许用户跳过分配。
Actually, though, you might not want to take a generic Iterator
: Use IntoIterator
instead. This way, can give you anything that you can easily turn into an iterator yourself by calling .into_iter()
on it. It’s also quite easy to determine which types implement IntoIterator
—as the documentation says:
实际上,你可能不想使用通用的 Iterator
:而是使用 IntoIterator
。这样,可以通过调用 .into_iter()
轻松将其转换为迭代器。确定哪些类型实现了 IntoIterator
也很容易——正如文档所述:
One benefit of implementing IntoIterator is that your type will work with Rust’s for loop syntax.
实现 IntoIterator 的一个好处是,您的类型将与 Rust 的 for 循环语法一起工作。
That is, everything a user can use in a for
loop, they can also give to your function.
也就是说,用户可以在 for
循环中使用的所有内容也可以传递给您的函数。
Returning/implementing iterators
返回/实现迭代器
If you want to return something your users can use as an iterator, the best practice is to define a new type that implements Iterator
. This may become easier once impl Trait
is stabilized (see the tracking issue). You can find a bit more information about this in the futures
tutorial (as returning a Future
and an Iterator
has similar characteristics).
如果您想返回用户可以用作迭代器的内容,最佳做法是定义一个实现 Iterator
的新类型。一旦 impl Trait
稳定下来(请参阅跟踪问题),这可能会变得更容易。您可以在 futures
教程中找到更多关于此的信息(返回 Future
和 Iterator
具有类似特征)。
Iterator
-like traits Iterator
-类特征
There are a few libraries that implement traits like Iterator
, e.g.:
有一些库实现了类似 Iterator
的特性,例如:
futures::Stream
: As written in thefutures
tutorial, whereIterator::next
returnsOption<Self::Item>
,Stream::poll
returns an async result ofOption<Self::Item>
(or an error).
futures::Stream
:正如futures
教程中所述,Iterator::next
返回Option<Self::Item>
,Stream::poll
返回Option<Self::Item>
的异步结果(或错误)。
Take closures 获取闭包
If a potentially expensive value (let’s say of type Value
) is not used in all branches in your control flow, consider taking a closure that returns that value (Fn() -> Value
).
如果在控制流中的所有分支中都没有使用一个可能昂贵的值(比如类型为 Value
),请考虑获取一个返回该值的闭包( Fn() -> Value
)。
If you are designing a trait, you can also have two methods that do the same thing, but where one takes a value and the other a closure that computes the value. A real-life example of this pattern is in Result
with unwrap_or
and unwrap_or_else
:
如果您正在设计一个特质,您也可以有两个方法来完成相同的事情,但其中一个接受一个值,另一个接受一个计算该值的闭包。这种模式的一个现实例子是在 Result
中的 unwrap_or
和 unwrap_or_else
:
let res: Result<i32, &str> = Err("oh noes");
res.unwrap_or(42); // just returns `42`
let res: Result<i32, &str> = Err("oh noes");
res.unwrap_or_else(|msg| msg.len() as i32); // will call the closure
Lazy tricks 懒惰的技巧
- Letting
Deref
do all the work: Wrapper type with an implementation ofDeref
that contains the logic to actually compute a value. The cratelazy
implements a macro to do that for you (it requires unstable features, though).
让Deref
做所有工作:包装类型,具有包含实际计算值逻辑的Deref
实现。Cratelazy
实现了一个宏来为您执行此操作(尽管需要不稳定的功能)。
Convenience traits 便利特性
Here are some traits you should try implement to make using your types easier/more consistent for your users:
以下是一些特性,您应该尝试实现,以使用户更容易/更一致地使用您的类型:
- Implement or derive the ‘usual’ traits like
Debug
,Hash
,PartialEq
,PartialOrd
,Eq
,Ord
实现或派生“通常”特征,如Debug
,Hash
,PartialEq
,PartialOrd
,Eq
,Ord
- Implement or derive
Default
instead of writing anew
method without arguments
实现或派生Default
,而不是编写一个没有参数的new
方法 - If you find yourself implementing a method on a type to return some of the type’s data as an
Iterator
, you should also consider implementingIntoIterator
on that type. (This only works when there is only one obvious way to iterate over your type’s data. Also see section on iterators above.)
如果您发现自己在类型上实现了一个方法,以返回类型的一些数据作为Iterator
,您还应考虑在该类型上实现IntoIterator
。(仅当有一种明显的方法可以迭代您类型的数据时才有效。另请参见上面关于迭代器的部分。) - If your custom data type can be thought of in a similar fashion as a primitive data type
T
fromstd
, consider implementingDeref<Target=T>
. But please don’t overdo this –Deref
is not meant to emulate inheritance!
如果您的自定义数据类型可以类似于从std
中的原始数据类型T
考虑,考虑实现Deref<Target=T>
。但请不要过度使用这个功能 -Deref
不是用来模拟继承的! - Instead of writing a constructor method that takes a string and creates a new instance of your data type, implement
FromStr
.
不要编写一个接受字符串并创建数据类型新实例的构造方法,而是实现FromStr
。
Custom traits for input parameters
输入参数的自定义特性
The Rust way to implement a kind of “function overloading” is by using a generic trait T
for one input parameter and implement T
for all types the function should accept.
Rust 实现一种“函数重载”的方式是使用一个泛型 trait T
作为一个输入参数,并为函数应该接受的所有类型实现 T
。
Example: str::find
示例: str::find
str::find<P: Pattern>(p: P)
accepts a Pattern
which is implemented for char
, str
, FnMut(char) -> bool
, etc.
str::find<P: Pattern>(p: P)
接受一个 Pattern
,该 Pattern
是为 char
, str
, FnMut(char) -> bool
等实现的。
"Lorem ipsum".find('L');
"Lorem ipsum".find("ipsum");
"Lorem ipsum".find(char::is_whitespace);
Extension traits 扩展特性
It’s a good practice to use types and traits defined in the standard library, as those are known by many Rust programmers, well-tested, and nicely documented. And while Rust’s standard library tends to offer types with semantic meaning4, the methods implemented on these types might not be enough for your API. Luckily, Rust’s “orphan rules” allow you implement a trait for a (generic) type if at least one of them is defined in the current crate.
在标准库中使用定义的类型和特质是一个好的实践,因为这些类型和特质为许多 Rust 程序员所熟知,经过充分测试,并且有很好的文档。虽然 Rust 的标准库倾向于提供具有语义含义的类型 4 ,但是这些类型上实现的方法可能不足以满足您的 API 需求。幸运的是,Rust 的“孤儿规则”允许您为(通用)类型实现特质,只要其中至少一个是在当前 crate 中定义的。
Decorating results 装饰结果
As Florian writes in “Decorating Results”, you can use this to write and implement traits to supply your own methods to built-in types like Result
. For example:
正如 Florian 在“装饰结果”中所写的那样,您可以使用此功能来编写和实现特性,以向内置类型(如 Result
)提供自己的方法。例如:
pub trait GrandResultExt {
fn party(self) -> Self;
}
impl GrandResultExt for Result<String, Box<Error>> {
fn party(self) -> Result<String, Box<Error>> {
if self.is_ok() {
println!("Wooohoo! 🎉");
}
self
}
}
// User's code
fn main() {
let fortune = library_function()
.method_returning_result()
.party()
.unwrap_or("Out of luck.".to_string());
}
Florian’s real-life code in lazers uses the same pattern to decorate the BoxFuture
(from the futures
crate) to make the code more readable (abbreviated):
Florian 在 lazers 中的真实代码使用相同的模式来装饰 BoxFuture
(来自 futures
crate)以使代码更易读(缩写):
let my_database = client
.find_database("might_not_exist")
.or_create();
Extending traits 扩展特征
So far, we’ve extended the methods available on a type by defining and implementing our own trait. You can also define traits that extend other traits (trait MyTrait: BufRead + Debug {}
). The most prominent example for this is the itertools crate, which adds a long list of methods to std
’s Iterators.
到目前为止,我们通过定义和实现自己的特质来扩展类型上可用的方法。您还可以定义扩展其他特质的特质( trait MyTrait: BufRead + Debug {}
)。最突出的例子是 itertools crate,它为 std
的迭代器添加了一长串方法。
FYI: RFC 445 wants you to add an Ext
suffix to extension traits.
FYI:RFC 445 希望您向扩展特性添加 Ext
后缀。
Builder pattern 生成器模式
You can make it easier to make complex API calls by chaining several smaller methods together. This works nicely with session types (see below). The derive_builder
crate can be used to automatically generate (simpler) builders for custom structs.
通过将几个较小的方法链接在一起,可以更轻松地进行复杂的 API 调用。这与会话类型很好地配合(请参见下文)。 derive_builder
crate 可用于为自定义结构体自动生成(更简单的)构建器。
Example: std::fs::OpenOptions
示例: std::fs::OpenOptions
use std::fs::OpenOptions;
let file = OpenOptions::new().read(true).write(true).open("foo.txt");
Session types 会话类型
You can encode a state machine in the type system.
您可以在类型系统中对状态机进行编码。
- Each state is a different type.
每个状态都是不同的类型。 - Each state type implements different methods.
每种状态类型实现不同的方法。 - Some methods consume a state type (by taking ownership of it) and return a different state type.
一些方法会消耗一个状态类型(通过拥有它)并返回一个不同的状态类型。
This works really well in Rust as your methods can move your data into a new type and you can no longer access the old state afterwards.
这在 Rust 中非常有效,因为您的方法可以将数据移动到新类型中,之后您将无法再访问旧状态。
Here’s an arbitrary example about mailing a package
(the type annotations are not necessary and are only added for clarity here):
这里有一个关于邮寄包裹的任意示例(类型注释并非必需,仅为了在此处更清晰地说明而添加):
let package: OpenPackage = Package::new();
let package: OpenPackage = package.insert([stuff, padding, padding]);
let package: ClosedPackage = package.seal_up();
// let package: OpenPackage = package.insert([more_stuff]);
//~^ ERROR: No method named `insert` on `ClosedPackage`
let package: DeliveryTracking = package.send(address, postage);
A good real-life example was given by /u/ssokolow in this thread on /r/rust:
/u/ssokolow 在/r/rust 的这个帖子中给出了一个很好的现实生活示例:
Hyper uses this to ensure, at compile time, that it’s impossible to get into situations like the “tried to set HTTP headers after request/response body has begun” that we see periodically on PHP sites. (The compiler can catch that because there is no “set header” method on a connection in that state and the invalidating of stale references allows it to be certain that only the correct state is being referenced.)
Hyper 使用此功能来确保在编译时无法进入类似“尝试在请求/响应主体开始后设置 HTTP 标头”的情况,这是我们在 PHP 网站上定期看到的情况。(编译器可以捕获该情况,因为在该状态下连接上没有“设置标头”方法,而使过时引用失效可以确保只引用了正确的状态。)
The hyper::server
docs go into a bit of detail on how this is implemented. Another interesting idea can be found in the lazers-replicator crate: It uses std::convert::From
to transition between states.
hyper::server
文档详细介绍了这是如何实现的。另一个有趣的想法可以在 lazers-replicator crate 中找到:它使用 std::convert::From
来在状态之间进行转换。
More information: 更多信息:
- The article “Beyond Memory Safety With Types” describes how this technique can be used to implement a nice and type safe interface for the IMAP protocol.
文章“通过类型实现超越内存安全”描述了如何使用这种技术来为 IMAP 协议实现一个良好且类型安全的接口。 - The paper “Session types for Rust” (PDF) by Thomas Bracht Laumann Jespersen, Philip Munksgaard, and Ken Friis Larsen (2015). DOI.
论文《Rust 的会话类型》(PDF),作者为 Thomas Bracht Laumann Jespersen,Philip Munksgaard 和 Ken Friis Larsen(2015 年)。DOI。 - Andrew Hobden’s post “Pretty State Machine Patterns in Rust” shows several ways how one can implement state machines in Rust’s type system: Using one
enum
for all states, explicitstruct
s, a basestruct
generic over statestruct
s, and transitions usingInto
.
Andrew Hobden 的帖子“在 Rust 中漂亮的状态机模式”展示了实现 Rust 类型系统中状态机的几种方法:使用一个enum
表示所有状态,显式struct
,一个基础struct
泛型覆盖状态struct
,以及使用Into
进行转换。
Use lifetimes well 善用生命周期
Specifying type and trait constraints on your API is essential to designing an API in a statically typed language, and, as written above, to help your users prevent logic errors. Rust’s type system can also encode another dimension: You can also describe the lifetimes of your data (and write constraints on lifetimes).
在静态类型语言中设计 API 时,指定类型和特质约束对于设计 API 至关重要,正如上文所述,有助于帮助用户避免逻辑错误。Rust 的类型系统还可以编码另一个维度:您还可以描述数据的生命周期(并对生命周期写约束)。
This can allow you (as a developer) to be more relaxed about giving out borrowed resources (instead of more computationally expensive owned data). Using references to data where possible is definitely a good practice in Rust, as high performance and “zero allocation” libraries are one of the languages selling points.
这可以让您(作为开发人员)更放心地提供借用资源(而不是更昂贵的拥有数据)。在可能的情况下使用数据引用绝对是 Rust 中的良好实践,因为高性能和“零分配”库是该语言的卖点之一。
You should try to write good documentation on this, though, as understanding lifetimes and dealing with references can present a challenge to users of your library, especially when they are new to Rust.
你应该尽量在这方面写好文档,因为理解生命周期并处理引用可能对你库的用户构成挑战,尤其是当他们刚接触 Rust 时。
For some reason (probably brevity), a lot of lifetimes are called 'a
, 'b
, or something similarly meaningless. If you know the resource for whose lifetime your references are valid, you can probably find a better name, though. For examples, if you read a file into memory and are working with references to that memory, call those lifetimes 'file
. Or if you are processing a TCP request and are parsing its data, you can call its lifetime 'req
.
由于某种原因(可能是为了简洁起见),很多生命周期被称为 'a
, 'b
,或类似毫无意义的名称。如果您知道您的引用有效的资源的生命周期,您可能会找到更好的名称。例如,如果您将文件读入内存并使用对该内存的引用,将这些生命周期称为 'file
。或者,如果您正在处理 TCP 请求并解析其数据,您可以将其生命周期称为 'req
。
Put finalizer code in drop
将终结器代码放在 drop
中
Rust’s ownership rules work for more than just memory: If your data type represents an external resource (e.g., a TCP connection), you can use the Drop
trait to close/deallocate/clean up the resource when it goes out of scope. You can use this the same way as you would use finalizers (or try … catch … finally
) in other languages.
Rust 的所有权规则不仅适用于内存:如果您的数据类型表示外部资源(例如 TCP 连接),您可以使用 Drop
trait 在其超出范围时关闭/释放/清理资源。您可以像在其他语言中使用终结器(或 try … catch … finally
)一样使用它。
Real-life examples of this are:
这些的真实例子是:
- The reference count types
Rc
andArc
useDrop
to decrease their reference count (and deallocate the inner data if the count hits zero).
引用计数类型Rc
和Arc
使用Drop
来减少它们的引用计数(如果计数达到零则释放内部数据)。 MutexGuard
usesDrop
to release its lock on aMutex
.
MutexGuard
使用Drop
释放其对Mutex
的锁定。- The diesel crate implements
Drop
to close database connections (e.g. in SQLite).
柴油箱实现Drop
以关闭数据库连接(例如在 SQLite 中)。
Case Studies 案例研究
Possible Rust libraries that use some nice tricks in their APIs:
可能使用一些不错技巧的 Rust 库:
- hyper: session types (see above)
hyper: 会话类型(见上文) - diesel: encodes SQL queries as types, uses traits with complex associated types
diesel:将 SQL 查询编码为类型,使用具有复杂关联类型的特征 - futures: very abstract and well documented crate
futures:非常抽象且文档完善的 crate
Other design patterns 其他设计模式
What I tried to cover here are design patterns for interfaces, i.e. APIs exposed to the user. While I believe that some of these patterns are only applicable to writing libraries, many also apply to writing generic application code.
我在这里尝试涵盖的是接口的设计模式,即暴露给用户的 API。虽然我相信其中一些模式仅适用于编写库,但许多模式也适用于编写通用应用程序代码。
You can find more information on this topic in the Rust Design Patterns repository.
您可以在 Rust 设计模式存储库中找到有关此主题的更多信息。
-
If you can construct your type without any parameters, you should implement
Default
on it, and use that instead ofnew
. An exception to this isnew
on “container” types, likeVec
orHashMap
, where it makes sense to initialize an empty container. ↩
如果您可以在不带任何参数的情况下构造您的类型,则应在其上实现Default
,并使用该类型代替new
。唯一的例外是“容器”类型上的new
,例如Vec
或HashMap
,在这种情况下初始化空容器是有意义的。 -
There is a slogan of “making illegal states unrepresentable” in other strongly typed languages. While I first heard this when talking to people about Haskell, it is also the title of this article by F# for fun and profit, and this talk by Richard Feldman presented at elm-conf 2016. ↩
在其他强类型语言中有一个口号:“使非法状态不可表示”。虽然我第一次听到这个口号是在与人们讨论 Haskell 时,但它也是 F# for fun and profit 这篇文章的标题,以及 Richard Feldman 在 2016 年 elm-conf 上的演讲标题。 -
In that regard, Rust’s Iterators are very similar to the
Iterator
interface in Java or theIteration
protocol in Python (as well as many others). ↩
在这方面,Rust 的迭代器与 Java 中的Iterator
接口或 Python 中的Iteration
协议(以及许多其他协议)非常相似。 ↩ -
For examples,
std
has anResult
type (withOk
andErr
variants) which should be used to handle errors, instead of anEither
type (withLeft
andRight
variants) which does not imply that meaning. ↩
例如,std
具有Result
类型(具有Ok
和Err
变体),应该用来处理错误,而不是Either
类型(具有Left
和Right
变体),这种类型并不意味着那个含义。 ↩