Rust 入門
〜第二回〜
所有権
メモリの開放について
- 従来
- ガベージコレクター(GC)を備えた言語では、GCは追跡し、使用されなくなったメモリをクリーンアップするため、考える必要はない
- GCがなければ、メモリが使用されなくなったときを識別し、コードを呼び出して明示的に行う必要がある
- 歴史的に難しいプログラミング問題
- 忘れてしまうとメモリを無駄にする
- 早すぎると、変数が無効になる
- 1つの割り当てと1つの空きをペアにする必要がある
- Rust
- メモリを所有する変数がスコープ外になると、メモリは自動的に返される
{
let s = String::from("hello"); // s is valid from this point forward
println!("{}", s);
} // this scope is now over, and s is no longer valid
Rustの約束
- ある値のメモリや資源は即座に解放される
- ダングリングポインタによる、プログラムのクラッシュや
セキュリティホールをコンパイル時に発見してくれる
// 【C言語】
char *sora = malloc(1); // メモリ確保
free(sora); // メモリ解放
sora; // 既にダングリングポインタ
*sora = 'dangling'; // ダングリングポインタへのアクセス
- CやC++では1つ目の約束はしているが、2つ目はしていない
-
ガベージコレクションで、2つ目の約束をしている。
ですが、メモリの解放タイミングはガベージコレクション任せ。
スタックとヒープ
スタックとヒープ
- プログラム中で一時的に使用するメモリ
- 多くのプログラミング言語では、考える必要のないこと
- Rustは、値がスタックに積まれるかヒープに置かれるかを考える
スタック
-
値を取得した順序で格納し、逆の順序で値を削除する
- 最後に入れたものが、最初に出てくる
- データの追加:スタックにプッシュ(push)
- データの削除:スタックからポップ(pop)
- スタック上のデータは全て基地の固定サイズ
ヒープ
- サーズが可変のデータについては、ヒープに格納する
-
OSはヒープ上に十分な大きさの空領域を見つけて、使用中にして、ポインタを返す
- ポインタとは、その場所へのアドレスのこと
- このプロセスはヒープに領域を割り当て(確保、allocate)と呼ばれる
- ヒープへのデータアクセスは、ポインタを追っていく必要があるため、スタックへのデータアクセスよりも低速
所有権が対処する問題
- コードのどの部分がヒープのどのデータを使用しているかを追い、
ヒープ上の重複データの量を最小限に抑え、スペースが不足しないよう
ヒープ上の未使用のデータをクリーンアップすること - 所有権を理解したら、スタックとヒープについて頻繁に考える必要はない
- ただし、「ヒープにあるデータを管理すること」が所有権の存在理由であることを理解する
Rustの特徴(所有権)
まだ4章!笑
所有権とは?
所有権のルール
- 各値には、その所有者と呼ばれる変数がある
- 一度に所有者は1つ
- 所有者が範囲外になると、値は削除される
変数スコープ
- スコープは、要素が有効になるプログラム内の範囲のこと
- その他の言語とほぼ同様
{ // sはまだ宣言されていない
let s = "sora"; // sを宣言
println!("{}", s); // sが
} // このスコープは終わり。もうsは有効ではない
文字列型(String型)
-
文字列リテラルは便利ですが、テキストを使用したいすべての状況に適しているわけではない
- 理由その1:不変であること
- 理由その2:コードを作成時、すべての文字列の値がわかるわけではない
-
2番目の文字列型String
- ヒープに割り当てられるため、
コンパイル時にサイズが不明なテキストを格納できる - String型のfrom関数を扱う
- ヒープに割り当てられるため、
let mut s = String::from("hello");
s.push_str(", world!"); // push_str() appends a literal to a String
println!("{}", s); // This will print `hello, world!`
メモリと割り当て(確保・allocate)
-
文字列リテラルが高速で効率的
- 文字列リテラルの場合、コンパイル時に内容がわかるため、
テキストは最終的な実行可能ファイルに直接ハードコードされる - ただし、この特性は、文字列リテラルの不変性にのみ適応
- コンパイル時にサイズが不明であり、プログラムの実行中にサイズが変化する可能性があるテキストの各部分について、メモリの塊をバイナリに入れることはできません
- 文字列リテラルの場合、コンパイル時に内容がわかるため、
{ // sはまだ宣言されていない
let s = "sora"; // sを宣言
println!("{}", s); // sが
} // このスコープは終わり。もうsは有効ではない
メモリと割り当て(確保・allocate)
-
String型の場合、可変で拡張可能なテキストを保持するために、
コンパイル時に不明な量のメモリをヒープに割り当てる必要がある-
メモリは、実行時にOSからを要求される
- String:: fromを呼び出すと、その実装は必要なメモリを要求します。
これはプログラミング言語ではほぼ普遍的
- String:: fromを呼び出すと、その実装は必要なメモリを要求します。
- 文字列(String型)を使い終わったら、このメモリをOSに戻す必要がある
- ガベージコレクター(GC)を備えた言語では、GCが使用されなくなった
メモリをクリーンアップするため、考える必要はない - GCがなければ、メモリが使用されなくなったときを識別し、
コードを呼び出して明示的に行う必要がある- 歴史的に難しいプログラミング問題
- 忘れてしまうとメモリを無駄にする
- 早すぎると、変数が無効になる
- 2回解放してもバグ
- allocateとfreeはペアにする必要がある(常に1対1)
- ガベージコレクター(GC)を備えた言語では、GCが使用されなくなった
-
メモリは、実行時にOSからを要求される
メモリと割り当て(確保・allocate)
-
Rustはメモリを所有する変数がスコープ外になると、メモリは自動的に返される
-
変数がスコープ外になると、自動的に drop関数を呼び出す
- 注:C ++では、アイテムの有効期間の終わりにリソースを解放する
このパターンは、リソース獲得はRAII(初期化:Resource Acquisition Is Initialization)と呼ばれることがあります。
- 注:C ++では、アイテムの有効期間の終わりにリソースを解放する
{
let s = String::from("hello"); // s is valid from this point forward
println!("{}", s);
} // this scope is now over, and s is no
// longer valid
移動:変数とデータが相互作用する方法
- 複数の変数が同じデータと異なる方法で相互作用できる
- 整数は既知の固定サイズの値であり、これら2つの5の値はスタックにプッシュされる
let x = 5;
let y = x;
移動:変数とデータが相互作用する方法
- 前のコードと非常によく似ているので、動作方法は同じであると想定できるが、同じ動きをしない
- 文字列は、ポインタ・長さ・容量で構成されている。この三つは、スタックに格納される
- s1をs2に割り当てると、文字列データがコピーされる。
スタック上にあるポインター・長さ・容量がコピーされる。
ポインターが参照するヒープ上のデータはコピーしない
let s1 = String::from("hello");
let s2 = s1;
移動:変数とデータが相互作用する方法
移動:変数とデータが相互作用する方法
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);
実行するとどうなる???
移動:変数とデータが相互作用する方法
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:5:28
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `std::string::String`, which does not implement the `Copy` trait
3 | let s2 = s1;
| -- value moved here
4 |
5 | println!("{}, world!", s1);
| ^^ value borrowed here after move
error: aborting due to previous error
For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership`.
To learn more, run the command again with --verbose.
移動:変数とデータが相互作用する方法
- ダブルフリーエラー
- 変数がスコープ外になると、自動的にdrop関数を呼び出し、その変数のヒープメモリをクリーンアップするため、同じ場所を指す両方のデータポインターを示している場合、同じメモリを開放しようとしてしまうこと
- メモリを2回解放すると、メモリの破損につながり、セキュリティの脆弱性につながる可能性がある
- 他の言語を使用しているときに浅いコピーと深いコピーという用語を聞いたことがある場合、データをコピーせずにポインタ、長さ、容量をコピーするという概念は、浅いコピーを作成するように思えるかもしれません
- Rustは最初の変数も無効にするため、浅いコピーと呼ばれるのではなく、移動と呼ぶ
- データの「深い」コピーを自動的に作成することはありません
移動:変数とデータが相互作用する方法
クローン:変数とデータが相互作用する方法
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
- スタックデータだけでなく、文字列のヒープデータを深くコピーしたい場合は、クローンと呼ばれる一般的なメソッドを使用できる
- cloneの呼び出しを見ると、いくつかの任意のコードが実行されており、
そのコードには負荷がかかる可能性がある
- cloneの呼び出しを見ると、いくつかの任意のコードが実行されており、
- ヒープデータもコピーした場合になる
クローン:変数とデータが相互作用する方法
コピー:スタックのみのデータ
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);
- クローンを呼び出す呼び出しはありませんが、xはまだ有効であり、yに移動されていない
- コンパイル時に既知のサイズを持つ整数などの型は完全にスタックに格納されるため、実際の値のコピーをすばやく作成できるため
- コピーであるタイプの一部
- u32などのすべての整数型
- ブール型の値で、値はtrueとfalse
- f64などのすべての浮動小数点型
- 文字タイプchar
- タプル(コピーでもあるタイプのみを含む場合)
たとえば、(i32、i32)はコピーですが、(i32、String)は違う
所有権と関数
fn main() {
let s = String::from("hello"); // s comes into scope
takes_ownership(s); // s's value moves into the function...
// ... and so is no longer valid here
println!("{}", s); // ← エラーになる
}
fn takes_ownership(some_string: String) { // some_string comes into scope
println!("{}", some_string);
} // Here, some_string goes out of scope and `drop` is called. The backing
// memory is freed.
- takes_ownershipの呼び出し後にsを使用しようとすると、
コンパイル時エラーになる
所有権と関数
fn main() {
let x = 5; // x comes into scope
makes_copy(x); // x would move into the function,
// but i32 is Copy, so it’s okay to still
// use x afterward
println!("{}", x); // エラーにならない
}
fn makes_copy(some_integer: i32) { // some_integer comes into scope
println!("{}", some_integer);
} // Here, some_integer goes out of scope. Nothing special happens.
- こちらはコピーになっているため、エラーにならない
所有権と関数
fn main() {
let x = 5; // x comes into scope
makes_copy(x); // x would move into the function,
// but i32 is Copy, so it’s okay to still
// use x afterward
println!("{}", x); // エラーにならない
}
fn makes_copy(some_integer: i32) { // some_integer comes into scope
println!("{}", some_integer);
} // Here, some_integer goes out of scope. Nothing special happens.
- こちらはコピーになっているため、エラーにならない
参照と借用
参照と借用(所有権をもらうことなく参照)
参照(所有権をもらうことなく参照)
-
Stringではなく&Stringを使用
- &String 構文を使用すると、string の値を参照するが、所有しない参照を作成できる
- 所有していないので、参照が範囲外になったときに、値はドロップされない
- 関数パラメーターのスコープと同じだが、所有権がないためスコープが外れたときに参照したものを削除しない
- 関数の引数に参照をとることを借用と呼びます。
fn main() {
let s1 = String::from("hello");
let len = calculate_string_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_string_length(s: &String) -> usize {
s.len()
}
可変参照
- &mutを使用して可変参照を作成し、some_string:&mut Stringを使用して可変参照を受け入れる必要がある
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
可変参照の制限
-
特定のスコープ内の特定のデータへの変更可能な参照は1つだけ
- 下記のようにコードを書くことができない
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
可変参照の制限
-
特定のスコープ内の特定のデータへの変更可能な参照は1つだけ
- 下記のようにコードを書くことができない
- この制限の利点は、コンパイル時のデータ競合を防止できること
- データの競合は3つの動作が発生したとき
- 2つ以上のポインターが同じデータに同時にアクセス
- 少なくとも1つのポインターがデータへの書き込みに使用されている
- データへのアクセスを同期する機構が使用されていない
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
可変参照の制限
- 新しいスコープを作成し、同時参照ではなく複数の可変参照を許可できる
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1 goes out of scope here, so we can make a new reference with no problems.
let r2 = &mut s;
スライス型
スライス型
-
所有権を持たないもう1つのデータ型はスライス
- コレクション内の要素の連続したシーケンスを参照できる
fn first_word(s: &String) -> usize {
// 文字列をバイトの配列に変換
let bytes = s.as_bytes();
// バイトの配列に対してイテレーターを作成
// enumerateから返されるタプルの最初の要素はインデックスで、2番目の要素は要素への参照
for (i, &item) in bytes.iter().enumerate() {
// スペースが見つかったら、位置を返す
if item == b' ' {
return i;
}
}
s.len()
}
文字列スライス
- 文字列全体への参照ではなく、文字列の一部への参照
- [starting_index..ending_index]を指定することで、スライスを作成できる
- 内部的に、スライスデータ構造は、スライスの開始位置と長さを格納
- let world =&s [6..11];の場合、worldは、長さが5のsの7番目のバイト(1から数えて)へのポインターを含むスライス
- 「..」範囲構文を使用して、最初のインデックス(ゼロ)から開始する場合は、「..」の前の値を削除
- スライスに文字列の最後のバイトが含まれている場合は、末尾の数字を削除
- &s[..]; のように全体を取得することもできる
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
パラメータとしての文字列スライス
- &String値と&str値の両方で同じ関数を使用できる
fn first_word(s: &str) -> &str {
fn main() {
let my_string = String::from("hello world");
// first_word works on slices of `String`s
let word = first_word(&my_string[..]);
let my_string_literal = "hello world";
// first_word works on slices of string literals
let word = first_word(&my_string_literal[..]);
// Because string literals *are* string slices already,
// this works too, without the slice syntax!
let word = first_word(my_string_literal);
}
-
文字列スライスがある場合は、
直接渡せる- 文字列がある場合は、文字列全体のスライスを渡すことができる
- 文字列への参照の代わりに文字列スライスを取得する関数を定義すると、APIは機能を失わない
その他のスライス
- 配列の一部を参照
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
次回、The Rust Programming Language を全て終えて、その中でも学びのあるものを抜粋します
Rust 入門〜第2回〜
By hirontan
Rust 入門〜第2回〜
- 218