Rust: RFC 1937 的跟踪问题:`?` in `main`

创建于 2017-07-18  ·  183评论  ·  资料来源: rust-lang/rust

这是 RFC “ ? in main ” (rust-lang/rfcs#1937) 的跟踪问题。

脚步:

稳定:

  • [x] 使用非 () 返回类型稳定main (https://github.com/rust-lang/rust/issues/48453) 合并在https://github.com/rust-lang/锈/拉/49162
  • [x] 使用非 () 返回类型稳定单元测试 (https://github.com/rust-lang/rust/issues/48854)

相关问题:

  • [x] 单元测试的错误信息不是很好 (https://github.com/rust-lang/rust/issues/50291)

未解决的问题:

B-RFC-approved C-tracking-issue E-mentor T-compiler T-lang WG-compiler-middle

最有用的评论

很抱歉在可能为时已晚的时候插话,但我想在这里留下我的反馈,以防万一。 我确实阅读了这个线程的大部分内容,所以我在说话时会考虑到这种情况。 但是,这个线程很长,所以如果看起来我忽略了某些东西,那么我可能已经忽略了,我会很感激有人向我指出。 :-)

TL;DR - 我认为显示错误的Debug消息是一个错误,更好的选择是使用错误的Display消息。

我的核心信念是,作为一个经常在 Rust 中构建 CLI 程序的人,我不记得很关心 $ ErrorDebug消息是什么。 也就是说,按照设计,错误的Debug是针对开发人员的,而不是针对最终用户的。 当你构建一个 CLI 程序时,它的界面基本上是供最终用户阅读的,因此Debug消息在这里几乎没有用处。 也就是说,如果我交付给最终用户的任何 CLI 程序在正常操作中显示了 Rust 值的调试表示,那么我会考虑修复一个错误。 我通常认为这应该适用于每个用 Rust 编写的 CLI 程序,尽管我理解这可能是理性的人可能不同意的一点。 话虽如此,我的观点有些令人吃惊的暗示是,我们已经有效地稳定了一个功能,其中它的默认操作模式让你从一个应该修复的错误(再次,IMO)开始。

通过默认显示Debug表示错误,我还认为我们鼓励不良做法。 特别是在编写 Rust CLI 程序的过程中,经常会观察到即使是错误的Display impl 也不足以被最终用户消费,必须做一些实际的工作修理它。 一个具体的例子是io::Error 。 显示没有相应文件路径的io::Error (假设它来自读取/写入/打开/创建文件)基本上是一个错误,因为最终用户很难用它做任何事情。 通过选择默认显示错误的Debug表示,我们让创建 CLI 程序的人更难发现这些错误。 (最重要的是, Debugio::Error远不如Display有用,但这本身并不是我的经验中的一个巨大痛点。 )

最后,为了完善我的论点,我也很难想象即使在示例中我也会使用?-in-main的情况。 也就是说,我一直在尝试编写与现实世界程序尽可能匹配的示例,这通常涉及编写如下内容:

use std::error::Error;
use std::process;

fn try_main() -> Result<(), Box<Error>> {
    // do stuff with `?`
}

fn main() {
    if let Err(err) = try_main() {
        eprintln!("{}", err);
        process::exit(1);
    }
}

从表面上看,用?-in-main替换它会是 _lovely_ ,但我不能,因为它不会显示Display的错误。 也就是说,在写一个真正的CLI程序时,我其实会使用上面的方法,所以如果我想让我的例子反映现实,那么我认为我应该展示我在实际程序中所做的事情,而不是走捷径(在合理的范围内) )。 我实际上认为这类事情真的很重要,从历史上看,它的一个副作用是它向人们展示了如何编写惯用的 Rust 代码,而不会到处乱扔unwrap 。 但是,如果我在示例中恢复使用?-in-main ,那么我就违背了我的目标:我现在正在设置那些可能不知道更好的人来编写默认情况下发出非常无用的错误消息。

“发出错误消息并以适当的错误代码退出”的模式实际上用于抛光程序中。 例如,如果?-in-main使用Display ,那么我今天可以在 ripgrep 中合并mainrun函数:

https://github.com/BurntSushi/ripgrep/blob/64317bda9f497d66bbeffa71ae6328601167a5bd/src/main.rs#L56 -L86

当然,一旦稳定下来,我可以通过为Termination trait 提供我自己的 impl 来使用?-in-main ,但是如果我可以写出main ,我为什么还要费心去做呢? impl以使它们与现实相匹配,在这一点上,我不妨坚持我今天的示例(使用maintry_main )。

从外观上看,解决这个问题似乎是一个突破性的变化。 也就是说,这段代码在今天的 Rust 稳定版上编译:

#[derive(Debug)]
struct OnlyDebug;

fn main() -> Result<(), OnlyDebug> {
    Err(OnlyDebug)
}

我认为切换到Display会破坏这段代码。 但我不确定! 如果这真的是一个船已经航行的问题,那么我理解并且没有太多用在详细说明这一点,但我确实对此感到足够强烈,至少可以说点什么,看看我是否不能说服别人看看如果有什么可以解决的。 (我也很可能在这里反应过度,但到目前为止,我已经有几个人在我的 CSV 示例中问我“你为什么不使用?-in-main ?”,而我的回答基本上是, “我看不出这样做是怎么可行的。”也许这不是?-in-main打算解决的问题,但有些人肯定有这种印象。随着它目前的实施,我可以看到它在文档测试和单元测试中很有用,但我很难想到我会使用它的其他情况。)

所有183条评论

如何处理退出状态?

@Screwtapello这条评论似乎太接近 FCP 的结尾,以至于无法对 RFC 进行任何更改以响应它。

简而言之: RFC 建议在失败时返回 2,其理由虽然有充分根据,但晦涩难懂并会产生稍微不寻常的结果; 最不令人惊讶的是,当程序没有任何迹象表明它需要更多细节而不仅仅是成功或失败时,它会返回 1。 这是否足以让我们在讨论它时不会觉得我们在扭曲 RFC 流程,或者我们现在是否被锁定在这个特定的实现细节中?

但这不是实现细节,是吗?

一些脚本使用退出代码作为从子进程获取信息的一种方式。

这特别是关于子流程(在 Rust 中实现)除了二进制“一切都很好”/“出了点问题”之外没有信息可提供的情况。

一些脚本使用退出代码作为从子进程获取信息的一种方式。

这种行为总是极其依赖于被调用的程序_except_,因为非零意味着失败。 鉴于std::process::exit带有 main-function 包装器和查找表对于那些想要更清晰的退出状态的人来说仍然是最佳选择,无论做什么,这似乎是一个微不足道的细节。

不过,我不认为 SemVer 有“大部分无关紧要的细节”例外。

我认为退出代码应该添加到未解决的问题列表中。 @zackw还打开了一个相关的内部线程

许多人同意退出代码在失败时应该是1 (而不是2 ):
https://www.reddit.com/r/rust/comments/6nxg6t/the_rfc_using_in_main_just_got_merged/

@arielb1你打算实现这个 rfc 吗?

@bkchr

不,只是为了指导它。 我分配了所以我不会忘记写指导笔记。

啊,很好,我有兴趣这样做:)
但是,我不知道从哪里开始:D

@bkchr

这就是我在这里的原因:-)。 我应该尽快写下指导说明。

好的,那我等你的指示。

指导说明

这是一个 [WG-compiler-middle] 问题。 如果你想寻求帮助,你可以加入 irc.mozilla.org 上的#rustc(我是 arielby)或https://gitter.im/rust-impl-period/WG-compiler-middle (我是@arielb1那里)。

#44505 有一个 WIP 编译器自述文件 - 它描述了编译器中的一些内容。

本 RFC 的工作计划:

  • [ ] - 将Termination语言项添加到 libcore
  • [ ] - 允许在main中使用Termination #$
  • [ ] - 允许在文档测试中使用Termination
  • [ ] - 允许在#[test]中使用Termination #$

Termination语言项添加到 libcore

首先,您需要将Termination特征添加到libcore/ops/termination.rs以及一些文档。 您还需要使用#[unstable(feature = "termination_trait", issue = "0")]属性将其标记为不稳定 - 这将阻止人们在它稳定之前使用它。

然后,您需要将其标记为src/librustc/middle/lang_items.rs中的语言项。 这意味着编译器可以在类型检查main时找到它(例如,参见 0c3ac648f85cca1e8dd89dfff727a422bc1897a6)。
这意味着:

  1. 将其添加到 lang-items 列表(在librustc/middle/lang_items.rs中)
  2. #[cfg_attr(not(stage0), lang = "termination")]添加到Termination特征。 您不能只添加#[lang = "termination"]属性的原因是因为“stage0”编译器(在引导期间)不会知道termination是存在的东西,所以它不能编译 libstd。 当我们更新 stage0 编译器时,我们将手动删除cfg_attr
    有关详细信息,请参阅 XXX 的引导文档。

允许在 main 中使用Termination

这是我知道如何处理的有趣部分。 这意味着制作一个返回() main进行类型检查(当前您收到main function has wrong type错误)并且可以工作。

要进行类型检查,您首先需要删除以下现有错误:
https://github.com/rust-lang/rust/blob/9a00f3cc306f2f79bfbd54f1986d8ca7a74f6661/src/librustc_typeck/lib.rs#L171 -L218

然后,您需要检查返回类型是否实现了Termination特征(您使用register_predicate_obligation添加特征义务 - 搜索其用途)。 这可以在这里完成:
https://github.com/rust-lang/rust/blob/9a00f3cc306f2f79bfbd54f1986d8ca7a74f6661/src/librustc_typeck/check/mod.rs#L1100 -L1108

另一部分是让它工作。 那应该很容易。 正如 RFC 所说,您希望lang_start在返回类型上具有通用性。

lang_start目前在这里定义:
https://github.com/rust-lang/rust/blob/9a00f3cc306f2f79bfbd54f1986d8ca7a74f6661/src/libstd/rt.rs#L32

因此,您需要将其更改为通用并匹配 RFC:

#[lang = "start"]
fn lang_start<T: Termination>
    (main: fn() -> T, argc: isize, argv: *const *const u8) -> !
{
    use panic;
    use sys;
    use sys_common;
    use sys_common::thread_info;
    use thread::Thread;

    sys::init();

    sys::process::exit(unsafe {
        let main_guard = sys::thread::guard::init();
        sys::stack_overflow::init();

        // Next, set up the current Thread with the guard information we just
        // created. Note that this isn't necessary in general for new threads,
        // but we just do this to name the main thread and to give it correct
        // info about the stack bounds.
        let thread = Thread::new(Some("main".to_owned()));
        thread_info::set(main_guard, thread);

        // Store our args if necessary in a squirreled away location
        sys::args::init(argc, argv);

        // Let's run some code!
        let exitcode = panic::catch_unwind(|| main().report())
            .unwrap_or(101);

        sys_common::cleanup();
        exitcode
    });
}

然后你需要从create_entry_fn调用它。 目前,它使用Instance::mono实例化单态lang_start $ ,您需要将其更改为使用monomorphize::resolve和正确的替代品。

https://github.com/rust-lang/rust/blob/9a00f3cc306f2f79bfbd54f1986d8ca7a74f6661/src/librustc_trans/base.rs#L697

允许在 doctests 中使用Termination

我真的不明白 doctests 是如何工作的。 也许问@alexcrichton (这就是我会做的)?

允许在#[test]中使用Termination #$

我真的不明白 libtest 是如何工作的。 也许问@alexcrichton (这就是我会做的)? 单元测试基本上是由宏生成的,因此您需要更改该宏或其调用者,以处理不是()的返回类型。

@bkchr

你至少可以加入 IRC/gitter 吗?

@bkchr刚刚签到——我看到你和@arielb1 前段时间在 gitter 上交谈,有什么进展吗? 在某处吸吮?

不抱歉,到目前为止没有进展。 目前我有很多事情要做,但我希望这周我能找到一些时间来做这件事。

@bkchr如果您需要帮助,请告诉我!

我目前有点卡住,我想创建义务。 要创建义务,我需要一个 TraifRef,对于 TraitRef,我需要一个 DefId。 有人可以指出一些关于如何从 Termination Trait 创建 DefId 的代码吗?

@bkchr特性应该添加到 lang 项目列表中,例如: https ://github.com/rust-lang/rust/blob/ade0b01ebf18550e41d24c6e36f91afaccd7f389/src/librustc/middle/lang_items.rs#L312
并用#[termination_trait]标记,例如: https ://github.com/rust-lang/rust/blob/ade0b01ebf18550e41d24c6e36f91afaccd7f389/src/libcore/fmt/mod.rs#L525 -L526

是的,这不是问题,我已经这样做了。 我需要检查 check_fn 函数中的终止特征。 我想使用 register_predicate_obligation ,为此我需要终止特征的定义。

哦,那么你只需要tcx.require_lang_item(TerminationTraitLangItem)

@bkchr怎么样? 只是再次检查。 =) 如果您很忙,请不要担心,只是想确保您获得所需的所有帮助。

抱歉,现在很忙 :/ 到目前为止,我得到了所有需要的帮助 :)

这是检查 TerminationTrait 的代码: https ://github.com/bkchr/rust/blob/f185e355d8970c3350269ddbc6dfe3b8f678dc44/src/librustc_typeck/check/mod.rs#L1108

我认为我没有检查函数的返回类型? 我收到以下错误:

error[E0277]: the trait bound `Self: std::ops::Termination` is not satisfied
  --> src/rustc/rustc.rs:15:11
   |
15 | fn main() { rustc_driver::main() }
   |           ^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::ops::Termination` is not implemented for `Self`
   |
   = help: consider adding a `where Self: std::ops::Termination` bound

我需要更改什么来检查函数的返回类型?

@bkchr我建议在 https://gitter.im/rust-impl-period/WG-compiler-middle 加入编译器中间工作组 gitter 以获得反馈,并在 https 尝试 #rust-internals IRC 频道://chat.mibbit.com/?server=irc.mozilla.org%3A%2B6697&channel=%23rust-internals 。 :)

@bstrie是的,谢谢,我已经是 gitter 聊天的一部分,可以解决我的问题。 :)

@bkchr你的问题在这条线上。 您要在那里构建的特征引用类似于R: Termination ,其中R是函数的返回类型。 这是通过建立一个适当的“substs”来指定的,它是替换特征类型参数的一组值(在本例中Self )。

但是,您正在对特征调用Substs::identity_for_item方法。 这将为您返回在特征定义本身中使用的替换。 即,在这种情况下,您将在Termination特征上声明的Self参数映射到Self 。 如果您正在检查Terminator特征中某些函数的定义,这将是合适的,但在这里不是很多。

相反,您想要的是获取入口函数的返回类型。 这只是变量ret_tyactual_ret_ty之一。 两者都可以,但我猜ret_ty更好——它对应于用户声明的返回类型(而actual_ret_ty是实际代码返回的类型)。

你可以通过调用 tcx 中的mk_substs()方法来制作你想要的替代品。 在这种情况下,只有一个参数,类型,所以我认为像let substs = fcx.tcx.mk_substs(&[ret_ty]);这样的东西会起作用。

我相信要使用的是tcx.mk_substs_trait(ret_ty, &[])

@bkchr刚刚签到——有机会使用该建议吗? (此外,为了更快的响应,在 gitter 上询问可能是明智的。)

是的,我可以用 gitter 解决问题 :)

@bkchr怎么样? 刚入住。

一切都好,这周我可能会抽出一些时间来研究代码。

是否有更多的人来帮助解决这个问题? 我想在年底前开始为 Rust 社区做贡献,并愿意为这个功能提供帮助。 希望两个人在这方面合作不会太令人困惑。

@U007D

这是一个小功能, @bkchr几乎完成了它。

啊,好的——很高兴知道,谢谢。 我会留意其他我可以提供帮助的事情。

@lnicola是的,我有! 我正试图在我对能够从事的工作充满信心(即,成为一个净积极的人)并且我对此充满热情的事情的交叉点上找到一些东西。 老实说,尽管我已经学习了大约一年的 Rust,但站出来自愿做某事仍然有点吓人。 FWIW,这绝不是 Rust 社区的错——Rust 社区已经竭尽全力使其成为一种开放、热情和包容的文化——这是我有幸体验到的最好的。 (我怀疑这更多地与技术行业多年经验的旧战斗伤疤有关,在这个行业中,团队倾向于竞争而不是协作。)

不管怎样,我今年的目标是挑选一些东西,至少开始做出积极的贡献。 是我参与的时候了! :)

感谢您的建议,@lnicola。 这是一个很好的资源。

@bkchr有什么更新吗?

我在上面(https://github.com/rust-lang/rust/pull/46479)。 现在我有假期和时间在拉取请求中的评论中工作。 抱歉所有延误:/

哦,对不起,没有注意到你有一个拉取请求。 交联它。

你好,嗯。 所以我想我会像传统一样,通过骑自行车来开始我潜在的 Rust 贡献者职业生涯。 具体来说,关于这个:

  • [ ] 被引入的特征的名称

Exit怎么样? 它简明扼要,适合现有的 Rust 词汇。 Exit-as-a-noun 是 exit-as-a-verb 的自然对应物,对于大多数人来说,这是一个熟悉的词,用于以受控方式“从内部”结束一个过程。

特别是对于 C++ 程序员来说,“终止”会让人想起std::terminate ,它默认为异常终止(调用abort )并且基本上是 C++ 等价于恐慌(但与恐慌不同,它永远不会解除堆)。

等等,忽略那条评论,看起来像 RFC 明确地开放讨论。

我确实喜欢将Exit作为特征名称。

我认为该功能会在特征出现之前就稳定下来,就像Carrier发生的那样。

FWIW 这是另一种情况,我很高兴临时名称在稳定之前被更改:D

作为 RFC 的作者,我不反对将特征名称更改为Exit或其他任何东西。 我不是特别擅长命名事物,并且很高兴别人有更好的主意。

https://github.com/rust-lang/rust/blob/5f7aeaf6e2b90e247a2d194d7bc0b642b287fc16/src/libstd/lib.rs#L507

特征应该是

  1. 放置在 libstd 而不是 libcore 中,并且
  2. 只是叫std::Termination ,而不是std::ops::Termination

无法将特征放入libcore中,因为Result的实现需要打印到stderr并且不能在libcore中完成。

@bkchr impl 在 libstd 中并不意味着特征也需要在 libstd 中。

@kennytm我知道,但是 libcore 中也定义了 Result,因此无法在 libstd 中为 Result 实现终止。

@zackw +1 投票支持Exit作为特征名称。

@U007D :您能否使用反应按钮(例如,👍)而不是发布这样的消息? 这可以让您通过不必要地 ping 他们来避免烦人的问题订阅者。

如果激活了language_feature (在板条箱中),我可以签入libtest / libsyntax吗? @arielb1 @nikomatsakis @alexcrichton

@bkchr在 libsyntax 中,您可能必须将其传递,但​​理论上这是可能的,但在运行时的 libtest 本身中,我不相信您可以检查。

@bkchr这里怎么样?

我仍在努力,但目前我没有更多问题:)

我认为这个 impl 太严格了:

#[unstable(feature = "termination_trait", issue = "43301")]
impl<T: Termination, E: Error> Termination for Result<T, E> {
    fn report(self) -> i32 {
        match self {
            Ok(val) => val.report(),
            Err(err) => {
                print_error(err);
                exit::FAILURE
            }
        }
    }
}


#[unstable(feature = "termination_trait", issue = "43301")]
fn print_error<E: Error>(err: E) {
    eprintln!("Error: {}", err.description());

    if let Some(ref err) = err.cause() {
        eprintln!("Caused by: {}", err.description());
    }
}

有几个常用的错误没有实现Error ,最重要的是Box<::std::error::Error>failure::Error 。 我也认为使用description方法而不是这个错误的显示 impl 是错误的。

我建议用这个更广泛的 impl 替换这个 impl:

#[unstable(feature = "termination_trait", issue = "43301")]
impl<T: Termination, E: Display> Termination for Result<T, E> {
    fn report(self) -> i32 {
        match self {
            Ok(val) => val.report(),
            Err(err) => {
                eprintln!("Error: {}", err)
                exit::FAILURE
            }
        }
    }
}

这确实失去了令人遗憾的原因链。

使用 display impl 而不是 description 绝对是更好的做法。

原因链是一个有趣的问题。 特别是,这个实现只打印原因链的前两个成员。

失败必须处理如何处理原因链并默认解决此行为(例如,如果您只是使用.context建立错误:

  • {}仅打印此错误
  • {:?}打印此错误及其原因(递归)

我们可以决定在这里使用:?并将其绑定为 Debug 而不是 Display。 不确定。

是的,我已经知道我需要改进 impl 来支持。 我对我们能做的事情持开放态度。 绑定到Debug可能是个好主意。

嗯,这是一个棘手的问题。 我想这取决于我们是否认为“抛光”程序会利用这个特征 impl。 我倾向于认为可以说“不,他们不会”——基本上,一个完善的程序要么(a)捕获输出并以其他方式处理它,要么(b)使用一些新类型或实现调试的东西正确的方式。 这意味着我们可以优化 impl 以转储有用的信息,但必须以最漂亮的形式(这似乎是Debug的角色)。

使用Debug明确地针对原型设计可能是正确的选择,因为我认为我们永远无法以对大多数生产用例正确的方式自动处理错误。

@withoutboats我同意。

@nikomatsakis我假设您的意思是“不一定是最漂亮的形式”? 如果是这样,是的,我同意。

更新:在为此工作了几天后,我对此进行了翻转。 见下文。

:+1: 在Debug上,在这里; 我喜欢来自https://github.com/rust-lang/rfcs/pull/1937#issuecomment -284509933 的@nikomatsakis的“类似于未捕获的异常”。 Diggsey 的评论还建议Debughttps ://github.com/rust-lang/rfcs/pull/1937#issuecomment -289248751

仅供参考,我已经打开了“更完整”与“更用户友好”的默认值(即DebugDisplay特征绑定)问题。

TL; DR 我现在认为我们应该将界限设置为Display (根据@withoutboats原始帖子),以便在“什么都不做”的情况下提供更清晰的摘要输出。

这是我的理由:

termination trait 的 RFC 问题上, @zackw提出了令人信服的观点,即 Rust 具有双重panic / Result系统,因为panic用于错误和Result用于错误。 由此,我认为可以提出一个令人信服的案例来独立于默认的恐慌呈现来评估默认的错误呈现。

当然,没有一个默认值可以让所有人都满意,所以应用最小惊喜的原则,我问自己哪个默认值更合适?

  • 设计通常不会处理错误,因为它旨在向用户传达某些(可能是可修复的)出错(未找到文件等)的信息。 因此,该用例存在并且可以合理地被认为是常见的,即用户是目标受众。

  • 正如@nikomatsakis指出的那样,无论我们选择什么默认值,任何希望更改行为的开发人员都可以使用 newtype 模式或在 main() 中开发自定义实现。

最后,在更主观的方面,在过去几天使用此功能时,我发现Debug输出让我觉得我的“Rust 应用程序”感觉更粗糙

$ foo
Error: Custom { kind: Other, error: StringError("returned Box<Error> from main()") }
$

对比

$ foo
Error: returned Box<Error> from main()
$

Dispay trait 似乎是一个更加文明的默认错误(而不是错误),不是吗?

@U007D等等,你更喜欢这两个输出中的哪一个?

(一) Error: Custom { kind: Other, error: StringError("returned Box<Error> from main()") }

要么

(b) Error: returned Box<Error> from main()

他更喜欢选项(b)。

@nikomatsakis最初,我对 a) Debug作为我脑海中的一个概念很好,但是在使用它几天后实际看到了输出,我现在更喜欢 b) Display作为默认。 我认为如果我对链式错误进行建模,我对 b) 的偏好会变得更加明显。

不过,我不认为“抛光”或“文明”是这样做的目标,因为我理解该线程已经接受这主要是作为示例,随着程序的成熟,人们完全希望添加自定义处理。

在这些情况下,对我来说“最不意外”的是面向开发人员的输出,就像unwrap一样。

如果担心长错误,是否值得在这里讨论{:#?}

每个工具和每个用例的最终用户错误报告都会有所不同,但开发人员的错误报告应该类似于 Rust 在其他情况下所做的,例如.unwrap() 。 由于只能有一个默认值,而且经过优化的软件无论如何都需要覆盖输出,所以我投票支持使用Debug来面向开发人员的默认值。

我认为这个讨论的核心实际上是“谁是默认消息的目标受众?”

假设我们都同意默认目标受众是开发人员。 我认为Debug默认绑定将是一个简单的选择。

现在让我们假设我们同意默认目标受众是用户,那么这就是我不同意其他一些人的地方,并认为像“抛光”和“文明”输出这样的主观品质确实有重要的一部分玩。 对于某些人来说,最终用户演示文稿“润色”可能是避免使用Display的最佳理由。 (我碰巧不同意这种观点,但我理解并尊重它。)

所以是的,我当然可以将任一组的合理论据视为默认目标。 我认为,如果围绕哪些受众应该成为默认目标达成强烈共识,那么特征界限的选择将是明确的(呃)...... :)

(我对整个主题并不完全精通,但是)如果它采用某种用户可呈现的格式,那么是否有一些小型实用程序的默认错误输出与Termination将是完全足够的像Display ? 在这种情况下,作者必须达到“自定义处理”的唯一原因是我们是否制作它们。

有人可以提供每种情况下输出的示例(我假设它还取决于所使用的特定E类型?),以及作者想要“自定义处理”时实际需要采取的步骤反而? 我只是在上面进行假设。

(输出看起来像上面粘贴的@U007D吗?为什么它会打印“returned Box\框<错误>?)

错误消息的Display多久对用户友好? 例如,以下程序:

fn main() {
    if let Err(e) = std::fs::File::open("foo") {
        println!("{}", e)
    }
}

发出以下消息:

No such file or directory (os error 2)

我想说,这不是很好的用户体验,尤其是没有提到文件名的情况下。 至少不会,除非程序从字面上将单个文件名作为输入。 另一方面,这也不是很好的开发人员体验,缺少源文件/行号/堆栈跟踪。 Debug输出显然是更糟糕的用户体验,并且也没有为减速器添加有用的信息。

所以我想我想说的是,在不改进库错误本身的信息内容的情况下, DebugDisplay都不是很好。

输出是否看起来像上面粘贴的@U007D ? 为什么会打印“returned Box”from main()" 而不是……那个 Box 的实际内容?

@glaebhoerl您是对的-在这种情况下,“ Box<Error>的实际内容”是我为测试termination_trait而创建的自定义message字段,逐字显示. 我本可以在那里写“foo bar baz”或其他任何东西(但这对于运行编译器测试的用户可能没有那么有用)。

使用@jdahlstrom的示例,这是Box ed 标准库“找不到文件” Error的实际输出(请注意,正如您正确指出的那样,在任何地方都没有提到拳击):
Debug

$ foo
Error { repr: Os { code: 2, message: "No such file or directory" } }
$

Display

$ foo
No such file or directory (os error 2)
$

@jdahlstrom我认为你说得很好。 我同意,虽然这两种信息都可能无法满足他们的目标受众,但我想强调的是,提供错误的信息会更糟(正如我认为你提到的那样):

向开发人员提供Display具有Debug的所有缺点,而且甚至会忽略显示的错误类型的特殊性。

向用户提供Debug具有Display的所有缺点,此外还增加了用户不需要并且可能无法理解的更多技术信息。

所以是的,我同意这些信息通常没有足够的针对性,对于任何一个观众。 我认为这突出了另一个重要原因,我们要清楚我们的目标是谁,以便我们为该群体提供我们所能提供的最佳体验(尽管存在缺陷)。

我需要一些帮助来实现对#[test] ?的支持。 我当前的实现可以在这里找到: https ://github.com/rust-lang/rust/compare/master...bkchr :termination_trait_in_tests

使用我的更改编译测试会导致以下错误:

error: use of unstable library feature 'test' (see issue #27812)
  |
  = help: add #![feature(test)] to the crate attributes to enable

@eddyb说我不应该再使用quote_item!/expr!了,因为它们是遗留的。
我现在应该怎么做,切换到新的quote!宏或将所有内容重新设计为手动 ast 构建?

我很感激任何帮助:)

我认为生成对libtest中定义的某些宏的宏调用可以很好地工作。

@eddyb我不确定我是否理解您的建议:

我认为生成对 libtest 中定义的某些宏的宏调用可以很好地工作。

哦,我想也许我会。 你是说——在 libtest 中定义一个宏,然后生成调用它的代码? 有趣的想法。 那个宏名称不会“泄漏”出来吗? (即,它成为 libtest 的公共接口的一部分?)


@bkchr

使用我的更改编译测试会导致以下错误:

你知道为什么会产生这个错误吗? 仅仅通过阅读差异,我没有,但我可以尝试在本地构建并弄清楚。

我不应该再使用quote_item!/expr! ,因为它们是遗留的。

我在这里没有强烈的意见。 我同意@eddyb ,它们是遗留物,但我不确定它们是否是一种遗留物,增加更多用途会使它们更难移除——即,一旦我们得到最近的替代品,会不会容易@eddyb从一个移动到另一个?

手动构建 AST 确实很痛苦,尽管我想我们有一些帮助。

大多数情况下,我们只需要进行微小的编辑,对吗? 即,从调用函数更改为测试report()的结果?

PS,我们可能想要生成类似Termination::report(...)的东西,而不是使用.report()表示法,以避免依赖Termination特性在范围内?

不,我不知道该错误来自哪里:(

RFC 提议生成一个调用原始测试函数的包装函数。 这也是我目前的做法。
我认为我们也可以删除包装函数,但是我们需要对函数指针进行装箱,因为每个测试函数都可以返回不同的类型。

嗯,由于测试已经导入了其他东西,所以也导入 Termination trait 并不复杂。

@alexcrichton你可能知道这个错误是从哪里来的吗?

@nikomatsakis libtest不稳定,即使不是,我们也不能将宏标记为不稳定吗?

@eddyb哦,好点。

RFC 提议生成一个调用原始测试函数的包装函数。 这也是我目前的做法。

包装函数对我来说似乎很好。

@eddyb与宏的意思是create_test之类的东西,在libtest中定义? 但我不明白的是“将宏标记为不稳定”。 你这样做的目的是什么? 你能给我举个例子吗?

@bkchr在宏定义上添加#[unstable(...)]属性,例如: https ://github.com/rust-lang/rust/blob/3a39b2aa5a68dd07aacab2106db3927f666a485a/src/libstd/thread/local.rs#L159 -L165

那么,如果第一个复选框...

实施 RFC

...现在检查链接的 PR 是否已合并?

@ErichDonGubler完成 :)

嗯,目前只有一半的rfc是用合并的pr实现的^^

分成3个复选框:)

我一直在尝试在main中使用此功能,但我发现它非常令人沮丧。 终止特征的现有实现不允许我方便地“累积”多种错误 - 例如,我不能使用failure::Fail ,因为它没有实现Error ; 我不能使用Box<Error> ,同样的原因。 我认为我们应该优先考虑更改为Debug 。 =)

嗨, @nikomatsakis

当我尝试使用termination_trait时,我感到与您完全相同的挫败感。

再加上你关于在编译器上进行黑客攻击的帖子,这启发了我在本月早些时候解决这个问题。 我已经在此处发布了Display的 impl(以及上一次提交中的Debug )以及测试: https://github.com/rust-lang/rust/pull/47544。 (这是非常小的,但仍然是我的第一个 Rust 编译器 PR!:tada:) :)

我的理解是 lang 团队将决定使用哪个 trait,但无论哪种方式,实现都已准备就绪。

我仍然感兴趣的一个问题:假设您不想依赖默认的错误消息输出(无论是Debug还是Display ),而是想要自己的,你怎么做那? (抱歉,如果这已经被写在某处而我错过了。)您不必完全停止使用? -in- main ,对吗? 这就像编写自己的结果和/或错误类型和/或impl ? (如果? -in- main只是一个玩具,这对我来说似乎很不幸,一旦你想“认真起来”,你就不得不恢复到不符合人体工程学的方式。)

@glaebhoerl这很简单:

  1. 创建一个新类型。
  2. 实施From您的旧错误类型。
  3. 实施Debug (或Display )。
  4. 替换main签名中的类型。

谢谢!

编写面向调试的Debug的自定义实现对我来说似乎有点奇怪,但我想这不是世界末日。

@glaebhoerl这就是为什么Result impl应该使用Display而不是Debug IMO。

Termination特征不能有一个名为messageerror_message或类似的额外方法,它有一个使用Debug / 的默认实现Display显示消息? 然后你只需要实现一个方法而不是创建一个新类型。

@glaebhoerl @Thomasdezeeuw类似于error_message方法的东西在 RFC 的原始草案中,但由于缺乏共识而被删除。 我当时的感觉是,最好让基本功能落地(不一定稳定)然后迭代。

@zackw同意,我个人可以不发送消息,只是一个数字,比如01错误代码。 但是,如果我们想在第一次迭代中获得消息,我想我会更支持Termination::message ,而不是DebugDisplay上的任何东西。

message方法会返回String吗? 这与在 libcore 中的Termination不兼容吗?

@SimonSapin Termination当前在libstd中定义。

嗯,但我认为向 trait 添加方法message并不是最好的实现。 对于像i32这样的类型,该方法会返回什么? 如何决定何时打印此消息? 当前 $ ResultTermination的实现,在report函数中打印错误。 这行得通,因为Result知道它是一个Err 。 我们可以在某处整合支票report() != 0然后打印,但感觉不对。
下一个问题是,我们想为Result提供标准实现,但是Error类型需要实现什么才能打印呢? 这将使我们回到当前的问题DebugDisplay

如果您想在“严重”程序的 main 中使用? ,另一个令人头疼的问题是,在某些情况下,命令行程序希望以非零状态退出但不打印 _anything_(考虑grep -q )。 因此,现在您需要一个Termination impl 来实现_isn't_ Error的东西,它什么都不打印,可以让您控制退出状态......并且您需要决定是否在解析命令行参数之后重新返回那个东西。

这就是我的想法:

返回Result<T, E>应该使用 E 的调试 impl。这是最方便的特性——它被广泛实现,并满足“快速和肮脏的输出”用例以及单元测试用例。 我宁愿不使用Display ,因为它实施得不太广泛,而且它给人的印象是这是经过优化的输出,我认为这不太可能。

但也应该有一种获得专业外观输出的方法。 幸运的是,已经有两种这样的方式。 首先,正如@withoutboats所说,如果人们想使用E: Display或以其他方式提供专业风格的输出,则可以制作“从调试显示”的桥梁。 但是,如果这感觉太 hacky,那么您也可以定义自己的类型来替换结果。 例如,您可能会这样做:

fn main() -> ProfessionalLookingResult {
    ...
}

然后为ProfessionalLookingResult实施Try #$ 。 然后你也可以实现Terminate ,无论如何:

impl Terminate for ProfessionalLookingResult {
    fn report(self) -> i32 {
        ...
        eprintln!("Something very professional here.");
        return 1;
        ...
    }
}

我同意@nikomatsakis这应该使用Debug

我还认为,对于完善的输出,在main中编写一些代码可能比创建一个新类型来实现TryTerminate更好。 在我看来, Terminate的意义在于,库可以轻松地为其设置一个很好的默认值,而对于最终用户(例如专业 CLI)程序如何终止的情况来说,情况并非如此.

当然其他人可能有不同的看法,并且有多种方法可以使用所涉及的特征来注入代码,而不是直接在main中编写代码。 很棒的是我们有多种选择,而且我们不必总是想出一种有福的方法来处理错误。

让我记下一些想法,尽管它们存在一些问题。

我很想看到以下内容

fn main() -> i32 {
    1
}

可以更一般地写成:

fn main() -> impl Display {
    1
}

这两个主要函数都应该返回一个 0 退出代码和println! Display of 1。

这应该像以下一样简单(我认为)。

impl<T> Termination for T where T: Display {
    fn report(self) -> i32 {
        println!("{}", self);
        EXIT_SUCCESS
    }
}

然后对于错误我们可以有:

impl<T: Termination, E: Debug> Termination for Result<T, E> { ... }

其中实现与 RFC 中的相同,只是使用"{:?}"
Debug格式。

如前所述,需要更多控制输出的人可以简单地
写:

fn main() -> Result<i32, MyError> { ... }
impl Termination for Result<i32, MyError> { ... }

尽管我猜这对于我们当前的编译器来说是无法确定的,因为它
会看到相互冲突的实现......所以我们按照@nikomatsakis 的建议去做
和写:

fn main() -> MyResult { ... }
impl Termination for MyResult { ... }
or, if you want something more general.
impl<T, E> Termination for MyResult<T, E> { ... }

我知道这部分是在重申已经说过的话,但我想我会完全展示我的愿景,展示一个更通用的解决方案来显示返回值,而不仅仅是结果。 似乎很多评论都在争论我们默认发布的Termination的实现。 此外,此评论与 RFC 中描述的impl Termination for bool之类的实现不一致。 我个人认为非零退出代码应该由Results或实现Termination的自定义类型专门处理。

这是一个有趣的问题,即如何在 main 中处理 $ Option类型的? ,因为它们没有Display的实现。

TL;DR:我对Debug没意见。

更详细的解释:
昨天和今天我花了一些时间思考这个问题,目的是揭示我已经或正在做出的隐含假设,以帮助得出最佳答案。

关键假设:在可以用 Rust 编写的所有各种类型的应用程序中,我认为控制台应用程序将受益最多/受此决定影响最大。 我的想法是,在编写库、AAA 游戏标题、IDE 或专有控制系统时,人们可能不会期望默认的主要终止特性来满足开箱即用的需求(实际上,甚至可能没有主要的)。 所以我对Display作为默认值的偏见来自于我们在使用小型命令行应用程序时期望看到的内容——例如:

$ cd foo
bash: cd: foo: No such file or directory

我们中的大多数人并不期望有任何类型的调试帮助,只是简单地指示出了什么问题。 我只是提倡将此作为默认立场。

当我想到编写一个Terminate impl 来获得这样的简单输出时,我意识到 main 功能中的?与今天的稳定 rust 并没有什么不同(就编写的代码量而言),通常会创建一个Result -aware "inner_main()" 以处理E

考虑到这个假设,作为一个思考练习,我试图确定我是否强烈认为今天存在的“ inner_main() ”风格实现的优势是更随意的Display风格(通过更具技术性的Debug风味)。 我的想法是,这将表明该功能可能如何实际使用。

无法说服自己就是这种情况。 (意思是,我认为目前在现有实现中不存在对Display的强烈偏见)。 事实上,在查看我在过去 16 个月中编写的自己的存储库时,我还发现这两种情况都足够了,我不能说默认实施Display就等于节省了净成本。

坚持“主要受益者是 cli 应用程序”的假设,有大量的控制台应用程序提供帮助和使用信息。 例如:

$ git foo
git: 'foo' is not a git command. See 'git --help'.

The most similar command is
    log

因此,即使在控制台应用程序中,我也很难通过使用Debug来识别“受伤的群体”。

最后,我会更乐意使用Debug impl,而不是将该功能再保留 6 个月,所以,自私地,就是这样 :)。

所以我的思考过程是公开的。 总而言之,我认为Debug不会比Display更好也不会差,因此,作为默认实现应该没问题。

像你们中的许多人一样,我敢肯定,我确实希望有一个让我感到更兴奋的实现——比如,“是的,就是这样!!!”,TBH。 但也许这只是我的期望不切实际......也许一旦我们有一个解决方案可以使用failure减少我的项目中的样板,它会在我身上成长。 :)

注意我打开了一个 PR 来支持测试中的终止特征:#48143(基于@bkchr的工作)。

我冒昧地用一种处理测试结果的方法扩展了Termination特征。 这简化了实现,但也很有意义,因为测试失败可能需要比可执行失败更详细的输出。

Termination应该重命名为Terminate ,因为我们对 libstd 中特征动词的一般偏好。

@withoutboats我认为在某些时候有人讨论过动词特征主要是那些具有与特征同名的单一方法的那些。 无论如何,我可以再次提出我自己的自行车棚建议Exit吗?

无偿自行车脱落:这是一种单一方法的特征。 如果我们想给它们起相同的名字,也许是ToExitCode / to_exit_code

稳定返回Result可以独立于稳定特征来完成,对吗?

对我来说,这感觉很像? ,其中大部分价值来自语言特性,我们可以延迟找出特征。 RFC 讨论甚至让我想知道是否需要稳定 trait _ever_,因为将代码放入inner_main似乎比 trait impl 更容易...

是的,我们不需要稳定特征 - 尽管它对于框架之类的东西很有用,框架不一定如此依赖inner_main

@SimonSapin我认为To是指类型转换,但这不是。 但是我们可以将方法命名为terminate (我也不认为这种对何时命名特征动词的限制成立。 Try是一个明显的反例。)

我建议我们稳定fn main() -> T ,其中T不是单位。 这留下了许多细节——特别是特征的名称/位置/细节——不稳定,但有些事情是固定的。 详情在这里:

https://github.com/rust-lang/rust/issues/48453

请提供您的反馈!

terminate似乎比report更具描述性。 我们总是terminate ,但可以省略report ing。

但与std::process::exit不同,此方法不会终止任何内容。 它仅将main()的返回值转换为退出代码(在可选地将Result::Err打印到 stderr 之后)。

再次为Exit投票。 我喜欢它简短、描述性强并且与退出代码/退出状态的传统概念一致。

我肯定更喜欢Exit而不是Terminate ,因为我们通过从 main 返回优雅地退出,而不是因为确实出了问题而突然硬终止我们所在的位置。

我添加了https://github.com/rust-lang/rust/issues/48854以提议稳定返回结果的单元测试。

哦,嘿,我找到了合适的地方谈论这个。

在文档测试中使用?

doctests 目前的工作方式是这样的:

  • rustdoc 扫描 doctest 是否声明了fn main

    • (目前它只是对不在//之后的“fn main”进行逐行文本搜索)

  • 如果fn main被发现,它不会触及已经存在的东西
  • 如果没有找到fn main ,它将把大部分的 doctest 包装在一个基本的fn main() { }

    • *完整版:它将提取#![inner_attributes]extern crate声明并将它们放在生成的主函数之外,但其他所有内容都放在里面。

  • 如果 doctest 不包含任何 extern crate 语句(并且记录的 crate 不称为std ),那么 rustdoc 还将在生成的 main 函数之前插入一个extern crate my_crate;语句。
  • rustdoc 然后将最终结果编译并作为独立二进制文件运行,作为测试工具的一部分。

(我遗漏了一些细节,但我很方便地在这里写了一个完整的文章。)

因此,要在 doctest 中无缝使用? ,需要更改的部分是它添加fn main() { your_code_here(); } fn main() -> Result<(), Error>部分在常规代码中 - rustdoc 甚至不需要在那里修改。 但是,要使其在不手动声明 main 的情况下工作,需要进行一些小的调整。 由于没有密切关注此功能,我不确定是否有万能的解决方案。 fn main() -> impl Termination可能吗?

fn main() -> impl 终止可能吗?

从表面上看,是的: https ://play.rust-lang.org/?gist=8e353379f77a546d152c9113414a88f7&version=nightly

不幸的是,由于内置的​​错误转换,我认为-> impl Trait?从根本上是麻烦的,它需要推理上下文来告诉它使用什么类型: https://play.rust- lang.org/?gist=23410fa4fa684710bc75e16f0714ec4b&version=nightly

就我个人而言,我想象? -in-doctests 通过类似https://github.com/rust-lang/rfcs/pull/2107的方式工作为fn main() -> Result<(), Box<Debug>> catch { your_code_here(); } (使用来自 https:// 的语法github.com/rust-lang/rust/issues/41414#issuecomment-373985777)。

不过, impl Trait版本很酷,如果 rustc 可以在? desugar 内部使用某种“如果不受限制,则首选相同类型”的支持,它可能会起作用,但我的回忆是像这样的功能的想法往往会让那些了解事物如何运作的人惊恐地退缩 :sweat_smile: 但也许它是一个内部的东西,只有在输出为impl Trait时才适用......

哦,这些都是非常现实的担忧。 我忘记了这会如何破坏类型推断。 如果 catch 块像那个链接的问题一样开始 Ok-wrapping,那么这似乎是一条更容易(并且更快,更稳定)的前进道路。

我唯一想知道的是版本转换将如何影响它。 catch语法在 2018 年不是在变化吗? Rustdoc 可能希望在与其运行的库相同的版本中编译 doctest,因此它需要根据它所传递的 epoch 标志来区分语法。

我担心现在情况已经稳定,但简单的情况显然仍然是 ICE: https ://github.com/rust-lang/rust/issues/48890#issuecomment -375952342

fn main() -> Result<(), &'static str> {
    Err("An error message for you")
}
assertion failed: !substs.has_erasable_regions(), librustc_trans_utils/symbol_names.rs:169:9

https://play.rust-lang.org/?gist=fe6ae28c67e7d3195a3731839d4aac84&version=nightly

我们在什么时候说“这太有问题了,我们应该不稳定”? 似乎如果这以目前的形式进入稳定频道,会引起很多混乱。

@frewsxcv我认为问题现在已经解决了,对吧?

@nikomatsakis我在https://github.com/rust-lang/rust/issues/48389中提出的问题在 1.26-beta 中得到解决,所以从我的角度来看是的。

是的,我担心的 ICE 现在已经修复了!

很抱歉在可能为时已晚的时候插话,但我想在这里留下我的反馈,以防万一。 我确实阅读了这个线程的大部分内容,所以我在说话时会考虑到这种情况。 但是,这个线程很长,所以如果看起来我忽略了某些东西,那么我可能已经忽略了,我会很感激有人向我指出。 :-)

TL;DR - 我认为显示错误的Debug消息是一个错误,更好的选择是使用错误的Display消息。

我的核心信念是,作为一个经常在 Rust 中构建 CLI 程序的人,我不记得很关心 $ ErrorDebug消息是什么。 也就是说,按照设计,错误的Debug是针对开发人员的,而不是针对最终用户的。 当你构建一个 CLI 程序时,它的界面基本上是供最终用户阅读的,因此Debug消息在这里几乎没有用处。 也就是说,如果我交付给最终用户的任何 CLI 程序在正常操作中显示了 Rust 值的调试表示,那么我会考虑修复一个错误。 我通常认为这应该适用于每个用 Rust 编写的 CLI 程序,尽管我理解这可能是理性的人可能不同意的一点。 话虽如此,我的观点有些令人吃惊的暗示是,我们已经有效地稳定了一个功能,其中它的默认操作模式让你从一个应该修复的错误(再次,IMO)开始。

通过默认显示Debug表示错误,我还认为我们鼓励不良做法。 特别是在编写 Rust CLI 程序的过程中,经常会观察到即使是错误的Display impl 也不足以被最终用户消费,必须做一些实际的工作修理它。 一个具体的例子是io::Error 。 显示没有相应文件路径的io::Error (假设它来自读取/写入/打开/创建文件)基本上是一个错误,因为最终用户很难用它做任何事情。 通过选择默认显示错误的Debug表示,我们让创建 CLI 程序的人更难发现这些错误。 (最重要的是, Debugio::Error远不如Display有用,但这本身并不是我的经验中的一个巨大痛点。 )

最后,为了完善我的论点,我也很难想象即使在示例中我也会使用?-in-main的情况。 也就是说,我一直在尝试编写与现实世界程序尽可能匹配的示例,这通常涉及编写如下内容:

use std::error::Error;
use std::process;

fn try_main() -> Result<(), Box<Error>> {
    // do stuff with `?`
}

fn main() {
    if let Err(err) = try_main() {
        eprintln!("{}", err);
        process::exit(1);
    }
}

从表面上看,用?-in-main替换它会是 _lovely_ ,但我不能,因为它不会显示Display的错误。 也就是说,在写一个真正的CLI程序时,我其实会使用上面的方法,所以如果我想让我的例子反映现实,那么我认为我应该展示我在实际程序中所做的事情,而不是走捷径(在合理的范围内) )。 我实际上认为这类事情真的很重要,从历史上看,它的一个副作用是它向人们展示了如何编写惯用的 Rust 代码,而不会到处乱扔unwrap 。 但是,如果我在示例中恢复使用?-in-main ,那么我就违背了我的目标:我现在正在设置那些可能不知道更好的人来编写默认情况下发出非常无用的错误消息。

“发出错误消息并以适当的错误代码退出”的模式实际上用于抛光程序中。 例如,如果?-in-main使用Display ,那么我今天可以在 ripgrep 中合并mainrun函数:

https://github.com/BurntSushi/ripgrep/blob/64317bda9f497d66bbeffa71ae6328601167a5bd/src/main.rs#L56 -L86

当然,一旦稳定下来,我可以通过为Termination trait 提供我自己的 impl 来使用?-in-main ,但是如果我可以写出main ,我为什么还要费心去做呢? impl以使它们与现实相匹配,在这一点上,我不妨坚持我今天的示例(使用maintry_main )。

从外观上看,解决这个问题似乎是一个突破性的变化。 也就是说,这段代码在今天的 Rust 稳定版上编译:

#[derive(Debug)]
struct OnlyDebug;

fn main() -> Result<(), OnlyDebug> {
    Err(OnlyDebug)
}

我认为切换到Display会破坏这段代码。 但我不确定! 如果这真的是一个船已经航行的问题,那么我理解并且没有太多用在详细说明这一点,但我确实对此感到足够强烈,至少可以说点什么,看看我是否不能说服别人看看如果有什么可以解决的。 (我也很可能在这里反应过度,但到目前为止,我已经有几个人在我的 CSV 示例中问我“你为什么不使用?-in-main ?”,而我的回答基本上是, “我看不出这样做是怎么可行的。”也许这不是?-in-main打算解决的问题,但有些人肯定有这种印象。随着它目前的实施,我可以看到它在文档测试和单元测试中很有用,但我很难想到我会使用它的其他情况。)

我同意在Debug上显示Display对 CLI 应用程序来说更好。 我不同意它应该是Display而不是Debug ,因为这极大地限制了实际上可能是?的错误,并违背了?-in-main的目的Display的非破坏性方式,并在不可用时回退到Debug

#[unstable(feature = "termination_trait_lib", issue = "43301")]
impl<E: fmt::Display> Termination for Result<!, E> {
    fn report(self) -> i32 {
        let Err(err) = self;
        eprintln!("Error: {}", err);
        ExitCode::FAILURE.report()
    }
}

最后,为了完善我的论点,我也很难想象在什么情况下我会使用 ?-in-main 甚至在示例中。

对于真正的 CLI 程序,我必须同意@BurntSushi ,但对于只有我打算使用的随机脚本和内部工具,这真的很方便。 此外,它对于原型和玩具来说真的很方便。 我们总是不鼓励在生产代码中实际使用它,对吧?

?-in-main 的部分目标是避免代码示例中的单一代码,即展开。 但是,如果 ?-in-main 本身变得单调,那么这会破坏整个功能。 我强烈希望避免任何会导致我们不得不阻止在生产代码或其他方式中使用它的结果。

我一直在尝试在 main 中使用此功能,但我发现它非常令人沮丧。 终止特征的现有实现不允许我方便地“累积”多种错误——例如,我不能使用 failure::Fail,因为它没有实现 Error; 我不能使用盒子,同理。 我认为我们应该优先考虑更改为 Debug。 =)

如果我们使用Display ,我们可以引入像MainError这样快速而肮脏的类型来累积多种错误:

pub struct MainError {
    s: String,
}

impl std::fmt::Display for MainError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        self.s.fmt(f)
    }
}

impl<T> From<T> for MainError where T: std::error::Error {
    fn from(t: T) -> Self {
        MainError {
            s: t.to_string(),
        }
    }
}

这将允许类似于下面的内容Box<Error>

fn main() -> Result<(), MainError> {
    let _ = std::fs::File::open("foo")?;
    Ok(())
}

先前关于显示与调试的讨论在这里隐藏部分,从https://github.com/rust-lang/rust/issues/43301#issuecomment -362020946 开始。

@BurntSushi我同意你的看法。 如果无法解决此问题,则可能有一种解决方法:

use std::fmt;
struct DisplayAsDebug<T: fmt::Display>(pub T);

impl<T: fmt::Display> Debug for DisplayAsDebug {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        fmt::Display::fmt(&self.0, f)
    }
}

impl<T: fmt::Display> From<T> for DisplayAsDebug {
    fn from(val: T) -> Self {
        DisplayAsDebug(val)
    }
}

如果这是在std::fmt中,我们可以使用这样的东西:

use std::{fmt, io};

fn main() -> Result<(), fmt::DisplayAsDebug<io::Error>> {
    let mut file = File::open("/some/file")?;
    // do something with file
}

在可预见的未来,我仍然没有时间为此做出更多贡献,但我也同意 main 中的?应该可以在严肃的 CLI 程序中使用,并且在最初的草案中有一些功能RFC 旨在促进这一点。 它们以渐进主义的名义被放弃,但也许应该重新审视它们。

我认为如果很快就完成了,那么在很多代码在稳定版上使用它之前,以向后不兼容的方式解决这个问题是可以的。

作为一个用 Rust 编写大量 CLI 程序的人,并且在工作中负责 Rust 风格,我在这里非常同意@burntsushi 。 我会很乐意使用 RFC 中指定的此功能的版本。

但我认为向用户展示Debug实现是一个错误,并且并不比在 CLI 程序中到处调用unwrap好多少。 所以即使在示例代码中,我也无法想象曾经使用过这个版本的功能。 这太糟糕了,因为从main中删除样板文件会简化新 Rust 开发人员在工作中的学习,我们不需要再解释quick_main!或类似的东西。

几乎我能想象使用它的唯一情况是从 doctests 中消除unwrap 。 但我不知道这是否还支持。

如果我们使用Display ,我们可以引入像MainError这样快速而肮脏的类型来累积多种错误:

就个人而言,这种解决方法会增加足够的复杂性,从而更容易完全避免? -in- main

@Aaronepower

据我所知(可能完全是错误的,因为我没有时间编译 stdlib 并且我不知道专业化是否涵盖了这一点)我们没有理由不能将以下 impl 添加到 Termination。 这将提供一种在可用时使用 Display 并在不可用时回退到 Debug 的非破坏性方式。

如果这能让我编写fn main() -> Result<(), failure::Error>并使用Display得到一个很好的、人类可读的错误,那肯定会满足我的主要担忧。 但是,我想这仍然会留下在设置 $# RUST_BACKTRACE=1 failure::Error中显示回溯的问题——但这可能超出范围,无论如何,或者至少是一个应该接受failure

这在前一段时间已经稳定下来,我认为我们无法真正改变Result的行为。 我个人对我倾向于使用的用例的当前行为(快速和肮脏的脚本等)仍然非常满意,但我同意更复杂的脚本不会想要它。 然而,在讨论的时候,我们也讨论了一些可以控制这种行为的方法(我现在似乎找不到这些评论,因为 github 对我隐瞒了一些事情)。

例如,您可以为错误类型定义自己的“包装器”,并为其实现From

struct PrettyPrintedError { ... }
impl<E: Display> From<E> for PrettyPrintedError { }

impl Debug { /* .. invoke Display .. */ }

现在你可以写这样的东西,这意味着你可以在main中使用? #$ :

fn main() -> Result<(), PrettyPrintedError> { ... }

也许这种类型应该是 quick-cli 或其他东西的一部分?

@nikomatsakis是的,我完全得到了这个解决方法,但我确实觉得这违背了使用?-in-main的简洁目的。 我觉得“使用这种解决方法可以使用?-in-main ” 不幸地破坏?-in-main本身。 例如,我不会用简洁的例子写出你的解决方法,也不会对我写的每个例子强加对quicli的依赖。 我当然也不会将它用于快速程序,因为我基本上希望在我编写的每个 CLI 程序中都有错误的Display输出。 我的观点是,错误的调试输出是您放在用户面前的 CLI 程序中的错误。

?-in-main的替代方案是一个附加的~4 行函数。 因此,如果修复?-in-main以使用Display的解决方法远不止于此,那么我个人认为根本没有理由使用它(在文档测试或单元测试之外,如上文提到的)。

这是可以在版本中更改的内容吗?

@BurntSushi您对std中的解决方法有何看法,因此您只需在main()上编写更长的返回类型声明?

@Kixunil我最初的感觉是它可能很好吃。 不过也没多想。

@BurntSushi

?-in-main的替代方案是一个额外的~4 行函数。

最终,行为是稳定的,我认为我们目前无法真正改变它。 (我的意思是这不是违反健全性或什么的。)

也就是说,我仍然发现当前的设置非常好,并且比Display更可取,但我想这取决于你想要“默认”是什么——也就是说,任何一个设置都可以像其他通过各种新类型。 因此,要么我们默认偏爱需要Debug的“quick-n-dirty”脚本,要么选择完善的最终产品。 我倾向于认为完善的最终产品是我可以轻松负担额外导入的地方,也是我想从许多不同的可能格式中选择的地方(例如,我只想要错误,还是想要包含 argv [0] 等)。

更具体地说,我想它看起来像这样:

use failure::format::JustError;

fn main() -> Result<(), JustError> { .. }

或者,为了获得不同的格式,也许我这样做:

use failure::format::ProgramNameAndError;

fn main() -> Result<(), ProgramNameAndError> { .. }

与您之前必须编写的 4 行函数相比,这两个函数都感觉相当不错:

use std::sys;

fn main() {
  match inner_main() {
    Ok(()) => { }
    Err(error) => {
      println!("{}", error);
      sys::exit(1);
    }
}

fn inner_main() -> Result<(), Error> {
  ...
}

我正在讨论这个话题,因为对于生产质量,将输出添加到本质上的自定义错误类型似乎是推动人们的好方向。 例如,这似乎与panic!默认情况下提供调试质量消息的方式相似。

@nikomatsakis这也许是一个更好的长期观点,我确实发现你的最终结果很开胃。 如果有一天这些错误类型最终以std结束,那就太好了,这样它们就可以以尽可能少的开销在示例中使用。 :-)

过程说明:我试图在这里用“有什么可以做的”而不是“让我们改变一些已经稳定的东西”来构建我的初始反馈,以努力专注于高层次的目标,而不是要求我们做一些事情肯定做不到。 :-)

没过多久: https: //crates.io/crates/exitfailure 😆

我想知道有多少人对使用Debug特征和Display特征感到惊讶。 最初的讨论草案和最终的 RFC都使用Display 。 这类似于普遍的 impl trait 事件,人们认为他们得到了一个东西,但只有在它变得稳定之后才知道他们得到了另一个。 另外考虑到很多人很惊讶不会使用这个功能,我觉得如果我们在下一个版本中改变它不会有很大的反斜杠。

@WiSaGaN RFC 的详细信息会不时更改。 这是预期的和健康的。 RFC 是设计文档,并不能完美地代表现在和将来会是什么。 信息发现难,是我没注意。 我希望我们可以避免重新考虑此功能的稳定性,而是专注于我们可以采取行动的更高级别的目标。

@nikomatsakis我看到的偏爱快速n-dirty案例的主要问题是已经有一种方法可以解决它们: .unwrap() 。 所以从这个角度来看, ?只是另一种编写快速n-dirty 事物的方式,但没有类似简单的方式来编写优美的事物。

当然,这艘船已经航行了,所以我们大部分都被搞砸了。 如果可能的话,我希望在下一版中看到这一点。

@Kixunilunwrap确实不是解决问题的好方法,因为该线程的标题表明我们希望能够在main中使用语法糖? #$ . 我可以理解你来自哪里(事实上,这是我最初犹豫使用?进行选项处理的一部分)但是在语言中包含?这个问题是相当不错的可用性赢得恕我直言。 如果您需要更好的输出,那么您可以选择。 你不会在没有先测试的情况下将东西发布给用户,并且发现你需要一个自定义类型来输出main将是一个非常快速的实现。

至于“为什么”我们想要?中的main ,请考虑一下它现在有多奇怪。 您几乎可以在其他任何地方使用它(因为您可以控制返回类型)。 然后main函数最终感觉有点特别,它不应该这样做。

.unwrap()是一种快速而肮脏的解决方案,它不组合,因此您在草拟程序时不能将代码放入和取出main()

相比之下, ?是一个快速而肮脏的解决方案,它确实可以组合,并且使它不是快速和肮脏的问题是在main()上放置正确的返回类型,而不是修改代码本身。

@Screwtapello啊,现在对我来说很有意义。 谢谢!

我只是想表达我认为这样做的目的是在main中为所有应用程序带来? ,而不必求助于额外的包装器......如果它只是为了测试我不看到它有很多好处,并且会继续坚持.unwrap() ,遗憾的是。

@oblitum这不仅仅是为了测试。 它只是(默认情况下)不会使用Display格式。 这可能是也可能不是您正在寻找的输出,但几乎肯定会比.unwrap的输出更好。 话虽如此,使用?可能会使您的代码“更好”。 在任何情况下,仍然可以自定义 $ main?错误的输出,正如@nikomatsakis上面详述的那样。

是否可以在展示柜的stdlib中引入新类型? 就像是:

#[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)]
#[must_use]
struct DisplayResult<E: fmt::Display>(Result<(), E>)

impl<E> Termination for DisplayResult<E> {
    // ... same as Result, but use "{}" instead of "{:?}"
}

impl<E> From<Result<(), E>> for DisplayResult<E> {}
impl<E> Deref<Result<(), E>> for DisplayResult<E> {}
impl<E> Try for DisplayResult<E> {}
// ...

这应该允许用户编写:

fn main() -> DisplayResult<MyError> {
    // Ordinary code; conversions happen automatically via From, Try, etc.
}

这里的主要动机是:

  • 返回错误时使用显示
  • 用户不必对其错误或结果类型进行任何额外的包装; 他们可以在他们的主要功能中使用?

后续行动:我突然想到?From的语义可能不允许它像我想要的那样隐式地工作。 我知道?将通过From在错误类型之间进行转换,但我不知道它是否对Try的不同实现者做同样的事情。 也就是说,它将从Result<?, io::Error>转换为Result<?, FromIoError> ,但不一定从Result<?, Err>转换为DisplayResult<Result<?, Err>> 。 基本上,我正在寻找这样的工作:

fn main() -> DisplayResult<io::Error> {
    let f = io::File::open("path_to_file")?;
    let result = String::new();
    f.read_to_string(result)?
}

假设这不起作用,也许可以在 Cargo.toml 中将其作为一个简单的条件编译标志?

@Lucretiel :您似乎假设程序员主要只想将退出消息打印到控制台。 这不适用于非技术性桌面应用程序、带有日志管道的守护进程等。我不希望我们在没有令人信服的证据(例如数字)的情况下将“常用”的东西添加到标准库中。

我假设从main返回Result<(), E> where E: Error的程序员有兴趣将错误消息打印到控制台,是的。 即使是通过Debug打印的当前行为也是“向控制台打印退出消息”。

在这个线程中似乎有广泛的共识,应该将某些内容打印到 stderr; 要做出的关键决定是“是否应该使用"{}""{:?}"或其他东西打印,以及它应该如何配置,如果有的话”。

@Lucretiel ,对我来说,返回Result的价值很好地控制了退出状态。 是否打印一些不是最好的东西会留给恐慌处理程序吗? 不仅是是否将某些内容打印到 stderr 的问题,还有要打印什么(回溯、错误消息等?)的问题。 请参阅https://github.com/rust-lang/rust/issues/43301#issuecomment -389099936,尤其是。 最后一部分。

我最近使用了这个,希望它叫做Display 。 很确定我们在这里打错电话了。

我注意到的一个过程失败是决定是由 lang 团队(主要是 Niko 和我)而不是 libs 团队做出的,但有问题的代码完全存在于 std. 可能libs会做出更好的决定。

@withoutboats改变是否为时已晚? 我注意到该功能在docs中仍被标记为 nightly-only/experimental ,但我可能不了解稳定过程背后的一些粒度。

当团队要求我将实现从Display更改为Debug时,我也希望看到这种变化并且很遗憾没有成为它的有力倡导者。

@Lucretiel Termination特征是每晚的,但功能本身(从main /tests 返回Termination实现)已经稳定。

是否有稳定特征名称的票或时间表?

@xfix这意味着可以更改实现,对吗? 行为没有改变(从main Termination #$ ),但特征本身将从:

impl<E: Debug> Termination for Result<(), E> ...

到:

impl<E: Display> Termination for Result<(), E> ...

有可能改变实现,对吧?

并非没有破坏向后兼容性。 这段代码在当今稳定的 Rust 中编译:

#[derive(Debug)]
struct X;

fn main() -> Result<(), X> {
    Ok(())
}

建议更改为 require Display ,它将停止编译。

正如已经提到的那样,专业化之类的东西可能会有所帮助,但我目前对专业化的理解是,这只对同时实现DisplayDebug的类型有用(即,有一种更特殊的实现)。

trait ResultTerm {
    fn which(&self);
}
impl<T: Debug> ResultTerm for T {
    default fn which(&self) {
        println!("{:?}", self)
    }
}
impl<T: Debug + Display> ResultTerm for T {
    fn which(&self) {
        println!("{}", self)
    }
}

enum MyResult<T, E> {
    Ok(T),
    Err(E),
}

impl<T, E> Termination for MyResult<T, E>
where
    E: ResultTerm,
{
    fn report(self) -> i32 {
        match self {
            MyResult::Err(e) => {
                e.which();
                1
            }
            _ => 0,
        }
    }
}

操场

这可能足以涵盖所有常见情况,但它确实公开了专业化。

啊,我明白你所说的稳定生锈是什么意思了。 出于某种原因,我不知道这是在稳定版中可用的。

这是可以在编译时使用 Cargo 标志切换的东西吗? 就像其他东西可以切换一样,比如panic = "abort"

可能是? 我可以看到在另一个 Rust 版本中对main中的?有不同的行为是可行的。 可能不是 2018 年,也许是 2021 年?

同一程序中的不同 crate 可以在不同的版本中,但使用相同的std ,因此std中可用的特征及其 impl 需要在不同版本中相同。 我们理论上可以做的(我不提倡这样做)是有两个特征, TerminationTermination2 ,并且需要返回类型main()来实现一个或其他取决于定义main()的板条箱的版本。

我认为我们不需要贬低任何东西。 我将回应@Aaronepower的观点,即仅限于 Display 类型可能只会让快速脚本用户不满意(派生 Debug 是微不足道的,实现 Display 不是),就像当前情况使生产用户不满意一样。 即使我们可以,我也不认为这最终会带来好处。 我认为@shepmaster提出的专业化方法很有希望,因为它会产生以下行为:

  1. 具有 Debug 但没有 Display 的类型将默认打印其 Debug 输出
  2. 同时具有 Debug 和 Display 的类型将默认打印其 Display 输出
  3. 具有 Display 但不具有 Debug 的类型会导致编译器错误

有人可能会反对第 2 点,如果您有一个带有 Display 的类型,但您希望它出于任何原因打印 Debug; 我认为 Niko 提出的各种预先设计的输出格式错误类型的提议可以解决这种情况(即使我们采用专业化路线,也可能值得探索)。

至于第 3 点,它有点笨拙(也许我们应该在 1.0 中做trait Display: Debug ?),但如果有人不厌其烦地编写一个 Display impl,那么要求他们打耳光就不是什么大问题了一个单一的派生(我怀疑,他们可能无论如何......有没有人原则上反对在他们的显示类型上使用调试?)。

仅限于 Display 类型可能只会让快速脚本用户不满意(派生 Debug 是微不足道的,实现 Display 不是)

在快速脚本中Display优于Debug的情况是使用&'static strString作为错误类型。 如果你有一个自定义的错误类型来打#[derive(Debug)]到你已经在错误处理上花费了一些不平凡的精力。

@SimonSapin我不会说它在快速脚本环境中更可取,或者说不够可取,以至于我希望将字符串打印为Display格式而不想付出努力有正确格式的错误,对我来说Debug是首选,因为它是字符串时的错误格式格式不正确,所以在它周围有Err("…")可以让我轻松地从任何其他错误中直观地挑选出错误可能已经发出的输出。

FWIW 格式化的不是Result<_, E>值,而是里面的E值。 所以你不会在输出中看到Err( 。 然而,当前代码添加了一个Error:前缀,如果使用Display ,它可能会保留。 除了不打印引号之外, Display也不会反斜杠转义字符串的内容,这在向用户显示(错误)消息时是 IMO 所希望的。

https://github.com/rust-lang/rust/blob/cb6eeddd4dcefa4b71bb4b6bb087d05ad8e82145/src/libstd/process.rs#L1527 -L1533

我认为鉴于这种情况,我们能做的最好的事情是专门化实现Display + Debug以打印Display impl 的类型。 不幸的是,不实现Debug的类型不能用作错误(直到我们支持交集 impls),但这是我们能做的最好的。

主要问题是该 impl 是否属于我们当前针对 std 中的专门 impl 的政策。 我们通常有一个规则,我们只在 std 中使用特化,我们不提供行为以后不会改变的稳定保证(因为特化本身是一个不稳定的特性)。 如果我们最终删除专业化作为一项功能,我们现在是否愿意提供这个 impl,因为有朝一日这些类型可能会恢复打印它们的Debug输出?

实际上,我认为我们现在不能允许这种专业化,因为它可能被用来引入不健全。 这个 impl 实际上正是那种给我们带来很多问题的专业化!

use std::cell::Cell;
use std::fmt::*;

struct Foo<'a, 'b> {
     a: &'a Cell<&'a i32>,
     b: &'b i32,
}

impl<'a, 'b> Debug for Foo<'a, 'b> {
    fn fmt(&self, _: &mut Formatter) -> Result {
        Ok(())
    }
}

impl<'a> Display for Foo<'a, 'a> {
    fn fmt(&self, _: &mut Formatter) -> Result {
        self.a.set(self.b);
        Ok(())
    }
}

由于目前专业化的工作方式,我可以在Foo<'a, 'b> report ,其中'b不会超过'a ,并且编译器目前完全有可能即使不满足生命周期要求,也会选择调用Display实例的 impl,从而允许用户无效地延长引用的生命周期。

我不了解你们,但是当我编写快速的“脚本”时,我只是unwrap()把所有东西都搞砸了。 它好 100 倍,因为它有回溯,可以为我提供上下文信息。 没有它,如果我的代码中有不止一次let ... = File::open() ,我已经无法确定哪个失败了。

我不了解你们,但是当我编写快速的“脚本”时,我只是unwrap()把所有东西都搞砸了。 它好 100 倍,因为它有回溯,可以为我提供上下文信息。 没有它,如果我的代码中有不止一次let ... = File::open() ,我已经无法确定哪个失败了。

这实际上就是为什么我对Display感觉如此强烈的原因,因为通常我在这种情况下所做的是做一个附加文件名和其他相关上下文信息的map_err 。 根据我的经验,通常调试输出对于跟踪实际出了什么问题没有多大帮助。

@Kixunil :或者,我更喜欢更精细的错误处理策略和/或更广泛的日志记录和/或调试。

我想“实施 RFC”可以打勾,对吧? 至少按照这个! :微笑:

如果这完全没有头绪,请原谅我,但你不能尽可能使用专业化来报告错误链:

use std::error::Error;

impl<E: fmt::Debug> Termination for Result<!, E> {
    default fn report(self) -> i32 {
        let Err(err) = self;
        eprintln!("Error: {:?}", err);
        ExitCode::FAILURE.report()
    }
}

impl<E: fmt::Debug + Error> Termination for Result<!, E> {
    fn report(self) -> i32 {
        let Err(err) = self;
        eprintln!("Error: {:?}", err);

        for cause in Error::chain(&err).skip(1) {
            eprintln!("Caused by: {:?}", cause);
        }
        ExitCode::FAILURE.report()
    }
}

https://github.com/rust-lang/rfcs/blob/f4b8b61a414298ba0f76d9b786d58ccdc34a44bb/text/1937-ques-in-main.md#L260 -L270

impl<T: Termination, E: Display> Termination for Result<T, E> {
    fn report(self) -> i32 {
        match self {
            Ok(val) => val.report(),
            Err(ref err) => {
                print_diagnostics_for_error(err);
                EXIT_FAILURE
            }
        }
    }
}

RFC的这一部分没有实施是有原因的吗?

我们确实对要使用的精确特征集和 impl 进行了一些更改,但我现在不记得细节了——我相信还有一些细节需要稳定。 您可能需要查看与顶级问题相关的稳定性报告以获取更多详细信息。

此功能的状态如何? 有稳定的建议吗?

@GrayJack这个问题应该关闭,因为这个问题很久以前就出现了。

是的,我有点困惑,我是从Termination特征的链接到这里的,我在问这个问题, termination_trait_lib是否有适当的问题?

这个问题应该关闭

这个问题仍然开放以跟踪Termination的稳定性。

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