参照と借用

Beginner

This tutorial is from open-source community. Access the source code

はじめに

参照と借用へようこそ。この実験は、Rust Bookの一部です。LabEx で Rust のスキルを練習することができます。

この実験では、Rust における参照を使って値を所有する代わりに借用する方法を学びます。これにより、呼び出し元の関数に所有権を戻す必要なく、データを渡して操作することができます。

参照と借用

リスト 4-5 のタプルコードの問題は、calculate_length関数に渡す前にStringを返さなければならないことです。なぜなら、calculate_length関数に渡した後でもStringを使い続ける必要があるからです。なぜなら、Stringcalculate_length関数に移動してしまうからです。代わりに、String値への参照を提供することができます。「参照」は、そのアドレスに格納されているデータにアクセスするためにたどることができるアドレスのようなポインタと同じです。そのデータは他の変数によって所有されています。ポインタとは異なり、参照はその参照の生存期間中、特定の型の有効な値を指すことが保証されています。

以下は、値の所有権を取得する代わりにオブジェクトへの参照をパラメータとして持つcalculate_length関数を定義して使用する方法です。

ファイル名:src/main.rs

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{s1}' is {len}.");
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

まず、変数宣言と関数の返り値にあるすべてのタプルコードが消えていることに注意してください。次に、&s1calculate_lengthに渡し、その定義ではStringではなく&Stringを受け取ることに注意してください。これらのアンパサンドは「参照」を表し、所有権を取得することなく値を参照することができます。図 4-5 はこの概念を示しています。

図 4-5: &String sString s1を指す図

注:&を使って参照することの逆は、参照解除です。これは、参照解除演算子*を使って行われます。第 8 章で参照解除演算子のいくつかの使い方を見て、第 15 章で参照解除の詳細について説明します。

ここで関数呼び出しをもう少し詳しく見てみましょう。

let s1 = String::from("hello");

let len = calculate_length(&s1);

&s1の構文は、s1の値を参照する参照を作成しますが、その所有権は取得しません。所有権を取得していないため、参照が使用されなくなったときに参照先の値は破棄されません。

同様に、関数のシグネチャは&を使って、パラメータsの型が参照であることを示しています。説明用の注釈を追加しましょう。

fn calculate_length(s: &String) -> usize { // s は String への参照
    s.len()
} // ここで、s はスコープ外になります。ただし、s が所有しているものではないため、
  // String は破棄されません

変数sが有効なスコープは、他の関数パラメータのスコープと同じですが、参照が指す値はsが使用されなくなったときに破棄されません。なぜなら、sは所有権を持っていないからです。関数が実際の値ではなく参照をパラメータとして持つ場合、所有権を返すために値を返す必要はありません。なぜなら、所有権を持っていなかったからです。

参照を作成する操作を「借用」と呼びます。現実の生活のように、ある人が何かを所有している場合、あなたは彼らからそれを借用することができます。使い終わったら、必ず返さなければなりません。あなたはそれを所有していません。

では、借用しているものを変更しようとするとどうなりますか?リスト 4-6 のコードを試してみてください。ネタバレ注意:動作しません!

ファイル名:src/main.rs

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

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}

リスト 4-6: 借用した値を変更しようとする

エラーはこちらです。

error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&`
reference
 --> src/main.rs:8:5
  |
7 | fn change(some_string: &String) {
  |                        ------- help: consider changing this to be a mutable
reference: `&mut String`
8 |     some_string.push_str(", world");
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `some_string` is a `&` reference, so
the data it refers to cannot be borrowed as mutable

変数はデフォルトで不変であるように、参照も同様です。参照先のものを変更することはできません。

可変参照

リスト 4-6 のコードを修正して、借用した値を変更できるようにするには、少しの小さな修正が必要です。それは、代わりに「可変参照」を使用します。

ファイル名:src/main.rs

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

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

まず、smutに変更します。次に、change関数を呼び出すときに&mut sで可変参照を作成し、関数のシグネチャを更新してsome_string: &mut Stringで可変参照を受け取るようにします。これにより、change関数が借用した値を変更することが非常に明確になります。

可変参照には大きな制限があります。値に対して可変参照を持っている場合、その値に対する他の参照はあり得ません。このコードはsに対して 2 つの可変参照を作成しようとしていますが、失敗します。

ファイル名:src/main.rs

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

let r1 = &mut s;
let r2 = &mut s;

println!("{r1}, {r2}");

エラーはこちらです。

error[E0499]: cannot borrow `s` as mutable more than once at a time
 --> src/main.rs:5:14
  |
4 |     let r1 = &mut s;
  |              ------ first mutable borrow occurs here
5 |     let r2 = &mut s;
  |              ^^^^^^ second mutable borrow occurs here
6 |
7 |     println!("{r1}, {r2}");
  |                -- first borrow later used here

このエラーは、一度にsを可変参照として複数回借用できないため、このコードは無効であることを示しています。最初の可変参照はr1にあり、println!で使用されるまで続かなければなりません。しかし、その可変参照の作成と使用の間に、r2sと同じデータを借用する別の可変参照を作成しようとしました。

同じデータに対する複数の可変参照を同時に防ぐ制限は、変更を可能にしますが、非常に制御された方法で行います。これは、新しい Rust プログラマが苦労することです。なぜなら、ほとんどの言語では、好きなときに変更できるからです。この制限の利点は、Rust がコンパイル時にデータ競合を防ぐことができることです。「データ競合」は、競合条件に似ており、次の 3 つの動作が発生するときに発生します。

  • 2 つ以上のポインタが同時に同じデータにアクセスする。
  • 少なくとも 1 つのポインタがデータに書き込むために使用されている。
  • データへのアクセスを同期するメカニズムがない。

データ競合は未定義の動作を引き起こし、実行時にそれを追跡しようとすると診断と修正が困難になる場合があります。Rust は、データ競合のあるコードをコンパイルしないことでこの問題を防ぎます!

いつものように、波括弧を使って新しいスコープを作成することができます。これにより、複数の可変参照が可能になりますが、同時にはできません。

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

{
    let r1 = &mut s;
} // ここで r1 はスコープ外になります。だから、問題なく新しい参照を作成できます

let r2 = &mut s;

Rust は、可変参照と不変参照を組み合わせるときに同様のルールを強制します。このコードはエラーになります。

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

let r1 = &s; // 問題なし
let r2 = &s; // 問題なし
let r3 = &mut s; // 大きな問題

println!("{r1}, {r2}, and {r3}");

エラーはこちらです。

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as
immutable
 --> src/main.rs:6:14
  |
4 |     let r1 = &s; // no problem
  |              -- immutable borrow occurs here
5 |     let r2 = &s; // no problem
6 |     let r3 = &mut s; // BIG PROBLEM
  |              ^^^^^^ mutable borrow occurs here
7 |
8 |     println!("{r1}, {r2}, and {r3}");
  |                -- immutable borrow later used here

ふーん!同じ値に対して不変参照がある間に可変参照を持つこともできません。

不変参照のユーザーは、値が突然変更されることを期待していません!ただし、複数の不変参照は許可されています。なぜなら、データを読み取るだけの誰もが、他の人のデータの読み取りに影響を与える能力を持っていないからです。

参照のスコープは、それが導入された場所から始まり、その参照が最後に使用される時点まで続きます。たとえば、このコードはコンパイルされます。なぜなら、不変参照の最後の使用であるprintln!は、可変参照が導入される前に発生するからです。

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

let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{r1} and {r2}");
// この時点以降、変数 r1 と r2 は使用されません

let r3 = &mut s; // no problem
println!("{r3}");

不変参照r1r2のスコープは、最後に使用されるprintln!の後で終了します。これは、可変参照r3が作成される前です。これらのスコープは重複していません。したがって、このコードは許可されます。コンパイラは、スコープの終了前の時点で参照がもはや使用されていないことを判断できます。

借用エラーは時々悔しいことがありますが、Rust コンパイラが潜在的なバグを早期に(実行時ではなくコンパイル時)指摘し、問題の正確な場所を示してくれることを忘れないでください。そうすれば、データが思ったものと異なる原因を追跡する必要がありません。

浮動参照

ポインタを持つ言語では、メモリの一部を解放しながらそのメモリへのポインタを保持することで、誤って「浮動ポインタ」(メモリ内の場所を参照するポインタであって、他の誰かに与えられている可能性のある場所を参照するもの)を作成してしまうことが簡単です。対照的に、Rust では、コンパイラが参照が浮動参照になることは決してないことを保証しています。つまり、あるデータへの参照がある場合、コンパイラは、そのデータへの参照がなくなる前に、そのデータがスコープ外にならないようにします。

浮動参照を作成して、Rust がコンパイル時エラーでそれを防ぐ方法を見てみましょう。

ファイル名:src/main.rs

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
    let s = String::from("hello");

    &s
}

エラーはこちらです。

error[E0106]: missing lifetime specifier
 --> src/main.rs:5:16
  |
5 | fn dangle() -> &String {
  |                ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value,
but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
  |
5 | fn dangle() -> &'static String {
  |                ~~~~~~~~

このエラーメッセージは、まだ扱っていない機能である「生存期間」に関連しています。第 10 章で生存期間について詳細に説明します。ただし、生存期間に関する部分を無視すると、このエラーメッセージには、なぜこのコードが問題なのかを示す鍵が含まれています。

this function's return type contains a borrowed value, but there
is no value for it to be borrowed from

dangleコードの各段階で実際に何が起こっているか、もう少し詳しく見てみましょう。

// src/main.rs
fn dangle() -> &String { // dangleはStringへの参照を返す

    let s = String::from("hello"); // sは新しいString

    &s // String、sへの参照を返す
} // ここで、sはスコープ外になり、破棄されます。したがって、そのメモリは消えます
  // 危険!

sdangleの中で作成されるため、dangleのコードが終了すると、sはデアロケートされます。しかし、その参照を返そうとしました。つまり、この参照は無効なStringを指すことになります。これは良くありません!Rust はこれを許さないです。

ここでの解決策は、直接Stringを返すことです。

fn no_dangle() -> String {
    let s = String::from("hello");

    s
}

これは問題なく動作します。所有権が移動し、何もデアロケートされません。

参照のルール

参照に関して議論した内容をまとめましょう。

  • 任意の時点で、可変参照を 1 つ または 不変参照を複数持つことができます。
  • 参照は常に有効でなければなりません。

次に、別の種類の参照であるスライスについて見ていきます。

まとめ

おめでとうございます!あなたは参照と借用の実験を完了しました。あなたのスキルを向上させるために、LabEx でさらに多くの実験を行うことができます。