高度なライフタイム
第10章の「ライフタイムで参照を検証する」節で、参照をライフタイム引数で注釈し、 コンパイラに異なる参照のライフタイムがどう関連しているかを指示する方法を学びました。全ての参照にはライフタイムがあるものの、 ほとんどの場合、コンパイラがライフタイムを省略させてくれることも見ました。ここでは、 まだ講義していないライフタイムの高度な機能を3つ見ていきます:
- ライフタイム・サブタイピング: あるライフタイムが他のライフタイムより長生きすることを保証する
- ライフタイム境界: ジェネリックな型への参照のライフタイムを指定する
- トレイトオブジェクトのライフタイムの推論: コンパイラにトレイトオブジェクトのライフタイムを推論させることと指定する必要があるタイミング
ライフタイム・サブタイピングにより、あるライフタイムが他よりも長生きすることを保証する
ライフタイム・サブタイピング(lifetime subtyping; 訳注
: あえて訳すなら、ライフタイムの継承)は、
あるライフタイムが他のライフタイムよりも長生きすべきであることを指定します。
ライフタイム・サブタイピングを探究するために、パーサを書きたいところを想像してください。
パース(訳注
: parse; 構文解析)中の文字列への参照を保持するContext
と呼ばれる構造を使用します。この文字列をパースし、
成功か失敗を返すパーサを書きます。パーサは構文解析を行うためにContext
を借用する必要があるでしょう。
リスト19-12は、コードに必要なライフタイム注釈がないことを除いてこのパーサのコードを実装しているので、コンパイルはできません。
ファイル名: src/lib.rs
struct Context(&str);
struct Parser {
context: &Context,
}
impl Parser {
fn parse(&self) -> Result<(), &str> {
Err(&self.context.0[1..])
}
}
コンパイラはContext
の文字列スライスとParser
のContext
への参照にライフタイム引数を期待するので、
このコードをコンパイルすると、エラーに落ち着きます。
簡単のため、parse
関数は、Result<(), &str>
を返します。つまり、関数は成功時には何もせず、
失敗時には、正しくパースできなかった文字列スライスの一部を返すということです。本物の実装は、
もっとエラーの情報を提供し、パースが成功したら、構造化されたデータ型を返すでしょう。そのような詳細を議論するつもりはありません。
この例のライフタイムの部分に関係ないからです。
このコードを単純に保つため、構文解析のロジックは何も書きません。ですが、構文解析ロジックのどこかで、 非合法な入力の一部を参照するエラーを返すことで非合法な入力を扱う可能性が非常に高いでしょう; この参照が、 ライフタイムに関連してこのコード例を面白くしてくれます。パーサのロジックが、最初のバイトの後で入力が不正だった振りをしましょう。 最初のバイトが合法な文字境界になければ、このコードはパニックする可能性があることに注意してください; ここでも、例を簡略化して関連するライフタイムに集中しています。
このコードをコンパイルできるようにするには、Context
の文字列スライスとParser
のContext
への参照のライフタイム引数を埋める必要があります。
最も率直な方法は、リスト19-13のように、全ての箇所で同じライフタイム名を使用することです。
第10章の「構造体定義のライフタイム注釈」節から、struct Context<'a>
、struct Parser<'a>
、
impl<'a>
それぞれが新しいライフタイム引数を宣言することを思い出してください。全部の名前が偶然一致しましたが、
この例で宣言された3つのライフタイム引数は、関連していません。
ファイル名: src/lib.rs
# #![allow(unused_variables)] #fn main() { struct Context<'a>(&'a str); struct Parser<'a> { context: &'a Context<'a>, } impl<'a> Parser<'a> { fn parse(&self) -> Result<(), &str> { Err(&self.context.0[1..]) } } #}
このコードは、単純にうまくコンパイルできます。コンパイラにParser
はライフタイム'a
のContext
への参照を保持し、
Context
はParser
のContext
への参照と同じ期間生きる文字列スライスを保持していると指示しています。
Rustコンパイラのエラーメッセージは、これらの参照にライフタイム引数が必要であることを述べていて、
今ではライフタイム引数を追加しました。
次にリスト19-14では、Context
のインスタンスを1つ取り、Parser
を使ってその文脈をパースし、
parse
が返すものを返す関数を追加します。このコードは期待通りに動きません。
ファイル名: src/lib.rs
fn parse_context(context: Context) -> Result<(), &str> {
Parser { context: &context }.parse()
}
parse_context
関数を追加してコードをコンパイルしようとすると、2つ冗長なエラーが出ます:
error[E0597]: borrowed value does not live long enough
(エラー: 借用された値は十分長生きしません)
--> src/lib.rs:14:5
|
14 | Parser { context: &context }.parse()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ does not live long enough
15 | }
| - temporary value only lives until here
|
note: borrowed value must be valid for the anonymous lifetime #1 defined on the function body at 13:1...
(注釈: 借用された値は、13:1の関数本体で定義された1番目の匿名のライフタイムに有効でなければなりません)
--> src/lib.rs:13:1
|
13 | / fn parse_context(context: Context) -> Result<(), &str> {
14 | | Parser { context: &context }.parse()
15 | | }
| |_^
error[E0597]: `context` does not live long enough
--> src/lib.rs:14:24
|
14 | Parser { context: &context }.parse()
| ^^^^^^^ does not live long enough
15 | }
| - borrowed value only lives until here
|
note: borrowed value must be valid for the anonymous lifetime #1 defined on the function body at 13:1...
--> src/lib.rs:13:1
|
13 | / fn parse_context(context: Context) -> Result<(), &str> {
14 | | Parser { context: &context }.parse()
15 | | }
| |_^
これらのエラーは、生成されたParser
インスタンスとcontext
引数がparse_context
関数の最後までしか生きないと述べています。
しかし、どちらも関数全体のライフタイムだけ生きる必要があります。
言い換えると、Parser
とcontext
は関数全体より長生きし、このコードの全参照が常に有効であるためには、
関数が始まる前や、終わった後も有効である必要があります。生成しているParser
とcontext
引数は、
関数の終わりでスコープを抜けます。parse_context
がcontext
の所有権を奪っているからです。
これらのエラーが起こる理由を理解するため、再度リスト19-13の定義、特にparse
メソッドのシグニチャの参照を観察しましょう:
fn parse(&self) -> Result<(), &str> {
省略規則を覚えていますか?省略するのではなく、参照のライフタイムを注釈するなら、シグニチャは以下のようになるでしょう:
fn parse<'a>(&'a self) -> Result<(), &'a str> {
要するに、parse
の戻り値のエラー部分は、Parser
インスタンスのライフタイムと紐づいたライフタイムになるのです
(parse
メソッドシグニチャの&self
のライフタイム)。それは、理に適っています: 返却される文字列スライスは、
Parser
に保持されたContext
インスタンスの文字列スライスを参照していて、Parser
構造体の定義は、
Context
への参照のライフタイムとContext
が保持する文字列スライスのライフタイムは同じになるべきと指定しています。
問題は、parse_context
関数は、parse
から返却される値を返すので、parse_context
の戻り値のライフタイムも、
Parser
のライフタイムに紐づくことです。しかし、parse_context
関数で生成されたParser
インスタンスは、
関数の終端を超えて生きることはなく(一時的なのです)、context
も関数の終端でスコープを抜けるのです(parse_context
が所有権を奪っています)。
コンパイラは、私たちが、関数の終端でスコープを抜ける値への参照を返そうとしていると考えます。
全ライフタイムを同じライフタイム引数で注釈したからです。注釈は、コンパイラにContext
が保持する文字列スライスのライフタイムは、
Parser
が保持するContext
への参照のライフタイムと一致すると指示しました。
parse_context
関数には、parse
関数内で返却される文字列スライスがContext
とParser
より長生きし、
parse_context
が返す参照がContext
やParser
ではなく、文字列スライスを参照することはわかりません。
parse
の実装が何をするか知ることで、parse
の戻り値がParser
インスタンスに紐づく唯一の理由が、Parser
インスタンスのContext
、
引いては文字列スライスを参照していることであることを把握します。従って、parse_context
が気にする必要があるのは、
本当は文字列スライスのライフタイムなのです。Context
の文字列スライスとParser
のContext
への参照が異なるライフタイムになり、
parse_context
の戻り値がContext
の文字列スライスのライフタイムに紐づくことをコンパイラに教える方法が必要です。
まず、試しにParser
とContext
に異なるライフタイム引数を与えてみましょう。リスト19-15のようにですね。
ライフタイム引数の名前として's
と'c
を使用して、どのライフタイムがContext
の文字列スライスに当てはまり、
どれがParser
のContext
への参照に当てはまるかを明確化します。この解決策は、完全には問題を修正しませんが、
スタート地点です。コンパイルしようとする時にこの修正で十分でない理由に目を向けます。
ファイル名: src/lib.rs
struct Context<'s>(&'s str);
struct Parser<'c, 's> {
context: &'c Context<'s>,
}
impl<'c, 's> Parser<'c, 's> {
fn parse(&self) -> Result<(), &'s str> {
Err(&self.context.0[1..])
}
}
fn parse_context(context: Context) -> Result<(), &str> {
Parser { context: &context }.parse()
}
参照のライフタイム全部をリスト19-13で注釈したのと同じ箇所に注釈しました。ですが今回は、
参照が文字列スライスかContext
に当てはまるかによって異なる引数を使用しました。また、
parse
の戻り値の文字列スライス部分にも注釈を追加して、Context
の文字列スライスのライフタイムに当てはまることを示唆しました。
今コンパイルを試みると、以下のようなエラーになります:
error[E0491]: in type `&'c Context<'s>`, reference has a longer lifetime than the data it references
(エラー: 型`&'c Cotnext<'s>`において、参照のライフタイムが参照先のデータよりも長くなっています)
--> src/lib.rs:4:5
|
4 | context: &'c Context<'s>,
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
note: the pointer is valid for the lifetime 'c as defined on the struct at 3:1
(注釈: ポインタは3:1の構造体で定義されたように、ライフタイム'cの間有効です)
--> src/lib.rs:3:1
|
3 | / struct Parser<'c, 's> {
4 | | context: &'c Context<'s>,
5 | | }
| |_^
note: but the referenced data is only valid for the lifetime 's as defined on the struct at 3:1
(注釈: しかし、参照されたデータは、3:1の構造体で定義されたように、ライフタイム'sの間だけ有効です)
--> src/lib.rs:3:1
|
3 | / struct Parser<'c, 's> {
4 | | context: &'c Context<'s>,
5 | | }
| |_^
コンパイラは、'c
と's
の間になんの関連性も知りません。合法であるために、Context
でライフタイム's
と参照されたデータは、
制限され、ライフタイム'c
の参照よりも長生きすることを保証する必要があります。's
が'c
より長くないと、
Context
への参照は合法ではない可能性があるのです。
さて、この節の要点に到達しました: Rustの機能、ライフタイム・サブタイピングは、あるライフタイム引数が、
少なくとも他のライフタイムと同じだけ生きることを指定します。ライフタイム引数を宣言する山カッコ内で、
通常通りライフタイム'a
を宣言し、'b
を'b: 'a
記法を使用して宣言することで、
'a
と少なくとも同じ期間生きるライフタイム'b
を宣言できます。
Parser
の定義で、's
(文字列スライスのライフタイム)が少なくとも'c
(Context
への参照のライフタイム)と同じ期間だけ生きると、
保証することを宣言するには、ライフタイム宣言を以下のように変更します:
ファイル名: src/lib.rs
# #![allow(unused_variables)] #fn main() { # struct Context<'a>(&'a str); # struct Parser<'c, 's: 'c> { context: &'c Context<'s>, } #}
これでParser
のContext
への参照とContext
の文字列スライスへの参照のライフタイムは、違うものになりました;
文字列スライスのライフタイムがContext
への参照よりも長いことを保証したのです。
非常に長くぐにゃぐにゃした例でしたが、この章の冒頭で触れたように、Rustの高度な機能は、非常に限定的です。 この例で解説した記法は、あまり必要になりませんが、そのような場面では、何かを参照し、それに必要なライフタイムを与える方法を知っているでしょう。
ジェネリックな型への参照に対するライフタイム境界
第10章の「トレイト境界」節で、ジェネリックな型にトレイト境界を使用することを議論しました。 また、ジェネリックな型への制限としてライフタイム引数を追加することもできます; これはライフタイム境界と呼ばれます。 ライフタイム境界は、コンパイラが、ジェネリックな型の中の参照が参照先のデータよりも長生きしないことを確かめる手助けをします。
例として、参照のラッパの型を考えてください。第15章の「RefCell<T>
と内部可変性パターン」節からRefCell<T>
型を思い出してください:
borrow
とborrow_mut
メソッドがそれぞれ、Ref
とRefMut
を返します。これらの型は、
実行時に借用規則を追いかける参照に対するラッパです。Ref
構造体の定義をリスト19-16に今はライフタイム境界なしで示しました。
ファイル名: src/lib.rs
struct Ref<'a, T>(&'a T);
明示的にジェネリック引数T
と関連してライフタイム'a
を制限しないと、ジェネリックな型T
がどれだけ生きるのかわからないので、
コンパイラはエラーにします:
error[E0309]: the parameter type `T` may not live long enough
(エラー: パラメータ型の`T`は十分長生きしないかもしれません)
--> src/lib.rs:1:19
|
1 | struct Ref<'a, T>(&'a T);
| ^^^^^^
|
= help: consider adding an explicit lifetime bound `T: 'a`...
(助言: 明示的なライフタイム境界`T: 'a`を追加することを考慮してください)
note: ...so that the reference type `&'a T` does not outlive the data it points at
(注釈: そうすれば、参照型の`&'a T`が、指しているデータよりも長生きしません)
--> src/lib.rs:1:19
|
1 | struct Ref<'a, T>(&'a T);
| ^^^^^^
T
はどんな型にもなるので、T
が参照や1つ以上の参照を保持する型になることもあり、その個々の参照が独自のライフタイムになることもあるでしょう。
コンパイラは、T
が'a
と同じだけ生きることを確信できません。
幸運なことに、この場合、エラーがライフタイム境界を指定する方法について役に立つアドバイスをくれています:
consider adding an explicit lifetime bound `T: 'a` so that the reference type
`&'a T` does not outlive the data it points at
リスト19-17は、ジェネリックな型T
を宣言する時にライフタイム境界を指定することで、
このアドバイスを適用する方法を示しています。
# #![allow(unused_variables)] #fn main() { struct Ref<'a, T: 'a>(&'a T); #}
このコードはもうコンパイルできます。T: 'a
記法により、T
はどんな型にもなり得ますが、何か参照を含んでいるのなら、
その参照は少なくとも、'a
と同じだけ生きなければならないと指定しているからです。
この問題をリスト19-18のStaticRef
構造体の定義で示したように、T
に'static
ライフタイム境界を追加し、異なる方法で解決することもできます。
これは、T
に何か参照が含まれるなら、'static
ライフタイムでなければならないことを意味します。
# #![allow(unused_variables)] #fn main() { struct StaticRef<T: 'static>(&'static T); #}
'static
は、参照がプログラム全体と同じだけ生きなければならないことを意味するので、何も参照を含まない型は、
全ての参照がプログラム全体と同じだけ生きるという基準を満たします(参照がないからです)。借用チェッカーが、
参照が十分長生きしないと心配することに関しては、参照が何もない型と永久に生きる参照がある型を現実的に区別できません:
どちらも、参照が参照先のライフタイムよりも短いか決定することに関しては同じです。
トレイトオブジェクトライフタイムの推論
第17章の「トレイトオブジェクトで異なる型の値を許容する」節で、参照の背後のトレイトから構成され、
ダイナミック・ディスパッチを使用できるトレイトオブジェクトを議論しました。まだ、トレイトオブジェクトのトレイトを実装する型が、
独自のライフタイムだった時に何が起きるか議論していません。トレイトRed
と構造体Ball
があるリスト19-19を考えてください。
Ball
構造体は参照を保持し(故にライフタイム引数があり)、トレイトRed
を実装もしています。
Ball
のインスタンスをBox<Red>
として使用したいです。
ファイル名: src/main.rs
trait Red { } struct Ball<'a> { diameter: &'a i32, } impl<'a> Red for Ball<'a> { } fn main() { let num = 5; let obj = Box::new(Ball { diameter: &num }) as Box<Red>; }
明示的にobj
に関連するライフタイムを注釈していないものの、このコードはエラーなくコンパイルできます。
ライフタイムとトレイトオブジェクトと共に働く規則があるので、このコードは動くのです:
- トレイトオブジェクトのデフォルトのライフタイムは、
'static
。 &'a Trait
や&'a mut Trait
に関して、トレイトオブジェクトのデフォルトのライフタイムは、'a
。- 単独の
T: 'a
節について、トレイトオブジェクトのデフォルトのライフタイムは、'a
。 - 複数の
T: 'a
のような節について、デフォルトのライフタイムはない; 明示しなければならない。
明示しなければならない時、Box<Red>
のようなトレイトオブジェクトに対して、参照がプログラム全体で生きるかどうかにより、
記法Box<Red + 'static>
かBox<Red + 'a>
を使用してライフタイム境界を追加できます。他の境界同様、
ライフタイム境界を追記する記法は、型の内部に参照があるRed
トレイトを実装しているものは全て、
トレイト境界に指定されるライフタイムがそれらの参照と同じにならなければならないことを意味します。
次は、トレイトを管理する他の一部の高度な機能に目を向けましょう。