特性を実装する関数を定義しているがimplを使用していない場合、Rustで何が起こりますか?うまくいかない?
特性を明示的に実装する必要があります。Rustには、一致する名前/署名を持つメソッドがあることは意味がありません。
ジェネリックコールディスパッチ
これらが唯一の重要な違いですか?その場合、Goのインターフェイス/タイプシステムは、実際には、認識されているほど弱くないように見えます。
静的ディスパッチを提供しないと、特定のケース(Iterator
以下で言及するケースなど)でパフォーマンスが大幅に低下する可能性があります。これがあなたの言っていることだと思う
Goは実際にここに応答しません。これは非常に強力な唯一のものであり、最終的には、異なるタイプシグネチャを持つメソッドをコピーして貼り付けるための単なる置き換えにすぎません。
違いを深く理解する価値があるので、より詳細に説明します。
さびた
Rustのアプローチにより、ユーザーは静的ディスパッチと動的ディスパッチを選択できます。例として、あなたが持っている場合
trait Foo { fn bar(&self); }
impl Foo for int { fn bar(&self) {} }
impl Foo for String { fn bar(&self) {} }
fn call_bar<T: Foo>(value: T) { value.bar() }
fn main() {
call_bar(1i);
call_bar("foo".to_string());
}
call_bar
上記の2つの呼び出しは、それぞれに呼び出しにコンパイルされます。
fn call_bar_int(value: int) { value.bar() }
fn call_bar_string(value: String) { value.bar() }
これらの.bar()
メソッド呼び出しは静的関数呼び出し、つまりメモリ内の固定関数アドレスへの呼び出しです。これにより、コンパイラーはどの関数が呼び出されているかを正確に把握しているため、インライン化などの最適化が可能になります。(これはC ++が行うことであり、「単相化」とも呼ばれます。)
囲Inで
Goは、「汎用」関数の動的ディスパッチのみを許可します。つまり、メソッドアドレスは値からロードされ、そこから呼び出されるため、正確な関数は実行時にのみ認識されます。上記の例を使用する
type Foo interface { bar() }
func call_bar(value Foo) { value.bar() }
type X int;
type Y string;
func (X) bar() {}
func (Y) bar() {}
func main() {
call_bar(X(1))
call_bar(Y("foo"))
}
現在、これらの2つcall_bar
は常に上記を呼び出し、インターフェイスのvtableからロードされたcall_bar
アドレスをbar
使用します。
低レベル
上記を言い換えると、C表記法です。Rustのバージョンは作成します
/* "implementing" the trait */
void bar_int(...) { ... }
void bar_string(...) { ... }
/* the monomorphised `call_bar` function */
void call_bar_int(int value) {
bar_int(value);
}
void call_bar_string(string value) {
bar_string(value);
}
int main() {
call_bar_int(1);
call_bar_string("foo");
// pretend that is the (hypothetical) `string` type, not a `char*`
return 1;
}
Goの場合は、次のようなものです。
/* implementing the interface */
void bar_int(...) { ... }
void bar_string(...) { ... }
// the Foo interface type
struct Foo {
void* data;
struct FooVTable* vtable;
}
struct FooVTable {
void (*bar)(void*);
}
void call_bar(struct Foo value) {
value.vtable.bar(value.data);
}
static struct FooVTable int_vtable = { bar_int };
static struct FooVTable string_vtable = { bar_string };
int main() {
int* i = malloc(sizeof *i);
*i = 1;
struct Foo int_data = { i, &int_vtable };
call_bar(int_data);
string* s = malloc(sizeof *s);
*s = "foo"; // again, pretend the types work
struct Foo string_data = { s, &string_vtable };
call_bar(string_data);
}
(これは正確ではありません--- vtableにより多くの情報が必要です---しかし、動的関数ポインタであるメソッド呼び出しはここで関連するものです。)
Rustは選択肢を提供します
に戻る
Rustのアプローチにより、ユーザーは静的ディスパッチと動的ディスパッチを選択できます。
これまで、静的にディスパッチされたジェネリックを持つRustを示しましたが、Rustは、特性オブジェクトを介してGo(本質的に同じ実装)のような動的なものにオプトインできます。のように表記されます&Foo
。これは、Foo
特性を実装する未知の型への借用参照です。これらの値は、Goインターフェイスオブジェクトと同じ/非常に類似したvtable表現を持ちます。(特性オブジェクトは、「実在型」の例です。)
動的ディスパッチが本当に役立つ場合があります(コードの膨張や重複を減らすなどによりパフォーマンスが向上する場合もあります)が、静的ディスパッチにより、コンパイラは呼び出しサイトをインライン化し、すべての最適化を適用できるため、通常は高速です。これは、Rustの反復プロトコルのようなものにとって特に重要です。静的ディスパッチ特性メソッドの呼び出しにより、これらの反復子はCの同等物と同じくらい高速でありながら、高レベルで表現力があります。
Tl; dr:Rustのアプローチは、プログラマーの裁量で、ジェネリックで静的および動的ディスパッチの両方を提供します。Goでは、動的ディスパッチのみが許可されます。
パラメトリック多型
さらに、特性を強調し、リフレクションを重視しないことで、Rustははるかに強力なパラメトリックポリモーフィズムを実現します。プログラマは、関数のシグネチャでジェネリック型が実装する特性を宣言する必要があるため、関数が引数で何ができるかを正確に知っています。
Goのアプローチは非常に柔軟ですが、関数の内部は追加の型情報を照会できる(そして実行する)ため、呼び出し側に対する保証が少なくなります(プログラマーが推論するのが多少難しくなります)(Goにはバグがありました) iircでは、ライターを使用する関数がリフレクションを使用してFlush
一部の入力を呼び出しますが、他の入力は呼び出しません。
抽象化の構築
これはやや辛い点なので、簡単に説明しますが、Rustのような「適切な」ジェネリックを使用すると、Goのような低レベルのデータ型が可能にmap
なり[]
、実際に標準ライブラリに強力なタイプセーフな方法で直接実装できます。ルストで書かれた(HashMap
そしてVec
、それぞれ)。
そしてそれらの型だけでなく、それらの上に型保証された一般的な構造を構築できます。例えばLruCache
、ハッシュマップの上に一般的なキャッシング層があります。つまり、人々は標準ライブラリから直接データ構造を使用でき、挿入/抽出時にデータを保存したり、型アサーションを使用したりする必要はありませんinterface{}
。つまり、を持っている場合LruCache<int, String>
、キーが常にint
sであり、値が常にString
s であることが保証されます。誤って間違った値を挿入する(または非を抽出しようとするString
)方法はありません。
AnyMap
は、Rustの長所の優れたデモンストレーションであり、特性オブジェクトをジェネリックと組み合わせて、Goで必然的に記述される壊れやすいものの安全で表現力豊かな抽象化を提供しますmap[string]interface{}
。