控制结构

JavaScript 中的大部分控制结构在 Solidity 中都是可用的,除了 switch 和 goto。 因此 Solidity 中有 if,else,while,do,for,break,continue,return,? : 这些与在 C 或者 JavaScript 中表达相同语义的关键词。
Solidity还支持 try/catch 语句形式的异常处理, 但仅用于 外部函数调用 和 合约创建调用.
注意:与 C 和 JavaScript 不同, Solidity 中非布尔类型数值不能转换为布尔类型,因此 if (1) { ... } 的写法在 Solidity 中 无效 。

函数调用

当前合约中的函数可以直接(“从内部”)调用,这些函数调用在 EVM 中被解释为简单的跳转。

外部函数调用

表达式 this.g(8); 和 c.g(2); (其中 c 是合约实例)也是有效的函数调用,但是这种情况下,函数将会通过一个消息调用来进行“外部调用”,而不是直接的跳转。 请注意,不可以在构造函数中通过 this 来调用函数,因为此时真实的合约实例还没有被创建。
当调用其他合约的函数时,随函数调用发送的 Wei 和 gas 的数量可以分别由特定选项 {value: 10, gas: 10000}
请注意,不建议明确指定gas,因为操作码的gas 消耗将来可能会发生变化。 任何发送给合约 Wei 将被添加到该合约的总余额中:


contract InfoFeed {
    function info() public payable returns (uint ret) { return 42; }
}

contract Consumer {
    InfoFeed feed;
    function setFeed(InfoFeed addr) public { feed = addr; }
    function callFeed() public { feed.info{value: 10, gas: 800}(); }
}
//payable 修饰符要用于修饰 info 函数,否则,value 选项将不可用。

具名调用和匿名函数参数

函数调用参数也可以按照任意顺序由名称给出,如果它们被包含在 { } 中, 如以下示例中所示。参数列表必须按名称与函数声明中的参数列表相符,但可以按任意顺序排列。


contract C {
    mapping(uint => uint) data;

    function f() public {
        set({value: 2, key: 3});
    }

    function set(uint key, uint value) public {
        data[key] = value;
    }

}

省略函数参数名称

未使用参数的名称(特别是返回参数)可以省略。这些参数仍然存在于堆栈中,但它们无法访问


contract C {
    // 省略参数名称
    function func(uint k, uint) public pure returns(uint) {
        return k;
    }
}

通过 new 创建合约

使用关键字 new 可以创建一个新合约。待创建合约的完整代码必须事先知道,因此递归的创建依赖是不可能的。


contract D {
    uint x;
    function D(uint a) payable {
        x = a;
    }
}

contract C {
    D d = new D(4); // 将作为合约 C 构造函数的一部分执行

    function createD(uint arg) public {
        D newD = new D(arg);
    }

    function createAndEndowD(uint arg, uint amount) public payable {
                //随合约的创建发送 ether
        D newD = (new D){value:amount}(arg);  
    }
}
//通过使用 value 选项创建 D 的实例时可以附带发送 Ether,但是不能限制 gas 的数量。 如果创建失败(可能因为栈溢出,或没有足够的余额或其他问题),会引发异常。

加“盐”的合约创建 create2

在创建合约时,将根据创建合约的地址和每次创建合约交易时的计数器(nonce)来计算合约的地址。
如果你指定了一个可选的 salt(一个bytes32值),那么合约创建将使用另一种机制来生成新合约的地址:
它将根据给定的盐值,创建合约的字节码和构造函数参数来计算创建合约的地址。地址计算公式如下:

address— 调用CREATE2的智能合约的地址
salt— 随机数,其实是确定的比如用userId计算出的哈希
init_code— 要部署合约的字节码

特别注意,不使用计数器(“nonce”)。 这样可以在创建合约时提供更大的灵活性:你可以在创建新合约之前就推导出(将要创建的)合约地址。甚至是,还可以依赖此地址(即便它还不存在)来创建其他合约。一个主要用例场景是充当链下交互仲裁合约,仅在有争议时才需要创建。

pragma solidity ^0.7.0;

contract D {
   uint public x;
   constructor(uint a) {
       x = a;
   }
}

contract C {
   function createDSalted(bytes32 salt, uint arg) public {
       /// 这个复杂的表达式只是告诉我们,如何预先计算地址。
       /// 这里仅仅用来说明。
       /// 实际上,你仅仅需要 ``new D{salt: salt}(arg)``.
       address predictedAddress = address(uint160(uint(keccak256(abi.encodePacked(
           bytes1(0xff),
           address(this),
           salt,
           keccak256(abi.encodePacked(
               type(D).creationCode,
               arg
           ))
       )))));

       D d = new D{salt: salt}(arg);
       require(address(d) == predictedAddress);
   }
}

表达式计算顺序

赋值

解构赋值和返回多值


contract C {
    uint index;

    function f() public pure returns (uint, bool, uint) {
        return (7, true, 2);
    }

    function g() public {
        //基于返回的元组来声明变量并赋值
        (uint x, bool b, uint y) = f();
        //交换两个值的通用窍门——但不适用于非值类型的存储 (storage) 变量。
        (x, y) = (y, x);
        //元组的末尾元素可以省略(这也适用于变量声明)。
        (index,,) = f(); // 设置 index 为 7
    }
}

数组和结构体的复杂性

在下面的示例中, 对 g(x) 的调用对 x 没有影响, 因为它在内存中创建了存储值独立副本。但是, h(x) 成功修改 x , 因为只传递引用而不传递副本。

pragma solidity >=0.4.22 <0.9.0;

 contract C {
    uint[20] x;

     function f() public {
        g(x);
        h(x);
    }

     function g(uint[20] memory y) internal pure {
        y[2] = 3;
    }

     function h(uint[20] storage y) internal {
        y[3] = 4;
    }
}

作用域和声明

Solidity 中的作用域规则遵循了 C99(与其他很多语言一样):变量将会从它们被声明之后可见,直到一对 { } 块的结束。作为一个例外,在 for 循环语句中初始化的变量,其可见性仅维持到 for 循环的结束。

算术运算的检查模式与非检查模式

当对无限制整数执行算术运算,其结果超出结果类型的范围,这是就发生了上溢出或下溢出。
在Solidity 0.8.0之前,算术运算总是会在发生溢出的情况下进行“截断”,从而得靠引入额外检查库来解决这个问题(如 OpenZepplin 的 SafeMath)。
而从Solidity 0.8.0开始,所有的算术运算默认就会进行溢出检查,额外引入库将不再必要。
如果想要之前“截断”的效果,可以使用 unchecked 代码块:

pragma solidity >0.7.99;
contract C {
    function f(uint a, uint b) pure public returns (uint) {
        // 溢出会返回“截断”的结果
        unchecked { return a - b; }
    }
    function g(uint a, uint b) pure public returns (uint) {
        // 溢出会抛出异常
        return a - b;
    }
}

错误处理及异常:Assert, Require, Revert

同样作为判断一个条件是否满足的函数,require会退回剩下的gas,而assert会烧掉所有的gas。require 常用来执行条件判断,作为函数执行的一个合理的判断。assert常用来中断函数,抛出异常。一般来说正常运行的函数不会运行到assert。
revert会撤回所有的状态转变。但是它有两点不同:

  1. 它允许你返回一个值;
  2. 它会把所有剩下的gas退回给caller