Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails以外の開発一般

Rustの借用の話をする

みなさんこんにちは。yoshiです。
夏のTechRachoフェア2023ということですが、夏とは特に関係なくRustの話をしようと思います。

借用はRustの大きな特徴の一つです。
私達は借用の様々な規則のおかげで安全にRustを使える訳ですが、改めてその挙動を確認してみようというのがこの記事の趣旨です。

🔗 そもそも借用とは

Rustについてよく知らない人向けに、借用とは何なのかを軽く説明しておきます。
借用(Borrowing)という概念は、所有権(Ownership)と切っても切り離せない関係にあります。
所有権というのは、Rustの値が所有者(Owner)である変数と結びついていることを指します。

{
    let a = 42i32;
    let b: String = "Foo".into();
}

例えば、上記のようなコードでは、 a42i32 という値が割り当てられた領域の所有者で、 b"Foo" という文字列を保持する String 型の値が割り当てられた領域の所有者です。
ab寿命(Lifetime)は最長でもブロック文の終わりで終了するため、その時点で領域が解放されます。
b に関しては、 String 型が Drop トレイト境界を実装しているため、同時に Drop::drop が呼び出され、内部的に確保したヒープ領域も解放されます。

Rustでは、(Rc のような場合を除いて)所有者は常に1つの変数になるように設計されていて、所有者を介して値にアクセスすることができます。
ムーブという機能を使って所有権を他の変数に移すことはできますが、その場合、元の所有者は所有権を失い、値にアクセスできなくなります。

ただ、値へのアクセスが必ず所有者を介さなければいけないとなると、これは不便です。例えば関数の呼び出しを行うのに、関数の引数に所有権をムーブしてしまうと、呼び出し元で値を再利用したいときにできなくなってしまいます。

そのため、参照(Reference)という特殊な値を作り、所有権はそのままに値にアクセスすることができます。この参照を作る仕組みを借用と呼ぶ訳です。

let a = 42i32;
let b = &a; // これが借用
//  ^bの型は &i32

上記の例では &a の部分で a を借用して参照を作っています。なお、参照自体も値ですが、参照の型は参照元の型に & をつけたものになります。この場合は b の型は &i32 になる訳です。

ここまでなら、なんだCのポインタなんかと同じじゃないか、という話になるのですが、Rustの借用はもう少し複雑です。

借用は参照を『借りて』いる訳なので、いずれ『返す』必要があります。少なくとも所有者が死ぬ前に返す必要があります。

let a = 42i32;
let mut b = &a;
{
    let c = 43i32;
    b = &c;
}
println!("{b}");

例えば上記のようなコードは、Rustではエラーになります。 c はブロック文の終わりで寿命が尽きるため、 b は少なくともその前に参照を返さなければいけませんが、その下の println!("{b}") で使用されるため、 b は有効な参照を保持している必要があります。どちらも満たすことはできないので、エラーとなる訳です。これはCのような単純なポインタでは実現できません(Cで同じようなコードを書いた場合、 b はダングリングポインタを保持することになります)。

ちなみに、C++にも参照がありますが、Rustは参照も値として扱い、書き換えなどができるのに対して、C++の参照は書き換えが不可能なので同等のコードを書くことができません。C++の場合は、デストラクタの明示的呼び出しなどでダングリングリファレンスになり得ます。

ちなみに、Rustの変数は、Non Lexical Lifetime(NLL) という機能により、文法的なブロックの終端まで行かずとも、不要になった時点で寿命が尽きるようになっています。
そのため、例えば以下のようなコードはエラーになりません。

let a = 42i32;
let mut b = &a;
{
    let c = 43i32;
    b = &c;
    println!("{b}"); // bの利用はここが最後なのでここで寿命が尽きる
} // <-- cのライフタイムは最大でここまでだが、bの寿命も尽きているため問題がない

このように、一見文法的には参照が存在するように見えても寿命が切れているケースがあるため、寿命が切れていない参照が存在することをこの文章では『有効な参照がある』と言うことにします。

🔗 不変参照と可変参照

借用の基本的な部分を説明したので、続けて不変参照可変参照について説明します。

前節の例で出した参照はすべて不変参照と呼ばれるもので、値の読み出しはできるけど、書き込みはできない参照です。

書き込みをしたいときは、可変参照を使う必要があります。可変参照は mut キーワードを使い、 & の代わりに &mut を付けることで作ることができます。
可変参照を作ることを(あまり使いませんが)可変借用と言ったりもします。

let mut a = 42i32;
let b = &mut a; // これが可変借用
//  ^bの型は &mut i32
*b += 1;
println!("{a}"); // 43 が出力される

可変参照が加わると、Rustはやや複雑な規則に従って借用チェックを行うようになります。大まかに言うと、以下のようなルールです。

  1. 同じ領域に対する有効な可変参照は最大で1つしか存在できない
  2. 不変参照は複数個存在できる
  3. 有効な不変参照が既に存在する場合、所有者は可変参照を作り出すことができない
  4. 有効な可変参照が既に存在する場合、所有者は不変参照を作り出すことができない
  5. 可変参照から不変参照を作り出すことができる
  6. 可変参照を介した値の書き込みは、有効な不変参照が存在しない場合に行うことができる
  7. 所有者を介した値の書き込みは、有効な可変参照、不変参照のいずれもが存在しない場合に行うことができる

🔗 1. 同じ領域に対する有効な可変参照は最大で1つしか存在できない

不変参照は、値を読み込みしかできない参照ですが、それ以外に、いつ何度値を読み込んでも同じであるという特徴があります。それを保証するためには、不変参照が存在している場合は外部で値の書き込みを禁じる必要があります。

また、可変参照は、可変参照を介して書き込みを行う前後では別の値になり得ますが、それ以外で外部から値の書き込みができてしまうと競合が発生してしまいます。

仮に2つ以上の可変参照が存在してしまうと、競合を防ぐには互いに別の可変参照からの書き込みを禁止する必要が生まれます。それでは単に不変参照があるのと変わらないため、可変参照は最大で1つというルールにするのが妥当という訳です。

let mut a = 42i32;
let b = &mut a;
let c = &mut a; // bが有効な可変参照であるためエラー
*b += 1;

🔗 2. 不変参照は複数個存在できる

不変参照は、読み込みしか行わず、読み込みだけなら何度行われようとどこで行われようと問題がないので、複数個作ることができます。

let a = 42i32;
let b = &a;
let c = &a; // bは有効だが不変参照なので問題なし
println!("{b}");

🔗 3. 有効な不変参照が既に存在する場合、所有者は可変参照を作り出すことができない

不変参照があるということは、可変参照を作っても書き込みができません。そのため、可変参照を作る意味がないため禁じられています。

let mut a = 42i32;
let b = &a;
let c = &mut a; // bが有効なので可変参照を作れない
println!("{b}");

🔗 4. 有効な可変参照が既に存在する場合、所有者は不変参照を作り出すことができない

可変参照がある時点で最後に所有者を参照した時から内容が変わっている可能性があります。

let mut a = 42i32;
let b = &mut a;
let c = &a; // bが有効な可変参照なので参照を作れない
*b += 1;

🔗 5. 可変参照から不変参照を作り出すことができる

3, 4のルールから、所有者は可変参照と不変参照を同時に作り出すことができません。ただし、一度可変参照を作ると、その可変参照から不変参照を作り出すことができます。従って、可変参照と不変参照が同時に存在するということはあり得ます。

let mut a = 42i32;
let b = &mut a;
let c: &i32 = b; // 可変参照から不変参照へのキャストは暗黙に行われる
let c = b as &i32; // 明示的なキャストをする場合

🔗 6. 可変参照を介した値の書き込みは、有効な不変参照が存在しない場合に行うことができる

不変参照と可変参照が同時に存在することはありますが、その場合、不変参照が有効である間は可変参照を介した書き込みはできません。

let mut a = 42i32;
let b = &mut a;
let c: &i32 = b;
*b += 1; // cが有効なのでbを介した書き込みはエラー
println!("{c}");

🔗 7. 所有者を介した値の書き込みは、有効な可変参照、不変参照のいずれもが存在しない場合に行うことができる

所有者は有効な参照がある場合、つまり借用されている場合は書き込みができません。

let mut a = 42i32;
let b = &mut a;
a = 43; // bが有効なのでエラー
*b += 1;
let c: &i32 = &a;
a = 44; // cが有効なのでエラー
println!("{c}");

🔗 可変参照の再借用

有効な可変参照は常に1つしか存在できず、不変参照は複数存在できるという性質は、不変参照には Copy トレイト境界が実装されていて、可変参照には実装されていないということになります。

Copy トレイトは特殊なマーカートレイトで、このトレイト境界が実装された型はC言語の memcpy 相当の操作で複製をすることができます。当然、ヒープにメモリを確保して Drop トレイト境界を実装するような型には実装できません。

Rustである変数の値を他に代入するとき、 Copy トレイト境界がある型とない型で振る舞いが変わります。 Copy トレイト境界がない場合、値はムーブされて元の変数は所有権を失います。しかし、 Copy トレイト境界がある場合、値は複製されて、元の変数にも引き続きアクセスすることができます。

なぜそうなっているかというと、代入操作はCopy トレイト境界の有無に関わらず memcpy 相当の操作で行われるのですが、 Copy トレイト境界が実装されている型はそもそも複製を memcpy 相当の操作で行えるため、元の変数に引き続きアクセスしても問題がないためです。

let a: i32 = 42;
let b: String = "Foo".into();

let a1 = a; // i32には Copy トレイト境界が実装されているため複製される
let b1 = b; // String には Copy トレイト境界が実装されていないためムーブされる

println!("{a}"); // 問題なし
println!("{b}"); // bは既にムーブされているのでエラー

不変参照は Copy トレイト境界を実装されているため自分自身を複製して不変参照を増やすことができます。

let a = 42i32;
let b = &a;
let c = b; // bはコピーされるので有効なまま
let d = b; // いくらでも増やせる

可変参照は Copy トレイト境界が実装されていないためムーブされます。

let mut a = 42i32;
let b = &mut a;
let c = b; // この時点でbは無効になる
let d = b; // これはエラー

ただし、可変参照に限り、このルールが成り立たないケースがあります。明示的に型アノテーションを書いた場合です。

let mut a = 42i32;
let b = &mut a;
let c: &mut _ = b; // ここでは、cが再借用を行い、bは一時的に無効になる
println!("{c}");   // ここでcの寿命が尽き、bが再び有効になる
println!("{b}");

let d = b;       // ここでは、dはbからムーブされ、bは以降ずっと無効になる
println!("{d}"); // ここでdの寿命は尽きるが、bが再び有効になることはない
println!("{b}"); // bはムーブ済みのためエラー

上記の例で可変参照は Copy トレイトを持たないにもかかわらず、cへの代入時点でムーブされず、一時的に無効な状態になります。存在しているけれどアクセスはできない状態で、ちょうど可変参照を作った後の所有者と似たような状態と言えます。

上記の他にも、例えば、

let mut a = 42i32;
let b = &mut a;
let c = b as &mut _;

などと書いても同じ結果が得られます。これはコピーをしているのではなく、可変参照から新たな可変参照を作り出していると捉えることができます。

なぜこのような複雑なルールになっているかと言うと、可変参照を受け取る関数を呼んだ後で元の可変参照が無効になってしまわないようにするためです。

struct X {
    a: i32,
}

impl X {
    fn inc(&mut self) { self.a += 1; }
}

let mut x = X { a: 42 };
let y = &mut x;
y.inc(); // ここでyがムーブされてしまうと困る
println!("{}", y.a);

上記のようなケースで自動的にムーブされてしまうことを防ぐため、 Copy を実装されていないにも関わらず可変参照は複製されたような挙動を示します。

ただし、有効な可変参照は最大1つである必要があるので、新しい可変参照が生存している間は元の可変参照は無効になります。

可変参照が指し示す領域へのアクセス権を又貸ししているようなもので、これを再借用と言います。

終わりに

今回、Rustの基礎的なところを書いてみました。締め切り迫ってたけど話題が思いつかなかったので。
今までなんとなくで使ってたけど改めて考えてみるとここどうなってるんだ?と思った場所(再借用のあたり)があったので、自分にとっても良い整理になったと思います。

Rustの特徴的な機能には、今回あまり深く触れないようにしていたライフタイムなどもあるので、そっちもそのうち記事にできたら良いですね。

それではみなさん、まだまだ残暑厳しいですがお元気で。



CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。