探讨所有权和借用语义对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 #either #puzzle
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<T, E>>,
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 #kcov #codecov
本文讲述了如何使用Travis CI 的workspace测试Nightly项目,并且使用kcov和codecov收集测试覆盖率。
「工具」orion 0.9发布
#orion #crypto
orion 纯Rust加密的另一种尝试
- 禁止 unsafe
- 提供高级抽象,关注可用性
- 不是要替代Ring或RustCrypto这样的库
- 现在不适合用于生产
新版本新增了 XChaCha20Poly1305等算法支持
「小游戏」Rust和WASM实现的贪食蛇
#wasm #snake
「小工具」Rust实现的密码生成工具
#passgen #password
「工具」JSON5的序列化和反序列化工具
#pest #serde #json5
基于pest和serde
「工具」s3-concat: 使用Rust快速合并S3文件
#s3 #s3_concat
lazy_static发布1.2.0版本
#lazy_static #no_std
该版本主要是让lazy_static可以在no_std下使用。
每日新闻订阅地址:
欢迎通过GitHub issues投稿。
评论区
写评论还没有评论