12
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

値には「所有権」がある

値を宣言すると、その値の「所有権」という概念が発生します。所有権は、その値が宣言されたスコープが所持します。スコープを抜けると、その値はメモリから解放されます(これをドロップと言います)。

この仕組みは、効率的なメモリ管理のために実装されています。

fn main() {
    let s = String::from("Hello");
    // `s`が"Hello"を所有
} // ここで`s`がスコープを抜けると、`s`が所有していたメモリが解放される

所有権は「移動」することができる

値の所有権は一つしか存在できません。その所有権を持つ変数を「所有者」と呼びます。所有権を他の変数に「移動」させると、元の所有者はその値にアクセスできなくなります。これを所有権のムーブ(move)と呼びます。値へのアクセスはその所有者のみが行えます。古い移動元の変数はもはや所有者ではないので、値を参照しようとするとコンパイルエラーになります。

所有権の移動でもっともシンプルな例は、変数から変数に代入するケースです。このケースは、Stringのような、参照を持つタイプの型に関して起こり得るものです。

fn main() {
    let s1 = String::from("Hello");
    let s2 = s1; // `s1`から`s2`に所有権が移動
    // println!("{}", s1); // コンパイルエラー: s1は無効
    println!("{}", s2); // OK
}

一方で、例えばi32型の変数の場合は所有権の移動は発生しません。

fn main() {
    let x = 5;
    let y = x; // 所有権の移動ではなく、値のコピーが発生
    println!("x: {}, y: {}", x, y); // 両方とも使用可能
}

所有権が移動する(もしくはしない)かどうかは、その変数の型が単純にコピー可能かどうかに基づきます。i32のような型は一つの変数あたりのメモリサイズが既知であるため、スタック上に新たなメモリを確保することが容易であり、変数の代入時は単純に同じ値がコピーされます(すなわち、所有権は移動しない)。
一方で、Stringのような型はヒープを利用します。String型の変数同士で代入を行うと、文字列自身のコピーではなくヒープへのポインタがコピーされます。すなわち、同じメモリアドレスへの参照が同時に二つ以上存在することになります。このままでは、それらの変数がスコープを抜けたとき、同じメモリに対して複数回ドロップを実行してしまいます。それを防ぐために所有権が存在します。常に所有者を一つに制限することにより、その所有者に関してのみドロップするかどうかを判断すればよくなります。

この所有権というのはRustにおける効率と安全性のための機構といえます。Rustならではすぎて、筆者は所有権についてはなかなか一発でコンパイルを通すことができません。。。(Rustのコンパイラは優秀で、所有権のムーブについて厳しい)

説明にもありますが、i32boolのようなコピー可能な型については特に気にしなくてもよいです。一方で、Stringや後日紹介する構造体のような参照でやり取りするような型は要注意です。

また、関数の引数に渡したときも所有権の移動が発生します。

fn main() {
    // String型の変数を作成
    let original_string = String::from("Hello, world!");

    // 所有権が移動してしまう関数呼び出し
    print_and_take_ownership(original_string);

    // ここでコンパイルエラー!
    // original_stringの所有権は既に移動済み
    println!("この行は実行できません: {}", original_string);
}

// String型の値を受け取る関数(所有権も一緒に移動する)
fn print_and_take_ownership(s: String) {
    println!("受け取った文字列: {}", s);
    // 関数のスコープを抜けるとsは破棄される
}

そのため、関数の引数に渡しつつも、そのあとも元の変数を利用したい場合は後述の「借用」を行います。

借用以外の方法として、「関数が新たな値を返却する」というのがあります。所有権を関数に渡すのと引き換えに、その関数が新たな値を戻り値として返却します。関数の戻り値の所有権は呼び出し元が持つため、呼び出し元はそのあとも同値の変数を扱うことができます。

ただし、これは厳密には、関数呼び出しの前後で違う変数を扱っており、新たな変数宣言の手間が発生するため、プラクティスとしては非推奨です。

所有権を移動させたくないときに使える「借用」

コピー不可能な変数をそのまま他の変数に割り当てたり関数に渡したりすると、元の所有権を手放してしまいます。所有者を変更したくない場合、所有権をそのまま渡すのではなく、その値への参照を渡すことで所有権を移すこと無く、別の変数や関数から値を参照することができます。この手段を「借用」と呼びます。

変数の先頭に&をつけると、その変数へのポインタを表現します。あくまでポインタであるため、そのポインタを新しい変数にバインディングしたとしても、その値の所有権は移動しません。また、ある型の値へのポインタは、&<型名>で表現します。

fn main() {
    let s = String::from("Hello");

    print_string(&s); // 借用: 所有権は移動せず`s`を参照する
    println!("元の文字列: {}", s); // 借用なので`s`は有効なまま
}

fn print_string(s: &String) {
    println!("{}", s);
}

借用のパターン

借用は主に共有参照と可変参照があります。

共有参照

元の変数が持つ値への参照を共有します。参照のみ許されているため、同時に複数の共有参照を持つことができます。

let mut data = String::from("hello");

// OK: 複数の共有参照を同時に持つことができる
let ref1 = &data;
let ref2 = &data;
println!("{}, {}", ref1, ref2);

元の値への参照が残っている状態で、元の値に対して変更を加えようとするとコンパイルエラーになります。

// NG: 共有参照が有効な間は値を変更できない
// data.push_str(" world"); // コンパイルエラー

共有参照が無くなれば(スコープを抜けた後であれば)変更は可能です。

// OK: 共有参照がスコープを抜けた後は変更可能
drop(ref1);
drop(ref2);
data.push_str(" world");
println!("{}", data);

可変参照

参照している側(借用している側)から、元の値の変更が可能なパターンです。下記の例では、借用しているmut_ref側から変更を加えています。
このパターンでは、参照を宣言するときには&mutのように&の後にmutをつけて宣言します。

let mut data = String::from("hello");

// OK: 1つの可変参照を作成
let mut_ref = &mut data;
mut_ref.push_str(" world");

可変参照は同時に一つまでしか存在できません。また、可変参照が存在している値に対しては、共有参照も作成できません。

// NG: 可変参照が存在する間は他の参照を作成できない
// let shared_ref = &data;        // コンパイルエラー
// let another_mut = &mut data;   // コンパイルエラー

// OK: 可変参照がスコープを抜けた後は新しい参照を作成可能
println!("{}", mut_ref);
drop(mut_ref);

let shared_ref = &data;
println!("{}", shared_ref);

関数で借用する

前項で取り上げた借用のパターンを、関数で使用する例を示します。

共有参照

関数の引数で借用することで、呼び出す側(下記の例ではmain関数のスコープ内)が所有権を持ったままになります。そのため、同じ変数(下記の例ではoriginal_string)を使いまわせていることが確認できます。

fn main() {
    // 所有権のある String を作成
    let original_string = String::from("こんにちは、世界!");

    // 参照を渡して中身を表示
    print_string(&original_string);

    // 参照を渡して文字数をカウント
    let length = count_chars(&original_string);
    println!("文字列の長さ: {}", length);

    // 元の文字列はまだ使用可能
    println!("元の文字列はまだ有効: {}", original_string);
}

// &String で文字列の参照を受け取る関数
fn print_string(s: &String) {
    println!("受け取った文字列: {}", s);
}

// 文字列の長さを計算する関数
fn count_chars(s: &String) -> usize {
    s.chars().count()
}

可変参照

引数を&mut <引数名>とすることで可変参照を受け取ることができます。
関数に参照を渡すときに&mut <変数名>で渡します。

fn main() {
    // 可変なString型変数を作成
    let mut original_string = String::from("こんにちは");

    // 可変参照を使って文字列を修正
    append_world(&mut original_string);
    println!("1回目の修正後: {}", original_string);
}

// 可変参照を受け取って文字列を追加する関数
fn append_world(s: &mut String) {
    s.push_str("、世界!");
}

他の言語でいうところの「参照渡し(参照の値渡し?)」か「値渡し」と対比するとイメージしやすいかもしれません。少なくともTypeScriptではこんなこと気にしたことはありませんでした。

12
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?