メモ: 匿名ライフタイム
匿名ライフタイムを理解するにあたって重要なのは、Edition 2018 で導入された
- '_, the anonymous lifetime - The Edition Guide
-
Lifetime elision in impl - The Edition Guide
- Trait を実装するときの impl で メソッド実装で不要な場合はライフタイム宣言を省略できる
このふたつと、ライフタイムの省略ルールが重要。
さらに、表現としては、「'_
プレースホルダによって、ライフタイムの省略ルールに従った匿名ライフタイムが割り当てられる」が正確かも。これには以下の効果がある。
- ライフタイムが省略されていることを明示する
- 明示的にライフタイムを宣言しなくてもライフタイムを指定できる
- Trait object に通常のライフタイム省略ルールを適用する
- impl Trait に通常のライフタイム省略ルールを適用する?
- まだ良くわからない
記事として公開しました。
そのうち記事にする
そもそもの話として、参照を持つ struct や enum のライフタイムが省略されていると、何かを借用していることが不明瞭という問題があった。
struct Foo<'a> {
x: &'a u32
}
fn foo(x: &u32) -> Foo { // Foo が x を借用することが分からない
}
そのため、Edition 2018 からはライフタイムパラメータの省略が非推奨となり、Lint でも elided-lifetimes-in-paths
ルールで対応されている(ただし、デフォルトでは allow)
なお、上記の Lint を含む Edition 2018 のイディオムは rust-2018-idioms
グループに含まれている。
#![deny(elided_lifetimes_in_paths)]
struct Foo<'a> {
x: &'a u32
}
fn foo(x: &u32) -> Foo {
Foo { x }
}
これをコンパイルすると、
error: hidden lifetime parameters in types are deprecated
--> src/main.rs:6:20
|
6 | fn foo(x: &u32) -> Foo {
| ^^^- help: indicate the anonymous lifetime: `<'_>`
|
note: the lint level is defined here
--> src/main.rs:1:9
|
1 | #![deny(elided_lifetimes_in_paths)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^
では、上記のエラーを直すことを考える。Edition 2018 以前では、明示的にライフタイムを指定するしか無かった。
fn foo<'a>(x: &'a u32) -> Foo<'a> {
Foo { x }
}
ここでライフタイムの省略ルールを適用すると、以下のルールに従ってライフタイムを省略できるのであった。
- 引数にはそれぞれ新しいライフタイムが割り当てられる。
- 引数に たったひとつのライフタイムしかない場合 は、そのライフタイムがすべての返り値にも割り当てられる。
なので、以下のプログラムは明示的にライフタイムを指定したバージョンと同じになる。
fn foo(x: &u32) -> Foo {
Foo { x }
}
しかし、非参照型へのライフタイムパラメータの省略は非推奨にしたいので、ライフタイムが省略されていることを '_
プレースホルダで明示することができるようになった。
fn foo(x: &u32) -> Foo<'_> {
Foo { x }
}
さらに '_
プレースホルダはライフタイムパラメータを指定する箇所であれば関数以外でも使える。
- 入力コンテキストでは、それぞれの
'_
に新しいライフタイムが割り当てられる。 - 出力コンテキストでは、それぞれの
'_
に、すべての出力に割り当てられたライフタイムひとつが割り当てられる。
なので、impl
でも使うことができる。今までは
impl<'a> Foo<'a> {...}
のように、まず、'a
を宣言してから Foo に 'a
を与えていたのを、
impl Foo<'_> {...}
のように書くことができる。ただし、注意してほしいのは、ここで与えたライフタイムを impl
内の関数で参照することはできない、ということ。
たとえば、構造体 Foo の impl を匿名ライフタイムを使って以下のように定義する。
struct Foo<'a> {
a: &'a i32,
}
impl Foo<'_> {
fn a(&self) -> &'_ i32 {
self.a
}
}
これは一見、正しく動くように見える。
fn main() {
let a = 35;
let x: &i32;
let foo = Foo { a: &a };
x = foo.a();
println!("x = {}", x); // => 35
}
しかし、新しいスコープを導入して、以下のように書くとうまくいかない。
fn main() {
let a = 35;
let x: &i32;
{
let foo = Foo { a: &a };
x = foo.a();
}
println!("x = {}", x);
}
結果
Compiling playground v0.0.1 (/playground)
error[E0597]: `foo` does not live long enough
--> src/main.rs:17:13
|
17 | x = foo.a();
| ^^^ borrowed value does not live long enough
18 | }
| - `foo` dropped here while still borrowed
19 |
20 | println!("x = {}", x);
| - borrow later used here
しかし、変数 a
は println!(...)
の時点まで生きているはずだ。
struct Foo<'a> {
a: &'a i32,
}
impl<'a> Foo<'a> {
fn a(&self) -> &'a i32 {
self.a
}
}
fn main() {
let a = 35;
let x: &i32;
{
let foo = Foo { a: &a };
x = foo.a();
}
println!("x = {}", x); // 35
}
そもそも Trait object と impl Trait は異なるので、さらに調査必要
- Implicit bounds (https://www.ncameron.org/blog/dyn-trait-and-impl-trait-in-rust/#implicit-bounds)
- Can someone explain the lifetime rule with impl Trait in this example to me? - help - The Rust Programming Language Forum
- 基本的な使い方 Traits: Defining Shared Behavior - The Rust Programming Language
- Reference には今のところあまり情報はない Impl trait type - The Rust Reference
- Edition 2018 での解説。引数と返り値で意味が異なる。 impl Trait for returning complex types with ease - The Edition Guide
- 1522-conservative-impl-trait - The Rust RFC Book impl Trait 導入
- 1951-expand-impl-trait - The Rust RFC Book impl Trait でのライフタイムパラメータなど
Trait object における '_
メソッドから impl Trait
を返すときも '_
プレースホルダが必要になるが、これには Default trait object lifetimes が関係していそうだ。
struct Foo {
items: Vec<i32>,
}
impl Foo {
fn items(&self) -> impl Iterator<Item = i32> + '_ {
self.items.iter().copied()
}
}
fn main() {
let foo = Foo {
items: vec![1, 2, 3],
};
for x in foo.items() {
println!("x = {}", x);
}
}
items()
メソッドの返り値から '_
プレースホルダーを取り除いてみる。
fn items(&self) -> impl Iterator<Item = i32> {
self.items.iter().copied()
}
これをコンパイルすると以下のエラーになる。
Compiling playground v0.0.1 (/playground)
error[E0759]: `self` has an anonymous lifetime `'_` but it needs to satisfy a `'static` lifetime requirement
--> src/main.rs:7:20
|
6 | fn items(&self) -> impl Iterator<Item = i32> {
| ----- this data with an anonymous lifetime `'_`...
7 | self.items.iter().copied()
| ---------- ^^^^
| |
| ...is captured here...
|
note: ...and is required to live as long as `'static` here
--> src/main.rs:6:24
|
6 | fn items(&self) -> impl Iterator<Item = i32> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^
help: to declare that the `impl Trait` captures data from argument `self`, you can add an explicit `'_` lifetime bound
|
6 | fn items(&self) -> impl Iterator<Item = i32> + '_ {
| ^^^^
error: aborting due to previous error
「self
の省略された匿名ライフタイムが 'static
と互換でなければいけない」と怒られてしまった。しかし、ライフタイムの省略ルールによれば、
- メソッドのレシーバが
&Self
または&mut Self
の場合、返り値の省略されたライフタイムにも同じライフタイムが割り当てられる
のではないだろうか?
ここで Default trait object lifetimes が関係してくる。実は、Trait object については、ライフタイムの省略ルールが異なっているのだ。詳細はリンク先を読んでもらうとして、Trait object にライフタイムが一切含まれない場合は 'static
が割り当てられる。ただし、'_
プレースホルダが使われた場合は、通常の省略ルールに従う。
item() メソッドの返り値のライフタイムは、明らかに 'static
ではないので、ここでは '_
が必要になる、というわけだ。
Rust のライフタイムのテストケース
use std::fmt::Debug;
fn any_lifetime<'a>() -> &'a u32 { &5 }
fn static_lifetime() -> &'static u32 { &5 }
fn any_lifetime_as_static_impl_trait() -> impl Debug {
any_lifetime()
}
fn lifetimes_as_static_impl_trait() -> impl Debug {
static_lifetime()
}
fn no_params_or_lifetimes_is_static() -> impl Debug + 'static {
lifetimes_as_static_impl_trait()
}
fn static_input_type_is_static<T: Debug + 'static>(x: T) -> impl Debug + 'static { x }
fn type_outlives_reference_lifetime<'a, T: Debug>(x: &'a T) -> impl Debug + 'a { x }
fn type_outlives_reference_lifetime_elided<T: Debug>(x: &T) -> impl Debug + '_ { x }
Type parameter から引き継ぐ。なければ 'static
で良さそう
以下の構造体がある時
struct Foo<'a> {
items: Vec<&'a i32>
}
これは ok。たぶん、ライフタイムは '_
impl<'a> Foo<'a> {
fn items(&self) -> impl IntoIterator<Item=&i32> {
self.items.iter().copied()
}
}
だめ。これもダメ。たぶん、ライフタイムは 'static
struct Foo<'a> {
items: Vec<&'a i32>,
}
impl<'a> Foo<'a> {
fn items(&self) -> impl IntoIterator<Item = i32> {
self.items.iter().copied().map(|x| *x)
}
}
これもダメ。たぶん、ライフタイムは 'a
impl<'a> Foo<'a> {
fn items(&self) -> impl IntoIterator<Item=&'a i32> {
self.items.iter().copied()
}
}
clippy が警告を表示していくれる
error: explicit lifetimes given in parameter types where they could be elided (or replaced with `'_` if needed by type declaration)
--> src/language_server/description.rs:9:1
|
9 | fn format_type_specifier<'a>(ty: Option<TypeKind<'a>>) -> String {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `-D clippy::needless-lifetimes` implied by `-D warnings`
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_lifetimes
error: aborting due to previous error
'static
ライフタイムについて
-
static
宣言で定数を作成する - 文字列リテラル
&'static str
型を持つ変数を作成する- これらは実行バイナリの一部として格納される。
- プログラムが動作している間、常に有効
これらは 'static
ライフタイム
pub fn downcast_ref<T: Error + 'static>(&self) -> Option<&T> { ... }
これが 'static
ライフタイム境界。
T
内の全ての参照は 'static
ライフタイムよりも長く(つまり同じだけ)生きていなければならない。
dyn Trait と impl Trait
- 引数の位置の impl Trait はトレイト境界のショートハンド
- Trait object:
dyn
Trait- Java のオブジェクトに近い。メソッドは動的ディスパッチされる
- ポインタで渡さなければならない
- Generic な関数ではなく、トレイトを実装したあらゆる値を受け取れる
- 返り値の位置の impl Trait はジェネリクスのショートハンドではない
- ジェネリクスは、呼ぶ側が実際の型を決める
- impl Trait は呼ばれる関数側が決める。関数の本体から推論される
- 呼ぶ側はトレイト境界しか分からない
- ただし、今のところ実際の型は一つのみ
この 3 種類のうちいずれを使うかの判断には、これらがどのように実装しているかを知っていると役に立つ
- ジェネリクスと引数の impl Trait
- monomorphisation (単相型、単相化?)
- それぞれの型インスタンスに対して関数のコピーを作成する。
- たとえば、
fn f(b: impl Bar)
をFoo
とBar
で呼び出しているなら、関数のコピーがふたつできる。 - 一切の間接呼び出しが発生しないのでパフォーマンスは良いが、コードサイズは増える
- 返り値の impl Trait には monomorphisation は必要なく、単に具象型で置き換えられる。
- Trait object
- こちらも monomorphisation は必要ない
- Trait object の型は fat pointer で実装されている
- 参照.
&dyn Bar
は値への参照だけでなく vtable への参照を含む - つまり動的ディスパッチされる
Implicit bounds
-
Send
やSync
のようなトレイトは、型を構成するそれぞれの型がこれらを実装している限り、自動で実装される -
impl Trait
が返り値で使われた場合、これらのトレイトは関数本体から暗黙のうちに推論される - つまり、
+ Send + Sync
をimpl Trait
に対して書く必要はない- Trait object では必要
匿名ライフタイムの記事ができてきたので、記事のメモをここに残しておく
このうち dyn
トレイトが一番古くからある機能ですが、引数の位置の impl
トレイトがもっとも簡単なので、先に説明します。
impl
トレイト
引数の位置の ジェネリクスの型パラメーターにはトレイト境界を指定することができます。
fn println_value<T>(value: T)
where
T: Display,
{
println!("value = {}", value);
}
こう書くこともできます。
fn println_value<T: Display>(value: T) {
println!("value = {}", value);
}
さらに、引数位置の impl
トレイトを使うともっと簡単に書くことができます。
fn println_value(value: impl Display) {
println!("value = {}", value);
}
型パラメータをなくすことができ、より直感的になりましたね。このように、引数位置の impl
トレイトは、型パラメータとトレイト境界のより短い記法です。[^7]
- 引数の位置の impl Trait はジェネリクスのトレイト境界のショートハンド
-
Trait object:
dyn
Trait- Java のオブジェクトに近い。メソッドは動的ディスパッチされる
- ポインタで渡さなければならない
- Generic な関数ではなく、トレイトを実装したあらゆる値を受け取れる
-
返り値の位置の impl Trait はジェネリクスのショートハンドではない
- ジェネリクスは、呼ぶ側が実際の型を決める
- impl Trait は呼ばれる関数側が決める。関数の本体から推論される
- 呼ぶ側はトレイト境界しか分からない
- ただし、今のところ実際の型は一つのみ
この 3 種類のうちいずれを使うかの判断には、これらがどのように実装しているかを知っていると役に立つ
- ジェネリクスと引数の impl Trait
- monomorphisation (単相型、単相化?)
- それぞれの型インスタンスに対して関数のコピーを作成する。
- たとえば、
fn f(b: impl Bar)
をFoo
とBar
で呼び出しているなら、関数のコピーがふたつできる。 - 一切の間接呼び出しが発生しないのでパフォーマンスは良いが、コードサイズは増える
- 返り値の impl Trait には monomorphisation は必要なく、単に具象型で置き換えられる。
- Trait object
- こちらも monomorphisation は必要ない
- Trait object の型は fat pointer で実装されている
- 参照.
&dyn Bar
は値への参照だけでなく vtable への参照を含む - つまり動的ディスパッチされる
Object Safety
- すべてのトレイトが Trait object になれるわけではない
- "Object safe" なトレイトのみ
- なぜ必要かというと、
&dyn Trait
を&impl Trait
な引数に渡すため
Implicit bounds
-
Send
やSync
のようなトレイトは、型を構成するそれぞれの型がこれらを実装している限り、自動で実装される -
impl Trait
が返り値で使われた場合、これらのトレイトは関数本体から暗黙のうちに推論される - つまり、
+ Send + Sync
をimpl Trait
に対して書く必要はない- Trait object では必要
- 一方、ライフタイム境界はもっと複雑
-
dyn Trait
はデフォルトで'static
ライフタイム境界を持つ - Default trait object lifetime
-
- 型変数と引数位置の
impl Trait
は暗黙的なライフタイム境界を持たない。-
impl Trait
の実体の型はスコープ中の型変数に依存する - ライフタイム境界も同様
-
- 何も見つからなければ
'sttaic
’static` ライフタイムについて
-
static
宣言で定数を作成する - 文字列リテラル
&'static str
型を持つ変数を作成する- これらは実行バイナリの一部として格納される。
- プログラムが動作している間、常に有効
これらは 'static
ライフタイム
pub fn downcast_ref<T: Error + 'static>(&self) -> Option<&T> { ... }
これが 'static
ライフタイム境界。
T
内の全ての参照は 'static
ライフタイムよりも長く(つまり同じだけ)生きていなければならない。あるいは参照を含まない(こちらのケースが多い)
clippy が警告を表示していくれる
error: explicit lifetimes given in parameter types where they could be elided (or replaced with `'_` if needed by type declaration)
--> src/language_server/description.rs:9:1
|
9 | fn format_type_specifier<'a>(ty: Option<TypeKind<'a>>) -> String {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `-D clippy::needless-lifetimes` implied by `-D warnings`
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_lifetimes
error: aborting due to previous error
記事にするときの構成
- はじめに
- プライベートで Rust を書き始めて数ヶ月
- ライフタイムに苦しめられている
- 匿名ライフタイムについてよく分からなかったので調べた
- そもそも何なのか?
- 何のためにあるのか?
- 日々のプログラミングでどのように役立つのか?
- プライベートで Rust を書き始めて数ヶ月
- 匿名ライフタイムとは何か?
- 匿名ライフタイムの例
- 適当なプログラム
- 標準ライブラリの例
-
impl Trait
を返すときの例
- Edition 2018 で導入された詳細
- これらをちゃんと読めば詳細は理解できる
- 何のために導入されて、どう役立つのかを中心に書きます
- 匿名ライフタイムの例
- 匿名ライフタイムの意義と効用
- 関数での匿名ライフタイム
- ライフタイムの省略ルールをおさらい
- 省略されていることを明示するための匿名ライフタイム
- 何が嬉しいのか?
- 参照を持つ構造体を返す例
- Edition 2018 での新しい警告
-
impl
ブロックでの匿名ライフタイム- Edition 2018 で導入された
- Trait を実装するときに便利
- Trait object の匿名ライフタイム
-
impl Trait
を返すときの例再掲 - ライフタイムの省略ルール再び
- 匿名ライフタイムでルールを変える
-
- 関数での匿名ライフタイム
- まとめ
- 構造体が他の参照を借用していることを示すのはいいこと
- Edition 2018 の idiom 警告は有効にしておいた方がいい
- 匿名ライフタイムを闇雲に使わない
- 借用チェッカーとライフタイムが通っても意味的に正しいとは限らない
- 匿名ライフタイムで十分だと明らかな場合のみ使う
- 関数の引数と返り値が省略されたライフタイムで動くが、ライフタイムを明示したい場合
- Trait を実装するが、メソッドでライフタイムが不要な場
- 構造体が他の参照を借用していることを示すのはいいこと