1. ホーム
  2. pointers

[解決済み] Box、ref、&、*の理解と関連性

2023-06-16 21:54:24

質問

Rustでポインタがどのように動作するのか、少し混乱しています。そこには ref , Box , & , * , といった感じで、連携しているのかよくわかりません。

現在、私が理解している方法は以下の通りです。

  1. Box は実際にはポインタではありません。ヒープ上でデータを割り当て、関数の引数でサイズのない型(特に traits)を渡すための方法です。
  2. ref はパターンマッチで、マッチするものを奪うのではなく、借用するために使われます。例えば

    let thing: Option<i32> = Some(4);
    match thing {
        None => println!("none!"),
        Some(ref x) => println!("{}", x), // x is a borrowed thing
    }
    println!("{}", x + 1); // wouldn't work without the ref since the block would have taken ownership of the data
    
    
  3. & はボロー(借用ポインタ)を作るために使われます。もし私が関数 fn foo(&self) とすると、関数が終了した後に失効する自分自身への参照を取り、呼び出し元のデータだけを残すことになります。また、所有権を保持したいデータを渡すには bar(&mydata) .

  4. * は、生のポインタを作るために使われます:例えば let y: i32 = 4; let x = &y as *const i32 . C/C++のポインタについては理解していますが、これがRustの型システムでどのように動作し、どのように安全に使用できるのかがわかりません。また、このタイプのポインタのユースケースがどのようなものであるかもよくわかりません。さらに * シンボルは参照解除に使用することができます(どんなものを、なぜ?)

どなたか、4 番目のタイプのポインターを説明していただき、他のタイプについての私の理解が正しいかどうか検証していただけないでしょうか。また、私が言及していない一般的な使用事例を指摘してくださる方を感謝します。

どのように解決するのですか?

まず、あなたが挙げた項目は、ポインタに関連するものであっても、実際にはすべて別物です。 Box はライブラリで定義されたスマートポインタの型です。 ref はパターンマッチのための構文です。 & は参照演算子で、参照型では紋章を兼ねる。 * は非参照演算子で、生のポインタ型では紋章を兼ねています。より詳しい説明は以下を参照。

Rustには4つの基本的なポインタ型があり、参照と生のポインタの2つのグループに分けることができます。

&T        - immutable (shared) reference
&mut T    - mutable (exclusive) reference

*const T  - immutable raw pointer
*mut T    - mutable raw pointer

最後の2つの違いは非常に薄く、どちらも何の制限もなく別のものにキャストすることができるので const / mut の区別は、ほとんどリントとして機能します。生のポインターは何にでも自由に作成でき、また、たとえば整数から何もないところから作成することもできます。

参照型とその相互作用は、Rustの重要な特徴の1つである「借用」を定義しています。参照は、いつどのように作成され、どのように使用され、どのように相互作用するかについて多くの制約があります。その代わり、参照は unsafe ブロックなしで使用できます。借用が正確には何であり、どのように機能するかは、この回答の範囲外ですが。

参照と生のポインタの両方が & 演算子で作成できます。

let x: u32 = 12;

let ref1: &u32 = &x;
let raw1: *const u32 = &x;

let ref2: &mut u32 = &mut x;
let raw2: *mut u32 = &mut x;

参照と生のポインタの両方は、以下のようにして参照解除することができます。 * 演算子を使うことができます。ただし、ローポインタの場合は unsafe ブロックが必要です。

*ref1; *ref2;

unsafe { *raw1; *raw2; }

参照解除演算子が省略されることが多いのは、別の演算子である "ドット" 演算子(つまり。 . など) は自動的に左引数を参照または非参照にします。ですから、例えば、次のような定義があるとします。

struct X { n: u32 };

impl X {
    fn method(&self) -> u32 { self.n }
}

とすると、それにもかかわらず method() が取る self を参照する。 self.n は自動的にそれを参照解除するので、わざわざ (*self).n . 同じようなことが method() が呼ばれた場合も同様です。

let x = X { n: 12 };
let n = x.method();

ここで、コンパイラは自動的に xx.method() と書く必要はありません。 (&x).method() .

次の最後のコードも、特殊な &self の構文も示しています。これは、単に self: &Self を意味し、もっと具体的に言うと self: &X をこの例では &mut self , *const self , *mut self も動作します。

つまり、Rustでは参照が主なポインタの種類であり、ほぼ常に使用されるべきものです。参照の制約を受けない生のポインタは、高レベルの抽象化(コレクション、スマートポインタなど)を実装する低レベルのコードや、FFI(Cライブラリとのやり取り)で使用されるべきものです。

Rustはまた 動的サイズ(またはサイズなし)型 . これらの型は静的に知られた明確なサイズを持たないため、ポインタや参照を通じてのみ使用することができます。しかし、ポインタだけでは十分ではありません。例えば、スライスの長さや形質オブジェクトのための仮想メソッドテーブルへのポインタなど、追加の情報が必要なのです。この情報は、サイズのない型へのポインタに "embedded" されるため、これらのポインタは "fat" となります。

ファットポインターは基本的に、データのピースへの実際のポインターといくつかの追加情報(スライスの長さ、形質オブジェクトの vtable へのポインター)を含む構造体です。ここで重要なのは、Rust はポインタの内容に関するこれらの詳細をユーザに対して完全に透過的に処理することです。 &[u32] または *mut SomeTrait のような値で囲むと、対応する内部情報が自動的に渡されます。

Box<T> はRust標準ライブラリのスマートポインタの1つです。対応する型の値を格納するのに十分なメモリをヒープ上に確保する方法を提供し、そのメモリへのポインタであるハンドルとして機能します。 Box<T> は、それが指すデータを所有します。それが削除されると、ヒープ上の対応するメモリの一部が割り当て解除されます。

ボックスを考えるのに非常に便利な方法は、固定サイズの通常の値として考えることです。つまり Box<T> というのは、単に T と同じですが、マシンのポインタサイズに対応するバイト数を常に取る点が異なります。私たちは、(所有) のボックスが 値セマンティクス . 内部的には、他の高レベルの抽象化と同様に、生のポインターを使用して実装されています。

Box のような他のスマートポインタのほとんど全てに当てはまります)。 Rc のように) も借りることができます。 &T から Box<T> . これは、自動的に . 演算子で自動的に行われますし、一度参照を解除して再度参照することで明示的に行うこともできます。

let x: Box<u32> = Box::new(12);
let y: &u32 = &*x;

この点については Box は組み込みポインタと似ていて、その中身に到達するために参照解除演算子を使うことができます。これは、Rust の参照解除演算子がオーバーロード可能であり、スマートポインタの型のほとんど (すべてではないにしても) に対してオーバーロードされているためです。これにより、これらのポインタの内容を簡単に借用することができます。

そして、最後に ref は、値の代わりに参照型の変数を取得するためのパターンの構文にすぎません。例えば

let x: u32 = 12;

let y = x;           // y: u32, a copy of x
let ref z = x;       // z: &u32, points to x
let ref mut zz = x;  // zz: &mut u32, points to x

上記の例は参照演算子で書き換えることができますが、参照演算子は

let z = &x;
let zz = &mut x;

(これはよりイディオム的でもある)場合、以下のような場合があります。 ref が必要な場合もあります。例えば、enum の variant を参照する場合などです。

let x: Option<Vec<u32>> = ...;

match x {
    Some(ref v) => ...
    None => ...
}

上記の例では x は全体の中で借用されているだけで match 文の中だけで借りられるので x の後に match . というように書けば

match x {
    Some(v) => ...
    None => ...
}

では x は、この match によって消費され、それ以降は使用できなくなります。