1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Rust公式チュートリアル(第4章:所有権を理解する)

Last updated at Posted at 2022-07-02

はじめに

The Rust Programming Language 日本語版の勉強のために、自分が大切だと思った箇所に装飾を付けて記録を残しました。

この記事の内容は、以下記事の引用です。
引用元 :
https://doc.rust-jp.rs/book-ja/ch01-00-getting-started.html

所有権を理解する

所有権はRustの最もユニークな機能であり、これのおかげで**ガベージコレクタ(プログラムで使用しなくなったメモリやプログラム間の隙間のメモリ領域を検知し、解放する機能であるガベージコレクションを実行するためのモジュール)**なしで安全性担保を行うことができます。

ゆえに、Rustにおいて所有権がどう動作するのかを理解するのは重要です。

この所有権以外にも、関連する機能をいくつか話していきます。借用、スライス、そして、コンパイラがデータをメモリにどう配置するかです。

所有権とは?

Rustの中心的な機能は、所有権です。
この機能は、説明するのは簡単なのですが、言語の残りの機能全てに関わるほど深い裏の意味を含んでいるのです。

全てのプログラムは、実行中にコンピュータのメモリの使用方法を管理する必要があります。

プログラムが動作するにつれて、定期的に使用されていないメモリを検索するガベージコレクションを持つ言語もありますが、他の言語では、プログラマが明示的にメモリを確保したり、解放したりしなければなりません。

Rustでは第三の選択肢を取っています。
メモリは、コンパイラがコンパイル時にチェックする一定の規則と共に所有権システムに通じて管理されています。

どの所有権機能も、実行中にプログラムの動作を遅くすることはありません

所有権は多くのプログラマにとって新しい概念なので、慣れるまでに時間がかかります。嬉しいことに、Rustと所有権システムの規則の経験を積むと、より自然に安全かつ効率的なコードを構築できるようになります。

所有権を理解した時、Rustを際立たせる機能の理解に対する強固な礎を得ることになるでしょう。

この章では、非常に一般的なデータ構造に着目した例を取り扱うことで所有権を学んでいきます。

スタックとヒープ

多くのプログラミング言語において、スタックヒープについて考える機会はそう多くないでしょう。

しかし、Rustのようなシステムプログラミング言語においては、値がスタックに積まれるかヒープに置かれるかは、言語の振る舞い方や特定の決断を下す理由などに影響以上のものを与えるのです。

この章の後半でスタックとヒープを交えて所有権の一部が解説されるので、ここでちょっと予行演習をしておきましょう。

スタックもヒープも、実行時にコードが使用できるメモリの一部になりますが、異なる手段で構成されています。

【スタック】は、得た順番に値を並べ、逆の順で値を取り除いていきます

変数宣言は全てスタックです。

これは、**last in, first out(最後に入れたものが最遺書に出てくる)**と呼ばれます。

お皿の山を思い浮かべてください。
お皿を追加するときには、山の一番上に起き、お皿が必要になったら、一番上から1枚をとりさりますよね。途中や一番下に追加したり、取り除いたりすることもできません。

データを追加することは、スタックにpushするするといい、データを取り除くことは、スタックからpopすると表現します。

データへのアクセス方法のおかげで、スタックは高速です。
新しいデータを置いたり、データを取得する場所を探す必要が絶対にないわけです。

というのも、その場所は常に一番上だからです。
スタックを高速にする特性は他にもあり、それはスタック上のデータは全て既知の固定さいずでなければならないということです。

コンパイル時にサイズがわからなかったり、サイズが可変のデータについては、代わりにヒープに格納することができます。

【ヒープ】は、もっとごちゃごちゃしています。
ヒープにデータを置く時、あるサイズのスペースを求めます

OSはヒープ上に十分な大きさの空の領域を見つけ、使用中にし、ポインタ(その場所へのアドレス:その場所のアドレスを指す変数)を返します

ポインタとは、その場所へのアドレスです。

この過程は、ヒープに領域を確保する(allocating on the heap)と呼ばれ、時としてそのフレーズを単にallocateするなどと省略したりします。

スタックに値を積むことは、メモリ確保とは考えられません。

ポインタは、既知の固定サイズなので、スタックに保管することが出来ますが、実データが必要になったら、ポインタ(その場所へのアドレス)を追いかける必要があります。

レストランで席を確保することを考えましょう。入店したら、グループの人数を告げ、店員が全員座れる空いている席を探し、そこまで誘導します。

もしグループの誰かが遅れてくるのなら、着いた席の場所を訪ねてあなたを発見することが出来ます。

ヒープへのデータアクセスは、スタックのデータへのアクセスよりも低速です。

ポインタを追って目的の場所に到達しなければならないからです。

現代のプロセッサは、メモリをあちこち行き来しなければ、より速くなります。

似た例えを続けましょう。
レストランで多くのテーブルから注文を受ける給仕人を考えましょう。最も効率的なのは、次のテーブルに移らずに1つのテーブルで全部の注文を受け付けてしまうことです。

テーブルAで注文を受け、それからテーブルBの注文、さらにまたA、それからまたBと渡り歩くのは、かなり低速な過程になってしまうでしょう。

同じ意味で、プロセッサはデータが隔離されている(ヒープではそうなっている可能性がある)よりも近くにある(スタックではこうなる)ほうが、仕事がうまくこなせるのです。

ヒープに大きな領域を確保する行為も時間がかかることがあります。

コードが関数を呼び出すと、**関数に渡された値(ヒープのデータへのポインタも含まれる可能性あり)**と、関数のローカル変数がスタックに載ります。

関数の実行が終了すると、それらの値はスタックから取り除かれます

どの部分のコードがどのヒープ上のデータを使用しているか把握すること、ヒープ上の重複するデータを最小化すること、メモリ不足にならないようにヒープ上の未使用のデータを掃除することはすべて、所有権が解決する問題です。

一度所有権を理解したら、あまり頻繁にスタックとヒープに関して考える必要はなくなるでしょうが、ヒープデータを管理することが所有権の存在する理由だと知っていると、所有権がありのままで動作する理由を説明するのに役立つこともあります。

By かひろくん

値はスタック領域のどこかに格納しなければ、使用できない。

    int memoryAddressNum(スタック領域) = 123456;
    int samplePointer(スタック領域兼ポインタ) = memoryAddressNum;

私達は直接ヒープ領域を触れず、組み込み関数(maloc関数)がヒープ領域を管理してくれている。

また、ヒープの中でしか、ポインタは出てこない。

可変長配列などの実行時に判断するものがヒープ。(例:ファイル読み込み、DBからの読み込み、標準入出力など->値の大きさは毎回変わるから。)

▼C++のコード
https://godbolt.org/z/134eMn

▼ポインターの説明
https://bi.biopapyrus.jp/cpp/syntax/malloc.html

所有権規則

まず、所有権のルールについて見ていきましょう。
この規則を具体化する例を扱っていく間もこれらのルールを肝に銘じておいてください。

  • Rustの各値は、所有者と呼ばれる変数と対応している。
  • いかなる時も所有者は一つである。
  • 所有者がスコープから外れたら、値は破棄される

変数スコープ

第二章で、Rustプログラムの例はすでに見ています。
もう基本的な記法は通り過ぎたので、fn main(){}というコードはもう例に含みません。

したがって、例をなぞっているなら、これからの例はmain関数に手動で入れ込まなければいけなくなるでしょう。

結果的に、例は少々簡潔になり、定型コードよりも具体的な詳細に集中しやすくなります。

所有権の最初の例として、何らかの変数のスコープについて見ていきましょう。

スコープとは、要素が有効になるプログラム内の範囲のことです。以下のような変数があるとしましょう。

let s = "hello";

変数sは、文字列リテラルを参照し、ここでは文字列の値はプログラムのテキストとして**ハードコード(別のところに分けておいた方が良いであろう処理や値をソースコードの中に直接埋め込むこと)**されています。

この変数は、宣言された地点から、現在のスコープの終わりまで有効になります。

以下には、変数sが有効な場所に関する注釈がコメントで追記されています。

{                      // sは、ここでは有効ではない。まだ宣言されていない
    let s = "hello";   // sは、ここから有効になる

    // sで作業をする
}                      // このスコープは終わり。もうsは有効ではない

言い換えると、ここまでに重要な点は2つあります。

  • sがスコープに入ると、有効になる
  • スコープを抜けるまで、有効なまま

ここで、スコープと変数が有効になる期間の関係は、他の言語に類似しています。さて、この理解のもとに、String型を導入して構築していきましょう。

String型

所有権の規則を具体化するには、第3章の「データ型」節で講義したものよりも、より複雑なデータ型が必要になります。

以前講義した型は全てスタックに保管され、スコープが終わるとスタックから取り除かれますが、ヒープに確保されるデータ型を観察して、コンパイラがどうそのデータを掃除すべきタイミングを把握しているかを掘り下げていきたいと思います。

ここでは、例としてString型を使用し、String型の所有権にまつわる部分に着目しましょう。

また、この観点は、標準ライブラリや自分で生成する他の複雑なデータ型にも適用されます。

String型については、第8章でより深く議論します。

すでに文字列リテラルは見かけましたね。
文字列リテラルでは、**文字列の値はプログラムにハードコード(別のところに分けておいた方が良いであろう処理や値をソースコードの中に直接埋め込むこと)**されます。

文字列リテラルは便利ですが、テキストを使いたいかもしれない場面全てに最適なわけではありません

一因は、文字列リテラルが不変であることに起因します。

別の原因は、コードを書く際に、全ての文字列値が判明するわけではないからです。

例えば、ユーザ入力を受け付け、それを保持したいとしたらどうでしょうか?

このような場面用に、Rustでは2種類目の文字列型、String型があります。

このString型はヒープにメモリを確保するので、コンパイル時にはサイズが不要なテキストも保持することが出来るのです。

from関数を使用して、文字列リテラルからString型を生成できます。

// Stringがヒープ
// "hello"はstr型
let s = String::from("hello");

この二重コロンは、string_fromなどの名前を使うのではなく、String型直下のfrom関数を特定する動きをする演算子です。

この種の文字列は、可変化することが出来ます。

let mut s = String::from("hello");
s.push_str(", world!"); //push_str()関数は、リテラルをStringに付け加える
println!("{}", s); //これはhello,world!と出力する

では、ここでの違いはなんでしょうか?
なぜ、String型は可変化出来るのに、リテラルは出来ないのでしょうか?

違いは、これら2つの型がメモリを扱う方法にあります。

メモリと確保

文字列リテラルの場合、中身はコンパイル時に判明しているので、テキストは最終的なバイナリファイルに直接ハードコードされます。

このため、文字列リテラルは、高速で効率的になるのです。

しかし、これらの特性は、その文字列リテラルの不変性にのみ端を発するものです。(端を発する:それがきっかけになって物事が始まる)

残念なことに、コンパイル時にサイズが不明だったり、プログラム実行に合わせてサイズが可変なテキスト型用に1塊のメモリをバイナリに確保しておくことは不可能です。

String型では、可変かつ伸長可能なテキスト破片をサポートするために、コンパイルときには不要な量のメモリをヒープに確保して内容を保持します。

つまり、

  • **メモリは、実行時にOSに要求される
  • String型を使用し終わったら、OSにこのメモリを変換する方法が必要である**

この最初の部分は、すでにしています。
String::from関数を呼んだら、その実装が必要なメモリを要求**するのです。

これは、プログラミング言語において、極めて普遍的です。

しかし、2番目部分は異なります。
**ガベージコレクタ(GC)**付きの言語では、GCがこれ以上、使用されないメモリを検知して片付けるため、プログラマはそのことを考慮する必要はありません。

GCがないなら、メモリがもう使用されないことを見計らって、明示的に変換するコードを呼び出すのは、プログラマの責任になります。

ちょうど要求の際にしたようですね。
これを正確にすることは、歴史的にも難しいプログラミング問題の一つであり続けています。

もし忘れていたら、メモリを無駄にします。
タイミングが早すぎたら、無効な変数を作ってしまいます。

2回解放しても、バグになるわけです。

alloocateとfreeは完璧に1対1対応にしなければならないのです。

Rustは、異なる道を歩んでいます。
ひとたびメモリを所有している変数がスコープを抜けたら、メモリは自動的に変換されます。

こちらの例は、スコープ例を文字列リテラルからString型を使うものに変更したバージョンです。

let s = String::from("hello"); // sはここから有効になる
// sで作業する
} // このスコープはここでおしまい。sはもう有効ではない

String型が必要とするメモリをOSに変換することが自然な地点があります。

s変数がスコープを抜けるときです。

変数がスコープを抜ける時、Rustは特別な関数を呼んでくれます

この関数はdropと呼ばれ、ここにString型の書き手はメモリを変換するコードを配置することができます。

★Rustは、綴じ波括弧で自動的にdrop関数を呼び出します

このパターンは、Rustコードの書かれ方に甚大な影響をもたらします。

現状は簡単そうに見えるかもしれませんが、ヒープ上に確保されたデータを複数の変数に使用させるようなもっと複雑な場面では、コードの振る舞いは、予期しないものになる可能性もあります。

これから、そのような場面を掘り下げてみましょう。

変数とデータの相互作用法:ムーブ

Rustにおいては、複数の変数が同じデータに対して異なる手段で相互作用することができます。

正数を使用した例を見てみましょう。

// 変数xの整数値をyに代入する
let x = 5;
let y = x;

もしかしたら、何をしているのか予想することが出来るでしょう。
「値5をxに束縛する、それからxの値をコピーしてyに束縛する」

これで2つの変数(xとy)が存在し、両方値は5になりました。

これは確かに起こっている現象を説明しています。

なぜなら、正数は既知の固定サイズの単純な値で、これら2つの5という値は、スタックに積まれるからです。

では、Stringバージョンも見ていきましょう。

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

このコードは先程のコードに酷似していますので、動作方法もナジだと思いこんでしまうかもしれません。

要するに、2行目でs1の値をコピーし、s2に束縛するということです。

ところが、これは起こることを言い当てていません。

以下の図を見て、ベールの下でStringに何が起きているかを確かめてください。

String型は、左側に示されているように、3つの部品で出来ています。

- 文字列の中身を保持するメモリへのポインタ

  • 長さ
  • 許容量

この種のデータは、スタックに保持されます。
右側には、中身を保持したヒープ上のメモリがあります。

▼s1に束縛された"hello"という値を保持するStringのメモリ上の表現
image.png

長さは、String型の中身が現在使用しているメモリ量をバイトで表したものです。
長さと許容量の違いは問題になることですが、この文脈では違うので、とりあえずは許容量を無視しても構わないでしょう。

s1にs2を代入すると、String型のデータがコピーされます。
つまり、スタックにあるポインタ、長さ、許容量をコピーするということです。

ポインタが指すヒープ上のデータはコピーしません。
言い換えると、メモリ上のデータ表現は以下のようになるということです。

▼s1のポインタ、長さ、許容量のコピーを保持する変数s2のメモリ上での表現
image.png

メモリ上の表現は、以下のようにはなりません。
これはRustが代わりにヒープデータもコピーするという選択をしていた場合のメモリ表現ですね。
Rustがこれをしていたら、ヒープ上のデータが大きい時にs1=s2という処理の実行時性能がとても悪くなっていた可能性もあるでしょう。
image.png

先程、変数がスコープを抜けたら、Rustは自動的にdrop関数を呼び出し、その変数が使っていたヒープメモリを片付けると述べました。

しかし、2つ上の図で両方のデータポインタが同じ場所を指していることを示しています。
これは問題です。

s2とs1がスコープを抜けたら、両方とも同じメモリを解放してしまいます。
これは二重解放エラーとして知られ、以前触れたメモリ安全性上のバグの1つになります。

メモリを2回解放することは、memory corruption(メモリの崩壊。意図せぬメモリの書き換え)に繋がり、セキュリティ上の脆弱性を生む可能性があります。

メモリ安全性を確保するために、Rustにおいてこの場面で起こることの詳細がもうひとつあります。
確保されたメモリをコピーしようとする代わりに、コンパイラはs1が最早有効ではないと考え、ゆえにs1がスコープを抜けた際に何も開放する必要がなくなるわけです。

s2の生成後にs1を使用しようとしたら、どうなるか確認してみましょう。
動かないでしょう。

let s1 = String::from("hello");
let s2 = s1;
println!("{}, world", s1); //  エラーになる。「println!」は標準出力

※println!とは?
実行時に関数の行で実行される。
マクロの場合は、関数みたいに存在しない。
実行時に関数内にソースコードが貼り付けられるが、
マクロの場合はコンパイル時にマクロが貼り付けられるため関数ジャンプが発生せず、実行速度が速くなる。
マクロはコンパイルする時に適用されるから、実行時に動作する関数とはコンパイル結果が違う

コンパイラが無効化された参照は使用させてくれないので、以下のようなエラーが出るでしょう。

error[E0382]: use of moved value: `s1`
              (ムーブされた値の使用: `s1`)
 --> src/main.rs:5:28
  |
3 |     let s2 = s1;
  |         -- value moved here
4 |
5 |     println!("{}, world!", s1);
  |                            ^^ value used here after move
  |                               (ムーブ後にここで使用されています)
  |
  = note: move occurs because `s1` has type `std::string::String`, which does
  not implement the `Copy` trait
    (注釈: ムーブが起きたのは、`s1`が`std::string::String`という
    `Copy`トレイトを実装していない型だからです)

他の言語を触っている間に"shallow copy"と"deep copy"という単語を耳にしたことがあるなら、
データのコピーなしにポインタと長さ、許容量をコピーするという概念は、shallow copyのように思えるかもしれません。

ですが、コンパイラは最初の変数をも無効化するので、shallow copyと呼ばれる代わりに、ムーブとして知られているわけです。

この例では、s1はs2にムーブされたと表現するでしょう。
以上より、実際に起きていることを以下に示しました。

▼s1が無効化された後のメモリ表現
image.png
これにて、一件落着です。
s2だけが有効なので、スコープを抜けたら、それだけがメモリを解放して終わりになります。

付け加えると、これにより暗示される設計上の選択があります。
Rustでは、自動的にデータの"deep copy"が行われることは絶対にないです。
それ故に、あらゆる自動コピーは、実行時性能の観点でいうと、悪くないと考えて良いことになります。

変数とデータの相互作用法:クローン

仮に、スタック上のデータだけでなく、本当にString型のヒープデータのdeep copyが必要ならば、cloneと呼ばれるよくあるメソッドを使うことができます。

メソッドの多くはプログラミング言語に見られる機能なので、以前に見かけたこともあるんじゃないでしょうか。
これは、cloneメソッドの動作例です。

let s1 = Srting::from("hello");
let s2 = s1.clone();

println!("s1 = {}, s2 = {}", s1, s2);

これは問題なく動作します。
ここでは、ヒープデータが実際にコピーされています。

cloneメソッドの呼び出しを見かけたら、何らかの任意のコードが実行され、その実行コストは高いと把握できます。

何か違うことが起こっているなと見た目でわかるわけです。

スタックのみのデータ:コピー

まだ話題にしていない別の問題があります。
この整数を使用したコードは、うまく動作する有効なものです。

let x = 5;
let y = x;
println!("x = {}, y = {}", x,y);

ですが、このコードは一見今学んだことと矛盾しているように見えます。
cloneメソッドの呼び出しがないのに、xは有効で、yにムーブされませんでした。

その理由は、整数のようなコンパイル時に既知のサイズを持つ型は、スタック上にすっぽり保持されるので、実際の値をコピーするのも高速だからです。

これは変数yを生成した後にもxを無効化したくなる理由がないことを意味します。
換言すると、ここではshallow copyとdeep copyの違いがないことになり、cloneメソッドを呼び出しても、
一般的なshallow copy以上のことをしなくなり、そのまま放置しておける
ということです。

Rustには、copyトレイトと呼ばれる特別な注釈があり、整数のようなスタックに保持される型に対して配置することができます。
(トレイトに関しては、第10章で詳しく話します)

型がcopyトレイトに適合してれば、代入後も古い変数が使用可能になります。

コンパイラは、型やその一部分でもDropトレイトを実装している場合、copyトレイトによる注釈をさせてくれません

型の値がスコープを外れた時に何か特別なことを起こす必要がある場合に、copy注釈を追加すると、コンパイルエラーが出ます。

では、どの型がcopyなのでしょうか?
ある型についてドキュメントをチェックすればいいのですが、一般的な規則として、単純なスカラー値(単独の値で浮動小数点数、論理値、文字の4種類を表す)の集合は何でもcopyであり、メモリの確保が必要だったり、何らかの形態のリソースだったりするものはcopyではありません

ここにcopy型の一部を並べておきます。

  • あらゆる整数型。u32など
  • 論理値型であるbool。trueとfalseという値がある
  • あらゆる浮動小数点型。f64など
  • 文字型であるchar(文字列はString(可変長サイズで変更可能)とstr型(固定サイズで変更不可能)の2つ)
  • タプル。ただ、copyの型だけを含む場合。例えば、(i32, i32)はcopyだが、(i32, String)は違う

所有権と関数

意味論的に、関数に値を渡すことと、値を変数に代入することは似ています。
関数に変数を渡すと、代入のようにムーブやコピーされます。

以下は変数がスコープに入ったり、抜けたりする地点について注釈してある例です。

▼ファイル名 src/main.rs

fn main() {
    let s = String::from("hello");  // sがスコープに入る

    takes_ownership(s);             // sの値が関数にムーブされ...
                                    // ... ここではもう有効ではない

    let x = 5;                      // xがスコープに入る

    makes_copy(x);                  // xも関数にムーブされるが、
                                    // i32はCopyなので、この後にxを使っても
                                    // 大丈夫

} // ここでxがスコープを抜け、sもスコープを抜ける。ただし、sの値はムーブされているので、何も特別なことは起こらない。
  //

fn takes_ownership(some_string: String) { // some_stringがスコープに入る。
    println!("{}", some_string);
} // ここでsome_stringがスコープを抜け、`drop`が呼ばれる。後ろ盾してたメモリが解放される。
  // 

fn makes_copy(some_integer: i32) { // some_integerがスコープに入る
    println!("{}", some_integer);
} // ここでsome_integerがスコープを抜ける。何も特別なことはない。

takes_ownershipの呼び出し後にsを呼び出そうとすると、コンパイラはコンパイルエラーを投げるでしょう。
これらの静的チェックにより、ミスを犯さないでいられます。

sやxを使用するコードをmainに追加してみて、どこで使えて、そして所有権規則により、どこで使えないかを確認してください。

戻り値とスコープ

値を返すことでも、所有権は移動します。

▼ファイル名 src/main.rs

fn main() {
    let s1 = gives_ownership();         // gives_ownershipは、戻り値をs1に
                                        // ムーブする

    let s2 = String::from("hello");     // s2がスコープに入る

    let s3 = takes_and_gives_back(s2);  // s2はtakes_and_gives_backにムーブされ
                                        // 戻り値もs3にムーブされる
} // ここで、s3はスコープを抜け、ドロップされる。s2もスコープを抜けるが、ムーブされているので、
  // 何も起きない。s1もスコープを抜け、ドロップされる。

fn gives_ownership() -> String {             // gives_ownershipは、戻り値を
                                             // 呼び出した関数にムーブする

    let some_string = String::from("hello"); // some_stringがスコープに入る

    some_string                              // some_stringが返され、呼び出し元関数に
                                             // ムーブされる
}

// takes_and_gives_backは、Stringを一つ受け取り、返す。
fn takes_and_gives_back(a_string: String) -> String { // a_stringがスコープに入る。

    a_string  // a_stringが返され、呼び出し元関数にムーブされる
}

変数の所有権は、毎回同じパターンをたどっています。
別の変数に値を代入すると、ムーブされます。

ヒープにデータを含む変数がスコープを抜けると、データが別の変数に所有されるようムーブされていない限り、dropにより片付けられるでしょう。

所有権を取り、またその所有権を戻す、ということを全ての関数でしていたら、ちょっとめんどくさいですね。
関数に値は使わせるものの所有権を取らないようにさせるにはどうするべきでしょうか。

返したいと思うかもしれない関数本体で発生したあらゆるデータとともに、再利用したかったら、渡されたものをまた返さなきゃいけないのは、非常に煩わしいことです。

タプルで複数の値を返すことは可能です。

▼ファイル名 src/main.rs

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

    let (s2, len) = calculate_length(s1);

    //'{}'の長さは、{}です
    println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len(); // len()メソッドは、Stringの長さを返します

    (s, length)
}

でも、これでは大げさすぎますし、ありふれているはずの概念に対して、作業量が多すぎます。
私達にとって幸運なことに、Rustにはこの概念に対する機能があり、参照と呼ばれます。

参照と借用

上記のタプルコードの問題は、String型を呼び出し元の関数に戻さないと、calculate_lengthを呼び出した後に、Stringオブジェクトが使えなくなるk遠出あり、これはStringオブジェクトがcalculate_lengthにムーブされてしまうためでした。

ここで、値の所有権をもらう代わりに引数としてオブジェクトへの参照を取るcalculate_length関数を定義し、使う方法を見てみましょう。

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

    let len = calculate_length(&s1);

    // '{}'の長さは、{}です
    println!("The length of '{}' is {}.", s1, len);
}

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

まず、変数宣言と関数の戻り値にあったタプルコードは全てなくなったことに気づいてください。

2番目に、「&s1」をcalcuate_lengthに渡し、その定義ではString型ではなく、&stringを受け取っていることに注目してください。

これらの&記号が参照であり、&記号の参照のおかげで所有権を貰うことなく値を参照することができるのです。

image.png

ここの関数呼び出しについて、もっと詳しく見てみましょう。

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が有効なスコープは、あらゆる関数の引数のものと同じですが、所有権はないのでsがスコープを抜けても、参照が指しているものをドロップすることはありません

関数が実際の値の代わりに参照を引数に取ると、所有権をもらわないので、所有権を返す目的で値を返す必要はありません

関数の引数に参照を取ることを「借用」と呼びます。

現実世界のように、誰かが何かを所有していたら、それを借りることが出来ます。用が済んだら、返さなきゃいけないわけです。

では、借用した何かを変更しようとしたら、どうなるでしょうか?
答えは、動きません。

▼借用した値を変更しようと試みる

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

    change(&s);
}

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

以下がエラーです。

error[E0596]: cannot borrow immutable borrowed content `*some_string` as mutable
(エラー: 不変な借用をした中身`*some_string`を可変で借用できません)
 --> error.rs:8:5
  |
7 | fn change(some_string: &String) {
  |                        ------- use `&mut String` here to make mutable
8 |     some_string.push_str(", world");
  |     ^^^^^^^^^^^ cannot borrow as mutable

変数が標準で不変なのと全く同様に、参照も不変なのです。
参照している何かを変更することは叶わないわけです。

可変な参照

一捻り加えるだけで、以下のエラーは解決します。

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

    change(&mut s);
}

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

はじめに、sをmutに変えなければなりませんでした。
そして、&mut sで可変な参照を生成し、some_string: &mut Stringで可変な参照を受け入れなければなりません。

ところが、可変な参照には大きな制約が1つあります。
特定のスコープで、ある特定のデータに対しては、1つしか可変な参照を持てないことです。

以下のコードは失敗します。

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

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

以下がエラーです。

error[E0499]: cannot borrow `s` as mutable more than once at a time
(エラー: 一度に`s`を可変として2回以上借用することはできません)
 --> borrow_twice.rs:5:19
  |
4 |     let r1 = &mut s;
  |                   - first mutable borrow occurs here
  |                    (最初の可変な参照はここ)
5 |     let r2 = &mut s;
  |                   ^ second mutable borrow occurs here
  |                    (二つ目の可変な参照はここ)
6 | }
  | - first borrow ends here
  |   (最初の借用はここで終わり)

この制約は、可変化を許可するものの、それを非常に統制の取れた形で行えます。

この制約がある利点は、コンパイラがコンパイル時にデータ競合を防ぐことが出来る点です。

データ競合とは、競合条件と類似していて、これら3つの振る舞いが起こる時に発生します。

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

データ競合は未定義の振る舞いを引き起こし、実行時に追いかけようとした時に特定し解決するのが難しい問題です。

しかし、Rustはデータ競合が起こるコードをコンパイルさえしないので、この問題が発生しないようにしてくれるわけです。

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

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

{
    let r1 = &mut s;

} // r1はここでスコープを抜けるので、問題なく新しい参照を作ることができる

let r2 = &mut s;

可変と不変な参照を組み合わせることに関しても、似たような規則が存在しています。

以下のコードはエラーになります。

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

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

以下がエラーです。

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as
immutable
(エラー: `s`は不変で借用されているので、可変で借用できません)
 --> borrow_thrice.rs:6:19
  |
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 | }
  | - immutable borrow ends here

不変な参照をしている間は、可変な参照をすることができません

不変参照の使用者は、それ以降に値が突然変わることを予想していません。しかしながら、複数の不変参照をすることは可能です。

データを読み込んでいるだけの人に、他人がデータを読みこむことに対して影響を与える能力はないからです。

これらのエラーは、時としてイライラするものではありますが、Rustのコンパイラがバグの可能性を早期に指摘してくれ(それも実行時ではなくコンパイル時に)、問題の発生箇所をずばり示してくれるのだと覚えておいてください。

そうして想定通りにデータの変わらない理由を追いかける必要がなくなります。

宙に浮いた参照

ポインタのある言語では、誤ってダングリングポインタを生成してしまいます。

ダングリングポインタとは、他人に渡されてしまった可能性のあるメモリを指すポインタのことであり、その箇所へのポインタを保持している間にメモリを解放してしまうことで発生します。

対象的にRustでは、コンパイラが参照がダングリング参照に絶対ならないよう保証してくれます。

つまり、何らかのデータへの参照があったら、コンパイラは参照がスコープを抜けるまで、データがスコープを抜けることがないよう確認してくれるわけです。

ダングリング参照を試してみますが、コンパイラはこれをコンパイルエラーで阻止します。

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

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

    &s
}

以下がエラーです。

error[E0106]: missing lifetime specifier
(エラー: ライフタイム指定子がありません)
 --> main.rs:5:16
  |
5 | fn dangle() -> &String {
  |                ^ expected 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 giving it a 'static lifetime
  ('staticライフタイムを与えることを考慮してみてください)

このエラーメッセージは、まだ講義していない機能について触れています。ライフタイムです。

このメッセージはたしかにこのコードが問題になる理由に関する鍵を握っています。

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

dangleコードの各段階で一体何が起こっているのかを詳しく見ていきましょう。

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

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

    &s // String sへの参照を返す
} // ここで、sはスコープを抜け、ドロップされる。そのメモリは消される。
  // 危険だ

sはdangle内で生成されているので、dangleのコードが終わったら、sは解放されてしまいますが、そこへの参照を返そうとしていました。

つまり、この参照は無効なStringを指していると思われるのです。

良くないことなので、コンパイラはこれを阻止してくれます。

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

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

    s
}

これはなんの問題もなく動きます。
所有権はムーブされ、何の解放されることはありません。

参照の規則

参照について議論したことを再確認しましょう。

  • 任意のタイミングで、一つの可変参照か不変な参照いくつでもどちらかを行える
  • 参照は常に有効でなければならない

次は、違う種類の参照を見ていきましょう。
スライスです。

スライス型

所有権のない別のデータ型は、スライス型です。

スライスにより、コレクション全体というより、そのうちの一連の要素を参照することができます。

ここに小さなプログラミング問題があります。
文字列を受け取って、その文字列中の最初の単語を返す関数を書いてください。

関数が文字列中に空白をみつけなかったら、文字列全体が1つの単語に違いないので、文字列全体が返されるべきです。

この関数の**シグニチャ(メソッド名、メソッドに引数、引数の型を組み合わせたもの)**について考えてみましょう。

fn first_word(s: &String) -> ?

この関数、first_wordは引数に**&String**を取ります。
所有権はいらないので、これで十分です。

ですが、何を返すべきでしょうか?
文字列の一部について語る方法が全くありません。

しかし、単語の終端の添字を返すことが出来ますね。

▼String引数へのバイト数で表された添字を返すfirst_word関数

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }

    s.len()
}

Stringの値を要素ごとに見て、空白かどうかを確かめる必要があるので、as_bytesメソッドを使って、Stringオブジェクトをバイト配列に変換しています。

let bytes = s.as_bytes();

次に、そのバイト配列に対して、iterメソッドを使用してイテレータを生成しています。

for (i, &item) in bytes.iter().enumerate() {

いてレーラについて詳しくは第13章で議論します。
今は、iterはコレクション内の各要素を返すメソッドであること、enumerateがiterの結果を結んで、代わりにタプルの一部として各要素を返すことを知っておいてください。

enumerateから帰ってくるタプルの第一要素は添字であり、2番目の要素は(コレクションの)要素への参照になります。

これは、手動で添字を計算するよりも少しだけ便利です。

enumerateメソッドがタプルを返すので、Rustのあらゆる場所同様、パターンを使ってそのタプルを分配できます。

したがって、forループ内でタプルの添字に対するiとタプルの1バイトに対応する$itemを含むパターンを指定しています。

.iter().enumerate()から要素への参照を取得するので、パターンに&を使っています。

forループ内でバイトリテラル表記を使用して空白を表すバイトを検索しています。

空白が見つかったら、その位置を返します。
それ以外の場合、**s.len()**を使って文字列の長さを返します。

    if item == b' ' {
        return i;
    }
}

s.len()

さて、文字列内の最初の単語の終端の添字を見つけ出せるようになりましたが、問題があります。

usize型を単独で返していますが、これは**&Stringの文脈でのみ意味を持つ数値**です。

言い換えると、Stringから切り離された値なので、将来的にも有効である保証がないのです。

first_word関数を仕様するプログラムを考えてみてください。

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

    let word = first_word(&s); // wordの中身は、値5になる

    s.clear(); // Stringを空にする。つまり、""と等しくする

    // wordはまだ値5を保持しているが、もうこの値を有効に使用できる文字列は存在しない。
    // wordは完全に無効なのだ!
}

このプログラムはなんのエラーもなくコンパイルが通り、wordをs.clear()の呼び出し後に使用しても、コンパイルが通ります。

wordはsの状態に全く関連付けられていないので、その値はまだ値5のままです。

その値5を変数sに使用し、最初の単語を取り出そうとすることはできますが、これはバグでしょう。

というのも、sの中身は5をwordに保存してから変わってしまったからです。

word内の添字がsに格納されたデータと同期されなくなるのを心配することは、面倒ですし問題になりやすいです。

これらの添字の管理は、second_word関数を書いたら、さらに難しくなります。

そのシグニチャは以下のようになるはずです。

fn second_word(s: &String) -> (usize, usize) {

※usizeのuとは、符号なしのこと

今、私達は開始と終端の添字を追えるようになりました。
特定の状態のデータから計算されたけど、その状態に全く紐付かない値が増えました。

いつの間にか変わってしまうので、同期を取る必要のある、関連性のない変数が3つになりました。

運のいいことに、Rustにはこの問題への解決策が用意されています。

文字列スライスです。

文字列スライス

文字列スライスとは、Stringの一部への参照で、こんな見た目をしています。

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

let hello = &s[0..5];
let world = &s[6..11];

これは、String全体への参照を取ることに似ていますが、余計な[0..5]という部分が似ています。

String全体への参照というよりも、Stringの一部への参照です。

開始..終点という記法は、開始から始まり、終点未満までずっと続く範囲です。

**[starting_index..ending_index]**と指定することで、角括弧に範囲を使い、スライスを生成できます。

ここでstarting_indexはスライスの最初の位置、endeing_indexはスライスの終端位置よりも1大きくなります。

内部的には、スライスデータの構造は、開始地点とスライスの長さを保持しており、スライスの長さはendeing_indexからstarting_indexを引いたものに対応します。

以上より、**let word = &s[6..11];**の場合には、wordはsの7バイト目へのポインタと5という長さを保持するスライスになるでしょう。

以下は、これを図解しています。
image.png

Rustの「..」という範囲記法で、最初の番号(ゼロ)から始めたければ2連ピリオドの前に値を書かなければいいのです。
つまり、これらは等価になります。

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

let slice = &s[0..2];
let slice = &s[..2];

同様の意味で、Stringの最後のバイトをスライスが含むのならば、末尾の数値を書かなければいいのです。
つまり、これらは等価になります。

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

let len = s.len();

let slice = &s[3..len];
let slice = &s[3..];

さらに、両方の値を省略すると、文字列全体のスライスを得られます。ゆえに、これらも等価です。

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

let len = s.len();

let slice = &s[0..len];
let slice = &s[..];

これら全ての情報を心に留めて、first_wordを書き直してスライスを返すようにしましょう。

文字列スライスを意味する型は、&strと記述します。

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

空白を発見したら、文字列の最初を開始地点、空白の添字を終了地点として使用して文字列スライスを返しています。

これで、first_wordを呼び出すと、元のデータに紐付けられた単独の値を得られるようになりました。

この値は、スライスの開始地点への参照とスライス中の要素数から構成されています。

second_word関数についても、スライスを返すことでうまくいくでしょう。

fn second_word(s: &String) -> &str {

これで素直なAPIになりました。
なぜなら、stringへの参照が有効なままであることがコンパイラが保証してくれるからです。

スライスバージョンのfirst_wordを使用すると、コンパイルエラーが発生します。

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

    let word = first_word(&s);

    s.clear(); // error!    (エラー!)

    println!("the first word is: {}", word);
}

以下がコンパイルエラーです。

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
(エラー: 不変として借用されているので、`s`を可変で借用できません)
  --> src/main.rs:18:5
   |
16 |     let word = first_word(&s);
   |                           -- immutable borrow occurs here (不変借用はここで発生しています)
17 | 
18 |     s.clear(); // error!        (エラー!)
   |     ^^^^^^^^^ mutable borrow occurs here (可変借用はここで発生しています)
19 | 
20 |     println!("the first word is: {}", word);
   |                                       ---- immutable borrow later used here
                                                (不変借用はその後ここで使われています)

error: aborting due to previous error

For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership`.

To learn more, run the command again with --verbose.

借用規則から、何かへの不変な参照がある時、さらに可変な参照を得ることはできないことを思い出してください。

clearはStringを切り詰める必要があるので、可変な参照を得ようとして失敗しているわけです。

RustのおかげでAPIが使いやすくなるだけでなく、ある種のエラー全てを完全にコンパイル時に排除してくれるのです!

文字列リテラルはスライスである

文字列は、バイナリに埋め込まれると話したことを思い出してください。

今やスライスのことを知ったので、文字列リテラルを正しく理解することが出来ます。

let s = "Hello, world!";

ここでのsの型は**&str**型です。
バイナリのその特定の位置を指すスライスです。

これは、文字列が不変である理由にもなっています。
要するに、&strは不変な参照なのです。

引数としての文字列スライス

リテラルやString値のスライスを得ることが出来ると知ると、first_wordに対して、もう1つ改善点を見出すことができます。

シグニチャです。

fn first_word(s: &String) -> &str {

こうすると、同じ関数をString値と&str値両方使えるようになります。

fn first_word(s: &str) -> &str {

もし、文字列スライスがあるなら、それを直接渡せます。
Stringがあるなら、そのString全体のスライスを渡せます

Stringへの参照の代わりに文字列スライスを取るよう関数を定義すると、何も機能を失うことなくAPIをより一般的で有用なものにできます

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

    // first_wordは`String`のスライスに対して機能する
    let word = first_word(&my_string[..]);

    let my_string_literal = "hello world";

    // first_wordは文字列リテラルのスライスに対して機能する
    let word = first_word(&my_string_literal[..]);

    // 文字列リテラルは、すでに文字列スライス*な*ので、
    // スライス記法なしでも機能するのだ!
    let word = first_word(my_string_literal);
}

他のスライス

文字列リテラルは、ご想像どおり文字列に特化したものです。
ですが、もっと一般的なスライス型も存在します。

この配列を考えてください。

let a = [1, 2, 3, 4, 5];

文字列の一部を参照したくなる可能性があるのと同様、配列の一部を参照したくなる可能性もあります。

以下のようにすれば、参照することができます。

let a = [1, 2, 3, 4, 5];

let slice = &a[1..3];

このスライスは、**&[i32]**という型になります。
これも文字列スライスと同じように動作します。

つまり、最初の要素への参照と長さを保持することです。

他の全ての種類のコレクションに対して、この種のスライスは使用するでしょう。

まとめ

所有権、借用、スライスの概念は、コンパイル時にRustプログラムにおいてメモリ安全性を保証します。

Rust言語も他のシステムプログラミング言語と同じように、メモリの使用法について制御させてくれるわけですが、所有者がスコープを抜けた時にデータの所有者に自動的にデータを片付けさせることは、この制御を得るために余計なコードを書いてデバッグする必要がないことを意味します。

所有権は、Rustの他の色んな部分が動作する方法に影響を与えるので、これ以降もこれらの概念についてさらに語っていく予定です。

第5章に映って、structでデータをグループ化することについて見ていきましょう。

1
0
0

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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?