Rust特性与Go Interface有何不同?


64

我对Go相对熟悉,在其中编写了许多小程序。当然,Rust我不太熟悉,但请注意。

最近阅读过http://yager.io/programming/go.html以后,我认为我将亲自研究泛型的两种处理方式,因为在实践中,当接口实际上没有太多内容时,这篇文章似乎不公平地批评了Go。不能优雅地完成。我一直在听到关于Rust的特质多么强大的炒作,除了人们对Go的批评之外,什么也没有。有了Go的经验,我想知道它的真实性以及最终的差异。我发现特征和接口非常相似!最终,我不确定是否遗漏了一些东西,因此这里是他们相似之处的快速教育总结,您可以告诉我我错过了什么!

现在,让我们从他们的文档中了解Go Interfaces :

Go中的接口提供了一种指定对象行为的方式:如果可以做到这一点,则可以在这里使用它。

到目前为止,最常见的接口是Stringer返回表示对象的字符串。

type Stringer interface {
    String() string
}

因此,String()在其上定义的任何对象都是Stringer对象。可以在类型签名中使用它,这样func (s Stringer) print()几乎可以获取所有对象并进行打印。

我们也有interface{}哪个对象。然后,我们必须在运行时通过反射确定类型。


现在,让我们从他们的文档中了解Rust特性:

简单来说,特征是一组零个或多个方法签名。例如,对于可以打印到控制台的内容,我们可以使用单个方法签名声明特征Printable:

trait Printable {
    fn print(&self);
}

这立即看起来与我们的Go Interfaces非常相似。我看到的唯一区别是,我们定义了特征的“实现”,而不仅仅是定义了方法。所以,我们做

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具有自动方法分派,与Rust相比,Rust require(?)impl可以实现Trait
    • 优雅vs显式
  2. Rust具有类型参数,可以使用适当的泛型而无需进行反射。
    • Go真的在这里没有任何回应。这是唯一功能强大得多的功能,并且最终只是替换具有不同类型签名的复制和粘贴方法。

这些是唯一的重要区别吗?如果是这样的话,那么Go的Interface / Type系统实际上并没有想象中那么脆弱。

Answers:


59

如果您定义实现特征的函数但不使用impl,在Rust中会发生什么?就是行不通吗?

您需要明确实现特征;碰巧具有匹配名称/签名的方法对Rust毫无意义。

通用呼叫调度

这些是唯一的重要区别吗?如果是这样的话,那么Go的Interface / Type系统实际上并没有想象中那么脆弱。

在某些情况下(例如,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上面的两个调用将分别编译为对的调用,

fn call_bar_int(value: int) { value.bar() }
fn call_bar_string(value: String) { value.bar() }

这些.bar()方法调用是静态函数调用,即到内存中的固定函数地址。这样可以进行内联等优化,因为编译器可以准确地知道正在调用哪个函数。(这也是C ++所做的,有时也称为“单态化”。)

在围棋

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"))
}

现在,这两个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可以通过trait对象选择加入动态程序,例如Go(具有基本相同的实现)。像一样&Foo标记,是对实现Foo特性的未知类型的借用引用。这些值与Go接口对象具有相同/非常相似的vtable表示形式。(特征对象是“现有类型”的示例。)

在某些情况下,动态调度确实很有用(例如,通过减少代码膨胀/重复,有时会提高性能),但是静态调度使编译器可以内联调用站点并应用所有优化,这通常会更快。对于Rust的迭代协议之事情而言,这尤其重要,在该协议中,静态分派特征方法调用允许那些迭代器与C等效项一样快,同时看起来仍然很高级且富有表现力

Tl; dr:Rust的方法可以由程序员自行决定在泛型中提供静态还是动态调度;Go仅允许动态调度。

参数多态性

此外,强调特征和不强调反射使Rust具有更强的参数多态性:程序员确切地知道函数可以用其参数做什么,因为程序员必须声明泛型类型在函数签名中实现的特征。

Go的方法非常灵活,但是对调用方的保证较少(使程序员难以推理),因为函数的内部可以(并且确实)查询其他类型信息(Go中存在错误)标准库,其中iirc是一个接受编写器的函数,它将使用反射来调用Flush某些输入,而不能调用其他输入)。

建立抽象

这有点痛,所以我只简单谈一谈,但是有了Rust这样的“适当的”泛型,就可以使用Go的低级数据类型,map并且[]实际上可以以高度类型安全的方式直接在标准库中实现,并且用Rust(HashMapVec分别)编写。

而且,不仅是那些类型,您还可以在它们之上构建类型安全的通用结构,例如,LruCache是在哈希图之上的通用缓存层。这意味着人们可以直接使用标准库中的数据结构,不必interface{}在插入/提取时将数据存储为和使用类型声明。也就是说,如果有一个LruCache<int, String>,则可以确保键始终int是s,值始终String是s:没有办法意外插入错误的值(或尝试提取非值String)。


我自己AnyMap很好地演示了Rust的优势,将特征对象与泛型相结合,可以对Go中必须编写的易碎内容提供安全且富有表现力的抽象map[string]interface{}
克里斯·摩根

正如我预期的那样,Rust功能更强大,并且本机/优雅地提供了更多选择,但是Go的系统非常接近,可以通过诸如的小技巧来完成它错过的大多数事情interface{}。尽管Rust在技术上似乎比较优越,但我仍然认为对Go ...的批评有点过于苛刻。对于99%的任务,程序员的力量几乎是同等的。
Logan

22
@ Logan,Rust面向的是低级/高性能域(例如,操作系统,Web浏览器……核心“系统”编程内容),没有静态分配选项(以及它提供/优化的性能)它允许)是不可接受的。这是Go在这类应用程序中不如Rust合适的原因之一。无论如何,程序员的能力无法真正发挥作用,对于任何可重用且非内置的数据结构,您都会失去(编译时)类型安全,而后退到运行时类型声明。
休2014年

10
没错-锈能为您提供更多功能。我认为Rust是安全的C ++,而Go则是快速的Python(或大大简化的Java)。对于开发人员生产力最重要的大部分任务(运行时和垃圾回收等问题不成问题),请选择Go(例如,Web服务器,并发系统,命令行实用程序,用户应用程序等)。如果您需要性能的最后一点(并且会削弱开发人员的生产力),请选择Rust(例如,浏览器,操作系统,资源受限的嵌入式系统)。
weberc2
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.