web-dev-qa-db-ja.com

なぜこの特性で「サイズ」の境界が必要なのですか?

2つの関連する機能を持つ特性があります。

_trait WithConstructor: Sized {
    fn new_with_param(param: usize) -> Self;

    fn new() -> Self {
        Self::new_with_param(0)
    }
}
_

2番目のメソッド(new())のデフォルトの実装で、型にSizedをバインドする必要があるのはなぜですか?スタックポインタの操作によるものだと思いますが、よくわかりません。

コンパイラがスタックにメモリを割り当てるためにサイズを知る必要がある場合、次の例ではSizedTを必要としないのはなぜですか?

_struct SimpleStruct<T> {
    field: T,
}

fn main() {
    let s = SimpleStruct { field: 0u32 };
}
_
24
eulerdisk

おそらくすでにご存じのとおり、Rustの型はサイズとサイズを変更できます。サイズが指定されていないタイプは、その名前が示すように、このタイプの値を格納するために必要なサイズがコンパイラに知られていません。たとえば、_[u32]_は_u32_ sのサイズなし配列です。要素の数はどこにも指定されていないため、コンパイラはそのサイズを認識しません。別の例は、次のようなベアトレイトオブジェクトタイプです。 、Display、型として直接使用される場合:

_let x: Display = ...;
_

この場合、コンパイラーは実際にどのタイプがここで使用されているかを認識しておらず、消去されているため、これらのタイプの値のサイズを認識していません。上記の行は無効です-サイズを知らずにローカル変数を作成することはできません(スタックに十分なバイトを割り当てるため)、そしてサイズなしの型の値を引数として関数に渡すことも、関数から返すこともできません

サイズなしの型は、ポインターを介して使用できますが、追加の情報-スライスで使用可能なデータの長さ(_&[u32]_)または仮想テーブルへのポインター(_Box<SomeTrait>_)を使用できます。ポインタは常に固定された既知のサイズであるため、ローカル変数に格納して、関数に渡したり、関数から返したりできます。

具象型を考えると、サイズ付きかサイズなしかをいつでも言うことができます。ただし、ジェネリックスを使用すると、問題が発生します-型パラメーターのサイズが設定されているかどうか。

_fn generic_fn<T>(x: T) -> T { ... }
_

Tのサイズが指定されていない場合、サイズの指定されていない値を直接渡すことができないため、このような関数の定義は正しくありません。サイズが決まっている場合は、すべて問題ありません。

Rustでは、すべてのジェネリック型パラメーターはデフォルトですべての場所でサイズが設定されます-関数、構造体、およびトレイトで。これらには暗黙のSizedバインドがあります。Sizedはサイズ指定されたタイプをマークするための特性です:

_fn generic_fn<T: Sized>(x: T) -> T { ... }
_

これは、圧倒的な回数でジェネリックパラメーターのサイズを変更する必要があるためです。ただし、サイズをオプトアウトしたい場合があります。これは_?Sized_バインドで実行できます。

_fn generic_fn<T: ?Sized>(x: &T) -> u32 { ... }
_

これで_generic_fn_をgeneric_fn("abcde")のように呼び出すことができ、Tはサイズ変更されていないstrでインスタンス化されますが、これは問題ありません。この関数はTへの参照を受け入れるため、問題はありません。

ただし、サイズの問題が重要な別の場所があります。 Rustの特性は、常にいくつかのタイプに実装されています:

_trait A {
    fn do_something(&self);
}

struct X;
impl A for X {
    fn do_something(&self) {}
}
_

ただし、これは利便性と実用性の目的でのみ必要です。特性を定義して、常に1つの型パラメーターを取り、その特性が実装されている型を指定しないようにすることができます。

_// this is not actual Rust but some Rust-like language

trait A<T> {
    fn do_something(t: &T);
}

struct X;
impl A<X> {
    fn do_something(t: &X) {}
}
_

これがHaskell型クラスのしくみであり、実際に、トレイルが実際に下位レベルでRust=)に実装されている方法です。

Rustの各トレイトには、Selfと呼ばれる暗黙のタイプパラメータがあり、このトレイトが実装されるタイプを指定します。これは、トレイトの本文で常に使用できます。

_trait A {
    fn do_something(t: &Self);
}
_

ここで、サイズの問題が問題になります。 Selfパラメーターのサイズはありますか?

いいえ、RustではSelfはデフォルトではサイズ設定されていません。各特性には、Selfにバインドされた暗黙の_?Sized_があります。サイズなしの型のために実装でき、まだ機能する多くの特性があるため、これが必要とされる理由の1つ。たとえば、参照によってSelfのみを取得して返すメソッドのみを含むすべての特性は、サイズなしの型に対して実装できます。動機についての詳細は RFC 546 で読むことができます。

特性とそのメソッドのシグネチャのみを定義する場合、サイズは問題になりません。これらの定義には実際のコードがないため、コンパイラーは何も想定できません。ただし、暗黙的なSelfパラメーターを使用するためデフォルトのメソッドを含む、この特性を使用する汎用コードの作成を開始するときは、サイズを考慮する必要があります。 Selfはデフォルトではサイズが設定されていないため、デフォルトの特性メソッドは値によってSelfを返すことも、値としてパラメータとして受け取ることもできません。したがって、Selfのデフォルトのサイズを指定する必要があります。

_trait A: Sized { ... }
_

または、Selfのサイズが指定されている場合にのみメソッドを呼び出せるように指定できます。

_trait WithConstructor {
    fn new_with_param(param: usize) -> Self;

    fn new() -> Self
    where
        Self: Sized,
    {
        Self::new_with_param(0)
    }
}
_
71

サイズなしの型でこれを行うとどうなるか見てみましょう。

new()moves呼び出し元へのnew_with_param(_)メソッドの結果。しかし、型のサイズが指定されていない限り、何バイトを移動する必要がありますか?私たちは単に知ることはできません。そのため、移動のセマンティクスにはSized型が必要です。

注:さまざまなBoxesは、まさにこの問題に対してランタイムサービスを提供するように設計されています。

5
llogiq