Rust: `ops::Try`(`try_trait` 功能)的跟踪问题

创建于 2017-05-31  ·  99评论  ·  资料来源: rust-lang/rust

来自https://github.com/rust-lang/rfcs/pull/1859Try特征 在 PR https://github.com/rust-lang/rust/pull/42275 中实现

为清楚起见,从https://github.com/rust-lang/rust/issues/31436拆分(根据 https://github.com/rust-lang/rust/pull/42275#discussion_r119167966)

  • [ ] 稳定这将允许人们实施Iterator::try_fold

    • [ ] 作为稳定的一部分,重新​​打开 PR #62606 以记录迭代器的 try_fold 实现

    • [ ] 确保其他事物的默认实现具有所需的长期 DAG,因为以后基本上不可能更改它们。 (具体来说,最好根据fold实现try_fold ,这样两者都不需要被覆盖。)

A-error-handling B-RFC-implemented B-unstable C-tracking-issue Libs-Tracked T-lang T-libs

最有用的评论

此功能的当前状态是什么?

所有99条评论

一些自行车棚:

  • 我们是否有特殊的动机来调用关联类型Error而不是Err ? 将其称为Err将使其与Result :另一个已经称为Ok

  • 我们是否有特别的动机来拥有单独的from_errorfrom_ok方法,而不是一个与into_result更对称的单个from_result ,后者是另一个一半的特质?

来自 RFC的游乐场链接的更新版本

@glaebhoerl

  • Error vs Err TryFromhttps://github.com/rust-lang/rust/issues/33417#issuecomment -269108968 和https:// 中TryFrom相关讨论github.com/rust-lang/rust/pull/40281; 我认为这里选择这个名字是出于类似的原因。
  • 我相信它们是分开的,因为它们有不同的用途,我希望很少有人真正拥有Result而他们正试图将其变成T:Try 。 我更喜欢Try::from_okTry::from_error总是调用Try::from_result(Ok(Try::from_result(Err( ,我很高兴在写出匹配时只使用这两种方法。 也许那是因为我认为into_result不是Into<Result> ,而是“它通过还是失败?”,特定类型是Result作为一个不重要的实现细节。 (不过,我不想建议或重新打开“应该有一种新的生产价值与早期回报类型。)对于文档,我喜欢from_error谈论? (或最终throw ),而from_ok谈论成功包装(#41414),而不是让它们都使用相同的方法。

我不确定这是否是此评论的正确论坛,如果不是,请重定向我: smiley:。 也许这应该在https://github.com/rust-lang/rfcs/pull/1859 上,我错过了评论期; 哎呀!


我想知道是否有拆分Try trait 或删除into_result方法的情况; 对我来说,目前Try有点像特征的总和FromResult (包含from_errorfrom_ok )和IntoResult (包含into_result )。

FromResult可以使用?运算符实现高度符合人体工程学的提前退出,我认为这是该功能的杀手级用例。 我认为IntoResult已经可以使用 per-use-case 方法或Into<Result>巧妙地实现; 我错过了一些有用的例子吗?

遵循Try特性 RFCexpr?可以脱糖为:

match expr { // Removed `Try::into_result()` here.
    Ok(v) => v,
    Err(e) => return Try::from_error(From::from(e)),
}

我考虑的激励示例是FutureOption

未来

我们可以自然地为struct FutureResult: Future实现FromResult struct FutureResult: Future为:

impl<T, E> FromResult for FutureResult {
    type Ok = T;
    type Error = E;
    fn from_error(v: Self::Error) -> Self {
        future::err(v)
    }
    fn from_ok(v: Self::Ok) -> Self {
        future::ok(v)
    }
}

假设?的合理实现将在应用于Result::Err时从impl Future值函数返回,那么我可以写:

fn async_stuff() -> impl Future<V, E> {
    let t = fetch_t();
    t.and_then(|t_val| {
        let u: Result<U, E> = calc(t_val);
        async_2(u?)
    })
}
fn fetch_t() -> impl Future<T, E> {}
fn calc(t: T) -> Result<U,E> {}

这正是我今天早些时候试图实现的, Try搞定了! 但是,如果我们尝试为Future实现当前的Try ,则可能没有into_result规范选择; 恐慌、阻塞或轮询一次可能很有用,但这些似乎都没有普遍用处。 如果没有into_resultTry我可以实现提前退出如上,如果我需要转换FutureResult (并从那里来任何Try )我可以用合适的方法转换它(调用wait方法来阻止,调用一些poll_once() -> Result<T,E>等)。

选项

Option也是类似的情况。 我们像在Try trait RFC 中一样自然地实现from_okfrom_err ,并且可以简单地使用o.ok_or(Missing)Option<T>转换Result<T, Missing> o.ok_or(Missing)或方便的方法ok_or_missing


希望有帮助!

我可能对这一切都迟到了,但本周末我突然想到?有一个相当自然的语义,如果你想在 _success_ 上短路。

fn fun() -> SearchResult<Socks> {
    search_drawer()?;
    search_wardrobe()
}

但是在这种情况下, Try特征方法的命名不合适。

不过,它会扩大?运算符的含义。

为 rayon设计Try::is_error(&self)用于Consumer::full()方法。 (Rayon 有这个,因为消费者基本上是推式迭代器,但我们仍然希望短路错误。)我不得不将中间值存储为Result<T::Ok, T::Err>以便我可以调用Result::is_err()

从那里我还希望Try::from_result回到T ,但是match映射到T::from_okT::from_error是不是太坏了。

如果需要from_okfrom_errorTry特性可以为人体工程学提供from_result方法。 或相反亦然。 从给出的例子中,我看到了提供两者的案例。

由于 PR #42275 已合并,这是否意味着此问题已解决?

@skade请注意,类型可以将任何一个定义为短路类型,就像LoopState对某些Iterator内部结构所做的那样。 如果Try谈论“继续或破坏”而不是成功或失败,那也许会更自然。

@cuviper我最终需要Ok类型或Error -in-original 类型。 我希望 destruct-and-rebuild 是一个足够通用的东西,它可以很好地优化并且不需要关于 trait 的一堆特殊方法。

@ErichDonGubler这是一个跟踪问题,因此在相应的代码稳定之前不会解决。

体验报告:

试图将这种特性付诸实践时,我有点沮丧。 现在好几次我都想在Result上定义我自己的变体,无论出于什么原因,但每次我最后都只使用Result ,主要是因为实现了Try太烦人了。 不过,我不完全确定这是否是一件好事!

例子:

在 Chalk VM 的新求解器中,我想要一个枚举来指示求解“链”的结果。 这有四种可能:

enum StrandFail<T> {
    Success(T),
    NoSolution,
    QuantumExceeded,
    Cycle(Strand, Minimums),
}

我想要? ,当应用于这个枚举时,解开“成功”但向上传播所有其他失败。 但是,为了实现Try特性,我必须定义一种“残差”类型,它只封装错误情况:

enum StrandFail {
    NoSolution,
    QuantumExceeded,
    Cycle(Strand, Minimums),
}

但是一旦我有了这种类型,那么我不妨将StrandResult<T>设为别名:

type StrandResult<T> = Result<T, StrandFail>;

这就是我所做的。

现在,这并不一定比拥有一个枚举更糟糕——但它确实感觉有点奇怪。 例如,通常当我为一个函数编写文档时,我不会将结果按“ok”和“error”“分组”,而是将各种可能性称为“equals”。 例如:

    /// Invoked when a strand represents an **answer**. This means
    /// that the strand has no subgoals left. There are two possibilities:
    ///
    /// - the strand may represent an answer we have already found; in
    ///   that case, we can return `StrandFail::NoSolution`, as this
    ///   strand led nowhere of interest.
    /// - the strand may represent a new answer, in which case it is
    ///   added to the table and `Ok` is returned.

请注意,我没有说“我们返回Err(StrandFail::NoSolution) 。这是因为Err感觉就像我必须添加的烦人的工件。

(另一方面,当前的定义将帮助读者了解?的行为是什么,而无需咨询Try impl。)

我想这个结果并不令人惊讶:当前的Try impl 强制您在与Result同构的事物上使用? Result 。 这种一致性并非偶然,但结果是,将?与基本上不是“只是Result ”的东西一起使用会让人厌烦。 (就此而言,它基本上与引起NoneError问题相同——需要人为地定义一个类型来表示Option的“失败”。)

我还要注意的是,在StrandFail的情况下,我并不特别想要普通结果获得的From::from转换,尽管它不会伤害我。

关联的Try::Error类型是否曾经直接暴露给用户/被用户直接使用? 或者它只是作为?脱糖的一部分需要? 如果是后者,我认为仅在“结构上”定义它没有任何实际问题 - type Error = Option<Option<(Strand, Minimums)>>或其他任何东西。 (必须弄清楚enum定义的“失败一半”的结构等价物并不是很好,但似乎没有重新调整整个面向公众的 API 那么烦人。)

我也不跟。 我已经成功地为具有内部错误表示的类型实现了 Try,并且OkError是相同类型的感觉很自然。 你可以在这里看到实现: https :

事实上,似乎有一个相当简单的模式来完成这项工作。

impl std::ops::Try for Value {
    type Ok = Self;
    type Error = Self;

    fn from_ok(v: Self::Ok) -> Self { v }
    fn from_error(v: Self::Error) -> Self { v }
    fn into_result(self) -> Result<Self::Ok, Self::Error> {
        if self.is_ok() { Ok(val) } else { Err(val) }
    }
}

如果你想打开成功,这样的事情应该有效:

impl std::ops::Try for StrandFail<T> {
    type Ok = T;
    type Error = Self;

    fn from_ok(v: Self::Ok) -> Self { StrandFail::Success(v) }
    fn from_error(v: Self::Error) -> Self { v }
    fn into_result(self) -> Result<Self::Ok, Self::Error> {
        match self {
            StrandFail::Success(v) => Ok(v),
            other => Err(other),
        }
    }
}

定义结构类型是可能的,但感觉很烦人。 我同意我可以使用Self 。 虽然你可以使用?StrandResultResult<_, StrandResult>等,但对我来说感觉有点古怪。

很棒的经验报告,@nikomatsakis! 我也对Try不满意(从另一个方向)。

TL/DR :我认为FoldWhile是正确的,我们应该加倍强调Break -vs- Continue?而不是谈论它在错误方面。

更长:

我一直使用Err来获得比“错误”更接近“成功”的东西,因为?非常方便。

  • 使用try_fold来实现position (以及一堆其他迭代器方法)时,我感到很困惑,这与使用Result (我的大脑不喜欢find发现它是一个Err ),我BreakContinue变体制作了自己的

  • 在 itertools 中写入match ,内部函数总是返回ErrNoneIterator::next()的“错误”,但同时它是fold的“成功”,因为您需要走到最后,这是一个奇怪的脱节要做。 使用? (好吧,在该代码中使用try! ,因为它需要在 Rust 1.12 上编译)来处理传播结束条件非常方便。

所以如果不出意外,我认为我为Try写的描述是错误的,不应该谈论“成功/失败的二分法”,而应该从“错误”中抽象出来。

我一直在考虑的另一件事是我们应该考虑为Try一些有点疯狂的实现。 例如, Ordering: Try<Ok = (), Error = GreaterOrLess>与“尝试函数”相结合,可以允许cmpstruct Foo<T, U> { a: T, b: U }

fn cmp(&self, other: &self) -> Ordering try {
    self.a.cmp(&other.a)?;
    self.b.cmp(&other.b)?;
}

我还不知道这是疯狂的好还是坏的 :laughing: 不过,它确实有一些优雅,因为一旦事物不同,你就会知道它们是不同的。 试图将“成功”或“错误”分配给任何一方似乎都不合适。

我还注意到,这是“错误转换”没有帮助的另一个实例。 我也从未在上面的“我想要?但这不是错误”示例中使用它。 而且它肯定会导致推理悲伤,所以我想知道它是否应该仅限于 Result (或者可能被删除以支持.map_err(Into::into) ,但这可能不可行)。

编辑:哦,所有这一切让我想知道“我一直使用 Result 来解决我的错误,而不是为我自己的类型实现Try ”的答案是否是“好的,这是预期的”。

编辑 2:这与上面的https://github.com/rust-lang/rust/issues/42327#issuecomment -318923393 没什么不同

编辑 3:似乎在https://github.com/rust-lang/rfcs/pull/1859#issuecomment -273985250 中也提出了Continue变体

我的两分钱:

我喜欢@fluffysquirrels将特征分成两个特征的建议。 一个用于转换为结果,另一个用于从结果转换。 但我确实认为我们应该保留into_result或等价物作为脱糖的一部分。 我认为此时我们必须使用Option作为Try已经稳定。

我也喜欢@scottmcm使用建议中断/继续而不是错误/确定的名称的想法。

把具体的代码放在这里,我喜欢它的读法:

https://github.com/rust-lang/rust/blob/ab8b961677ac5c74762dcea955aa0ff4d7fe4915/src/libcore/iter/iterator.rs#L1738 -L1746

它当然不如直循环那么好,但是使用“尝试方法”它会很接近:

self.try_for_each(move |x| try { 
    if predicate(&x) { return LoopState::Break(x) } 
}).break_value() 

为了比较,我发现“错误词汇”版本确实具有误导性:

self.try_for_each(move |x| { 
    if predicate(&x) { Err(x) } 
    else { Ok(()) } 
}).err() 

我们可以为 NoneError 实现 Display 吗? 它将允许失败箱自动派生From<NoneError> for failure::Error 。 见https://github.com/rust-lang-nursery/failure/issues/61
它应该是 3 行更改,但我不确定 RFC 等的过程。

@cowang4我想现在尽量避免启用 Result-and-Option 的更多混合,因为类型不稳定主要是为了保持我们的选项在那里打开。 如果我们最终将Try和 desugar 的设计更改为不需要NoneError东西,我不会感到惊讶......

@scottmcm好的。 我明白你的意思。 当库函数返回 None 时,我最终想要一种干净的方法来修饰返回 Err 的模式。 也许你知道除了Try之外的另一个? 例子:

fn work_with_optional_types(pb: &PathBuf) -> Result<MyStruct, Error> {
    if let Some(filestem) = pb.file_stem() {
        if let Some(filestr) = filestem.to_str() {
            return Ok(MyStruct {
                filename: filestr.to_string()
            });
        }
     }
    Err(_)
}

一旦我发现了这个实验性功能和failure板条箱,我自然而然地被吸引到:

use failure::Error;
fn work_with_optional_types(pb: &PathBuf) -> Result<MyStruct, Error> {
    Ok({
        title: pb.file_stem?.to_str()?.to_string()
    })
}

_几乎_有效,除了缺少我之前提到的impl Display for NoneError
但是,如果这不是我们想要使用的语法,那么可能还有另一个函数/宏可以简化模式:

if option.is_none() {
    return Err(_);
}

@cowang4我相信如果你为failure::Error实现了From<NoneError> failure::Error ,它使用你自己的类型实现了Display

但是,使用opt.ok_or(_)?可能是更好的做法,因此您可以明确说明如果 Option 为 None 则错误应该是什么。 例如,在您的示例中,如果pb.file_stem为 None 而不是to_str()返回 None,则您可能需要不同的错误。

@tmccombs我尝试创建自己的错误类型,但我一定是做错了。 它是这样的:

#[macro_use] extern crate failure_derive;

#[derive(Fail, Debug)]
#[fail(display = "An error occurred.")]
struct SiteError;

impl From<std::option::NoneError> for SiteError {
    fn from(_err: std::option::NoneError) -> Self {
        SiteError
    }
}

fn build_piece(cur_dir: &PathBuf, piece: &PathBuf) -> Result<Piece, SiteError> {
    let title: String = piece
        .file_stem()?
        .to_str()?
        .to_string();
    Ok(Piece {
        title: title,
        url: piece
            .strip_prefix(cur_dir)?
            .to_str()
            .ok_or(err_msg("tostr"))?
            .to_string(),
    })
}

然后我尝试使用我的错误类型...

error[E0277]: the trait bound `SiteError: std::convert::From<std::path::StripPrefixError>` is not satisfied
   --> src/main.rs:195:14
    |
195 |           url: piece
    |  ______________^
196 | |             .strip_prefix(cur_dir)?
    | |___________________________________^ the trait `std::convert::From<std::path::StripPrefixError>` is not implemented for `SiteError`
    |
    = help: the following implementations were found:
              <SiteError as std::convert::From<std::option::NoneError>>
    = note: required by `std::convert::From::from`

error[E0277]: the trait bound `SiteError: std::convert::From<failure::Error>` is not satisfied
   --> src/main.rs:195:14
    |
195 |           url: piece
    |  ______________^
196 | |             .strip_prefix(cur_dir)?
197 | |             .to_str()
198 | |             .ok_or(err_msg("tostr"))?
    | |_____________________________________^ the trait `std::convert::From<failure::Error>` is not implemented for `SiteError`
    |
    = help: the following implementations were found:
              <SiteError as std::convert::From<std::option::NoneError>>
    = note: required by `std::convert::From::from`

好的,我刚刚意识到它想知道如何从其他错误类型转换为我的错误,我可以通用地写:

impl<E: failure::Fail> From<E> for SiteError {
    fn from(_err: E) -> Self {
        SiteError
    }
}

不...

error[E0119]: conflicting implementations of trait `std::convert::From<SiteError>` for type `SiteError`:
   --> src/main.rs:183:1
    |
183 | impl<E: failure::Fail> From<E> for SiteError {
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |
    = note: conflicting implementation in crate `core`:
            - impl<T> std::convert::From<T> for T;

好的,那std::error::Error呢?

impl<E: std::error::Error> From<E> for SiteError {
    fn from(_err: E) -> Self {
        SiteError
    }
}

那也行不通。 部分是因为它与我的From<NoneError>冲突

error[E0119]: conflicting implementations of trait `std::convert::From<std::option::NoneError>` for type `SiteError`:
   --> src/main.rs:181:1
    |
175 | impl From<std::option::NoneError> for SiteError {
    | ----------------------------------------------- first implementation here
...
181 | impl<E: std::error::Error> From<E> for SiteError {
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `SiteError`
    |
    = note: upstream crates may add new impl of trait `std::error::Error` for type `std::option::NoneError` in future versions

这很奇怪,因为我认为NoneError没有实现std::error::Error 。 当我注释掉我的非通用impl From<NoneError>我得到:

error[E0277]: the trait bound `std::option::NoneError: std::error::Error` is not satisfied
   --> src/main.rs:189:25
    |
189 |       let title: String = piece
    |  _________________________^
190 | |         .file_stem()?
    | |_____________________^ the trait `std::error::Error` is not implemented for `std::option::NoneError`
    |
    = note: required because of the requirements on the impl of `std::convert::From<std::option::NoneError>` for `SiteError`
    = note: required by `std::convert::From::from`

我是否必须手动编写所有From 。 我认为失败箱应该派生他们?

也许我应该坚持option.ok_or()

我是否必须手动编写所有 Froms。 我认为失败箱应该派生他们?

我不认为失败箱会那样做。 但我可能是错的。

好的,所以我重新检查了故障箱,如果我正在阅读文档和不同版本的正确,它被设计为始终使用failure::Error作为Result的错误类型,看到这里。 而且,它确实为大多数错误类型实现了一个全面的impl Fail特性:

impl<E: StdError + Send + Sync + 'static> Fail for E {}

https://github.com/rust-lang-nursery/failure/blob/master/failure-1.X/src/lib.rs#L218

然后是impl From以便它可以将Try / ?其他错误(如 std 中的错误)转换为总体failure::Error类型。
rust impl<F: Fail> From<F> for ErrorImpl
https://github.com/rust-lang-nursery/failure/blob/d60e750fa0165e9c5779454f47a6ce5b3aa426a3/failure-1.X/src/error/error_impl.rs#L16

但是,只是由于与此锈蚀问题相关的错误NoneError是实验性的,因此还不能自动转换,因为它没有实现Display特性。 而且,我们不希望它这样做,因为这更加模糊了OptionResult之间的界限。 这一切可能最终都会重新处理和整理,但现在,我将坚持我学到的脱糖技术。

谢谢大家的帮助。 我正在慢慢学习 Rust! :微笑:

我会坚持我学到的脱糖技术。

:+1: 老实说,我认为.ok_or(...)?仍然是要走的路(或.ok_or_else(|| ...)? ,当然)。 即使NoneError _had_ 一个Display impl,它会说什么? “有什么东西不在那里”? 这不是一个很大的错误......

试图更接近一个具体的建议......

我开始喜欢TrySuccess替代品。 有趣的是,我认为_没有人_,包括我自己,最初喜欢那个——它甚至不在 RFC 的最终版本中。 但值得庆幸的是,它存在于历史中: https :

对我来说,最大的反对意见是合理的抱怨,即仅关联类型( trait TrySuccess { type Success; } )的整个核心特征是矫枉过正。 但是catch try阻止(https://github.com/rust-lang/rust/issues/41414#issuecomment-373985777)进行 ok-wrapping(如在 RFC 中) ,突然之间它有一个直接而重要的用途:这是控制包装的特征。 再加上我认为通常需要的“ ?应该始终产生相同类型”的目标,该特性似乎更能保持其重要性。 稻草人:

trait TryContinue {
    type Continue;
    fn from_continue(_: Self::Continue) -> Self;
}

该关联类型,作为将从?运算符返回的类型,也是显然需要存在的类型。 这意味着它没有遇到Niko 所表达的“必须定义一种‘残余’类型”的烦恼。 并且()是合理的,甚至是常见的,因此避免了NoneError类的扭曲。

编辑:对于自行车棚,“返回”可能是一个好词,因为当您从try方法(又名 ok-wrapping)中return时会发生这种情况。 return也是这个的monad 操作符名称,iiuc...

编辑 2:我对其他特征/方法的想法尚未稳定,@tmccombs。

@scottmcm只是为了清楚,或者您建议以下两个特征?

trait TryContinue {
  type Continue;
  fn from_continue(_: Self::Continue) -> Self;
}

trait Try<E>: TryContinue {
  fn try(self) -> Result<Self::Continue, E>
}

和 desuuring x?看起来像:

x.try() match {
    Ok(c) => c,
   Err(e) => throw e // throw here is just a placeholder for either returning or breaking out of a try block
}

try { ...; expr}将脱糖为类似的东西:

{ 
    ...
    TryContinue::from_continue(expr);
}

@scottmcm在考虑 ok-wrapping 时,我也发现该变体更具吸引力 =)

继续讨论下一个特征,我认为?变成了这个(模大规模自行车棚):

trait Try<Other: TryContinue = Self>: TryContinue + Sized {
    fn check(x: Other) -> ControlFlow<Other::Continue, Self>;
}

enum ControlFlow<C, B> {
    Continue(C),
    Break(B),
}

各种理由:

  • 这需要TryContinue作为其参数类型,以便x?表达式的类型始终相同
  • 对于像try_fold这样检查并返回相同类型的简单情况,默认类型参数为Self ,因此只需T: Try
  • 这个 trait 扩展了TryContinue因为如果用作返回类型的类型允许在其主体中使用? ,那么它也应该支持 ok-wrapping。
  • 新的 enum 与 result 是同构的,但避免了如上所述的“错误”,因此我认为它为每个变体提供了非常明确的含义。
  • ?是泛型类型,因此,像TryContinue ,它“从某物产生Self

完整的概念验证演示,包括try{} / ?宏和OptionResultOrderinghttps ://play.rust-lang.org/?gist=18663b73b6f35870d20fd172643a4f96&version=stable (感谢@nikomatsakis在一年前制作版本🙂)

缺点:

  • Result impl 中仍然有我不太喜欢的幻像类型参数

    • 但也许这是一个很好的权衡,因为在Ordering impl 中不需要额外的LessOrGreater类型。

    • 无论如何,impls 中的幻像类型参数都没有类型那么讨厌

  • 不提供一致的From转换

    • 但这可能很好,因为StrandResult无论如何都不在乎,对于像SearchResult这样的东西,它会很奇怪,因为 Break 路径是成功路径,它最终可能有助于推理无论如何,嵌套的?案例。

  • throw语法是什么并不那么明显

    • 这失去了?作为Ok(x) => x, Err(r) => throw e.into() ,我真的很喜欢

    • 但也可以让throw;成为产生None (通过类似impl<T> Throw<()> for Option<T> ),这比throw NoneError;好得多

    • 无论如何,将其分开可能会很好,因为throw LessOrGreater::Less会_真的_愚蠢。

编辑: Ping @glaebhoerl ,作为 RFC 1859 的重要参与者,我想对此发表意见。

编辑 2:也抄送 @colin-kiegel,对于来自https://github.com/rust-lang/rfcs/pull/1859#issuecomment -287402652 的声明:

我想知道本质主义方法是否可以在不牺牲上述目标的情况下采用还原论者的一些优雅。

我真的很喜欢那个提议。

throw 语法是什么并不那么明显

忽略来自其他语言的 throw 关键字的包袱,“throw”确实有意义,因为您将值“抛出”到更高的范围。 除了错误情况外,Python 和 scala 也确实对控制流使用异常(不过,在 scala 的情况下,您通常不会直接使用 try/catch),因此有一些先例可以将 throw 用于成功路径。

@tmccombs

忽略来自其他语言的 throw 关键字的包袱,“throw”确实有意义,因为您将值“抛出”到更高的范围。

尽管它带有类似的包袱,但我怀疑“加注”更合适:

throw (v):把或导致去或进入某个地方、位置、条件等,好像通过投掷:

raise (v):移动到更高的位置; 抬起; 提升

可能有一种方法可以将“加注”与?的逻辑结合起来,因为加注也可能意味着“收集”。 类似于: Ok(v) => v, Err(e) => raise From::from(e) ,其中raise模仿匹配的模式(例如,给定一个模式Err(e)它是ControlFlow::Break(Err(...))语法魔术)。

我怀疑“加注”更合适

好点子

@scottmcm

throw 语法是什么并不那么明显

有没有理由我们不能拥有from_err(value: Other)

更新:实际上,也许我对Other的角色感到困惑。 我必须多研究一下这个特征。 =)

@nikomatsakis

有没有理由我们不能拥有from_err(value: Other)

好吧, Other是一个完整的? -able 类型(如Result ),所以我不希望throw Ok(4)工作。 (并且它需要是整个析取以避免强制引入人为错误类型。)例如,我认为我们当前的 Option-Result 互操作将等同于这个(以及相反的):

impl<T, F, U> Try<Option<U>> for Result<T, F>
   where F: From<NoneError>
{
    fn check(x: Option<U>) -> ControlFlow<U, Self> {
        match x {
            Some(x) => ControlFlow::Continue(x),
            None => ControlFlow::Break(Err(From::from(NoneError))),
        }
    }
}

我目前对throw倾向是这样的:

trait TryBreak<T> : TryContinue { from_break(_: T) -> Self }

throw $expr  ==>  break 'closest_catch TryBreak::from_break($expr)
  • 扩展TryContinue以便 ok-wrapping 也可用于使用它的方法的返回类型
  • 不是关联类型,因此您可以根据需要同时为类型实现TryBreak<TryFromIntError>TryBreak<io::Error>

    • impl<T> TryBreak<()> for Option<T>仅启用throw;感觉似乎合理,而()的相关错误类型却没有。

    • impl<T> TryBreak<!> for T也不错,但可能语无伦次。

(旁白:这些特征和方法名称很糟糕;请帮忙!)

我对这里提出的其他问题的想法还没有形成一个易于表达的形式,但是关于From::from()脱糖周围的类型推断问题(我不记得最近是否也在其他地方讨论过这个问题,还是只在这里?):

本能的感觉(来自 Haskell 的经验)“那种谎言类型推断歧义”是我不想将From转换作为原始 RFC 的一部分的原因之一(尽管不是主要原因)。 但是,既然它已经融入蛋糕中,我想知道我们是否不能尝试通过“只是”在类型推断过程中为其添加一个特殊的默认规则来尝试拥有那个蛋糕并吃掉它?

也就是说,我认为:每当我们看到From::from() [可选:由?脱糖插入的,而不是手动编写的],并且我们确切地知道这两种类型中的一种(输入vs. 输出),而另一个是不明确的,我们默认另一个与另一个相同。 换句话说,我认为,当不清楚使用哪个impl From时,我们默认为impl<T> From<T> for T 。 我认为,这基本上总是你真正想要的? 这可能有点临时,但如果它确实有效,恕我直言,这些好处似乎值得付出代价。

(我还认为From已经是一个 lang 项,正是由于?脱糖,但它似乎不是?无论如何,出于这个原因,它在某些方面已经很特别.)

@scottmcm的提议中, From::from() _not_ 是脱糖的一部分,而是在TryResult

@tmccombs我没有对他的提案提出修正案。

try{} RFC (https://github.com/rust-lang/rfcs/pull/2388) 一直在讨论try块,人们不关心结果。 这种替代方法似乎可以很好地处理这个问题,因为它可以选择完全忽略错误类型,如果它愿意,允许

let IgnoreErrors = try {
    error()?;
    none()?;
};

使用与以前相同的特征的概念验证实现: https :

我认为那里有一些有趣的可能性,特别是因为自定义实现可以只获取结果并绑定E: Debug因此它会自动记录发生的任何错误。 或者,可以将一个版本专门用作main的返回类型,并结合Termination “正常工作”,让您使用?而没有复杂的类型签名(https ://github.com/rust-lang/rfcs/issues/2367)。

我遇到了与@nikomatsakis使用现有版本的Try特征所证明的问题类似的问题。 具体问题参见https://github.com/SergioBenitez/Rocket/issues/597#issuecomment -381533108。

@scottmcm提出的特征定义解决了这些问题。 然而,它们似乎比必要的要复杂。 我尝试重新实现它们并提出以下几点:

#[derive(Debug, Copy, Clone)]
enum ControlFlow<C, B> {
    Continue(C),
    Break(B),
}

// Used by `try { }` expansions.
trait FromTry: Try {
    fn from_try(value: Self::Continue) -> Self;
}

// Used by `?`.
trait Try<T = Self>: Sized {
    type Continue;
    fn check(x: T) -> ControlFlow<Self::Continue, Self>;
}

主要的变化是Continue关联类型在Try特征上,而不是FromTry (以前是TryContinue )。 除了简化定义之外,这样做的好处是Try可以独立于FromTry ,我认为这更常见,并且实现FromTry一次Try就简化TryFromTry一起实现,我们可以简单地将from_try方法移动到Try

请参阅完整的操场,其中包含ResultOption以及 Rocket 的Outcome在这个操场上

感谢您的报告,@SergioBenitez! 该实现与 RFC 1859 中原始特征提案的“翻转类型参数”替代版本相匹配: https :

最大的损失是typeof(x?)仅依赖于typeof(x)的属性。 缺少该属性是原始版本的问题之一(“我有点担心代码的可读性”https://github.com/rust-lang/rfcs/pull/1859#issuecomment-279187967)和一个优势最终的还原论提案(“对于任何给定的类型 T,? 可以产生一种正确/错误值”https://github.com/rust-lang/rfcs/pull/1859#issuecomment-283104310)。 当然,也有人认为该属性是不必要的或限制性太强,但在 FCP 之前的最终总结中,它仍然作为优势存在(https://github.com/rust-lang/rfcs/pull/1859#issuecomment -295878466)。

除了简化定义之外,这样做的好处是Try可以独立于FromTry ,我认为这更常见

当然今天from_ok不太常见,因为Trydo catch是不稳定的。 即使do catch是稳定的,我也同意它的使用量会少于? (因为大多数这样的块包含多个? )。

然而,从特征及其操作的角度来看,我认为“在载体类型中包装'继续前进'值”是?绝对必要的部分。 今天很少有人通过这个特性——只是使用Ok(...)Some(...)代替——但它对于泛型使用至关重要。 例如, try_fold

https://github.com/rust-lang/rust/blob/8728c7a726f3e8854f5a80b474d1a8bacab10304/src/libcore/iter/iterator.rs#L1478 -L1482

如果一个函数要返回一个载体类型,那么有一种方法可以将感兴趣的值放入该载体类型中,这一点至关重要。 而且,重要的是,我不认为提供这个是困难的。 它对结果、选项、结果等有一个非常自然的定义。

@Centril之前还指出,“将标量包装在载体中”在理论上也是一个更简单的构造。 例如,Haskell 将其称为Pointed类型类,尽管我认为我们不想在 Rust 中将它泛化到 _that_ 远:允许try { 4 }vec![4]对我来说似乎有点矫枉过正.

我也在想象一个未来,就像async函数被提议将块值包装到未来一样,我们可能有try函数将块值包装成易出错的类型。 同样, TryContinue是关键部分——如果我们有一个throw构造,这样的函数甚至可能不会使用?

因此,不幸的是,所有这些都比具体更具哲学性。 让我知道它是否有意义,或者您是否认为任何部分的相反:slightly_smiling_face:

编辑:如果您收到一封包含早期版本的电子邮件,我们深表歉意; 我太早点击“评论”了...

最大的损失是 typeof(x?) 只依赖于 typeof(x) 的属性。

啊,是的,绝对。 感谢您指出了这一点。

当然,也有人认为该属性是不必要的或限制性太强,但在 FCP 之前的最终总结中,它仍然作为优势存在(rust-lang/rfcs#1859 (comment))。

是否有任何具体示例说明它可能过于严格?

如果一个函数要返回一个载体类型,那么有一种方法可以将感兴趣的值放入该载体类型中,这一点至关重要。 而且,重要的是,我不认为提供这个是困难的。

我认为这是一个公平的分析。 我同意。

@SergioBenitez来自https://github.com/rust-lang/rfcs/pull/1859#issuecomment -279187967

所以问题是,提议的限制是否足够? 在确定成功类型时是否可以很好地利用错误案例上下文? 是否存在滥用行为?

我可以从我在期货方面的经验说,这里很可能有一些有用的案例。 特别是,您谈论的 Poll 类型可以通过几种方式进行处理。 有时,我们想要跳出 NotReady 变体并留下本质上的 Result 值。 有时,我们只对 Ready 变体感兴趣,并想跳出其他变体中的任何一个(如您的草图)。 如果我们允许成功类型部分由错误类型决定,那么支持这两种情况更合理,并且基本上使用上下文来确定需要哪种分解。

OTOH,我有点担心这些方面的代码可读性。 这感觉与简单地转换错误分量有质的不同——这意味着基本匹配? 将执行取决于上下文信息。

因此,人们可以想象一种将 _both_ 类型移动到类型参数的特征,例如

trait Try<T,E> {
    fn question(self) -> Either<T, E>;
}

并使用它来启用所有

let x: Result<_,_> = blah.poll()?; // early-return if NotReady
let x: Async<_> = blah.poll()?; // early-return if Error
let x: i32 = blah.poll()?; // early-return both NotReady and Errors

但我认为这绝对是一个坏主意,因为这意味着这些不起作用

println!("{}", z?);
z?.method();

因为没有类型上下文来说明要生成什么。

另一个版本是启用这样的功能:

fn foo() -> Result<(), Error> {
    // `x` is an Async<i32> because NotReady doesn't fit in Result
    let x = something_that_returns_poll()?;
}
fn bar() -> Poll<(), Error> {
    // `x` is just i32 because we're in a Poll-returning method
    let x = something_that_returns_poll()?;
}

我的直觉是,推论“流出了?” 有太多令人惊讶的,因此这是在“太聪明”的范围内。

关键是,我不认为没有它是太严格了。 my_result? -> Poll函数中的Result的方法中在Poll ?上使用.wait(): T (阻止结果)或.ready(): Option<T> (检查是否完成)选择模式可能更好。

这是“有趣的” https://play.rust-lang.org/?gist=d3f2cd403981a631f30eba2c3166c1f4&version=nightly&mode=debug

我不喜欢这些 try (do catch) 块,它们似乎不太适合新手。

我正在尝试汇总提案的当前状态,该状态似乎分散在多个评论中。 是否有当前提议的特征集的单个摘要(删除Error关联类型)?

在这个线程的早期,我看到一条关于将Try拆分为与错误特征分开的评论——是否有任何计划来实现这种拆分?

在问号上进行从Result<T, E>到任何类型Other<T2, E>透明转换会很有用——这将允许现有的 IO 函数从具有更专业化的函数中以漂亮的语法调用(例如懒惰)返回类型。

pub fn async_handler() -> AsyncResult<()> {
    let mut file = File::create("foo.txt")?;
    AsyncResult::lazy(move || {
        file.write_all(b"Hello, world!")?;
        AsyncResult::Ok(())
    })
}

从语义上讲,这感觉就像From::from(E) -> Other<T2, E> ,但是From的使用目前仅限于现有的Result等效的Try实现。

我真的认为NoneError应该有一个单独的跟踪问题。 即使Try特性永远不会稳定, NoneError应该稳定下来,因为它使得在Option上使用?更加符合人体工程学。 对于诸如struct MyCustomSemanthicalError;错误或实现Default错误,请考虑这一点。 None可以通过From<NoneError>轻松转换为MyCustomSeemanthicalError From<NoneError>

在处理https://github.com/rust-analyzer/rust-analyzer/ 时,我遇到了一个与?操作符不足的剪纸略有不同,特别是当返回类型为Result<Option<T>, E>

对于这种类型, ?有效地脱糖是有意义的:

match value? {
    None => return Ok(None),
    Some(it)=>it,
}

其中value的类型Result<Option<V>, E> ,或者:

value?

其中valueResult<V, E> 。 我相信如果标准库以以下方式为Option<T>实现Try ,这是可能的,尽管我没有明确测试过这个,我认为可能存在专业化问题。

enum NoneError<E> {
    None,
    Err(E),
}

impl From<T> for NoneError<T> {
    fn from(v: T) -> NoneError<T> {
        NoneError:Err(v)
    }
}

impl<T, E> std::Ops::Try for Result<Option<T>, E> {
    type OK = T;
    type Error = NoneError<E>;
    fn into_result(self) -> Result<Self::OK, Self::Error> {
        match self {
            Ok(option) => {
                if let Some(inner) = option {
                    Ok(inner)
                } else {
                    Err(NoneError::None)
                }
            }
            Err(error) => {
                Err(NoneError::Err(error));
            }
        }
    }
    fn from_error(v: Self::Error) -> Self {
        match v {
            NoneError::Err(error) => Err(error),
            None => Some(None),
        }
    }
    fn from_ok(v: Self::OK) -> Self {
        Ok(Some(v))
    }
}

impl<T> std::Ops::Try for Option<T> {
    type OK = T;
    type Error = NoneError<!>;
    fn into_result(self) -> Result<Self::OK, Self::Error> {
        match self {
            None => Err(NoneError::None),
            Some(v) => Ok(v),
        }
    }
    fn from_error(v: Self::Error) -> Self {
        match v {
            NoneError::None => Some(None),
            _ => unreachable!("Value of type ! obtained"),
        }
    }
    fn from_ok(v: Self::OK) -> Self {
        Ok(v)
    }
}

当询问@matklad为什么他不能创建实现Try的自定义枚举(在这种情况下称为Cancellable时,他指出std::ops::Try是不稳定的,所以它无论如何都不能使用,因为rust-analyzer (当前)针对的是稳定的锈蚀。

https://github.com/rust-lang/rust/issues/31436#issuecomment -441408288 转帖因为我想对此发表评论,但我认为这是错误的问题:

本质上,我遇到的情况是 GUI 框架中的回调 - 而不是返回OptionResult ,他们需要返回UpdateScreen ,以告诉框架屏幕需要更新与否。 通常我根本不需要记录(记录每个小错误是不切实际的),并且在发生错误时只返回UpdateScreen::DontRedraw 。 但是,使用当前的?运算符,我必须一直这样写:

let thing = match fs::read(path) {
    Ok(o) => o,
    Err(_) => return UpdateScreen::DontRedraw,
};

由于我无法通过 Try 运算符将Result::Err转换为UpdateScreen::DontRedraw ,因此这变得非常乏味 - 通常我在哈希映射中进行简单的查找可能会失败(这不是错误) - 在一次回调中,我经常使用?运算符 5 - 10 次。 因为上面写的很冗长,我目前的解决方案是像这样impl From<Result<T>> for UpdateScreen ,然后像这样在回调中使用一个内部函数:

fn callback(data: &mut State) -> UpdateScreen {
     fn callback_inner(data: &mut State) -> Option<()> {
         let file_contents = fs::read_to_string(data.path).ok()?;
         data.loaded_file = Some(file_contents);
         Some(())
     }

    callback_inner(data).into()
}

由于回调用作函数指针,我不能使用-> impl Into<UpdateScreen> (出于某种原因,当前不允许函数指针返回impl )。 因此,我使用Try运算符的唯一方法就是执行内部函数技巧。 如果我能简单地做这样的事情就好了:

impl<T> Try<Result<T>> for UpdateScreen {
    fn try(original: Result<T>) -> Try<T, UpdateScreen> {
        match original {
             Ok(o) => Try::DontReturn(o),
             Err(_) => Try::Return(UpdateScreen::DontRedraw),
        }
    }
}

fn callback(data: &mut State) -> UpdateScreen {
     // On any Result::Err, convert to an UpdateScreeen::DontRedraw and return
     let file_contents = fs::read_to_string(data.path)?;
     data.loaded_file = Some(file_contents);
     UpdateScreen::Redraw
}

我不确定当前的提案是否可行,只是想添加我的用例以供考虑。 如果自定义 Try 运算符可以支持这样的操作,那就太好了。

@joshtriplett对不起,我花了一段时间才回到这个问题。 我已经在https://github.com/rust-lang/rust/compare/master...scottmcm :try-trait-v2 上将提案的工作原型放在一起。 我希望用它尝试更多的东西。

@scottmcm你对这些变化有什么更高层次的解释吗?

@scottmcm FWIW 我也试过你对人造丝的改变:
https://github.com/rayon-rs/rayon/compare/master...cuviper :try-trait-v2
(仍然使用私人副本而不是不稳定的项目)

那么转换Option NoneError的解决方法是什么? 似乎,因为它实现了 Try Trait,除非您启用使用该特定(不稳定?)功能,否则它不会编译。

所以基本上,你不能使用 ? 据我所知,带有 Option 的操作员?

@omarabid ,该运算符对于OptionResult使用是稳定的,但是在稳定之前您不能将Try用作通用约束。 在返回Option的函数中对Option使用?完全没问题,因为您根本不必涉及NoneError 。 如果您擦除类型,您还可以返回Result

use std::fmt::Debug;

pub struct Error(Box<dyn Debug>);

impl<T: Debug + 'static> From<T> for Error {
    fn from(error: T) -> Self {
        Error(Box::new(error))
    }
}

pub fn try_get<T>(slice: &[T], index: usize) -> Result<&T, Error> {
    Ok(slice.get(index)?)
}

操场

@scottmcm ,你的原型try-trait-v2在这个例子中失败了!

如果我们不希望我的示例出错, try-trait-v2将需要如下内容:

#[unstable(feature = "try_trait_v2", issue = "42327")]
impl<T, U, E: From<NoneError>> ops::Bubble<Result<U, E>> for Option<T> {
    fn bubble(self) -> ops::ControlFlow<T, Result<U, E>> {
        match self {
            Some(x) => ops::ControlFlow::Continue(x),
            None => ops::ControlFlow::Break(Err(E::from(NoneError))),
        }
    }
}

此功能的当前状态是什么?

PR #62606 记录为迭代器实现try_fold文档应该在稳定后重新打开。

编辑:使用跟踪项目更新操作 ~ scottmcm

是否有计划用此线程中建议的任何替代方案替换Try特性? @scottmcm建议的版本看起来不错。 我想继续将?运算符与Option ,并且我认为应该更改Try特性,不要在Option上强制使用Result语义Option

使用@scottmcm的替代方法将允许我们使用?Option并摆脱NoneError 。 我同意@nikomatsakis评论)的观点,即Try特性不应该促进“人为地定义一种类型来表示Option的'失败'”的需要。

pub struct Error(Box<dyn std::fmt::Debug>);
impl<T: std::fmt::Debug + 'static> From<T> for Error { fn from(error : T) -> Self { Error(Box::new(error)) } }
type Result<T> = std::result::Result<T, Error>;

这里的初学者,我有点固执地想要得到? 自动键入擦除任何错误和选项。
在花了太多时间试图理解为什么其他可能的解决方案无法实现之后,我发现@cuviper是我能得到的最接近的。
一些解释会有所帮助,但至少我必须熟悉一些 Rust 元编程限制。
所以我试图弄清楚并具体解释。
这个线程似乎是最有可能的十字路口,我希望可以帮助像我这样绊倒的人,随时纠正:

  • 使用 Debug (常见于 StdError 和 NoneError)而不是 StdError :
    一个通用的From<T: StdError> for Error和一个特殊的From<NoneError> for Error冲突
    因为如果 NoneError 实现了 StdError 就会模棱两可(显然即使默认也不允许覆盖并强制执行排他性)
    这可以通过不受支持的“负边界”来解决,也许是因为它是唯一的用例? (过度专业化)
    impl StdError for NoneError 只能与 StdError 或 NoneError 本身一起定义,因此它在任何下游都是一致的。
  • 错误不能是别名:
    type Error = Box<Debug>绑定 Debug,这使得From<T:Debug> for ErrorFrom<T> for T冲突(自反为幂等)

因此,因为 Error 无法实现 Debug 您可能希望有一个帮助程序使用可传递的 Debug 解包为 Result :

fn from<T>(result : Result<T>) -> std::result::Result<T, Box<dyn std::fmt::Debug>> { match result { Ok(t) => Ok(t), Err(e) => Err(e.0) } }

它不能是impl From<Result<T>> for StdResult<T>Into<StdResult> for Result<T> (不能为上游类型实现上游特征)

例如,您可以使用它来获取 Termination 的 Debug 返回:

fn main() -> std::result::Result<(), Box<dyn std::fmt::Debug>> { from(run()) }

邪恶的想法:也许?操作符可以以某种方式代表一个 monadic 绑定,所以

let x: i32 = something?;
rest of function body

变成

Monad::bind(something, fn(x) {
    rest of function body
})

一个糟糕的主意,但它让我内心的极客高兴。

@derekdreery我认为这不适用于像returncontinue这样的命令式控制流

请记住, ?运算符的语义已经稳定。 只有实际的Try trait 是不稳定的,任何更改都必须对OptionResult保持相同的稳定效果。

@KrishnaSannasi

@derekdreery我认为这不适用于像 return 和 continue 这样的命令式控制流

我同意这个说法。

@cuviper

请记住 ? 运营商已经稳定了。 只有实际的 Try trait 是不稳定的,任何更改都必须对 Option 和 Result 保持相同的稳定效果。

跨时代也是如此吗?

更一般地说,我认为不可能在 Rust 中统一.await?/早期返回、Option::map、Result::map、Iterator::map 等概念,但是理解这些都共享一些结构肯定有助于我成为一个更好的程序员。

为 OT 道歉。

我不确定是否允许一个纪元/版本更改?脱糖,但这肯定会使许多跨板条箱的问题(如宏)复杂化。

我对稳定性保证和时代 RFC 的解释是?行为(糖或其他)可以改变,但它在Option / Result类型上的当前行为必须保持不变同样,因为打破它会产生比我们希望证明的更多的流失。

如果Option不知何故是Result的类型别名怎么办? 即type Option<T> = Result<T, NoValue>Option::<T>::Some(x) = Result::<T, NoValue>::Ok(x)Option::<T>::None = Result::<T, NoValue>::Err(NoValue) ? 这需要一些魔法来实现,在当前的语言环境中并不现实,但也许值得探索。

我们无法进行此更改,因为存在依赖于OptionResult是不同类型的特征实现。

如果我们忽略这一点,那么我们可以更进一步,甚至将bool设为Result<True, False>的别名,其中TrueFalse是单位类型。

是否考虑为单位()添加Try impl ? 对于不返回任何内容的函数,在非关键函数(例如日志回调)出现错误的情况下,提前返回可能仍然有用。 或者,单元是否被排除在外是因为在任何情况下都最好不要默默忽略错误?

或者,单元是否被排除在外是因为在任何情况下都最好不要默默忽略错误?

这个。 如果您想忽略非关键上下文中的错误,您应该使用unwrap或其变体之一。

能够在foo() -> ()类的东西上使用?在条件编译上下文中非常有用,应该强烈考虑。 我认为这与您的问题不同。

您应该使用 unwrap 或其变体之一。

unwrap()会引起恐慌,因此我不建议在非关键上下文中使用它。 在需要简单返回的情况下,这是不合适的。 由于?在源代码中的显式、可见使用,人们可以提出一个论点,即?并非完全“沉默”。 这样?unwrap()与单元函数类似,只是返回语义不同。 我看到的唯一区别是unwrap()会产生可见的副作用(中止程序/打印堆栈跟踪)而?不会。

现在,在单元函数中提前返回的人机工程学比返回ResultOption要差得多。 也许这种状态是可取的,因为在这些情况下,用户应该使用ResultOption的返回类型,这为他们更改 API 提供了动力。 无论哪种方式,最好在 PR 或文档中讨论单元返回类型。

能够在foo() -> ()类的东西上使用?在条件编译上下文中非常有用,应该强烈考虑。 我认为这与您的问题不同。

这将如何运作? 总是评估为 Ok(()) 并被忽略?

另外,你能举一个例子说明你想在哪里使用它吗?

是的,这个想法是像 MyCollection::push 这样的东西,根据编译时的配置,如果集合被配置为在错误时恐慌,那么它可能有一个 Result<(), AllocError> 返回值或一个 () 返回值。 使用集合的中间代码不应该考虑这一点,所以如果它可以简单地_always_使用?即使返回类型是()也会很方便。

将近 3 年之后,这是否更接近于解决?

@Lokathor只有在返回类型Result<Result<X,Y>,Z>或类似的不可能时才有效。 但它是。 因此不可能。

我不明白为什么分层结果会导致问题。 你能详细说明一下吗?

出于交叉引用的目的, @dureuill提出了一种关于内部结构的替代公式:

https://internals.rust-lang.org/t/a-slightly-more-general-easier-to-implement-alternative-to-the-try-trait/12034

@Lokathor
好吧,我想得更深入了,我想我可能已经找到了一个很好的解释。

使用注释

问题是返回类型的解释或奇怪的注释会使代码混乱。 这是可能的,但它会使代码更难阅读。 (前提: #[debug_result]确实应用了你想要的行为,并修改了一个函数在发布模式下恐慌而不是返回Result::Err(...) ,并解包Result::Ok ,但这部分很棘手)

#[debug_result]
fn f() -> Result<X, Y>;

#[debug_result]
fn f2() -> Result<Result<A, B>, Y>;

#[debug_result]
fn g() -> Result<X, Y> {
    // #[debug_result_spoiled]
    let w = f();
    // w could have type X or `Result<X,Y>` based on a #[cfg(debug_...)]
    // the following line would currently only work half of the time
    // we would modify the behavoir of `?` to a no-op if #[cfg(not(debug_...))]
    // and `w` was marked as `#[debug_result]`-spoiled
    let x = w?;

    // but it gets worse; what's with the following:
    let y = f2();
    let z = y?;
    // it has completely different sematics based on a #[cfg(debug_...)],
    // but would (currently?) print no warnings or errors at all,
    // and the type of z would be differently.

    Ok(z)
}

因此,它会使代码更难阅读。
我们不能简单地将?的行为简单地修改为无操作,
特别是如果#[debug_result]的返回值保存在临时变量中,然后使用?运算符“尝试传播”。 它会使?的语义变得非常混乱,因为它取决于许多“变量”,这些“变量”在“编写时”不一定知道,或者仅通过阅读源代码就很难猜测。 #[debug_result]将需要破坏分配了函数值的变量,但如果一个函数用#[debug_result]标记而一个函数没有标记,则它将不起作用,例如以下将是类型错误。

// f3 is not annotated
fn f3() -> Result<X, Y>;

// later inside of a function:
   // z2 needs to be both marked spoiled and non-spoiled -> type error
   let z2 = if a() {
       f3()
   } else {
       f()
   };
   // althrough the following would be possible:
   let z2_ = if a() {
       f3()?
   } else {
       f()?
   };

替代方案:使用新类型

“更干净”的解决方案可能是DebugResult<T, E>类型,当设置了某个编译标志并且它是从错误构造的时,它只会发生恐慌,否则将等效于Result<T, E> 。 但我认为,对于当前的提案,这也是可能的。

回复@zserik的最后

@tema3210我知道。 我只想指出@Lokathor的想法在实践中通常行不通。 唯一可能部分起作用的是以下内容,但只有许多限制,我认为从长远来看不值得:

// the result of the fn *must* have Try<Ok=()>
// btw. the macro could be already implemented with a proc_macro today.
#[debug_result]
fn x() -> Result<(), XError> {
  /* ..{xbody}.. */
}

// e.g. evaluates roughly to:
//..begin eval
fn x_inner() -> Result<(), XError> {
  /* ..{xbody}.. */
}

#[cfg(panic_on_err)]
fn x() {
  let _: () = x_inner().unwrap();
}
#[cfg(not(panic_on_err))]
fn x() -> Result<(), XError> {
  x_innner()
}

//..end eval

fn y() -> Result<(), YError> {
  /* ... */
  // #[debug_result] results can't be matched on and can't be assigned to a
  // variable, they only can be used together with `?`, which would create
  // an asymetry in the type system, but could otherwise work, althrough
  // usage would be extremely restricted.
  x()?;
  // e.g. the following would fail to compile because capturing the result
  // is not allowed
  if let Err(z) = x() {
    // ...
  }
}

@zserik它实际上有可能采用这样的形式吗?

#[cfg(panic_on_err)]
fn x() -> Result<(), !> {
  let _: () = x_inner().unwrap();
}

#[cfg(not(panic_on_err))]
fn x() -> Result<(), XError> {
  x_innner()
}

OK好主意。

我不确定这是否是在稳定之前需要考虑的事情,但我对实现错误返回跟踪感兴趣,我认为它涉及对Try trait 或至少其提供的 impl 的更改Result让它工作。 最终我计划把它变成一个 RFC,但我想确保 try 特性没有以一种使以后无法添加的方式稳定下来,因为我需要一段时间来编写所说的 RFC。 基本思路是这样的。

您有一个特性,用于将跟踪信息传递到其中使用专业化和 T 的默认 impl 以保持向后兼容性

pub trait Track {
    fn track(&mut self, location: &'static Location<'static>);
}

default impl<T> Track for T {
    fn track(&mut self, _: &'static Location<'static>) {}
}

然后你修改Try为实现Result使用track_caller并将此信息传递到内型,

    #[track_caller]
    fn from_error(mut v: Self::Error) -> Self {
        v.track(Location::caller());
        Self::Err(v)
    }

然后对于您想要为您收集回溯的类型实施 Track

#[derive(Debug, Default)]
pub struct ReturnTrace {
    frames: Vec<&'static Location<'static>>,
}

impl Track for ReturnTrace {
    fn track(&mut self, location: &'static Location<'static>) {
        self.frames.push(location);
    }
}

用法最终看起来像这样

#![feature(try_blocks)]
use error_return_trace::{MyResult, ReturnTrace};

fn main() {
    let trace = match one() {
        MyResult::Ok(()) => unreachable!(),
        MyResult::Err(trace) => trace,
    };

    println!("{:?}", trace);
}

fn one() -> MyResult<(), ReturnTrace> {
    try { two()? }
}

fn two() -> MyResult<(), ReturnTrace> {
    MyResult::Err(ReturnTrace::default())?
}

一个非常糟糕的回溯版本的输出看起来像这样

ReturnTrace { frames: [Location { file: "examples/usage.rs", line: 18, col: 42 }, Location { file: "examples/usage.rs", line: 14, col: 16 }] }

这是它在行动中的概念证明https://github.com/yaahc/error-return-traces

我认为只有一种我们可以转换为try trait 实现者的错误类型可能是不够的,所以,我们可以提供这样的接口:

trait Try{
     type Error;
     type Ok;
     fn into_result(self)->Result<Self::Ok,Self::Error>;
     fn from_ok(val: Self::Ok)->Self;
     fn from_error<T>(val: T)->Self;
}

注意,编译器可以单模化from_error ,避免From::from调用,并且可以手动为不同的错误类型提供方法实现,从而导致?运算符“解包”不同类型的错误。

 fn from_error<T>(val: T)->Self;

正如所写的那样,实现者必须接受 _any_ 大小的T ,不受约束。 如果您打算让它成为自定义约束,则它必须是一个特征参数,例如Try<T> 。 这类似于@scottmcmhttps://github.com/rust-lang/rust/issues/42327#issuecomment-457353299 中TryBlock / Bubble<Other>组合。

正如所写,实现者必须接受 _any_ 大小的T ,不受约束。 如果您打算让它成为自定义约束,则它必须是一个特征参数,例如Try<T> 。 这类似于@scottmcm#42327 (comment) 中TryBlock / Bubble<Other>组合。

我的意思是用法应该是这样的:

trait Try{
     //same from above
}
struct Dummy {
    a: u8,
}
struct Err1();
struct Err2();
impl Try for Dummy {
    type Ok=();
    type Error=();

    fn into_result(self)->Result<Self::Ok,Self::Error>{
        std::result::Result::Ok(())
    }
    fn from_ok(val: Self::Ok)->Self{
        Self{a: 0u8}
    }
    fn from_error<T>(val: Err1)->Self where T == Err1{
        Self{a: 1u8}
    }
    fn from_error<T>(val: Err2)->Self where T == Err2{
        Self{a: 2u8}
    }
}

您需要拆分 Try 和 TryFromError。 我确实比最初的提案更喜欢这个,但我认为它需要一个新的 RFC。

(我仍然认为它应该被称为“传播”而不是“尝试”,但我离题了)

@tema3210我想我理解你的意图,但这不是有效的 Rust。

@SoniEx2

您需要拆分 Try 和 TryFromError。

对,这就是我提到TryBlock / Bubble<Other>设计的原因。 我们可以讨论这个命名选择,但我们的想法是提前返回并不总是关于_错误_本身。 例如,很多内部Iterator方法都使用自定义的LoopState类型。 对于像find这样的东西,找到你要找的东西不是错误,但我们想在那里停止迭代。

https://github.com/rust-lang/rust/blob/3360cc3a0ea33c84d0b0b1163107b1c1acbf2a69/src/libcore/iter/mod.rs#L375 -L378

我们可以对命名选择进行辩论,但我们的想法是,提前返回本身并不总是与错误有关。

正是为什么我不喜欢“try”这个名字,而更喜欢“propagate”这个名字,因为它“传播”了一个“早期”的回报:p

(不确定这是否有意义?上次我提出这个“传播”​​的事情只是出于某种原因对我有意义,我从来没有向其他人解释过。)

当尝试覆盖默认的?行为 fe 以添加日志挂钩 fe 以记录调试信息(如文件/行号)时,此特征是否有帮助?

目前支持覆盖 stdlib 宏,但似乎?运算符没有明确转换为try!宏。 那真不幸。

@stevenroose为了仅对Try特性添加支持,需要修改Try特性以包含有关? “发生”位置的文件位置信息.

@stevenroose为了仅对Try特性添加支持,需要修改Try特性以包含有关? “发生”位置的文件位置信息.

这不是真的,即使特征定义不包含注释,#[track_caller] 也可以用于特征实现

@stevenroose回答您的问题,是的,但是如果您有兴趣打印错误传播的所有?位置,您应该查看上面的错误返回跟踪注释

https://github.com/rust-lang/rust/issues/42327#issuecomment -619218371

我不确定是否有人已经提到过这一点,我们应该impl Try for bool ,也许Try<Ok=(), Error=FalseError>
这样我们就可以做这样的事情

fn check_key(v: Vec<A>, key: usize) -> bool {
  let x = v.get_mut(key)?; // Option
  x.is_valid()?; // bool
  x.transform()?; // Result
  true
}

现在我必须在大多数情况下使用返回类型Option<()> ,我认为?可以使代码更加清晰。

我不确定是否有人已经提到过这一点,我们应该impl Try for bool ,也许Try<Ok=(), Error=FalseError>

这将使Trybool上表现为&&运算符。 正如其他人在上面指出的那样,也有成功短路的用例,在这种情况下, Try应该表现为|| 。 由于&&||操作符不需要输入太多,我也看不出有这个实现有什么好处。

@calebsander感谢您的友好回复。
对于一些简单的情况确实如此,但我认为情况并非如此,尤其是我们永远不会在表达式中使用let x = v.get_mut(key)?这样的赋值语句。
如果&&||总能奏效,我们也可以使用.unwrap_or_else().and_then()来表示OptionError案例。
你能在&&||表达流动的代码吗?

fn check_key(v: Vec<A>, key: usize) -> bool {
  let x = v.get_mut(key)?; // Option
  x.not_valid().not()?; // bool
  for i in x.iter() {
    if i == 1 { return true }
    if i == 2 { return false }
    debug!("get {}" i);
  }
  let y = x.transform()?; // Result
  y == 1
}

对于true表示失败而false表示成功的某些情况, !expr?可能会令人困惑,但我们可以使用expr.not()?来解决这个问题(注意:对于ops::Try ,我们总是有false用于Error )

对于一些简单的情况确实如此,但我认为情况并非如此,尤其是我们永远不会在表达式中使用let x = v.get_mut(key)?这样的赋值语句。

那么,除非我误解你的建议,只是实现Try<Ok = (), Error = FalseError>bool将不允许这样做。 您还需要impl From<NoneError> for FalseError以便?运算符可以将None转换为false 。 (同样,如果你想申请?Result<T, E>内部函数,返回bool ,则需要impl From<E> for FalseError 。我觉得这样的一揽子实现会有问题。)如果您对返回值Result<bool, bool>没问题,您也可以只使用some_option().ok_or(false)?some_result().map_err(|_| false)? Result<bool, bool> (您可以使用.unwrap_or_else(|err| err)折叠)

撇开将其他Try错误转换为bool不谈,您为bool提议的Try实现只是&&运算符。 例如,这些是等价的

fn using_try() -> bool {
    some_bool()?;
    some_bool()?;
    some_bool()
}

fn using_and() -> bool {
    some_bool() &&
    some_bool() &&
    some_bool()
}

(诚​​然,像ifloop这样返回非()的控制流不太容易翻译。)

对于true表示失败而false表示成功的某些情况, !expr?可能会令人困惑,但我们可以使用expr.not()?来解决这个问题(注意:对于ops::Try ,我们总是有false用于Error )

我不清楚false应该代表Error情况。 (例如, Iterator.all()会希望false短路,但Iterator::any()会希望true代替。)正如您所指出的,您可以得到通过反转传递给?的值(并反转函数的返回值)来相反的短路行为。 但我认为这不会产生非常可读的代码。 单独的struct s And(bool)Or(bool)实现两种不同的行为可能更有意义。

同样,如果你想申请? 结果在返回 bool 的函数中,您需要 impl From对于 FalseError。 我认为这样一揽子实施将是有问题的。

不,我不想impl From<T> for FalseError ,也许我们可以做result.ok()?

我不清楚 false 应该代表 Error 情况。

我认为当我们有bool::thentrue映射到Some时,这很自然。

如果我错过了,请原谅我 - 为什么不NoneError impl std::error::Error ? 这使得它对任何返回Box<dyn Error>或类似的函数都没用。

如果我错过了,请原谅我 - 为什么不NoneError impl std::error::Error ? 这使得它对任何返回Box<dyn Error>或类似的函数都没用。

不是这里的专家,但我可以看到试图提出有用的错误消息的重大问题“某物是None而你期望它是Some ”(这基本上就是你会在这里获得 - 一些诊断信息)。 根据我的经验,使用Option::ok_or_else组合器来使用另一种错误类型总是最有意义的,因为作为调用代码,我通常有更好的上下文可以提供。

我同意@ErichDonGubler。 无法将?Option一起使用是非常烦人的,但是这是有充分理由的,而且,作为语言用户,我认为正确处理错误符合每个人的最大利益。 做这个将错误上下文绑定到None而不是仅仅做?额外工作真的很烦人,但它强制正确传达错误,这是非常值得的。

我允许None被解释为错误的唯一一次是在非常粗略的原型设计中。 我想,如果我们添加类似Option::todo_err()调用,那将返回Ok的内部值,但会在None上发生恐慌。 在您分析代码创作的“粗略原型”模式的需求之前,这是非常违反直觉的。 它与Ok(my_option.unwrap())非常相似,但不需要Ok(...)包装。 此外,它明确指定了“todo”性质,从而传达了从生产逻辑中删除此代码的意图,用适当的错误绑定替换它。

但会对 None 感到恐慌

强烈认为我们应该把它留给unwrap 。 如果我们添加一个 todo_err 它应该返回一个实际的错误类型,这就引出了返回什么错误类型的问题。

此外,我怀疑fn todo_err(self) -> Result<Self, !>会存在需要!稳定的明显问题,呃,好吧,总有一天。

我的用例确实是粗略的原型设计,我不太关心错误。 难道整个NoneError受到您列出的这些问题的困扰吗? 如果决定它应该存在(我认为这是一件好事,至少对于原型设计),我相信它应该 impl Error因为它被命名为一个。

由于缺少这个 impl 的类型,我只好在它上面加上一个.ok_or("error msg") ,这也有效,但不太方便。

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

behnam picture behnam  ·  3评论

zhendongsu picture zhendongsu  ·  3评论

SharplEr picture SharplEr  ·  3评论

Robbepop picture Robbepop  ·  3评论

nikomatsakis picture nikomatsakis  ·  3评论