觀感
Rust的Trait和Golang的interface看起來非常相似,從開發(fā)者角度來看掏缎,都可以實(shí)現(xiàn)具體類型的抽象化经磅。
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與具體的結(jié)構(gòu)體之間是自動關(guān)聯(lián)的药蜻,不像Rust需要顯式的用一個(gè)impl
來關(guān)聯(lián)瓷式。
此外,回顧下前文范型相關(guān)的內(nèi)容看语泽,Rust可以為非確定類型實(shí)現(xiàn)trait,但是Golang僅能對確定的struct實(shí)現(xiàn)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())
}
}
在這個(gè)例子中踱卵,為T
類型實(shí)現(xiàn)了MetroCodeCheck
,而T
是一個(gè)范型据过,可能對應(yīng)于其他的已定義的類型惋砂,并不與一個(gè)確切的類型綁定。
靜態(tài)分發(fā)
下面我們通過模擬編譯器的行為來分別分析靜態(tài)分發(fā)绳锅。對于Golang而言西饵,僅允許動態(tài)分發(fā)精肃,每一個(gè)Interface中的方法地址是從值中動態(tài)加載然后調(diào)用的找蜜,所以只有在運(yùn)行期間才能知道具體的函數(shù)。
考慮一個(gè)例子:
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的原理公你,忽略掉一些必要的細(xì)節(jié)后原朝,會得到類似的代碼:
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中的函數(shù)的地址保存在vtable中驯嘱,在調(diào)用過程中,必須先根找到對應(yīng)的vtable才能獲取到函數(shù)地址喳坠。
如果是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直接在編譯階段為不同的類型生成了不同的函數(shù)版本,然后直接根據(jù)類型調(diào)用不同的版本即可壕鹉,不涉及到從vtable獲取函數(shù)地址剃幌。
從調(diào)用過程可以直接看出聋涨,靜態(tài)分發(fā)模式下,省去了動態(tài)查找负乡,也可以做一些更加深層次的優(yōu)化牍白,導(dǎo)致的結(jié)果就是Rust比Golang效率更高,但是可能會導(dǎo)致代碼膨脹敬鬓。
動態(tài)分發(fā)
Rust同時(shí)支持靜態(tài)分發(fā)與動態(tài)分發(fā)淹朋,看一個(gè)實(shí)際的例子。
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!("喵喵.....");
}
}
如果是采用靜態(tài)分發(fā)钉答,那么使用方法如下:
fn animal_speak<T: Animal>(animal: T) {
animal.speak();
}
fn main() {
let dog = Dog;
let cat = Cat;
animal_speak(dog);
animal_speak(cat);
}
實(shí)際上相當(dāng)于為Dog
與Cat
分別實(shí)現(xiàn)了animal_speak
方法:
fn dog_speak(dog: dog) {
dog.speak();
}
fn cat_speak(cat: Cat) {
cat.speak();
}
如果是動態(tài)分發(fā)础芍,那么使用方法如下:
fn animal_speak(animal: &dyn Animal) {
animal.speak();
}
fn main() {
let dog = Dog;
let cat = Cat;
animal_speak(dog);
animal_speak(cat);
}
這里使用了dyn
作為動態(tài)分發(fā)的標(biāo)記。
總結(jié)
Rust trait同時(shí)支持靜態(tài)分發(fā)與動態(tài)分發(fā)数尿,靜態(tài)分發(fā)不需要通過虛表來尋找實(shí)際需要的函數(shù)指針仑性,而是直接獲取了函數(shù)指針,中間少了一步尋址過程右蹦。
從性能角度看诊杆,動態(tài)分發(fā)會帶來運(yùn)行時(shí)開銷,靜態(tài)分發(fā)性能更好何陆,但是可能會造成二進(jìn)制文件膨脹晨汹。