目次
4.0 概要
- 所有権とは
- 参照
- 借用
- スライス
- コンパイラはデータをメモリにどう配置するか
4.1 所有権とは
所有権は「ヒープ領域にメモリ領域を確保したり、それを解放する機能を言語側で管理するための概念」
所有権により以下が可能になる
- どの部分のコードがどのヒープ上のデータを使用するかの把握
- ヒープ上のデータの重複の最小化
- メモリ不足の防止
コラム: 「スタックとヒープ」
スタック
- コンパイル時に必要メモリサイズが分かっているデータを保持
- データは固定長
- 得た順にデータを並べ、逆順で取り除く
ヒープ
- コンパイル時には必要メモリサイズが不明なデータを保持
- OS が管理
- OS に、ある大きさのメモリ領域をヒープ領域に確保するよう依頼すると、OS はメモリ領域を確保したのちに、そのメモリ領域のベースアドレスを返す
- そのポインタを指定してオブジェクトの破棄をOSに依頼すると、メモリ領域が解放され、貸出可能状態に戻る
所有権の規則
- Rust の各値は、所有者と呼ばれる変数と対応している。
- いかなる時も所有者は一つである。
- 所有者がスコープから外れたら、値は破棄される。
例1: str 型
{
// *1
let s = "hello"; // *2
// *3
} // *4
- 上記のコード内で変数
s
にはプログラムにハードコードされた"hello"
という文字列への参照が束縛される - この文字列への参照は、
s
が宣言された行 (*2 の時点) からs
がスコープから抜けるまで (*3 まで) はメモリ上に存在するが、それ以外の領域では存在しない
例2:ヒープに値を保持する場合(String型)
{
// *1
let s = String::from("hello"); // *2
// *3
} // *4
上記のコードでは
- 変数
s
の宣言時に OS に(ヒープ領域内での)メモリ領域の確保が依頼され - 確保された領域のベースアドレスなどの情報( pointer, size, capacity )が
s
に束縛される -
s
に束縛された値は *2 ~ *3 の範囲ではメモリ上に存在するが、それ以外の領域では存在しない - また、ヒープ領域に確保されたメモリ領域は
s
がスコープを抜けると同時に解放される- より厳密には、以下のような仕組みになっている:
- Rust では、閉じ括弧
}
が自動的に(該当スコープで有効なすべての(Drop
トレイトが有効な)変数の)drop
関数を呼び出す -
drop
はその変数と結びついたヒープ領域内のメモリ領域を解放するよう定義されている
- Rust では、閉じ括弧
- より厳密には、以下のような仕組みになっている:
二重解放エラーを防ぐ仕組み(ムーブセマンティクス)
- Rust では2つ以上の異なる変数に、同じヒープ領域へのポインタが束縛されることはない
- ヒープ領域と結びついたある変数
a
をほかの変数b
に代入let b = a
すると、a
はそれ以降無効化される
- ヒープ領域と結びついたある変数
例:String型の変数のムーブの挙動
let s1 = String::from("hello");
によって
- ヒープ領域内に
b'h'
,b'e'
,b'l'
,b'l'
,b'o'
の一連のデータが保持され - このメモリ領域へのポインタがスタック領域に保持されたとする
- この時、このポインタは変数
s1
に束縛される
ここでさらに
let s2 = s1;
とすると
- 変数
s2
にヒープ領域へのポインタが束縛され - 変数
s1
は無効化される
- 無効化された変数を利用しようとするとコンパイルエラーが発生する
- たとえば以下のコードコンパイルエラーを起こす
let s1 = String::from("hello"); let s2 = s1; // この時点で s1 は無効化される println!("s1: {}, s2: {}", s1, s2);
ヒープ領域内のデータを複製する方法(クローン)
- スタック上のデータだけではなくヒープ領域内のデータの deep copy が必要なら
clone
メソッドを用いる - たとえば以下のコードは問題なく動く
let s1 = String::from("hello"); let s2 = s1.clone(); // clone メソッドを使うことでヒープ領域上のデータごと複製されるため、s1 は破棄されない println!("s1: {}, s2: {}", s1, s2);
スタックのみのデータの複製(コピー)
- スタック上で完結するデータ型には
Copy
トレイトが実装されていることが期待される -
Copy
トレイトが実装された型の値はムーブされずにコピーされる -
Drop
トレイトを実装した型、もしくはDrop
トレイトを実装した型を一部に含む型にはCopy
トレイトの実装ができない(コンパイルエラーを起こす) - 以下は自動的に
Copy
- スカラー型
-
Copy
の型だけを含むタプル
所有権と関数
- 関数に変数を渡すと、その変数のもつ所有権が関数内に移動する
- 変数を別の変数に代入した場合と同様に、あるヒープ領域と結びついた変数
a
を関数f
に渡す( つまりf(a)
のようにする) と、a
はそれ以降無効化される
- 変数を別の変数に代入した場合と同様に、あるヒープ領域と結びついた変数
例:所有権と関数
fn main() {
let s = String::from("hello"); // sがスコープに入る
takes_ownership(s); // 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にムーブされる
} // ここで、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が返され、呼び出し元関数にムーブされる
}
4.2 参照と借用
参照
-
変数
hoge
の参照は&hoge
で取得できる -
型
Hoge
の変数の参照の型は&Hoge
となる -
&hoge
にはhoge
へのポインタ(=「hoge
に束縛された(ヒープデータへの)ポインタ」へのポインタ)が束縛される -
&hoge
がスコープを抜けてもhoge
に結びついたヒープデータはドロップされない -
&hoge
がスコープを抜けてもhoge
に束縛された(ヒープデータへの)ポインタはドロップされない -
&hoge
がスコープを抜けると&hoge
束縛されたhoge
へのポインタが消去される -
参照外し演算子
*
で参照を外せる -
例:以下のコードに登場する
s
とその参照s = &s1
の関係は下図の通り;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() }
参照と関数
- 関数の引数に参照を取ることを借用と呼ぶ
可変参照
- 参照はデフォルトではimmutable(不変)
- 可変変数
hoge
の可変参照は&mut hoge
で取得できる - 例:
fn main() { let mut s = String::from("hello"); change(&mut s); } fn change(some_string: &mut String) { some_string.push_str(", world"); }
参照についてのルール
「参照は、一つの可変参照か複数の不変参照」
「ダングリング参照は許されない」
1. 一つのスコープ内では、一つのデータに対して、一つしか可変な参照を作れない
- このルールにより、複数のポインタが同じデータに同時にアクセスすることで発生する問題などを防げる
- 例1:以下のコードはコンパイルエラーを起こす
let mut s = String::from("hello"); let r1 = &mut s; let r2 = &mut s; println!("{}, {}", r1, r2);
- 例2:以下のコードは問題なく動く
let mut s = String::from("hello"); { let r1 = &mut s; } // r1はここでスコープを抜けるので、問題なく新しい参照を作ることができる let r2 = &mut s;
2. 不変参照と可変参照は同一スコープ内で共存できない
3. 不変参照は複数共存可能
- 以下のコードはコンパイルエラーを起こす
let mut s = String::from("hello"); let r1 = &s; // 問題なし let r2 = &s; // 問題なし let r3 = &mut s; // 問題あり!
4. ダングリング参照はコンパイラエラーで阻止される
- 以下のコードはコンパイルエラーを起こす
fn main() { let reference_to_nothing = dangle(); } fn dangle() -> &String { let s = String::from("hello"); &s // ここで s はドロップされる // --> ドロップ済みの値への参照をこの関数の外側にムーブしようとしていることになる // --> 無効な参照を作る不法な操作なのでコンパイラに怒られる }
4.3 スライス型
-
スライスにより、コレクション全体ではなく、その内の一連の要素を参照することができる
-
スライスは、
&hoge[1..3]
のような形で取得する -
スライスは、元のコレクションオブジェクトの不変参照である
-
スライスは、通常の参照と異なり、スタック上のポインタへのポインタではなく、ヒープデータへのポインタを保持する
-
このような成り立ちのために、スライスのスライスはスライスである(型は変わらない)
- ただし、元のコレクションオブジェクトとは型が異なる
- たとえば、
String
型に対するスライスは&str
,&str
のスライスは&str
- たとえば、
[i32; 10]
型に対するスライスは&[i32]
,&[i32]
のスライスは&[i32]
- たとえば、
- ただし、元のコレクションオブジェクトとは型が異なる
-
一方、
Hoge
型の変数への参照は&Hoge
であり、その参照への参照は&&Hoge
であるので注意// `String` のスライスは `&str` // `&str` のスライスは `&str` fn main() { let s: String = String::from("hello"); let slice: &str = &s[1..4]; // ell let slice: &str = &slice[1..]; // ll let slice: &str = &slice[..1]; // l println!("slice: {}", slice); }
// `[i32; 10]` のスライスは `&[i32]` // `&[i32]` のスライスは `&[i32]` fn main() { let a:[i32; 10] = [1, 2, 3, 4, 5, 1, 2, 3, 4, 5]; let slice: &[i32] = &a[1..9]; // [2, 3, 4, 5, 1, 2, 3, 4] let slice: &[i32] = &slice[..7]; // [2, 3, 4, 5, 1, 2, 3] let slice: &[i32] = &slice[3..]; // [5, 1, 2, 3] println!("slice: {:?}", slice); }
// `String` の参照は `&String`, // `&String` の参照は `&&String` fn main() { let s: String = String::from("hello"); let s: &String = &s; let s: &&String = &s; }
-
また、
&&..&Hoge
のスライスを取るのと、Hoge
のスライスを取るのは同等らしい(Rust 側で参照外しをやってくれるっぽい?)fn main() { let s: String = String::from("hello"); let s: &String = &s; let s: &&String = &s; let s: &&&String = &s; let slice: &str = &s[1..4]; // ell println!("slice: {}", slice); }