[Rust] 所有権

所有

所有権は Rust の最もユニークな機能であり、これにより Rust は GC (ガベージ コレクション) なしでメモリの安全性を確保できます。Rust の中核的な機能は所有権であり、すべてのプログラムは実行時にコンピューター メモリの使用方法を管理する必要があります。一部の言語には、プログラムの実行中に未使用のメモリを常に検索するガベージ コレクション メカニズムがあります。他の言語では、プログラマは明示的にメモリを割り当て、解放する必要があります。

Rust は 3 番目のアプローチを採用しています。メモリは、コンパイラがコンパイル時にチェックする一連のルールで構成される所有権システムを通じて管理されます。所有権機能により、プログラムの実行中に速度が低下することはありません。

積み重ねて積み重ねる

Rust のようなシステムレベルのプログラミング言語では、値がスタック上にあるかヒープ上にあるかが、言語の動作と特定の決定を下す理由に大きな影響を与えます。コードの実行中は、スタックとヒープの両方がメモリとして利用できますが、構造は大きく異なります。

  • スタックは値を受け取った順に格納され、値は逆の順序で削除されます (後入れ先出し、LIFO)。データの追加はスタックへのプッシュと呼ばれ、データの削除はスタックのポップと呼ばれます。スタック。値をスタックにプッシュすることは、割り当てとは呼ばれません。ポインタは固定サイズであるため、ポインタをスタックに格納できます。
  • スタックに格納されるすべてのデータは、既知の固定サイズでなければなりません。コンパイル時にサイズが不明なデータ、または実行時にサイズが変更される可能性があるデータは、ヒープに格納する必要があります。
  • ヒープ メモリは適切に構成されていません。データをヒープに入れると、一定量の領域が要求されます。オペレーティング システムは、ヒープ内で十分な大きさの領域を見つけ、使用中としてマークし、ポインタ、つまりアドレスを返します。この空間の。このプロセスはヒープへの割り当てと呼ばれ、単に「割り当て」と呼ばれることもあります。
  • スタックへのプッシュは、(プッシュ時に) アロケーターが新しいデータを格納するためにメモリ領域を検索する必要がなく、その場所が常にスタックの先頭にあるため、ヒープにメモリを割り当てるよりも高速です。対照的に、ヒープ上にメモリを割り当てるには、より多くの作業が必要です。これは、アロケータがまずデータを保存するのに十分なメモリ領域を見つけてから、次の割り当てに備えていくつかのレコードを作成する必要があるためです。
  • ヒープ内のデータを見つけるにはポインターが必要となるため、ヒープ内のデータへのアクセスはスタック内のデータへのアクセスよりも遅くなります。最新のプロセッサでは、キャッシュによるメモリ内でのジャンプが少ないほど命令が高速になります。
  • データが近くに格納されている場合、プロセッサの処理速度は (スタック上で) 速くなります。データ間の距離が比較的長い場合(ヒープ上)の処理速度は遅くなります。ヒープ上に大量のスペースを割り当てるにも時間がかかります。
  • コードが関数を呼び出すと、値が関数に渡されます (また、ヒープへのポインターも渡されます)。関数のローカル変数はスタックにプッシュされます。関数が終了すると、値がスタックからポップされます。

所有権には理由があって存在する

所有権によって解決される問題: コードのどの部分がヒープ上のどのデータを使用しているかを追跡し、ヒープ上の重複データの量を最小限に抑え、スペース不足を避けるためにヒープ上の未使用データをクリーンアップします。所有権を理解すると、スタックやヒープについて頻繁に考える必要はなくなりますが、ヒープ データの管理が所有権の存在理由であることを理解すると、所有権がそのように動作する理由を説明するのに役立ちます。

所有権ルール

  • 各値には、値の所有者である変数があります。
  • 各値は一度に 1 人の所有者のみを持つことができます。
  • 所有者が範囲外になると、値は削除されます。

変数スコープ

スコープは、プログラム内の項目の有効なスコープです。

fn main() {
    
    
    //s 不可用
    let s = "hello";//s 可用
                    //可以对 s 进行相关操作
}//s 作用域到此结束,s 不再可用

文字列型

文字列は、これらの基本的なスカラー データ型よりも複雑です。文字列リテラル: プログラム内で手動で書き込まれた不変の文字列値。Rust には 2 番目の文字列型 String もあります。ヒープ上に割り当てられ、コンパイル時に未知の量のテキストを保存できます。

fn main() {
    
    
    let mut s = String::from("Hello");
    s.push_str(",World");
    println!("{}",s);
}

String 型の値は変更できるのに、文字列リテラルは変更できないのはなぜですか。メモリの処理方法が異なるためです。

メモリと割り当て

文字列リテラル値とその内容はコンパイル時に認識され、そのテキスト内容は最終的な実行可能ファイルに直接ハードコーディングされるため、高速かつ効率的です。その不変性のため。

可変性をサポートするために、String 型はコンパイル時に未知のテキスト コンテンツを保存するためにヒープ上にメモリを割り当てる必要があります。オペレーティング システムは実行時にメモリを要求する必要があり、このステップは String::from を呼び出すことで実現されます。String が使い果たされた場合、メモリをオペレーティング システムに返す何らかの方法が必要です。このステップは、GC を備えた言語では、未使用のメモリを追跡してクリーンアップします。GC を使用しない場合、メモリが使用されなくなった時期を特定し、それを返すコードを呼び出す必要があります。―忘れるとメモリの無駄になるし、事前にやると変数が不正になるし、二回やるとバグになる。割り当ては割り当て解除に対応している必要があります。

Rust は別のアプローチを採用しています。値の場合、それを所有する変数がスコープ外になるとすぐに、メモリが自動的にオペレーティング システムに返されます。Rustは、変数がスコープ外になったときに特別な関数dropを呼び出してメモリを解放します。

変数がデータとどのように相互作用するか

1.移動

複数の変数は、独自の方法で同じデータと対話できます。

let x = 5;
let y = x;

整数は既知の固定サイズの単純な値であり、2 つの 5 がスタックにプッシュされます。

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

String は、文字列の内容を格納するポインタ、長さ、容量の 3 つの部分で構成されます。これらはスタックに格納され、文字列の内容を格納する部分はヒープ上にあり、長さ len は文字列の内容を格納するのに必要なバイト数です。容量 Capacity は、オペレーティング システム デバイスから String によって取得されるメモリの総バイト数を指します。

ここに画像の説明を挿入

s1 を s2 に代入すると、String データのコピーが代入され、ポインタ、長さ、容量がスタック上にコピーされ、ポインタが指すヒープ上のデータはコピーされません。変数がスコープから外れるとき、Rustは自動的にdrop関数を呼び出し、変数が使用していたヒープメモリを解放します。s1 と s2 がスコープを離れると、両方とも同じメモリを解放しようとします。これは二重解放バグです。

メモリの安全性を確保するために、Rust は割り当てられたメモリをコピーしようとせず、s1 を無効にすることを選択します。s1 がスコープ外になった場合、Rust は何も解放する必要がありません。

ここに画像の説明を挿入

他の言語で浅いコピーと深いコピーという用語を聞いたことがある場合は、データをコピーせずにポインター、長さ、容量をコピーすることは浅いコピーのように聞こえるかもしれません。ただし、Rust は最初の変数も無効にするため、この操作はシャロー コピーではなく移動と呼ばれます。暗黙の設計原則: Rust はデータの深いコピーを自動的に作成せず、自動割り当ては実行時のパフォーマンスの点で安価です。

2.クローン

スタック上のデータだけでなく、ヒープ上の文字列データのディープ コピーを実際に作成したい場合は、clone メソッドを使用できます。

let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);

ここに画像の説明を挿入

3.コピー

let x = 5;
let y = x;

このコードは、先ほど学んだことと矛盾しているようです。clone は呼び出されませんが、x はまだ有効であり、y に移動されていません。

その理由は、コンパイル時にサイズがわかっている整数などの型は完全にスタックに格納されるため、実際の値のコピーが高速であるためです。これは、変数 y が作成された後に x を無効にする理由がないことを意味します。つまり、浅いコピーと深いコピーには違いがないため、ここで clone を呼び出しても、通常の浅いコピーと何ら変わりません。

Rust は Copy トレイトを提供します。これは、スタックに完全に格納される整数などの型に使用できます。型が Copy 特性を実装している場合、古い変数は代入後も引き続き使用できますが、型または型の一部が Drop 特性を実装している場合、Rust は Copy 特性を実装することを許可しません。

Copy 特性を持つ一部の型: 単純なスカラーである複合型はすべて Copy になりますが、メモリまたはある種のリソースを割り当てる必要があるものはすべて Copy ではありません。

  • u32 などのすべての整数型
  • ブール
  • 文字
  • f64 などのすべての浮動小数点型
  • タプル (タプル)、すべてのフィールドがコピーの場合

所有権と機能

意味的には、関数に値を渡すことは変数に値を割り当てることと似ており、値を関数に渡すと移動またはコピーが行われます。

fn main() {
    
    
    let mut s = String::from("Hello,World");

    take_ownership(s);//s 被移动 不再有效

    let x = 5;

    makes_copy(x);//复制

    println!("x:{}",x);
}

fn take_ownership(some_string: String){
    
    
    println!("{}",some_string);
}

fn makes_copy(some_number: i32){
    
    
    println!("{}",some_number);
}

戻り値とスコープ

所有権の移転は、関数が値を返すときにも発生します。

fn main() {
    
    
    let s1 = gives_ownship();gives_ownership 将返回值转移给s1

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

    let s3 = takes_and_give_back(s2);//s2 被移动到takes_and_gives_back 中,它也将返回值移给 s3
}

fn gives_ownship()->String{
    
    
    let some_string = String::from("hello");
    some_string
}

fn takes_and_give_back(a_string:String)->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には参照という機能があります。

引用

パラメータのタイプは String ではなく &String で、& 記号は参照を意味します。これにより、所有権を取得せずに値を参照できるようになります。

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()
}

ここに画像の説明を挿入

借りる

関数のパラメータとして参照を使用する行為を借用と呼びます。借用した変数は変更できません。変数と同様に、参照もデフォルトでは不変です。

可変参照

fn main() {
    
    
    let mut s1 = String::from("Hello");

    let len = calculate_length(&mut s1);

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &mut String) -> usize {
    
    
    s.push_str(",World");
    s.len()
} 

可変参照には重要な制限があります。特定のスコープ内では、データへの可変参照は 1 つだけ存在できます。これには、コンパイル時のデータ競合を防ぐという利点があります。データ競合は、2 つ以上のポインターが同時に 1 つのデータにアクセスする、少なくとも 1 つのポインターがデータの書き込みに使用される、データへのアクセスを同期するメカニズムが使用されない、という 3 つの動作で発生します。新しいスコープを作成して、複数の変更可能な参照を同時に作成できないようにすることができます。

    let mut s = String::from("hello");
    {
    
    
        let r1 = &mut s;
    }
    let r2 = &mut s;

もう 1 つの制限は、可変参照と不変参照を同時に持つことができないことです。複数の不変参照が可能です。

ぶら下がり参照

ダングリング ポインタ (ダングリング ポインタ): ポインタはメモリ内のアドレスを指し、このメモリは他の人が使用できるように解放され、割り当てられている可能性があります。

Rust では、コンパイラは参照が決してぶら下がっていないことを保証します。
データを参照する場合、コンパイラは、参照がスコープ外になるまでデータがスコープ外にならないことを保証します。

引用されたルール

常に、次の条件のうち 1 つだけが満たされます。

  • 変更可能な参照
  • 不変参照はいくつでも常に有効でなければなりません

参照は常に有効である必要があります。

スライス

Rust のもう 1 つの非所有権データ型であるスライス。

スペースで区切られた単語の文字列を受け取り、文字列内で最初に見つかった単語を返す関数を作成します。関数が文字列内にスペースを検出しない場合、文字列全体が 1 つの単語であるため、文字列全体が返される必要があります。

fn main() {
    
    
    let mut s = String::from("Hello world");
    let wordIndex = first_word(&s);

    s.clear();
    println!("{}", wordIndex);
}

fn first_word(s: &String) -> usize {
    
    
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
    
    
        if item == b' ' {
    
    
            return i;
        }
    }
    s.len()
}

このプログラムはエラーなしでコンパイルされますが、wordIndex は s 状態とはまったく関係ありません。s がクリアされた後も、s が関数に渡されると、wordIndex は状態の値を返します。Rust は、この状況に対する解決策を提供します。文字列のスライス。

文字列スライス

文字列スライスは、文字列の一部への参照です。形状:

[开始索引..结束索引]

開始インデックスはスライスの開始時のインデックス値であり、終了インデックスはスライスの終了時のすべての値です。

let s = String::from("Hello World");

let hello = &s[0..5];
let world = &s[6..11];

let hello2 = &s[..5];
let world2 = &s[6..];

ここに画像の説明を挿入

文字列スライスの範囲インデックスは、有効な UTF-8 文字境界内に存在する必要があります。マルチバイト文字から文字列スライスを作成しようとすると、プログラムはエラーを報告して終了します。

firstworld を書き換えます:


fn main() {
    
    
    let mut s = String::from("Hello World");

    let word = first_word(&s);

    //s.clear(); // 错误!
    println!("the first word is: {}", word);
}

fn first_word(s: &String) -> &str {
    
    
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
    
    
        if item == b' ' {
    
    
            return &s[0..i];
        }
    }
    &s[..]
}

文字列リテラルはスライスであり、文字列リテラルはバイナリ プログラムに直接格納されます。

文字列スライスを引数として渡します

経験豊富な Rust 開発者は、String 型と &str 型のパラメータを同時に受け取ることができるため、パラメータ型として &str を使用します。

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

文字列スライスを使用すると関数を直接呼び出し、String を使用すると関数を呼び出すための完全な文字列スライスが作成されます。

関数を定義するときに文字列参照の代わりに文字列スライスを使用すると、機能を失うことなく API がより汎用的になります。

fn main() {
    
    
    let mut s = String::from("hello world");

    let word = first_word(&s);

    let mut s2 = "hello world";

    let word2 = first_word(s2);
    //s.clear(); // 错误!
    println!("the first word of s is: {}", word);
    println!("the first word of s2 is: {}", word2);
}

fn first_word(s: &str) -> &str {
    
    
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
    
    
        if item == b' ' {
    
    
            return &s[0..i];
        }
    }

    &s[..]
}

他の種類のスライス

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

let slice = &a[1..3];

最初のコレクション要素への参照とコレクションの全長を保存することで、文字列スライスと同じように機能します。

おすすめ

転載: blog.csdn.net/weixin_43912621/article/details/131430630