Rust特征对象

2025/2/231688 字

引入

在看了《Rust圣经》Box堆对象分配 - Rust语言圣经(Rust Course)这一章后。

想对之前学到的 Box<dyn trait>这种表示特征对象的方式进行深入的探究。

我们知道,特征对象可以有各种类型,他们之间唯一的共同点是实现了同样的方法,因此特征对象是一种 DST

要同时使用一个变量访问这些 DST,就必须创建一个特别的引用,这就是特征对象。

但是你有没有好奇过到底是怎么实现的?

下面我们将会通过

rust
fn print_data_of_t<T>(pt: &T) {
    // 将a地址转换为*const String类型
    let address_of_t: *const T = pt as *const _;
    // 获取a占用的内存大小
    let size_of_t: usize = std::mem::size_of_val(pt);
    // 直接去a的地址看看a到底装的什么
    let bytes: &[u8] = unsafe { std::slice::from_raw_parts(address_of_t as *const u8, size_of_t) };
    // 打印bytes数组,16进制
    for b in bytes {
        print!("{:02x} ", b);
    }
    println!();
}

这个可以直接打印变量在内存中的值的函数对特征对象进行探究。


实验

代码示例

首先我使用的示例代码同样来自于《Rust圣经》。如下

rust
trait Draw {
    fn draw(&self);
}

struct Button {
    id: u32,
}
impl Draw for Button {
    fn draw(&self) {
        println!("这是屏幕上第{}号按钮", self.id)
    }
}

struct Select {
    id: u32,
}

impl Draw for Select {
    fn draw(&self) {
        println!("这个选择框贼难用{}", self.id)
    }
}

有一个特征 Draw和两个实现了这个特征的结构体 Button和 Select


观察现象

首先我们先不使用特征对象,先看一下直接创建出来的结构体的引用是什么有样子的。

rust
fn main() {
    let drawable_one: Box<Button> = Box::new(Button { id: 1 });
    drawable_one.draw();

    print_data_of_t(&drawable_one);
}
plaintext
这是屏幕上第1号按钮
10 0b 6b 83 12 56 00 00 

可以看到可以正常使用结构体的方法。

然后我们也看到了 drawable_one这个引用的值为 10 0b 6b 83 12 56 00 00

这看起来是个指向堆上的地址。经过简单的推理,可以知道这个就是在堆栈上存储的Button对象的地址。

这非常合理。

但是倘若我们使用 Box <dyn Draw>特征对象来指向 Button对象呢?

rust
fn main() {
    let drawable_one: Box<dyn Draw> = Box::new(Button { id: 1 });
    drawable_one.draw();

    print_data_of_t(&drawable_one);
}
plaintext
这是屏幕上第1号按钮
10 fb 17 01 23 56 00 00 68 45 40 f4 22 56 00 00 

同样可以进行方法调用。

但是发现,此时 drawable_one这个引用这次在内存中占用了16个字节,并且似乎是两个指向堆上数据的地址。

我们知道一个地址是指向 Button对象的,那么另一个呢?

我们先来推理一下,一个特征对象可能是好几种不同的类型,因此它们各自实现的特征方法可能在结构体中的不同位置,那么怎样才能通过同样的引用调用他们都实现了的特征方法呢?

答案已经呼之欲出了,就是在引用中添加方法的地址。


揭秘

那真的是这样吗,我们验证一下。

//由于接下来的操作涉及到直接操作指针和内存,因此直接用 unsafe包裹

首先打印 Button中 draw方法的地址,再访问 drawable_one的内容

rust
fn main() {
    unsafe {
        // 获取Button的draw方法的地址
        let p_draw_fn: *const u64 = Button::draw as *const u64;
        println!("Button::draw的地址是:{:p}", p_draw_fn);

        // 创建一个Draw特征对象
        let drawable_one: Box<dyn Draw> = Box::new(Button { id: 2 });

        // 看一下drawable_one里面有什么
        print!("drawable_one的内容是:");
        print_data_of_t(&drawable_one);
    }
}
plaintext
Button::draw的地址是:0x557852f8f430
drawable_one的内容是:10 ab 84 6f 78 55 00 00 b8 11 fd 52 78 55 00 00 

发现并不一样,这是怎么回事??

查阅文档后发现,原来特征对象的另一个地址并不直接是特征方法的地址,而是一个叫做 vtable的东西的地址

那么这个 vtable里面会有我们想要的方法的地址吗?

看一看就知道了,直接操作指针获取 vtable的地址

rust
    // 获取drawable_one的地址指针
    let p_drawable_one: *const u64 = std::mem::transmute::<_, *const u64>(&drawable_one);
    println!("drawable_one的地址是:{:p}", p_drawable_one);

    // 偏移地址指针
    let p_offset: *const u64 = p_drawable_one.add(1);
    println!("p_drawable_one偏移1个地址指向:0x{:x}", *p_offset as u64);

    // 获取vtable地址,转换为指针
    let p_vtable = *p_offset as *const u64;
    println!("vtable地址:{:p}", p_vtable);

然后就可以看里面有什么了

rust
    // 看一下vtable里面有什么
    for i in 0..4 {
        let p_fn = p_vtable.add(i);
        println!("vtable[{}]:0x{:x}", i, *p_fn as u64)
    }

最终结果如下

plaintext
Button::draw的地址是:0x557852f8f430
drawable_one的内容是:10 ab 84 6f 78 55 00 00 b8 11 fd 52 78 55 00 00 
drawable_one的地址是:0x7ffc82227d98
p_drawable_one偏移1个地址指向:0x557852fd11b8
vtable地址:0x557852fd11b8
vtable[0]:0x0
vtable[1]:0x4
vtable[2]:0x4
vtable[3]:0x557852f8f430

神奇的发现,我们终于在 vatable[3]处发现了我们想要找的特征方法 Button:draw的地址

大功告成,总算亲自体验了一下特征对象是如何实现的。


完整代码

rust
trait Draw {
    fn draw(&self);
}

struct Button {
    id: u32,
}
impl Draw for Button {
    fn draw(&self) {
        println!("这是屏幕上第{}号按钮", self.id)
    }
}

struct Select {
    id: u32,
}

impl Draw for Select {
    fn draw(&self) {
        println!("这个选择框贼难用{}", self.id)
    }
}


fn main() {
    unsafe {
        // 获取Button的draw方法的地址
        let p_draw_fn: *const u64 = Button::draw as *const u64;
        println!("Button::draw的地址是:{:p}", p_draw_fn);

        // 创建一个Draw特征对象
        let drawable_one: Box<dyn Draw> = Box::new(Button { id: 2 });

        // 看一下drawable_one里面有什么
        print!("drawable_one的内容是:");
        print_data_of_t(&drawable_one);

        // 获取drawable_one的地址指针
        let p_drawable_one: *const u64 = std::mem::transmute::<_, *const u64>(&drawable_one);
        println!("drawable_one的地址是:{:p}", p_drawable_one);

        // 偏移地址指针
        let p_offset: *const u64 = p_drawable_one.add(1);
        println!("p_drawable_one偏移1个地址指向:0x{:x}", *p_offset as u64);

        // 获取vtable地址,转换为指针
        let p_vtable = *p_offset as *const u64;
        println!("vtable地址:{:p}", p_vtable);

        // 看一下vtable里面有什么
        for i in 0..4 {
            let p_fn = p_vtable.add(i);
            println!("vtable[{}]:0x{:x}", i, *p_fn as u64)
        }
    }
}

fn print_data_of_t<T>(pt: &T) {
    // 将a地址转换为*const String类型
    let address_of_t: *const T = pt as *const _;
    // 获取a占用的内存大小
    let size_of_t: usize = std::mem::size_of_val(pt);
    // 直接去a的地址看看a到底装的什么
    let bytes: &[u8] = unsafe { std::slice::from_raw_parts(address_of_t as *const u8, size_of_t) };
    // 打印bytes数组,16进制
    for b in bytes {
        print!("{:02x} ", b);
    }
    println!();
}

💬 评论区

写评论

暂无评论,来发表第一条吧。

© 2026 Corn 的自留地