English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Rust の所有権

コンピュータプログラムは実行中に使用するメモリリソースを管理する必要があります。

ほとんどのプログラミング言語にはメモリ管理の機能があります:

C/C++ このような言語は主に手動でメモリを管理し、開発者は手動でメモリリソースを申請および解放する必要があります。しかし、開発効率を向上させるために、プログラム機能の実現に影響を与えない限り、多くの開発者がメモリを即座に解放する習慣がありません。そのため、手動でメモリを管理する方法はよくリソースの無駄を引き起こします。

Java言語で書かれたプログラムは仮想マシン(JVM)で実行され、JVMはメモリリソースを自動的に回収する機能を持ちます。しかし、この方法は通常、実行時の効率を低下させるため、JVMはできるだけリソースを回収しません。これにより、プログラムが大きなメモリリソースを占有することになります。

所有権は多くの開発者にとって新しい概念であり、Rust 言語がメモリの効率的な使用を設計するための文法メカニズムです。所有権概念は、Rust がコンパイル段階でメモリリソースの有用性を効果的に分析し、メモリ管理を実現するために生まれた概念です。

所有権ルール

所有権には以下の3つのルールがあります:

  • Rust の各値には所有者が存在し、それがその所有者と呼ばれます。

  • 一度に1つの所有者しかいません。

  • 所有者がプログラムの実行範囲外にいる場合、その値は削除されます。

これらの3つのルールは所有権概念の基本です。

次に、所有権概念に関連する概念を紹介します。

変数範囲

以下のプログラムを使用して、変数範囲の概念を説明します:

{
    // 宣言の前に、変数 s は無効です
    let s = "w3codebox";
    // ここは変数 s の有効範囲です
}
// 変数範囲が終わった後、変数 s は無効です

変数範囲は変数の属性であり、変数の有効範囲を表し、デフォルトでは変数の宣言から有効範囲の終わりまでです。

メモリと分配

もし変数を定義し、それに値を割り当てると、その値はメモリに存在します。この状況は非常に一般的です。しかし、データの長さが不確定である場合(例えば、ユーザーが入力する文字列の長さ)、データの長さを定義することができず、プログラムがデータを保存するために固定長のメモリ空間を割り当てることはできません。(ある人は、できるだけ大きな空間を割り当てることで問題を解決できると言いますが、この方法は非常に非文明です)。これは、プログラムが実行時にメモリを使用するためのメカニズムを提供する必要があります——スタックです。本章で述べるすべての「メモリリソース」は、スタックが占めるメモリ空間を指します。

分配がある以上、プログラムは常にあるメモリリソースを占有することはできません。したがって、リソースがタイムリーに解放されるかどうかが、リソースが無駄になるかどうかの決定要因です。

私たちは、C 言語などの等価のプログラムで文字列の例を書きます:

{
    char *s = "w3codebox";
    free(s); // s リソースを解放
}

明らかに、Rust では、文字列 s のリソースを free 関数で解放することはありません(C 言語では、"w3codebox" はスタックに存在しない、ここではそれが)(Rust がリリースのステップを明示しない理由は、変数の範囲が終わると、Rust コンパイラが自動的にリソースを解放する関数を呼び出すステップを追加するからです。

このメカニズムは非常にシンプルに見える:プログラマーが適切な場所にリソースを解放する関数呼び出しを追加する手助けをするだけであり、ただそれだけです。しかし、このシンプルなメカニズムは、歴史上でプログラマーが最も苦手なプログラミング問題を効果的に解決することができます。

データとの相互作用の方法

データとの相互作用方法は主に移動(Move)とクローン(Clone)の2種類があります:

移動

Rustでは、同じデータとデータとの相互作用は異なる方法で行うことができます:

let x = 5;
let y = x;

このプログラムは値 5 変数 xにバインドし、その値をコピーして変数 yに割り当てます。これにより、スタックには2つの値が存在します 5。この場合のデータは「基本データ型」のデータであり、スタックに保存する必要はありません。スタックのデータの「移動」方法は直接コピーであり、これにより時間やストレージ空間が増加しません。"基本データ型"にはこれらがあります:

  • すべての整数型、例えば i32 、u32 、i64 などです。

  • 論理型 bool、trueまたはfalseの値を持っています。

  • すべての浮動小数点型、f32 そして f64。

  • 文字型 char。

  • 上記のタイプのデータのみを含むタプル(Tuples)。

しかし、データの交換がスタックのデータである場合、状況は全く異なります:

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

第1ステップでは「hello」という値を持つStringオブジェクトが生成されます。その中の「hello」は長さが不定のデータと考えられ、スタックに保存する必要があります。

第2ステップの状況は少し異なります(これは完全に真実ではなく、比較のために用意されています):

図のように:二つのStringオブジェクトがスタックにあります。それぞれのStringオブジェクトには、スタックの「hello」文字列を指すポインタがあります。s2 を割り当てるとき、スタックのデータのみがコピーされ、スタックの「hello」は元の文字列のままです。

について、私たちは既に述べましたが、変数が範囲を超えた場合、Rustは自動的にリソース解放関数を呼び出し、その変数のスタックメモリをクリーンアップします。しかし、s1 s と2 が全て解放された場合、スタックの「hello」が二度解放されます。これはシステムが許可しないことです。安全を確保するために、s2 を割り当てるとき s1 が無効になります。確かに、s1 の値を s2 以降 s1 以降は使用できなくなります。以下のプログラムは間違っています:

let s1 = String::from("hello");
let s2 = s1; 
println!("{}, world!", s1); // エラー!s1 無効

実際の状況はこのようにです:

s1 名存実亡。

クローン

Rustはプログラムの実行コストをできるだけ低く抑えるために、デフォルトでは、長いデータはスタックに保存され、データの交換には移動方式が使用されます。しかし、データを単にコピーして他の用途に使用する必要がある場合、データの第二种の交換方法である「クローン」を使用することができます。

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

実行結果:

s1 = hello, s2 = hello

ここでは実際にスタックの「hello」をコピー了一份、ので s1 s と2 それぞれに値がバインドされ、解放される際には2つのリソースとして扱われます。

もちろん、コピーは必要に応じてのみ使用されるべきであり、データのコピーには時間がかかります。

関数の所有権メカニズムに関連しています

変数にとってこれは最も複雑な状況です。

変数を関数に引数として渡す場合、所有権を安全に処理する方法はどうですか?

以下のプログラムは、このような場合の所有権メカニズムの動作原理を説明しています:

fn main() {
    let s = String::from("hello");
    // s が有効と宣言されました
    takes_ownership(s);
    // s の値が関数に引数として渡されます
    // したがって、s が既に移動されていると考え、ここから無効です
    let x = 5;
    // x が有効と宣言されました
    makes_copy(x);
    // x の値が関数に引数として渡されます
    // しかし x は基本型であり、まだ有効
    // ここではまだ x を使用できますが、s を使用することはできません
} // 関数が終了するとき、x が無効になり、それから s. しかし s は既に移動されているので、解放する必要はありません
fn takes_ownership(some_string: String) { 
    // 一つの String 引数 some_string が渡され、有効
    println!("{}", some_string);
} // 関数が終了するとき、引数 some_string がここで解放されます
fn makes_copy(some_integer: i32) { 
    // 一つの i32 引数 some_integer が渡され、有効
    println!("{}", some_integer);
} // 関数が終了するとき、引数 some_integer は基本型であり、解放する必要はありません

変数を関数に引数として渡すと、その効果は移動と同じです。

関数の戻り値の所有権メカニズム

fn main() {
    let s1 = gives_ownership();
    // gives_ownership はその戻り値を s に移動しました1
    let s2 = String::from("hello");
    // s2 有効と宣言されました
    let s3 = takes_and_gives_back(s2);
    // s2 引数として移動されました, s3 戻り値の所有権を取得しました
} // s3 無効が解放されました, s2 移動されました, s1 無効が解放されました.
fn gives_ownership() -> String {
    let some_string = String::from("hello");
    // some_string が有効と宣言されました
    return some_string;
    // some_string が関数から戻り値として移動されました
}
fn takes_and_gives_back(a_string: String) -> String { 
    // a_string が有効と宣言されました
    a_string  // a_string が関数の返り値として移動され出されます
}

関数の返り値としての変数の所有権は、関数から移動され、呼び出し元の関数に戻され、直接無効にされることはありません。

参照とレンタル

参照(Reference)はC++ 開発者がよく知っている概念です。

ポインタの概念に慣れている場合は、それをポインタとして考えてください。

「引用」とは、変数の間接的なアクセス方法です。

fn main() {
    let s1 = String::from("hello");
    let s2 = &s1;
    println!("s1 is {}, s2 is {}", s1, s2);
}

実行結果:

s1 is hello, s2 is hello

& 演算子は変数の「参照」を取得できます。

変数の値が参照された場合、変数自体は無効とされません。なぜなら「参照」はスタックで変数の値をコピーしていないからです:

関数の引数の渡し方の原理と同じです:

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);
    println!("「{}」の長さは{}です。", s1, len);
}
fn calculate_length(s: &String) -> usize {
    s.len()
}

実行結果:

「hello」の長さは 5.

参照は値の所有権を得ません。

参照は値の所有権をレンタルするだけです。

参照自体も型であり、値を記録しており、その値は他の値の場所を示していますが、参照はその値の所有権を持ちません:

fn main() {
    let s1 = String::from("hello");
    let s2 = &s1;
    let s3 = s1;
    println!("{}", s)2);
}

このプログラムは正しくありません:なぜならs2 レンタルのs1 は所有権をsに移動しました3、そのためs2 sのレンタル使用を続けることができません1 の所有権。sを使用する必要がある場合2 この値を使用するには、再びレンタルする必要があります:

fn main() {
    let s1 = String::from("hello");
    let mut s2 = &s1;
    let s3 = s2;
    s2 = &s3; // sから再びレンタルします3 レンタル所有権
    println!("{}", s)2);
}

このプログラムは正しいです。

参照は所有権を持っていないため、所有権をレンタルしても、それだけ使用权を持ちます(家をレンタルするのと同じです)。

レンタル権を利用してデータを変更しようと試みると、阻止されます:

fn main() {
    let s1 = String::from("run");
    let s2 = &s1; 
    println!("{}", s)2);
    s2.push_str("oob"); // エラー、レンタルの値の変更は禁止されています
    println!("{}", s)2);
}

このプログラムのs2 sを変更しようと試みます1 の値は変更を阻止され、レンタルの所有権は所有者の値を変更することができません。

もちろん、可変なレンタル方法もあります。例えば、あなたが家をレンタルし、管理者が所有者に家の構造を変更することができることを指定し、レンタル時にその権利をあなたに譲渡する場合、あなたは家をリノベーションすることができます:

fn main() {
    let mut s1 = String::from("run");
    // s1 は可変です
    let s2 = &mut s1;
    // s2 は可変の参照です
    s2.push_str("oob");
    println!("{}", s)2);
}

このプログラムには問題はありません。可変参照の修飾子として &mut を使用します。

可変参照と不可変参照と比較して、権限が異なる以外に、可変参照は多重参照を許可しませんが、不可変参照は許可できます:

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

このプログラムは正しくありません。s に対して多重可変参照が行われています。

Rust が可変参照に対するこの設計は、並行状態でのデータアクセスクラッシュを避けるためのものであり、コンパイル段階でそのようなことが発生することを避けます。

データアクセスクラッシュが発生する必要条件のうちの一つは、データが少なくとも1人のユーザーによって書かれ、少なくとも1人の他のユーザーによって読んだり書かれたりするため、値が可変参照によって参照されると、再度参照することは許可されていません。

垂れ下がる参照(Dangling References)

これは名前を変えた概念であり、ポインタ概念を持つプログラミング言語に置かれれば、実際にアクセスできるデータを指していないポインタ(空ポインタではなく、解放されたリソースも含まれます)を指します(注意、空ポインタではなく、解放されたリソースも含まれます)。それらは失われた悬挂物体の紐のように見え、そのため「垂れ下がる参照」と呼ばれます。

「垂れ下がる参照」は Rust 言語では許可されていません。あれば、コンパイラがそれを見つけます。

以下は、垂れ下がる典型的な例です:

fn main() {
    let reference_to_nothing = dangle();
}
fn dangle() -> &String {
    let s = String::from("hello");
    &s
}

もちろん、dangle 関数の終了とともに、そのローカル変数の値自体がリターン値として使われず、解放されました。しかし、その参照はリターンされ、その参照が指す値は存在しないと確かめられないため、その出現は許可されていません。