借用と所有権の取得

Essential
最終更新: タグ: Rust, 所有権

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>