RFC 介绍 | try-trait v2
编辑: 张汉东
编者按:
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)
还需要手动包装。
比如:
#![allow(unused)] fn main() { fn foo() -> Result<PathBuf, io::Error> { let base = env::current_dir()?; Ok(base.join("foo")) } }
那么这就引出了一个 术语: Ok-Wrapping 。很明显,这个写法不够优雅,还有很大的改进空间。
因此 Rust 官方成员 withoutboats 开发了一个库 fehler,引入了一个 throw 语法。
用法如下:
#![allow(unused)] fn main() { #[throws(i32)] fn foo(x: bool) -> i32 { if x { 0 } else { throw!(1); } } // 上面foo函数错误处理等价于下面bar函数 fn bar(x: bool) -> Result<i32, i32> { if x { Ok(0) } else { Err(1) } } }
通过 throw 宏语法来帮助开发者省略 Ok-wrapping 和 Err-wrapping 的手动操作。这个库一时在社区引起了一些讨论。它也在促进着 Rust 错误处理体验提升。
于是错误处理就围绕着 Ok-wrapping 和 Err-wrapping 这两条路径,该如何设计语法才更加优雅为出发点。
try
块 和 try trait
的区别
当前 Nightly Rust 中也提供了一个 try
块语法,要使用 #![feature(try_blocks)]
。
用法如下:
#![allow(unused)] #![feature(try_blocks)] fn main() { use std::path::PathBuf; fn foo() -> Result<PathBuf, std::io::Error> { try { let base = std::env::current_dir()?; base.join("foo") } } }
try
块在 Ok
情况下自动 Ok-wrapping 返回 Ok(PathBuf)
,而问号操作符返回 Err(io::Error)
。所以,这个 try
块语法 和 try trait
是相互配合的。
所以:
try
块 (try-block)是控制 Ok-wrappingtry trait
是控制问号操作符的行为 Err-wrapping
try-trait RFC 导读
经过很久很久的讨论,try-trait-v2 RFC 被合并了,意味着一个确定的方案出现了。
在这个方案中,引入了一个新类型:ControlFlow
。
#![allow(unused)] fn main() { enum ControlFlow<B, C = ()> { /// Exit the operation without running subsequent phases. Break(B), /// Move on to the next phase of the operation as normal. Continue(C), } impl<B, C> ControlFlow<B, C> { fn is_break(&self) -> bool; fn is_continue(&self) -> bool; fn break_value(self) -> Option<B>; fn continue_value(self) -> Option<C>; } }
ControlFlow
中包含了两个值:
ControlFlow::Break
,表示提前退出。但不一定是Error
的情况,也可能是Ok
。ControlFlow::Continue
,表示继续。
还引入了一个新的trait:FromResidual
#![allow(unused)] fn main() { trait FromResidual<Residual = <Self as Try>::Residual> { fn from_residual(r: Residual) -> Self; } }
Residual 单词有 「剩余」之意,因为 要把 Result / Option/ ControlFlow 之类的类型,拆分成两部分(两条路径),用这个词就好理解了。
而 Try
trait 继承自 FromResidual
trait :
#![allow(unused)] fn main() { pub trait Try: FromResidual { /// The type of the value consumed or produced when not short-circuiting. type Output; /// A type that "colours" the short-circuit value so it can stay associated /// with the type constructor from which it came. type Residual; /// Used in `try{}` blocks to wrap the result of the block. fn from_output(x: Self::Output) -> Self; /// Determine whether to short-circuit (by returning `ControlFlow::Break`) /// or continue executing (by returning `ControlFlow::Continue`). fn branch(self) -> ControlFlow<Self::Residual, Self::Output>; } pub trait FromResidual<Residual = <Self as Try>::Residual> { /// Recreate the type implementing `Try` from a related residual fn from_residual(x: Residual) -> Self; } }
所以,在 Try
trait 中有两个关联类型:
Output
,如果是 Result 的话,就对应 Ok-wrapping 。Residual
,如果是 Result 的话,就对应 Err-wrapping 。
所以,现在 ?
操作符的行为就变成了:
#![allow(unused)] fn main() { match Try::branch(x) { ControlFlow::Continue(v) => v, ControlFlow::Break(r) => return FromResidual::from_residual(r), } }
然后内部给 Rusult 实现 Try
:
#![allow(unused)] fn main() { impl<T, E> ops::Try for Result<T, E> { type Output = T; type Residual = Result<!, E>; #[inline] fn from_output(c: T) -> Self { Ok(c) } #[inline] fn branch(self) -> ControlFlow<Self::Residual, T> { match self { Ok(c) => ControlFlow::Continue(c), Err(e) => ControlFlow::Break(Err(e)), } } } impl<T, E, F: From<E>> ops::FromResidual<Result<!, E>> for Result<T, F> { fn from_residual(x: Result<!, E>) -> Self { match x { Err(e) => Err(From::from(e)), } } } }
再给 Option 实现 Try
:
#![allow(unused)] fn main() { impl<T> ops::Try for Option<T> { type Output = T; type Residual = Option<!>; #[inline] fn from_output(c: T) -> Self { Some(c) } #[inline] fn branch(self) -> ControlFlow<Self::Residual, T> { match self { Some(c) => ControlFlow::Continue(c), None => ControlFlow::Break(None), } } } impl<T> ops::FromResidual for Option<T> { fn from_residual(x: <Self as ops::Try>::Residual) -> Self { match x { None => None, } } } }
再给 Poll 实现 Try
:
#![allow(unused)] fn main() { impl<T, E> ops::Try for Poll<Result<T, E>> { type Output = Poll<T>; type Residual = <Result<T, E> as ops::Try>::Residual; fn from_output(c: Self::Output) -> Self { c.map(Ok) } fn branch(self) -> ControlFlow<Self::Residual, Self::Output> { match self { Poll::Ready(Ok(x)) => ControlFlow::Continue(Poll::Ready(x)), Poll::Ready(Err(e)) => ControlFlow::Break(Err(e)), Poll::Pending => ControlFlow::Continue(Poll::Pending), } } } impl<T, E, F: From<E>> ops::FromResidual<Result<!, E>> for Poll<Result<T, F>> { fn from_residual(x: Result<!, E>) -> Self { match x { Err(e) => Poll::Ready(Err(From::from(e))), } } } impl<T, E> ops::Try for Poll<Option<Result<T, E>>> { type Output = Poll<Option<T>>; type Residual = <Result<T, E> as ops::Try>::Residual; fn from_output(c: Self::Output) -> Self { c.map(|x| x.map(Ok)) } fn branch(self) -> ControlFlow<Self::Residual, Self::Output> { match self { Poll::Ready(Some(Ok(x))) => ControlFlow::Continue(Poll::Ready(Some(x))), Poll::Ready(Some(Err(e))) => ControlFlow::Break(Err(e)), Poll::Ready(None) => ControlFlow::Continue(Poll::Ready(None)), Poll::Pending => ControlFlow::Continue(Poll::Pending), } } } impl<T, E, F: From<E>> ops::FromResidual<Result<!, E>> for Poll<Option<Result<T, F>>> { fn from_residual(x: Result<!, E>) -> Self { match x { Err(e) => Poll::Ready(Some(Err(From::from(e)))), } } } }
再给 ControlFlow 实现 Try
:
#![allow(unused)] fn main() { impl<B, C> ops::Try for ControlFlow<B, C> { type Output = C; type Residual = ControlFlow<B, !>; fn from_output(c: C) -> Self { ControlFlow::Continue(c) } fn branch(self) -> ControlFlow<Self::Residual, C> { match self { ControlFlow::Continue(c) => ControlFlow::Continue(c), ControlFlow::Break(b) => ControlFlow::Break(ControlFlow::Break(b)), } } } impl<B, C> ops::FromResidual for ControlFlow<B, C> { fn from_residual(x: <Self as ops::Try>::Residual) -> Self { match x { ControlFlow::Break(r) => ControlFlow::Break(r), } } } }
这就实现了 错误类型转换 大统一。
我在 2017 年给官方提过一个 Issue: why havn't implemented Error trait for std::option::NoneError ?,是因为当时引入了 NoneError,但没有个
NoneError
实现 Error
trait,所以无法在 Result 和 Option 之间无缝转换。
现在如果这个 RFC 实现,Result/Option 之间可以无缝转换,而完全不需要 NoneError
了,也许 NoneError
就可以移除了。甚至在写异步 poll 方法的时候,也会变得非常简单了。
最后再看一个示例:
#![allow(unused)] fn main() { #[derive(Debug, Copy, Clone, Eq, PartialEq)] #[repr(transparent)] pub struct ResultCode(pub i32); impl ResultCode { const SUCCESS: Self = ResultCode(0); } use std::num::NonZeroI32; pub struct ResultCodeResidual(NonZeroI32); impl Try for ResultCode { type Output = (); type Residual = ResultCodeResidual; fn branch(self) -> ControlFlow<Self::Residual> { match NonZeroI32::new(self.0) { Some(r) => ControlFlow::Break(ResultCodeResidual(r)), None => ControlFlow::Continue(()), } } fn from_output((): ()) -> Self { ResultCode::SUCCESS } } impl FromResidual for ResultCode { fn from_residual(r: ResultCodeResidual) -> Self { ResultCode(r.0.into()) } } #[derive(Debug, Clone)] pub struct FancyError(String); impl<T, E: From<FancyError>> FromResidual<ResultCodeResidual> for Result<T, E> { fn from_residual(r: ResultCodeResidual) -> Self { Err(FancyError(format!("Something fancy about {} at {:?}", r.0, std::time::SystemTime::now())).into()) } } }