V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX  ›  FrankHB  ›  全部回复第 11 页 / 共 92 页
回复总数  1830
1 ... 7  8  9  10  11  12  13  14  15  16 ... 92  
@Nugine0 性能大多数时候都是次要的,至少会让位于功能正确性。只有后者不成问题,才考虑性能。
虽然 API 设计通常不暴露给最终用户,但对可复用软件的用户(开发者)经常还是重要的,所以提供维持不需要动下游代码的可修改性一定程度上属于功能需求。只有在没得选的情况下,被迫劣化设计,才会让这个前提不存在。而剩下的情况就得考虑性能差异是否足够要紧到劣化 API 的功能设计上。

对支持确定性资源释放的语言,unwind 对维持不变量至关重要——这明确属于功能需求。如果 unwind 会是劣势,那说明没用上这个保证。C++不允许用户以可移植的方式选择这种情形属实无能,但至少行为上能够保证正确(硬要无视,自己包装类类型故意跳过释放,反而逻辑上基本是错的;现有 C++规则让用户实际损失的主要是调整释放的副作用顺序的自由);而没法 unwind 隐含保证顺序依赖的资源释放,相比之下更加无能。
传播错误的实现开销不只是一个寄存器,还有正常路径上的分支,只不过实现不好的异常也可能有另外的正常路径开销,所以之前没展开。但关键是,不管这里的开销有多小,都比实现正常的默认策略的正常路径无额外开销更差。而异常路径本来就相对对性能不敏感,所以只要不是慢到没法用,就能接受,不需要另外“优化”。用默认路径的开销作为代价的方法本来就是可疑的。

(题外话,如果“错误”需要立即处理,不需要考虑 neutrality 的默认策略,那么本来就不适合通过异常提供。不过始终需要立即处理的东西是不是真的应该叫“错误”都是个问题。利用返回值实现的替代方式中,诸如 expected 之类的词汇对这点的混淆,也是个现时问题。)

函数签名的问题是静态类型系统的缩影。根本上,静态类型系统提供的是静态可证明保证。如果程序翻译时不需要这些保证,那么出现在程序中就是语义噪音。
描述 API 的类型签名是静态类型的使用理由中最充分的一个。作为一种约定,它必须在程序运行前确定。对大多数没有多阶段支持的语言,这意味着翻译前确定。这其中毫无疑问地包含了输入的前置条件和输出的后置条件,对应为参数类型和返回类型(用类型构造器→组合就是函数类型)。
而任何超越接口约束的信息,编码到类型中,都是一种滥用,尽管这种滥用可能允许实现生成更高效的代码,它是以暴露实现细节、阻碍接口设计的普适性(依赖更特定应用领域不关心的具体类型系统的元语义)为代价的。
异常本质上编码的是特定条件的备选行为,不属于输入或者输出。所以逻辑上,默认就不该让函数类型依赖异常是否存在,更不应该依赖异常具体是什么。C++ dynamic exception 就是个失败的例子。真要依赖,也就是一个提示(如 C++ noexcept ),就像 __attribute__((pure)) 保证可假定不存在非局域副作用一样,noexcept 保证其中没有传播到外部的非局域控制作用。
C++17 noexcept 影响函数类型已经是特定类型系统的设计侵入普遍接口设计的工作流了:它指明了 API 设计者默认情况下有义务明确函数是否会通过异常退出(即便如此还是鸡肋,实际上组合现有实现时也没法静态确保 noexcept 不会被违反,还是得 terminate )。
只不过,这些多余的工作量在已经考虑到 noexcept 的 API 设计中,问题不大,其它情况也容易通过调用方一次性解决(大不了再包装个;比如 C 函数指针要求你自定义 malloc/free 备胎的情况可能就得多塞进去 lambda ),实用问题相对比较小——但仍然已经体现出危害,比如 std::function 缺的特化。
(实际上,虽然没考虑清楚这里的问题,之前 WG21 已经知道了 noexcept 影响接口验证的问题,因此限制 std 中的 noexcept 的使用;参见 N3279 。)

为了在允许完善地解决这些矛盾,出路有两个方向:
一个就是让开发者更明确地表达约束,使用 union type (或者弱一点的 sum type )等方式把可能出现的异常明确当成输出的一部分,而跟真正意义上的异常划清界限。这个方式也更普遍地符合对静态类型的其它使用实践。但是注意,偏离显式类型的糖就算能减少代码啰嗦程度,也是一种这种实践的弱化。
第二是让类型系统不那么静态而可选:根据用户需要,期望传播带有静态检查就影响函数类型,不需要就默认不影响。很遗憾,这需要让类型依赖阶段(以及和经典的→不同的构造器),普通的静态类型做不到这个。对大多数不能重头重造的语言就是此路不通。
@Nugine0 “大多数”不直接决定决策结果,但这里足够推理出决策的方向。
无视“大多数”的下场就是这里无谓的工作量会显而易见更多。无谓在于:
作为实现细节,是需求方不关心的;
还会可预见地或多或少劣化代码的可读性(阻碍关注点分离)和可修改性(接口签名传染性),是属于留着就找不出什么好处的类型。
所以,这种能预测的纯粹成本应当能优化就优化掉。没干掉只能说是受到现实条件的限制而不是说不想要,否则就真是三观问题了(比如行数刷 KPI 什么的)。

Herbception 不是真正的传统意义上的异常,而是 union type 的另一种变体,只是和现有 C++ 的语法差异更小,改动少而已——但仍然不是没有。
注意到使用这种机制需要侵入到声明中的 throws 指定变换。在 ABI 上,它同样需要预留改变布局的约定。
所以没什么新鲜的,只是类似 P0323 的语法糖而已(反正 ABI 上 std 里的东西本来就允许飞线)。
除了略没那么啰嗦(改进有限),它仍然具有类似的设计的所有既有问题。特别地,它宣称的异常可见性问题恰恰就是消除传统异常的优势——我说过,如果需要,那么就老实用 union type 的方案,更通用也更明确。用糖来混淆反而削弱目的。
而剩下关于开销的问题,本来就全是实现细节。任何一个能和既有实现划清界限的新设计都有资格来做,但是削弱不修改签名来做就可能得不偿失。反之,例如,如果用 throws 改变 throw (意味提供一个新的、可以无视兼容包袱能在运行时把更多事做对的控制操作符)而不是用 throws 改变 throw 的含义,就不见得有那么明显的问题。
这里的设计思路导致上面的主要缺陷仍然没有改变,结果也只是提供了一种更简单的新写法转移视线罢了,性价比很成问题。(特别是考虑 C++ 已经够复杂的情况下。)
另外,这种侵入式修改在 co_* 中已经表现出很大的争议性。类似的问题是过于暴露实现细节(如果不是为了允许一些固定的静态实现策略,可以直接如 P0534 一样支持非侵入的语法),而完全不足以代表所在问题域的通解。这类提案即便通过,也很难是最佳实践。( C++20 coroutine 大概还没怼赢 Clang++ 的冷屁股。)
2022-08-15 00:46:29 +08:00
回复了 gps32251070 创建的主题 程序员 关于编程语言内存对齐的疑问
补:关于填 padding 还有避免 cache line 上 false sharing 的问题,某种意义上也是对齐问题但跟一般访存对齐不同,不过这个就更依赖实现细节,真要优化可能得分析具体的一致性协议。
cf. zhuanlan.zhihu.com/p/374586744
2022-08-15 00:29:49 +08:00
回复了 gps32251070 创建的主题 程序员 关于编程语言内存对齐的疑问
@root111 总体原因是局域性。
即便只是核内的第一级缓存,cache 和执行访存的实现电路(比如 LSU )不是一个部件,要操作 cache 物理上必须发信号等待同步,确保满足 cache coherency 以保证之后 cache 的状态可预测。只要不是允许禁用缓存这个开销就无法避免,但后面几级缓存不确定性就大了,比如都 hit 就很快,反之要跟后一级缓存直至主存同步,相比就慢得多。
现在的级联 cache 设计的关联策略可以保证前级 cache 如果只操作同一个 cache line ,后级 cache 也可以在同一个 cache line (如果只是 load 都 hit 就可以不管后级 cache ),反过来难以保证。所以一旦跨 cache line ,脸不好就引发刷后级 cache 直至刷到 uncore 里的 LLC 甚至主存的最慢的路径,差距很大。另外,如果占用多个 cache line ,意味着其它数据能占用的 cache 就少了(也更容易刷出去),会全局地阻碍 cache 的加速作用。
跨 page 涉及到的东西就更多。page 是主存提供主要空间的地址空间里的结构,现代机器基本都是 MMU 实现的,里面有一些专用寄存器帮助实现 page table 、TLB 之类一大坨数据结构,具体补课体系结构和操作系统。跨 page 的访问基本上是得拆到两个 page 的,以反应不同 page 允许具有的不同状态。同一个 page 内硬件可能按历史访存请求以减少访问这些数据结构的开销,但跨 page 这些就大半失效了。
当然不管是不是跨都可能有物理内存没加载就绪的情况 page fault 了。基本如果要能用也是得操作系统一大坨软件代码分配空间,不能用也是不常见的慢速中断路径,要快就见鬼了。这里跨 page 可能一次性开销×2 。
2022-08-14 23:51:25 +08:00
回复了 gps32251070 创建的主题 程序员 关于编程语言内存对齐的疑问
你对实现机制理解严重不足。
对齐的直接对象是处理器访存指令中的地址操作数。访存要对齐,根本原因不对齐的地址需要额外的计算而不划算。假定对齐的地址可以直接当做低几位是 0 。
对 CISC 处理器,硬件可能加更多电路以确保地址的每一位都有效(有时还得检查是否对齐引发异常),而假定对齐的地址访问直接就把低几位忽略了。
RISC 设计甚至就基本把不对齐访问给省了,ISA 层面上不支持不对齐访问,真不对齐可能就直接异常(并行的,原则上正常路径不耗时间)。如果你要强行非对齐访存,那么就得用粒度更小、延迟可能更大的特设访存指令,或者访存完再截取一段数据这种软件方式模拟,这些都是开销更大的,差一个数量级都正常。
这个意义下,同样一次逻辑意义上的访存,两者的开销本来就不保证一样大(就算同时支持对齐和不对齐访存,非对齐的访问可能更耗指令周期;虽然也设计有一样的,但一般至少不能反过来指望不对齐更快)。
@Nugine0 这才是最扼腕的地方:这些场景明明客观上就是显然的大多数,但是很多用户就会因为自己使用的语言和习惯为缺陷辩护,而不是直面不足。
在工程上,很多时候经验上的“大多数”足够决定一个决策的正确性。反其道而行之是不明智的。这跟银弹完全是两回事。以银弹为理由而拒绝重新审视现有实践,逃避全面分析问题的成本和收益,是不成熟的优化的一种。

Google C++ 规范不用异常,简而言之就是菜。
Google 相关人士有提到过,他们知道现代 C++ 默认就该放手用户使用异常,但是他们自己现有代码的历史包袱太重,没法承担放开手下人用异常的质量风险。

技术上提出的问题全是比较好笑的:
* 他们认为加异常就需要遍历所有实现的路径。
这直接就不是默认情况——并且说明原来的代码质量普遍就很糟糕,没写清楚接口约束,也习惯用户不看这类“错误条件”就瞎用。
* 他们认为用了异常就难以看清逻辑。
大体同上,只是更偏向于内部接口的实用。(另外类似地,是不是能认为用了赋值就难看清逻辑?其实某些 PFP 教徒还真是那么想的……虽然他们甚至未必都拎清楚这是为了 equational reasoning 这回事。)
* 他们认为写出异常安全的代码是负担。
但事实上异常安全不是线程安全那样需要另外加逻辑才能维持可组合性的属性,不引入异常安全操作,整个程序默认都是异常安全的,就像 Rust 你不去用 unsafe 默认就不用管一些问题——区别无非是 Rust 有明确的关键字和编译器强制检查,这里依赖自觉;但这个自觉工程上很容易做到,甚至人肉检查都没多大开销(特别是按标识符索 poisoned 属于最容易 review 中找茬刷 KPI 的一类,比找拼写错误都容易)。
只有滥用破坏安全性不变量的低级接口才会有这问题。类似地,正常人代码禁绝 malloc/new (不担保资源所有权不变量,恰好同样不担保异常安全)满天飞就能几乎完全静态避免内存泄漏(除了循环引用)。这逻辑就像“写出不泄漏的代码代价太大,学智能指针( RAII )麻烦,所以还不如老老实实用 malloc/new”一样可笑。更荒谬的地方在于,这些代码组合进启用了异常且正确实现了异常安全的正常 C++代码以后,还可能因为这类局部的不安全的传播污染整个程序而使整个程序的安全性失效,基本就快直说“Google 的 C++代码别指望给正常 C++代码复用”了。( Google 还敢在自家开源项目里鼓吹这个,呵、呵。)
实际 Google 倒是有老实用智能指针而不一定同时具有所有的具体问题,但同样缺乏普遍保证,而且不少是自家比 std 劣等残次多了的发明。
* 异常很可能存在额外开销。这是唯一一个真实的工程问题,但是 Google 绝大多数项目实际根本踩不到这里的问题。
即便是这个问题也不是 C++ 自己作死的原因,TR 18015 指出语言上没直接开销(当然这里对空间开销分析不足,但你指望一个提了 stack unwinding 却连什么叫 stack 都说不清楚、多大 satck overrun UB 的语言啥呢)。
实际开销绝大多数问题都是对 ABI 的不切实际的假设。这些也不只是异常,unique_ptr 因为就因为写 SysV ABI 的 C 厨的不走心在 *NIX 实现里走不了寄存器,还得编译器开洞没法默认用。
如果这种问题算回避不了的开销,那么 C++ 早就是性能洼地了。实际呢?不用 C++ ,用 C 用汇编或者其它手段能写出跟 C++ 实现相比像样的代码用户有多少?

退一步讲,就算不深究造成这些历史包袱的责任,这些问题的坑也都是合格的 C++ 用户自动会绕开的,反而要改变习惯还费事(比如 new 得多加 nothrow 免得换到正常配置里直接呵呵了)。你旧代码多,所以新项目代码也跟着喂食,这什么姿势?
就这点原因都敢写到全公司的规范,Google 这里的技术水平整体(先不说个别 team )远不如微软之类,能和鹅比都不错了。
我建议分析 Google 这些规范的不靠谱性列入任何想用 C++ 正经干活的组织的标准面试流程。
@Nugine0 有风险是对的,但改 API 本来就要人来改,人来检查正确性,谁改谁承担风险。
(当然其实这不太公平,因为合理设计的 API 允许一定的行为不同作为实现细节而不被 API 依赖,出问题原始设计者也该背锅。这个现在的语言更不好强制,基本得自觉。)
要求改代码对于大多数静态本机语言的感知还是很大的,特别是只会部署二进制的程序,光是无法满足重新编译,很可能就足够枪毙一个调整 API 的方案。

盲目 unwind 当然会有问题(比如不方便控制某些资源生存期,比如掉 frame 不便调试),但和这里的问题不直接相关。更一般的 control effect primitive 原则上允许用户自选实现策略来实现出异常,不过这更复杂(比如考虑 abnormal 但不 exceptional 的 path ),缺乏现成成熟应用,或者其实就是相当不成熟(提供 call/cc 的 Scheme 一般异常也是内建实现的)。但这也就是说,仍有很大改进空间,而不是此路不通。
异常不都能保证实现这里期望的性质,但至少提供了一种既有成熟的方案,而不是像你提到的其它替代一样注定不可能克服问题。
@Nugine0 Rust 的 ? operator 或者 C# 的 ?? (null-coalescing) operator 这种 ad-hoc 的解法是可以避免一些写起来的啰嗦。不幸的是,无法解决更多更根本的问题:
实质上还是 union type 或者 checked exception 的弱化形式,添加错误处理或者修改错误同样会改变接口签名(后者可以用错误擦除类型缓解,但开销嘛……);
就算是解决传播错误废话多的问题,相比原本没错误处理的代码还是得动源代码、引入语法噪音、阻碍关注点分离,比起上游引入新的异常 /在异常中添加和修改错误条件可以直接不用动所有下游不需要处理错误的 caller 实现(包括二进制代码),还是有很大的工程劣势;
因为编码错误和结果共同占用返回值,在较长的调用链(默认情况)中一旦没内连(很常见),至少会浪费寄存器带宽,同时基本不可能被优化(因为侵入签名,优化可能需要 ABI 魔法,不用魔法的地方基本都能直接内联了);
依赖操作数具有一些特定的类型,实际上是弱化的 monad 糖,但又没完全版本的 monad 那么可扩展能自定义。
2022-08-13 03:13:19 +08:00
回复了 inSpring 创建的主题 Android 把给 app 打包的 keystore 文件弄丢了,怎么办?
老实官方承认问题要求用户重装吧。

@edis0n0 你这么会让懂点行的用户怀疑作者是不是被黑了,现在是替身……
@codehz 我不满现有语言,就自己造了。但是理由是别的(光这里 C++ 其实都够用)。

对一般用户,最简单的就是去熟悉带有异常支持的语言,习惯怎么简化默认策略之后,去提议增强没这种机制的语言。
本质上,异常是一种控制作用(control effect)的具体应用。控制作用和修改对象一样同属副作用,是很基本的东西,语言缺乏这种特性多少是残的,对通用的编程语言,只要不是像 C 那么躺平(但就算 C 都有 setjmp/longjmp ),实在是逃不过的。
当然你可以说纯函数式语言就拒绝副作用,但其实真完全拒绝的就没法编程了。典型做法是副作用和控制作用都打包成 monad 隔离。但说实话这要没语法糖就不是给人用的,有也就是强迫用户用糖化 CPS 风格强制代替自由书写直接风格的程序的自由罢了,属实没事找事。
(用 monad 能组装出修改状态是因为假定 monad law 存在下的一些规则起到了能表达 delimited control 的类似的作用,后者能表达可变状态。其实典型的异常严格意义上比 delimited control 还强点,理论上一个不存在对象修改而只有异常作为副作用的语言也可以写出修改对象的实现。)

缺少 checked exception 不是问题,实际通常反而有才是( https://github.com/FrankHB/pl-docs/blob/master/zh-CN/typing-vs-typechecking.md#%E8%BF%87%E5%BA%A6%E8%AE%BE%E8%AE%A1 ),而且我很怀疑多数 Java 用户就是因为 checked exception 的原因养成过于习惯到处 IDE 加自动生成的 catch 的坏习惯,才失去了自主发现 exception neutrality 的重要性的机会。
用接口的不管怎么样都要看文档搞清楚错误条件。忘记处理异常会有问题,基本说明接口设计有大问题。而实现者不该忘记的是,在实现时确定是不是应该无视原地处理异常的决策过程,即便大部分情况就不应该处理。如果你真需要类似 checked exception 的强制 caller 处理而且不在乎污染签名,那就用 union type ,反而更容易在没异常的语言里找到变通方法(没真正的 union type 基本直接用 sum type 代替,原地 /一层 caller 处理时差别不大)。
至于 bad_alloc 其实基本就不用处理了,因为恢复不了( new_handler 已经救过了),约等于 panic ,比有的 panic 直接 abort 强一点的地方是还能自定义 terminate 最后挣扎一下。
std::ios_base::failure 默认就不该遇到,除非你故意设置 mask 指定对某个状态抛异常,那不接就是你的锅了。这是个用不用异常都不省心的地方(这些异常中一部分通常明显比其它异常更该原地忽略,但另一部分不是),所以直接允许自定义。
2022-08-13 01:34:06 +08:00
回复了 dxatgp02 创建的主题 Java Java 对象里为什么要用 get set?
@CodeCodeStudy “Java 的成员变量没有多态”——这位童鞋也还是表扬一下吧。
这个算是意外地比较接近核心的说法。
C++ 中尽管没原生支持所谓的 property ,但可以用带有用户定义非 explicit 转换函数的 proxy object 模拟实现。(所以原生 property 多余,这是另一部分 C++ 用户反对的理由。)
转换函数的这种隐式机制是强制(coercion) ,是一种经典的特设多态(ad-hoc polymorphism)。这个意义上,原生的 property 也能视为同类的玩意儿。
这种多态经常是“不好”的,是常见栽赃出所谓“弱类型”的理由之一。不过就破坏求值算法的可预测性和增加复杂性来讲,这里的确是不好(即便对 proxy object 作为表达式的求值可以确定性地分解为符号求值和模拟的函数调用)。
和另一种特设多态——重载一样,这阻碍了直接以名称引用时的一些操作。&个重载函数会直接搞出个非一等的 overload set 还要在特定的上下文中消歧义,而要引用一个 property 的名称区分是不是已经转换的值相比就更加没事找事——更何况万一这玩意还不止 get 和 set 还有更多私货转换呢?(要转换不那么欠扁,除非是 lvalue-to-rvalue conversion 这种完全能不看类型声明就确定转换方向的,那就可以直接当成另外的非特设的子类型多态了)。一个表面上的语法糖搞出了杂七杂八的语义包袱,还有用户会当简单,真是呵呵哒。
2022-08-13 01:07:45 +08:00
回复了 dxatgp02 创建的主题 Java Java 对象里为什么要用 get set?
@FrankHB 俩 typo:
class 能近似模拟 environment → object 能近似模拟 environment
record syntax → record grammar ,反正这里的 syntax 不是下文的那种
2022-08-13 01:02:02 +08:00
回复了 dxatgp02 创建的主题 Java Java 对象里为什么要用 get set?
@guoqiao 算是这整层楼里差不多唯一孺子可教的。

反过来像一些什么面向对象吹继承封装多态的……项目组里见到这种小朋友建议趁早出清。

@Oktfolio 是这层楼里唯一值得当反面教材批评的。
和其他人不同,此君提到了 TC39 的“丑陋”proposal 。

我补充一下,TC39 的人是知道这并非必要的:github.com/tc39/proposal-class-fields/blob/main/PRIVATE_SYNTAX_FAQ.md#why-is-encapsulation-a-goal-of-this-proposal
为何?因为 ES 本就烂用户集中营,搞不定 js 这种缝合怪,又引进外来种解决用不好的问题,然后 TC39 的品味向这种糟粕趋同了而已。

关键是那么一点:你要类的访问控制,是为了实现封装,而不是 TC39 这里还需要去解释封装是一个实现所谓私有字段的理由——这种典型的倒果为因和妥协才是真正的丑陋。
(顺便说明挂面向对象小朋友的原因之一:封装是普遍需求,跟是否面向对象无关。)
至于为啥要封装?理论上的理由不少,不过“为了更好地实现易用和不易误用的抽象”就够一般工业界用户堵嘴了。

为何要类的字段?因为抄的 C++/Java 之类的不支持一等环境(first-class environment)的静态语言,这类语言中环境(基本就实现为某种内部符号表)不对用户可见,更别想在运行时改,于是所有配置都得靠预先声明。字段声明带访问控制也就是这种限制下比较容易的做法。
对 ES 这样 class 能近似模拟 environment 的强力语言来说,不去发挥长处反而向无法简单扩展的弱鸡限制看齐,这也是耻辱。要知道 v8 之类的高效的实现为了这种程度的动态性已经吃了大亏了。你加了字段又不能把这里的开销塞回去不实现了,结果就是纯纯增加实现和使用的复杂度。多了个啥……字段连正经的糖都不是,半吊子。
不过这里倒不是 TC39 特别拉胯。Scheme 在 R6RS 前后也对一些太偏 syntax 而非 procedure 的 record syntax 有争议。虽然也是一地鸡毛,比 ES 党人高的是,他们好歹知道 syntax 和 procedure 的区别,ES 这种没 hygienic macro 的语言(=所谓的 syntax )自然也不配有自动考虑到这种深度的用户了。

清楚这个以后,就知道所谓少语法糖根本不着边际,而是允许这种语法糖本来就代表不同的思路。所谓 getter/setter 根本不是所谓 property 的模拟。
前者本质是函数,允许被调用同时其中蕴含副作用。
而后者本来“什么都不是”,就是个对象引用,或者更准确地说是符号求值——本质上里面有的是个 name resolution ,语义是拿标识符确定在环境中引用的对象作为求值结果。如果建模出环境写成小步语义,就会很清楚地看到符号求值原则上是纯的,意味着编译器之类的实现可以相当静态地确定这种操作的结果而轻易做出不少优化(只要环境能被确定——这在典型地静态语言中是完全“免费”的)。
而纯函数在函数语法中通常不被支持(除非是 PFP 语言不允许非纯函数,甚至用 η-equivalence 把两种语法当作一种,比如 Haskell ),如果支持要么就扩展到处 __attribute__((pure)) 之类。不算这种扩展,引入任意的所谓 property ,就把 property 折叠为了函数调用的糖,损失了一种原生的纯求值语法,妥妥地削弱语言的抽象能力。(顺便另一侧考虑,这也是我黑 Haskell 的原因之一。)
对支持 hygienic macro 的语言,这类设计通称 identifier macro ( C 这种不 hygienic 的叫 object-like macro ),会让 macro expander 的算法和实现以及对象语言程序的兼容出现更多蛋疼的问题。具体就不展开了。

因为见识的语言太稀薄就歪到所谓面向对象的道道上的用户真该反思自己真没想象力了。

基于同样的一些理由,一些 C++ 用户反对 property ( MSVC 扩展有)。虽然 C++已经是复杂到“怎样都好”了,不过能少点鸡肋不如的特性还是少点好。
2022-08-13 00:17:58 +08:00
回复了 yodhcn 创建的主题 程序员 JSON 发明人:老朽的 JavaScript 编程语言早该入土了
@musi 我主要是指虽然传统上很多 C 用户就有看反汇编的习惯,甚至调试器可能挂不到源码上就只能调试汇编,但是 js 用户可没被逼出来这种习惯。看 wasm 显然比看 js 麻烦,就算 wasm 工具齐全了也不太改变习惯。就算是 C 用户,只要能对着源码调试,谁还非得没事去看汇编啊……再说浏览器跑 C 跟 js 比起来,实在不成气候,将来 js 用户会长期占绝对多数。
@codehz @neoblackcap 这些说法是有问题的。
是否能够忽略错误,根本上就是应该是接口具有的含义确定。有的接口的用途决定一些错误就一定是实现细节,而应该及时被处理掉;此时 caller 被设计为无权依赖这些实现细节,更不应该被这些泄漏的细节干扰。
只是这里 2 的接口设计怎么都跟允许忽略错误的接口设计需求不搭。

反而一般的接口实现作为程序的一部分,不应该随便决定整个程序是不是能处理错误(况且判断是不是该死其实也是一般意义上的一种“处理”),因为 caller 通常总是比 callee 更配代表整个程序来决定是不是该死掉。所以不能就地处理的错误默认就该甩锅给 caller ,出问题就是 caller 的问题。
如果整个调用链都是默认策略,在这里就是最终 panic ,也就是选 1 。而在某个位置因为接口的设计,可以能出现类似 2 的情况(但不应设计成这样容易误用的接口,下同),只是一般较少见。
这种“默认”的性质是可组合的,叫做 error neutrality 。但是因为语言设计的原因导致一些语言这种“默认”策略编码起来无畏地啰嗦,具体如 C 、Go 、Rust 、Java 用户通常在这方面的意识很弱,或者基本没有。一些 C++用户倒是容易注意这些问题——没事别乱 catch ,要及时 rethrow——于是实现了 exception neutrality ;不过另一部分死脑筋嚷嚷排除 exception 的用户(基本同 C 用户,除了那些会 sjlj 的)这里一样没救。
因为大量初级用户容易默认语言既有设计真实反映了实用需求,这种啰嗦的一个直接问题是让许多用户无法意识到 1 和 2 的比例在实际需求中的差异,以及造成这种差异的理由,而放弃深究问题的来源,停留在流于基于既有经验的瞎蒙,以期望得到 best practice ,却不知道已经踏入了具体语言设计局限性的陷阱之中。
2022-08-12 05:00:35 +08:00
回复了 chiyagao 创建的主题 程序员 编写代码的工作区文件路径需要全部保持纯英文么?
该用啥用啥,只要你清楚代价。
最大化可移植性不是目的,否则干脆把 MS-DOS 的 8.3 也支持了,但一般显然没必要。
编码么同一个环境通常不是问题,但不排除比较极端的例外(比如 Windows 下升级 hg 版本可能活久见乱码)。
不过就算不考虑这些,只要可能换系统玩,文件系统大小写问题也够你喝一壶了。( NTFS 现在虽然支持大小写敏感,但得单独设置,而且设置以后像一些 ps1 命令直接罢工了。)

@nightwitch 非 BMP 问题还不如甩锅给 Unicode 自己。一开始就妄想 BMP 解决问题,直接搞出 UCS-2 半吊子,还要 UTF-8 擦屁股,又回过头另外擦出个 U+10FFFF 的 magic number 。这还没完,刷版本到处给跟不上的兼容,Han unification 漏去重之类的添乱不说,UTS#51 什么屑玩意儿……甚至还能遇到 zzzq 问题( https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p1949r7.html#all-emoji-become-excluded-instead-of-just-some ),xswl……
即便是 BMP 在这里也还有问题,比如有多少用户敢确信清楚自己用的环境对 NFD/NFC/NFKC 的支持?能塞零宽和控制字符进去么?(虽然 OP 只是说中文基本不用管这破事。)(其实控制字符嘛,就是 ASCII 也有这个问题,但很容易限制;关键差别是 Unicode 的破烂即便不考虑版本都不是能指望人能枚举完的。)
2022-08-12 03:53:06 +08:00
回复了 Ayanokouji 创建的主题 程序员 程序员区提到的“内存”不应该默认是“memory”吗
@FrankHB 注释:RAM disk 也是约定俗称的说法,其实照我上面的观点并不太支持,正经的说法应该是 RAM-backed virtual drive 。但这个实在太啰嗦和不常见了,就仍然沿用了 RAM disk 这个本质有问题但是至少外延上不会有歧义(不会找错东西)的说法——如果照虚拟设备(像 VHD 就不关心真的是不是真能让你看到个长得像 disk 的东西,而只是把 disk 该有的软件可访问的性质都模拟了)这样的意思的理解还是能说得通的。
2022-08-12 03:47:35 +08:00
回复了 Ayanokouji 创建的主题 程序员 程序员区提到的“内存”不应该默认是“memory”吗
@catalysia 歧义生效的主要问题是指代不明,同时没法通过补充定义来消歧义。排除歧义的优先顺序不一定就是看歧义“大不大”,而首先要考虑添加消歧义后如何避免逻辑矛盾,否则意味着整个说法就是一团乱。
一股脑儿把包括提供硬盘空间在内的存储部件都叫 memory 可以不是歧义,因为至少是技术上正确的,memory 就是个系统能支持的顶层分类。全混沌抽象成 memory ,最多只是不够明确而不是歧义。虽说考虑到大多数典型实现的习惯,在物理器件上,强调持久性的 memory 一般会直接叫做 storage ,而名义叫做 memory 的实际自动退化为 internal memory ,但这种情况下仍然不算错,因为内存都是存储。

同理,内存不足都是存储不足,反过来未必。理由么,用 PL 的话来说,内存是存储的真(proper)子类型,在这个偏正结构的上下文中符合协变,因此形式地有:内存⫋存储→内存不足⫋存储不足。这个推理的依据是普遍的类型论公理,跟直觉相一致。
删除软件可能真是在释放内存,比如你软件存 RAM disk 上。当然,更多时候软件放在外存,所以直接说“释放内存”一般就是错的。这里麻烦的关节其实是基本会至少隔了个文件系统,所以用户见到的效果不同。如果要强调可用性,那么还不如直接叫“释放空间”,尽管“空间”其实比“存储”更抽象而笼统,但不同上下文体验容易一致。
cache 和 swap 自然都算 memory 。前者总是内存所以是存储;后者在系统可见的虚拟地址空间视图上属于主存,即便不保证不是内存(无聊点可以 RAM disk 上继续划 swap 出来)且系统自身看到可能不同,说成“存储”也是没歧义的。

直接把手机外存说成硬盘是错的,因为几乎就没手机真带硬盘。很早以前的 WM 设备能带动内含微硬盘的 CF 卡,基本也就只有这种非主流奇葩配置下才可能偶然正确。
顺便,现在市面上的手机也有带 SSD 的,但 SSD 不是硬盘。PC 上所谓的硬盘很多也是离谱的,真正的硬“盘”是 HDD 这样的带有机械盘片结构的磁盘(跟软盘相对),而 SSD 一类封装 IC 驱动器大部分存储部件通常就是闪存,恰恰不是硬盘,硬说“盘”(扁的,不要求圆的能转的,其实是 plate 而不是 disk 了)也就是闪存盘。iPod Classic 那种叫硬盘没问题,因为真是 HDD 。
把 SSD 算硬盘这个怕是真没救了。但这里至少也可以用更准确的说法代替来回避。

IC 实现的非易失存储可以都叫闪存,尽管这个提法在历史上不能涵盖 PROM 之类的形态。但现时闪存一般不被认为专有的商标,所以就像 PC 不需要专指 IBM PC 一样,可以按照构词追溯到语源之前,追认传统的 PROM 作为现在意义的闪存( flash 指可擦写),这也不会有什么问题。字面上反倒是 ROM 的“只读”尴尬多了,但就是这个都能靠加前缀救回来,扩展闪存的外延又算啥呢?
再者,现时几乎也就 NAND 和 NOR Flash ROM 两种,限制闪存的具体实现也是不明智的。不必要排除历史或未来的其它实现。

至于 CCD ,如果你说的是能作为传感器实现部分的那种,那么很明确不属于体系结构意义上的 memory ,尽管它能是物理实现意义上的。就像 SSD 主控里带的处理器甚至 CPU 微架构内部隐藏的协处理器(比如 Intel ME 跑 Minix 的 RISC CPU )也不应被视为整个系统意义上的控制器的一部分一样,这类部件都是体系结构意义下的可选实现细节。即便这类器件在物理上保持作为不同角色的能力,系统的拓扑结构限制了这些能力对系统外部(软件)可见,所以让角色重新回归也没什么意义——除非你讨论的系统能物理地把自己拆了动态重配置这些器件的角色——我是没见过。
1 ... 7  8  9  10  11  12  13  14  15  16 ... 92  
关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   2326 人在线   最高记录 6543   ·     Select Language
创意工作者们的社区
World is powered by solitude
VERSION: 3.9.8.5 · 33ms · UTC 03:24 · PVG 11:24 · LAX 20:24 · JFK 23:24
Developed with CodeLauncher
♥ Do have faith in what you're doing.