【问题标题】:Confusing lifetime issues when returning `impl Fn`返回`impl Fn`时令人困惑的生命周期问题
【发布时间】:2019-12-04 19:42:38
【问题描述】:

我一直在尝试了解与我编写的返回 impl Fn 的函数相关的一些生命周期冲突。让我们从头开始。我有以下无法编译的代码文件:

use nom::bytes::complete::is_not;
use nom::character::complete::multispace0;
use nom::combinator::verify;
use nom::error::{
    ParseError,
    VerboseError,
};
use nom::sequence::terminated;
use nom::IResult;

fn one_token<'a, E>(input: &'a str) -> IResult<&str, &str, E>
where
    E: ParseError<&'a str>,
{
    terminated(is_not(" \t\r\n"), multispace0)(input)
}

fn str_token<'a, E>(expected_string: String) -> impl Fn(&'a str) -> IResult<&str, &str, E>
where
    E: ParseError<&'a str>,
{
    verify(one_token, move |actual_string| {
        actual_string == expected_string
    })
}

fn main() {
    let parser_1 = str_token::<VerboseError<_>>("foo".into());
    let string = "foo bar".to_string();
    let input = &string[..];
    let parser_2 = str_token::<VerboseError<_>>("foo".into());

    println!("{:?} {:?}", parser_1(input), parser_2(input),);
}

我收到此错误消息:

error[E0597]: `string` does not live long enough
  --> src/main.rs:30:18
   |
30 |     let input = &string[..];
   |                  ^^^^^^ borrowed value does not live long enough
...
34 | }
   | -
   | |
   | `string` dropped here while still borrowed
   | borrow might be used here, when `parser_1` is dropped and runs the destructor for type `impl std::ops::Fn<(&str,)>`
   |
   = note: values in a scope are dropped in the opposite order they are defined

似乎返回的impl Fn 分配给parser_1 仅适用于生命周期至少与parser_1 变量一样长的值。这违背了我的预期,即parser_1 可以使用任何生命周期的变量。我最初怀疑这可能是由于str_token 上的生命周期参数'a 和错误类型参数E 之间的一些交互。所以我只是明确了错误类型:

fn one_token(input: &str) -> IResult<&str, &str, VerboseError<&str>> {
    terminated(is_not(" \t\r\n"), multispace0)(input)
}

fn str_token<'a>(
    expected_string: String,
) -> impl Fn(&'a str) -> IResult<&str, &str, VerboseError<&str>> {
    verify(one_token, move |actual_string| {
        actual_string == expected_string
    })
}

这并没有解决问题。它会导致完全相同的编译错误。然后我尝试修改str_token 以使用更高级别的特征边界:

fn str_token(
    expected_string: String,
) -> impl for<'a> Fn(&'a str) -> IResult<&str, &str, VerboseError<&str>> {
    verify(one_token, move |actual_string| {
        actual_string == expected_string
    })
}

然后我得到这个错误:

error[E0277]: expected a `std::ops::Fn<(&'a str,)>` closure, found `impl std::ops::Fn<(&str,)>`
  --> src/main.rs:14:6
   |
14 | ) -> impl for<'a> Fn(&'a str) -> IResult<&str, &str, VerboseError<&str>> {
   |      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected an `Fn<(&'a str,)>` closure, found `impl std::ops::Fn<(&str,)>`
   |
   = help: the trait `for<'a> std::ops::Fn<(&'a str,)>` is not implemented for `impl std::ops::Fn<(&str,)>`
   = note: the return type of a function must have a statically known size

error[E0271]: type mismatch resolving `for<'a> <impl std::ops::Fn<(&str,)> as std::ops::FnOnce<(&'a str,)>>::Output == std::result::Result<(&'a str, &'a str), nom::internal::Err<nom::error::VerboseError<&'a s
tr>>>`
  --> src/main.rs:14:6
   |
14 | ) -> impl for<'a> Fn(&'a str) -> IResult<&str, &str, VerboseError<&str>> {
   |      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected bound lifetime parameter 'a, found concrete lifetime
   |
   = note: the return type of a function must have a statically known size

而且,坦率地说,我基本上不知道如何解释。任何人都可以评论这里发生的事情吗?为什么返回的impl Fn 的生命周期会绑定到生成它的工厂函数的生命周期,即使它的行为实际上并不依赖于该生命周期?如何解决此问题并仍然使用 impl Fn 返回值?当 HRTB 看起来是它们的完美应用时,为什么不工作呢?我在这里迷路了。

顺便说一句,我正在使用此处找到的 nom 解析库:https://github.com/Geal/nom/

另外,verify 函数的代码在这里:https://github.com/Geal/nom/blob/851706460a9311f7bbae8e9b7ee497c7188df0a3/src/combinator/mod.rs#L459

如果有人想玩一个包含所有这些示例的货物项目,这里有一个:https://github.com/davesque/nom-test/

您可以克隆它,签出first-versionno-error-parameterhigher-rank-trait-bounds 标签,然后调用cargo run

注意

我最近在这里问了一个类似的问题:How to use higher-rank trait bounds to make a returned impl Fn more generic?

但是,我最终认为这个问题对我实际尝试做的事情不够具体。已经有人回答了,所以我不想做大的修改,导致答案变得混乱,显然与我的问题无关。

【问题讨论】:

  • 好吧,我不完全理解这个问题,而且 Playground 有一个旧版本的 nom,但我突然想到了一件事情:你有 Fn(&amp;'a str) -&gt; IResult&lt;&amp;str, &amp;str, E&gt;verify 返回 impl Fn(I) -&gt; IResult&lt;I, O1, E&gt;;也就是说,函数接受的I 必须与它返回的IResult 的第一个类型参数相同。如果您将其中一个设为&amp;'a str,则另一个不能只是&amp;str,因为这意味着它们不再是同一类型了。
  • 我不必包含 'a 因为终身省略。假定其他 &amp;str 类型的生命周期为 'a
  • 你确定吗?据我了解,永远不会假定省略的生命周期与显式生命周期相同。
  • 来自关于生命周期省略的 rustnomicon 部分 (doc.rust-lang.org/nomicon/lifetime-elision.html):“如果只有一个输入生命周期位置(是否省略),则该生命周期将分配给所有省略的输出生命周期。”
  • 哦,我错了。那么......为什么你决定在参数而不是输出上注释生命周期?如果它是否被省略并不重要,那么在某些地方省略而不是在其他地方省略似乎会令人困惑。也许只有我。

标签: rust


【解决方案1】:

当你有一个像&lt;'a&gt; 这样的生命周期的函数/结构/特征时,这意味着任何标记为'a 的引用必须比函数/结构/特征寿命更长。存活意味着(除其他外)被引用的东西必须在函数被调用/结构被创建/实现特征的项目之前已经存在。以后不能创建引用,因为这意味着它的生命周期比要求的晚。

在您的情况下,str_token&lt;'a&gt; 表示&amp;'a str 标记的字符串必须在调用str_token 函数之前已经创建并且已经存在。

您的代码违反了您的要求:

 let parser_1 = str_token::<VerboseError<_>>("foo".into());
 let input = &string[..];

因为parser_1 是在input 之前创建的,但其上的生命周期注释允许它仅用于在解析器之前创建的字符串

如果你交换这些行的顺序,它应该可以工作。

for&lt;'b&gt; impl Fn(&amp;'b str) 会更灵活,因为这意味着无论您使用什么函数都可以“动态地”定义生命周期,因此任何生命周期都可以工作。但是您正在使用的库显然需要不太灵活的方法,这可能是出于与您的使用没有直接关系的充分理由。

这是一个最小的测试用例:

fn parser<'a>() -> impl Fn(&'a str) -> &str {
    |a| a
}

fn main() {
    let s1 = String::new();
    let p = parser();
    let s2 = String::new();
    p(&s1);
    //p(&s2);
}

【讨论】:

  • 是的,我的印象是str_token&lt;'a&gt; 中的生命周期参数'astr_token 的调用站点上是“绑定”的,尽管我的直觉是它应该在调用时绑定str_token返回值 的站点。所以就像你说input 的生命周期不会超过parser_1 的生命周期。我想nom 库必须返回impl for&lt;'b&gt; Fn(..) 特征才能使其工作?而且我想他们不会这样做,因为他们只是返回 impl Fn(I) 而通用参数 I 并不期望生命周期参数。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-05-31
相关资源
最近更新 更多