Solidity 高级特性

包括入参和出参、函数等

入参和出参(Input Parameters and Output Parameters)

同JavaScript一样,函数有输入参数,但与之不同的是,函数可能有任意数量的返回参数。

入参(Input Parameters)

入参(Input Parameter)与变量的定义方式一致,稍微不同的是,不会用到的参数可以省略变量名称。一种可接受两个整型参数的函数如下:

pragma solidity ^0.4.0;
contract Simple {
    function taker(uint _a, uint) {
        // do something with _a.
    }
}

出参(Output Parameters)

出参(Output Paramets)在returns关键字后定义,语法类似变量的定义方式。下面的例子展示的是,返回两个输入参数的求和,乘积的实现:

pragma solidity ^0.4.0;
contract Simple {
    //return sum and product
    function arithmetics(uint _a, uint _b) returns (uint o_sum, uint o_product) {
        o_sum = _a + _b;
        o_product = _a * _b;
    }
}

出参的的名字可以省略。返回的值,同样可以通过return关键字来指定。return也可以同时返回多个值,参见Returning Multiple Values。出参的默认值为0,如果没有明确被修改,它将一直是0。

入参和出参也可在函数体内用做表达式。它们也可被赋值。

返回多个值(Returning Multiple Values) 当返回多个参数时,使用return (v0, v1, ..., vn)。返回结果的数量需要与定义的一致。

控制结构

不支持switch和goto,支持if,else,while,do,for,break,continue,return,?:。

条件判断中的括号不可省略,但在单行语句中的大括号可以省略。

需要注意的是,这里没有像C语言,和JavaScript里的非Boolean类型的自动转换,比如if(1){...}在Solidity中是无效的。

函数调用(Function Calls)

内部函数调用(Internal Function Calls) 在当前的合约中,函数可以直接调用(内部调用方式),包括也可递归调用,来看一个简单的示例:

contract C {
    function g(uint a) returns (uint ret) { return f(); }
    function f() returns (uint ret) { return g(7) + f(); }
}

这些函数调用在EVM中被翻译成简单的跳转指令。这样带来的一个好处是,当前的内存不会被回收。所以在一个内部调用时传递一个内存型引用效率将非常高。当然,仅仅是同一个合约的函数之间才可通过内部的方式进行调用。

外部函数调用(External Function Calls)

表达式this.g(8);和c.g(2)(这里的c是一个合约实例)是外部调用函数的方式。实现上是通过一个消息调用,而不是直接通过EVM的指令跳转。需要注意的是,在合约的构造器中,不能使用this调用函数,因为当前合约还没有创建完成。

其它合约的函数必须通过外部的方式调用。对于一个外部调用,所有函数的参数必须要拷贝到内存中。

当调用其它合约的函数时,可以通过选项.value(),和.gas()来分别指定,要发送的ether量(以wei为单位),和gas值。

pragma solidity ^0.4.0;
contract InfoFeed {
  
    function info() payable returns (uint ret) { 
        return msg.value;
    }
}
contract Consumer {
  
    function deposit() payable returns (uint){
        return msg.value;
    } 
  
    function left() constant returns (uint){
        return this.balance;
    }
  
    function callFeed(address addr) returns (uint) { 
        return InfoFeed(addr).info.value(1).gas(8000)(); 
    }
}

上面的代码中,我们首先调用deposit()为Consumer合约存入一定量的ether。然后调用callFeed()通过value(1)的方式,向InfoFeed合约的info()函数发送1ether。需要注意的是,如果不先充值,由于合约余额为0,余额不足会报错Invalid opcode1。

InfoFeed.info()函数,必须使用payable关键字,否则不能通过value()选项来接收ether。

代码InfoFeed(addr)进行了一个显示的类型转换,声明了我们确定知道给定的地址是InfoFeed类型。所以这里并不会执行构造器的初始化。显示的类型强制转换,需要极度小心,不要尝试调用一个你不知道类型的合约。

我们也可以使用function setFeed(InfoFeed _feed) { feed = _feed; }来直接进行赋值。.info.value(1).gas(8000)只是本地设置发送的数额和gas值,真正执行调用的是其后的括号.info.value(1).gas(8000)()。

如果被调用的合约不存在,或者是不包代码的帐户,或调用的合约产生了异常,或者gas不足,均会造成函数调用发生异常。

如果被调用的合约源码并不事前知道,和它们交互会有潜在的风险。当前合约会将自己的控制权交给被调用的合约,而对方几乎可以做任何事。即使被调用的合约是继承自一个已知的父合约,但继承的子合约仅仅被要求正确实现了接口。合约的实现,可以是任意的内容,由此会有风险。另外,准备好处理调用你自己系统中的其它合约,可能在第一调用结果未返回之前就返回了调用的合约。某种程度上意味着,被调用的合约可以改变调用合约的状态变量(state variable)来标记当前的状态。如,写一个函数,只有当状态变量(state variables)的值有对应的改变时,才调用外部函数,这样你的合约就不会有可重入性漏洞。

命名参数调用和匿名函数参数(Named Calls and Anonymous Function Paramters) 函数调用的参数,可以通过指定名字的方式调用,但可以以任意的顺序,使用方式是{}包含。但参数的类型和数量要与定义一致。

pragma solidity ^0.4.0;
contract C {
    function add(uint val1, uint val2) returns (uint) { return val1 + val2; }
    function g() returns (uint){
        // named arguments
        return add({val2: 2, val1: 1});
    }
}

省略函数名称(Omitted Function Parameter Names) 没有使用的参数名可以省略(一般常见于返回值)。这些名字在栈(stack)上存在,但不可访问。

pragma solidity ^0.4.0;
contract C {
    // omitted name for parameter
    function func(uint k, uint) returns(uint) {
        return k;
    }
}

payable标识的函数

函数上增加payable标识,即可接收ether,并会把ether存在当前合约,如下述示例中的deposit函数。

pragma solidity ^0.4.0;
contract supportPay{
    //存入一些ether用于后面的测试
    function deposit() payable{
    }
    //查询当前的余额
    function getBalance() constant returns(uint){
        return this.balance;
    }
}

在上面的代码中,你可以通过deposit()向当前合约存入ether,注意这是通过函数调用,在调用中通过address.call(某个方法).value(要发送的ether)来实现的

send()函数发送ether

地址对象中的send()可以向某地址直接进行支付,下面是一个向合约帐户支付的示例:

当我们使用address.send(ether to send)向某个地址转帐,如果是普通地址将会直接收到,就非常简单了。我们这里将用合约来模拟发送与接收:

pragma solidity ^0.4.0;
contract SendAndReceiveByContract{
    //fallback函数对应记录事件
    event fallbackTrigged(bytes data);
    //合约接收send()的 ether时,必须存在
    function() payable{fallbackTrigged(msg.data);}
    //存入一些ether用于后面的测试
    function deposit() payable{
    }
    //查询当前的余额
    function getBalance() constant returns(uint){
        return this.balance;
    }
    event SendEvent(address to, uint value, bool result);
    //使用send()发送ether
    function sendEther(){
            //使用this来模拟从另一个合约发送
        bool result = this.send(1);
        SendEvent(this, 1, result);
    }
}

在上述的代码中,我们先要使用deposit()合约存入一些ether,否则由于余额不足,调用send()函数将报错。存入ether后,我们调用sendEther(),使用send()向合约发送数据,将会触发下述事件:

SendEvent[
"0xc35f7ac1351648b0b8a699c5f07dd6a78f626714",
"1",
"true"
]
fallbackTrigged[
"0x"
]
```
可以看到,我们成功使用send()发送了1wei到合约。
这里需要特别注意的是,下面大家先记着,合约要接收通过send()函数发送的ether,有下面的限制:
如果我们要在合约中通过send()函数接收,就必须定义fallback函数,否则会抛异常。 fallback函数必须增加payable关键字,否则send()执行结果将会始终为false。 支付中可能的失败
* send()失败:由于调用者可以强制指定调用堆栈的深度,当调用的栈深超过指定值时,一般是1024;或者接收地址处理支付过程中out of gas。由于失败,此时的send()的结果是false。
* 合约的fallback():如果是合约地址,在执行send()时,会默认关联执行fallback()(如果存在这个函数)。这是EVM的默认行为,不可被阻止。所以这个函数引起out of gas或其它失败,整个交易被撤销。由于失败,此时的send()的结果是false。
* payable标识:细心的读者可能发现在deposit函数上有一个payable关键字,如果一个函数需要进行货币操作,必须要带上payable关键字,这样才能正常接收msg.value。
## 创建合约实例(Creating Contracts via `new`)
一个合约可以通过new关键字来创建一个合约。要创建合约的完整代码,必须提前知道,所以递归创建依赖是不可能的。
```js

pragma solidity ^0.4.0;

contract Account{

uint accId;
//construction?
function Account(uint accountId) payable{
    accId = accountId;
}

}

contract Initialize{

Account account = new Account(10);
function newAccount(uint accountId){
    account = new Account(accountId);
}
function newAccountWithEther(uint accountId, uint amount){
    account = (new Account).value(amount)(accountId);
}

}

从上面的例子可以看出来,可以在创建合约中,发送ether,但不能限制gas的使用。如果创建因为out-of-stack,或无足够的余额以及其它任何问题,会抛出一个异常。
## 赋值(Assignment)
#### 解构赋值和返回多个结果(Destructing Assignments and Returning Multip Values)
Solidity内置支持元组(tuple),也就是说支持一个可能的完全不同类型组成的一个列表,数量上是固定的(Tuple一般指两个,还有个Triple一般指三个)。
这种内置结构可以同时返回多个结果,也可用于同时赋值给多个变量。
```js

pragma solidity ^0.4.0;

contract C {

uint[] data;
function f() returns (uint, bool, uint) {
    return (7, true, 2);
}
function g() {
    // Declares and assigns the variables. Specifying the type explicitly is not possible.
    var (x, b, y) = f();
    // Assigns to a pre-existing variable.
    (x, y) = (2, 7);
    // Common trick to swap values -- does not work for non-value storage types.
    (x, y) = (y, x);
    // Components can be left out (also for variable declarations).
    // If the tuple ends in an empty component,
    // the rest of the values are discarded.
    (data.length,) = f(); // Sets the length to 7
    // The same can be done on the left side.
    (,data[3]) = f(); // Sets data[3] to 2
    // Components can only be left out at the left-hand-side of assignments, with
    // one exception:
    (x,) = (1,);
    // (1,) is the only way to specify a 1-component tuple, because (1) is
    // equivalent to 1.
}

}

#### 数组和自定义结构体的复杂性(Complication for Arrays and Struts)
对于非值类型,比如数组和数组,赋值的语法有一些复杂。
* 赋值给一个状态变量总是创建一个完全无关的拷贝。
* 赋值给一个局部变量,仅对基本类型,如那些32字节以内的静态类型(static types),创建一份完全无关拷贝。
* 如果是数据结构或者数组(包括bytes和string)类型,由状态变量赋值为一个局部变量,局部变量只是持有原始状态变量的一个引用。对这个局部变量再次赋值,并不会修改这个状态变量,只是修改了引用。但修改这个本地引用变量的成员值,会改变状态变量的值。
## 作用范围和声明(Scoping And Decarations)
一个变量在声明后都有初始值为字节表示的全0值。也就是所有类型的默认值是典型的零态(zero-state)。举例来说,默认的bool的值为false,uint和int的默认值为0。
对从byte1到byte32定长的字节数组,每个元素都被初始化为对应类型的初始值(一个字节的是一个字节长的全0值,多个字节长的是多个字节长的全零值)。对于变长的数组bytes和string,默认值则为空数组和空字符串。
函数内定义的变量,在整个函数中均可用,无论它在哪里定义)。因为Solidity使用了JavaScript的变量作用范围的规则。与常规语言语法从定义处开始,到当前块结束为止不同。由此,下述代码编译时会抛出一个异常,Identifier already declared。
```js

pragma solidity ^0.4.0;

contract ScopingErrors {

function scoping() {
    uint i = 0;
    while (i++ < 1) {
        uint same1 = 0;
    }
    while (i++ < 2) {
        uint same1 = 0;// Illegal, second declaration of same1
    }
}
function minimalScoping() {
    {
        uint same2 = 0;
    }
    {
        uint same2 = 0;// Illegal, second declaration of same2
    }
}
function forLoopScoping() {
    for (uint same3 = 0; same3 < 1; same3++) {
    }
    for (uint same3 = 0; same3 < 1; same3++) {// Illegal, second declaration of same3
    }
}
function crossFunction(){
    uint same1 = 0;//Illegal
}

}

另外的,如果一个变量被声明了,它会在函数开始前被初始化为默认值。所以下述例子是合法的。
```js

pragma solidity ^0.4.0;

contract C{

function foo() returns (uint) {
    // baz is implicitly initialized as 0
    uint bar = 5;
    if (true) {
        bar += baz;
    } else {
        uint baz = 10;// never executes
    }
    return bar;// returns 5
}

}

## 异常(Excepions)
有一些情况下,异常是自动抛出来的(见下),你也可以使用throw来手动抛出一个异常。抛出异常的效果是当前的执行被终止且被撤销(值的改变和帐户余额的变化都会被回退)。异常还会通过Solidity的函数调用向上冒泡(bubbled up)传递。(send,和底层的函数调用call,delegatecall,callcode是一个例外,当发生异常时,这些函数返回false)。
捕捉异常是不可能的(或许因为异常时,需要强制回退的机制)。
在下面的例子中,我们将如展示如何使用throw来回退转帐,以及演示如何检查send的返回值。
```js

pragma solidity ^0.4.0;

contract Sharer {

function sendHalf(address addr) payable returns (uint balance) {
    if (!addr.send(msg.value / 2))
        throw; // also reverts the transfer to Sharer
    return this.balance;
}

}

下一节:Solidity中合约有点类似面向对象语言中的类。合约中有用于数据持久化的状态变量(state variables),和可以操作他们的函数。调用另一个合约实例的函数时,会执行一个EVM函数调用,这个操作会切换执行时的上下文,这样,前一个合约的状态变量(state variables)就不能访问了。