蛋疼、プログラミングの概念のノートの半分を書いている途中で push を忘れました。Obsidian の git プラグインを使っていますが、自動バックアップが好きではなく、書き終わったら手動で同期する必要があります。
聖書を最初から最後まで読んでいると、所有権が複雑なデザインの源であることに気づきました。当初は半分読んでから所有権をもう一度数回読むつもりでしたが、最終的には最初から最後まで一気に読むことを選びました。以前に何度か Rust を学ぼうと試みましたが、すべて所有権のところで行き詰まりました。今回は 2 回目の読みで少し親しみを感じています。
所有権とは何ですか#
高度な言語である JavaScript を使っているフロントエンドの開発者にとって、面接の前以外は基本的にメモリ管理を考慮する必要はありません。なぜなら、JavaScript にはガベージコレクション(GC)と呼ばれるものがあり、一定の間隔で使われていないメモリを自動的にクリーンアップしてくれるからです。一方、C などの伝統的な低レベル言語では、ロジックの実装だけでなく、メモリの割り当てと解放も自分で行う必要があります。そして、Rust では、メモリを管理するために非常に新しい方法、つまり所有権を使っています。
なぜ所有権が必要なのか#
低レベル言語では、パフォーマンスとプロダクトのサイズを考慮する必要があります。GC は便利ですが、プログラムと一緒に実行する必要があります。エンジンが最適化を行っていても、実行時のスキャンと分析を避けることはできません。開発者が直接メモリを管理すると、ミスによるメモリリークやヌルポインタのクラッシュが発生しやすくなります。Rust はメモリセーフティを重視しており、それは所有権によって実現されています。私は所有権を抽象的な制約と見なしています。開発者は特定のパターンでコードを書くことで、コンパイラが静的解析を行い、最終的にメモリ管理のコンテンツを挿入するのに便利です。メモリ管理は行われますが、完全に制御されていないという感じです。さらに、所有権モデルは開発者が行動を制限し、コードを理解しやすくし、一般的なエラーを回避し、不要なメモリの割り当てとコピーを減らしてパフォーマンスを向上させるのに役立ちます。
ヒープとスタックについてはここでは議論しません。このセクションは私に多くの忘れていた知識を思い出させてくれました。
どのように使うか#
エラーメッセージに従って修正するだけです🤪
ルールを再述します。値(文字、数字、オブジェクトなど)は、ある時点で 1 つの所有者(変数)だけを持つことができます。最終的に所有者がスコープ外に出ると、値は破棄されます。
理解する#
Rust は、二重解放によるメモリの脆弱性を回避するために、ヒープ上のメモリには 1 つのポインタしか許可せず、JS のような浅いコピーの動作は許可しません。値の転送にはデフォルトでムーブが使用されます。また、リソースを消費するclone
を使用してディープコピーを実現することもできます。スタック上に存在する単純なデータに対しては、Copy
トレイトを実装することで、所有権を考慮せずに高速にコピーすることができます。
キャパシティ#
String のメモリ割り当てについて説明する際に、スタック上に格納される内容、ポインタ ptr は言及しましたが、_len_と_capacity_はそれぞれ長さと容量を表しています。Vecのドキュメントでは、両者の違いが説明されています:
ベクタの容量は、将来ベクタに追加される要素のために割り当てられるスペースの量です。これは_length_と混同しないでください。後者はベクタ内の実際の要素の数を指定します。ベクタの長さが容量を超えると、容量が自動的に増加しますが、要素は再割り当てされる必要があります。
たとえば、容量が 10 で長さが 0 のベクタは空のベクタであり、そのスペースには 10 個の要素が格納できます。10 個以下の要素を追加しても、容量は変わらず、再割り当ても行われません。ただし、ベクタの長さが 11 になると、再割り当てが必要になり、パフォーマンスが低下する可能性があります。このため、ベクタの予想サイズを指定するためにVec::with_capacity
を使用することをお勧めします。
参照と借用#
所有権のメカニズムにより、値を再利用することが複雑になります。特に所有権が関数に渡される場合、所有権を関数から返す必要があります。値の所有権を複製することなく、値の参照を作成するために、&
キーワードを使用することができます。参照は値へのポインタであり、値自体の所有権は渡されません。参照を作成する行為は借用と呼ばれます。
所有#
私が作った概念ですが、ライフタイムの概念のようですね。
参照も可変と不変に分かれており、さらに制約として、所有している可変参照がある場合、他の可変または不変の参照を所有することはできません。所有とは、作成から最後の使用までの間の期間を指します。作成の順序に関係なく、可変参照と他の参照の所有段階は交互になることはできません(少し抽象的ですが)。この制約はデータ競合を回避するためのものであり、作成後に同じレベルで使用されない場合はデータ競合の問題はありません。同様に、複数の不変な参照を所有することも許可されています。Rust は可変性に対する制約が本当に多いですね。
let str = String::from("x");
let s1 = &mut str;
// この間、s1を所有しているので他の参照は作成できません
println!("{s1}")
参照はダングリングポインタを引き起こす可能性があります。つまり、参照がまだ存在しているが、参照が指すメモリが存在しないか、または参照が作成されたときに指していたものではない場合です。Rust は [[ライフタイム]] を使用してこれを回避します。修正が必要なので、所有権の不便さを補うために、ライフタイムが借用にもたらす問題を補うのです。
参照の取得とアドレスの取得#
Lv777: Rust では、
&v[0]
は v の参照のインデックス 0 の要素を参照しているのか、v のインデックス 0 の要素の参照を作成しているのか、理解する必要があります。
Raycast AI: Rust では、
&v[0]
はベクタv
のインデックス 0 の要素への参照を表します。具体的には、v
はベクタであり、&v[0]
はそのベクタの最初の要素を指す不変の参照です。この式の型は&T
であり、T
はベクタの要素の型です。したがって、この要素の値にアクセスするには、*(&v[0])
またはv[0]
を使用できます。
Lv777:
&
キーワードの優先順位
Raycast AI: Rust では、
&
記号には 2 つの異なる使い方があり、それぞれ参照の取得とアドレスの取得です。それらの優先順位は異なります。
参照の取得の優先順位はアドレスの取得よりも高いです。つまり、&x.foo()
のようなコードを書くと、それは&(x.foo())
と解釈されますが、(&x).foo()
と解釈されません。
変数のアドレスを取得し、それをポインタ型に変換する場合は、as
キーワードを使用することができます。これは&
記号よりも優先順位が低いです。たとえば、&x as *const T
はx
のアドレスを型T
の定数ポインタに変換します。
スライス#
この概念は最初に読んだときに気づかず、後でどの章に出てきたか忘れました。
スライスは値の一部の参照であり、範囲を指定することで宣言されます。開始インデックスは切り取る最初の位置を指定し、終了インデックスは切り取る最後の位置の次の値を指定します。
let str = String::from("123456")
let sli = str[1..3]; // 23
let all = str[..]; // 開始インデックスが0または終了インデックスがlenの場合、省略できます
終了インデックスについて疑問がありますが、why-is-slice-end-index-logic-as-it-isを見てみましょう。
[[# 容量]] で説明したように、String はスタック上に格納される内容であり、ポインタはヒープ上のデータの開始位置を指します。スライスも同様で、そのポインタはスライスの作成時に宣言された開始インデックスの位置を指し、_capacity_はありません。_len_は開始インデックスから終了インデックスを引いて計算されます。スライスは特殊な不変の参照であり、[[# 持有]] の制約も受けます。
ドキュメントでは、String の使用例を通じてスライスの便利さを説明していますが、うまく説明できませんでした。最初は理解が難しかったのは、JavaScript の文字列に対する認識を Rust の String に持ち込んでしまったからです。Rust の String はスカラーではなく、単純なコピーの考え方では扱えません。