『これJavaならどう書くの?』から始めるRust入門 ③:String引数と所有権編


『これ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では synchronizedvolatile を使ってプログラマが実行時に防ぐ必要がありました。書き忘れたまま本番に出て、再現困難なバグとして悩まされた経験がある方も多いのではないでしょうか。Rustでは「読み取り専用なら同時に何個でもOK、書き換えるなら同時に1つだけ」というルールをコンパイラが強制することで、データ競合をそもそもコンパイルが通らないようにしています。

つまりRustの所有権と借用の仕組みは、こうした問題を実行時ではなくコンパイル時に防ぐためのものです。書き慣れるまでは「コンパイラに怒られてばかり」という感覚になりますが、裏を返せば今まで実行時に踏んでいた地雷をコンパイル時に全部教えてもらえるということなので、慣れてくるとむしろ安心感があります。

➕ 💡 CやC++の世界ではさらに深刻だった
CやC++では、解放済みメモリへのアクセス(ダングリングポインタ)や同じメモリの二重解放といった問題がセキュリティ脆弱性の温床になっていました。Rustでは所有権が常に1つの変数にしか属さず、スコープを抜けた時点で自動的にメモリが解放されるため、こうした問題も構造的に起きません。

実際には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 が唯一の正解」というわけではなく、用途に応じて使い分けるという点は頭の片隅に置いておくとよさそうです。

➕ 💡 Q. 数値を渡したら所有権は移動するの?

「じゃあ数字の 10 を渡しても、所有権が移動するの?」と思ったのですが、i32 などの基本データ型は渡すときに自動的に「コピー」されます(Copy トレイト といいます)。

なので数値を引数に渡しても呼び出し元でそのまま使い続けられます。Javaの int などのプリミティブ型の感覚と同じですね。

ちなみに「トレイト」はJavaでいうインターフェースに近い概念です。さらにRustのトレイトにはデフォルト実装を持たせることもできるので、Java 8以降のインターフェースのデフォルトメソッドを思い浮かべるとイメージしやすいかもしれません。Copy トレイトは「この型は値をコピーして渡せますよ」という性質をコンパイラに伝えるためのものです。トレイトについても今後の回であらためて取り上げる予定です。

Javaとの考え方の違いを一つひとつ確かめながら進めると、じわじわ理解が深まる感じがあって面白いです。
次回は、借用ルールの詳細と可変参照について、もう少し踏み込んでみようと思います。

水上凌佑

 水上凌佑

スキル:Java/C/C++/Node.js/PHP/Python/TypeScript/PostgreSQL/MySQL/SpringBoot/Rust/Linux(Omarchy)/AWS/Gemini/Antigravity/Security/Software Design

コメント

この記事へのコメントはありません。

関連記事