Rustライフタイムの深掘り:ボローチェッカーとメモリ管理
Emily Parker
Product Engineer · Leapcell

ライフタイムとは?
ライフタイムの定義
Rustでは、すべての参照にライフタイムがあります。これは、参照が指す値がメモリに存在する期間を表します(参照が有効であるコード行の範囲と考えることもできます)。ライフタイムは、参照がそのライフタイム全体を通して有効な状態を保つことを保証します。これらは参照の有効性を保証するために存在します。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
上記のコードでは、関数longest
は2つの入力パラメータを持ち、どちらも文字列スライスへの参照です。また、文字列スライスへの参照である戻り値も持ちます。Rustはメモリ安全性を非常に重視しているため、ライフタイムが導入され、参照の有効性が保証されます。返される参照が有効であることを確認するには、まずそのライフタイムを決定する必要があります。しかし、どうすればそれを決定できるのでしょうか。
Rustは、関数パラメータと戻り値のライフタイムを自動的に推論できます。これについては、後のセクションで説明します。しかし、この推論は普遍的ではありません。Rustは、3つの特定のシナリオでのみライフタイムを推論できます。上記のコードは、これらのケースのいずれにも当てはまりません。このような状況では、ライフタイムを手動でアノテーションする必要があります。明示的なアノテーションがない場合、Rustのボローチェッカーは戻り値のライフタイムを判別できず、したがって参照の有効性を検証できません。
もう一度コードを見てみましょう。戻り値はパラメータから来ています。戻り値がパラメータと同じライフタイムを持つようにすれば十分でしょうか?少なくとも関数呼び出しのスコープ内では、これにより参照が有効な状態を保てます。ただし、パラメータが2つあるため、それらのライフタイムは異なる可能性があります。戻り値はどちらに関連付けるべきでしょうか。解決策は簡単です。戻り値は、最も短いライフタイムを持つパラメータと同じライフタイムを持つべきです。こうすることで、戻り値は両方のパラメータが有効である限り、少なくとも有効な状態を保ちます。したがって、上記のコードのアノテーション'a
は、戻り値のライフタイムが両方の'a
パラメータのライフタイムの交差であることを意味します。これにより、戻り値のライフタイムが明確に定義され、Rustがその参照が有効かどうかをチェックできます。
ライフタイムとメモリ管理
Rustはライフタイムを使用してメモリを管理します。変数がスコープ外になると、その変数が占有していたメモリが解放されます。参照がすでに解放されたメモリを指している場合、それはダングリング参照になり、それを使用しようとするとコンパイルエラーが発生します。
fn main() { let r; { let x = 5; r = &x; } println!("r: {}", r); }
上記のコードでは、変数x
はスコープ外に出ると割り当て解除されますが、変数r
は依然としてそれへの参照を保持しています。これにより、ダングリング参照が作成されます。Rustコンパイラはこの問題を検出し、エラーメッセージを表示します。
なぜライフタイムが必要なのか?
ダングリング参照の防止とメモリ安全性の確保
前述のように、Rustはライフタイムを使用してダングリング参照を防ぎます。コンパイラは、コード内のすべての参照のライフタイムをチェックし、それらがライフタイム全体を通して有効な状態を保つことを保証します。
fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let result = longest(string1.as_str(), string2); println!("The longest string is {}", result); } fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
上記のコードでは、関数longest
は文字列スライスへの参照を返します。コンパイラは、戻り値のライフタイムが有効かどうかをチェックします。戻り値がダングリング参照である場合、コンパイラはエラーを生成します。
Rustがライフタイムを通じてメモリ安全性をどのように確保するかを示す別の例を次に示します。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } fn main() { let string1 = String::from("long string is long"); let result; { let string2 = String::from("xyz"); result = longest(string1.as_str(), string2.as_str()); } println!("The longest string is {}", result); }
このコードでは、longest
という関数を定義します。この関数は、2つの文字列スライスをパラメータとして受け取り、文字列スライスを返します。この関数は、ライフタイムパラメータ'a
を使用して、入力パラメータと戻り値のライフタイムの関係を指定します。
main
関数では、2つの文字列変数string1
とstring2
を作成し、それらのスライスをlongest
に渡します。longest
は、入力パラメータと戻り値が同じライフタイムを持つ必要があるため、コンパイラはスライスがこの要件を満たしているかどうかをチェックします。ここでは、string2
のライフタイムがstring1
よりも短いため、コンパイラはエラーを報告し、戻り値にダングリング参照が含まれている可能性があることを警告します。このメカニズムにより、メモリ安全性が確保されます。
ライフタイムの構文
ライフタイムのアノテーション
関数定義では、山括弧を使用してライフタイムパラメータをアノテーションできます。ライフタイムパラメータ名は、'a
のようにアポストロフィで始まる必要があります。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
上記のコードでは、関数longest
は2つの入力パラメータを持ち、どちらも文字列スライスへの参照です。これらの参照にはライフタイムパラメータ'a
があり、同じライフタイムを持つ必要があることを示しています。戻り値にもライフタイムパラメータ'a
があり、そのライフタイムが入力パラメータのライフタイムと一致することを意味します。
ライフタイム省略の規則
多くの場合、Rustコンパイラは参照のライフタイムを自動的に推論できるため、ライフタイムのアノテーションを省略できます。
fn longest(x: &str, y: &str) -> &str { if x.len() > y.len() { x } else { y } }
この場合、コンパイラはパラメータと戻り値のライフタイムを判別できません。戻り値は2つのパラメータの比較に依存するため、コンパイラはどのパラメータのライフタイムを使用する必要があるかを推論できません。
コンパイラが関数の戻り値のライフタイムを判別できない場合、エラーを発行し、開発者がライフタイムパラメータを明示的に指定する必要があります。たとえば、longest
関数を次のように変更できます。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
ここでは、ライフタイムパラメータ'a
は、入力パラメータと戻り値が同じライフタイムを持つ必要があることを指定します。これにより、コンパイラは関数引数がライフタイム制約を満たしているかどうかをチェックし、戻り値にダングリング参照が含まれていないことを保証できます。
ただし、多くの場合、Rustコンパイラはライフタイムを自動的に推論できます。Rustは、一連のライフタイム省略の規則を適用して、正しいライフタイムを推論します。これらの規則は次のとおりです。
- 各参照パラメータは、独自のライフタイムパラメータを取得します。たとえば、
fn foo(x: &i32)
はfn foo<'a>(x: &'a i32)
に変換されます。 - 関数に単一の入力ライフタイムパラメータがある場合、そのライフタイムはすべての出力ライフタイムパラメータに割り当てられます。たとえば、
fn foo<'a>(x: &'a i32) -> &i32
はfn foo<'a>(x: &'a i32) -> &'a i32
に変換されます。 - 関数に複数の入力ライフタイムパラメータがあるが、そのうちの1つが
&self
または&mut self
である場合、戻り値はself
のライフタイムを受け取ります。たとえば、fn foo(&self, x: &i32) -> &i32
はfn foo<'a, 'b>(&'a self, x: &'b i32) -> &'a i32
に変換されます。
これらの規則により、Rustコンパイラは多くの場合、ライフタイムを自動的に推論できます。ただし、複雑なシナリオでは、コンパイラは明示的なライフタイムのアノテーションを依然として必要とする場合があります。
ライフタイムのユースケース
関数のパラメータと戻り値
関数の入力パラメータまたは戻り値に参照が含まれている場合、これらの参照の有効性を保証するためにライフタイムを使用する必要があります。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
上記のコードでは、関数longest
は2つの入力パラメータを持ち、どちらも文字列スライスへの参照です。これらの参照にはライフタイムパラメータ'a
があり、同じライフタイムを共有する必要があることを意味します。関数の戻り値にもライフタイムパラメータ'a
があり、そのライフタイムが入力パラメータと一致することを示しています。
構造体の定義
構造体に参照が含まれている場合、参照の有効性を保証するためにライフタイムを使用する必要があります。
struct ImportantExcerpt<'a> { part: &'a str, } fn main() { let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next().expect("Could not find a '.'"); let i = ImportantExcerpt { part: first_sentence }; }
上記のコードでは、構造体ImportantExcerpt
に文字列スライスへの参照が含まれています。この参照にはライフタイムパラメータ'a
があり、明確に定義されたライフタイムを持つ必要があることを示しています。ダングリング参照を防ぐために、文字列スライスは構造体と同じライフタイムを持つ必要があり、構造体が有効である限り、文字列スライスも有効であることを保証します。
ライフタイムの高度な使用法
ライフタイムのサブタイピングとポリモーフィズム
Rustはライフタイムのサブタイピングとポリモーフィズムをサポートしています。ライフタイムのサブタイピングとは、あるライフタイムが別のライフタイムのサブセットになる可能性があることを意味します。
fn longest<'a>(x: &'a str, y: &str) -> &'a str { x }
この例では、最初の入力パラメータにはライフタイム'a
がありますが、2番目の入力パラメータには明示的なライフタイムのアノテーションがありません。これは、2番目の入力パラメータが任意のライフタイムを持つことができ、戻り値に影響を与えないことを意味します。
静的ライフタイム
Rustには'static
と呼ばれる特別なライフタイムがあり、参照がプログラムの実行期間全体にわたって有効であることを示します。
let s: &'static str = "I have a static lifetime.";
この例では、変数s
は静的ライフタイムを持つ文字列スライスへの参照です。つまり、プログラムの実行全体を通して有効な状態を保ちます。
ライフタイムとボローチェッカー
ボローチェッカーの役割
Rustのコンパイラには、すべての参照が借用規則に従っていることを保証するボローチェッカーが含まれています。規則が破られた場合、コンパイラはエラーを生成します。
fn main() { let mut s = String::from("hello"); let r1 = &s; let r2 = &s; let r3 = &mut s; println!("{}, {}, and {}", r1, r2, r3); }
このコードでは、変数s
は同じスコープ内に不変参照(r1
とr2
)と可変参照(r3
)の両方を持っています。これはRustの借用規則に違反します。コンパイラはこの問題を検出し、エラーを生成します。
ライフタイムチェックは、参照が存続期間全体を通して有効な状態を保つことを保証します。ただし、ライフタイムが同じであることは、必ずしも借用が許可されることを意味するわけではありません。Rustの借用規則は、ライフタイムの有効性と可変性制約の両方を考慮します。
上記のコードでは、r1
、r2
、r3
のライフタイムが同じであっても、同じスコープ内で同じ変数への不変参照と可変参照の両方を作成しようとしているため、Rustの借用規則に違反しています。Rustの借用規則によれば、
- 同時に変数への複数の不変参照を持つことができます。
- 1つの可変参照を持つことができますが、同時に他の参照(可変または不変)を持つことはできません。
これにより、メモリの安全性が確保され、データ競合が防止されます。
ライフタイムの制限
Rustはライフタイムを使用してメモリを管理し、安全性を確保しますが、ライフタイムにはいくつかの制限もあります。たとえば、場合によっては、コンパイラが正しいライフタイムを自動的に推論できないため、プログラマからの明示的なアノテーションが必要になります。これにより、開発者の負担が増加し、コードの可読性が低下する可能性があります。
【Leapcellは、Rustプロジェクトをホストするための最適な選択肢です。】(https://leapcell.io/)
Leapcellは、Webホスティング、非同期タスク、およびRedisのための次世代サーバーレスプラットフォームです。
多言語サポート
- Node.js、Python、Go、または Rust で開発できます。
無制限のプロジェクトを無料でデプロイ
- 使用量に対してのみ課金されます — リクエストも料金もかかりません。
比類のない費用対効果
- アイドル時の料金なしで、従量課金制です。
- 例: 25 ドルで、平均応答時間 60 ミリ秒で 694 万リクエストをサポートします。
合理化された開発者エクスペリエンス
- 簡単なセットアップのための直感的な UI。
- 完全に自動化された CI/CD パイプラインと GitOps 統合。
- 実用的な洞察を得るためのリアルタイムのメトリクスとロギング。
簡単なスケーラビリティと高性能
- 高い同時実行性を容易に処理するための自動スケーリング。
- 運用オーバーヘッドはゼロ — 構築に集中できます。
詳細については、ドキュメントをご覧ください。
X でフォローしてください: @LeapcellHQ