Rust TraitsはGoインターフェイスとどう違うのですか?


64

私はGoに比較的精通しており、小さなプログラムをいくつか書いています。錆は、もちろん、あまり馴染みがありませんが、注視しています。

最近http://yager.io/programming/go.htmlを読んで、Genericsが処理される2つの方法を個人的に調べたいと思いました。エレガントに達成できませんでした。私は、Rustの特徴がいかに強力であるかについての誇大宣伝を聞き続けました。Goでいくつかの経験を積んで、それがどのように真実であり、最終的にどのような違いがあったのかと思いました。私が見つけたのは、特性とインターフェースがかなり似ているということです!最終的に、私は何かが欠けているかどうかわからないので、ここでそれらの類似点の簡単な教育的要約があります。

それでは、ドキュメントからGoインターフェイスを見てみましょう。

Goのインターフェイスは、オブジェクトの動作を指定する方法を提供します。これができる場合は、ここで使用できます。

最も一般的なインターフェイスはStringer、オブジェクトを表す文字列を返すことです。

type Stringer interface {
    String() string
}

そのため、そのString()上で定義されているStringerオブジェクトはすべてオブジェクトです。これfunc (s Stringer) print()は、ほとんどすべてのオブジェクトを取得して印刷するようなタイプシグネチャで使用できます。

またinterface{}、いずれのオブジェクトも受け取ります。その後、実行時にリフレクションを介してタイプを決定する必要があります。


それでは、ドキュメントからRust Traitsを見てみましょう:

最も単純な場合、特性はゼロ個以上のメソッドシグネチャのセットです。たとえば、単一のメソッドシグネチャを使用して、コンソールに出力できるものに対して特性Printableを宣言できます。

trait Printable {
    fn print(&self);
}

これは、すぐにGoインターフェースに非常によく似ています。私が見る唯一の違いは、単にメソッドを定義するのではなく、特性の「実装」を定義することです。だから、私たちはやる

impl Printable for int {
    fn print(&self) { println!("{}", *self) }
}

の代わりに

fn print(a: int) { ... }

ボーナス質問:特性を実装するが使用しない関数を定義すると、Rustで何が起こりますimplか?うまくいきませんか?

Goのインターフェイスとは異なり、Rustの型システムには型パラメーターがありinterface{}、コンパイラーとランタイムが実際に型を認識している間、適切なジェネリックなどを実行できます。例えば、

trait Seq<T> {
    fn length(&self) -> uint;
}

どの型でも動作し、コンパイラはリフレクションを使用するのではなく、コンパイル時にSequence要素の型を知っています。


さて、実際の質問:ここで違いがありませんか?彼らは実際にあるものと類似?ここに欠けている基本的な違いはありませんか?(使用法。実装の詳細は興味深いですが、機能が同じであれば最終的には重要ではありません。)

構文の違いに加えて、実際に見られる違いは次のとおりです。

  1. Goにはimpl、トレイトを実装する ための自動メソッドディスパッチとRustのrequire(?)があります
    • エレガントvs明示的
  2. Rustには、リフレクションなしで適切なジェネリックを許可する型パラメーターがあります。
    • Goは実際にここに応答しません。これは非常に強力な唯一のものであり、最終的には、異なるタイプシグネチャを持つメソッドをコピーして貼り付けるための単なる置き換えにすぎません。

これらが唯一の重要な違いですか?その場合、Goのインターフェイス/タイプシステムは、実際には、認識されているほど弱くないように見えます。

回答:


59

特性を実装する関数を定義しているが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>、キーが常にintsであり、値が常にStrings であることが保証されます。誤って間違った値を挿入する(または非を抽出しようとするString)方法はありません。


私自身AnyMapは、Rustの長所の優れたデモンストレーションであり、特性オブジェクトをジェネリックと組み合わせて、Goで必然的に記述される壊れやすいものの安全で表現力豊かな抽象化を提供しますmap[string]interface{}
クリスモーガン14

予想どおり、Rustはより強力で、ネイティブ/エレガントに選択肢が増えますが、Goのシステムは十分に近いので、見逃したほとんどのことをのような小さなハックで実現できますinterface{}。Rustは技術的には優れているように見えますが、Goに対する批判はやや厳しすぎると思います。プログラマの力は、タスクの99%でほぼ同等です。
ローガン14

22
@Logan、低レベル/高パフォーマンスドメインRustは、静的ディスパッチ(およびそれが与える/最適化するオプション)のオプションを持たず、(オペレーティングシステム、Webブラウザ、コア「システム」プログラミングなど)を目指しています。許可されます)は受け入れられません。Goがこれらの種類のアプリケーションに対してRustほど適切ではない理由の1つです。いずれにせよ、プログラマの力は実際には同等ではなく、再利用可能な非組み込みデータ構造の型安全性を失い(コンパイル時間)、実行時型アサーションにフォールバックします。
huon 14

10
それはまさにその通りです。Rustはより多くのパワーを提供します。Rustは安全なC ++であり、Goは高速なPython(または非常に単純化されたJava)であると考えています。開発者の生産性が最も重要なタスクの大部分(およびランタイムやガベージコレクションなどは問題ありません)では、Goを選択します(Webサーバー、コンカレントシステム、コマンドラインユーティリティ、ユーザーアプリケーションなど)。パフォーマンスのすべての最後のビットが必要な場合(および開発者の生産性が低下する場合)、Rust(たとえば、ブラウザー、オペレーティングシステム、リソースに制約のある組み込みシステム)を選択します。
weberc2
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.