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
Last updated