ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Carbon 语言赋值语句提案深度解析:赋值仅限语句、复合赋值与自增自减的接口化设计

Carbon 语言赋值语句提案深度解析:赋值仅限语句、复合赋值与自增自减的接口化设计 Carbon 语言赋值语句提案深度解析赋值仅限语句、复合赋值与自增自减的接口化设计【免费下载链接】carbon-langCarbon Languages main repository: documents, design, implementation, and related tools. (NOTE: Carbon Language is experimental; see README)项目地址: https://gitcode.com/GitHub_Trending/ca/carbon-lang导读本文深入解析 Carbon 语言正式采纳的赋值语句设计提案对应仓库 proposals/p002511-assignment-statements.md。该提案确立了 Carbon 赋值语法与 C 家族语言的重要分水岭赋值只允许作为完整语句出现不允许作为子表达式同时提供复合赋值$与前自增/前自减/--并把全部运算符语义翻译为对接口interface成员的调用。读完本文你将掌握 Carbon 赋值语句的完整语法、接口化实现机制、与 C 的差异取舍以及提案背后对代码可读性、安全性、演进性的设计权衡。背景C 家族赋值运算符的历史包袱在 C 家族语言中存在三类赋值类运算符简单赋值variable value复合赋值variable $ value对任意二元运算符$而言其语义等价于variable variable $ value唯一区别是variable只被求值一次自增与自减variable与--variable表示variable 1与variable - 1而后置形式variable、variable--行为类似但返回variable的旧值在 C/C 中这些运算符与其他二元运算符一样可以作为更大表达式的子表达式使用。链式赋值也被支持且从右向左结合a b 1先把1赋给b再把该赋值的结果赋给a。在 C 中该结果通常是引用b的左值而在 C 中则是包含类型转换结果的右值。注意C/C 中所有其他运算符的结合方向与此相反。提案原文指出了这套机制长期积累的一系列已知问题赋值与比较的混淆如if (variable 3) { ... }这类写法极易误写。现实中已形成一种由编译器警告强制推动的事实约定在少数确实有意使用赋值的场合必须额外加括号写成if ((variable 3))。未排序修改与访问带来的未定义行为例如n a n;在 C/C 中就是未定义行为因为自增n的动作与其余计算包括赋值本身之间没有明确的顺序关系sequencing。C 后置自增的性能陷阱后置自增被期望返回变量的旧值这要求保留旧值副本只有operator能被内联时编译器才可能优化掉额外拷贝而这以编译器多做工作为代价。运算符重载不完整在 C 中重载运算符$不会自动获得对应的$导致要么补写更多代码要么运算符集合不完整。这些痛点直接构成了 Carbon 重新设计赋值语句的动机。提案核心赋值仅限语句Carbon 的提案给出了一个简洁而彻底的回答variable value;是简单赋值语句对除比较运算符以外的每个二元运算符$variable $ value;是复合赋值语句。特别地与只表示小于等于与大于等于比较绝不表示比较并赋值variable;与--variable;作为自增、自减语法被支持。由于它们只能是语句前置与后置没有区别后置自增/自减不被提供。所有这些操作都会被翻译为对接口成员的调用详见下文接口化实现一节。为什么禁止赋值作为子表达式提案的备选方案章节专门讨论了允许赋值作为子表达式这一选项。结论是不支持理由有三层该特性在 C 中实际效用非常有限却持续导致相等比较与赋值之间的混淆把赋值建模为语句更容易把赋值视为变量从未成形状态unformed state过渡到完全成形状态的转换点——这能防止这种状态转换发生在表达式求值过程中某个未完全排序的时间点若想消除语法混淆也可以借鉴 Python 的海象运算符variable : value允许子表达式赋值但 Carbon 目前选择暂不采纳以观察此类特性的实际需求强度。提案还指出若未来支持三表达式for语句for (init; cond; incr)可以考虑允许在incr项中放置赋值但当前 Carbon 并不支持这种for语法。对于确实需要赋值作为表达式的场景提案给出了代价可控的替代写法var a: i32; var b: i32; var c: i32; // ✅ 允许按待定提案 #2006 的规则简单赋值语句。 a 1; // 可能报错或告警b 处于未成形状态。 b.(Assign.Op)(1); // 尚未决定是否保证对 c 的赋值先于对 c 的读取。 a c.(Assign.Op)(1) c;即可以用直接的接口成员函数调用x.(Assign.Op)(y)或用 lambda 包裹赋值语句来实现类似效果后置自增a可用${var b: auto a; a; return b;}变换其中${...}是 Carbon 未来 lambda 语法的占位符。需要特别说明的是这类非赋值语句的替代写法在静态分析时大概率只会被视为捕获了x而不会被当作确定初始化了x因此 Carbon 设计可能包含的未成形检查等分析会对其采取保守处理。明确不支持的链式赋值作为赋值作为子表达式的特例链式赋值a b c 0;也被明确排除。提案指出可以限制链式赋值只适用于简单赋值而非a b - c * 2;这类混写但根据 leads issue #451 的决定Carbon 初期不支持链式赋值——这主要是因为缺乏足够有说服力的论据去证明引入特殊情况的复杂性是值得的若将来出现强有力的理由应当重新考虑。接口化实现从语法到接口成员调用提案的核心机制是把这些运算翻译为接口成员调用。结合当前仓库 core/prelude/operators/arithmetic.carbon 与 core/prelude/operators/bitwise.carbon 的源码可以看到这套接口体系的实际落地形态。复合赋值接口XxxAssignWith(Other)算术运算方面复合赋值接口采用fn Op(ref self, other: Other);的形式左操作数通过ref self原位修改// 加法复合赋值a b。 interface AddAssignWith(Other: type) { fn Op(ref self, other: Other); } // 减法复合赋值a - b。 interface SubAssignWith(Other: type) { fn Op(ref self, other: Other); } // 乘法、除法、取模依次类推。 interface MulAssignWith(Other: type) { fn Op(ref self, other: Other); } interface DivAssignWith(Other: type) { fn Op(ref self, other: Other); } interface ModAssignWith(Other: type) { fn Op(ref self, other: Other); }位运算方面bitwise.carbon 提供了对称的五个复合赋值接口interface BitAndAssignWith(Other: type) { fn Op(ref self, other: Other); } // a b interface BitOrAssignWith(Other: type) { fn Op(ref self, other: Other); } // a | b interface BitXorAssignWith(Other: type) { fn Op(ref self, other: Other); } // a ^ b interface LeftShiftAssignWith(Other: type) { fn Op(ref self, other: Other); } // a b interface RightShiftAssignWith(Other: type) { fn Op(ref self, other: Other); } // a b命名上遵循$的接口 $的接口 的接口这一直接对应关系AddWithAssign→AddAssignWith。源码中所有*With接口都统一用With后缀描述右操作数类型这与AddWith、LeftShiftWith等普通运算符接口的命名风格保持一致。自增自减接口Inc与Dec自增与自减是独立的一对接口// 自增a。 interface Inc { fn Op(ref self); } // 自减--a。 interface Dec { fn Op(ref self); }注意它们的Op同样接收ref self因为/--是语句而非表达式不存在返回旧值的问题。内建类型的实现证据在 core/prelude/types/int.carbon 中Int(N)类型为全部十个复合赋值接口提供了实现并通过ImplicitAs约束支持右操作数隐式转换impl forall [N: IntLiteral, U: ImplicitAs(Int(N))] Int(N) as AddAssignWith(U) { fn Op(ref self, other: Self) int.add_assign; } impl forall [N: IntLiteral, U: ImplicitAs(Int(N))] Int(N) as SubAssignWith(U) { fn Op(ref self, other: Self) int.ssub_assign; }这些实现以字符串字面量绑定到内置运算如int.add_assign、int.ssub_assign即语法层面的/-最终经接口调度落到对应的内建操作。Int(N)的Inc/Dec实现则演示了自增如何经由复合赋值与类型转换组合而来int.carbonimpl forall [N: IntLiteral] Int(N) as Dec { fn Op(ref self) { fn AsInt(n: IntLiteral) - Int(N) int.convert_checked; self - AsInt(1); } } impl forall [N: IntLiteral] Int(N) as Inc { fn Op(ref self) { fn AsInt(n: IntLiteral) - Int(N) int.convert_checked; self AsInt(1); } }这里self - AsInt(1);中的-又通过SubAssignWith接口完成形成自增/自减 → 复合赋值 → 接口成员的层层翻译链。源码注释还揭示了一个实现细节self - 1;本应可行但因IntLiteral as ImplicitAs(Int(N))的实现并非final导致字面量1到Int(N)的转换要延迟到运行时才尝试、此时常量值已丢失因此改用显式AsInt(1)调用int.convert_checked内建转换。uint.carbon 中的UInt(N)以及 cpp/int.carbon 中的CppCompat各整数类型也提供了同构的Inc/Dec实现。这些运算符接口通过 core/prelude/operators.carbon 统一导出export import library prelude/operators/arithmetic、prelude/operators/bitwise等作为Core预置库的一部分对用户类型开放任何类型都可以通过impl接入这套运算符体系。提案边界默认赋值语义不在本提案内提案明确声明本提案不定义类class默认获得的赋值语义。Leads issue #686 给出了一些相关规则但这些规则不属于本提案的范畴。读者在理解本提案时应注意这一边界——它只解决赋值的语法形式与运算符到接口的翻译机制而类如何默认支持赋值是独立的设计议题。设计理由与项目目标的对齐提案的 Rationale 章节逐条映射到 Carbon 的项目目标见 docs/project/goals.md语言工具与生态Language tools and ecosystem变量的值只可能在两种场合改变——取地址包括addr自参数隐式取地址时或执行赋值语句时。这使得在函数控制流中推断变量的值可能在何处改变变得更容易为静态分析提供便利。软件与语言演进Software and language evolution本方案是保守的未来可以平滑演进以支持赋值作为子表达式。易于阅读、理解和编写的代码Code that is easy to read, understand, and write子表达式中的赋值往往难以阅读和理解。在语言层面禁止它们可以避免潜在的混淆代价是让n arr[i];这类构造稍微难写一些同时由于$与$可以通过实现单个接口获得完整的运算符集合编写完整运算符集合变得更容易。实用安全与测试机制Practical safety and testing mechanisms把 C/C 中对if (a b)的正确性警告升级为语言层面的硬性规则从源头杜绝这类错误。与既有 C 代码的互操作与迁移Interoperability and migration from existing C code提供与 C 大体相同的符号集合、$、、--降低迁移成本代价是迁移时可能需要把子表达式中的赋值改写为独立语句。备选方案与关键决策提案详细记录了七组备选方案及放弃理由这些决策过程对理解设计的边界条件至关重要。一、允许赋值作为子表达式已被否决已在为什么禁止赋值作为子表达式一节详述效用有限、易与比较混淆、破坏状态转换的排序性。可选的:海象语法暂不采纳。二、允许链式赋值已被否决初始不支持原因是没有充分论据支撑引入特殊情况的复杂度有待将来重新评估。三、不提供自增自减已被否决可改用var 1;与var - 1;。但来自 C 家族语言的开发者会预期这些运算符存在且比更能直接传达在一维粒度空间中计数与导航的意图。四、把自增视为加 1的语法糖已被否决一种替代设计是把a;视为a 1;的语法糖正如a 1;是a a 1;的语法糖这会让浮点类型也获得自增自减与 C 一致。其潜在优点是字面量1有自己的类型类型不必为支持自增而整体支持加上任意整数因此非随机访问迭代器可以只提供it 1而不暴露非常量时间的it n且能通过类型检查的泛型约束如T:! Assign Add where i32 is ImplicitAs(.Self)可以同时保证a a 1;与a;均可行。但该方案被否决的原因在于自增自减的语义会被绑定到非常数值化的加 1概念上这对所有想表达更一般的移动到下一个值的类型并不合适浮点类型恰好加 1 不一定产生不同的数更有意义的是移动到下一个可表示的值。在 C 中浮点自增虽然被允许但实际使用极其罕见复数类型如高斯整数c;沿实轴方向移动一个单位显得很武断有理数类型与浮点类型类似恰好加 1不太可能是常见的导航操作。五、用$定义$已被否决与提案方向相反不把$定义为$加的组合而是把$定义为$加拷贝。C 的经验表明$常可比$实现得更高效就地操作、复用左操作数的已分配存储因此这可能是更好的默认。但提案选择不这样做理由包括本提案方向更符合程序员直觉——从 token 的形态和通常的教学方式看$由$与定义更自然在本提案规则下Add Assign约束与AddAssign约束因AddAssign基于Add和Assign的 blanket 实现而实际等价——约束一个类型同时提供Add和Assign即可使用这是理想结果若默认方向反转则无法达成若采用反向规则仍需要两个独立的impl才能实现在$左侧允许隐式转换、在$左侧不允许的区分存在$比拷贝后$实现更高效的情况——对大类型用$实现$需要对目的地做两次遍历而非一次常数因子性能会变差。六、不允许重载的行为已被否决一种激进的固定语义是表达式总是按以下三步执行——用右操作数初始化一个左操作数类型的值销毁左操作数把新值移入左操作数。但这会剥夺类型设计的灵活性并可能损害性能例如左操作数持有可复用的缓冲区时本可直接复用存储。同时这会显著偏离 C其中部分类型正利用了这种额外灵活性损害互操作与迁移。七、把左侧视为模式已被否决一种更激进的语法设想是允许在左侧使用模式pattern语法例如fn GCD(var a: i32, var b: i32) - i32 { if (b a) { // 交换 a 和 b。 (a, b) (b, a); } while (a ! 0) { // 同时计算 b % a 与 a再赋给 a、b。 (a, b) (b % a, a); } return b; }问题在于这是对模式语法的一种全新解释现有模式语法没有任何机制能赋值给已存在的变量——按当前机制(a, b)匹配(b % a, a)会变成把a与b % a比较、把b与a比较。提案认为这种新颖性不足以抵偿该功能带来的价值因此未采纳。八、接口的备选命名方案提案花费了相当篇幅讨论接口命名最终选定的方案是Assign、AssignWith、OpAssign、OpAssignWith其中Op为具体运算符名如Add。该方案的优点统一用With后缀描述右侧类型接口名与词法运算符直接对应——$的接口名就是$的接口名加上的接口名名称中的词序描述了操作被执行的顺序先Op后Assign复合赋值接口会与对应运算符按字母序排列在一起而不是与赋值排在一起便于在排序列表中查找与 Rust 的选择一致。被否决的候选命名包括AssignFrom英文读起来更自然但破坏With的一致性提案表示若AssignWith持续引发担忧可重新考虑、AssignOp/AssignOpWith、AssignGiven/OpAssignGiven、以及InPlaceOp/InPlaceOpWith与AssignFrom的组合。最终结论是尽管个别名称读起来不完全自然但一致性优先——正如先前对LeftShiftWith的决策一样这套命名集合仍是整体最佳选择。从提案到实现现状与演进值得注意的是本提案编号 p002511是最初的赋值语句设计而当前仓库中的接口体系如AddAssignWith、Inc、Dec是后续设计演进包括 proposals/p001178-rework-operator-interfaces.md 对运算符接口的重构等的结果。读者在阅读时应意识到提案中的接口名如AddAssign与仓库当前源码中的命名如AddAssignWith存在演进差异但赋值语句翻译为接口成员调用这一核心机制自始至终被保留并落地。对于希望亲手验证的读者可以通过仓库中的工具链测试代码验证这些接口的实际行为例如Int(N)的AddAssignWith、Inc、Dec实现见 core/prelude/types/int.carbon以及预置库对全部运算符库的导出见 core/prelude/operators.carbon。总结Carbon 的赋值语句设计以赋值仅限语句为纲一次性解决了 C/C 赋值体系长期存在的混淆、未定义行为、性能陷阱与运算符重载不完整问题、$、/--全部成为语句形态的语法糖其语义经由Assign/AssignWith/OpAssign/OpAssignWith一族接口翻译为成员调用既保持了与 C 的符号级兼容以利于迁移又通过语言规则取代了依赖编译器警告的编码约定同时为未成形状态检查等安全机制预留了清晰的状态转换点。这套设计的取舍逻辑——从链式赋值、子表达式赋值到自增语义的逐一否决与论证——为理解 Carbon保守演进、安全优先的语言哲学提供了一个极具代表性的案例。【免费下载链接】carbon-langCarbon Languages 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),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进