# 1.6.5 表达式及控制结构

## 1.6.5.1 输入参数和输出参数

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

1）输入参数

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

pragma solidity ^0.4.16;

contract Simple {

&#x20;function taker(uint \_a, uint \_b) public pure {

&#x20;// 输入参数 \_a 和 \_b.

&#x20;}

}

2）返回值

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

pragma solidity ^0.4.16;

contract Simple {

&#x20;function arithmetics(uint \_a, uint \_b)

&#x20;public

&#x20;pure

&#x20;returns (uint o\_sum, uint o\_product)

&#x20;{

&#x20;o\_sum = \_a + \_b;

&#x20;o\_product = \_a \* \_b;

&#x20;}

}

返回值的名字可省略。返回值可用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 {

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

&#x20;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 {

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

}

contract Consumer {

&#x20;InfoFeed feed;

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

&#x20;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 {

&#x20;function f(uint key, uint value) public {

&#x20;// ...

&#x20;}

&#x20;function g() public {

&#x20;// 命名参数

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

&#x20;}

}

4）省略函数参数名

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

pragma solidity ^0.4.16;

contract C {

&#x20;// 省略函数参数名

&#x20;function func(uint k, uint) public pure returns(uint) {

&#x20;return k;

&#x20;}

}

## 1.6.5.4 用new创建智能合约

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

pragma solidity ^0.4.0;

contract D {

&#x20;uint x;

&#x20;function D(uint a) public payable {

&#x20;x = a;

&#x20;}

}

contract C {

&#x20;D d = new D(4); // 会在合约C的构造函数中执行

&#x20;function createD(uint arg) public {

&#x20;D newD = new D(arg);

&#x20;}

&#x20;function createAndEndowD(uint arg, uint amount) public payable {

&#x20;// 创建的同时发送以太币

&#x20;D newD = (new D).value(amount)(arg);

&#x20;}

}

在上例中，可以用.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 {

&#x20;uint\[] data;

&#x20;function f() public pure returns (uint, bool, uint) {

&#x20;return (7, true, 2);

&#x20;}

&#x20;function g() public {

&#x20;// 把函数返回的记录值赋值给多个变量

&#x20;(uint x, bool b, uint y) = f();

&#x20;// 数值互换-但不能用于非数值的storage存储类型变量.

&#x20;(x, y) = (y, x);

&#x20;//变量可被略去

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

&#x20;// 只有赋值语句左边才能略去变量

&#x20;// 一种特例意外:

&#x20;(x,) = (1,);

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

&#x20;}

}

### 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 {

&#x20;function scoping() public {

&#x20;uint i = 0;

&#x20;while (i++ < 1) {

&#x20;uint same1 = 0;

&#x20;}

&#x20;while (i++ < 2) {

&#x20;uint same1 = 0;// 非法，重复定义same1。

&#x20;}

&#x20;}

&#x20;function minimalScoping() public {

&#x20;{

&#x20;uint same2 = 0;

&#x20;}

&#x20;{

&#x20;uint same2 = 0;// 非法，重复定义same2。

&#x20;}

&#x20;}

&#x20;function forLoopScoping() public {

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

&#x20;}

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

&#x20;}

&#x20;}

}

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

pragma solidity ^0.4.0;

contract C {

&#x20;function foo() public pure returns (uint) {

&#x20;//在这里，baz被隐式初始化为0

&#x20;uint bar = 5;

&#x20;if (true) {

&#x20;bar += baz;

&#x20;} else {

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

&#x20;}

&#x20;return bar;// 返回值为 5

&#x20;}

}

## 1.6.5.8 错误处理：Assert，Require，Revert和Exceptions

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

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

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

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

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

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

pragma solidity ^0.4.22;

contract Sharer {

&#x20;function sendHalf(address addr) public payable returns (uint balance) {

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

&#x20;uint balanceBeforeTransfer = this.balance;

&#x20;addr.transfer(msg.value / 2);

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

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

&#x20;return this.balance;

&#x20;}

}

&#x20;下列场景会产生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。

&#x20;下列场景会产生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()执行失败。

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

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

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

pragma solidity ^0.4.22;

contract VendingMachine {

&#x20;function buy(uint amount) payable {

&#x20;if (amount > msg.value / 2 ether)

&#x20;revert("Not enough Ether provided.");

&#x20;// 另一种方式:

&#x20;require(

&#x20;amount <= msg.value / 2 ether,

&#x20;"Not enough Ether provided."

&#x20;);

&#x20;// Perform the purchase.

&#x20;}

}

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

0x08c379a0 // Function selector for Error(string)

0x0000000000000000000000000000000000000000000000000000000000000020 // Data offset

0x000000000000000000000000000000000000000000000000000000000000001a // String length

0x4e6f7420656e6f7567682045746865722070726f76696465642e000000000000 // String data
