编辑: 张汉东

编者按:

RFC 3058 try_trait_v2 被合并了,这意味着,? 操作符的行为在设计上已经趋于稳定,只等待它的实现。

在 RustFriday 飞书群线上沙龙 第四期 也讲过这个 RFC ,可以观看录播:https://www.bilibili.com/video/BV1xy4y147Ve/

Rust 中文社群 飞书群 邀请你加入:https://applink.feishu.cn/TeLAcbDR


背景介绍

目前 Rust 允许通过 ? 操作符可以自动返回的 Result<T, E>Err(e) ,但是对于 Ok(o) 还需要手动包装。

比如:

  1. fn foo() -> Result<PathBuf, io::Error> {
  2. let base = env::current_dir()?;
  3. Ok(base.join("foo"))
  4. }

那么这就引出了一个 术语: Ok-Wrapping 。很明显,这个写法不够优雅,还有很大的改进空间。

因此 Rust 官方成员 withoutboats 开发了一个库 fehler,引入了一个 throw 语法。

用法如下:

  1. #[throws(i32)]
  2. fn foo(x: bool) -> i32 {
  3. if x {
  4. 0
  5. } else {
  6. throw!(1);
  7. }
  8. }
  9. // 上面foo函数错误处理等价于下面bar函数
  10. fn bar(x: bool) -> Result<i32, i32> {
  11. if x {
  12. Ok(0)
  13. } else {
  14. Err(1)
  15. }
  16. }

通过 throw 宏语法来帮助开发者省略 Ok-wrapping 和 Err-wrapping 的手动操作。这个库一时在社区引起了一些讨论。它也在促进着 Rust 错误处理体验提升。

于是错误处理就围绕着 Ok-wrapping 和 Err-wrapping 这两条路径,该如何设计语法才更加优雅为出发点。

try块 和 try trait 的区别

当前 Nightly Rust 中也提供了一个 try 块语法,要使用 #![feature(try_blocks)]

用法如下:

  1. #![feature(try_blocks)]
  2. use std::path::PathBuf;
  3. fn foo() -> Result<PathBuf, std::io::Error> {
  4. try {
  5. let base = std::env::current_dir()?;
  6. base.join("foo")
  7. }
  8. }

try 块在 Ok 情况下自动 Ok-wrapping 返回 Ok(PathBuf),而问号操作符返回 Err(io::Error)。所以,这个 try 块语法 和 try trait 是相互配合的。

所以:

  • try 块 (try-block)是控制 Ok-wrapping
  • try trait 是控制问号操作符的行为 Err-wrapping

try-trait RFC 导读

经过很久很久的讨论,try-trait-v2 RFC 被合并了,意味着一个确定的方案出现了。

在这个方案中,引入了一个新类型:

  1. enum ControlFlow<B, C = ()> {
  2. /// Exit the operation without running subsequent phases.
  3. Break(B),
  4. /// Move on to the next phase of the operation as normal.
  5. Continue(C),
  6. }
  7. impl<B, C> ControlFlow<B, C> {
  8. fn is_break(&self) -> bool;
  9. fn is_continue(&self) -> bool;
  10. fn break_value(self) -> Option<B>;
  11. fn continue_value(self) -> Option<C>;
  12. }

ControlFlow 中包含了两个值:

  • ControlFlow::Break,表示提前退出。但不一定是Error 的情况,也可能是 Ok
  • ControlFlow::Continue,表示继续。

还引入了一个新的trait:

  1. trait FromResidual<Residual = <Self as Try>::Residual> {
  2. fn from_residual(r: Residual) -> Self;
  3. }

Residual 单词有 「剩余」之意,因为 要把 Result / Option/ ControlFlow 之类的类型,拆分成两部分(两条路径),用这个词就好理解了。

Try trait 继承自 FromResidual trait :

  1. pub trait Try: FromResidual {
  2. /// The type of the value consumed or produced when not short-circuiting.
  3. type Output;
  4. /// A type that "colours" the short-circuit value so it can stay associated
  5. /// with the type constructor from which it came.
  6. type Residual;
  7. /// Used in `try{}` blocks to wrap the result of the block.
  8. fn from_output(x: Self::Output) -> Self;
  9. /// Determine whether to short-circuit (by returning `ControlFlow::Break`)
  10. /// or continue executing (by returning `ControlFlow::Continue`).
  11. fn branch(self) -> ControlFlow<Self::Residual, Self::Output>;
  12. }
  13. pub trait FromResidual<Residual = <Self as Try>::Residual> {
  14. /// Recreate the type implementing `Try` from a related residual
  15. fn from_residual(x: Residual) -> Self;
  16. }

所以,在 Try trait 中有两个关联类型:

  • Output,如果是 Result 的话,就对应 Ok-wrapping 。
  • Residual,如果是 Result 的话,就对应 Err-wrapping 。

所以,现在 ? 操作符的行为就变成了:

  1. match Try::branch(x) {
  2. ControlFlow::Continue(v) => v,
  3. ControlFlow::Break(r) => return FromResidual::from_residual(r),
  4. }

然后内部给 Rusult 实现 Try

  1. impl<T, E> ops::Try for Result<T, E> {
  2. type Output = T;
  3. type Residual = Result<!, E>;
  4. #[inline]
  5. fn from_output(c: T) -> Self {
  6. Ok(c)
  7. }
  8. #[inline]
  9. fn branch(self) -> ControlFlow<Self::Residual, T> {
  10. match self {
  11. Ok(c) => ControlFlow::Continue(c),
  12. Err(e) => ControlFlow::Break(Err(e)),
  13. }
  14. }
  15. }
  16. impl<T, E, F: From<E>> ops::FromResidual<Result<!, E>> for Result<T, F> {
  17. fn from_residual(x: Result<!, E>) -> Self {
  18. match x {
  19. Err(e) => Err(From::from(e)),
  20. }
  21. }
  22. }

再给 Option 实现 Try

  1. impl<T> ops::Try for Option<T> {
  2. type Output = T;
  3. type Residual = Option<!>;
  4. #[inline]
  5. fn from_output(c: T) -> Self {
  6. Some(c)
  7. }
  8. #[inline]
  9. fn branch(self) -> ControlFlow<Self::Residual, T> {
  10. match self {
  11. Some(c) => ControlFlow::Continue(c),
  12. None => ControlFlow::Break(None),
  13. }
  14. }
  15. }
  16. impl<T> ops::FromResidual for Option<T> {
  17. fn from_residual(x: <Self as ops::Try>::Residual) -> Self {
  18. match x {
  19. None => None,
  20. }
  21. }
  22. }

再给 Poll 实现 Try :

  1. impl<T, E> ops::Try for Poll<Result<T, E>> {
  2. type Output = Poll<T>;
  3. type Residual = <Result<T, E> as ops::Try>::Residual;
  4. fn from_output(c: Self::Output) -> Self {
  5. c.map(Ok)
  6. }
  7. fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {
  8. match self {
  9. Poll::Ready(Ok(x)) => ControlFlow::Continue(Poll::Ready(x)),
  10. Poll::Ready(Err(e)) => ControlFlow::Break(Err(e)),
  11. Poll::Pending => ControlFlow::Continue(Poll::Pending),
  12. }
  13. }
  14. }
  15. impl<T, E, F: From<E>> ops::FromResidual<Result<!, E>> for Poll<Result<T, F>> {
  16. fn from_residual(x: Result<!, E>) -> Self {
  17. match x {
  18. Err(e) => Poll::Ready(Err(From::from(e))),
  19. }
  20. }
  21. }
  22. impl<T, E> ops::Try for Poll<Option<Result<T, E>>> {
  23. type Output = Poll<Option<T>>;
  24. type Residual = <Result<T, E> as ops::Try>::Residual;
  25. fn from_output(c: Self::Output) -> Self {
  26. c.map(|x| x.map(Ok))
  27. }
  28. fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {
  29. match self {
  30. Poll::Ready(Some(Ok(x))) => ControlFlow::Continue(Poll::Ready(Some(x))),
  31. Poll::Ready(Some(Err(e))) => ControlFlow::Break(Err(e)),
  32. Poll::Ready(None) => ControlFlow::Continue(Poll::Ready(None)),
  33. Poll::Pending => ControlFlow::Continue(Poll::Pending),
  34. }
  35. }
  36. }
  37. impl<T, E, F: From<E>> ops::FromResidual<Result<!, E>> for Poll<Option<Result<T, F>>> {
  38. fn from_residual(x: Result<!, E>) -> Self {
  39. match x {
  40. Err(e) => Poll::Ready(Some(Err(From::from(e)))),
  41. }
  42. }
  43. }

再给 ControlFlow 实现 Try :

  1. impl<B, C> ops::Try for ControlFlow<B, C> {
  2. type Output = C;
  3. type Residual = ControlFlow<B, !>;
  4. fn from_output(c: C) -> Self {
  5. ControlFlow::Continue(c)
  6. }
  7. fn branch(self) -> ControlFlow<Self::Residual, C> {
  8. match self {
  9. ControlFlow::Continue(c) => ControlFlow::Continue(c),
  10. ControlFlow::Break(b) => ControlFlow::Break(ControlFlow::Break(b)),
  11. }
  12. }
  13. }
  14. impl<B, C> ops::FromResidual for ControlFlow<B, C> {
  15. fn from_residual(x: <Self as ops::Try>::Residual) -> Self {
  16. match x {
  17. ControlFlow::Break(r) => ControlFlow::Break(r),
  18. }
  19. }
  20. }

这就实现了 错误类型转换 大统一。

我在 2017 年给官方提过一个 Issue: why havn’t implemented Error trait for std::option::NoneError ?,是因为当时引入了 NoneError,但没有个 NoneError 实现 Error trait,所以无法在 Result 和 Option 之间无缝转换。

现在如果这个 RFC 实现,Result/Option 之间可以无缝转换,而完全不需要 NoneError 了,也许 NoneError就可以移除了。甚至在写异步 poll 方法的时候,也会变得非常简单了。

最后再看一个示例:

  1. #[derive(Debug, Copy, Clone, Eq, PartialEq)]
  2. #[repr(transparent)]
  3. pub struct ResultCode(pub i32);
  4. impl ResultCode {
  5. const SUCCESS: Self = ResultCode(0);
  6. }
  7. use std::num::NonZeroI32;
  8. pub struct ResultCodeResidual(NonZeroI32);
  9. impl Try for ResultCode {
  10. type Output = ();
  11. type Residual = ResultCodeResidual;
  12. fn branch(self) -> ControlFlow<Self::Residual> {
  13. match NonZeroI32::new(self.0) {
  14. Some(r) => ControlFlow::Break(ResultCodeResidual(r)),
  15. None => ControlFlow::Continue(()),
  16. }
  17. }
  18. fn from_output((): ()) -> Self {
  19. ResultCode::SUCCESS
  20. }
  21. }
  22. impl FromResidual for ResultCode {
  23. fn from_residual(r: ResultCodeResidual) -> Self {
  24. ResultCode(r.0.into())
  25. }
  26. }
  27. #[derive(Debug, Clone)]
  28. pub struct FancyError(String);
  29. impl<T, E: From<FancyError>> FromResidual<ResultCodeResidual> for Result<T, E> {
  30. fn from_residual(r: ResultCodeResidual) -> Self {
  31. Err(FancyError(format!("Something fancy about {} at {:?}", r.0, std::time::SystemTime::now())).into())
  32. }
  33. }