Rustを学び始めると、早い段階で「所有権(Ownership)」という考え方が出てきます。文字列を別の変数に代入しただけで元の変数が使えなくなったり、数値だと使えたりと、最初は戸惑うところです。
組み込みでC言語を長く書いてきた私には、「malloc したメモリを誰が free するのか」を、言語のルールとして決めたものと考えると、所有権が分かりやすくなりました。この記事では、C言語と比べながら、所有権のルール、ムーブ、Copy、借用を順に確かめます。
コードは、ブラウザでRustを動かせるRust Playgroundに貼れば、そのまま試せます。
所有権の3つのルール
Rustの公式の解説書(The Rust Programming Language)では、所有権のルールを次の3つにまとめています。
- Rustの値には、それぞれ「所有者」がいる
- 所有者は、同時に1つだけ
- 所有者がスコープ(
{ }の範囲)を抜けると、値は破棄される
C言語では、malloc で確保したメモリを自分で free します。JavaやGoでは、ガベージコレクション(GC)が実行中に使われなくなったメモリを探して解放します。Rustはどちらでもなく、「所有者がスコープを抜けたところで解放する」というルールを、コンパイルのときに確かめます。ルールに合わないコードは、実行する前にコンパイルエラーになります。
fn main() {
{
let s = String::from("hello"); // ここで文字列のメモリが確保される
println!("{}", s);
} // s がスコープを抜けるので、ここで自動で解放される
}free を書かなくても、} のところで自動で解放されます。
代入すると所有権が移る(ムーブ)
可変長の文字列である String を、別の変数に代入してみます。
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 から s2 へ所有権が移る(ムーブ)
// println!("{}", s1); // コンパイルエラー:s1 はもう使えない
println!("{}", s2); // OK
}s1 の行のコメントを外すと、次のようなコンパイルエラーになります(一部を抜粋)。
error[E0382]: borrow of moved value: `s1`「移動(ムーブ)した値を使おうとしている」という意味です。String の文字の中身はヒープ領域にあり、s2 = s1 でこの中身の持ち主が s2 に移ります。持ち主ではなくなった s1 は使えなくなります。
C言語だとどうなるか
C言語で同じようなことをすると、ポインタだけがコピーされ、同じメモリを指す変数が2つできます。両方から free すると「二重解放」になります。
#include <stdlib.h>
#include <string.h>
typedef struct {
char *data;
} MyString;
int main(void) {
MyString s1;
s1.data = malloc(6);
strcpy(s1.data, "hello");
MyString s2 = s1; // 構造体の代入:ポインタの値だけがコピーされる
free(s1.data);
free(s2.data); // 同じメモリをもう一度解放(二重解放)
return 0;
}C言語はこのコードをそのままコンパイルし、実行して初めて不具合が起きます。実行するたびに出方が違うこともあり、組み込みの現場でも探すのに苦労する種類のバグです。Rustでは、代入した時点で s1 が使えなくなり、解放するのは所有者の s2 だけなので、この二重解放はコンパイルの段階で防がれます。
関数に渡してもムーブする
関数の引数に渡したときも、代入と同じように所有権が移ります。
fn print_and_drop(s: String) {
println!("{}", s);
} // ここで s が解放される
fn main() {
let s1 = String::from("hello");
print_and_drop(s1); // 所有権が関数に移る
// println!("{}", s1); // コンパイルエラー:s1 はもう使えない
}関数に渡したあとも使いたいときは、次に紹介する clone か借用を使います。
中身ごと複製する(clone)
両方の変数を使い続けたいときは、clone() で中身ごと複製します。C言語でいえば、新しく malloc して strcpy するのに近い動きです。
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // ヒープの中身も含めて複製する
println!("s1 = {}, s2 = {}", s1, s2); // 両方使える
}便利ですが、大きなデータを clone するとその分メモリと時間を使います。中身を読むだけなら、次の借用を使うほうが向いています。
数値は代入しても使える(Copy)
数値の場合は、代入したあとも元の変数を使えます。
fn main() {
let x = 42;
let y = x; // 値がコピーされる(ムーブしない)
println!("x = {}, y = {}", x, y); // 両方使える
}i32 などの整数や浮動小数点数、bool、char には、Copy というトレイト(型に付けられる性質)が実装されています。Copy の型は、代入や関数への受け渡しで値がそのまま複製されるので、ムーブが起きません。
| 型の例 | Copy | 代入したとき |
|---|---|---|
i32、f64、bool、char | あり | 値がコピーされ、元の変数も使える |
Copy の型だけでできたタプルや配列((i32, bool)、[i32; 3] など) | あり | 値がコピーされ、元の変数も使える |
String、Vec<T>、Box<T> | なし | 所有権がムーブし、元の変数は使えない |
String のように、ヒープに確保したメモリを持ち、最後に解放が必要な型は Copy にできません。単純に複製すると、C言語の例と同じ二重解放になるからです。数値にも所有者はいますが、代入のたびに別の値ができるので、問題にならないというわけです。
所有権を移さずに貸す(借用)
関数に渡すたびに所有権が移ると不便なので、Rustには「借用」があります。& を付けて参照を渡すと、所有権は元の変数に残ったまま、中身を使わせられます。C言語でいえばポインタを渡すのに近いですが、使える範囲をコンパイラが確かめてくれるところが違います。
fn print_len(s: &String) {
println!("長さ: {}", s.len());
} // 借りているだけなので、ここでは解放されない
fn add_world(s: &mut String) {
s.push_str(", world"); // &mut なら中身を書き換えられる
}
fn main() {
let mut s = String::from("hello");
print_len(&s); // 読むために貸す
add_world(&mut s); // 書き換えるために貸す
println!("{}", s); // hello, world(所有権は s に残っている)
}借用のルール
借用には、次のルールがあります。
- 読むだけの参照(
&)は、同時にいくつでも作れる - 書き換えられる参照(
&mut)は、同時に1つだけ。そのあいだは&も作れない - 参照は、元の値より長く使えない(解放済みのメモリを指す参照は作れない)
fn main() {
let mut s = String::from("hello");
let r1 = &s;
// let r2 = &mut s; // コンパイルエラー:r1 を使っているあいだは書き換え用に貸せない
println!("{}", r1);
}読んでいる途中で別の場所から書き換えられたり、解放したメモリを指すポインタ(ダングリングポインタ)を使ったりするといった、C言語でよくある不具合を、このルールでコンパイルのときに防いでいます。
グローバル変数(static)はどうなる?
組み込みのC言語ではグローバル変数をよく使いますが、Rustでは扱いが厳しくなっています。書き換えない static はそのまま使えますが、書き換えられる static mut を読み書きするには unsafe ブロックが必要です。どこからでも書き換えられる変数は、上の借用のルールをコンパイラが確かめられず、複数のスレッドから同時に書き換えるとデータが壊れるおそれがあるからです。
グローバルな値を安全に書き換えたいときは、Mutex(排他制御)や AtomicU32 などの Atomic 型を使います。
use std::sync::atomic::{AtomicU32, Ordering};
static COUNTER: AtomicU32 = AtomicU32::new(0);
fn main() {
COUNTER.fetch_add(1, Ordering::SeqCst); // unsafe なしで安全に書き換えられる
println!("{}", COUNTER.load(Ordering::SeqCst)); // 1
}まとめ
- 値には所有者が1つだけいて、所有者がスコープを抜けると解放される
Stringなどは、代入や関数への受け渡しで所有権がムーブし、元の変数は使えなくなる。C言語の二重解放をこれで防ぐ- 両方使いたいときは
clone()、読むだけなら借用(&)、書き換えるなら&mut i32などCopyの型は、代入しても値がコピーされるので元の変数も使える- 書き換えられるグローバル変数は
unsafeが必要。Mutexや Atomic 型を使う
最初はコンパイルエラーが多くて厳しく感じますが、C言語では実行してから探していた不具合を、コンパイラが先に教えてくれていると考えると、エラーの意味が分かりやすくなります。もっと詳しく知りたいときは、公式の解説書の日本語版「The Rust Programming Language 日本語版」の所有権の章も参考になります。
ほかの開発の記事は、開発・プログラミングメモにまとめています。



コメント