如果语言能够提供足够的表达性,是否可以不需要反射和 AOP 的概念?

2023-01-24 05:04:07 +08:00
 netabare

在用 Kotlin 写 API 的时候突然想到的。

一般来说,像是 @before@after 这种,用扩展函数就可以很方便的实现了。更复杂一点的概念,如果把 Kotlin 里面的 receiver 、scope 函数、 @this 之类的雕花技弄熟练然后配合良好的 OOP 设计感觉其实也可以实现差不多的效果吧。(草,不加代码块直接变成 at 人了(捂脸

我不熟悉 Scala ,不过如果本着「 Kotlin 能做到的 Scala 都能做到」的原则,那应该 Scala 也不是问题(雾

感觉这样写起来也许比起 AOP 的写法会啰嗦一点,但是代码层面上是很透明的,类型安全也可以得到保证,可读性和可维护性应该都是优于反射和 AOP 满天飞的 Java 代码吧。

并不是很喜欢 lombok 那套,感觉预处理的写法让代码变得不透明了。AOP 里面拿字符串写跳转更是让人头皮发麻。

感觉这也是不喜欢 Java 的一个很重要的因素……很多本来可以让语言来提供的功能都一概欠缺,然后实际工程里发现写不下去就不得不上一些非常 dirty hack 的写法,如果团队的水平不高直接变成灯谜大会。

3319 次点击
所在节点    奇思妙想
17 条回复
h0099
2023-01-24 05:47:23 +08:00
1. `@before` 和 `@after` 是指 JUnit 中的一个 attribute? https://stackoverflow.com/questions/20295578/difference-between-before-beforeclass-beforeeach-and-beforeall 结合阁下最近的回复 https://www.v2ex.com/t/910378#reply8 ,我暂且蒙古

> Kotlin 能做到的 Scala 都能做到

2. 经典类型系统的表达力决定了整个语言能做到什么

> lombok 那套,感觉预处理的写法让代码变得不透明了

3. 不用 lombok 难道您很喜欢手动复制粘贴满屏幕的 g/setter 方法和字段访问吗(而很明显 java 没有 c#的 auto prop 语法糖)?要是复制粘贴过程中扣错字段名了呢?
4. 如果阁下不喜欢静态语言中用于实现 ruby 那样的元编程的 source generator 轮子那我建议您也少碰 c/cpp 的宏和`php: hypertext preprocessor`和 css 预处理器( sass scss less ),因为他们的目的都是像 c#人滥用语法糖那样让代码变得不透明(然而语法糖远没有元编程自由,所以不透明的程度不如后者)
5. 与此同时截止 2023 年 1 月,奥利金德数理理论学家 dc 神的旗舰开源项目免 fq 上 p 站的 pixeval 第三方 c#客户端中仍在使用 source generator 来自动复制粘贴 i18n 文本: https://github.com/Pixeval/Pixeval/commit/ee13443205f8ed68dcc6dce87687f4cb341dde27 https://github.com/Pixeval/Pixeval/pull/278 在我看来这同样的`灯谜大会`
6. 但我在 https://github.com/n0099/TiebaMonitor/commit/8874e423b5345e66f81fc59e1ffe83f64a7d6d89 之后也希望能用 source generator 来自动生成我来回复制粘贴了十分钟的这些类型不安全(如果符号名不同也没有错误,也就是 3.中的`复制粘贴过程中扣错字段名`)的狗屎样板

7. 请问如何在这 https://github.com/n0099/TiebaMonitor/commit/4f098c8ef7f2fdd089a43ca99746a666c4bd10fc 之中避免使用 attribute 同时又不需要在每次使用 jsonserializer.serialize 时手动判断参数类型并决定是否传入这个 converter? https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/converters-how-to

8. 阁下不应该只将 attribute 视作运行时反射的唯一用途,尽管这是其常见用途,对于刚从动态语言转静态语言的人(如我)而言滥用反射主要是为了实现动态语言中常用的表达(所以我以前说动态语言天天反射)

9. 而在 https://github.com/n0099/TiebaMonitor/commit/34f9c32dd346a22878ea8dcf2ac82fe46169c8bc 中我将类型确定的反射都改成了朴素的所谓`member selector`(也就是通过函数参数将 project 出某个类成员的任务委托给 caller 而不是硬编码在内部)

10. 但在 https://github.com/n0099/TiebaMonitor/blob/c414ca3429ceb1cd4a7607c10fb79cb608b7cd2d/crawler/src/Tieba/Crawl/Saver/CommonInSavers.cs#L79 中我并没有将这的反射给彻底削除,因为 https://github.com/n0099/TiebaMonitor/blob/c414ca3429ceb1cd4a7607c10fb79cb608b7cd2d/crawler/src/Tieba/Crawl/Saver/StaticCommonInSavers.cs#L5 的目的只是把类 https://github.com/n0099/TiebaMonitor/blob/v2/crawler/src/Db/Post/ThreadPost.cs 和类 https://github.com/n0099/TiebaMonitor/blob/v2/crawler/src/Db/Revision/ThreadRevision.cs 之间名字相同的字段给合并赋值一遍(也就是 js 人最爱的`Object.assign({}, {})`),我当然可以用同样的手法来把这个反射 setvalue 给换成调用 revivison 类中的某种 merge 方法并在其中逐一复制所有已知的硬编码的类实例中同名的 prop 值,就像是阁下最痛恨的对`两个结构相似的类`却要写高度相似的重复代码来处理不同的类型: https://github.com/n0099/TiebaMonitor/blob/c414ca3429ceb1cd4a7607c10fb79cb608b7cd2d/crawler/src/Tieba/Crawl/Parser/ThreadParser.cs#L17 ,也就是我此前于 https://sora.ink/archives/1574?replytocom=800#respond 中所说的:
> @dylech30th ts 那种 ducktype 也算传统 oo 的严格 subtype?
> 就像四叶 CS 硕士 PLT 中级高手 irol 阁下此前锐评 java 魔怔人为了让两个结构十分相似的类能够兼容而写出一个逐类 prop 去复制粘贴的 converter (常见于 bean 中,而我也被迫在 c#中写出了这样的恶俗玩意: https://github.com/n0099/TiebaMonitor/blob/e84a230fa0eb1c1095f6b6aa74b34a29f1f6a69d/crawler/src/Tieba/Crawl/Parser/ThreadParser.cs#L45 )的刷代码函数和运行时开销的罪恶行径,而在 ducktyping 中只要结构相似那他们就是互相兼容同一个类型(如果不考虑逆变协变不变)

11. 然而我也只多举了一个反射的常见用例,事实上由于 8.中的`动态语言天天反射`使得您总能找到新的奇妙深刻反射(远比您所见的基于字符串的 DI/IoCcontainer/AOP 谔谔)

12. 如果阁下想完全不用反射建议去写 AOT 编译的 c/cpp/rust ,并且也别使用 RAII 在编译时给类结构附赠的元数据
agagega
2023-01-24 09:48:14 +08:00
在需要依赖用户输入的情况才需要反射吧(当然也可以用设计模式规避),如果只是获取源码中的静态信息,理论上都可以用元编程代替。元编程场景很多,比如怎么获取某个类的所有字段定义。
ql562482472
2023-01-24 10:01:45 +08:00
lombok 被很多人不接受的原因主要是实现太 trick 了 不直观,反直觉
TWorldIsNButThis
2023-01-24 11:17:44 +08:00
roman (kotlin leader)在 medium 上的确有一篇博文讲 funciton vs aop 的

https://elizarov.medium.com/aop-vs-functions-2dc66ae3c260
h0099
2023-01-24 11:21:10 +08:00
如果某人(切勿对号入座)觉得 lombok 特色基于 attribute 的元编程以避免 g/setter 样板代码:
```java
@Getter @Setter private int age = 10;
```
足够的反直觉
那么他大概率也会将 c#的 auto prop 语法糖视作类似的谜语:
```csharp
public int Age { get; set; } = 10;
```
oldshensheep
2023-01-24 12:14:18 +08:00
面向字符串编程,可以。
nebkad
2023-01-24 15:48:49 +08:00
反射和 AOP 都是在增强静态类型编程语言能力。在不牺牲静态类型安全性的前提下,向动态类型语言靠拢的一种能力。
只要需求足够灵活多变,那么它们就是刚需。
h0099
2023-01-24 15:54:15 +08:00
> 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_method

17. 并且在有 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 他以获得类实例,而这在编译时是完全不可能预知的)程度的能力
h0099
2023-01-24 15:55:04 +08:00
typo:最后一个 18.应该是 19
h0099
2023-01-29 02:07:04 +08:00
@netabare 请帮我在 AD 里删掉我的 2/MFA 并禁用执行某些操作时要求账户已开启 MFA 的条件,我在这么做时按照指引在 M$FT authenticator app 上扫码登录后又删掉了登录设备后成功地把自己给锁住了:
- 您必须输入 authenticator app 上的验证码才能继续操作
- 但我没有 authenticator app
- 您可以在 https://mysignins.microsoft.com/security-info 上删除 MFA
- 您必须输入 authenticator app 上的验证码才能继续操作
netabare
2023-01-29 04:16:30 +08:00
@h0099 我给你私发邮件了,如果你邮箱没改的话。
h0099
2023-01-29 04:20:03 +08:00
@netabare 小心某奥利金德皇帝把我的域名邮箱都给活摘了,毕竟当年是他注册的企业微信,而至今里头还存着十万甚至九万条伊美尔
netabare
2023-01-29 04:25:22 +08:00
@h0099 那请您留一个可以长期使用的稳定联系方式,可以通过我留在 GitHub 的那个邮箱或者 zulip 或者别的社交软件留给我。
netabare
2023-01-29 04:29:23 +08:00
@h0099 以及建议去 /chamber 里面开一个专门的讨论。
h0099
2023-01-29 04:30:38 +08:00
什么讨论
h0099
2023-01-29 04:32:19 +08:00
不适合首页上的登录墙内信息茧房:

这就是 v2 机场
h0099
2023-01-29 04:33:10 +08:00
#13 @netabare 建议直接使用四叶信安底层壬上壬上海贵族 FSF EFF 精神会员杨博文阁下 @yangbowen 最爱的 GPG PGP 包装信息

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/910403

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX