[软件设计/软件工程] 无法为返回引用的闭包推断正确的生命周期

[复制链接]
发表于 2022-5-4 13:27:18
问题
考虑以下代码:
  1. fn foo<'a, T: 'a>(t: T) -> Box<Fn() -> &'a T + 'a> {
  2.     Box::new(move || &t)
  3. }
复制代码

我期望的是:

&amp;#39;一个











实际发生了什么:
  1. error[E0495]: cannot infer an appropriate lifetime for borrow expression due to conflicting requirements
  2. --> src/lib.rs:2:22
  3.   |
  4. 2 |     Box::new(move || &t)
  5.   |                      ^^
  6.   |
  7. note: first, the lifetime cannot outlive the lifetime  as defined on the body at 2:14...
  8. --> src/lib.rs:2:14
  9.   |
  10. 2 |     Box::new(move || &t)
  11.   |              ^^^^^^^^^^
  12. note: ...so that closure can access `t`
  13. --> src/lib.rs:2:22
  14.   |
  15. 2 |     Box::new(move || &t)
  16.   |                      ^^
  17. note: but, the lifetime must be valid for the lifetime 'a as defined on the function body at 1:8...
  18. --> src/lib.rs:1:8
  19.   |
  20. 1 | fn foo<'a, T: 'a>(t: T) -> Box<Fn() -> &'a T + 'a> {
  21.   |        ^^
  22.   = note: ...so that the expression is assignable:
  23.           expected std::boxed::Box<(dyn std::ops::Fn() -> &'a T + 'a)>
  24.              found std::boxed::Box<dyn std::ops::Fn() -> &T>
复制代码

我不明白这种冲突。我该如何解决?

回答
非常有趣的问题!我想我理解这里的问题。让我解释。

tl; dr:闭包不能返回对移动捕获值的引用,因为那将是对 self 的引用。这样的引用不能返回,因为 Fn* 特性不允许我们表达它。这与流迭代器基本上是相同的问题,可以使用 GAT(通用关联类型)修复。
  1. impl
  2. blocks for the appropriate
  3. Fn traits, so closures are basically syntax sugar. Let's try to avoid all that sugar and build your type manually.What you want is a type which owns another type and can return references to that owned type. And you want to have a function which returns a boxed instance of said type. struct Baz<T>(T);

  4. impl<T> Baz<T> {
  5.     fn call(&self) -> &T {
  6.         &self.0
  7.     }
  8. }

  9. fn make_baz<T>(t: T) -> Box<Baz<T>> {
  10.     Box::new(Baz(t))
  11. }
  12. This is pretty equivalent to your boxed closure. Let's try to use it:let outside = {
  13.     let s = "hi".to_string();
  14.     let baz = make_baz(s);
  15.     println!("{}", baz.call()); // works

  16.     baz
  17. };

  18. println!("{}", outside.call()); // works too
  19. This works just fine. The string s is moved into the Baz type and that Baz instance is moved into the Box. s is now owned by baz and then by outside.It gets more interesting when we add a single character:let outside = {
  20.     let s = "hi".to_string();
  21.     let baz = make_baz(&s);  // <-- NOW BORROWED!
  22.     println!("{}", baz.call()); // works

  23.     baz
  24. };

  25. println!("{}", outside.call()); // doesn't work!
  26. Now we cannot make the lifetime of baz bigger than the lifetime of s, since baz contains a reference to s which would be an dangling reference of s would go out of scope earlier than baz. The point I wanted to make with this snippet: we didn't need to annotate any lifetimes on the type Baz to make this safe; Rust figured it out on its own and enforces that baz lives no longer than s. This will be important below.Writing a trait for itSo far we only covered the basics. Let's try to write a trait like Fn to get closer to your original problem:trait MyFn {
  27.     type Output;
  28.     fn call(&self) -> Self::Output;
  29. }
  30. In our trait, there are no function parameters, but otherwise it's fairly identical to the real Fn trait.Let's implement it!impl<T> MyFn for Baz<T> {
  31.     type Output = ???;
  32.     fn call(&self) -> Self::Output {
  33.         &self.0
  34.     }
  35. }
  36. Now we have a problem: what do we write instead of ???? Naively one would write &T... but we need a lifetime parameter for that reference. Where do we get one? What lifetime does the return value even have? Let's check the function we implemented before:impl<T> Baz<T> {
  37.     fn call(&self) -> &T {
  38.         &self.0
  39.     }
  40. }
  41. So here we use &T without lifetime parameter too. But this only works because of lifetime elision. Basically, the compiler fills in the blanks so that fn call(&self) -> &T is equivalent to:fn call<'s>(&'s self) -> &'s T
  42. Aha, so the lifetime of the returned reference is bound to the self lifetime! (more experienced Rust users might already have a feeling where this is going...). (As a side note: why is the returned reference not dependent on the lifetime of T itself? If T references something non-'static then this has to be accounted for, right? Yes, but it is already accounted for! Remember that no instance of Baz<T> can ever live longer than the thing T might reference. So the self lifetime is already shorter than whatever lifetime T might have. Thus we only need to concentrate on the self lifetime)But how do we express that in the trait impl? Turns out: we can't (yet). This problem is regularly mentioned in the context of streaming iterators -- that is, iterators that return an item with a lifetime bound to the self lifetime. In today's Rust, it is sadly impossible to implement this; the type system is not strong enough. What about the future?Luckily, there is an RFC "Generic Associated Types" which was merged some time ago. This RFC extends the Rust type system to allow associated types of traits to be generic (over other types and lifetimes).Let's see how we can make your example (kinda) work with GATs (according to the RFC; this stuff doesn't work yet ?). First we have to change the trait definition:trait MyFn {
  43.     type Output<'a>;   // <-- we added <'a> to make it generic
  44.     fn call(&self) -> Self::Output;
  45. }
  46. The function signature hasn't changed in the code, but notice that lifetime elision kicks in! The above fn call(&self) -> Self::Output is equivalent to:fn call<'s>(&'s self) -> Self::Output<'s>
  47. So the lifetime of the associated type is bound to the self lifetime. Just as we wanted! The impl looks like this:impl<T> MyFn for Baz<T> {
  48.     type Output<'a> = &'a T;
  49.     fn call(&self) -> Self::Output {
  50.         &self.0
  51.     }
  52. }
  53. To return a boxed MyFn we would need to write this (according to this section of the RFC:fn make_baz<T>(t: T) -> Box<for<'a> MyFn<Output<'a> = &'a T>> {
  54.     Box::new(Baz(t))
  55. }
  56. And what if we want to use the realFn trait? As far as I understand, we can't, even with GATs. I think it's impossible to change the existing Fn trait to use GATs in a backwards compatible manner. So it's likely that the standard library will keep the less powerful trait as is. (side note: how to evolve the standard library in backwards incompatible ways to use new language features is something I wondered about a few times already; so far I haven't heard of any real plan in this regards; I hope the Rust team comes up with something...)SummaryWhat you want is not technically impossible or unsafe (we implemented it as a simple struct and it works). However, unfortunately it is impossible to express what you want in the form of closures/Fn traits in Rust's type system right now. This is the same problem streaming iterators are dealing with. With the planned GAT feature, it is possible to express all of this in the type system. However, the standard library would need to catch up somehow to make your exact code possible.
复制代码






上一篇:Eclipse/Java 代码完成不起作用
下一篇:在 Swift 中获取 double 的小数部分

使用道具 举报

Archiver|手机版|小黑屋|吾爱开源 |网站地图

Copyright 2011 - 2012 Lnqq.NET.All Rights Reserved( ICP备案粤ICP备14042591号-1粤ICP14042591号 )

关于本站 - 版权申明 - 侵删联系 - Ln Studio! - 广告联系

本站资源来自互联网,仅供用户测试使用,相关版权归原作者所有

快速回复 返回顶部 返回列表