高度なライフタイム
第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..])
}
}
リスト19-12: ライフタイム注釈なしでパーサを定義する
コンパイラは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..]) } } #}
リスト19-13: ContextとParserの全参照をライフタイム引数で注釈する
このコードは、単純にうまくコンパイルできます。コンパイラに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()
}
リスト19-14: Contextを取り、Parserを使用するparse_context関数を追加する試み
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-15: 文字列スライスとContextへの参照に異なるライフタイム引数を指定する
参照のライフタイム全部をリスト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でライフタイム'cと参照されたデータは、
制限され、ライフタイム'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);
リスト19-16: ライフタイム境界なしでジェネリックな型への参照をラップする構造体を定義する
明示的にジェネリック引数Tと関連してライフタイム'aを制限することがないと、ジェネリックな型Tがどれだけ生きるのかわからないので、
コンパイラはエラーにします:
error[E0309]: the parameter type `T` may not live long enough
--> 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); #}
リスト19-17: Tにライフタイム境界を追加してTのどんな参照も少なくとも、'aと同じだけ生きると指定する
このコードはもうコンパイルできます。T: 'a記法により、Tはどんな型にもなり得ますが、何か参照を含んでいるのなら、
その参照は少なくとも、'aと同じだけ生きなければならないと指定しているからです。
この問題をリスト19-18のStaticRef構造体の定義で示したように、Tに'staticライフタイム境界を追加し、異なる方法で解決することもできます。
これは、Tに何か参照が含まれるなら、'staticライフタイムでなければならないことを意味します。
# #![allow(unused_variables)] #fn main() { struct StaticRef<T: 'static>(&'static T); #}
リスト19-18: Tに'staticライフタイム境界を追加してTを'static参照だけがあるか参照なしの型に制限する
'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>; }
リスト19-19: トレイトオブジェクトでライフタイム引数のある型を使用する
明示的にobjに関連するライフタイムを注釈していないものの、このコードはエラーなくコンパイルできます。
ライフタイムとトレイトオブジェクトと共に動く規則があるので、このコードは動くのです:
- トレイトオブジェクトのデフォルトのライフタイムは、
'static。 &'a Traitや&'a mut Traitに関して、トレイトオブジェクトのデフォルトのライフタイムは、'a。- 単独の
T: 'a節について、トレイトオブジェクトのデフォルトのライフタイムは、'a。 - 複数の
T: 'aのような節について、デフォルトのライフタイムはない; 明示しなければならない。
明示しなければならない時、Box<Red>のようなトレイトオブジェクトに対して、参照がプログラム全体で生きるかどうかにより、
記法Box<Red + 'static>かBox<Red + 'a>を使用してライフタイム境界を追加できます。他の境界同様、
ライフタイム境界を追記する記法は、型の内部に参照があるRedトレイトを実装しているものは全て、
トレイト境界に指定されるライフタイムがそれらの参照と同じにならなければなりません。
次は、トレイトを管理する他の一部の高度な機能に目を向けましょう。