漫谈反射

创建于 1/29/2024

各种风格的反射的特性,包括宏和 Generic

许多功能,若想在某类型上实现之,通常只需知道其类型的结构。例如哈希结构相等深复制等。很多时候,转字符串也只为调试,也只需按照结构进行,如 Rust Debug 和 Haskell Show

此类功能,若手写之,千篇一律,有违 DRY 之道,后患无穷。各人不胜其烦,发明了工具以简化写法,其工具以反射为首。

反射,用于获取数据的结构,或者说元信息。其流派众多,风格各异,适应甚广。无论类型系统是动态还是静态,都能用上反射。

动态类型语言的反射无处不在,参考 JavaScript,根本无需专门说反射。用 ES5 写个复制对象的函数看看,遍历属性的过程就是反射。

静态类型语言的反射,才是需要说明的反射。

  1. 最主流的做法,是动态的、无关类型的,例如 Java。无论一个类型结构怎样,其元信息的类型都是一样的。其反射总是在运行时进行,会导致性能下降,以及无法在编译时发现错误;而且无法保留类型,例如无法写出强类型的“返回第一个字段”的方法。

  2. 把动态的变为静态的,便可免除缺点,Zig 如此。其接口差不多,但得益于 Zig 强大的编译期运算能力,得以提高性能,提前发现错误;把类型视为值,则可以保留类型。

  3. 一种神奇的方法是 Haskell 的 Generic。其能力很强,是有关类型的,可以实现强类型的“返回第一个字段”,以及泛型约束之类的。这需要类型系统支持 Type Family,例如 C++ 和 Rust 就行。

  4. 最后,有一种简单粗暴的方法,就是。许多语言都有,Rust 广泛用之。如果你要哈希,那就用个宏,读取类型定义的代码,然后直接生成哈希的代码。需要编译器支持比较强的宏。此法能力最强,极其可定制,善于应对刁钻需求;而且完全静态,性能最高。就是写起宏来比较麻烦。

这四种方法不是孤立的;也不是语言不提供就没有,有的可以自己实现。

在类型系统支持的情况下,通过手动注册,可以实现前三种。如果宏的能力够,用宏可以自动从代码生成元数据。即使宏不支持,也只需手写一遍元数据,然后哈希什么的就能自动实现。

我猜测,在 C++ 中,通过手动注册,应该可以实现和 Zig 类似的效果。

此外,自己实现而非语言内置的反射,有约束问题。内置的反射对任何类型有效,自己实现的反射则需要约束为某个接口之类。例如 Haskell 的 Generic,就是一个 typeclass。控制反射的使用范围,只在实现哈希等接口时使用,便可减少此麻烦。

于此所述大多接口,我并未实际用过,或有错误。姑妄言之,姑妄听之。