Carbon 语言比较运算符(Comparison Operators)完全指南:运算符语义、隐式转换规则与 EqWith/OrderedWith 接口扩展
【免费下载链接】carbon-langCarbon Language's main repository: documents, design, implementation, and related tools. (NOTE: Carbon Language is experimental; see README)项目地址: https://gitcode.com/GitHub_Trending/ca/carbon-lang
导读
本文以 Carbon 语言设计文档 docs/design/expressions/comparison_operators.md 为主体,系统讲解 Carbon 中==、!=、<、<=、>、>=六个比较运算符的标准数学语义、优先级与结合性规则、内建类型(整数/浮点/字符)的隐式转换比较约束,以及用户自定义类型如何通过标准库的EqWith、OrderedWith接口扩展比较行为。读完本文,你将掌握 Carbon 比较运算的精确规则(尤其是与 C++ 不同的"数学正确优先"策略)、如何在impl中为自定义类型实现比较接口,以及元组与数据类默认比较的字段级语义,并了解 core/prelude/operators/comparison.carbon 等标准库源码中的真实接口定义。
概述:六个运算符与统一语义
Carbon 提供相等与关系(relational)两类比较运算符,每个运算符都有标准的数学含义:
| 类别 | 运算符 | 示例 | 数学含义 | 说明 |
|---|---|---|---|---|
| 相等 | == | x == y | = | 等于 |
| 相等 | != | x != y | ≠ | 不等于 |
| 关系 | < | x < y | < | 小于 |
| 关系 | <= | x <= y | ≤ | 小于或等于 |
| 关系 | > | x > y | > | 大于 |
| 关系 | >= | x >= y | ≥ | 大于或等于 |
所有比较运算符都返回bool:当比较成立时求值为true。它们全部是中缀二元运算符(infix binary operators)。
这些运算符对部分 Carbon 内建类型 有预定义语义,对结构体、元组这类简单的"数据"类型也有默认语义(详见 基本类型与数据类型的默认实现)。用户自定义类型可以通过实现标准库提供的接口来定义这些运算的含义(详见 扩展性)。
从标准库源码看,比较能力通过Core包的 prelude 统一导出:core/prelude/operators.carbon 将prelude/operators/comparison库导出,而比较接口与内建bool、IntLiteral的实现集中在 core/prelude/operators/comparison.carbon。
运算符优先级(Precedence)
所有比较运算符处于同一优先级层级。该层级:
- 低于用于计算(非
bool)值的运算符(如算术运算符+、*); - 高于逻辑运算符
and和or(参见 docs/design/expressions/logical_operators.md); - 与
not的优先级不可比较(incomparable),因此混用not与比较运算符会构成歧义。
合法与非法示例:
// ✅ 合法:优先级提供了求值顺序。 if (n + m * 3 < n * n and 3 < m and m < 6) { ... } // 上面等价于: if (((n + (m * 3)) < (n * n)) and ((3 < m) and (m < 6))) { ... } // ❌ 歧义而非法:是 `(not a) == b` 还是 `not (a == b)`? if (not a == b) { ... } // ❌ 优先级非法:请写成 `a == (not b)`。 if (a == not b) { ... } // ❌ 优先级非法:请写成 `not (f < 5.0)`。 if (not f < 5.0) { ... }not与比较运算符优先级"不可比较"的设计意图在于:not是单目运算符而比较是中缀运算符,二者叠加时括号是必需的;强制使用括号能消除阅读歧义,这正是设计文档刻意保留的约束。
结合性(Associativity)
比较运算符是非结合(non-associative)的,不允许链式书写:
// ❌ 非法:请写成 `3 < m and m < 6`。 if (3 < m < 6) { ... } // ❌ 非法:请写成 `a == b and b == c`。 if (a == b == c) { ... } // ❌ 非法:请写成 `(m > 1) == (n > 1)`。 if (m > 1 == n > 1) { ... }如果允许a == b == c这类链式写法,其语义究竟是(a == b) == c(把bool与c再比较)还是a == (b == c),存在本质歧义;强制用户显式使用and连接两个比较,使得多条件判断的意图一目了然。
内建比较与隐式转换
内建比较在以下四种情况下被允许:
- 两个操作数都是标准 Carbon 整数类型(
Int(n)或Unsigned(n)); - 两个操作数都是标准 Carbon 浮点类型(
Float(n)); - 一个操作数是浮点类型、另一个是整数类型,且整数类型的所有取值都能在浮点类型中被精确表示;
- 两个操作数都是
char或Core.CharLiteral类型。
每种情况下,结果都是数学上正确的答案——即便是在Int(n)与Unsigned(m)比较时也是如此。
// ✅ 合法:符合第 1 种情况。`compared` 的值为 `true`,因为 `a` 小于 `b`, // 尽管对回绕(wrapping)的 `i32`/`u32` 而言该比较结果为 `false`。 fn Compare(a: i32, b: u32) -> bool { return a < b; } let compared: bool = Compare(-1, 4_000_000_000); // ❌ 非法:不符合第 3 种情况,因为 `i64` 的值总体上无法在 `f32` 中精确表示。 let float: f32 = 1.0e18; let integer: i64 = 1_000_000_000_000_000_000; let eq: bool = float == integer;注意上述Compare(-1, 4_000_000_000)示例的精髓:按位级回绕语义,-1与4_000_000_000在u32视角下反而是-1 < 4_000_000_000为假(-1回绕后极大);但 Carbon 选择数学语义,结果强制为true。
涉及整数与浮点常量的比较不受上述规则覆盖,另行讨论,见 与常量比较。
与隐式转换的一致性
Carbon 支持以下 隐式转换:
- 从
Int(n)到Int(m),当m > n时; - 从
Unsigned(n)到Int(m)或Unsigned(m),当m > n时; - 从
Float(n)到Float(m),当m > n时; - 从
Int(n)到Float(m),当Float(m)能表示Int(n)的全部取值时。
这些规则可概括为:类型T可以转换到U,当且仅当T的每一个值都是U的一个值(即转换是无损的、值保持的)。
此外,某些整数常量与浮点常量在能被类型表示时,可隐式转换到Int(n)与Float(n)。
所有内建比较都可以被看作:至多对其中一个操作数执行隐式转换,从而得到一对相同或非常相近的类型,再进行比较。目标类型组合(对每个合适的宽度n)为:
Int(n)与Int(n)Unsigned(n)与Unsigned(n)Int(n)与Unsigned(n)Unsigned(n)与Int(n)Float(n)与Float(n)
一般而言,存在多种隐式转换组合都能到达上述形式之一,但无论选择哪一种,结果都相同——因为所有比较都是数学正确的,而所有隐式转换都是无损的。实现可以自由选择最高效的方式:例如对u16 < i32,最可能的选择是把u16提升到i32而非u32。
由于至多转换一个操作数,Carbon绝不会使用比两个输入类型都大的中间类型。例如i32和f32都能隐式转换为f64,但 Carbon 不允许i32与f32直接比较——即便可以在f64中完成该比较。如果允许,结果可能出乎意料:
// `i32` 可以精确表示这个值。 var integer: i32 = 2_000_000_001; // 这个值在 `f32` 的可表示范围内,但由于 `f32` 精度有限, // 它会被舍入为 2_000_000_000.0。 var float: f32 = 2_000_000_001.0; // ❌ 非法:`f32` 无法精确表示 `i32` 的全部取值。 if (integer == float) { ... } // ✅ 合法:任一侧显式转换为 `f64` 后代码合法, // 但比较结果是不相等:`float` 被舍入为 2_000_000_000.0, // 而 `integer` 会精确转换为 2_000_000_001.0。 if (integer == float as f64) { ... } if (integer as f64 == float) { ... }两种混合类型比较(整数 vs 浮点、有符号 vs 无符号)可能因定义域略宽而比同类比较 效率略低。
这一设计明显背离 C++ 的做法:C++ 会先把两个操作数统一转换为一个公共类型,有时产生有损(lossy)转换从而得到错误结果,有时转换两个操作数,有时使用比两个操作数类型都更宽的类型。
标准库中的实现证据
设计文档中的规则在 prelude 中有直接的对应实现:
- core/prelude/types/int.carbon 中,
Int(N)与Int(M)之间定义了final impl的EqWith/OrderedWith;同时对任意T: ImplicitAs(Int(N))提供了左右两侧的impl(Int(N) as EqWith(T)与T as EqWith(Int(N))),正是"至多转换一个操作数"机制的落地——由ImplicitAs完成单侧无损提升。 - core/prelude/types/uint.carbon 中,
UInt(N)与UInt(M)、UInt(N)与Int(M)、以及Int(N)与UInt(M)的交叉比较均有impl,印证了Int(n)vsUnsigned(n)目标组合的存在;同样以T: ImplicitAs(...)形式提供两侧的单向提升。 - core/prelude/types/float.carbon 目前只实现了
Float(N)与Float(N)的同型比较,源码中保留TODO: Support mixed-type comparisons注释,说明设计文档中的混合整数-浮点比较规则仍在逐步落地中。 - core/prelude/types/char.carbon 中
class Char { adapt u8; },即Char基于u8适配实现;其比较通过int.eq/int.less等内建操作完成(见 core/prelude/types/char.carbon),并以CharLiteral as ImplicitAs(Char)(core/prelude/types/char.carbon)支持字符字面量的比较。 - 内建
bool与IntLiteral的比较实现在 core/prelude/operators/comparison.carbon 中,通过= "bool.eq"、= "int.eq"等形式链接到编译器内建实现。
与常量比较
Carbon 允许以下涉及常量的比较:
- 常量可以与任意它能隐式转换到的类型的值比较;
- 任意两个常量之间都可以比较,即使不存在能同时表示两者的类型。
正如 docs/design/expressions/implicit_conversions.md 中所述:整数常量可以隐式转换为任何能表示其值的整数或浮点类型,浮点常量可以隐式转换为任何能表示其值的浮点类型。
需要注意:这条规则禁止了例如i32与"无法在i32中表示的整数字面量"的比较——这种比较恒为真(tautological),毫无意义。设计文档同时指出,若该限制在实践(如模板代码中字面量有时在范围内)中造成问题,应重新评估此决定。
扩展性
用户自定义类型可以通过实现接口来扩展比较运算符的行为。本节描述的各种性质,是这类实现"应当"(should)满足的;这些性质通常不会被强制检查,但标准库在部分场景下可能检测到某些违反,且 checked generic 代码可能假定这些性质成立——一旦违反,将导致不可预期的行为。
相等(Equality):EqWith接口
通过实现EqWith与OrderedWith接口,可以为用户自定义类型提供比较运算符。
EqWith接口用于定义给定一对类型上==与!=运算符的语义:
interface EqWith(U: type) { fn Equal(self, u: U) -> bool; default fn NotEqual(self, u: U) -> bool { return not (self == u); } } constraint Eq { extend EqWith(Self); }给定x: T、y: U:
- 表达式
x == y调用x.(EqWith(U).Equal)(y); - 表达式
x != y调用x.(EqWith(U).NotEqual)(y)。
标准库中的实际接口定义(core/prelude/operators/comparison.carbon)与设计一致,唯一的差异是当前实现中NotEqual尚未声明为default且接口头部的Eq/Ordered命名约束以 TODO 形式标注待补——从源码结构看,prelude 版本比设计文档更早期或精简,属于实现逐步对齐设计的过程。
自定义类的相等实现示例:
class Path { private var drive: String; private var path: String; private fn CanonicalPath(self) -> String; impl as Eq { fn Equal(self, other: Self) -> bool { return (self.drive, self.CanonicalPath()) == (other.drive, other.CanonicalPath()); } } }这里Path通过元组把drive与规范化路径CanonicalPath()组合比较,实现语义上的路径相等(而非字符串形式的原始相等)。
使用like允许隐式转换
EqWith的重载选择不考虑可能的隐式转换。若希望在==重载的操作数中允许隐式转换,可以使用like运算符:
class MyInt { var value: i32; fn Value(self) -> i32 { return self.value; } } impl i32 as ImplicitAs(MyInt); impl like MyInt as EqWith(like MyInt) { fn Equal(self, other: Self) -> bool { return self.Value() == other.Value(); } } fn CompareBothWays(a: MyInt, b: i32, c: MyInt) -> bool { // OK,上面这个实现会被调用三次。 return a == a and a != b and b == c; }like让EqWith(like MyInt)同时覆盖了MyInt与其可隐式转换来源类型(此处是i32)的组合,a != b与b == c才得以成立。
覆盖NotEqual与 NaN 场景
NotEqual的行为可以独立于Equal被覆盖,以支持浮点 NaN 这类"既不相等、也不不相等"的值——两个值比较时两个函数都返回false:
impl like MyFloat as EqWith(like MyFloat) { fn Equal(self: MyFloat, other: MyFloat) -> bool { if (self.IsNaN() or other.IsNaN()) { return false; } return self.Representation() == other.Representation(); } fn NotEqual(self: MyFloat, other: MyFloat) -> bool { if (self.IsNaN() or other.IsNaN()) { return false; } return self.Representation() != other.Representation(); } }但EqWith的实现不应让Equal与NotEqual对同一对值同时返回true;此外,这些操作不应有可观察的副作用。
异构比较需双向定义
异构(heterogeneous)比较必须双向定义:
impl like MyInt as EqWith(like MyFloat); impl like MyFloat as EqWith(like MyInt);TODO:标准库应提供一个适配器(adapter),以简化反向比较的定义。
排序(Ordering):OrderedWith接口
OrderedWith接口用于定义给定一对类型上<、<=、>、>=四个运算符的语义:
choice Ordering { Less, Equivalent, Greater, Incomparable } interface OrderedWith(U: type) { fn Compare(self, u: U) -> Ordering; default fn Less(self, u: U) -> bool { return self.Compare(u) == Ordering.Less; } default fn LessOrEquivalent(self, u: U) -> bool { let c: Ordering = self.Compare(u); return c == Ordering.Less or c == Ordering.Equivalent; } default fn Greater(self, u: U) -> bool { return self.Compare(u) == Ordering.Greater; } default fn GreaterOrEquivalent(self, u: U) -> bool { let c: Ordering = self.Compare(u); return c == Ordering.Greater or c == Ordering.Equivalent; } } constraint Ordered { extend OrderedWith(Self); } // Ordering.Less < Ordering.Equivalent < Ordering.Greater。 // Ordering.Incomparable 与三者都不可比较。 impl Ordering as Ordered;TODO:在枚举类型的具体设计确定后修订上述定义。
给定x: T、y: U:
- 表达式
x < y调用x.(OrderedWith(U).Less)(y); - 表达式
x <= y调用x.(OrderedWith(U).LessOrEquivalent)(y); - 表达式
x > y调用x.(OrderedWith(U).Greater)(y); - 表达式
x >= y调用x.(OrderedWith(U).GreaterOrEquivalent)(y)。
一个按面积排序自定义类的完整示例:
class MyWidget { var width: i32; var height: i32; fn Size(self) -> i32 { return self.width * self.height; } // 控件默认按尺寸排序。 impl as Ordered { fn Compare(self, other: Self) -> Ordering { return self.Size().(Ordered.Compare)(other.Size()); } } } fn F(a: MyWidget, b: MyWidget) -> bool { return a <= b; }这里MyWidget只需实现一个Compare,四个关系运算符均由默认实现派生;self.Size().(Ordered.Compare)(other.Size())复用了内建Int的排序比较。
与EqWith一样,可以使用like运算符 允许比较调用时的隐式转换,且异构比较必须双向定义:
fn ReverseOrdering(o: Ordering) -> Ordering { return Ordering.Equivalent.(Ordered.Compare)(o); } impl like MyInt as OrderedWith(like MyFloat); impl like MyFloat as OrderedWith(like MyInt) { fn Compare(self, other: Self) -> Ordering { return Reverse(other.(OrderedWith(Self).Compare)(self)); } }反向实现中,MyFloat一侧通过Reverse把MyInt侧的Compare结果反转,从而保持排序的一致性。
可覆盖的默认实现与传递性
Less、LessOrEquivalent、Greater、GreaterOrEquivalent的默认实现可以被覆盖(例如实现一个更高效的版本)。此类覆盖的行为应遵循默认实现的语义,且OrderedWith实现的成员不应有可观察的副作用。
OrderedWith实现应当是可传递的(transitive)。给定V: type、U: OrderedWith(V)、T: OrderedWith(U) & OrderedWith(V)、a: T、b: U、c: V,则:
- 若
a <= b且b <= c,则a <= c;进一步地,若a < b或b < c任一成立,则a < c; - 若
a >= b且b >= c,则a >= c;进一步地,若a > b或b > c任一成立,则a > c; - 若
a与b等价,则a.Compare(c) == b.Compare(c);同理,若b与c等价,则a.Compare(b) == a.Compare(c)。
OrderedWith实现还应当在反转下保持一致(consistent under reversal)。即给定类型T与U,其中T impls OrderedWith(U)且U impls OrderedWith(T),以及值a: T、b: U:
- 若
a.(OrderedWith.Compare)(b)是Ordering.Greater,则b.(OrderedWith.Compare)(a)是Ordering.Less,反之亦然; - 否则,
a.(OrderedWith.Compare)(b)与b.(OrderedWith.Compare)(a)返回相同值。
值得注意的是,并不要求Ordered实现是全序、弱序或偏序。特别是浮点类型的实现三者都不是——因为 NaN 与自身既不小于、也不等价(NaN < NaN与NaN <= NaN均为假)。
TODO:标准库应提供一种方式,在 checked generic 中声明某个排序是弱序、偏序或全序,并支持请求这样的排序。
相等与排序的兼容性
并不要求实现OrderedWith的类型对也必须实现EqWith。但如果一对类型两者都实现了,那么x.(EqWith.Equal)(y)提供的相等关系,应当是x.(OrderedWith.Compare)(y) == Ordering.Equivalent所提供等价关系的细化(refinement)——即相等的值必然等价,但等价的值未必相等(例如排序忽略的次要字段)。
自定义结果类型
TODO:设计文档计划支持一种更低层的扩展机制,允许比较结果类型不是bool(例如返回Ordering或自定义枚举的三路比较)。
基本类型与数据类型的默认实现
除标准 Carbon 数值类型外,相等与关系比较对所有"数据"(data)类型也有定义:
- 元组(Tuples)
- 结构体类型(Struct types)
- 实现了标识其为数据类的接口的类
这些类型的关系比较提供字典序(lexicographical)排序。每种情况下,仅当所有元素类型都支持该比较时,比较才可用。
由于数据类之间的隐式转换可能重排字段(field reordering),数据类的默认实现在一般情形下不允许对参数做隐式转换,取而代之:
- 相等比较:允许任何两个具有相同无序字段名集合(unordered set of field names)的数据类进行比较,只要每对对应字段都有
EqWith实现。字段按左操作数中的出现顺序比较。 - 关系比较:允许任何两个具有相同有序字段名序列(ordered sequence of field names)的数据类进行比较,只要每对对应字段都有
OrderedWith实现。字段按顺序比较。
也就是说:相等只看"字段集合"是否相同,不关心字段顺序;而排序要求字段顺序也一致。这与设计上"相等是排序等价的细化"的理念一脉相承——重排字段不改变相等性,但会改变字典序。
元组之间的比较则允许任一侧(但不能两侧同时)的隐式转换。
开放问题(Open questions)
bool类型应被视为一种 choice 类型,因此当且仅当 choice 类型普遍支持相等与关系比较时,bool才应支持这些比较。该决定留给未来的提案处理。从当前源码看,bool的比较已先行实现在 core/prelude/operators/comparison.carbon(impl bool as EqWith(Self)),但关系比较尚未实现,与"留给未来提案"的开放状态一致。
备选方案(Alternatives considered)
设计过程中曾考虑并被否决或推迟的备选方案,详见对应提案:
- 备选符号(Alternative symbols)
- 链式比较(Chained comparisons)
- 像 C++ 一样转换操作数(Convert operands like C++)
- 提供三路比较运算符(Three-way comparison operator)
- 允许比较作为
not的操作数(Allow comparisons as the operand ofnot) - 将
OrderedWith改名为ComparableWith
其中"链式比较"被否决直接对应本文"非结合"一节;"像 C++ 一样转换操作数"被否决对应"至多转换一个操作数、杜绝有损中间类型"一节;而三路比较与自定义结果类型则是文档中"TODO"标注的未来方向。
参考资料
- 提案 #702:Comparison operators(本文设计文档的提案来源)
- 提案 #1178:Rework operator interfaces(重命名/重构运算符接口)
- 数据类默认比较相关 Issue #710(Default comparison for data classes,见设计文档 References 一节)
- 提案 #6710:
charredesign(char类型重构,对应Core.CharLiteral比较规则) - 标准库实现:core/prelude/operators/comparison.carbon、core/prelude/types/int.carbon、core/prelude/types/uint.carbon、core/prelude/types/float.carbon、core/prelude/types/char.carbon
- 相关设计:隐式转换、逻辑运算符、元组、类与结构体、泛型
like运算符
提示:Carbon 语言仍处于实验阶段(参见仓库 README.md),上述设计文档与标准库实现正在同步演进中;源码中标注的多个
TODO(如OrderedWith的Compare成员、混合浮点比较、Eq/Ordered命名约束)即体现了这一点,实际行为请以当前仓库代码为准。
【免费下载链接】carbon-langCarbon Language's main repository: documents, design, implementation, and related tools. (NOTE: Carbon Language is experimental; see README)项目地址: https://gitcode.com/GitHub_Trending/ca/carbon-lang
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考