簡體   English   中英

無法為返回引用的閉包推斷合適的生命周期

[英]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)
}

我的期望:

  • 類型 T 的生命周期為'a
  • tT一樣長。
  • 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
  • tT一樣長。

這沒有任何意義。 值不能像類型一樣“活得一樣長”,因為類型不存在。 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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM