
【Rust每日新闻】 2018-11-05
探讨所有权和借用语义对API接口设计的影响
假如你有一个结构体:
struct Person {
name: String
}
然后为其实现一个new方法:
impl Person {
pub fn new(name: String) -> Person {
Person { name }
}
}
这个new方法,接收的参数是具有移动语义的String字符串。这个API是让客户端(调用方)来分配字符串,或者是move别人分配的字符串。当传入字符串字面量的时候,会编译错误。
let someone = Person::new("Patrick Bateman"); // this won’t compile
这里 "Patrick Bateman"是 &'static str 类型的字符串字面量,而不是String类型。
如何修复上面代码?
let someone = Person::new("Patrick Bateman".to_owned());
但这样并不理想。尝试以下方案:
impl Person {
pub fn new(name: &str) -> Person {
Person {
name: name.to_owned()
}
}
}
这样实现,下面的调用是可以编译了:
let someone = Person::new("Patrick Bateman"); // okay because 'static is a subtype of 'a
但是,又不能使用String的字符串了。
let x: String = …;
let someone = Person::new(x); // not okay now since it’s a type mismatch
为此,只能传入x的引用才行:
let x: String = …;
let someone = Person::new(&x); // okay, here &x derefs to &str
上面可以编译是因为String实现了Deref<Target = str>。
还有另外一个比较好的技巧就是使用AsRef trait。
impl Person {
pub fn new<N>(name: N) -> Person
where N: AsRef<str> {
Person {
name: name.as_ref().to_owned()
}
}
}
&str和String都实现了AsRef<str>。然而这里依旧存在问题,如果在调用Person::new之后还需要使用x,这代码是没问题的。但是在调用之后不再使用x,那这里就是浪费了一次move,而又重新分配一次(to_owned)。
一个完美的API,最好是在需要clone的时候就clone,需要move的时候move。
另外一个解决方案是ToOwned + Cow
enum Cow<'a, T> where T: 'a + ?Sized + ToOwned {
Borrowed(&'a T),
Owned(T::ToOwned)
}
impl<'a, T> Cow<'a, T> where T: 'a + ?Sized + ToOwned {
pub fn into_owned(self) {
match self {
Cow::Borrowed(b) => b.to_owned(),
Owned(o) => o
}
}
}
看得出来Cow可以持有借用或者所有权,并且提供了into_owned方法。所以我们的代码可以修改为:
impl Person {
pub fn new<'a, N>(name: N) -> Person where N: Into<Cow<'a, str>> {
Person { name: name.into().into_owned() }
}
}
所以,当为new方法传递String的时候,就会move。传递&str的时候,则是clone。就像这样:
let _ = Person::new("Patrick Bateman");
let dawg = "Dawg";
let _ = Person::new(format!("Doggo {}", dawg));
对于此API,调用者可以选择使用独占类型还是借用。但是Cow::into_owned因为使用了match匹配,所以会存在一点点运行时开销。
当然,也可以直接使用Into来简化此方案:
impl Person {
pub fn new<N>(name: N) -> Person where N: Into<String> {
Person { name: name.into() }
}
}
显然,这里有一些缺点:
Into<String>无法表达任何生命周期。如果需要动态检查需要clone还是move,则使用Cow<str>。- 如果明确地知道此处不需要move,则继续使用
Into<String>。 - 有些类型可能没有实现
Into<String>。
结论:
本文主要想强调&_, AsRef<_>, Cow<_>, Into<_> 都有不同的语义,在用它们编写公开API的时候,满足不同的契约。
&T意味着,不需要客户端(调用方)move或者clone。只进行只读计算。- 不需要客户端拥有T。
- 客户端不必要担心会有内存分配。
- 强迫使用引用,会传达更多的信息,比如切片类型,这就要求数据的连续性。
Q: AsRef<T>,意味着,你打算执行只读计算(只读契约),或者,在需要独占类型的时候,也不需要客户端来提供。但记住,这里有个隐藏属性:因为你在只读契约上接受一个被move的值(比如前例中的String),所以你也要接受该值必然会被drop(之后无法使用)。Cow<T>,使用它,表明客户端可以选择使用独占(拥有所有权)或借用。但它是在运行时来选择是否需要独占。Q: Into<T>,当你确定此处需要独占类型(拥有所有权)时候,可以使用它们。Q: IntoIterator<Item = T>, 使用该限定,可以直接使用Vec<_>这种类型,而不是使用迭代器。
Rust谜题:让内嵌于迭代中的Result扁平化
ndarray-csv的作者第三次重构该库的时候碰到一个问题。
他想实现一个函数:
pub fn flatten_nested_results<T, E, II, IO>(iter_outer: IO) -> impl Iterator<Item = Result<T, E>>
where
II: Iterator<Item = Result<T, E>>,
IO: Iterator<Item = Result<II, E>>,
{
/// Fill me in!
}
然后用于处理像下面这种迭代器中的Result
fn vec_to_nested_iter(
vec_outer: Vec<Result<Vec<Result<i8, f32>>, f32>>,
) -> impl Iterator<Item = Result<impl Iterator<Item = Result<i8, f32>>, f32>> {
vec_outer
.into_iter()
.map(|vec_inner| vec_inner.map(Vec::into_iter))
}
/// Without any Errs, we should return the whole sequence
#[test]
fn test_all_ok() {
let iter_outer = vec_to_nested_iter(vec![Ok(vec![Ok(1), Ok(2)]), Ok(vec![Ok(3)])]);
let expected: Result<Vec<i8>, f32> = Ok(vec![1, 2, 3]);
assert_eq!(expected, flatten_nested_results(iter_outer).collect())
}
作者经过三次失败的flatten_nested_results函数实现,首先找出的解决方案是使用trait object。
pub fn flatten_nested_results<T, E, II, IO>(iter_outer: IO) -> impl Iterator<Item = Result<T, E>>
where
T: 'static,
E: 'static,
II: 'static + Iterator<Item = Result>,
IO: 'static + Iterator<Item = Result<II, E>>,
{
iter_outer.flat_map(|iter_inner_result| match iter_inner_result {
Ok(iter_inner) => Box::new(iter_inner) as Box<Iterator<Item = Result<T, E>>>,
Err(err) => Box::new(once(Err(err))) as Box<Iterator<Item = Result<T, E>>>,
})
}
但他还不喜欢这个方案,最终他使用了either库来解决此问题
extern crate either;
use either::Either;
use std::iter::once;
pub fn flatten_nested_results<T, E, II, IO>(iter_outer: IO) -> impl Iterator<Item = Result<T, E>>
where
II: Iterator<Item = Result<T, E>>,
IO: Iterator<Item = Result<II, E>>,
{
iter_outer.flat_map(|iter_inner_result| match iter_inner_result {
Ok(iter_inner) => Either::Right(iter_inner),
Err(err) => Either::Left(once(Err(err))),
})
}
「博文」Rust Nightly,Travis CI和代码覆盖率
本文讲述了如何使用Travis CI 的workspace测试Nightly项目,并且使用kcov和codecov收集测试覆盖率。
「工具」orion 0.9发布
orion 纯Rust加密的另一种尝试
- 禁止 unsafe
- 提供高级抽象,关注可用性
- 不是要替代Ring或RustCrypto这样的库
- 现在不适合用于生产
新版本新增了 XChaCha20Poly1305等算法支持
「小游戏」Rust和WASM实现的贪食蛇
「小工具」Rust实现的密码生成工具
「工具」JSON5的序列化和反序列化工具
基于pest和serde
「工具」s3-concat: 使用Rust快速合并S3文件
lazy_static发布1.2.0版本
该版本主要是让lazy_static可以在no_std下使用。
每日新闻订阅地址:
欢迎通过GitHub issues投稿。
Leave 【Rust每日新闻】 2018-11-05 to:
Read more #rust posts
Best Posts From ChaosBot
We have not curated any of blackanger's posts yet. But you can encourage our curation team to review posts by visiting them regularly and by referring other readers. Because we give priority to frequently read content.