Rustの「借用」と「参照」の仕組み、利用方法を理解する基本からしっかり学ぶRust入門(6)

Rustについて基本からしっかり学んでいく本連載。第6回は、Rustの「借用」と「参照」について。

» 2021年12月14日 05時00分 公開

この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。

「基礎からしっかり学ぶRust入門」のインデックス

連載:基礎からしっかり学ぶRust入門

 本連載のサンプルコードをGitHubで公開しています。こちらからダウンロードしてみてください。具体的な利用方法は連載第1回を参考にしてください。


 Rustによるメモリ安全なプログラミングを実現する機能の代表が所有権と借用です。前回は所有権を取り上げました。今回は、所有権による制約の中で値を利用するための借用と参照の仕組みを解説します。

借用とは?

 借用(borrowing)とは、所有権を持たずに値を利用することです。所有していないために本来は使えませんが、借りることで使えるようにするものです。実は、この借用は今までも意識しないで利用していました。以下のコードは文字列を出力するコードです。なお、今回のサンプルはborrowsパッケージに作っていきます。

let s = String::from("Hello, Rust!!");
println!("{}", s);      // sの1回目の借用
println!("{}", s);      // sの2回目の借用
src/bin/borrow.rsのソースコード

 println!()にString型変数のsを渡しているため、1回目の呼び出しで所有権の移動が発生し、2回目の呼び出しではエラーになると思うかもしれません。しかし実行してみるとエラーにはなりません。これは所有権の移動ではなく借用で済ませているからです。つまり借用とは、所有権を失わずに誰かに値を使わせてあげることとも言い換えることができます。この借用のおかげで、所有権の制約の中での値の柔軟な利用が可能になるのです。

所有権の移動と借用 所有権の移動と借用

借用がない場合

 本連載第5回で示したように、所有権の移動に配慮した代入や関数呼び出しが必要になります。借用がない場合、ちょっとした目的の関数を作りたくても冗長な記述を強いられます。1つありがちな例を挙げます。以下のコードは文字列から文字を探して、見つかればその位置を返すという関数です。

fn main() {
    let s1 = String::from("Hello, world!!");
    let c = 'w';
    let (s2, pos) = search_position(s1, c);
    println!("文字'{}'の「{}」中の位置は{}文字目です。", c, s2, pos);
                // 「文字'w'の「Hello, world!!」中の位置は7文字目です。」
}
// sからcを探してその位置を返す関数
fn search_position(s: String, c: char) -> (String, usize) {
    let pos = s.find(c).unwrap();
    (s, pos)
}
src/bin/search_pos.rsのソースコード

 search_position()関数は、String型変数と文字型変数を受け取って、そのString型変数と位置(整数型)からなるタプルを返します。必要なのは位置だけなのに受け取った変数と戻り値を返している理由は、受け取った文字列を返してあげないと、呼び出し側ではそれを引き続き使えないためです。動きとしては理屈にかなっていますが、位置を調べるのにいちいち元の文字列を渡すのも冗長です。そこで関数を呼び出すとき、所有権を移動させるのではなく借用で済ませられれば、もっとシンプルな形にできそうです。ここで登場するのが参照を使った借用です。

参照とは?

 参照とは、所有権を持たずに値の使用を可能にするデータ型の一つです。C/C++におけるポインタに近いと考えれば理解が早いかもしれません。前回は以下のように文字列リテラルを使う場合には変数s1は&strという参照になるということに触れました。

let s1 = "Hello, World!";

 これは文字列リテラル"Hello, World!"を参照するだけのデータ型である、&str型の変数s1を宣言しています。ここには所有権は一切関わりません。変数s1を使って、"Hello, World!"の値を読み取ることができます。しかし、ポインタと異なり値の変更はできません。あくまで読み取るだけすなわち「参照」だけの型になります。この参照を利用すれば以下のように簡潔に書けます。

fn main() {
    let s = String::from("Hello, world!!");
    let c = 'w';
    let pos = search_position(&s, c);                   // 引数に&を付ける
    println!("文字'{}'の「{}」中の位置は{}文字目です。", c, s, pos);
                // 「文字'w'の「Hello, world!!」中の位置は7文字目です。」
}
// sからcを探してその位置を返す関数
fn search_position(s: &String, c: char) -> usize {      // 引数は参照
    let pos = s.find(c).unwrap();
    pos                                                 // 位置だけを返す
}
src/bin/search_pos2.rsのソースコード

 search_position()関数の戻り値がタプルでなく整数型になり、呼び出しもシンプルになったことが分かると思います。さらに、String型変数sはsearch_position()関数の呼び出し後にも生きている(所有権を保持している)ので、println!()にも渡せます。

 これを可能にしているのが、search_position()関数の仮引数sの型(String)と呼び出し側の実引数に付けた&(アンパサンド)です。&は、値の参照を返す演算子です。既出の&strの&も同じ意味です。参照なので読み取るだけという制約付きですが、値の使用が可能になります。関数内部で変更されないことが保証されるため、所有権を移動させる必要がなくなります。

 println!()は参照を受け取っていたため、所有権の移動を伴わずに値が利用できたというわけです。

参照が有効な場合の制約

 参照型の変数がスコープ内にあって実際に使われているときなどある値への参照が有効な場合は、その値を破棄したり所有権を移動させたりできません(後者の場合、移動先で破棄される可能性があるためです)。つまり、誰かが借用している間は、値を破棄できないのです。次のソースコードを見てみましょう。

let s1 = String::from("hello");
let r = &s1;                            // 参照を作成
println!("s1は「{}」です。", s1);
let s2 = s1;                            // (1)ここで所有権が移動
println!("s2は「{}」です。", s2);
println!("rは「{}」です。", r);          // (2)参照を使用
src/bin/ref_move.rsのソースコード

 上記のコードをコンパイルすると、以下のようにコンパイルエラーになります。

error[E0505]: cannot move out of `s1` because it is borrowed
 --> src/bin/ref_move.rs:6:14
  |
4 |     let r = &s1;
  |             --- borrow of `s1` occurs here
5 |     println!("s1は「{}」です。", s1);
6 |     let s2 = s1;
  |              ^^ move out of `s1` occurs here
7 |     println!("s2は「{}」です。", s2);
8 |     println!("rは「{}」です。", r);
  |                                 - borrow later used here

 エラーの内容は「借用されているのでs1からの移動はできない」です。最終的には(2)でrを使用することが問題ですが、エラーは(1)で所有権が移動した時点で発生します。試しに(2)の行をコメントアウトするとエラーにはなりません。あくまでも参照が有効なときだけの制約となります。

 これがボローチェッカーの仕組みであり、参照の使用を安全なものにしています。なお、スカラー型では常に所有権が複製されるので、借用は基本的に発生しません。借用は、Copyトレイトを持たないString型の値などに限って行われるものだと考えておきましょう。

【補足】C/C++のアドレス演算子

 &はC/C++では値のアドレスを取り出す演算子ですが、ポインタ変数の自由自在な振る舞いを許す象徴のようなものでした。Rustでは、似たような意味合いながらも能力はかなり抑えられたものとなっています。値の更新はできませんし、演算(ポインタを進めたり戻したりすることなど)もできません。後者は、代行する「スライス」という仕組みがあります。スライスは記事の最後に取り上げます。

変更可能な参照

 参照は読み取りだけで値を変更できないのが基本です。しかし、変数宣言と同様にmutを付加することで「変更可能な参照」になります。つまり、C/C++において&演算子で取り出したアドレスのような振る舞いができるようになります。変更可能な参照の例を示します。

fn main() {
    let mut s = String::from("Hello");
    println!("変更前の文字列は「{}」です。", s);
                // 「変更前の文字列は「Hello」です。」
    change_string(&mut s);
    println!("変更された文字列は「{}」です。", s);
                // 「変更された文字列は「Hello, Rust!!」です。」
}
fn change_string(s: &mut String) {
    s.push_str(", Rust!!");
}
src/bin/ref_mod.rsのソースコード

 change_string()関数には、引数にmut付きの参照を渡しています。change_string()関数側では、文字列をString型のpush_str()メソッドを利用して付加しています(push_str()メソッドは、String型変数の値に文字列を追加します)。所有権の移動は起きていませんが内容は変わることに問題はないでしょうか。

 Rustでは、参照を変更可能にするに当たって「変更可能な参照は同時に1個しか持てない」という制約を付けることで問題にならないようにしています。そのため、以下のコードはエラーになります。

let mut s = String::from("Hello");
let r1 = &mut s;
let r2 = &mut s;                // ここでコンパイルエラーになる
println!("{}, {}", r1, r2);

 変数r1, r2はsの変更可能な参照ですが(このとき変数sも変更可能である必要があります)、変数r2の宣言箇所で、変数sを可変として同時に2回以上借用することはできないというエラーになります。

error[E0499]: cannot borrow `s` as mutable more than once at a time

 考えれば自然なことで、複数のコンテキストで値を変更されてしまうとコンパイラはそれを追えません。しかし1つであると仮定すれば、変更の可能性のある箇所を追跡できます。

無効な参照

 無効な参照とは、その通り有効な場所を指し示していない参照のことです。その代表が初期化されていない参照ですが、Rustでは初期化されていない変数を使用するとコンパイルエラーになります。

fn main() {
    let s: &String;     // 初期化していない参照
    let c = 'w';
    let pos = search_position(&s, c);   // ここでコンパイルエラーになる
    println!("文字'{}'の「{}」中の位置は{}文字目です。", c, s, pos);
}
fn search_position(s: &String, c: char) -> usize {
    let pos = s.find(c).unwrap();
    pos
}
src/bin/dand_ref1.rsのソースコード
error[E0381]: borrow of possibly-uninitialized variable: `s`    (1)
 --> src/bin/dand_ref1.rs:5:31
  |
5 |     let pos = search_position(&s, c);
  |                               ^^ use of possibly-uninitialized `s`

 (1)は「おそらく(possibly)初期化されていない(uninitialized)変数sの借用」というエラーです。エラーは、search_position()関数の呼び出し箇所で発生しており、少なくとも未初期化の参照を使おうとすればコンパイル時点で指摘されます。未初期化と無効は少々異なりますが、未初期化の場合にはその旨が指摘される例として取り上げました。

 では以下のようなコードはどうでしょうか。Cでは、しばしばこのようなコードが実際に作られ問題を生み出してきました。

fn main() {
    let s = dangling_function();
    println!("sは{}です。", s);
}
fn dangling_function() -> &String {
    let s = String::from("hello");
    &s
}
src/bin/dand_ref2.rsのソースコード

 dangling_function()関数はString型への参照を返すはずですが、返しているのは参照なので所有権は移動しません。関数内で宣言された変数sはスコープ、つまり関数の終了とともに破棄されます。dangling_function()関数が返す参照は、どこも指し示していないという状態(ダングリング)になります。Rustでこのようなコードを書くと、以下のようにコンパイルエラーになります。

error[E0106]: missing lifetime specifier        (1)
 --> src/bin/dand_ref2.rs:6:27
  |
6 | fn dangling_function() -> &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     (2)
  |
6 | fn dangling_function() -> &'static String {
  |                           ^^^^^^^^

 (1)は「ライフタイム指定子がない」というエラーです。(2)において、ヒントとして「staticライフタイム指定子を付与せよ」とあります。staticとは、プログラムの実行中ずっと有効なスコープです。このスコープを抜けるのはプログラムが終了するときです。ですから、関数を抜けても値は存在し続けるので、その参照を返しても無効にはなりません。ただし、何でもかんでもstaticにするのは推奨できません。プログラム全体を通じて値を保持しておかなければならないなど目的が明確なときに限定しましょう。

スライス

 ここまで、借用の仕組みと参照の利用方法を紹介してきました。最後にスライス(slice)を取り上げます。スライスとは一部分という意味です。

 String型の文字列の一部だけを借用するというスライスを考えます。冒頭で文字列から文字を検索して返す関数を紹介しましたが、その位置は文字列の内容が変わってしまったら意味を成しませんし、バグの原因になります。スライスを用いると、文字列に対して借用が発生するので、誰かが借用中に文字列の内容を変えようとするとコンパイルエラーになります。この仕組みによって、文字列の一部あるいは全部に対する安全な利用が可能になります。

 スライスは、参照を示す&とともに、範囲を示す[..]を指定します。

let s = String::from("Hello, Rust!!");
let t = &s[0..5];       (1)
println!("スライスは「{}」です。", t);  // 「スライスは「Hello」です。」
src/bin/slice1.rsのソースコード

 このコードの場合、String型変数sに対して0文字目から4文字目のスライスを参照型変数tに持たせます(範囲は、「始点..終点+1」で指定するため)。なお、..の前を省略すると0、後を省略すると文字列長になります。すなわち[..]は文字列全体になります。

ここで(1)のスライス取得後に文字列の内容を変更したらどうなるでしょうか? これを次のコードで示します。

let mut s = String::from("Hello, Rust!!");
let t = &s[0..5];               // 不変の参照が発生
s.clear();                      // 不変の参照があるのに値を変更しようとした
println!("スライスは「{}」です。", t);  // ここで不変の参照を使っている
src/bin/slice2.rsのソースコード
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable     (1)
 --> src/bin/slice2.rs:5:5
  |
4 |     let t = &s[0..5];
  |              - immutable borrow occurs here
5 |     s.clear();
  |     ^^^^^^^^^ mutable borrow occurs here
6 |     println!("スライスは「{}」です。", t);
  |                                        - immutable borrow later used here

 (1)のエラーは「不変としても借用されているから、sは可変として借用できない」という意味です。変数tは変数sのスライスですがmutなしになるため不変です。変数sにclear()関数を実行しようとしていますが、この関数は文字列を消去(変更)します。変数sはmut付きなので変更できるように見えますが、不変の変数tで借用されているため変更はできずエラーとなるのです。

 スライスは配列などのコレクション全般に使用できます。安全に配列の一部を扱うのに一役買うので覚えておきましょう。

まとめ

 今回は、Rustの持つメモリ安全性を実現する仕組みである所有権に続き、借用と参照、そしてスライスを解説しました。参照やスライスを利用すると、所有権の仕組みの中でメモリ安全を確保しながら便利にメモリを使えることがお分かりいただけたと思います。

 次回は、さまざまな型を作る構造体を紹介します。

筆者紹介

WINGSプロジェクト

有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティー(代表山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手掛ける。2021年10月時点での登録メンバーは55人で、現在も執筆メンバーを募集中。興味のある方は、どしどし応募頂きたい。著書、記事多数。

サーバーサイド技術の学び舎 - WINGS(https://wings.msn.to/
RSS(https://wings.msn.to/contents/rss.php
Twitter: @yyamada(https://twitter.com/yyamada
Facebook(https://www.facebook.com/WINGSProject


Copyright © ITmedia, Inc. All Rights Reserved.

スポンサーからのお知らせPR

注目のテーマ

Microsoft & Windows最前線2025
AI for エンジニアリング
ローコード/ノーコード セントラル by @IT - ITエンジニアがビジネスの中心で活躍する組織へ
Cloud Native Central by @IT - スケーラブルな能力を組織に
システム開発ノウハウ 【発注ナビ】PR
あなたにおすすめの記事PR

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。