BackEnd

Rust入門してみた その5 ライフタイム

投稿日:

はじめに

Rustには、借用した値の参照の寿命を意味する「ライフタイム」という概念があり、この仕組みによって破棄された所有に対する参照をしてしまうことがないように、コンパイル時にチェックします。
多くの場合では、ライフタイムをさほど意識する必要はなさそうですが、複数のライフタイムを返す関数を作成する場合など、ライフタイムを意識せざるを得ない場合もあるため、ライフタイムの概念について抑えておきたいと思います。

ライフタイムと借用チェッカー

Rustのコンパイラには借用チェッカーがあり、スコープを抜けて破棄された参照にアクセスしようとしていないか、コンパイル時にチェックします。
例えば、以下のコードはコンパイルエラーになります。

Rustでは、所有はスコープ内でのみ有効であり、スコープを抜けると破棄されます。このコードでは、内側のスコープで宣言されたxへの参照を、外側のスコープのrに入れようとします。しかし、xは内側のスコープを抜けると破棄されてしまうため、rはその後使用できなくなってしまいます。
このような状況を「ダングリング参照」と呼びます。

この、借用できる有効範囲こそがライフタイムのことです。上記のコードでは、’a と ‘b のライフタイムがあるとして、改めてソースコードを見てみます。
そうすると、 ‘a より ‘b のほうがライフタイムが短いのに、 ‘a は ‘b への参照を持ってしまっています。

このコードを修正すると、次のようになります。

このように、Rustで借用をする場合には、常にライフタイムを意識して、破棄された所有への参照が残らないようにする必要があります。

コンパイラにライフタイムを教える

借用チェッカーはある程度のライフタイムはコードから推論してくれますが、中には分からないものもあります。そうした場合には、ライフタイムをコード上で教える必要があります。
例えば、次のコードを見てください。

sliece_by_first_sentence() は第1引数textのなかから最初の1文を探して、見つかればそこまでの文字列スライスを返します。(文字列スライスは、Stringの一部への参照です。)
見つからなければ、第2引数defaultをそのまま返します。
(ちなみに引数のライフタイムを「入力ライフタイム」、戻り値のライフタイムは「出力ライフタイム」と称されます。)

ここで、main関数を見てみましょう。text, first_sentenceは外側のスコープにいますが、default_valは内側のスコープにします。そして、default_valは内側のスコープを抜けたタイミングで破棄されます。
しかし、slice_by_first_sentenceの戻り値として、第2引数に渡したdefault_valの参照がそのまま返ってくる可能性があり、その場合はやはりダングリング参照が起きてしまいます。

ですが、このようなケースは借用チェッカーでは検知できず、別のコンパイルエラーとなります。
なぜなら、 slice_by_first_sentnce の実際の戻り値が、textなのかdefault_valなのか、コンパイラには分からないからです。
こうした場合には、次のようにライフタイム注釈をします。

ライフタイム注釈の書き方はちょっと特殊で 'a のように書きます。
上記のコードは、textとdefaultの両方ともがとある 'a というライフタイムであり、戻り値もまた 'a ライフタイムであることをコンパイラに教えています。

ちなみに <'a> というようにジェネリクスに準じたシンタックスであることから想像が付くかもしれませんが、このライフタイム注釈はジェネリックなものです。
そのためtextやdefault_valの実際のライフタイムはこの関数の関心ごとではなく、渡されてきたそれぞれの参照のライフタイムのうち、どのライフタイムを返すか、ということが注釈できていれば良く、textのライフタイムにもなり得るし、default_valのライフタイムにもなり得るということです。

ただし、main()関数からの視点で見てみると、実際には 'a はtextもしくはdefault_valのうち、ライフタイムが短い方に等しいライフタイムになります。
そのため、ライフタイム注釈を書いた状態でビルドしてみると、借用チェッカーにより想定したエラーが出ます。

最後に、main関数を修正して、ビルドが通る状態にします。

ライフタイムの指定が不要なケース

先程の関数を少し変更してみました。

引数default_valの代わりに、separatorとし、文章の区切り文字を引数で渡せるようにしました。また、区切り文字が見つからなかった場合は、渡されたtext全体を1文とみなし、そのまま返すようにしました。(つまり、必ずtextの参照が返ります)
このような場合、sepratorにライフタイム注釈は不要です。なぜなら、separatorがreturnされることがなく、渡ってきた引数textのライフタイムを気にする必要がないためです。

ライフタイムの省略

ところで、これまでの入門編コードでは関数やメソッドで、引数で借用するコードを書いてきましたが、ライフタイム注釈は一切書いてきませんでした。
それには、ライフタイムを省略できる規則があり、その規則通りであればライフタイムはコンパイル時に推論されます。
詳細は、以下のドキュメントが参考になります。

https://doc.rust-jp.rs/book-ja/ch10-03-lifetime-syntax.html#%E3%83%A9%E3%82%A4%E3%83%95%E3%82%BF%E3%82%A4%E3%83%A0%E7%9C%81%E7%95%A5

さいごに

基本的には、ダングリング参照が起きないようなコーディングをした上で、ライフタイム注釈は必要な場面でのみ使えば良いということが分かりました。
Rust特有の概念だと思いますが、基本的にはコンパイラがチェックしてくれるので、コンパイルエラーになった場合には、適宜対応していけば良さそうです。

おすすめ書籍

Webアプリ開発で学ぶ Rust言語入門 パーフェクトRust プログラミングRust 第2版

blog-page_footer_336




blog-page_footer_336




-BackEnd
-

執筆者:


comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA


関連記事

rails

Railsで複合主キーのテーブルを扱う

1 はじめに1.1 前提条件2 実装例2.1 config2.2 マイグレーション2.3 モデル3 さいごに はじめに RailsでWebサービスを開発する際のDB設計では基本的にidが主キーになると ...

rails

さらば「rails migrate」、よろしく「ridgepole」

1 はじめに2 Ridgepoleとは3 rails migrateではなく、Ridgepoleを選定した理由4 rails migrateからRidgepoleへの移行手順5 capistrano3 ...

laravel logo

Deployerで複数の環境やサーバにデプロイするには

1 はじめに2 hosts.ymlファイルの作成3 プロダクション・ステージング環境を分ける3.1 ymlファイルの記載3.2 デプロイコマンド4 複数台のサーバに同時リリースする4.1 ymlファイ ...

Go言語

Go言語でテスト作成 testifyの基本的な使い方

1 はじめに2 Goテストフレームワークのスター数3 testifyについて3.1 testifyを導入する方法4 assartionについて4.1 assertion紹介4.2 ElementsMa ...

laravel logo

LaravelのFacade(ファサード)とは? 何気なく使用していた裏側の仕組みを解説!

1 はじめに1.1 Facadeを使用しているクラス2 Facadeの仕組み3 Facadeの作成3.1 サンプルコードに必要な実装3.2 Serviceの作成3.3 Facadeクラスの作成3.4 ...

フォロー

blog-page_side_responsive

2023年8月
 12345
6789101112
13141516171819
20212223242526
2728293031  

アプリ情報

私たちは無料アプリもリリースしています、ぜひご覧ください。 下記のアイコンから無料でダウンロードできます。