运算操作符

  • 算术运算符

  • 比较运算符

  • 逻辑运算符/关系运算符

  • 赋值运算符

  • 条件运算符/三元运算符

  • 位运算符

  • delete

  • unchecked

  • 两个类型不一样的操作数,也可以进行算术和位操作运算

    • 例如,你可以计算 y = x + z ,其中 x 是 uint8 , z 是 int32 类型。 在这些情况下,将使用以下机制来确定运算结果的类型(这在溢出的情况下很重要)。

    • 如果右操作数的类型可以隐含地转换为左操作数的类型的类型,则使用左操作数的类型。

    • 如果左操作数的类型可以隐含地转换为右操作数的类型的类型,则使用右操作数的类型。

    • 否则,该操作不被允许。

如果其中一个操作数是一个常量数字,会首先被转换为能容纳该值的最小的类型 (相同位数时,无符号类型被认为比有符号类型 “小”)。 如果两者都是常量数字,则以任意的精度进行计算。

操作符的结果类型与执行操作的类型相同,除了比较运算符,其结果总是 bool

运算符 **(幂), <<>> 使用左边操作数的类型来作为运算结果类型。

算术运算符

  • +-*/%(取余,取模),

  • ++(递增),--(递减),+=(加法赋值),-=(减法赋值)

  • **(次方)

不废话,直接上代码;

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

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.18;

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.18;

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 += 1a -= 1,但表达式本身的值等于 a 在计算之前的值。

  • 与之相反, --a++a 虽然最终 a 的结果与之前的表达式相同,但表达式的返回值是计算之后的值。

关系运算符

关系运算符一共有六种:分别为: 大于 、小于 、 大于等于 、 小于等于 、 等于 和 不等于 。

  • > (大于)

  • < (小于)

  • >= (大于等于)

  • <= (小于等于)

  • == (等于)

  • != (不等于)

返回的结果是一个布尔值;

直接上代码;

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

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.18;

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.18;

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

定长字节数组 支持的运算符

  • 比较运算符: <=<==!=>=> (返回布尔型)

  • 位运算符: &|^ (按位异或), ~ (按位取反)

  • 移位运算符: << (左移位), >> (右移位)

  • 索引访问:如果 xbytesI 类型,那么 x[k] (其中 0 <= k < I)返回第 k 个字节(只读)。

该类型可以和作为右操作数的无符号整数类型进行移位运算(但返回结果的类型和左操作数类型相同),右操作数表示需要移动的位数。 进行有符号整数位移运算会引发运行时异常。

逻辑运算符

基础用法

  • && (逻辑与)

    • 如果两个操作数都是 true ,则条件为真。

  • || (逻辑或)

    • 如果两个操作数有一个为 true ,则条件为真。

  • ! (逻辑非)

    • 反转操作数的逻辑状态。如果条件为真,则逻辑非操作将使其为假。

不废话,直接上代码;

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

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.18;

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.18;

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.18;

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

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.18;

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.18;

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 模式,则会触发异常。