观感
Rust的Trait和Golang的interface看起来非常相似,从开发者角度来看,都可以实现具体类型的抽象化。
golang:
type geometry interface {
area() float64
}
type rect struct {
width, height float64
}
func (r rect) area() float64 {
return r.width * r.height
}
func measure(g geometry) {
fmt.Println(g)
fmt.Println(g.area())
}
func main() {
r := rect{width: 3, height: 4}
measure(r)
}
Rust:
use core::f64::consts::PI;
use core::fmt::Debug;
trait Geometry {
fn area(&self) -> f64;
}
#[derive(Debug)]
struct Rect {
width: f64,
height: f64,
}
impl Geometry for Rect {
fn area(&self) -> f64 {
self.width * self.height
}
}
fn main() {
fn measure<T>(g: &T)
where T: Geometry + Debug
{
println!("{:?}", g);
println!("{:?}", g.area());
}
let r = Rect{width: 3.0, height: 4.0};
measure(&r);
}
从上面的代码可以简单看出来,Golang中的Interface与具体的结构体之间是自动关联的,不像Rust需要显式的用一个impl
来关联。
此外,回顾下前文范型相关的内容看,Rust可以为非确定类型实现trait,但是Golang仅能对确定的struct实现Interface。
pub trait MetroCodeCheck {
fn metro_status(&self) -> String;
}
impl<T> MetroCodeCheck for T
where
T: TravelCodeCheck,
{
fn metro_status(&self) -> String {
format!("{}", self.travel_status())
}
}
在这个例子中,为T
类型实现了MetroCodeCheck
,而T
是一个范型,可能对应于其他的已定义的类型,并不与一个确切的类型绑定。
静态分发
下面我们通过模拟编译器的行为来分别分析静态分发。对于Golang而言,仅允许动态分发,每一个Interface中的方法地址是从值中动态加载然后调用的,所以只有在运行期间才能知道具体的函数。
考虑一个例子:
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"))
}
如果用C语言模拟Golang的原理,忽略掉一些必要的细节后,会得到类似的代码:
void bar_int(...) { ... }
void bar_string(...) { ... }
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 = "abc";
struct Foo string_data = { s, &string_vtable };
call_bar(string_data);
}
可以看出Interface中的函数的地址保存在vtable中,在调用过程中,必须先根找到对应的vtable才能获取到函数地址。
如果是Rust,会得到如下的C代码:
void bar_int(...) { ... }
void bar_string(...) { ... }
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("abc");
return 1;
}
Rust直接在编译阶段为不同的类型生成了不同的函数版本,然后直接根据类型调用不同的版本即可,不涉及到从vtable获取函数地址。
从调用过程可以直接看出,静态分发模式下,省去了动态查找,也可以做一些更加深层次的优化,导致的结果就是Rust比Golang效率更高,但是可能会导致代码膨胀。
动态分发
Rust同时支持静态分发与动态分发,看一个实际的例子。
trait Animal {
fn speak(&self);
}
struct Dog;
impl Animal for Dog {
fn speak(&self) {
println!("旺旺.....");
}
}
struct Cat;
impl Animal for Cat {
fn speak(&self) {
println!("喵喵.....");
}
}
如果是采用静态分发,那么使用方法如下:
fn animal_speak<T: Animal>(animal: T) {
animal.speak();
}
fn main() {
let dog = Dog;
let cat = Cat;
animal_speak(dog);
animal_speak(cat);
}
实际上相当于为Dog
与Cat
分别实现了animal_speak
方法:
fn dog_speak(dog: dog) {
dog.speak();
}
fn cat_speak(cat: Cat) {
cat.speak();
}
如果是动态分发,那么使用方法如下:
fn animal_speak(animal: &dyn Animal) {
animal.speak();
}
fn main() {
let dog = Dog;
let cat = Cat;
animal_speak(dog);
animal_speak(cat);
}
这里使用了dyn
作为动态分发的标记。
总结
Rust trait同时支持静态分发与动态分发,静态分发不需要通过虚表来寻找实际需要的函数指针,而是直接获取了函数指针,中间少了一步寻址过程。
从性能角度看,动态分发会带来运行时开销,静态分发性能更好,但是可能会造成二进制文件膨胀。
网友评论