- 算术运算符
- 比较运算符
- 逻辑运算符/关系运算符
- 赋值运算符
- 条件运算符/三元运算符
- 位运算符
- delete
- unchecked
- 两个类型不一样的操作数,也可以进行算术和位操作运算。
- 例如,你可以计算 y = x + z ,其中 x 是
uint8
, z 是int32
类型。 在这些情况下,将使用以下机制来确定运算结果的类型(这在溢出的情况下很重要)。 - 如果右操作数的类型可以隐含地转换为左操作数的类型的类型,则使用左操作数的类型。
- 如果左操作数的类型可以隐含地转换为右操作数的类型的类型,则使用右操作数的类型。
- 否则,该操作不被允许。
- 例如,你可以计算 y = x + z ,其中 x 是
如果其中一个操作数是一个常量数字,会首先被转换为能容纳该值的最小的类型 (相同位数时,无符号类型被认为比有符号类型 “小”)。 如果两者都是常量数字,则以任意的精度进行计算。
操作符的结果类型与执行操作的类型相同,除了比较运算符,其结果总是 bool
。
运算符 **
(幂), <<
和 >>
使用左边操作数的类型来作为运算结果类型。
+
,-
,*
,/
,%
(取余,取模),++
(递增),--
(递减),+=
(加法赋值),-=
(减法赋值)**
(次方)
不废话,直接上代码;
// 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 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 。
a = i++
: 先把 i 的值赋予 a,然后在执行 i=i+1;a = ++i
: 先执行 i=i+1,然后在把 i 的值赋予 a;
// 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
的结果与之前的表达式相同,但表达式的返回值是计算之后的值。
关系运算符一共有六种:分别为: 大于 、小于 、 大于等于 、 小于等于 、 等于 和 不等于 。
>
(大于)<
(小于)>=
(大于等于)<=
(小于等于)==
(等于)!=
(不等于)
返回的结果是一个布尔值;
直接上代码;
// 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 个字节(只读)。
该类型可以和作为右操作数的无符号整数类型进行移位运算(但返回结果的类型和左操作数类型相同),右操作数表示需要移动的位数。 进行有符号整数位移运算会引发运行时异常。
- && (逻辑与)
- 如果两个操作数都是 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);
}
}
原理:
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 费。
三元运算符是一个表达是形式: <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)
则无效。 这是因为前者是以无限精度来进行有理表达式运算,只有它的最终结果值才是重要的。 后者涉及到将小数有理数转换为整数,这在目前是不允许的。
位运算在数字的二进制补码表示上执行。 这意味着: ~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
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);
}
}
优先级 | 描述 | 操作符 |
---|---|---|
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 | 逗号 | , |
表达式的计算顺序不是特定的(更准确地说,表达式树中某节点的字节点间的计算顺序不是特定的,但它们的结算肯定会在节点自己的结算之前)。该规则只能保证语句按顺序执行,布尔表达式的短路执行。
- 比较运算符:
<=
,<
,==
,!=
,>=
,>
比较结果的返回值为 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 模式,则会触发异常。
- 表达式