[PR] 本記事にはプロモーションが含まれています。

【Rust】所有権(Ownership)とは?ムーブ・Copy・借用を、C言語の malloc / free と比べて理解する

Rust

Rustを学び始めると、早い段階で「所有権(Ownership)」という考え方が出てきます。文字列を別の変数に代入しただけで元の変数が使えなくなったり、数値だと使えたりと、最初は戸惑うところです。

組み込みでC言語を長く書いてきた私には、「malloc したメモリを誰が free するのか」を、言語のルールとして決めたものと考えると、所有権が分かりやすくなりました。この記事では、C言語と比べながら、所有権のルール、ムーブ、Copy、借用を順に確かめます。

コードは、ブラウザでRustを動かせるRust Playgroundに貼れば、そのまま試せます。

所有権の3つのルール

Rustの公式の解説書(The Rust Programming Language)では、所有権のルールを次の3つにまとめています。

  1. Rustの値には、それぞれ「所有者」がいる
  2. 所有者は、同時に1つだけ
  3. 所有者がスコープ({ } の範囲)を抜けると、値は破棄される

C言語では、malloc で確保したメモリを自分で free します。JavaやGoでは、ガベージコレクション(GC)が実行中に使われなくなったメモリを探して解放します。Rustはどちらでもなく、「所有者がスコープを抜けたところで解放する」というルールを、コンパイルのときに確かめます。ルールに合わないコードは、実行する前にコンパイルエラーになります。

Rust
fn main() {
    {
        let s = String::from("hello"); // ここで文字列のメモリが確保される
        println!("{}", s);
    } // s がスコープを抜けるので、ここで自動で解放される
}

free を書かなくても、} のところで自動で解放されます。

代入すると所有権が移る(ムーブ)

可変長の文字列である String を、別の変数に代入してみます。

Rust
fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // s1 から s2 へ所有権が移る(ムーブ)

    // println!("{}", s1); // コンパイルエラー:s1 はもう使えない
    println!("{}", s2);    // OK
}

s1 の行のコメントを外すと、次のようなコンパイルエラーになります(一部を抜粋)。

Plaintext
error[E0382]: borrow of moved value: `s1`

「移動(ムーブ)した値を使おうとしている」という意味です。String の文字の中身はヒープ領域にあり、s2 = s1 でこの中身の持ち主が s2 に移ります。持ち主ではなくなった s1 は使えなくなります。

C言語だとどうなるか

C言語で同じようなことをすると、ポインタだけがコピーされ、同じメモリを指す変数が2つできます。両方から free すると「二重解放」になります。

C
#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 だけなので、この二重解放はコンパイルの段階で防がれます。

関数に渡してもムーブする

関数の引数に渡したときも、代入と同じように所有権が移ります。

Rust
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 するのに近い動きです。

Rust
fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone(); // ヒープの中身も含めて複製する

    println!("s1 = {}, s2 = {}", s1, s2); // 両方使える
}

便利ですが、大きなデータを clone するとその分メモリと時間を使います。中身を読むだけなら、次の借用を使うほうが向いています。

数値は代入しても使える(Copy)

数値の場合は、代入したあとも元の変数を使えます。

Rust
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言語でいえばポインタを渡すのに近いですが、使える範囲をコンパイラが確かめてくれるところが違います。

Rust
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つだけ。そのあいだは & も作れない
  • 参照は、元の値より長く使えない(解放済みのメモリを指す参照は作れない)
Rust
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 型を使います。

Rust
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 日本語版」の所有権の章も参考になります。

ほかの開発の記事は、開発・プログラミングメモにまとめています。

ほかの開発・プログラミングの記事は、開発・プログラミングメモの目次から、テーマ別に探せます。

このブログは、にほんブログ村と人気ブログランキングに参加しています。記事が役に立ったら、下のランキングのバナーのどれかを押して応援してもらえると励みになります。

ブログランキング・にほんブログ村へにほんブログ村

サーバー構築・運用ランキング プログラムランキング

Rust
watasoftdevをフォローする

コメント