『これJavaならどう書くの?』から始めるRust入門 ③:String引数と所有権編
対象読者: 環境構築を終え、いよいよRustのコードを書き始める前のJava経験者
目的: Javaとは異なる「String引数」と、Rustの特徴である「所有権」の基本について共有する
前回(第②回)では用語の整理とRustlingsの導入を書きました。
今回は、いざコードを書き始めてすぐに「あれ?」となった「関数への引数の渡し方」について、自分がはまった体験とあわせて書いていきます。
1. JavaとRustで「引数を渡す」感覚がこんなに違う
Javaでオブジェクトを関数に渡すとき、裏でGC(ガベージコレクタ)がメモリを面倒みてくれているので、基本的には「型が合っていれば動く」という感覚でした。
実はJavaのオブジェクト参照は、「読み書きできる参照を、何個でも同時に持てる」という状態です。どこからでも自由に中身を触れるので便利な反面、複数箇所から同時に書き換えが起きてバグの原因になることもあります。Javaでは、GCがメモリ管理を引き受けてくれるおかげで、この自由さがそのまま成り立っています。
一方、RustにはGCがありません。その代わりに、「データの権利(所有権)をどう扱うか」 を3パターンの中から自分で選んで明示することになります。Javaの「制約のない自由な参照」を、Rustでは目的に応じて明確に使い分ける、というイメージです。
| 渡し方 | 記法 | 意味・用途 | Javaとの対比 |
|---|---|---|---|
| 値渡し(ムーブ) | T |
データを完全にあげる。元の場所では使えなくなる。 | Javaにはない概念。 |
| 不変参照(借用) | &T |
読み取り専用で貸す。同時に何個でも持てる。 | Javaの final 参照が近いが、Rustはコンパイラが強制する。 |
| 可変参照 | &mut T |
書き換えの権利付きで貸す。同時に1つしか持てない。 | Javaの通常の参照に近いが、Rustは「同時に1つだけ」という排他制約がある。 |
なぜRustはこんなに厳しいのか
Javaの参照は言ってみれば「制約のない &mut」です。便利ですが、この自由さがバグの温床になることがあります。「面倒なルールだな」と最初は思いましたが、この仕組みは過去のエンジニアたちを長年悩ませてきた問題を根本から防ぐために設計されています。
Java経験者にとって一番身近なのは NullPointerException でしょう。Javaではオブジェクト参照が null になりうるため、どれだけ気をつけていてもヌルポに遭遇します。Rustには null という概念自体が存在せず、値がないかもしれない場合は Option<T> 型で明示する設計になっているので、ヌルポがそもそも起きません。
もう一つはデータ競合です。複数スレッドが同じデータに同時に読み書きしてデータが壊れる問題で、Javaでは synchronized や volatile を使ってプログラマが実行時に防ぐ必要がありました。書き忘れたまま本番に出て、再現困難なバグとして悩まされた経験がある方も多いのではないでしょうか。Rustでは「読み取り専用なら同時に何個でもOK、書き換えるなら同時に1つだけ」というルールをコンパイラが強制することで、データ競合をそもそもコンパイルが通らないようにしています。
つまりRustの所有権と借用の仕組みは、こうした問題を実行時ではなくコンパイル時に防ぐためのものです。書き慣れるまでは「コンパイラに怒られてばかり」という感覚になりますが、裏を返せば今まで実行時に踏んでいた地雷をコンパイル時に全部教えてもらえるということなので、慣れてくるとむしろ安心感があります。
実際にはRustの借用にはもう少し細かいルールがあり、たとえば「可変参照と不変参照は同時に存在できない」「所有権が借用されている間はムーブできない」といった制約もあります。この借用ルールの詳細と具体例については、次回あらためて取り上げる予定です。
今回は「値渡し(ムーブ)」と「不変参照」を中心に、まずはStringでの基本的な使い分けを見ていきます。
2. 「String」で思いっきりハマった話
所有権の仕組みが具体的に影響してくるのが「文字列」です。
Javaと同じノリで文字列を関数に渡そうとすると、Rustではコンパイルエラーが出ます。自分も最初はここで何度も詰まりました。
graph TD
subgraph java [Javaの感覚]
A[String変数] -->|参照を渡す| B(関数)
A -->|そのまま使える| C(別の処理)
end
subgraph move [Rustで意図せずムーブしてしまうパターン]
D[String変数] -->|所有権が移動 ムーブ| E(関数)
D -.->|使えなくなる ❌| F(別の処理)
end
subgraph borrow [参照で借用するパターン]
G[String変数] -->|&strで貸す| H(関数)
G -->|引き続き使える ⭕| I(別の処理)
end
// 🔸 意図せずムーブしてしまうパターン
// String で受け取ると所有権が移動(ムーブ)してしまい、呼び出し元でこの変数が使えなくなる
fn print_name(name: String) {
println!("{}", name);
}
// 🔹 多くの場合はこちらが適切
// &str(文字列スライスへの参照)として「貸す」だけにする
fn print_name(name: &str) {
println!("{}", name);
}
文字列を関数に渡すときは、String ではなく &str(文字列スライスへの参照)で受け取るのが基本のパターンで、これに気づいてからはエラーに悩む回数がぐっと減りました。
ただし、受け取った文字列を構造体のフィールドに格納するなど、関数の中で所有権を持ちたい場合は String で受け取ることもあります。「&str が唯一の正解」というわけではなく、用途に応じて使い分けるという点は頭の片隅に置いておくとよさそうです。
Javaとの考え方の違いを一つひとつ確かめながら進めると、じわじわ理解が深まる感じがあって面白いです。
次回は、借用ルールの詳細と可変参照について、もう少し踏み込んでみようと思います。
コメント