#4 所有権【知識0がRustやってみる】
【所有権を理解する】
所有権はRustの最もユニークな機能であり、これのおかげでガベージコレクタなしで安全性担保を行うことができるのです。 故に、Rustにおいて、所有権がどう動作するのかを理解するのは重要です。この章では、所有権以外にも、関連する機能を いくつか話していきます: 借用、スライス、そして、コンパイラがデータをメモリにどう配置するかです。
来ました!!よろしくお願いします!
ガベージコレクタとは(今調べた)
メモリ上の不要なデータを自動的に削除する仕組みのこと。
Java、Python、Rudy、PHP、JavaScript、Kotlin、Swift等はこのガベージコレクタによってメモリの管理が行われている。
人間はひとつの物事をいつまでも記憶メモリに残していると、脳への負荷が高いので自然と忘れていくようになっているが、これとガベージコレクタは似ている(勝手にそう思った)
逆にガベージコレクタ機能がない言語では、プログラマが明示的にメモリを確保したり、解放したりしなければなりませんが、Rustでは第3の選択肢を取っているそう。これが所有権である。
ここでRust Bookではスタックとヒープとい用語を詳しく解説しており、かなり重要な解説ですがとても長いので注釈として最後に置きます。[1]
ようするに、スタックとはズボンのポケットのようなもので、数値のような小さいメモリの物ならポケットにしまっておけるが、文字列などの大きくサイズも不確定なものはヒープという大きいカバンに入れるしかない。
ピープへ新しく入れるにはカバンを整理してスペースを空ける必要があり、使い終わったものは破棄しカバンの空きを作っておく必要がある。
所有権とは?
所有権規則
所有権には3つの規則がある
- Rustの各値は、所有者と呼ばれる変数と対応している。
- いかなる時も所有者は一つである。
- 所有者がスコープから外れたら、値は破棄される。
変数スコープ
変数sは、文字列リテラルを参照し、ここでは、文字列の値はプログラムのテキストとしてハードコードされています。 この変数は、宣言された地点から、現在のスコープの終わりまで有効になります。リスト4-1には、 変数sが有効な場所に関する注釈がコメントで付記されています。
文字列リテラルとは
人間語で書いたプログラムの元ネタ(ソースコード)の中に直接ベタ書きした文字列のこと(文字列リテラルは不変値)
{ // sは、ここでは有効ではない。まだ宣言されていない
let s = "hello"; // sは、ここから有効になる
// sで作業をする
} // このスコープは終わり。もうsは有効ではない
少し分かりづらいですが、平たく言えば{ }
でスコープが作成され、変数は定義されたスコープ内でのみ有効である。
定数の説明でどのスコープでも有効とドヤってたのはこれか
シャドーイングがスコープを抜けると元の状態へ戻るのはシャドーイングの特性ではなく、スコープを抜けると変数のメモリが解放される仕様から来るのか。
String型
String型はヒープにメモリを確保するので、 コンパイル時にはサイズが不明なテキストも保持することができるのです。
今更感ありますが、String型はこのように定義する。
// ↓ リテラルをString型へ変換する
let s = String::from("hello");
// ↑文字列リテラル
let mut s = String::from("hello");
s.push_str(", world!"); // push_str()関数は、リテラルをStringに付け加える
println!("{}", s); // これは`hello, world!`と出力する
メモリと確保
String型では、可変かつ伸長可能なテキスト破片をサポートするために、コンパイル時には不明な量のメモリを ヒープに確保して内容を保持します。
- メモリは、実行時にOSに要求される。
- String型を使用し終わったら、OSにこのメモリを返還する方法が必要である。
メモリを所有している変数がスコープを抜けたら、 メモリは自動的に返還される。
String型が必要とするメモリをOSに返還することが自然な地点があります: s変数がスコープを抜ける時です。 変数がスコープを抜ける時、Rustは特別な関数を呼んでくれます。この関数は、dropと呼ばれ、 ここにString型の書き手はメモリを返還するコードを配置することができます。Rustは、閉じ波括弧で自動的にdrop関数を呼び出します。
ムーブ
let x = 5;
let y = x;
println!("{}{}",x,y)
上の例は問題なく通ります。x
もy
も値は整数でありスタックに格納できる。
下の例では、一般的な言語ではヒープに格納されたhello
という値をs2
がコピーしているように見えますが、rustではヒープ上のメモリを毎回コピーする手法は取っておらず、s2
はs1
が所有するhello
を参照します。
ここでポイントが、s1
もs2
も同じ値を参照しているというところ。
もしs1
とs2
が同時にスコープを抜けた場合、hello
という値は1つしかないのに2回のメモリ開放を行ってしまいエラーになる。これを二重解放エラーと言うらしい。
rustでは先に定義されていた変数を無効化し、後に定義した変数へ所有者を移すことで、この二重解放エラーを回避している。
それをムーブと表現する。
このムーブのおかげで変数に対応した所有者が常に一つのみになる。
- Rustの各値は、所有者と呼ばれる変数と対応している。
- いかなる時も所有者は一つである。
- 所有者がスコープから外れたら、値は破棄される。
let s1 = String::from("hello"); // s2が値を参照しているのでムーブされる
let s2 = s1; // コピーではなくs1の"hello"へ参照を向ける
println!("{}{}",s1,s2); // s1はムーブされているのでエラー
ちなみに明示的に宣言しヒープデータのコピーを行うこともでき、この場合はs2
にも新たな所有権が与えられる。↓
let s1 = String::from("hello");
let s2 = s1.clone();
所有権と関数
所有権を持つ変数を、関数の引数に渡した場合も、しっかりと関数へ所有権が渡る。
fn main() {
let s1 = String::from("hello"); // hello関数へ所有権を渡しムーブされる
hello(s1); // hello関数に値の所有権が移る
println!("{}", s1); // s1はムーブされているのでエラー
}
fn hello(txt: String) { // 所有権を持つ
println!("{}", txt);
} // スコープが終わり"hello"の値は解放される
↓所有権が必要ないならコピーするか借用しろとおっしゃる。借用?
エラー[E0382]: 移動された値の借用: `s1`
--> src/main.rs:4:20
|
2 | let s1 = String::from("hello");
| -- `s1` の型が `String` であり、`Copy` 特性を実装していないため、移動が発生します。
3 | こんにちは(s1);
| -- 値がここに移動されました
4 | println!("{}", s1);
| ^^ 移転後ここで借りた値
|
注: 値を所有する必要がない場合は、関数 `hello` でこのパラメータの型を変更して、代わりに借用することを検討してください。
--> src/main.rs:6:15
|
6 | fn hello(txt: 文字列) {
| ----- ^^^^^^ このパラメータは値の所有権を取得します
| |
| この関数では
= 注意: このエラーはマクロ `$crate::format_args_nl` に起因しており、これはマクロ `println` の展開に由来します (Nightly ビルドでは、詳細については -Z Macro-backtrace を指定して実行してください)。
ヘルプ: パフォーマンス コストが許容できる場合は、値のクローン作成を検討してください。
|
3 | hello(s1.clone());
| ++++++
このエラーの詳細については、「rustc --explain E0382」を試してください。
エラー: 前のエラーが 1 つあったため、「rust」 (bin "rust") をコンパイルできませんでした
戻り値とスコープ
値を返すことでも、所有権は移動します。
fn main() {
let s1 = String::from("hello");
// シャドーイングで新たに値を束縛、一時的に所有権を渡すが返却される
let s1 = hello(s1);
println!("{}", s1); // 所有権を持つので出力できる
}
fn hello(txt: String) -> String { // 戻り値を定義
println!("{}", txt);
txt // 最後に受け取った引数を式として返却すると所有権も一緒に返却できる
}
hello
hello
参照と借用
不変な参照
実は値の所有権をもらう代わりに引数としてオブジェクトへの参照を取ることができます。
↑所有権と関数のところ書いててもどかしかった...
↓では引数に&s1
を渡し、関数の引数の型は&String
と示されています。
fn main() {
let s1 = String::from("hello"); // 所有権を持つ
hello(&s1); // s1の所有権を借用し関数へ渡す
println!("{}", s1); // // 所有権を持ったままなので出力できる
}
fn hello(txt: &String) { // 借用された値を受け取る
println!("{}", txt);
}
平たく言えば所有権の借用とは、若者がよくやる被写体をiPhoneのカメラで撮ってるiPhoneの画面を別のiPhoneから撮ってるアレです(適当)
所有権を持ってる変数Aが見てる値を変数A越しに見てるんですね。なので変数Aの所有権は動きません。
let s1 = String::from("hello");
let s2 = s1;
// s1 × -> hello
// s2 -----↑
let s1 = String::from("hello");
let s2 = &s1;
// s2 -> s1 -> hello
わかりづらい..
なのでs1
が所有権を持つ値を、借用しているs2
が変更してしまうと、s1
はびっくりしてエラーになります。
fn main() {
let s1 = String::from("hello");
hello(&s1);
}
fn hello(txt: &String) {
txt.push_str(", world");
}
エラー[E0596]: `&`参照の背後にあるため、`*txt`を可変として借用できません
--> src/main.rs:6:5
|
6 | txt.push_str(", 世界");
| ^^^ `txt` は `&` 参照であるため、それが参照するデータを可変として借用することはできません
|
ヘルプ: これを変更可能な参照に変更することを検討してください
|
5 | fn hello(txt: &mut String) {
| +++
変数が標準で不変なのと全く同様に、参照も不変なのです。参照している何かを変更することは叶わないわけです。
これが不変の参照か。
可変な参照
一捻り加えるだけで先ほどのエラーは解消できます。
fn main() {
let mut s1 = String::from("hello"); // mutを宣言し変数を可変に
hello(&mut s1); // &mutを宣言し可変な参照に
}
fn hello(txt: &mut String) { // &mutを宣言し可変な参照に引数にとる
txt.push_str(", world");
}
まずはs1
をmut
で可変の変数にします。
次にhello関数
に渡す引数を&mut
で可変な参照として渡します。
そして関数内で&mut
を受け取ることで値を変更できます。
mutに&を付けるってのがまたややこしい..
let mut s = String::from("hello");
let r1 = &s; // 不変の参照
let r2 = &mut s; // 可変の参照
可変な参照には大きな制約が一つあります
!?
特定のスコープで、ある特定のデータに対しては、 一つしか可変な参照を持てないことです。
なるほど、スコープ内で可変の参照ができるのは1つまでなのか。
たしかに1つの値で何個も可変の参照ができるとぐちゃぐちゃになるかも
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; // 大問題!
複数の不変参照か、一つの可変参照かどちらか選ぶがよい...
宙に浮いた参照
参照は値が存在することが前提なので、その値が解放されてしまっているのに参照しようとするとエラーになる。
↓の場合は戻り値でs
への参照を返そうとしているけど、s
は関数のスコープが終わると解放されてしまうので、戻り値の&s
は参照先がなくなる。
fn dangle() -> &String { // dangleはStringへの参照を返す
let s = String::from("hello"); // sは新しいString
&s // sへの参照を返す
} // ここで、sはスコープを抜けメモリは消される。ここでエラー。
fn main(){
let a = no_dangle();
println!("{}",a);
}
そんなことはせず直接返せばOK
fn no_dangle() -> String {
let s = String::from("hello");
s // 式としてsの値が返る
}
fn main(){
let a = no_dangle();
println!("{}",a); // hello
}
参照の規則
- 任意のタイミングで、一つの可変参照か不変な参照いくつでものどちらかを行える。
- 参照は常に有効でなければならない。
スライス型
所有権のない別のデータ型は、スライスです。スライスにより、コレクション全体ではなく、 その内の一連の要素を参照することができます。
スライス型は所有権を持たないので参照です。
参照なので一つの可変参照か、複数の不変参照のルールが適応されます。
一般的には一部文字列の参照や配列の一部だけ引数に渡すとかで使うのかな?
let arr = [1, 2, 3, 4, 5];
let slice = &arr[0..3]; // 最初の3要素へのスライス
let slice = &arr[..2]; // 最初の2要素へのスライス
let slice = &arr[2..]; // 3番目の要素から末尾までのスライス
let slice = &arr[..]; // 全要素へのスライス
文字列スライス (String)
文字列スライスは&mut
を宣言しても変更は許されない。
let s = String::from("Hello, world!");
let slice = &s[0..5]; // "Hello" の部分への参照
// let slice = &mut s[0..5]; コンパイルエラー
まとめ
難しくなってきましたな..
次はClassに代わる?のか知らんけど構造体とimpl
について
参考
-
多くのプログラミング言語において、スタックとヒープについて考える機会はそう多くないでしょう。 しかし、Rustのようなシステムプログラミング言語においては、値がスタックに積まれるかヒープに置かれるかは、 言語の振る舞い方や、特定の決断を下す理由などに影響以上のものを与えるのです。 この章の後半でスタックとヒープを交えて所有権の一部が解説されるので、ここでちょっと予行演習をしておきましょう。
スタックもヒープも、実行時にコードが使用できるメモリの一部になりますが、異なる手段で構成されています。 スタックは、得た順番に値を並べ、逆の順で値を取り除いていきます。これは、 last in, first out(訳注: あえて日本語にするなら、「最後に入れたものが最初に出てくる」といったところでしょうか)と呼ばれます。 お皿の山を思い浮かべてください: お皿を追加する時には、山の一番上に置き、お皿が必要になったら、一番上から1枚を取り去りますよね。 途中や一番下に追加したり、取り除いたりすることもできません。データを追加することは、 スタックにpushするといい、データを取り除くことは、スタックからpopすると表現します(訳注: 日本語では単純に英語をそのまま活用してプッシュ、ポップと表現するでしょう)。
データへのアクセス方法のおかげで、スタックは高速です: 新しいデータを置いたり、 データを取得する場所を探す必要が絶対にないわけです。というのも、その場所は常に一番上だからですね。 スタックを高速にする特性は他にもあり、それはスタック上のデータは全て既知の固定サイズでなければならないということです。
コンパイル時にサイズがわからなかったり、サイズが可変のデータについては、代わりにヒープに格納することができます。 ヒープは、もっとごちゃごちゃしています: ヒープにデータを置く時、あるサイズのスペースを求めます。 OSはヒープ上に十分な大きさの空の領域を見つけ、使用中にし、ポインタを返します。ポインタとは、その場所へのアドレスです。 この過程は、ヒープに領域を確保する(allocating on the heap)と呼ばれ、時としてそのフレーズを単にallocateするなどと省略したりします。 (訳注: こちらもこなれた日本語訳はないでしょう。allocateは「メモリを確保する」と訳したいところですが) スタックに値を積むことは、メモリ確保とは考えられません。ポインタは、既知の固定サイズなので、 スタックに保管することができますが、実データが必要になったら、ポインタを追いかける必要があります。
レストランで席を確保することを考えましょう。入店したら、グループの人数を告げ、 店員が全員座れる空いている席を探し、そこまで誘導します。もしグループの誰かが遅れて来るのなら、 着いた席の場所を尋ねてあなたを発見することができます。
ヒープへのデータアクセスは、スタックのデータへのアクセスよりも低速です。 ポインタを追って目的の場所に到達しなければならないからです。現代のプロセッサは、メモリをあちこち行き来しなければ、 より速くなります。似た例えを続けましょう。レストランで多くのテーブルから注文を受ける給仕人を考えましょう。最も効率的なのは、 次のテーブルに移らずに、一つのテーブルで全部の注文を受け付けてしまうことです。テーブルAで注文を受け、 それからテーブルBの注文、さらにまたA、それからまたBと渡り歩くのは、かなり低速な過程になってしまうでしょう。 同じ意味で、プロセッサは、 データが隔離されている(ヒープではそうなっている可能性がある)よりも近くにある(スタックではこうなる)ほうが、 仕事をうまくこなせるのです。ヒープに大きな領域を確保する行為も時間がかかることがあります。
コードが関数を呼び出すと、関数に渡された値(ヒープのデータへのポインタも含まれる可能性あり)と、 関数のローカル変数がスタックに載ります。関数の実行が終了すると、それらの値はスタックから取り除かれます。
どの部分のコードがどのヒープ上のデータを使用しているか把握すること、ヒープ上の重複するデータを最小化すること、 メモリ不足にならないようにヒープ上の未使用のデータを掃除することは全て、所有権が解決する問題です。 一度所有権を理解したら、あまり頻繁にスタックとヒープに関して考える必要はなくなるでしょうが、 ヒープデータを管理することが所有権の存在する理由だと知っていると、所有権がありのままで動作する理由を 説明するのに役立つこともあります。 ↩︎
Discussion