05.运算操作符
算术运算符
比较运算符
逻辑运算符/关系运算符
赋值运算符
条件运算符/三元运算符
位运算符
delete
unchecked
两个类型不一样的操作数,也可以进行算术和位操作运算。
例如,你可以计算 y = x + z ,其中 x 是
uint8, z 是int32类型。 在这些情况下,将使用以下机制来确定运算结果的类型(这在溢出的情况下很重要)。如果右操作数的类型可以隐含地转换为左操作数的类型的类型,则使用左操作数的类型。
如果左操作数的类型可以隐含地转换为右操作数的类型的类型,则使用右操作数的类型。
否则,该操作不被允许。
如果其中一个操作数是一个常量数字,会首先被转换为能容纳该值的最小的类型 (相同位数时,无符号类型被认为比有符号类型 “小”)。 如果两者都是常量数字,则以任意的精度进行计算。
操作符的结果类型与执行操作的类型相同,除了比较运算符,其结果总是 bool。
运算符 **(幂), << 和 >> 使用左边操作数的类型来作为运算结果类型。
1️⃣ 算术运算符
+,-,*,/,%(取余,取模),++(递增),--(递减),+=(加法赋值),-=(减法赋值)**(次方)
不废话,直接上代码;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract Test {
uint256 public a = 5;
uint256 public b = 2;
uint256 public c1 = a + b;
uint256 public c2 = a - b;
uint256 public c3 = a * b;
uint256 public c4 = a / b;
uint256 public c5 = a % b;
// uint256 public c6 = a++; // 会影响a的值
function increment1() public view returns (uint256) {
uint256 temp = a;
return temp++;
}
function increment2() public view returns (uint256) {
uint256 temp = a;
return ++temp;
}
function reduce1() public view returns (uint256) {
uint256 temp = a;
return temp--;
}
function reduce2() public view returns (uint256) {
uint256 temp = a;
return --temp;
}
function plusAssign() public view returns (uint256) {
uint256 temp = a;
return temp += 2;
}
function minusAssign() public view returns (uint256) {
uint256 temp = a;
return temp -= 2;
}
function test1() public view returns (uint256) {
return b**3;
}
}
unchecked
默认情况下,算术运算都会进行溢出检查,但是也可以禁用检查,可以通过 unchecked block 来禁用检查,此时会返回截断的结果。
function f(uint a, uint b) pure public returns (uint) {
// 减法溢出会返回“截断”的结果
unchecked { return a - b; }
}
溢出的检查功能是在 0.8.0 版本加入的,在此版本之前,请使用 OpenZepplin SafeMath 库。
一元运算负 -
表达式 -x 相当于 (T(0) - x) 这里 T 是指 x 的类型。 -x 只能应用在有符号型的整数上。 如果 x 为负数, -x 为正数。
由于使用两进制补码表示数据,你还需要小心:如果有 int x = type(int).min;, 那 -x 将不在正数取值的范围内。 这意味着这个检测 unchecked { assert(-x == x); } 是可以通过的(即这种情况下,不能假设它的负数会是正数),如果是 checked 模式,则会触发异常。
除法运算
除法运算结果的类型始终是其中一个操作数的类型,整数除法总是产生整数。在 Solidity 中,分数会取零。 这意味着 int256(-5) / int256(2) == int256(-2) 。
模运算(取余)
模运算 a%n 是在操作数 a 的除以 n 之后产生余数 r ,其中 q = int(a / n) 和 r = a - (n * q) 。 这意味着模运算结果与左操作数相同的符号相同(或零)。 对于 负数的 a : a % n == -(-a % n), 几个例子:
int256(5) % int256(2) == int256(1)int256(5) % int256(-2) == int256(1)int256(-5) % int256(2) == int256(-1)int256(-5) % int256(-2) == int256(-1)
对 0 取模会发生错误 Panic 错误,该检查不能通过unchecked { … } 。
幂运算
幂运算仅适用于无符号类型。 结果的类型总是等于基数的类型. 请注意类型足够大以能够容纳幂运算的结果,要么发生潜在的 assert 异常或者使用截断模式。
在 checked 模式下,幂运算仅会为小基数使用相对便宜的 exp 操作码。 例如 x**3 的例子,表达式 x*x*x 也许更便宜。 在任何情况下,都建议进行 gas 消耗测试和使用优化器。
扩展 TODO: 可以自己测试多少为临界值
注意 0**0 在 EVM 中定义为 1 。
i++ 和 ++i 区别
a = i++: 先把 i 的值赋予 a,然后在执行 i=i+1;a = ++i: 先执行 i=i+1,然后在把 i 的值赋予 a;
for 循环中,++i 更省钱
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract Test {
// 25153 gas
function test1() public pure returns (uint256 temp) {
for (uint256 index = 0; index < 10; index++) {
temp += index;
}
}
// 25081 gas
function test2() public pure returns (uint256 temp) {
for (uint256 index = 0; index < 10; ++index) {
temp += index;
}
}
}
赋值运算符
=(简单赋值)+=(相加赋值)−=(相减赋值)*=(相乘赋值)/=(相除赋值)%=(取模赋值)
注意: 同样的逻辑也适用于位运算符,因此它们将变成 <<=、>>=、>>=、&=、|=和^=。
不废话,直接上代码;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract Test {
uint256 public a = 20;
uint256 public b = 10;
function test1() public view returns (uint256 temp) {
temp = a + b;
}
function test2() public view returns (uint256 temp) {
temp = a;
temp += b;
}
function test3() public view returns (uint256 temp) {
temp = a;
temp -= b;
}
function test4() public view returns (uint256 temp) {
temp = a;
temp *= b;
}
function test5() public view returns (uint256 temp) {
temp = a;
temp /= b;
}
function test6() public view returns (uint256 temp) {
temp = a;
temp %= b;
}
}
a += e 等同于 a = a + e。其它运算符如 -=, *=, /=, %=, |=, &= , ^= , <<= 和 >>= 都是如此定义的。
a++和a--分别等同于a += 1和a -= 1,但表达式本身的值等于a在计算之前的值。与之相反,
--a和++a虽然最终a的结果与之前的表达式相同,但表达式的返回值是计算之后的值。
2️⃣ 关系运算符
关系运算符一共有六种:分别为: 大于 、小于 、 大于等于 、 小于等于 、 等于 和 不等于 。
>(大于)<(小于)>=(大于等于)<=(小于等于)==(等于)!=(不等于)
返回的结果是一个布尔值;
直接上代码;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract Test {
uint256 public a = 20;
uint256 public b = 10;
function test1() public view returns (bool) {
return a == b;
}
function test2() public view returns (bool) {
return a != b;
}
function test3() public view returns (bool) {
return a > b;
}
function test4() public view returns (bool) {
return a >= b;
}
function test5() public view returns (bool) {
return a < b;
}
function test6() public view returns (bool) {
return a <= b;
}
}
布尔类型 支持的运算符
包括:
!逻辑非==等于,!=不等于&&逻辑与,||逻辑或&&,||为短路运算符
地址类型 支持的运算符
<=,<,==,!=,>=and>
== 和 !=
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract Demo {
address public immutable owner;
constructor() {
owner = msg.sender;
}
function isOwner() external view returns (bool) {
return owner == msg.sender;
}
function test1(address _ads) external pure returns (bool) {
return address(0) != _ads;
}
}
> >= < <=
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract Demo {
function test1(address _ads) external pure returns (bool) {
return _ads > address(9);
}
function test2(address _ads) external pure returns (bool) {
return _ads >= address(9);
}
function test3(address _ads) external pure returns (bool) {
return _ads < address(9);
}
function test4(address _ads) external pure returns (bool) {
return _ads <= address(9);
}
}
交换地址
Uniswap V2 中 createPair 时的判断逻辑:
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient
定长字节数组 支持的运算符
比较运算符:
<=,<,==,!=,>=,>(返回布尔型)位运算符:
&,|,^(按位异或),~(按位取反)移位运算符:
<<(左移位),>>(右移位)索引访问:如果
x是bytesI类型,那么x[k](其中 0 <= k < I)返回第 k 个字节(只读)。
该类型可以和作为右操作数的无符号整数类型进行移位运算(但返回结果的类型和左操作数类型相同),右操作数表示需要移动的位数。 进行有符号整数位移运算会引发运行时异常。
3️⃣ 逻辑运算符
1. 基础用法
&& (逻辑与)
如果两个操作数都是 true ,则条件为真。
|| (逻辑或)
如果两个操作数有一个为 true ,则条件为真。
! (逻辑非)
反转操作数的逻辑状态。如果条件为真,则逻辑非操作将使其为假。
不废话,直接上代码;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract Test {
uint256 public a = 20;
uint256 public b = 10;
function test1() public view returns (bool) {
bool assertion1 = a > 15;
bool assertion2 = b > 15;
return assertion1 && assertion2;
}
function test2() public view returns (bool) {
bool assertion1 = a > 15;
bool assertion2 = b > 15;
return assertion1 || assertion2;
}
function test3() public view returns (bool assertion1, bool assertion2) {
assertion1 = a > 15;
assertion2 = !(b > 15);
}
}
2. && 和 || 的短路用法
原理:
A && B,如果 A 为 false,B 就不执行了A || B,如果 A 为 true,B 就不执行了
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract Test {
uint256 public a = 10;
uint256 public b = 20;
event Assertion1(string msg);
event Assertion2(string msg);
// 29319 gas
function test1() public returns (bool) {
return assertion1() || assertion2();
}
// 29365 gas
function testA() public returns (bool) {
bool as1 = assertion1();
bool as2 = assertion2();
return as1 && as2;
}
// 25430 gas
// 因为短路操作,减少了很多 gas
function testB() public returns (bool) {
return assertion1() && assertion2();
}
function assertion1() private returns (bool) {
emit Assertion1("Assertion1 run");
return a > 15;
}
function assertion2() private returns (bool) {
emit Assertion2("Assertion1 run");
return b > 15;
}
}
合理的使用短路操作,可以省一些 gas 费。
4️⃣ 三元运算符
三元运算符是一个表达是形式: <expression> ? <trueExpression> : <falseExpression> 。 它根据 <expression> 的执行结果,选择后两个给定表达式中的一个。 如果 <expression> 执行结果 true ,那么 <trueExpression> 将被执行,否则 <falseExpression> 被执行。
代码如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract Test {
uint256 public a = 20;
uint256 public b = 10;
function test1() public view returns (bool) {
uint256 temp = a + b;
return temp > 25 ? true : false;
}
function test2() public view returns (bool) {
uint256 temp = a + b;
return temp < 25 ? true : false;
}
}
三元运算符的结果类型是由两个操作数的类型决定的,方法与上面一样,如果需要的话,首先转换为它们的最小可容纳类型(mobile type )。
因此, 255 + (true ? 1 : 0) 将由于算术溢出而被回退。 原因是 (true ? 1 : 0) 是 uint8 类型,这迫使加法也要在 uint8 中执行。 而 256 超出了这个类型所允许的范围。
另一个结果是,像 1.5 + 1.5 这样的表达式是有效的,但 1.5 + (true ? 1.5 : 2.5) 则无效。 这是因为前者是以无限精度来进行有理表达式运算,只有它的最终结果值才是重要的。 后者涉及到将小数有理数转换为整数,这在目前是不允许的。
5️⃣ 位运算符
位运算在数字的二进制补码表示上执行。 这意味着: ~int256(0)== int256(-1)。
假设 A 等于 2;B 等于 3。
&(位与): 对其整数参数的每个位执行位与操作。例: (A & B) 为 2.
|(位或): 对其整数参数的每个位执行位或操作。例: (A | B) 为 3.
^(位异或): 对其整数参数的每个位执行位异或操作。例: (A ^ B) 为 1.
~(位非): 一元操作符,反转操作数中的所有位。例: (~B) 为 -4.
<<(左移位)): 将第一个操作数中的所有位向左移动,移动的位置数由第二个操作数指定,新的位由 0 填充。将一个值向左移动一个位置相当于乘以 2,移动两个位置相当于乘以 4,以此类推。例: (A << 1) 为 4.
>>(右移位): 左操作数的值向右移动,移动位置数量由右操作数指定例: (A >> 1) 为 1.
如果两个中的任一个数是小数,则不允许进行位运算。如果指数是小数的话,也不支持幂运算(因为这样可能会得到一个无理数)。
数学运算:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract Test {
uint256 public a = 5;
uint256 public b = 2;
// 左移操作符将第一个操作数向左移动指定位数,左边超出的位数将会被清除,右边将会补零。
function test2() public view returns (uint256) {
// a: ...000000000101
// b: ...000000000010
// ------------------
// =: 00...000000010100
// =: ...000000010100
return a << b;
}
// 右移操作符 (>>) 是将一个操作数按指定移动的位数向右移动,右边移出位被丢弃,左边移出的空位补符号位。
function test3() public view returns (uint256) {
// a: ...000000000101
// b: ...000000000010
// ------------------
// =: ...00000000000101
// =: ...000000000001
return a >> b;
}
}
移位操作的结果具有左操作数的类型,同时会截断结果以匹配类型。 右操作数必须是无符号类型。尝试按带符号的类型移动将产生编译错误。对于移位操作不会像算术运算那样执行溢出检查,其结果总是被截断。
移位可以通过用 2 的幂的乘法来 “模拟”(方法如下)。请注意,左操作数的截断总是在最后发生。
x << y等于数学表达式x * 2 ** y。5 << 2=5*2**2= 20
x >> y等于数学表达式x / 2 ** y, 四舍五入到负无穷。5 >> 2=5/2**2= 1
6️⃣ delete
delete a 的结果是将 a 类型初始值赋值给a。换句话说,在 delete a 之后 a 的值与在没有赋值的情况下声明 a 的情况相同。
delete 适用于整型,数组,结构体映射。
对于整型变量:相当于
a = 0。对于动态数组:是将重置为数组长度为 0 的数组
对于静态数组:是将数组中的所有元素重置为初始值。
对于数组而言:
delete a[x]仅删除数组索引x处的元素,其他的元素和长度不变,这为数组留出了一个空位。如果打算删除项,映射可能是更好的选择。对于结构体:则将结构体中的所有属性(成员)重置。
mapping : 是将所选择的 key 重置为初始值。
代码如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract Demo {
struct Book {
string title;
string author;
uint256 book_id;
}
// 整数
uint256 public a1 = 1;
int256 public a2 = -1;
// 数组
uint256[2] b1 = [1, 2]; // 定长
uint256[] b2 = [1, 2]; // 变长
// mapping
mapping(address => uint256) public balances;
// struct
Book public java; // 一本 java 书
constructor() {
java = Book({title: "Java", author: "LiSi", book_id: 1});
balances[msg.sender] = 999;
}
function deleteFn() public {
delete a1;
delete a2;
delete b1;
delete b2;
delete balances[msg.sender];
delete java;
}
function getB1() public view returns (uint256[2] memory) {
return b1;
}
function getB2() public view returns (uint256[] memory) {
return b2;
}
}
需要注意以下几点:
delete 对整个映射是无效的(因为映射的键可以是任意的,通常也是未知的)。因此在你删除一个结构体时,结果将重置所有的非映射属性(成员),这个过程是递归进行的,除非它们是映射。然而,单个的键及其映射的值是可以被删除的。
理解 delete a 的效果就像是给 a 赋值很重要,换句话说,这相当于在 a中存储了一个新的对象。
当 a 是应用变量时,我们可以看到这个区别, delete a 它只会重置 a 本身,而不是更改它之前引用的值。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract DeleteLBC {
uint256 data;
uint256[] dataArray;
function f() public {
uint256 x = data;
delete x; // 将 x 设为 0,并不影响数据
delete data; // 将 data 设为 0,并不影响 x,因为它仍然有个副本
uint256[] storage y = dataArray;
delete dataArray;
// 将 dataArray.length 设为 0,但由于 uint[] 是一个复杂的对象,y 也将受到影响,
// 因为它是一个存储位置是 storage 的对象的别名。
// 另一方面:"delete y" 是非法的,引用了 storage 对象的局部变量只能由已有的 storage 对象赋值。
assert(y.length == 0);
}
}
7️⃣ 操作符的优先级
| 优先级 | 描述 | 操作符 |
|---|---|---|
| 1 | 后置自增和自减 | ++, -- |
| 创建类型实例 | 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 | 赋值操作符 | =, |=, ^=, &=, <<=,
>>=, +=, -=, *=, /=,
%= |
| 16 | 逗号 | , |
表达式的计算顺序不是特定的(更准确地说,表达式树中某节点的字节点间的计算顺序不是特定的,但它们的结算肯定会在节点自己的结算之前)。该规则只能保证语句按顺序执行,布尔表达式的短路执行。
8️⃣ 不同数据类型的总结
整型 支持的运算符
比较运算符:
<=,<,==,!=,>=,>比较结果的返回值为 bool 类型位运算符:
&,|,^(异或),~(非,位取反)移位运算符:
<<(左移) ,>>(右移)数学运算:
+,-, 一元运算负-(仅针对有符号整型),*,/,%(取余),++,--,+=,-=**(次方)
定长浮点型 支持的运算符
比较运算符:<=, <, ==, !=, >=, > (返回值是布尔型)
算术运算符:+, -, 一元运算 -, 一元运算 +, *, /, % (取余数)
比较:
==,!=,>,>=,<,<=返回值为 bool 类型。
位运算符:
&,|,^(异或),~非
#️⃣ 问答题
算数运算符的注意
表达式
-x相当于(T(0) - x)这里T是指x的类型。-x只能应用在有符号型的整数上。整数除法总是产生整数。
一元运算负
-有什么需要注意的表达式
-x相当于(T(0) - x)这里T是指x的类型。-x只能应用在有符号型的整数上。 如果x为负数,-x为正数。由于使用两进制补码表示数据,你还需要小心:如果有
int x = type(int).min;, 那-x将不在正数取值的范围内。 这意味着这个检测unchecked { assert(-x == x); }是可以通过的(即这种情况下,不能假设它的负数会是正数),如果是 checked 模式,则会触发异常。