1.6.5 表达式及控制结构

1.6.5.1 输入参数和输出参数

和Javascript一样,函数有输入参数,但和Javascrip以及C语言不同的是,Solidity函数可以返回任意数量的返回值。

1)输入参数

输入参数的定义和变量的定义一样,如果某参数不使用,可以省略其变量名。我们看下面的例子。

pragma solidity ^0.4.16;

contract Simple {

function taker(uint _a, uint _b) public pure {

// 输入参数 _a 和 _b.

}

}

2)返回值

返回值的定义紧跟在关键字returns之后。如下例所示。

pragma solidity ^0.4.16;

contract Simple {

function arithmetics(uint _a, uint _b)

public

pure

returns (uint o_sum, uint o_product)

{

o_sum = _a + _b;

o_product = _a * _b;

}

}

返回值的名字可省略。返回值可用return指定。return可返回多个值。返回值都会被初始化为0,如果它们没有被显式地赋值,将一直为0。

1.6.5.2 控制结构

Javascript中的控制结构除了switch和goto,其它的在Solidity中也都存在。Solidity的控制语句有:if, else, while, do, for, break, continue, return, ? : 。它们的用法和定义与Javascript或C语言一样。

在条件语句中,括号不能省略,但在单一语句中,花括号可省略。

注意:不像C语言和Javascript,它们可以把非布尔值转换为布尔值,Solidity不支持这样的转换,因此if (1) { ... }在Solidity中是错误的用法。

1.6.5.3 函数调用

1)内部函数调用(Internal Function Calls)

当前合约的函数可以被直接(内部)调用,甚至是递归调用,见下例。

pragma solidity ^0.4.16;

contract C {

function g(uint a) public pure returns (uint ret) { return f(); }

function f() internal pure returns (uint ret) { return g(7) + f(); }

}

上例中的函数调用在以太坊虚拟机中会被翻译成简单的跳转语句。调用完后当前内存不被清零,在内存中传递引用,这种方式非常高效。只有同一个合约中的函数才能进行内部调用。

2)外部函数调用(External Function Calls)

表达式this.g(8);和c.g(2);(c是个合约实例)也是函数调用,但这种函数调用是外部函数调用,它通过消息而不是跳转语句进行调用。注意,不能在合约的构造函数中使用this调用,因为在构造函数调用时合约还未真正实例化。

在一个合约中想调用其它合约的函数只能是外部函数调用。在外部函数调用时,函数的参数都会被拷贝到内存。当调用其它合约的函数时,要发送的wei和gas可用.value()和.gas()定义,如下例所示。

pragma solidity ^0.4.0;

contract InfoFeed {

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

}

contract Consumer {

InfoFeed feed;

function setFeed(address addr) public { feed = InfoFeed(addr); }

function callFeed() public { feed.info.value(10).gas(800)(); }

}

在上例中修饰符payable必须被用于info函数,否则.value()将无法使用。

值得注意的是表达式InfoFeed(addr)进行了显式转换,告诉系统,地址addr的合约类型是InfoFeed,并且不会调用构造函数。显式类型转换在使用时必须非常小心,在没有把握时,不要使用。

我们也可以直接使用函数setFeed(InfoFeed feed){feed = feed;}。注意feed.info.value(10).gas(800)仅仅只在本地局部设置函数调用的gas值,gas(800)才是真正的函数调用。

如果被调用的合约不存在,合约抛出异常或gas耗尽,函数调用就会抛出异常。

注意:任何调用其它合约的行为都有风险,尤其是当被调用的合约源代码未知时。调用另外一个合约就意味着当前合约把控制权给了另外一个合约,那么另外那个合约做什么就完全超出了控制。特别要提醒的是,尽量在当前合约的状态已经进行了明确的改变之后,再调用其它的合约,这样操作能在最大程度上防止重入攻击(reentrancy exploit)。

3)命名调用和匿名函数参数(Named Calls and Anonymous Function Parameters)

函数的参数可用{}表示,参数列表必须符合函数定义中对参数的定义,顺序可任意,如下例所示。

pragma solidity ^0.4.0;

contract C {

function f(uint key, uint value) public {

// ...

}

function g() public {

// 命名参数

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

}

}

4)省略函数参数名

函数中不用的参数(尤其是返回值)可被省略,虽然这些参数仍然在堆栈中,但无法使用。

pragma solidity ^0.4.16;

contract C {

// 省略函数参数名

function func(uint k, uint) public pure returns(uint) {

return k;

}

}

1.6.5.4 用new创建智能合约

用户可以在智能合约中用关键字new创建新的合约实例。待创建的合约必须已经定义。

pragma solidity ^0.4.0;

contract D {

uint x;

function D(uint a) public 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 {

// 创建的同时发送以太币

D newD = (new D).value(amount)(arg);

}

}

在上例中,可以用.value()选项创建合约实例D并发送以太币,但不能定义gas值。如果创建失败,会抛出异常。

1.6.5.5 条件判断表达式的执行顺序

在Solidity中并没有明确定义条件判断表达式的执行顺序,但会保证条件判断表达式都会执行。条件判断表达式在做布尔运算时会做短路运算。

1.6.5.6 赋值运算

1.6.5.6.1 赋值运算及返回多个值

Solidity允许定义记录类型(tuple),所谓记录类型就是包含一系列不同类型对象的数据结构,每个记录类型的长度固定。记录可被用于返回多个返回值。如下例所示。

pragma solidity >0.4.23 <0.5.0;

contract C {

uint[] data;

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);

//变量可被略去

(data.length,,) = f(); // Sets the length to 7

// 只有赋值语句左边才能略去变量

// 一种特例意外:

(x,) = (1,);

// (1,) 是仅有的能用来定义只有一个元素的记录,因为 (1) 等于 1.

}

}

1.6.5.6.2 数组和结构体的赋值运算

对于非值类型的变量比如数组和结构体来说,赋值运算的定义就要复杂一些。给一个状态变量赋值通常会把这个值进行拷贝。给一个局部变量赋值则会复制一个基本类型的值,也即长为32个字节的静态类型。如果把一个结构体和数组(包括bytes和string)由一个状态变量赋值给一个局部变量,这时局部变量只是引用了状态变量。当对此局部变量再赋值时,只会改变局部变量的引用,而不改变状态变量的值。如果对该局部变量的成员进行赋值,则状态变量对应的成员值也会被改变。

1.6.5.7 变量声明和作用域

每一个被声明的变量都有个初始值,通常初始值为0。比如bool变量的初始值为false。uint和int的初始值为0。对定长数组以及bytes1到bytes32的类型,其初始值会被设为其元素所对应类型的初始值。对变长数组以及bytes和string,其初始值为空。

通常函数中定义的变量其作用域为整个函数,与其具体在函数的哪个位置定义无关。关于作用域的规则,Solidity完全承袭了Javascript。注意:下例中会出现编译错误。

// 无法通过编译

pragma solidity ^0.4.16;

contract ScopingErrors {

function scoping() public {

uint i = 0;

while (i++ < 1) {

uint same1 = 0;

}

while (i++ < 2) {

uint same1 = 0;// 非法,重复定义same1。

}

}

function minimalScoping() public {

{

uint same2 = 0;

}

{

uint same2 = 0;// 非法,重复定义same2。

}

}

function forLoopScoping() public {

for (uint same3 = 0; same3 < 1; same3++) {

}

for (uint same3 = 0; same3 < 1; same3++) {//非法,重复定义same3。

}

}

}

此外,函数中的一个变量一旦被定义,就会在函数的起始部分被初始化。因此,下例中,尽管代码风格很差,但却是合法可以通过编译的。

pragma solidity ^0.4.0;

contract C {

function foo() public pure returns (uint) {

//在这里,baz被隐式初始化为0

uint bar = 5;

if (true) {

bar += baz;

} else {

uint baz = 10;//这条语句永远不会执行

}

return bar;// 返回值为 5

}

}

1.6.5.8 错误处理:Assert,Require,Revert和Exceptions

Solidity是通过回退状态的方式来处理错误。发生异常时会撤消当前的调用(及其所有的子调用)所改变的状态,同时给调用者返回一个错误标识。

Solidity提供了两个函数assert和require来进行条件检查,如果条件不满足则抛出异常。assert函数通常只用来检查内部错误,require函数可用来检查输入变量或合约的状态变量是否满足条件,还可以验证合约调用的返回值是否有效。正确使用assert,可以帮我们发现智能合约及函数调用中的错误。

除了assert和require,另外还有两种方式来触发异常:revert和throw。revert函数可以用来标记错误并回退当前调用。还可以用string定义出错时的消息并把错误信息返回给调用者。使用throw关键字也可以抛出异常(从0.4.13版本开始,throw关键字已被弃用,将来会被淘汰。),但无法返回错误信息。

当子调用(sub-call)中发生异常时,异常会自动向上“冒泡”(异常会再次抛出)。 不过也有一些例外:send和底层的函数调用call, delegatecall,callcode发生异常时,这些函数返回false,不抛出异常。

注意:如果从一个不存在的地址调用底层函数call,delegatecall和callcode, 它们也会返回成功,所以我们在进行调用时,应该要优先检查函数是否存在。在Solidity中,异常可以抛出但无法捕捉。

下面这个示例说明了如何使用require来检查输入条件,用assert检查内部错误:

pragma solidity ^0.4.22;

contract Sharer {

function sendHalf(address addr) public payable returns (uint balance) {

require(msg.value % 2 == 0, "Even value required.");

uint balanceBeforeTransfer = this.balance;

addr.transfer(msg.value / 2);

// transfer调用会在调用失败时抛出异常而无法回调,我们将无法取回一半的金额。

assert(this.balance == balanceBeforeTransfer - msg.value / 2);

return this.balance;

}

}

下列场景会产生assert类型的异常:

1) 越界,或用负值下标访问数组,如i >= x.length 或 i < 0时访问x[i]。

2) 序号越界,或用负值下标访问一个定长的bytesN。

3) 被除数为0, 如5/0 或 23 % 0。

4) 移位运算时所移位数为负值,如:5<<i; i为-1。

5)将一个过大值或负值转为枚举类型。

6) 调用一个初始化为0的内部函数类型变量。

7) 调用assert的参数为false。

下列场景会产生require类型的异常:

1) 调用throw

2) 调用require的参数为false

3)用户通过消息调用一个函数,在调用的过程中,函数没有正确结束(gas不足,没有匹配到对应的函数,或被调用的函数抛出异常)。调用底层操作如call,send,delegatecall或callcode时除外,它们不会抛出异常,它们会通过返回false来表示失败。

4) 使用关键字new创建一个新合约时没有正常完成。

5) 通过外部函数调用合约时,合约未实现。

6) 合约用来接收以太币的public函数没有用payable修饰符(包括构造函数,和回退函数)定义。

7) 合约通过一个public的getter函数(public getter 函数)接收以太币。

8) .transfer()执行失败。

当发生require类型异常时,Solidity会执行一个回退操作(指令0xfd)。当发生assert类型异常时,Solidity会执行一个无效操作(指令0xfe)。在上述的两种情况下,EVM都会回撤所有状态的改变,这样设计是期望如果函数一旦执行,要么就正确的执行,一旦执行可能会出问题,就不再继续安全地执行,必须保证交易的原子性(一致性,要么全部执行,要么一点改变都没有,不能只改变一部分),所以一旦出现异常,就需要撤销所有的操作,让整个系统状态不受此失败交易的影响。

注意:assert类型的异常会消耗掉所有的gas, 而require从大都会版本(Metropolis, 即目前主网所在的版本)起不会消耗gas。

下例显示了如何与revert和require一起使用error字符串:

pragma solidity ^0.4.22;

contract VendingMachine {

function buy(uint amount) payable {

if (amount > msg.value / 2 ether)

revert("Not enough Ether provided.");

// 另一种方式:

require(

amount <= msg.value / 2 ether,

"Not enough Ether provided."

);

// Perform the purchase.

}

}

上例中如果错误是通过Error(string)函数调用产生的,则信息将会是ABI编码。revert("Not enough Ether provided.")将会产生如下数据作为错误返回信息。

0x08c379a0 // Function selector for Error(string)

0x0000000000000000000000000000000000000000000000000000000000000020 // Data offset

0x000000000000000000000000000000000000000000000000000000000000001a // String length

0x4e6f7420656e6f7567682045746865722070726f76696465642e000000000000 // String data