> 12. 并且也别使用 RAII 在编译时给类结构附赠的元数据
13. 应该说是 RTTI ,然而即便 RTTI 也只是标准化了 boost 那样的库早已实现的部分中极少量的编译时生成的类型信息,如`typeid(some_class_ref)`所获得的`std::type_info`引用中有用的信息就一个 name (所以 c#直接把这简化成了[`nameof()`](
https://stackoverflow.com/questions/31695900/what-is-the-purpose-of-nameof)),您连想要运行时获知这个 some_class_ref (即便这个 some_class_ref 的类型已经被限定编译时已知了,也就是不是 c 人最爱的迫真泛型之`void*`)里头有多少成员,分别叫什么都不可能靠`type_info`做到,而是需要使用 boost 人最爱的宏-模板实现元编程魔法之
https://stackoverflow.com/questions/41453/how-can-i-add-reflection-to-a-c-application而 RTTI 的`dynamic_cast<>`则用于标准化其他库中对类实例继承层级进行 downcast 的操作,但这在 java 人眼中就是一个普通的`(sub)base`
所以
https://en.wikipedia.org/wiki/Run-time_type_information 声称`运行时类型信息是一个更通用的概念的特化,称为类型自省`,而
https://en.wikipedia.org/wiki/Type_introspection 又进一步区分出:
> 内省不应与反射相混淆,后者更进一步,是程序在运行时操作对象的值、元数据、属性和函数的能力
一个简单的比喻就是 introspection 是`r--`的,而 reflection 是`rwx`的
---
14. 实际上如果完全没有反射可用(哪怕只是能获知某个已知类型里有哪些成员,所以我认为`std::type_info`绝非反射也不是完整的 type introspection ),那么实现任何程度的(un)serialize (将任何程序外部数据结构不论是人类可读的字符串表达( json/xml/yaml/http/1.1 )还是朴素的`byte[]`( protobuf/http/2/3/mysql 等任何数据库的二进制通信)转换为程序内部的,反之亦然)都必须得绕到元编程 approach 上,因此结果就是您要么手写这样的大段模板+四叶信安底层壬上壬上海贵族 FSF EFF 精神会员杨博文阁下 @
yangbowen 最爱的 constexpr 所实现的元编程:
https://stackoverflow.com/questions/17549906/c-json-serialization 来处理 json ,要么使用基于`std::vector/map`(也就是`array/dict`)的在某野榜
https://github.com/miloyip/nativejson-benchmark 中名列前茅的某库
https://github.com/nlohmann/json 来直接把所有解析出来的 jsonelement 塞进集合数据结构中然后疯狂重载运算符来尽可能使您交互这个数据结构的语法看起来像是在操作类 /struct (但很明显还是得通过满屏幕的`["字符串"]`来访问字段,因此这个库类似于我去年 c#重写 tbm 爬虫时询问`奥利金德数理逻辑带手子当代图灵可计算性理论中级高手 dc 神` @
dylech30th 时他所指出使用.net6 中引入的 JsonNode 从语法上看就像是 php 人操作 array 来缓存 json:
https://kevsoft.net/2021/12/29/manipulate-json-with-system-text-json-nodes.html 但这并不像 js 人那样直接把 object 当 map 用,而是反过来拿动态结构的 map 当静态结构的 object 使用,实际上.net4 时为了 IronPython/Ruby (以及现在的 peachpie )所依赖的 DLR 而引入的 dynamic 类型也是如此实现的,可谓是极限一换一)
如果只从运行时(不考虑编译时耗时)性能而不是用户要写多少样板代码的角度来看,有`基于编译时 codegen 的元编程>运行时对着类反射>运行时缓存在 dict 里`,这背后的原因是显然的:编译时 codegen 是典型的空间换时间+编译时时间换运行时时间以实现运行时只需要像 9.中提到的`member selector`那样对着编译时已知的类成员读写,而运行时反射是拖延到了运行时来获知有哪些类成员并读写,存在 dict 里则是朴素的实现:拿 map 当 object
所以也就有了允许用户自己通过一个 attribute 就能 optin 的把基于运行时反射的 jsonserializer 换成基于编译时 source generator 的:
https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/source-generation-modes进一步的还可以把运行时构造正则表达式的 NFA/DFA 这个较耗时的过程也给提前到编译时 codegen
https://devblogs.microsoft.com/dotnet/regular-expression-improvements-in-dotnet-7/#source-generation而 cpp 人杨博文阁下则有着高度自由的 constexpr 可以实现基于编译时 codegen 的元编程
15. 根据`8. 阁下不应该只将 attribute 视作运行时反射的唯一用途`,可以合理假设阁下似乎搞混了完完全全两码事的两个抽象概念之 attribute 与 reflection ,attribute 本质上就是给代码(用 PLT 的话说是给 AST 上的某些 node )添加一些用户(或 codegen 生成,没有人阻止阁下通过 source generator 来给程序集中的所有类的所有成员都加个`[A114514Attribute]`)定义的元数据,但 attribute 本身并不规定您(用户或 codegen )要用什么方式来读取 attribute ,而最常见的(某些语言中甚至是唯一)的读取方式就是使用 reflection 提供的 api (因为 reflection 本身就可以在运行时获取类型元数据,这其中自然可以包括 attribute ),所以阁下才会将 attribute 视作是 reflection 的一部分,然而实际上在编译时 codegen 实现的元编程( lombok/source generator )中,您写的元编程代码的确是在使用 reflection api 获得 attribute 并根据其生成一些代码,但`您写的元编程代码`实际上早在编译时而非运行时就已经被执行了,因此运行时根本没有什么 lombok/source generator 给您在程序启动后现场制作这些代码,除非是 18.中提及的运行时 codegen
16. 然而即便没有 attribute 也仍然无法阻止往 AST node 添加元数据的自由行径,典型例子就是 naming convention as metadata ,如在没有 member visibility 约束的语言(如 es2022 之前的 js
https://v8.dev/features/class-fields )中人们会约定以`_`开头的函数暗示其是私有的所以外部不应该使用,又比如 java 人最爱复制粘贴的 g/setter 样板代码中 g/set 开头的类方法暗示了其是对这个类的某种属性的抽象 accessor ,也就是
https://en.wikipedia.org/wiki/Mutator_method17. 并且在有 reflection 时阁下可以进一步的基于命名约定对符合特定模式的类成员进行批量操作,而这并不需要他们具有 attribute 所带来的元数据(假如这个语言(如 js 与 php7.x )没有 attribute ,或者说命名约定本身就是一种元数据),例如 laravel 从 ror 那抄来的用于在 orm 中允许用户复用他重复使用的 query builder 们(为了 DRY )而引入的 scope:
https://laravel.com/docs/9.x/eloquent#local-scopes https://guides.rubyonrails.org/active_record_querying.html#scopes ,其本质不过就是直接去 model 类中寻找以 scope 开头的同名方法然后传参返回其放回值而已,再比如更常见的各种(un)serializer 库通常会允许您自定义不同命名规范之间的转换规则,假如您在 c#中想要解析一些使用 snake_case 字段名的 json ,而众所周知 c#对 public 类成员是 CamelCase ,所以直接(un)serialize 永远不会得到您想要的类 /json ,那您就需要指定一个 CamelCase<->snake_case 的命名转换器来让(un)serializer 库能够改变其通过反射或 codegen 来在内部查找类成员 /json 字段时所依据的字符串名字,回顾经典之隔壁
https://www.v2ex.com/t/910246#r_12602250 @
Rocketer 所遇到的:
> 它每遇到一个大写字母,就会转成下划线加小写的形式,比如我写 imageURL ,它实际操作的是 html 里 image_u_r_l 这个属性,所以要求我们必须用 imageUrl 这样的命名,才能操作到预期的 image_url 这个属性
18. 在 13.中我所做的奇妙深刻比喻之`reflection 是 rwx`中的 x 具体是指运行时继续进行元编程 codegen 的罪恶行径并允许执行生成的代码(在 c#中这叫 emit
https://stackoverflow.com/questions/2312623/real-world-uses-of-reflection-emit 其主要用于我以前跟阁下提到的 linq2sql (
http://www.albahari.com/nutshell/linqkit.aspx ) IQueryable 所依赖的 expression tree 的`.compile()`(运行时通过代码来构造一个 AST ,然后一键 compile 其内部就通过 emit 给您在 CLR 中凭空生成了一个指向 lambda 类结构的委托供您使用
https://www.tutorialsteacher.com/linq/expression-tree ),在 java/jvm 语言中叫 asm hack ,玩 mcmod 的 dddd ,这也意味着在 clr/jvm 中您也可以做动态语言人最爱的 eval is evil ,例如将用户输入字符串作为 linq2sql 的 expression tree:
https://dynamic-linq.net/expression-language ,或是 json 中的字符串作为代码来执行:
https://github.com/microsoft/RulesEngine ),而本帖中的其他元编程基本都是在编译时就进行了的
18. TL;DR:
attribute 是向代码中( AST node 上)添加元数据程度的能力
reflection 是编译 /运行时自省( reflect )程序集( assembly )自身程度的能力(这句话我前几年就多次重复过)
type introspection 是类似于 reflection 程度的能力,但自由度 /表达力上是后者的子集(基本上就只能对类型继承树做 down/upcasting )
metaprogramming 是用代码操作(读写执行等)代码(例如 lisp 中一切皆 s-expr 使得代码和数据混为了一谈)程度的能力,这也意味着一个静态分析器甚至阁下最爱写的读阁下亲自设计的 DSL 代码然后走 lexer parser 管道出 AST 的程序也是元编程
codegen 是用代码写代码(增殖)程度的能力,请注意许多泰国第几的自媒体都武断地将元编程描述为用代码写代码,然而 codegen 实际上只是元编程的一个子集(如同 type introspection 是 reflection 的子集)
编译时 codegen 是在 AOT 编译时完成的 codegen 任务然后把生成代码也缝合进构建结果中程度的能力
运行时 codegen 是在 JIT (这的 JIT 是指在运行时而不是 JIT 优化器)运行时再次解释 /编译代码(既可以是一些字符串作为代码,也可以是 AST )然后把生成代码缝合进当前运行时符号表中程度的能力
eval 是对调用 /执行运行时 codegen 的解释 /编译结果(例如运行时 codegen 导致一个新的类声明被凭空产生,然后就可以 eval 地 new 他以获得类实例,而这在编译时是完全不可能预知的)程度的能力