Rustがメモリセーフである理由②(参照を理解する)

Rust

前回の記事でRustには「所有権」という機能があり、「所有権」は以下のメリットをもたらすことを確認しました。
・ガベージコレクションを動作せずに実行できるため性能を高めることができる
・明示的にメモリを確保、開放するための記述が必要ない
・値のムーブにより二重開放エラーを防ぐことができる

ただ、所有権には前回も確認した通り、コードを冗長にしてしまうというデメリットがあります。今回はこのデメリットを解消するための機能である「参照」について説明します。

Rustがメモリセーフである理由①(所有権を理解する)
・Rustがメモリセーフである理由②(参照を理解する)← 今回はココ!

参照とは何か

参照とは所有権を引き渡さずに値を受け渡しするための機能です。 参照により、所有権をもらうことなく値を受け渡しすることが可能になります。関数の引数に参照を取ることを借用と呼びます。

では、具体的に参照を活用することで前回のコードがどのように改善されるかを見ていきましょう。前回の記事の最後に載っていたコード(main.rs)を再掲します。

fn main() {
    let s1 = String::from("hello");
    
    // calculate_lengthに引数としてs1を渡した時点でs1の所有権は無効になる
    // そのため、s1をs2に渡すことで同じ文字列の使用を可能にする
    let (s2, len) = calculate_length(s1);
    println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len();
    (s, length)
}

上記のコードでは関数:calculate_length()は戻り値としてString型のsとusize型(符号なし整数型)のlengthを返します。calculate_length()の目的は与えられた文字列の長さを返すことであり、関数の目的を踏まえると与えられた文字列sを返す必要はありません。

しかし、呼び出し元mainがcalculate_length()に文字列s1を与えてしまうと、s1に格納されたデータの所有権がcalculate_length()に移るため、呼び出し元でs1を再度使うことができなくなります。そのため、calculate_length()では戻り値としてlengthだけではなく与えられた文字列であるsを返却しているというわけです。

上記のコードに参照を適用すると以下のように改善されます。

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

    // s1の参照である&s1を関数に渡すことでs1の所有権は有効のままである
    // そのため、s1はcalculate=length()が呼ばれた後も使用可能となる
    let len = calculate_length(&s1);
    println!("The length of '{}' is {}.", s1, len);
}

// 引数の型はString型の参照である&Stringとなる
// 関数の引数に参照を取ること(今回であれば&Stringを取ること)を借用と呼ぶ
fn calculate_length(s: &String) -> usize {
    s.len()
}

上記のコードでは、calculate_length()に文字列s1ではなく文字列s1の参照である&s1を与えています。こうすることで、s1の所有権はcalculate_length()が呼び出された後も有効になるため、calculate_length()で文字列sを返す必要がなくなり、コードが簡潔になります。

なお、関数の引数に参照を取ることを所有権の「借用」と呼びます。

ここで注意が必要なのが、calculate_length()に参照として与えられた変数sは変更することができない点です。例えば、calculate_length()の末尾にs.push_str(“, length is calcurated”);を付け加えてコンパイルするとコンパイルエラーとなってしまいます。

では、参照された値を変更することはできないのでしょうか。次は参照された値を変更する方法について見ていきます。

可変な参照

可変な参照を作りたい場合は、変数の型を「&mut 参照したい変数の型」とすることで生成することができます。以下にコード例を示します。

fn main() {
    // 可変な文字列であるs1を生成する
    let mut s1 = String::from("hello");

    // push_worldに可変な参照としてs1を渡す
    push_world(&mut s1);
    println!("{}", s1);
}

// push_worldは可変な参照としてsを受け取る
fn push_world(s: &mut String) {
    s.push_str(", world")
}

上記のコードでは、main()がpush_world()にs1を可変な参照として引き渡し、push_world()は文字列sを受け取り、sに”, world”を追加します。

可変な参照の制約

可変な参照を使用する場合は以下に示す規則を満たす必要があります。この規則を満たさない場合はコンパイルエラーとなってしまいます。

①特定のスコープで、ある特定のデータに対しては一つしか可変な参照を持てない
②特定のスコープで、ある特定のデータに対して可変な参照を持っている時に同じデータに対して不変の参照を持つことはできない(逆もしかり)

①を満たさない場合の例を以下に示します。以下では文字列sの可変な参照であるr1を生成した後に同様に可変な参照としてr2を生成しようとしているため、コンパイルエラーとなってしまいます。

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

    let r1 = &mut s;
    // 変数sに対して二つ目の可変な参照であるr2を生成しようとしている
    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を生成した後に、可変な参照としてr2を生成したため、コンパイルエラーとなってしまいます。

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

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

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

可変な参照に制約がある理由

ではなぜ、可変な参照には上記に挙げたような制約があるのでしょうか。

その理由はデータの競合を防ぐためです。 データの競合は以下の条件が満たされる場合に発生します。

①2つ以上のポインタが同じデータに同時にアクセスする。
②少なくとも一つのポインタがデータに書き込みを行っている。
③データへのアクセスを同期する機構が使用されていない

Rustはコンパイル時に上記の制約を持たせることで、 上記①の動きを監視し、②が行われた場合にコンパイルエラーとすることで、データの競合を防ぐことができます。

部分的な参照(スライス)

では最後に、データの全てではなくデータの一部に対して参照する方法を説明します。データの一部に対して参照したい場合はスライスを使用します。スライスは変数の型を「&参照したい変数の型[最初のインデックス…最後のインデックス]」とすることで使用できます。

以下にコード例を示します。関数:first_word()は与えられた文字列の最初の単語を返す関数です。

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

    // wordはsの参照をとる
    let word = first_word(&s);
    println!("the first word is: {}", word);
}

// 文字列スライスの型は&strであるため、戻り値の型を&strと定義する
fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            // 戻り値としてsの部分的な参照(文字列スライス)を返す
            return &s[0..i];
        }
    }
   
    // 文字列スライスとして、文字列の一部ではなく全てを返すこともできる
    &s[..]
}

関数:first_word()では与えられた文字列にスペースが含まれれば、戻り値として文字列スライスである&s[0..i]を返します。文字列を末尾まで検査しスペースが含まれなければ与えられた文字列の全て(&s[..])を返します。なお、文字列スライスの型は&strになります。

参考文献

The Rust Programming Language 日本語版 - The Rust Programming Language 日本語版

Rustの開発コミュニティが提供しているRust学習用のドキュメントです。
今回の記事はこのドキュメントを元に自分が理解に困った箇所を補記する形で記述しています。詳細な解説を確認したい方はこちらを参照ください。

コメント

タイトルとURLをコピーしました