借用と所有権の取得
Essential前提知識
Rustの関数シグネチャにおけるすべてのパラメータは設計上の決断だ。引数の所有権を取得するか、単に借用するかを選ぶ。その選択によって、呼び出し元が何を手放さなければならないか、関数内で値に何ができるか、そして誰がクリーンアップに責任を持つかが決まる。
値渡しで取得する
T として宣言されたパラメータは所有権を取得する。呼び出し元の変数は関数にムーブされる(Copy 型の場合はコピーされる)。
fn greet(name: String) {
println!("Hello, {name}!");
} // name はここでドロップされる — ヒープ割り当てが解放される
let user = String::from("Alice");
greet(user);
// user はもう有効ではない — greet にムーブされた
関数の内側では値を完全に所有する。structフィールドに格納したり、返したり、変換したり、スコープ終了時にドロップさせたりできる。呼び出し元は関数を呼んだ時点でバインディングを手放す。
&T で借用する
&T として宣言されたパラメータは値を借用する。関数は読み取り専用の参照を受け取る。呼び出し元は所有権を保持する。
fn greet(name: &str) {
println!("Hello, {name}!");
}
let user = String::from("Alice");
greet(&user);
println!("{user}"); // まだ有効 — greet は借用しただけ
所有権の移譲はない。関数の内側で Drop は実行されない。関数が返った後、呼び出し元の値は変わらない。
&mut T でミュータブルに借用する
&mut T として宣言されたパラメータは排他的な読み書きアクセスのために借用する。呼び出し元はまだ値を所有している。関数は参照を通じて変更できる。
fn shout(s: &mut String) {
s.make_ascii_uppercase();
}
let mut msg = String::from("hello");
shout(&mut msg);
println!("{msg}"); // "HELLO"
排他的とは文字通りその意味だ。&mut 参照が生きている間、同じ値への他の参照は一切存在できない。ボローチェッカーはこれを静的に強制するため、&mut T パラメータは mut バインディングからのみ渡せる。
核心のトレードオフ
適切なシグネチャは、関数が値に何をする必要があるかから決まる。
| 必要なこと | シグネチャ |
|---|---|
| 変更せずに読む | &T |
| その場で変更する | &mut T |
| structフィールドに格納する | T — 保持するには所有しなければならない |
| 入力を元に変換されたバージョンを返す | T — 消費して生成する |
| 入力から導かれた情報を返す | &T — 所有せずに計算する |
迷ったら借用する。所有権を取得すると呼び出し元のすべてのコールサイトにコストが生じる。呼び出し元は値を手放さなければならない(まだ必要なら明示的にクローンするか、型が Copy でなければならない)。借用はコールサイトでは常に安上がりだ。
Drop のコストを誰が払うか
所有権を取得すると、関数がドロップに責任を持つ。これが望ましい場合もある——意図的に入力を消費する関数だ。
impl Builder {
pub fn build(self) -> Product { todo!() }
}
.build() を呼ぶとビルダーを確定させ Product を生成する。ビルダーはもう使えない——それがポイントだ。self を消費することで、この意図が型シグネチャに直接表現される。
借用はドロップの責任を移さない。呼び出し元のスコープがまだ値を所有しており、呼び出し元のスコープが終わるときにドロップが実行される。
ライフタイム: 借用を格納する際の隠れたコスト
関数が自身の実行中にのみ参照を使う場合、ライフタイム(lifetime)は暗黙的でコンパイラが推論する。しかし借用した参照をstructに格納したり関数から返したりする必要がある場合は、ライフタイムを明示的に注釈しなければならない。
struct Cache<'a> {
data: &'a [u8], // ソースより長く生きてはならない
}
所有権を取得するとこれを完全に回避できる——所有された Vec<u8> にはライフタイムパラメータがない。特定のコンテキストでライフタイムの管理が煩雑になる場合、&T の代わりに T を受け取ることが適切なトレードオフになることもある。
APIの慣例
慣用的なRustのAPIは認識しやすいパターンに従う。
読み取り専用操作は借用する。
fn display(value: &impl std::fmt::Display) { println!("{value}"); }
fn process(data: &[u8]) { /* ... */ }
コンストラクタは呼び出しサイトでのコピーを避けるために所有権を取ることが多い。
pub fn new(name: String) -> Self {
Self { name }
}
impl Into<T> パターンは、どちらも .to_string() を強制することなく、呼び出し元が文字列リテラルや既存の String を渡せるようにする。
pub fn new(name: impl Into<String>) -> Self {
Self { name: name.into() }
}
// new("literal") も new(existing_string) も両方動く
消費メソッドは self を値渡しで取り、呼び出し後に値が使い果たされることを示す。
impl Request {
pub fn send(self) -> Response { todo!() }
}
まとめ
Tは所有権を取得する。呼び出し元の変数はムーブされる。関数は値を格納、返却、またはドロップできる。&Tは読み取りのために借用する。所有権の移譲はなく、呼び出し後も呼び出し元の値は変わらない。&mut Tは変更のために借用する。排他的な書き込みアクセスだが、所有権の移譲はない。- 値を格納、転送、または消費する必要がない限り借用を優先する——所有権の取得は呼び出し元にムーブまたはクローンのコストを課す。
- 所有権の取得はライフタイム注釈を回避する。structに借用した参照を格納するにはライフタイム注釈が必要だ。
- よくある慣例: 読み取り専用 →
&T; その場での変更 →&mut T; 格納または消費 →T; 柔軟なコンストラクタ →impl Into<T>。