• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

不要用 boxed trait objects

武飞扬头像
有关心情
帮助1

起步

此文基本算是 《Don’t use boxed trait objects》 的中译,但又不全是 《Don’t use boxed trait objects》 的中译。

什么是 boxed trait 对象 ?

通常来说,rust 中的 trait 类似于 go 里的 interface —— 一个存放 n(n>=0 ) 个方法的集合,而 go 借助 interface 这一概念,很容易实现“多态”的效果。

看一个 golang 版本的“小鸟飞”示例:

package main

import (
	"flag"
	"fmt"
)

type Bird interface {
	fly()
}

// Woodpecker 啄木鸟
type Woodpecker struct {}

func (Woodpecker) fly()  {
	fmt.Println("啄木鸟 在飞...")
}


// Cuckoo 杜鹃鸟
type Cuckoo struct {}

func (Cuckoo) fly()  {
	fmt.Println("杜鹃鸟 在飞...")
}

func GetOneKindBirdByName(name string) Bird {  // 这里返回的是 Bird interface
	switch name {
	case "woodpecker":
		return Woodpecker{}
	case "cuckoo":
		return Cuckoo{}
	default:
		panic("IncompleteError") // 尚未实现
	}
}

var name = flag.String("name", "woodpecker/cuckoo", "输入鸟的名字")

func main() {
	flag.Parse()
	bird := GetOneKindBirdByName(*name)
	bird.fly()
}
学新通

运行效果如下:

PS D:\code\go-test> .\go-test.exe -h
Usage of D:\code\go-test\go-test.exe:
  -name string
        输入鸟的名字 (default "woodpecker/cuckoo")
PS D:\code\go-test> .\go-test.exe -name=woodpecker
啄木鸟 在飞...  # 此时 bird 的具体类型是 Woodpecker
PS D:\code\go-test> .\go-test.exe -name=cuckoo
杜鹃鸟 在飞...  # 此时 bird 的具体类型是 Cuckoo
PS D:\code\go-test>

也就是说,不到执行完 GetOneKindBirdByName 函数,我们总是不知道 main 函数中变量 bird 的具体类型。


既然 trait ≈ interface,是不是也可以照葫芦画瓢一个 rust 版的“小鸟飞”呢?像下面这样:

use clap::{App, Arg};

trait Bird {
    fn fly(&self);
}

struct Woodpecker {}

impl Bird for Woodpecker {
    fn fly(&self) {
        println!("啄木鸟 在飞...")
    }
}

struct Cuckoo {}

impl Bird for Cuckoo {
    fn fly(&self) {
        println!("杜鹃鸟 在飞...")
    }
}

// !!!不可正常运行的代码
fn get_one_kind_bird_by_name(name: String) -> Bird {
    match name.as_str() {
        "cuckoo" => Cuckoo {},
        "woodpecker" => Woodpecker {},
        _ => panic!("IncompleteError"),
    }
}

fn main() {
    // 解析命令行参数
    let opts = App::new("choose one kind bird")
        .arg(
            Arg::with_name("name")
                .short("name")
                .long("name")
                .value_name("woodpecker/cuckoo")
                .help("输入鸟的名字")
                .takes_value(true),
        )
        .get_matches();
    
    let name = opts.value_of("name").unwrap().into();
    let bird = get_one_kind_bird_by_name(name);
    bird.fly();
}
学新通

上述的写法跟 go 版的如出一辙,看起来很合理的样子。可编译会报错,报错信息里反复强调 doesn't have a size known at compile-time

49 | fn get_one_kind_bird_by_name(name: String) -> Bird {
   |                                               ^^^^ doesn't have a size known at compile-time
   ...
70 |     let bird = get_one_kind_bird_by_name(name);
   |         ^^^^ doesn't have a size known at compile-time
   ...
70 |     let bird = get_one_kind_bird_by_name(name);
   |                ^^^^^^^^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time

这是 rust 的特点,它无法忍受栈上面有个不知道大小的变量 —— 其他语言也不能忍受,只是 rust 不会帮你做隐式处理。我们只需要把未知大小的变量放到堆上,再用栈上的已知大小的指针指过去就可以了。有请 Box。

// 可正常运行的代码
fn get_one_kind_bird_by_name(name: String) -> Box<dyn Bird> {
    match name.as_str() {
        "cuckoo" => Box::new(Cuckoo {}),
        "woodpecker" => Box::new(Woodpecker {}),
        _ => panic!("IncompleteError"),
    }
}

运行效果如下:

$ ./rust-code -h
choose one kind bird 

USAGE:
    rust-code [OPTIONS]

FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information

OPTIONS:
    -n, --name <woodpecker/cuckoo>    输入鸟的名字
$ ./rust-code --name=woodpecker
啄木鸟 在飞...
$ ./rust-code --name=cuckoo
杜鹃鸟 在飞...
学新通

简单总结一下。当需要用接口变量接收一个实例时,go 只需要声明这个变量是什么接口类型即可:

// go
var bird Bird = Woodpecker{}

而 rust 不但需要明确是什么接口(trait),还需要明确这个实例要在堆上:

// rust
let bird: Box<dyn Bird> = Box::new(Woodpecker{});

这就是 boxed trait 对象,你大可将其类比为 go 中的接口实例。

Box<dyn Trait> 有什么问题吗?

如果你对 go 的接口有一定了解你就知道,interface 的存在还有一个意义:隔离,“屏蔽”掉这个实例与接口无关的那部分内容。无可厚非,这样做好像没错。可如果我们需要原来的具体类型呢?这好办,用类型断言

// Woodpecker 啄木鸟
type Woodpecker struct {
	name string
}

func main() {
	var bird Bird = Woodpecker{name: "啄木鸟"}
	if woodpecker, ok := bird.(Woodpecker); ok {
		fmt.Println(woodpecker.name)
	}
	// 你不能 fmt.Println(bird.name)
}

rust 不支持类型断言,但可以用 downcast 还原类型。这需要在 trait 中添加 fn as_any(&self) -> &dyn Any

use std::any::Any;

trait Bird {
    fn fly(&self);
    fn as_any(&self) -> &dyn Any;
}

struct Woodpecker {
    name: String,
}

impl Bird for Woodpecker {
    fn fly(&self) {
        println!("啄木鸟 在飞...")
    }
    // 实现 as_any 方法
    fn as_any(&self) -> &dyn Any {
        self
    }
}

fn main() {
    let bird: Box<dyn Bird> = Box::new(Woodpecker{name: "啄木鸟".into()});
    // 还原类型
    let woodpecker = bird.as_any().downcast_ref::<Woodpecker>().unwrap();
    println!("{}", woodpecker.name);
    // 你不能 println!("{}", bird.name)
}
学新通

你会觉得莫名其妙吗?Bird trait 与 as_any 之间本来毫无关系,我们对鸟的定义是通过能不能“飞(fly)”判断的,as_any 插足进来破坏了 Bird trait 在设计上的纯粹。

倒也不是不能跑……但我们还可以寻求其他方法。

结构体泛型

我们试试 “wrapper 结构体泛型” 这种解决方案。

struct Woodpecker {
    name: String,
}

struct Cuckoo {
    nick: String,
}

// ...  省略掉接口的定义与实现

struct BirdWrapper<B: Bird> {
    bird: B
}

fn main() {
    let woodpecker = BirdWrapper{bird: Woodpecker{name: "啄木鸟".into()}};
    woodpecker.bird.fly();
    println!("{}", woodpecker.bird.name);  // 调用 name
    
    let cuckoo = BirdWrapper{bird: Cuckoo{nick: "杜鹃鸟".into()}};
    cuckoo.bird.fly();
    println!("{}", cuckoo.bird.nick);      // 调用 nick
}
学新通

既能达到“多态”效果,又确保了原类型(Woodpecker / Cuckoo)不会丢失,仿佛很不错哦!从易用性的角度,还可以为 BirdWrapper 实现 Bird trait。

impl<B: Bird> Bird for BirdWrapper<B> {
    fn fly(&self) {
        self.bird.fly();
    }
}

fn main() {
    let woodpecker = BirdWrapper{bird: Woodpecker{name: "啄木鸟".into()}};
    woodpecker.fly();
    
    let cuckoo = BirdWrapper{bird: Cuckoo{nick: "杜鹃鸟".into()}};
    cuckoo.fly();
}

所有实现了 Bird trait 的 struct 都可以被 BirdWrapper 包装,如果你不想这样 —— 如果你要的是“只允许 Woodpecker、Cuckoo 被包装”,可以用 rust enum 限制:

// ... 省略 Woodpecker,Cuckoo 定义与接口实现

enum BirdWrapper {
    Woodpecker(Woodpecker),
    Cuckoo(Cuckoo),
}

impl Bird for BirdWrapper {
    fn fly(&self) {
        match self {
            BirdWrapper::Woodpecker(woodpecker) => woodpecker.fly(),
            BirdWrapper::Cuckoo(cuckoo) => cuckoo.fly(),
        }
    }
}

fn get_one_kind_bird_by_name(name: String) -> BirdWrapper {
    match name.as_str() {
        "cuckoo" => BirdWrapper::Cuckoo(Cuckoo{nick: "啄木鸟".into()}),
        "woodpecker" => BirdWrapper::Woodpecker(Woodpecker{name: "啄木鸟".into()}),
        _ => panic!("IncompleteError"),
    }
}

fn main() {
    let bird = get_one_kind_bird_by_name("cuckoo".into());
    bird.fly();
}
学新通

总结

尽管本文的题目叫 “不要用 boxed trait objects”,但本文并不想表达这种观点。只是内容大量参考了 《Don’t use boxed trait objects》,延用标题是致敬原作者的一种表现。

所以我没有在这里强调 boxed trait 运行时会带来一定的开销,以及 “wrapper 结构体泛型” 可以在编译时计算大小的优势。等等之类,这些都不重要。本文的存在仅出于一个目的:就是告诉 go --转–> rust 者 —— 或出于喜爱也好,或出于好奇也好 —— 在 “泛型” 的世界里,我们还有其他选择。

那就期待 go 1.18 吧!

感谢

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgbicbg
系列文章
更多 icon
同类精品
更多 icon
继续加载