考虑以下代码:
# Map static resources from the /public folder to the /assets URL path
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
GET /assets/ mypackage.AssetController.getData(name: Option[String])
GET /assets/:id mypackage.AssetController.getDataId(id: Long)
我的期望:
fn foo<'a, T: 'a>(t: T) -> Box<Fn() -> &'a T + 'a> {
Box::new(move || &t)
}
。'a
的有效期为t
。T
移动到闭包,因此只要t
t
的引用,该引用已移至闭包。因此,只要闭包存在,引用就是有效的。实际发生的事情:
t
我不明白这场冲突。我该如何解决?
答案 0 :(得分:14)
非常有趣的问题!我想我理解这里的问题。让我试着解释一下。
tl; dr :move
闭包不能返回对其环境的引用,因为它会引用self
。这样的引用无法返回,因为Fn*
特征不允许我们表达。这与streaming iterator problem基本相同,可以通过GAT修复(通用关联)类型)。
正如您可能知道的,当您编写闭包时,编译器将为适当的impl
特性生成struct和Fn
块,因此闭包基本上是语法糖。让我们尝试避免所有糖并手动构建您的类型。
你想要的是拥有另一种类型的类型,并且可以返回对该拥有类型的引用。并且您希望拥有一个返回所述类型的盒装实例的函数。
struct Baz<T>(T);
impl<T> Baz<T> {
fn call(&self) -> &T {
&self.0
}
}
fn make_baz<T>(t: T) -> Box<Baz<T>> {
Box::new(Baz(t))
}
这相当于你的盒装封口。让我们尝试使用它:
let outside = {
let s = "hi".to_string();
let baz = make_baz(s);
println!("{}", baz.call()); // works
baz
};
println!("{}", outside.call()); // works too
这很好用。字符串s
已移至Baz
类型,Baz
实例将移至Box
。 s
现在由baz
拥有,然后由outside
拥有。
当我们添加单个字符时,它会变得更有趣:
let outside = {
let s = "hi".to_string();
let baz = make_baz(&s); // <-- NOW BORROWED!
println!("{}", baz.call()); // works
baz
};
println!("{}", outside.call()); // doesn't work!
现在我们无法使baz
的生命周期大于s
的生命周期,因为baz
包含对s
的引用,s
将是baz
的悬空引用1}}会超出Baz
之前的范围。
我想用这个片段做点:我们不需要在类型baz
上注释任何生命周期以使其安全; Rust自己想出来并强制s
的生命不超过Fn
。这在下面很重要。
到目前为止,我们只介绍了基础知识。让我们尝试写一个像trait MyFn {
type Output;
fn call(&self) -> Self::Output;
}
这样的特质来更接近你原来的问题:
impl<T> MyFn for Baz<T> {
type Output = ???;
fn call(&self) -> Self::Output {
&self.0
}
}
在我们的特质中,没有函数参数,但它与the real Fn
trait完全相同。
让我们实现它!
???
现在我们遇到了一个问题:我们写的是什么而不是&T
?天真地会写impl<T> Baz<T> {
fn call(&self) -> &T {
&self.0
}
}
...但我们需要一个生命周期参数用于该参考。我们从哪里得到一个?返回值甚至有几年的寿命?
让我们检查之前实施的功能:
&T
所以我们在这里使用fn call(&self) -> &T
没有生命周期参数。但这仅仅是因为终身省略。基本上,编译器填充空白,以便fn call<'s>(&'s self) -> &'s T
等同于:
self
啊哈,所以返回引用的生命周期绑定到T
生命周期! (更有经验的Rust用户可能已经有了这样的感觉......)。
(作为旁注:为什么返回的引用不依赖于T
本身的生命周期?如果'static
引用了非Baz<T>
的内容,那么必须考虑到这一点,对吗?是的,但它已经被考虑了!请记住,T
的任何实例都不能比self
可能引用的东西长寿。所以T
的生命周期已经比生命周期短self
可能有。因此我们只需要专注于self
生命周期)
但是我们如何在特质impl中表达呢?事实证明:我们不能(还)。在流迭代器的上下文中经常提到此问题 - 也就是说,返回一个生命周期为trait MyFn {
type Output<'a>; // <-- we added <'a> to make it generic
fn call(&self) -> Self::Output;
}
生命周期的项的迭代器。在今天的Rust中,很难实现这一点;类型系统不够强大。
幸运的是,有一段RFC "Generic Associated Types"已经合并了一段时间。此RFC扩展了Rust类型系统,以允许相关类型的特征是通用的(超过其他类型和生命周期)。
让我们看看我们如何让你的例子(有点)与GAT一起工作(根据RFC;这些东西还没有工作☹)。首先,我们必须改变特征定义:
fn call(&self) -> Self::Output
代码中的功能签名没有改变,但请注意终身省略!以上fn call<'s>(&'s self) -> Self::Output<'s>
相当于:
self
因此,关联类型的生命周期与impl
生命周期绑定。就像我们想要的那样! impl<T> MyFn for Baz<T> {
type Output<'a> = &'a T;
fn call(&self) -> Self::Output {
&self.0
}
}
看起来像这样:
MyFn
要退回装箱fn make_baz<T>(t: T) -> Box<for<'a> MyFn<Output<'a> = &'a T>> {
Box::new(Baz(t))
}
,我们需要写一下(根据this section of the RFC:
Fn
如果我们想使用真正的 Fn
特征怎么办?据我所知,即使有GAT,我们也无法做到。我认为改变现有Fn
特征以向后兼容的方式使用GAT是不可能的。因此,标准库可能会保持不那么强大的特性。 (旁注:如何以向后不兼容的方式发展标准库以使用新的语言功能已经有几次I wondered about了;到目前为止,我还没有听说过这方面的任何真实计划;我希望Rust团队想出了一些东西......)
你想要的不是技术上不可能或不安全的(我们把它实现为一个简单的结构,它可以工作)。但是,遗憾的是,现在无法以Rust的类型系统中的闭包/ {{1}}特征的形式表达您想要的内容。这是流迭代器正在处理的问题。
通过计划的GAT功能,可以在类型系统中表达所有这些。但是,标准库需要以某种方式赶上来使您的确切代码成为可能。
答案 1 :(得分:8)
我的期望:
- 类型
T
的有效期为'a
。- 值
t
的有效期为T
。
这没有任何意义。值不能作为类型“活得很长”,因为类型不能存活。 “T
有生命'a
”是一个非常不精确的陈述,很容易被误解。 T: 'a
的真正含义是“T
的实例必须至少与有效期'a
保持有效。例如,T不得为生命周期短于'a
的引用或者包含这样一个引用的结构。请注意,这与将引用转换为 T
无关,即&T
。
然后,值t
只要它的词法范围(它是一个函数参数)就会存在,它与'a
完全无关。
- ,闭包就会生效
t
移动到闭包,因此只要t
这也是不正确的。只要封闭在词汇上,封闭就会存在。它在结果表达式中是临时的,因此一直存在到结果表达式的末尾。 t
的生命关系根本不涉及闭包,因为它内部有T
变量,t
的捕获。由于捕获是t
的复制/移动,因此它不受t
生命周期的任何影响。
然后将临时闭包移动到盒子的存储器中,但这是一个具有自己生命周期的新对象。 闭包的生命周期绑定到框的生命周期,即它是函数的返回值,稍后(如果将函数存储在函数外),存储的任何变量的生命周期中的方框。
所有这些意味着返回对其自己的捕获状态的引用的闭包必须将该引用的生命周期绑定到它自己的引用。不幸的是,这是不可能的。
原因如下:
Fn
特征意味着FnMut
特征,而特征又暗示FnOnce
特征。也就是说,可以使用by-value self
参数调用 Rust中的每个函数对象。这意味着每个函数对象必须仍然有效,并使用by-value self
参数调用并返回相同的内容。
换句话说,尝试编写一个返回对其自己的捕获的引用的闭包扩展到大致这个代码:
struct Closure<T> {
captured: T,
}
impl<T> FnOnce<()> for Closure<T> {
type Output = &'??? T; // what do I put as lifetime here?
fn call_once(self, _: ()) -> Self::Output {
&self.captured // returning reference to local variable
// no matter what, the reference would be invalid once we return
}
}
这就是为什么你要做的事情从根本上是不可能的。退后一步,想一下你用这个闭包实际想要完成的事情,并找到其他方法来实现它。
答案 2 :(得分:1)
您希望类型T
具有生命周期'a
,但t
不是对类型T
的值的引用。该函数通过参数传递来获取变量t
的所有权:
// t is moved here, t lifetime is the scope of the function
fn foo<'a, T: 'a>(t: T)
你应该这样做:
fn foo<'a, T: 'a>(t: &'a T) -> Box<Fn() -> &'a T + 'a> {
Box::new(move || t)
}
答案 3 :(得分:1)
其他答案都是一流的,但我想提出原始代码无法工作的其他原因。一个大问题在于签名:
fn foo<'a, T: 'a>(t: T) -> Box<Fn() -> &'a T + 'a>
这表示调用者可以在调用foo
时指定任何生命周期,并且代码有效并且内存安全。对于这段代码来说,这可能不是真的。将'a
设置为'static
来调用它是没有意义的,但是这个签名的任何内容都不会阻止它。