8.0 KiB
06 · 表达式与运算
0/20 项已确认。当前推进 06-01;本分类尚未完成选择。
本页整理原讨论的问题、验收要求和候选方案。勾选代表讨论中已确认的规则,不代表编译器或运行时已经实现。
06-01 普通操作数的求值顺序
- 待讨论
明确二元运算、嵌套表达式的求值顺序,并确定是否采用一条适用于调用参数等表达式的共同规则;用有副作用的例子验收。
06-02 求值失败与中断
- 待讨论
明确某一步发生错误或不终止时,后续哪些步骤还会执行;说明副作用是否保留。依赖【错误处理】。
06-03 运算符清单与分组规则
- 待讨论
列明本轮支持的运算符、优先级、结合性,以及是否允许连续比较;用存在歧义的表达式验收。与【语法】核对。
06-04 数值运算的类型组合与转换
- 待讨论
确定哪些数值组合可运算、是否隐式转换、结果类型,以及不允许组合的失败方式。依赖【类型系统】。
06-05 算术结果的具体含义
- 待讨论
明确支持的加减乘除、余数及其他算术规则,覆盖负数、零、除零、舍入和精度。
06-06 溢出与特殊数值
- 待讨论
确定整数越界、浮点溢出、无穷、NaN、正负零等情况是否存在,以及相应行为。依赖【数值类型】。
06-07 同类型值的相等
- 待讨论
确定相等比较的含义、结果要求,以及复合值按结构还是其他方式比较;说明是否允许违反自反性等性质。
06-08 跨类型相等
- 待讨论
确定不同类型的值能否相等、是否转换,以及无法比较时返回假还是失败;与数值转换规则核对。
06-09 身份比较
- 待讨论
决定是否提供身份判断、哪些值具有身份,以及复制、共享对身份的影响。依赖【对象与类型模型】【内存与生命周期】。
06-10 大小比较
- 待讨论
确定可比较的类型、全序或偏序、不可比较时的行为,以及大小比较与相等的关系。
06-11 哈希及其约束
- 待讨论
决定哪些值可哈希,明确相等与哈希一致性的要求,以及可变值和自定义相等的影响。与【集合与容器】核对。
06-12 条件位置接受什么值
- 待讨论
确定条件必须为布尔值还是允许其他值,并说明转换规则和失败行为。与【控制流】【类型系统】核对。
06-13 逻辑运算与短路
- 待讨论
确定逻辑运算符、是否跳过右侧、结果为布尔值还是操作数,以及能否自定义这些行为。
06-14 位运算与移位
- 待讨论
决定是否支持;若支持,明确适用类型、位宽、负数、右移和非法移位量的行为。
06-15 运算扩展的声明方式
- 待讨论
决定用户能否定义或重载运算,以及通过函数、协议、类型成员或其他机制声明。依赖【类型系统】【定制协议】。
06-16 运算实现的选择规则
- 待讨论
若允许扩展,确定左右操作数、类型匹配、转换和候选优先级,以及歧义的处理;若不允许扩展,记录内置选择规则。
06-17 不支持操作时的行为
- 待讨论
明确“不支持”、运算内部出错、存在歧义的区别,以及编译期和运行期分别如何处理。依赖【类型系统】【错误处理】。
06-18 复合赋值与修改语义
- 待讨论
决定是否支持 += 等形式;若支持,明确目标求值次数、是否原地修改、失败后的状态。依赖【变量与赋值】【对象与类型模型】。
06-19 常量求值与优化边界
- 待讨论
明确常量表达式和普通表达式是否遵守相同结果、错误与精度规则,以及优化必须保留哪些可观察行为。依赖【执行模型】。
06-20 跨分类一致性验收
- 待讨论
核对上述规则与相关分类的最终决定;通过包含副作用、混合数值、不可比较值、哈希、自定义运算和失败路径的代表性例子,解决所有冲突。
当前问题的方案与语言对照
以下沿用原讨论材料;建议和参考语言的做法均不自动成为本语言规则。
当前仍是 06-01:普通操作数的求值顺序。要决定的是:一个表达式包含多个计算步骤时,语言保证怎样的先后顺序。
例如下面的伪代码,假设各函数都会记录自己被调用了:
a() + b() * c()
“先乘后加”决定运算分组;它并不意味着必须先调用 b()。我们讨论的是这些函数以及普通变量读取的先后顺序。
相关语言的对照如下。这里说的是语言规则,后面的取舍分析不代表其历史设计动机。
| 语言 | 规则与具体行为 | 对我们设计的启发与取舍 |
|---|---|---|
| Python 3 | 普通表达式从左到右求值。上例依次调用 a、b、c,再完成乘法、加法。赋值另有规则:先求右侧,再处理左侧目标。语言参考 |
顺序容易预测;实现需要保留可观察行为。也提醒我们应明确赋值等特殊结构的边界。 |
| JavaScript / TypeScript | JS 的普通算术表达式先求左操作数,再求右操作数;上例也是 a、b、c。TS 对这类表达式提供静态检查,运行时遵循生成的 JS 的规则,类型标注不会建立另一套求值顺序。ECMAScript 规范、TS 文档 |
类型检查与求值顺序可以分别设计;无论选择静态还是动态类型,都可以保证从左到右。 |
| Rust | 普通算术、调用参数、元组元素等规定按源码从左到右求值,上例也是 a、b、c。赋值和复合赋值另有专门规则。Rust Reference、赋值规则 |
明确顺序与编译优化可以并存;需要谨慎控制特殊规则,否则使用者记忆负担会上升。 |
| Go | 采用部分顺序保证:函数调用、方法调用、接收操作等按词法从左到右,但并非所有普通读取都固定顺序。所以上例调用仍是 a、b、c,但下面的例子可能有两个结果。Go 规范 |
可以只固定部分操作的顺序,给其他求值留下自由;代价是“从左到右”的直觉不足以解释所有表达式。 |
Go 的区别可以用这个例子看出来:
a := 1
f := func() int { a++; return a }
x := []int{a, f()}
规范允许 x 是 [1, 2] 或 [2, 2]:普通读取 a 与调用 f() 之间的顺序没有固定。这说明,仅保证函数调用从左到右,比保证所有普通操作数从左到右更弱。Go 规范中的示例
据此,我们有三个可选方案:
- A:普通操作数统一从左到右。 变量读取、函数调用等都遵守;逻辑短路、赋值等结构单独明确。最容易推理。
- B:普通操作数顺序不作保证。 必要的先后依赖仍须遵守;带副作用的表达式可能允许多个结果,实现选择空间更大。
- C:部分操作保证从左到右。 类似 Go,固定调用等操作的顺序,允许其他求值顺序不确定。折中,但规范和使用都更复杂。
我建议 A,原因是它能为后续错误传播、重载和复合表达式提供清晰基础;不能观察到差别的优化仍可进行。错误发生后的传播规则留在 06-02,短路留在 06-13,赋值与复合赋值的顺序另行核对。
你选择 A、B,还是 C?
讨论来源
《语言设计 06:表达式与运算》,整理日期 2026-10-28。章节编号沿用原清单。