< 返回版块

ChaosBot 发表于 2018-11-05 21:45

Tags:rustnews

探讨所有权和借用语义对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))),
    })
}

Read More


「博文」Rust Nightly,Travis CI和代码覆盖率

#travis #ci #kcov #codecov

本文讲述了如何使用Travis CI 的workspace测试Nightly项目,并且使用kcov和codecov收集测试覆盖率。

Read More


「工具」orion 0.9发布

#orion #crypto

orion 纯Rust加密的另一种尝试

  • 禁止 unsafe
  • 提供高级抽象,关注可用性
  • 不是要替代Ring或RustCrypto这样的库
  • 现在不适合用于生产

新版本新增了 XChaCha20Poly1305等算法支持

Read More


「小游戏」Rust和WASM实现的贪食蛇

#wasm #snake

rust-snake-wasm


「小工具」Rust实现的密码生成工具

#passgen #password

passgen-rs


「工具」JSON5的序列化和反序列化工具

#pest #serde #json5

基于pest和serde

json5-rs


「工具」s3-concat: 使用Rust快速合并S3文件

#s3 #s3_concat

s3-concat

Read More


lazy_static发布1.2.0版本

#lazy_static #no_std

该版本主要是让lazy_static可以在no_std下使用。

Read More


每日新闻订阅地址:

欢迎通过GitHub issues投稿。

评论区

写评论

还没有评论

1 共 0 条评论, 1 页