拓冰建站拓冰建站
首页 / 资讯中心 / 正文

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 语言设计文档 docs/design/assignment.md 系统讲解赋值语句的语法与语义简单赋值、$复合赋值以及/--自增自减的规则、与 C 的异同以及如何通过标准库接口为自定义类型提供这些运算符。文中将结合 core/prelude/operators/arithmetic.carbon、core/prelude/operators/bitwise.carbon 等前奏库源码和 proposals/p002511-assignment-statements.md 提案带你理解 Carbon 为什么把赋值限定为完整语句、如何利用(ref self: Self)参数保证赋值目标可修改、以及泛型默认实现如何让$自动由$与推导出来。注意Carbon 语言仍处于实验阶段参见 README.md本文描述的是当前仓库中的设计文档所规定的语义部分内容如元组、结构体的赋值规则在设计文档中仍标记为 TODO。赋值语句的总体设计赋值是命令式编程的基石。Carbon 在语法上延续了 C 家族的传统提供三类赋值类运算符简单赋值variable value;复合赋值variable $ value;其中$是某个二元运算符算术或位运算自增与自减variable;与--variable;基本用法如下来自 assignment.md 的 Overview 部分var a: i32 5; a 6;对于每个二元算术或位运算运算符$Carbon 都提供对应的复合赋值$执行原地操作// 等价于 a a 1; a 1; // 等价于 a a 3; a 3;此外还提供自增与自减// 等价于 a a 1; a; // 等价于 a a - 1; --a;赋值只能作为完整语句与 C/C 最大的不同在于简单赋值、复合赋值、自增、自减这四类运算符只能作为完整语句使用不能作为其他运算符的子表达式即使加上括号也不行var n: i32; // 错误不允许把赋值作为子表达式。 if (F() and (n GetValue()) 5) { }这一限制在提案 p002511-assignment-statements.md 中被正式确立。提案中列举了 C/C 中赋值表达式混入子表达式带来的一系列问题赋值与比较混淆例如if (variable 3) { ... }这类代码极难阅读业界甚至形成了用额外括号规避编译警告的惯例if ((variable 3))。未定序的修改与访问例如n a n;在 C/C 中是未定义行为因为自增n与表达式中其他运算的次序不确定。后置自增的性能陷阱C 的后置自增需要保留旧值可能带来额外拷贝即使编译器内联优化也增加了编译器负担。Carbon 选择把赋值建模为语句而非表达式带来的好处包括可以从语言层面根除if (a b)这类拼写错误不再依赖编译器的 warning而是语言规则本身就不允许同时赋值作为语句也天然成为变量从未成形状态unformed state过渡到完全成形状态的清晰节点避免变量状态转换发生在表达式求值的非定序中间点。作为代价n arr[i];这类写法需要改写为更啰嗦的语句序列——这是设计团队在 项目目标 中权衡后接受的取舍。语法约束左侧操作数必须可修改这些运算符的操作数可以是任意表达式但第一个操作数赋值目标必须是可修改的因为它会被传给一个(ref self: Self)参数。ref self意味着函数接收的是对象的引用而非值拷贝从而可以在原地修改对象。这一约束排除了大多数表达式形式只允许以下几类var绑定的名字指针解引用dereference of a pointer产生可修改结果的数组下标array indexing成员访问命名字段且该对象本身是上述表达式之一。换句话说5 x;、(a b) c;这类写法在语法层面就被拒绝。测试 toolchain/check/testdata/operators/overloaded/fail_assign_non_ref.carbon 即用于验证把不可修改的表达式作为赋值目标这类错误场景。简单赋值语义与初始化精确对应简单赋值语句的设计目标是精确镜像初始化的语义。下面两段代码在语义上应当等价前提是二者都合法// 声明并初始化。 var v: T init;// 声明与初始化分离。 // 要求 T 具有未成形状态unformed state。 var v: T; v init;这种等价性并非由编译器强制保证但在对象处于未成形状态时运行赋值函数是可选的——正如运行析构函数是可选的。如果赋值函数没有被运行对象将直接从右侧操作数初始化。不过类型仍然必须实现AssignWith赋值才被允许。设计文档给出了一个围绕返回对象时避免额外拷贝的示例class C { ... } fn F() - C { returned var c: C {...}; // 这里的 c 在第一次调用 F() 时是 x。 // 这里的 c 在第二次调用 F() 时可以是 y。 return var; } fn G() { var x: C F(); var y: C; y F(); }这个示例展示了returned var与返回值语义的配合F()可以直接在调用者的存储x或y中构造返回值。对于var y: C;这种未成形对象编译器可以选择跳过赋值函数、直接就地初始化y从而省去一次多余的写入。这一点与 values.md 中介绍的初始化表达式initializing expressions模型一致函数返回可以在调用者的存储中直接初始化对象。复合赋值语义语法糖与移动到下一个值a $ b;的定位是a a $ b;的语法糖但有两点例外考量类型可能能为复合赋值形式提供比未组合形式更高效的实现原地操作、复用左操作数已分配的内存。类型可能无法或不愿意提供未组合形式。例如创建一个新实例需要额外的资源上下文对象、分配器而这些资源在复合赋值场景下不可用。语法糖通过$的默认实现来实现见后文。/--不是加 1 的语法糖与 C 不同a;和--a;不是a a 1;与a a - 1;的简单语法糖。Carbon 将这两个运算符解释为移动到下一个值与移动到上一个值对于链表迭代器这类类型移动是有意义且可用的但加一个整数未必是理想操作对于有理数这类类型加 1有意义但移动到下一个值反而未必有意义。因此/--的可用性与整数加法的可用性彼此独立。这也解释了为什么浮点类型不提供/--对浮点数加 1 不一定会产生不同的数而移动到下一个可表示值是另一个有意义的概念提案 p002511-assignment-statements.md 的 Treat increment as syntactic sugar for adding 1 一节对此有详细讨论。因为赋值是语句而非表达式Carbon 中没有前置/后置自增之分也不提供后置自增。内置类型的赋值行为对于内置类型Carbon 提供了开箱即用的赋值语义简单赋值整数类型、浮点类型、bool、指针类型都支持。右操作数会被隐式转换为左操作数的类型转换后的值替换左操作数的值。复合赋值$整数和浮点类型对每个受支持的运算符$自动提供经由默认实现机制见下节。自增/自减整数类型的n;、--n;分别等价于n 1;、n - 1;浮点类型不提供/--。在仓库的 prelude 源码中这些内置实现清晰可见。例如 core/prelude/types/int.carbon 中为Int(N)定义了全部 9 个复合赋值接口的实现每个都映射到内建指令impl forall [N: IntLiteral, U: ImplicitAs(Int(N))] Int(N) as AddAssignWith(U) { fn Op(ref self, other: Self) int.sadd_assign; } impl forall [N: IntLiteral, U: ImplicitAs(Int(N))] Int(N) as LeftShiftAssignWith(U) { fn Op(ref self, other: Self) int.left_shift_assign; }可以看到整数的使用int.sadd_assign内建指令、使用int.left_shift_assign等且右操作数允许任何ImplicitAs(Int(N))的类型这为整数字面量的隐式转换提供了通道。浮点类型在 core/prelude/types/float.carbon 中只提供了AddAssignWith、DivAssignWith、MulAssignWith、SubAssignWith四个复合赋值实现无ModAssignWith也无Inc/Dec印证了设计文档中浮点类型不提供/--的规则final impl forall [N: IntLiteral] Float(N) as AddAssignWith(Self) { fn Op(ref self, other: Self) float.add_assign; }C 互操作兼容类型CppCompat.Long32、CppCompat.Long64、CppCompat.LongLong64也在 core/prelude/types/cpp/int.carbon 中提供了对应的全套复合赋值实现保证迁移自 C 的代码语义一致。元组、结构体、choice 类型与 data class关于元组、结构体struct、choice 类型以及 data class 的赋值规则设计文档目前标注为TODO并引用了 leads issue #686: Operation order in struct/class assignment/initialization该 issue 讨论结构体/类赋值与初始化中的操作次序问题。相关语义仍在设计中本文不做臆测。可扩展性为自定义类型提供赋值运算符用户自定义类型可以通过实现标准库提供的一系列接口来定义赋值运算的语义。内置类型也通过这些接口的实现获得上文所述语义。这是 Carbon 的运算符即接口哲学的直接体现——运算符会被改写成对接口方法的调用而不是像 C 那样依赖自由函数重载详见 重载运算符相关设计 与提案 p001144-generic-details-11-operator-overloading.md。简单赋值接口AssignWith// 简单 。 interface AssignWith(U: type) { fn Op(ref self, other: U); } constraint Assign { extend AssignWith(Self); }给定var x: T与y: U语句x y;会被改写成x.(AssignWith(U).Op)(y);即一次限定成员调用。注意Op接收ref self因此改写后的调用会修改x本身。constraint Assign是非参数化版本供泛型约束中书写T:! Assign使用。算术复合赋值接口族// 复合 。 interface AddAssignWith(U: type) { fn Op(ref self, other: U); } constraint AddAssign { extend AddAssignWith(Self); } // 复合 -。 interface SubAssignWith(U: type) { fn Op(ref self, other: U); } constraint SubAssign { extend SubAssignWith(Self); } // 复合 *。 interface MulAssignWith(U: type) { fn Op(ref self, other: U); } constraint MulAssign { extend MulAssignWith(Self); } // 复合 /。 interface DivAssignWith(U: type) { fn Op(ref self, other: U); } constraint DivAssign { extend DivAssignWith(Self); } // 复合 %。 interface ModAssignWith(U: type) { fn Op(ref self, other: U); } constraint ModAssign { extend ModAssignWith(Self); } // 自增 。 interface Inc { fn Op(ref self); } // 自减 --。 interface Dec { fn Op(ref self); }给定var x: T与y: U改写规则如下x y;→x.(AddAssignWith(U).Op)(y);x - y;→x.(SubAssignWith(U).Op)(y);x * y;→x.(MulAssignWith(U).Op)(y);x / y;→x.(DivAssignWith(U).Op)(y);x % y;→x.(ModAssignWith(U).Op)(y);x;→x.(Inc.Op)();--x;→x.(Dec.Op)();注意Inc/Dec不接收参数语义由实现者决定移动到下一个/上一个值。在 prelude 中这些接口实际定义于 core/prelude/operators/arithmetic.carbon与设计文档一一对应// 加法a b。 interface AddWith(Other: type) { let Result: type; fn Op(self, other: Other) - Result; } // 带赋值的加法a b。 interface AddAssignWith(Other: type) { fn Op(ref self, other: Other); } // 自增a。 interface Inc { fn Op(ref self); }源码中还保留了 TODO 注释说明设计文档规定的default let Result: type Self与各非With命名约束如constraint Add尚未实现——这属于实验阶段正常的渐进落地。位运算与移位复合赋值接口族// 复合 。 interface BitAndAssignWith(U: type) { fn Op(ref self, other: U); } constraint BitAndAssign { extend BitAndAssignWith(Self); } // 复合 |。 interface BitOrAssignWith(U: type) { fn Op(ref self, other: U); } constraint BitOrAssign { extend BitOrAssignWith(Self); } // 复合 ^。 interface BitXorAssignWith(U: type) { fn Op(ref self, other: U); } constraint BitXorAssign { extend BitXorAssignWith(Self); } // 复合 。 interface LeftShiftAssignWith(U: type) { fn Op(ref self, other: U); } constraint LeftShiftAssign { extend LeftShiftAssignWith(Self); } // 复合 。 interface RightShiftAssignWith(U: type) { fn Op(ref self, other: U); } constraint RightShiftAssign { extend RightShiftAssignWith(Self); }给定var x: T与y: U改写规则x y;→x.(BitAndAssignWith(U).Op)(y);x | y;→x.(BitOrAssignWith(U).Op)(y);x ^ y;→x.(BitXorAssignWith(U).Op)(y);x y;→x.(LeftShiftAssignWith(U).Op)(y);x y;→x.(RightShiftAssignWith(U).Op)(y);这些接口同样能在 prelude 中找到对应实现见 core/prelude/operators/bitwise.carbon。其命名规律是BitAndWith/BitAndAssignWith成对出现Assign后缀直接对应赋值语义。默认实现$自动由$与推导当一个类型同时提供了赋值与二元运算符$使a a $ b;合法时Carbon 会自动提供一个默认的$实现使a $ b;合法且与a a $ b;含义相同。这一默认机制由OpAssignWith(U)的参数化实现完成定义于AssignWith与OpWith之上impl forall [U: type, T: OpWith(U) where .Self impls AssignWith(.Self.Result)] T as OpAssignWith(U) { fn Op(ref self, other: U) { // 这里 $ 是 OpWith 所描述的运算符。 *self *self $ other; } }其约束逻辑是T必须满足OpWith(U)存在二元运算$结果类型为.Result且.Self即T实现了AssignWith(.Self.Result)能把这个结果赋回自身。这样x $ y就等价于把x $ y的结果赋回x。泛型实现还带来一个实际好处在泛型约束中Add Assign约束由于AddAssign的 blanket 实现效果上等价于AddAssign约束——即只要约束一个类型同时提供Add与Assign就可以放心使用。提供更高效的覆盖实现如果类型存在更高效的复合赋值方式可以提供一个更具体的impl覆盖默认实现。设计文档以字符串类型为例impl like MyString as AddWith(like MyString) { // 分配新内存并执行加法。 } impl MyString as AddAssignWith(like MyString) { // 尽可能复用已有存储。 }这里AddWith版本每次都分配新内存而AddAssignWith版本可以复用左操作数已有的缓冲区避免反复分配。这正是设计文档强调的复合赋值允许提供比未组合形式更高效的实现的落地方式也是 C 经验中$常可高效实现的原因参见提案 p002511-assignment-statements.md 中Define$in terms of$一节的讨论——设计团队专门评估过反向定义的可能性最终出于直觉一致性与泛型约束便利性选择了由$与定义$的方向。编译器的落地与测试证据赋值语句的接口化设计在工具链中已有对应实现与测试检查阶段type checktoolchain/check 中的handle_expr_statement.cpp、运算符处理逻辑负责将x y、x y、x等语句改写成对AssignWith、AddAssignWith、Inc等接口方法的调用。测试语料toolchain/check/testdata/operators/overloaded/ 目录下存放了大量针对重载运算符的测试文件例如add.carbon的重载测试fail_assign_non_ref.carbon验证把非引用不可修改表达式当作赋值目标的错误fail_no_impl.carbon 与 fail_no_impl_for_arg.carbon验证缺少接口实现时的诊断。lower 阶段toolchain/lower/testdata/operators/increment.carbon 验证的自增操作在代码生成阶段的处理。这些测试文件连同 prelude 中的接口定义共同构成赋值语句 接口方法调用这条设计在编译器中的可验证闭环。被否决的备选方案设计文档末尾列出了在提案 p002511-assignment-statements.md 讨论过程中被否决的备选方案文档中以锚点链接形式给出细节允许把赋值作为子表达式否决理由是与比较运算符混淆风险高、难以阅读、使未成形状态转换难以定序未来若确有需求可考虑 Python 风格的海象运算符:但当前不推进。允许链式赋值a b c 0leads issue #451 决定初始不支持链式赋值。不提供自增/自减否决——C 家族开发者会期待这两个运算符且它们能更直接地表达计数/在一维粒度空间导航的语义。把自增当作加 1的语法糖否决——会把语义绑定到数值化的加 1上不适合迭代器、浮点、有理数等类型的移动到下一个值语义。用$反过来定义$否决——由$和定义$更符合直觉与教学惯例且能让Add Assign约束直接支持反向定义还会损害大类型上$的单遍实现效率。不允许重载的行为否决——会失去复用左操作数缓冲等性能优化机会也与 C 的迁移/互操作目标冲突。把左侧当作模式pattern否决——现有模式语法没有赋值给已有变量的机制(a, b) (b % a, a)会被理解为模式匹配比较而非赋值语义新颖性不值得为此引入。为接口使用不同命名讨论了AssignFrom、AssignOp、AssignGiven、InPlaceOp等候选最终基于With后缀的一致性、与词法运算符的直接对应关系$的接口 $的接口 的接口、以及与 Rust 的选择一致保留了AssignWith/OpAssignWith体系。参考与延伸阅读设计文档docs/design/assignment.md本文主体提案proposals/p002511-assignment-statements.md赋值语句的完整设计与备选方案论证算术运算符设计docs/design/expressions/arithmetic.md位运算与移位设计docs/design/expressions/bitwise.mdprelude 接口定义core/prelude/operators/arithmetic.carbon、core/prelude/operators/bitwise.carbon内置类型实现core/prelude/types/int.carbon、core/prelude/types/uint.carbon、core/prelude/types/float.carbon语言总览docs/design/README.md其中 Variables 一节 展示了i 3;、i;、i 2;在真实函数中的用法【免费下载链接】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),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门