ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Solidity 运算符完全指南:类型提升、三元表达式、delete 与优先级详解

Solidity 运算符完全指南:类型提升、三元表达式、delete 与优先级详解 Solidity 运算符完全指南类型提升、三元表达式、delete 与优先级详解【免费下载链接】soliditySolidity, the Smart Contract Programming Language项目地址: https://gitcode.com/GitHub_Trending/so/solidity导读本文以 Solidity 官方文档 docs/types/operators.rst 为主体系统讲解智能合约开发中最容易踩坑的运算符语义不同整数类型混合运算时的共同类型common type确定机制、字面量的**移动类型mobile type**规则、三元运算符的隐蔽溢出陷阱、复合赋值与自增自减的区别以及delete对值类型/引用类型/映射/结构体的不同行为。文章同时结合当前仓库的 libsolidity/ast/Types.cpp 源码与 docs/types/operator-precedence-table.rst 完整优先级表帮助你写出类型安全、行为可预期且不会因溢出意外回滚的合约代码。一、混合类型运算共同类型Common Type确定机制算术运算与位运算并不要求两个操作数类型完全相同。例如可以计算y x z其中x是uint8、z是uint32。此时编译器必须确定两件事运算在哪种类型下执行——这直接关系到溢出行为运算结果的类型。Solidity 采用如下三步机制确定这也是文档中定义的规则如果右操作数的类型可以隐式转换为左操作数的类型则使用左操作数的类型否则如果左操作数的类型可以隐式转换为右操作数的类型则使用右操作数的类型否则该运算不被允许编译报错。这段规则的源码实现位于 Types.cpp 的Type::commonTypeType const* Type::commonType(Type const* _a, Type const* _b) { if (!_a || !_b) return nullptr; else if (_a-mobileType() _b-isImplicitlyConvertibleTo(*_a-mobileType())) return _a-mobileType(); else if (_b-mobileType() _a-isImplicitlyConvertibleTo(*_b-mobileType())) return _b-mobileType(); else return nullptr; }可以看到判断的核心是isImplicitlyConvertibleTo隐式可转换性。结果类型与运算执行类型相同唯一的例外是比较运算符——比较运算的结果永远是bool无论参与比较的操作数是什么类型。二、字面量与移动类型Mobile Type当其中一个操作数是数字字面量时它不会直接以字面量身份参与运算而是先被转换为它的移动类型mobile type——即能容纳该值的最小类型。这里有一个关键细节相同位宽下无符号类型被认为比有符号类型“更小”优先选择。例如字面量255的移动类型是uint8而-1的移动类型是int8因为uint8无法容纳负数。如果两个操作数都是字面量运算以“实际上无限精度”的方式求值——表达式会被计算到不损失任何精度的程度直到结果与某个非字面量类型一起使用时才发生截断。移动类型的源码实现在 Types.cppType const* RationalNumberType::mobileType() const { if (!isFractional()) return integerType(); // 整数 - 最小的可容纳整型 else return fixedPointType(); // 小数 - 定点类型 }其中integerType()会根据值的正负与大小选择有符号/无符号的最小型见 Types.cpp。三、特殊运算符**、、的结果类型绝大多数二元运算符的结果类型等于运算执行时的共同类型但有三个例外**幂运算左移右移这三个运算符始终使用左操作数的类型进行运算并作为结果类型。源码依据见 Types.cpp 的IntegerType::binaryOperatorResultif (TokenTraits::isShiftOp(_operator)) { // Shifts are not symmetric with respect to the type if (isValidShiftAndAmountType(_operator, *_other)) return this; // 移位运算结果类型取左操作数 ... } else if (Token::Exp _operator) { ... return this; // 幂运算结果类型取左操作数底数 }同时该实现还隐含了另一个重要约束移位操作的右操作数移位量必须是无符号类型用有符号类型作为移位量会产生编译错误幂运算的指数同样不允许是有符号类型也不允许是负数或小数。另外注意与算术运算不同移位运算永远不做溢出检查结果总是被截断以匹配左操作数的类型。例如uint8(255) 8的结果是uint8类型高位移出的位会被丢弃详见 value-types.rst 中关于 Shifts 的说明。四、三元运算符表达式语义与两个经典陷阱三元运算符的形式为expression ? trueExpression : falseExpression它根据主表达式expression的求值结果只对其后两个表达式中的一个进行求值。其类型规则有两个要点三元运算符的结果没有有理数rational类型即使三个操作数都是有理数字面量也是如此结果类型由两个分支操作数按照与普通二元运算相同的方式先转换为移动类型确定。这带来了两个非常经典的“反直觉”后果是面试和审计中常见考点陷阱一255 (true ? 1 : 0)会因溢出而回滚uint8 x 255; uint8 y x (true ? 1 : 0); // 回滚原因是(true ? 1 : 0)的结果类型是uint8字面量1和0的移动类型都是uint8这迫使整个加法也在uint8下执行而256超出了uint8的取值范围[0, 255]触发算术溢出检查交易回滚。如果写成255 1两个字面量会以无限精度求值得256再根据上下文例如赋值给uint16决定是否合法。陷阱二1.5 1.5合法1.5 (true ? 1.5 : 2.5)不合法1.5 1.5是两个有理数字面量的运算以无限精度求值结果3.0是合法的有理数表达式只有最终值才被关注1.5 (true ? 1.5 : 2.5)中括号内的三元表达式结果不是有理数类型而是一个定点数fixed point类型需要一个“分数有理数转整数”的转换而这一转换目前是被禁止的因此编译失败。五、复合赋值与自增/自减运算符如果a是一个LValue左值即可被赋值的变量或表达式以下运算符可以作为简写使用运算符等价形式a ea a ea - ea a - ea * ea a * ea / ea a / ea % ea a % ea | ea a \| ea ea a ea ^ ea a ^ ea ea a ea ea a e自增自减有两组语义返回值的时机不同后缀形式a、a--等价于a 1/a - 1但表达式本身的值为a变化前的值前缀形式a、--a对a的影响相同但返回的是a变化后的值。例如uint x 5; uint y x; // y 5, x 6 uint z x; // z 7, x 7六、delete运算符按类型区分的重置语义delete a将a重置为该类型的初始值。理解delete的关键是它本质上是一次对a的赋值——即把一个新的对象存入a。这一点在a是引用类型变量时尤为重要delete a只会重置a本身而不会重置它之前所引用的对象。6.1 各类类型的行为整数等值类型delete a等价于a 0动态数组赋值为长度为 0 的动态数组静态数组赋值为长度不变、所有元素均重置为初始值的数组delete a[x]删除数组索引x处的元素其他元素和数组长度保持不变。这意味着数组中间会留下一个“空洞”如果频繁删除元素mapping 通常是更好的选择结构体将所有成员重置。但映射成员不会被重置映射的键是任意的、通常不可枚举delete会递归重置所有非映射成员遇到映射成员则跳过映射delete a对映射本身无效但delete a[x]可以删除键x对应的值。6.2 官方示例别名与引用语义文档给出了一个完整的可运行示例operators.rst 原文// SPDX-License-Identifier: GPL-3.0 pragma solidity 0.4.0 0.9.0; contract DeleteExample { uint data; uint[] dataArray; function f() public { uint x data; delete x; // 将 x 置为 0不影响 data delete data; // 将 data 置为 0不影响 x uint[] storage y dataArray; delete dataArray; // 将 dataArray.length 置为 0但由于 uint[] 是复杂对象 // y 作为该 storage 对象的别名同样受到影响 // 反过来delete y 是无效的因为指向 storage 对象的局部变量 // 只能由已存在的 storage 对象赋值而来。 assert(y.length 0); } }这个例子精妙地展示了两点值类型uint的delete只影响被删除的变量本身别名互不影响引用类型uint[] storage的delete修改的是底层 storage 对象因此所有指向该对象的别名如y都会观察到变化——这正是“delete真的表现为一次赋值”的含义。同时delete y不合法因为局部变量指向 storage 对象的引用只能从已存在的 storage 对象赋值得到。七、运算符优先级完整表Solidity 运算符按优先级从高到低共分 15 级内容完整取自 docs/types/operator-precedence-table.rst高优先级先求值优先级说明运算符1后缀自增、自减,--new 表达式new typename数组下标array[index]成员访问object.member函数式调用func(args...)括号(statement)2前缀自增、自减,--一元负号-一元运算delete逻辑非!按位取反~3幂运算**4乘、除、取模*,/,%5加、减,-6按位移位,7按位与8按位异或^9按位或\|10不等式比较,,,11等式比较,!12逻辑与13逻辑或\|\|14三元运算符conditional ? if-true : if-false赋值运算符,\|,^,,,,,-,*,/,%15逗号运算符,几个需要注意的要点赋值运算符包括复合赋值与三元运算符同处第 14 级低于所有算术、位、逻辑与比较运算因此a b c中先算b c再赋值delete是一元运算符优先级第 2 级远高于它常常搭配使用的下标运算第 1 级中的[]但下标是后缀操作先于delete应用因此delete a[x]的语义是先定位a[x]再删除比较运算符第 10 级优先级高于等式运算符第 11 级因此a b c d按(a b) (c d)解析。八、结合源码的实战要点溢出、移位与取模为了让运算结果符合预期以下语义细节值得结合整型文档与 Types.cpp 源码一起掌握默认所有算术都是 checked带检查模式运算结果超出类型范围会通过失败断言回滚可用unchecked { ... }切换为 wrapping回绕模式。因此混合类型运算的“共同类型”选择直接决定了是否会溢出回滚——这也是文档强调类型确定机制“在溢出情况下很重要”的原因。移位不做溢出检查结果总是被截断以匹配左操作数类型源码中isValidShiftAndAmountType只校验类型合法性不校验范围。除零与取模零无法被unchecked豁免它们触发Panic错误。唯一会发生溢出的除法是type(int).min / (-1)。整数除法向零取整int256(-5) / int256(2) int256(-2)。取模结果的符号与左操作数一致int256(-5) % int256(2) int256(-1)且恒有a % n -(-a % n)对负数a。幂运算**的指数必须为无符号类型源码 Types.cpp 对 signed 指数直接报错结果类型等于底数类型0**0在 EVM 中被定义为1。结语运算符是 Solidity 语法中表面简单、实则细节密集的部分。混合类型运算的“共同类型”机制、字面量的移动类型转换、三元运算符的非有理数结果、delete的引用别名语义以及严格的优先级顺序共同决定了合约的计算行为。建议在实际编写合约时显式声明类型避免依赖隐式转换在涉及边界值的场景主动用type(X).min/type(X).max校验范围对复杂 storage 结构的清理优先考虑 mapping 或自定义的pop式结构避免delete在数组中留下空洞。如需进一步了解整型的全部运算符语义与 unchecked 模式细节可继续阅读 docs/types/value-types.rst 与仓库中的类型系统实现 libsolidity/ast/Types.cpp。【免费下载链接】soliditySolidity, the Smart Contract Programming Language项目地址: https://gitcode.com/GitHub_Trending/so/solidity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表