[英]Cannot infer an appropriate lifetime for a closure that returns a reference
考慮以下代碼:
fn foo<'a, T: 'a>(t: T) -> Box<Fn() -> &'a T + 'a> {
Box::new(move || &t)
}
我的期望:
'a
。t
與T
一樣長。t
移動到閉包,所以閉包只要t
t
的引用。 所以只要閉包存在,引用就是有效的。實際發生的情況:
error[E0495]: cannot infer an appropriate lifetime for borrow expression due to conflicting requirements
--> src/lib.rs:2:22
|
2 | Box::new(move || &t)
| ^^
|
note: first, the lifetime cannot outlive the lifetime as defined on the body at 2:14...
--> src/lib.rs:2:14
|
2 | Box::new(move || &t)
| ^^^^^^^^^^
note: ...so that closure can access `t`
--> src/lib.rs:2:22
|
2 | Box::new(move || &t)
| ^^
note: but, the lifetime must be valid for the lifetime 'a as defined on the function body at 1:8...
--> src/lib.rs:1:8
|
1 | fn foo<'a, T: 'a>(t: T) -> Box<Fn() -> &'a T + 'a> {
| ^^
= note: ...so that the expression is assignable:
expected std::boxed::Box<(dyn std::ops::Fn() -> &'a T + 'a)>
found std::boxed::Box<dyn std::ops::Fn() -> &T>
我不明白沖突。 我該如何解決?
非常有趣的問題! 我想我理解了這里的問題。 讓我試着解釋一下。
tl;dr :閉包不能返回對通過移動捕獲的值的引用,因為那將是對self
的引用。 不能返回這樣的引用,因為Fn*
特性不允許我們表達。 這與流迭代器問題基本相同,可以通過 GAT(通用關聯類型)修復。
您可能知道,當您編寫閉包時,編譯器將為適當的Fn
特征生成 struct 和impl
塊,因此閉包基本上是語法糖。 讓我們盡量避免所有這些糖並手動構建您的類型。
您想要的是一種擁有另一種類型並且可以返回對該擁有類型的引用的類型。 並且您想要一個函數來返回該類型的盒裝實例。
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
。
我想用這段代碼表達的一點是:我們不需要在Baz
類型上注釋任何生命周期來確保安全; Rust 自己解決了這個問題,並強制baz
壽命不超過s
。 這在下面很重要。
到目前為止,我們只介紹了基礎知識。 讓我們試着寫一個像Fn
這樣的特征來更接近你的原始問題:
trait MyFn {
type Output;
fn call(&self) -> Self::Output;
}
在我們的 trait 中,沒有函數參數,但除此之外,它與真正的Fn
trait相當。
讓我們實施它!
impl<T> MyFn for Baz<T> {
type Output = ???;
fn call(&self) -> Self::Output {
&self.0
}
}
現在我們有一個問題:我們寫什么而不是???
? 天真地有人會寫&T
... 但我們需要一個生命周期參數作為該引用。 我們從哪里得到一個? 返回值甚至有什么生命周期?
讓我們檢查一下我們之前實現的函數:
impl<T> Baz<T> {
fn call(&self) -> &T {
&self.0
}
}
所以這里我們也使用沒有生命周期參數的&T
。 但這僅適用於終身省略。 基本上,編譯器填充空白,以便fn call(&self) -> &T
等效於:
fn call<'s>(&'s self) -> &'s T
啊哈,所以返回引用的生命周期與self
生命周期綁定! (更有經驗的 Rust 用戶可能已經感覺到這是怎么回事......)。
(附帶說明:為什么返回的引用不依賴於T
本身的生命周期?如果T
引用了一些非'static
東西,那么這必須被考慮在內,對吧?是的,但它已經被考慮了!記住沒有Baz<T>
實例可以永遠比T
引用的事物活得更長。所以self
生命周期已經比T
可能擁有的任何生命周期都短。因此我們只需要專注於self
生命周期)
但是我們如何在 trait impl 中表達它呢? 事實證明:我們不能(還)。 這個問題經常在流迭代器的上下文中提到——也就是說,迭代器返回一個生命周期綁定到self
生命周期的項目。 在今天的 Rust 中,遺憾的是不可能實現這一點; 類型系統不夠強大。
幸運的是,前段時間合並了一個RFC“通用關聯類型” 。 這個 RFC 擴展了 Rust 類型系統,允許關聯的特征類型是通用的(在其他類型和生命周期上)。
讓我們看看我們如何讓您的示例(有點)與 GAT 一起工作(根據 RFC;這些東西還沒有工作☹)。 首先,我們必須更改特征定義:
trait MyFn {
type Output<'a>; // <-- we added <'a> to make it generic
fn call(&self) -> Self::Output;
}
代碼中的函數簽名沒有改變,但請注意生命周期省略開始了! 上面的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
我們需要這樣寫(根據RFC 的這一部分:
fn make_baz<T>(t: T) -> Box<for<'a> MyFn<Output<'a> = &'a T>> {
Box::new(Baz(t))
}
如果我們想使用真正的Fn
特征怎么辦? 據我所知,我們不能,即使有 GAT。 我認為以向后兼容的方式改變現有的Fn
特征來使用 GAT 是不可能的。 因此,標准庫很可能會保持功能較弱的特性。 (旁注:如何以向后不兼容的方式發展標准庫以使用新的語言特性是我已經想過幾次的事情;到目前為止,我還沒有聽說過任何這方面的真正計划;我希望 Rust 團隊來有事……)
你想要的在技術上不是不可能或不安全的(我們將它實現為一個簡單的結構並且它可以工作)。 然而,不幸的是,現在不可能在 Rust 的類型系統中以閉包/ Fn
特征的形式表達你想要的。 這與流迭代器正在處理的問題相同。
使用計划中的 GAT 功能,可以在類型系統中表達所有這些。 但是,標准庫需要以某種方式迎頭趕上,以使您的確切代碼成為可能。
我的期望:
- 類型
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
特質,而FnMut
特質又隱含了FnOnce
特質。 也就是說,Rust 中的每個函數對象都可以使用按值self
參數調用。 這意味着每個函數對象都必須仍然有效,使用按值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
}
}
這就是為什么您嘗試做的事情從根本上是不可能的。 退后一步,想想你實際上想要用這個閉包完成什么,並找到其他方法來完成它。
您希望類型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)
}
其他答案是一流的,但我想補充一下您的原始代碼無法工作的另一個原因。 一個大問題在於簽名:
fn foo<'a, T: 'a>(t: T) -> Box<Fn() -> &'a T + 'a>
這表示調用者可以在調用foo
時指定任何生命周期,並且代碼將有效且內存安全。 對於這段代碼,這不可能是真的。 用'a
set to 'static
調用它是沒有意義的,但是這個簽名不會阻止它。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.