控制结构
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会撤回所有的状态转变。但是它有两点不同:
- 它允许你返回一个值;
- 它会把所有剩下的gas退回给caller