如果您定义实现特征的函数但不使用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(HashMap
和Vec
分别)编写。
而且,不仅是那些类型,您还可以在它们之上构建类型安全的通用结构,例如,LruCache
是在哈希图之上的通用缓存层。这意味着人们可以直接使用标准库中的数据结构,而不必interface{}
在插入/提取时将数据存储为和使用类型声明。也就是说,如果有一个LruCache<int, String>
,则可以确保键始终int
是s,值始终String
是s:没有办法意外插入错误的值(或尝试提取非值String
)。
AnyMap
很好地演示了Rust的优势,将特征对象与泛型相结合,可以对Go中必须编写的易碎内容提供安全且富有表现力的抽象map[string]interface{}
。