Skip to content

Instantly share code, notes, and snippets.

@y-takagi
Last active April 20, 2021 10:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save y-takagi/9ae968d293076a9060370de860bdec7d to your computer and use it in GitHub Desktop.
Save y-takagi/9ae968d293076a9060370de860bdec7d to your computer and use it in GitHub Desktop.
The Rust Programming Language

The Rust Programming Language

  • 輪読会発表資料
  • 教材

4. 所有権を理解する

所有権はRustの最も特徴的な機能であり、これによってGCなしで自動メモリ管理が可能になっています。

4.1 所有権とは?

全てのプログラムは、実行中にメモリを管理する必要があります。プログラミング言語によってメモリ管理の方法が違いますが、大まかには自動で管理するGCタイプ(SwiftのARCも実行時OHを考慮してGCとする)と、手動でメモリを確保、解放するタイプがあります。Rustでは第3の選択肢を取っています。メモリは、コンパイラがコンパイル時にチェックする一定の規則とともに所有権システムを通じて管理されています。どの所有権機能も、実行中にプログラムの動作を遅くすることはありません。

スタックとヒープ

簡単に説明すると

  • スタック領域
    • コンパイル時にサイズが決定しているデータを格納する場所
    • データアクセスが高速
  • ヒープ領域
    • 実行時にサイズが決定するデータを格納する場所
    • メモリをあちこち行き来する必要があり、データアクセスが低速

所有権規則

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

変数スコープ

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

{                     // sは、ここでは有効ではない。まだ宣言されていない
    let s = "hello";  // sは、ここから有効になる
}                     // このスコープは終わり。もうsは有効ではない

変数sは、文字列リテラルを参照し、ここでは、文字列の値はプログラムのテキストとしてハードコードされています。 この変数はスコープが有効になるとスタックに積まれ、スコープが終わるとスタックから取り除かれます。

以降はヒープに確保されるデータ型を観察して、コンパイラがどうそのデータを掃除すべきタイミングを把握しているかを掘り下げていきます。

String型

Rustには、コンパイル時にサイズが決定する文字列リテラルと、実行時にサイズが決定するString型の2種類の文字列があります。

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

この二重コロンは、string_fromなどの名前を使うのではなく、String型直下のfrom関数を特定する働きをする演算子です。 この記法について詳しくは、第5章の「メソッド記法」節と、第7章の「モジュール定義」でモジュールを使った名前空間分けについて話をするときに議論します。

String型の文字列は可変なので、操作可能です。

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

メモリの確保と解放

String型を例に見てみます。RustではString::from関数を呼ぶと、String型データがヒープに確保されます。 また、メモリを所有している変数がスコープを抜けたら、メモリは自動的に解放されます。

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

変数がスコープを抜ける時、Rustは特別な関数dropを呼んでメモリを解放してくれます。

所有権の移動

Rustにおける変数とデータの関係を見ていきます。まずは整数を使用した簡単な例を見ていきます。

let x = 5;
let y = x;

これは「値5をxに束縛し、それからxの値をコピーしてyに束縛している」ことを表しています。これは直感的で分かりやすいでしょう。

rust-doc-1

次はString型の例を見ていきます。

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

このコードは先程のコードに酷似していますが、実際にはまったく違う動作をします。String型は、下図に示す通り3つの部品でできています。 文字列の中身を保持するメモリへのポインタ、長さ、許容量です。これはスタックに保持され、文字列の中身はヒープ上に保持されます。

rust-doc-2

s1をs2に代入すると、String型のデータがコピーされます。しかし、ポインタが指すヒープ上のデータはコピーされず、下図のようになります。

rust-doc-3

もし、Rustがヒープデータもコピーするという選択をしていた場合、実行時性能がとても悪くなっていた可能性があるでしょう。

先程、変数がスコープを抜けたら、Rustは自動的にdrop関数を呼び出し、その変数が使っていたヒープメモリを片付けると述べました。 しかし、s1とs2がスコープを抜けたら、両方とも同じヒープメモリを解放しようとします。これは二重解放エラーとして知られ、メモリ安全性上のバグの一つになります。

Rustでは、s1がs2に代入された際に、コンパイラがs1は最早有効ではないと考え、故にs1がスコープを抜けた際に何も解放する必要がなくなるわけです。 s2へ代入後にs1を使用したらどうなるか確認してみます。

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

println!("{}, world!", s1);

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

error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:4:28
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 |     println!("{}, world!", s1);
  |                            ^^ value borrowed here after move

これはs1が保持していたデータへの参照が、s2に移ったことを表していて、Rustでは所有権の移動と表現します。 所有権の移動後は、s2だけが有効なので、スコープを抜けたらそれだけがメモリから開放されます。

補足) Rustでは、自動的にデータのコピーが行われることはありません。よって、あらゆる自動コピーは、実行時性能の観点で言うと悪くないと考えられます。

ヒープデータのコピー

もし本当にヒープデータのコピーが必要であれば、cloneと呼ばれるメソッドを使うことができます。

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

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

cloneメソッドの呼び出しを見かけたら、何らかの恣意的なコードが実行され、その実行コストは高いということがわかります。

スタックのみのデータ

以下の例を見てみましょう。

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

このコードは一見、今まで学んできたことと矛盾しているように思えます。cloneメソッドの呼び出しがないのに、xは有効なままになっています。 その理由は、整数のようなコンパイル時にサイズが決定するデータは、スタック上に保持され、実際の値をコピーするのも高速なので、別変数への代入の際にはデータのコピーが行われるからです。

RustにはCopyトレイトと呼ばれる特殊なアノテーションがあり、整数のようなスタックに保持される型に対して指定することができます(トレイトについては10章で説明します)。Copyトレイトを実装するためには、以下を満たす必要があります。

  • メンバーが全てCopyトレイトを実装している
  • 安全にコピー出来ること
  • Dropを実装していないこと

型にCopyアノテーションをつける方法は、付録Cの「導出可能なトレイト」を参照してください。

一般的には、単純なスカラー値はCopyトレイトが実装されており、メモリ確保が必要だったり、何らかの形態のリソースだったりするものはCopyではありません。Copyの型は以下のようなものがあります。

  • 整数型 (u32)
  • 浮動小数点型 (f64)
  • 論理値型 (bool)
  • 文字型 (char)
  • タプル (ただし、Copyの型だけを含む場合)

所有権と関数

関数に値を渡すことは、変数に値を代入することに似ています。

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がスコープを抜ける。スタックから除かれる。

戻り値とスコープ

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

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に移動される

}                                                     // s1とs3はスコープを抜け、dropされる

fn gives_ownership() -> String {
  let some_string = String::from("hello");            // some_stringがスコープに入る
  some_string                                         // some_stringの所有権が呼び出し元に移動する
}

fn takes_and_gives_back(a_string: String) -> String { // a_stringがスコープに入る
  a_string                                            // a_stringの所有権が呼び出し元に移動する
}

所有権を取り、またその所有権を戻す、ということを全ての関数でしていたら、ちょっと面倒です。 例えば以下の例は、やりたいことに対して少し大袈裟なコードになっています。

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();
  (s, length)
}

そこでRustでは参照という機能が備わっています。

4.2 参照と借用

先程の例で、値の所有権をもらう代わりに引数としてオブジェクトへの参照を取る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()
}

上記の&記号が参照を表していて、これのおかげで所有権をもらうことなく値を参照することができます。

rust-doc-4

&s1という記法により、s1の値を参照する参照を生成することができますが、これを所有することはありません。 所有してないということは、指している値は、参照がスコープを抜けてもdropされないということです。

関数の引数に参照を取ることを借用と呼びます。借用した何かを変更しようとしたらどうなるか、見てみます。

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

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

これがエラーです。

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

変数が標準で不変なのと同様に、参照も不変なのです。

可変な参照

可変な参照にする場合は、変数の時同様にmutを加える必要があります。

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

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

ただし、可変な参照には以下の制約があります。

  • 特定のスコープにおいて、同じデータへの可変な参照を複数持てない
  • 特定のスコープにおいて、不変な参照と可変な参照を同時に持てない

この制約の利点は、データ競合の可能性をコンパイル時に防ぐことにあります。

宙に浮いた参照

ポインタのある言語では、無効なメモリ領域を指すポインタ、ダングリングポインタを容易に生成してしまいます。 Rustではコンパイラが、参照がダングリング参照にならないよう保証してくれます。以下に例を示します。

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

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

これはコンパイルエラーになります。sは、dangle内で生成されているので、dangleのコードが終わったら、sは解放されてしまいますが、そこへの参照を返そうとしました。つまり、この参照は無効なStringを指していると思われるのです。よくないことです!コンパイラは、これを阻止してくれるのです。

4.3 スライス型

所有権のない別のデータ型は、スライスです。スライスにより、コレクションの一部または全体を参照することができます。

文字列スライス

文字列スライスとは、Stringの一部への参照で、以下の形で表されます。

let s = String::from("hello world");
let hello = &s[0..5]
let world = &s[6..11];

[starting_index..ending_index]と指定することで、各括弧に範囲を使い、スライスを生成できます。 内部的には、スライスデータ構造は、開始地点とスライスの長さを保持しており、スライスの長さはending_indexからstarting_indexを引いたもの対応します。よって、let world = &s[6..11]の場合には、worldsの7バイト目へのポインタと5という長さを保持するスライスになります。

rust-doc-5

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

let slice = &s[0..2];   // "he"
let slice = &s[..2];    // "he"

let slice = &s[3..len]; // "lo"
let slice = &s[3..];    // "lo"

let slice = &s[0..len]; // "hello"
let slice = &s[..];     // "hello"
注釈) 文字列スライスの範囲添え字は、有効なUTF-8文字境界に置かなければなりません。マルチバイト文字の真ん中で文字列スライスを生成しようとしたら、エラーでプログラムは落ちるでしょう。文字列スライスを導入する目的で、この節ではASCIIのみを想定しています。UTF-8に関するより徹底した議論は、第8章の「文字列でUTF-8エンコードされたテキストを格納する」節で行います。

文字列スライスを使って以下の問題を考えてみます。

問題) 文字列を受け取って、その文字列中の最初の単語を返す関数を書いてください。関数が文字列中に空白を見つけなかったら、文字列全体が一つの単語に違いないので、文字列全体を返します。

fn first_word(s: &String) -> &str {
  let bytes = s.as_bytes();                    // Stringをバイト配列に変換

  for (i, &item) in bytes.iter().enumerate() { // バイト配列に対してイテレータを生成
    if item == b' ' {                          // バイトリテラル表記を使用して空白を検索
      return &s[0..i];                         // 空白が見つかったら、先頭からその場所までのスライスを返す
    }
  }

  &s[..]                                       // 文字列に空白が見つからなかったら、全部返す
}

上記のようにスライスを返す実装をすることで、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);
}

clearpub fn clear(&mut self)と定義されていて、可変な参照を得ようとします。 しかし、借用規則から不変な参照と可変な参照を同じスコープで得ることはできないので、エラーになっています。

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

文字列リテラルは、バイナリに埋め込まれると話したことを思い出してください。今やスライスのことを知ったので、 文字列リテラルを正しく理解することができます。

let s = "Hello, World!";

ここでのsの型は、&strで、バイナリのその特定の位置を指すスライスです。つまり、&strは不変な参照なのです。

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

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

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

もっと経験を積んだRustaceanなら、代わりに以下のように書くでしょう。

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

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

他のスライス

整数スライスをみてみます。

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

このスライスの型は&[i32]になります。

まとめ

所有権、借用、スライスの概念は、コンパイル時にRustプログラムにおいて、メモリ安全性を保証してくれます。 所有権は、Rustの他のいろんな部分に影響を与えているので、これ以降もこれらの概念についてさらに語っていく予定です。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment