Rust如何实现反射?

78

Rust有Any trait,但它也有一个“不为不必要的东西付费”的政策。Rust是如何实现反射的?

我猜测Rust使用了延迟标记技术。每个类型最初都没有分配,但是如果该类型的实例被传递给期望Any trait的函数时,该类型将被分配一个TypeId

或者Rust在其实例可能被传递给该函数的每种类型上放置一个TypeId?我猜前者会比较昂贵。

1个回答

93
首先,Rust没有反射; 反射意味着您可以在运行时获取有关类型的详细信息,例如字段,方法,接口实现等。在Rust中,你不能这样做。最接近的方法是显式实现(或派生)一个提供这些信息的trait。
每种类型在编译时都会分配一个TypeId。由于全局排序ID很难处理,ID是从类型定义以及包含它的crate的各种元数据组合得到的整数。换句话说,它们不是按任何顺序分配的,它们只是定义类型的各个位的哈希。
如果您查看Any trait的源代码, 您将看到Any的唯一实现。
impl<T: 'static + ?Sized > Any for T {
    fn get_type_id(&self) -> TypeId { TypeId::of::<T>() }
}

(这些范围可以“非正式地”缩小为“所有不是从其他地方借来的类型”。)
您还可以找到TypeId的定义:
pub struct TypeId {
    t: u64,
}

impl TypeId {
    pub const fn of<T: ?Sized + 'static>() -> TypeId {
        TypeId {
            t: unsafe { intrinsics::type_id::<T>() },
        }
    }
}

intrinsics::type_id是编译器识别的内部函数,给定一个类型,返回其内部类型ID。该调用只是在编译时被替换为字面整数类型ID;这里没有实际调用。[2]这就是TypeId如何知道一个类型的ID。然后,TypeId只是一个包装器,将此u64隐藏起来,以使用户看不到实现细节。如果您觉得概念上更简单,可以将类型的TypeId视为编译器在编译时就“知道”的常量64位整数。

Anyget_type_id转发到此,这意味着get_type_id实际上只是将特性方法绑定到适当的TypeId::of方法。它只是确保如果您有一个Any,则可以找到原始类型的TypeId

现在,Any 已经为 大多数 类型实现了,但这并不意味着所有这些类型都 实际上拥有 一个在内存中漂浮的 Any 实现。实际发生的是,编译器只有在 有人 编写需要它的代码时才会生成类型的 Any 实现的实际代码 [3]。换句话说,如果您从未使用过给定类型的 Any 实现,则编译器将永远不会生成它。
这就是 Rust 如何实现“不为你不使用的东西付费”的方式:如果您从未将给定类型作为 &AnyBox<Any> 传递,那么相关的代码将永远不会生成,也不会占用编译后二进制文件中的任何空间。
[1]: 令人沮丧的是,这意味着类型的 TypeId 值可能会因编译库的方式不同而发生变化,以至于将其作为依赖项编译(而非独立构建)会导致 TypeId 发生改变。
[2]: 就我所知,如果我错了,我会非常惊讶。
[3]: 这通常适用于 Rust 中的泛型。

12
有趣的是,std::any的文档中使用了"runtime reflection"这个短语,但他们所指的东西可能并不是我们通常所说的"runtime reflection",因为它只能实现类型检查和类型转换,而不能检查任意结构体的内容。 - Ixrec
2
好的。通过反射,我指的是运行时类型信息(RTTI)。 - Xwtek
2
您可以在没有运行时成本的情况下使用TypeId,以进行有限形式的针对每种类型的特殊处理,而无需使用Any - bluss
所以在这种情况下,Rust基本上就像C++一样... 就像dynamic_cast一样,只是RTTI。 - Konrad
2
@Konrad:不,Rust没有任何RTTI,C++有(假设您没有禁用它)。Rust没有等效于dynamic_cast,因为它没有必要的RTTI。 - DK.
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接