< 返回版块

zhylmzr 发表于 2023-02-19 00:05

Tags:lifetime

fn main() {
    let foo = Foo::new();
    foo.run2();
    foo.run();
}

struct Foo<'a> {
    func: Box<dyn Fn(&'a Foo) + 'a>,
    // func: Box<fn(&'a Foo)>,
}

impl<'a> Foo<'a> {
    fn new() -> Self {
        Self {
            func: Box::new(|_| {}),
        }
    }

    fn run(&'a self) {}

    fn run2(&self) {}
}

上面这段代码, run2没有报错,run报错 borrowed value does not live long enough,它们两个的区别就在于手动标注了生命周期。

如果把func的定义换成Box<fn>,run、run2都不会报错了。

有两个问题我很费解:

  1. 调用run时,为什么标注了'a会报错,它不应该和省略生命周期效果一样的吗?
  2. 为什么把func的定义换成Box<fn>就不会报错了?

评论区

写评论
苦瓜小仔 2023-02-20 18:37

只有一个地方想吐槽

nothing: PhantomData<&'a i32>, // 为了不报错 'a 未使用

新手常见错误:遇事不决,PhantomData 救场。。。用它虽然通过编译,但在这里对你的代码没带来任何好处 —— 没有人喜欢无意义的泛型参数。让它成为一个简单的 &self 引用不好吗?

作者 zhylmzr 2023-02-20 16:12

根据这篇帖子的说明,我自己的理解如下,先放例子:

fn main() {
    let foo = Foo::new();
    foo.run();
}

struct Foo<'a> {
    func: Box<dyn Fn(&'a Foo) + 'a>, // not work
    // func: Box<dyn Fn(&Foo) + 'a>, // work
}

impl<'a> Foo<'a> {
    fn new() -> Self {
        Self {
            func: Box::new(|_| {}),
        }
    }

    fn run(&'a self) {}
}

关键问题在于Foo<'a>中的'a到底是什么,以及'a的范围多大。 func 作为 Foo 的字段,那么'a在Foo::new()时必须存活,我们把 let foo = Foo::new()时生命周期称之为'1,那么 'a:'1,也就是下图的(A)。

再者我们把调用foo.run()时的生命周期称之为'2,因为run的声明,'a = '2,也就是图(B),两个范围是矛盾的,所以报错(B)处的借用不够(A)长,(A)处因为trait object编译器保守的认为有drop glue需要drop

fn main() {
    let foo = Foo::new(); // -----------------+ '1 ----+ 'a                (A)
    //                                        |        |
    foo.run();// ----------------+ '2 -----------------|-------+ 'a        (B)
    //                           |            |        |       |
} // ----------------------------+------------+--------+-------+

知道了这个原因就可以修正了:省略func的生命周期同于HRTB,或者指明func生命周期和Foo实例的生命周期分开

fn main() {
    let foo = Foo::new(); // -----------------+ '1 ----+ 'b
    //                                        |        |
    foo.run();// ----------------+ '2 -----------------|-------+ 'a
    //                           |            |        |       |
} // ----------------------------+------------+--------+-------+

struct Foo<'a, 'b> {
    nothing: PhantomData<&'a i32>, // 为了不报错 'a 未使用
    func: Box<dyn Fn(&'b Foo) + 'b>,
}

impl<'a, 'b> Foo<'a, 'b> {
    fn new() -> Self {
        Self {
            nothing: PhantomData::default(),
            func: Box::new(|_| {}),
        }
    }

    fn run(&'a self) {}
}

最后再来看看楼上提到的例子:

fn main() {
    let boxed_f: F = Box::new(|_| {}); // --------------+ '1 -----+ 'a
    //                                                  |         |
    test(&boxed_f);//----------------+ '2 -----+ 'a ----|---------|
    //                               |         |        |         |
}// ---------------------------------+---------+--------+---------+
type F<'a> = Box<dyn Fn(&'a u8)>;
fn test<'a>(f: &'a F<'a>) -> &'a F<'a> {
    f
}

修复方法:

fn main() {
    let boxed_f: F = Box::new(|_| {}); // ---------------+ '1 -----+ 'a
    //                                                   |         |
    test(&boxed_f); //----------------+ '2 -----+ 'b ----|---------|
    //                                |         |        |         |
} // ---------------------------------+---------+--------+---------+

// 为了不混淆,我把这里的生命周期参数重命名了
type F<'b> = Box<dyn Fn(&'b u8)>;
// 这里也可以明确标识出 'a 一定比 'b 存活长
fn test<'a: 'b, 'b>(f: &'b F<'a>) -> &'b F<'a> {
    f
}
lithbitren 2023-02-20 01:12

评论不了是太菜了插不上话🤡🤡,还是请多多分享生命周期实战经验。

--
👇
苦瓜小仔: 生命周期的帖子没什么人看,也没什么人评论。懒得解释地那么清楚了。

它不应该和省略生命周期效果一样的吗?

不一样。fn run2(&self) {} 的脱糖是 fn run2<'b>(&'b self) {},所以 receiver type 为 &'b Foo<'a> 并且 'a: 'b。而 run1 的 receiver 为 &'a Foo<'a>

换成Box就不会报错了?

首先,你得知道 fn 是什么,fn 和 Fn 的区别是什么。去翻标准库文档、reference 吧,链接太多了,懒得贴。我自己的总结 在这

其次,编译器报错是三段式的描述,反映了典型的 NLL,如果你的确想知道它,去翻它的 RFC,我翻译过。记住最重要的一个基本观点,生命周期实质上是约束。

最后,你对 trait object 了解多少?具体来说: dyn Fn(&'a Foo) + 'b 中 'a 和 'b 约束了什么?你知道 trait object 保守地被认为具有 drop glue 吗? 你对 drop 有多少了解?

要明白你的例子为什么这样,为什么不能这样,上面提到的东西都得熟悉。

如果你把 Box<fn> 换成 fn,去看不同情况下的 MIR,会发现 fn 的情况下不需要 drop,而 Box<fn> 的情况与 Box<dyn ...> 一样需要 drop,而 drop 时允不允许生命周期悬空,就是这里症结所在。

苦瓜小仔 2023-02-19 22:35

我找到一种更简单的方式推理。Drop trait 文档 描述了析构函数由两部分组成:

  1. A call to Drop::drop for that value, if this special Drop trait is implemented for its type.
  2. The automatically generated “drop glue” which recursively calls the destructors of all the fields of this value.
fn main() {
    let foo = Foo::new(); // foo: Foo<'1>
    foo.run(); // &'1 Foo<'1>
} // Foo.func 字段具有 drop glue,从而需要调用析构函数(如果你不知道的话:dyn Trait 由两部分构成 —— 指向数据的指针;指向虚表的指针,其中虚表里有 drop 方法)
// 而调用 drop 方法需要获得 &'3 mut Box<dyn Fn(&'1 Foo) + '1>,
// 这无法做到,因为 Box<dyn '1> 完全因为 &'1 Foo<'1> 而借用成 &'1 Box<dyn '1>
// 要获取 &'3mut Box<dyn '1>,必须让 &'1 Box<dyn '1> 先结束,于是  &'1 Foo<'1> 悬空,而 drop Foo<'1> 不允许 '1 悬空,引发编译错误

fn main() {
    let foo = Foo::new(); // foo: Foo<'1>
    foo.run2(); // &'2 Foo<'1>
} // 调用 Foo.func 的 drop 方法需要获得 &'3 mut Box<dyn Fn(&'1 Foo) + '1>,
// 这能做到:先让 &'2 Box<dyn '1> 结束,再产生 &'3 mut Box<dyn '1>,同时 Box<dyn '1>、Foo<'1> 一直有效,直到各自被 drop

不重要的补充

我之前给的方式是仿照那篇帖子的回答,为什么要遵循这个方式,现在来看比较直观了:

let var;
    {
        // 局部作用域赋值
    }
var must end or dangle first to get a new &mut for dropping

这也类似于 MIR 的代码结构

苦瓜小仔 2023-02-19 15:34

我的例子是为了更好地说明才让函数返回引用,所以 let _f = test(&boxed_f); 应视为得到 &'a Foo<'a> 的操作。

即调用 fn run(&'a self) 方法的第一步,得到 &'a Foo<'a>,这个步骤。所以不要问我写的和你写的因为返回值不同而不一样。

也不要问 Box<dyn Fn(&'a Foo) + 'a> 和我的 type F<'a> = Box<dyn Fn(&'a u8)>; 不一样,我知道一个 +'a,另一个 +'static,但它不是这里的重点。

呼,回答问题真的好累,看不懂也别问我了:(

苦瓜小仔 2023-02-19 15:19

trait obejct 仅在new的时候构建了,后续并没有去调用它 ... 因为它接受的是不可变引用不会独占。

这与可变性(占用/共享)无关。

fn main() {
    let boxed_f = Box::new(|_| {}) as F;
    let _f = test(&boxed_f);
}
//   let boxed_f: F<'1> = ...; // '1 是多长呢?
//   let _f;
//   {
//      _f = test(&boxed_f); // &'1 F<'1> 表明 '1 至少要到这
//   }
//   _f: dangling
// } main block 结束前 drop boxed_f,dyn Trait 具有 drop glue,
// 也就是说需要 drop F<'1>,从而必须要求 '1 存活,而 _f 已经悬空,'1 无法存活

// 也就是报错描述的内容:
// 3 |     let _f = test(&boxed_f);
//   |                   ^^^^^^^^ borrowed value does not live long enough
// 4 | }
//   | -
//   | |
//   | `boxed_f` dropped here while still borrowed
//   | borrow might be used here, when `boxed_f` is dropped and
//     runs the destructor for type `Box<dyn Fn(&u8)>`

type F<'a> = Box<dyn Fn(&'a u8)>;
fn test<'a>(f: &'a F<'a>) -> &'a F<'a> {
    f
}

对比 run2

fn main() {
    let boxed_f = Box::new(|_| {}) as F;
    let _f = run2(&boxed_f);
}
//   let boxed_f: F<'1> = ...; // '1 是多长呢?
//   let _f;
//   {
//      _f = test(&boxed_f); // &'2 F<'1>,'1 严格比 '2 更长
//   }
//   _f: dangling
// } main block 结束前 drop boxed_f,dyn Trait 具有 drop glue,
// 也就是说需要 drop F<'1>,从而必须要求 '1 存活,'1 可以(被编译器推断而)存活到这里

type F<'a> = Box<dyn Fn(&'a u8)>;
fn run2<'a, 'b>(f: &'b F<'a>) -> &'b F<'a> {
    f
}
作者 zhylmzr 2023-02-19 13:46

根据这个提供的帖子,除了 Copy bound 外的其他所有 trait object 编译器都会生成 drop glue,这就导致了如果传入可变引用和返回的 trait object 生命周期相同,那么会导致可变引用一直占用了,这个帖子里的解释我能理解。

回到我的例子中,trait obejct 仅在new的时候构建了,后续并没有去调用它,退一步说哪怕我后续调用了这个函数也不应该有问题,因为它接受的是不可变引用不会独占。

--
👇
苦瓜小仔: 生命周期的帖子没什么人看,也没什么人评论。懒得解释地那么清楚了。

它不应该和省略生命周期效果一样的吗?

不一样。fn run2(&self) {} 的脱糖是 fn run2<'b>(&'b self) {},所以 receiver type 为 &'b Foo<'a> 并且 'a: 'b。而 run1 的 receiver 为 &'a Foo<'a>

换成Box就不会报错了?

首先,你得知道 fn 是什么,fn 和 Fn 的区别是什么。去翻标准库文档、reference 吧,链接太多了,懒得贴。我自己的总结 在这

其次,编译器报错是三段式的描述,反映了典型的 NLL,如果你的确想知道它,去翻它的 RFC,我翻译过。记住最重要的一个基本观点,生命周期实质上是约束。

最后,你对 trait object 了解多少?具体来说: dyn Fn(&'a Foo) + 'b 中 'a 和 'b 约束了什么?你知道 trait object 保守地被认为具有 drop glue 吗? 你对 drop 有多少了解?

要明白你的例子为什么这样,为什么不能这样,上面提到的东西都得熟悉。

如果你把 Box<fn> 换成 fn,去看不同情况下的 MIR,会发现 fn 的情况下不需要 drop,而 Box<fn> 的情况与 Box<dyn ...> 一样需要 drop,而 drop 时允不允许生命周期悬空,就是这里症结所在。

苦瓜小仔 2023-02-19 11:42

我还是没理解为什么明确指定函数参数的生命周期和实例一致时会报错

你知道 trait object 保守地被认为具有 drop glue 吗?

drop 时允不允许生命周期悬空

作者 zhylmzr 2023-02-19 11:34

问题一我理解了,省略时的生命周期是按照调用来推断的。 其实把func的声明中函数的参数生命周期省略和用for<'b>是一样的。但是我还是没理解为什么明确指定函数参数的生命周期和实例一致时会报错。

playground

--
👇
Hello World: > 调用run时,为什么标注了'a会报错,它不应该和省略生命周期效果一样的吗?

肯定是不一样的,注意到 'a 是标注在 Foo 上的,与 run2 等价的应该是

fn run<'b>(&'b self) {}

之前已经有人提过类似的,你可以看这里

为什么把func的定义换成Box就不会报错了?

应该是用了 dyn object 导致意外延长了生命期,可以用 HRTB 修改:

struct Foo<'a> {
    func: Box<dyn for<'b> Fn(&'b Foo)>,
    // func: Box<fn(&'a Foo)>,
}
苦瓜小仔 2023-02-19 11:13

生命周期的帖子没什么人看,也没什么人评论。懒得解释地那么清楚了。

它不应该和省略生命周期效果一样的吗?

不一样。fn run2(&self) {} 的脱糖是 fn run2<'b>(&'b self) {},所以 receiver type 为 &'b Foo<'a> 并且 'a: 'b。而 run1 的 receiver 为 &'a Foo<'a>

换成Box就不会报错了?

首先,你得知道 fn 是什么,fn 和 Fn 的区别是什么。去翻标准库文档、reference 吧,链接太多了,懒得贴。我自己的总结 在这

其次,编译器报错是三段式的描述,反映了典型的 NLL,如果你的确想知道它,去翻它的 RFC,我翻译过。记住最重要的一个基本观点,生命周期实质上是约束。

最后,你对 trait object 了解多少?具体来说: dyn Fn(&'a Foo) + 'b 中 'a 和 'b 约束了什么?你知道 trait object 保守地被认为具有 drop glue 吗? 你对 drop 有多少了解?

要明白你的例子为什么这样,为什么不能这样,上面提到的东西都得熟悉。

如果你把 Box<fn> 换成 fn,去看不同情况下的 MIR,会发现 fn 的情况下不需要 drop,而 Box<fn> 的情况与 Box<dyn ...> 一样需要 drop,而 drop 时允不允许生命周期悬空,就是这里症结所在。

Hello World 2023-02-19 10:53

调用run时,为什么标注了'a会报错,它不应该和省略生命周期效果一样的吗?

肯定是不一样的,注意到 'a 是标注在 Foo 上的,与 run2 等价的应该是

fn run<'b>(&'b self) {}

之前已经有人提过类似的,你可以看这里

为什么把func的定义换成Box就不会报错了?

应该是用了 dyn object 导致意外延长了生命期,可以用 HRTB 修改:

struct Foo<'a> {
    func: Box<dyn for<'b> Fn(&'b Foo)>,
    // func: Box<fn(&'a Foo)>,
}
1 共 11 条评论, 1 页