Solidity 官方文档中文版(三)

赋值(Assignment)

解构赋值和返回多个结果(Destructing Assignments and Returning Multip Values)

Solidity内置支持元组(tuple),也就是说支持一个可能的完全不同类型组成的一个列表,数量上是固定的(Tuple一般指两个,还有个Triple一般指三个)。

这种内置结构可以同时返回多个结果,也可用于同时赋值给多个变量。

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),创建一份完全无关拷贝。
  • 如果是数据结构或者数组(包括bytesstring)类型,由状态变量赋值为一个局部变量,局部变量只是持有原始状态变量的一个引用。对这个局部变量再次赋值,并不会修改这个状态变量,只是修改了引用。但修改这个本地引用变量的成员值,会改变状态变量的值。

表达式的执行顺序(Order of Evaluation of Expressions)

表达式的求值顺序并不是确定的,更正式的说法是,表达式树一个节点的某个节点在求值时的顺序是不确定的,但是它肯定会比节点本身先执行。我们仅仅保证语句(statements)按顺序执行和布尔表达式的短路运算的支持。查看运算符的执行顺序了解更多。


创建合约实例(Creating Contracts via `new`)

一个合约可以通过new关键字来创建一个合约。要创建合约的完整代码,必须提前知道,所以递归创建依赖是不可能的。

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,或无足够的余额以及其它任何问题,会抛出一个异常。


函数调用(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;
    }
}

参考资料


  1. 如何使用Remix向合约发送ether参见这里。http://me.tryblockchain.org/%E6%94%AF%E4%BB%98%E7%9B%B8%E5%85%B3.html ↩


控制结构

不支持switchgoto,支持ifelsewhiledoforbreakcontinuereturn?:

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

需要注意的是,这里没有像C语言,和javascript里的非Boolean类型的自动转换,比如if(1){...}在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)。返回结果的数量需要与定义的一致。


地址相关(Address Related)

.balance (uint256)

Address的余额,以wei为单位。

.transfer(uint256 amount)

发送给定数量的ether,以wei为单位,到某个地址。失败时抛出异常。

.send(uint256 amount) returns (bool):

发送给定数量的ether,以wei为单位,到某个地址。失败时返回false

.call(...) returns (bool)

发起底层的call调用。失败时返回false

.callcode(...) returns (bool)

发起底层的callcode调用,失败时返回false

.delegatecall(...) returns (bool)

发起底层的delegatecall调用,失败时返回false

更多信息参加Address章节1

使用send方法需要注意,调用栈深不能超过1024,或gas不足,都将导致发送失败。使用为了保证你的ether安全,要始终检查返回结果。当用户取款时,使用transfer或使用最佳实践的模式2

合约相关

this(当前合约的类型)

当前合约的类型,可以显式的转换为Address

selfdestruct(address recipt):

销毁当前合约,并把它所有资金发送到给定的地址。

另外,当前合约里的所有函数均可支持调用,包括当前函数本身。

参考资料


  1. 更新关于地址的call,callcode,delegateCall的介绍。http://me.tryblockchain.org/Solidity-call-callcode-delegatecall.html ↩

  2. http://solidity.readthedocs.io/en/develop/common-patterns.html#withdrawal-from-contracts ↩


数学和加密函数(Mathematical and Cryptographic Functions)

asser(bool condition):

如果条件不满足,抛出异常。

addmod(uint x, uint y, uint k) returns (uint):

计算(x + y) % k。加法支持任意的精度。但不超过(wrap around?)2**256

mulmod(uint x, uint y, uint k) returns (uint):

计算(x * y) % k。乘法支持任意精度,但不超过(wrap around?)2**256

keccak256(...) returns (bytes32):

使用以太坊的(Keccak-256)计算HASH值。紧密打包。

sha3(...) returns (bytes32):

等同于keccak256()。紧密打包。

sha256(...) returns (bytes32):

使用SHA-256计算HASH值。紧密打包。

ripemd160(...) returns (bytes20):

使用RIPEMD-160计算HASH值。紧密打包。

ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address):

通过签名信息恢复非对称加密算法公匙地址。如果出错会返回0,附录提供了一个例子1.

revert()

取消执行,并回撤状态变化。

需要注意的是参数是“紧密打包(tightly packed)”的,意思是说参数不会补位,就直接连接在一起的。下面来看一个例子:

keccak256("ab", "c")
keccak256("abc")
//hex
keccak256(0x616263)
keccak256(6382179)
//ascii
keccak256(97, 98, 99)

上述例子中,三种表达方式都是一致的。

如果需要补位,需要明确的类型转换,如keccak256("\x00\x12")等同于keccak256(uint16(0x12))

需要注意的是字面量会用,尽可能小的空间来存储它们。比如,keccak256(0) == keccak256(uint8(0))keccak256(0x12345678) == keccak256(uint32(0x12345678))

私链(private blockchain)上运行sha256,ripemd160ecrecover可能会出现Out-Of-Gas报错。因为它们实现了一种预编译的机制,但合约要在收到第一个消息后才会存在。向一个不存在的合约发送消息,非常昂贵,所以才会导致Out-Of-Gas的问题。一种解决办法是每个在你真正使用它们前,先发送1 wei到这些合约上来完成初始化。在官方和测试链上没有这个问题。

参考资料


  1. 使用ecrecover实现签名检验的例子。http://me.tryblockchain.org/web3js-sign-ecrecover-decode.html ↩


特殊变量及函数(Special Variables and Functions)

有一些变量和函数存在于¥全局上下文中。主要用来提供一些区块链当前的信息。

区块和交易的属性(Block And Transaction Properties)

  • block.blockhash(uint blockNumber) returns (bytes32),给定区块号的哈希值,只支持最近256个区块,且不包含当前区块。
  • block.coinbase (address) 当前块矿工的地址。
  • block.difficulty (uint)当前块的难度。
  • block.gaslimit (uint)当前块的gaslimit
  • block.number (uint)当前区块的块号。
  • block.timestamp (uint)当前块的时间戳。
  • msg.data (bytes)完整的调用数据(calldata)。
  • msg.gas (uint)当前还剩的gas
  • msg.sender (address)当前调用发起人的地址。
  • msg.sig (bytes4)调用数据的前四个字节(函数标识符)。
  • msg.value (uint)这个消息所附带的货币量,单位为wei
  • now (uint)当前块的时间戳,等同于block.timestamp
  • tx.gasprice (uint) 交易的gas价格。
  • tx.origin (address)交易的发送者(完整的调用链)

msg的所有成员值,如msg.sender,msg.value的值可以因为每一次外部函数调用,或库函数调用发生变化(因为msg就是和调用相关的全局变量)。

如果你想在库函数中,用msg.sender实现访问控制,你需要将msg.sender做为参数(就是说不能使用默认的msg.value,因为它可能被更改)。

为了可扩展性的原因,你只能查最近256个块,所有其它的将返回0.


时间单位(Time Units)

seconds,minutes,hours,days,weeks,years均可做为后缀,并进行相互转换,默认是seconds为单位。

默认规则如下:

  • 1 == 1 seconds
  • 1 minutes == 60 seconds
  • 1 hours == 60 minutes
  • 1 days == 24 hours
  • 1 weeks = 7 days
  • 1 years = 365 days

如果你需要进行使用这些单位进行日期计算,需要特别小心,因为不是每年都是365天,且并不是每天都有24小时,因为还有闰秒。由于无法预测闰秒,必须由外部的oracle来更新从而得到一个精确的日历库(内部实现一个日期库也是消耗gas的)。

后缀不能用于变量。如果你想对输入的变量说明其不同的单位,可以使用下面的方式。

pragma solidity ^0.4.0;

contract DeleteExample{
    
    function nowInSeconds() returns (uint256){
        return now;
    }
    
    function f(uint start, uint daysAfter) {
        if (now >= start + daysAfter * 1 days) { 
            
        }
    }
}

货币单位(Ether Units)

一个字面量的数字,可以使用后缀wei,finney,szaboether来在不同面额中转换。不含任何后缀的默认单位是wei。如2 ether == 2000 finney的结果是true

pragma solidity ^0.4.0;

contract EthUnit{
    uint a;
    
    function f() returns (bool){
      if (2 ether == 2000 finney){
          return true;
      }
      
      return false;
    }
}

类型推断(Type Deduction)

为了方便,并不总是需要明确指定一个变量的类型,编译器会通过第一个向这个对象赋予的值的类型来进行推断1

uint24 x = 0x123;
var y = x;

函数的参数,包括返回参数,不可以使用var这种不指定类型的方式。

需要特别注意的是,由于类型推断是根据第一个变量进行的赋值。所以代码for (var i = 0; i < 2000; i++) {}将是一个无限循环,因为一个uint8i的将小于2000

pragma solidity ^0.4.4;

contract Test{
    function a() returns (uint){
      uint count = 0;
        for (var i = 0; i < 2000; i++) {
            count++;
            if(count >= 2100){
                break;
            }
        }
        return count;
    }
}

参考资料


  1. http://solidity.readthedocs.io/en/develop/types.html#type-deduction ↩


基本类型间的转换

语言中经常会出现类型转换1。如将一个数字字符串转为整型,或浮点数。这种转换常常分为,隐式转换和显式转换。

隐式转换

如果运算符支持两边不同的类型,编译器会尝试隐式转换类型,同理,赋值时也是类似。通常,隐式转换需要能保证不会丢失数据,且语义可通。如uint8可以转化为uint16,uint256。但int8不能转为uint256,因为uint256不能表示-1

此外,任何无符号整数,可以转换为相同或更大大小的字节值。比如,任何可以转换为uint160的,也可以转换为address

显式转换

如果编译器不允许隐式的自动转换,但你知道转换没有问题时,可以进行强转。需要注意的是,不正确的转换会带来错误,所以你要进行谨慎的测试。

pragma solidity ^0.4.0;

contract DeleteExample{
    uint a;
    
    function f() returns (uint){
      int8 y = -3;
      uint x = uint(y);
      return x;
    }
}

如果转换为一个更小的类型,高位将被截断。

uint32 a = 0x12345678;
uint16 b = uint16(a); // b will be 0x5678 now

参考资料


  1. http://solidity.readthedocs.io/en/develop/types.html#conversions-between-elementary-types ↩


左值的相关运算符

左值1,是指位于表达式左边的变量,可以是与操作符直接结合的形成的,如自增,自减;也可以是赋值,位运算。

可以支持操作符有:-=,+=,*=,%=,|=,&=,^=,++,--

特殊的运算符delete

delete运算符,用于将某个变量重置为初始值。对于整数,运算符的效果等同于a = 0。而对于定长数组,则是把数组中的每个元素置为初始值,变长数组则是将长度置为0。对于结构体,也是类似,是将所有的成员均重置为初始值。

delete对于映射类型几乎无影响,因为键可能是任意的,且往往不可知。所以如果你删除一个结构体,它会递归删除所有非mapping的成员。当然,你是可以单独删除映射里的某个键,以及这个键映射的某个值。

需要强调的是delete a的行为更像赋值,为a赋予一个新对象。我们来看看下文的示例:

pragma solidity ^0.4.0;

contract DeleteExample {
    uint data;
    uint[] dataArray;

    function f() {
        //值传递
        uint x = data;
        //删除x不会影响data
        delete x;

        //删除data,同样也不会影响x,因为是值传递,它存的是一份原值的拷贝。
        delete data; 

        //引用赋值
        uint[] y = dataArray;

        //删除dataArray会影响y,y也将被赋值为初值。
        delete dataArray;

        //下面的操作为报错,因为删除是一个赋值操作,不能向引用类型的storage直接赋值从而报错
        //delete y;
    }
}

通过上面的代码,我们可以看出,对于值类型,是值传递,删除x不会影响到data,同样的删除data也不会影响到x。因为他们都存了一份原值的拷贝。

而对于复杂类型略有不同,复杂类型在赋值时使用的是引用传递。删除会影响所有相关变量。比如上述代码中,删除dataArray同样会影响到y

由于delete的行为更像是赋值操作,所以不能在上述代码中执行delete y,因为不能对一个storage的引用赋值2

参考资料


  1. http://solidity.readthedocs.io/en/develop/types.html#operators-involving-lvalues ↩

  2. strorage的引用不能赋值原因,可以看看关于数据位置的这篇文章:http://me.tryblockchain.org/solidity-data-location.html。 ↩


映射/字典(mappings)

映射或字典类型,一种键值对的映射关系存储结构。定义方式为mapping(_KeyType => _KeyValue)。键的类型允许除映射外的所有类型,如数组,合约,枚举,结构体。值的类型无限制。

映射可以被视作为一个哈希表,其中所有可能的键已被虚拟化的创建,被映射到一个默认值(二进制表示的零)。但在映射表中,我们并不存储键的数据,仅仅存储它的keccak256哈希值,用来查找值时使用。

因此,映射并没有长度,键集合(或列表),值集合(或列表)这样的概念。

映射类型,仅能用来定义状态变量,或者是在内部函数中作为storage类型的引用。引用是指你可以声明一个,如var storage mappVal的用于存储状态变量的引用的对象,但你没办法使用非状态变量来初始化这个引用。

可以通过将映射标记为public,来让Solidity创建一个访问器。要想访问这样的映射,需要提供一个键值做为参数。如果映射的值类型也是映射,使用访问器访问时,要提供这个映射值所对应的键,不断重复这个过程。下面来看一个例子:

contract MappingExample{
    mapping(address => uint) public balances;
    
    function update(uint amount) returns (address addr){
        balances[msg.sender] = amount;
        return msg.sender;
    }
}

由于调试时,你不一定方便知道自己的发起地址,所以把这个函数,略微调整了一下,以在调用时,返回调用者的地址。编译上述合同后,可以先调用update(),执行成功后,查看调用信息,能看到你更新的地址,这样再查一下这个地址的在映射里存的值。

如果你想通过合约进行上述调用。

pragma solidity ^0.4.0;

//file indeed for compile
//may store in somewhere and import
contract MappingExample{
    mapping(address => uint) public balances;
    
    function update(uint amount) returns (address addr){
        balances[msg.sender] = amount;
        return msg.sender;
    }
}


contract MappingUser{
    
    address conAddr;
    address userAddr;
    
    function f() returns (uint amount){
    //address not resolved!
    //tringing
        conAddr = hex"0xf2bd5de8b57ebfc45dcee97524a7a08fccc80aef";
        userAddr = hex"0xca35b7d915458ef540ade6068dfe2f44e8fa733c";
        
        return MappingExample(conAddr).balances(userAddr);
    }
}

映射并未提供迭代输出的方法,可以自行实现一个数据结构。参见iterable mapping。


结构体(struct)

Solidity提供struct来定义自定义类型。我们来看看下面的例子:

pragma solidity ^0.4.0;

contract CrowdFunding{
    struct Funder{
        address addr;
        uint amount;
    }
    
    struct Campaign{
        address beneficiary;
        uint goal;
        uint amount;
        uint funderNum;
        mapping(uint => Funder) funders;
    }
    
    uint compaingnID;
    mapping (uint => Campaign) campaigns;
    
    function candidate(address beneficiary, uint goal) returns (uint compaingnID){
        // initialize
        campaigns[compaingnID++] = Campaign(beneficiary, goal, 0, 0);
    }
    
    function vote(uint compaingnID) payable {
        Campaign c = campaigns[compaingnID];
        
        //another way to initialize
        c.funders[c.funderNum++] = Funder({addr: msg.sender, amount: msg.value});
        c.amount += msg.value;
    }
    
    function check(uint comapingnId) returns (bool){
        Campaign c = campaigns[comapingnId];
        
        if(c.amount < c.goal){
            return false;
        }
        
        uint amount = c.amount;
        // incase send much more
        c.amount = 0;
        if(!c.beneficiary.send(amount)){
            throw;
        }
        return true;
    }
}

上面的代码向我们展示的一个简化版的众筹项目,其实包含了一些struct的使用。struct可以用于映射和数组中作为元素。其本身也可以包含映射和数组等类型。

我们不能声明一个struct同时将这个struct作为这个struct的一个成员。这个限制是基于结构体的大小必须是有限的。

虽然数据结构能作为一个mapping的值,但数据类型不能包含它自身类型的成员,因为数据结构的大小必须是有限的。

需要注意的是在函数中,将一个struct赋值给一个局部变量(默认是storage类型),实际是拷贝的引用,所以修改局部变量值时,会影响到原变量。

当然,你也可以直接通过访问成员修改值,而不用一定赋值给一个局部变量,如campaigns[comapingnId].amount = 0


数组

数组可以声明时指定长度,或者是变长的。对storage1的数组来说,元素类型可以是任意的,类型可以是数组,映射类型,数据结构等。但对于memory1的数组来说。如果函数是对外可见的2,那么函数参数不能是映射类型的数组,只能是支持ABI的类型3

一个类型为T,长度为k的数组,可以声明为T[k],而一个变长的数组则声明为T[]
你还可以声明一个多维数据,如一个类型为uint的数组长度为5的变长数组,可以声明为uint[][5] x。需要留心的是,相比非区块链语言,多维数组的长度声明是反的。

要访问第三个动态数据的,第二个元素,使用x[2][1]。数组的序号是从0开始的,序号顺序与定义相反。

bytesstring是一种特殊的数组。bytes类似byte[],但在外部函数作为参数调用中,会进行压缩打包,更省空间,所以应该尽量使用bytes4string类似bytes,但不提供长度和按序号的访问方式。

由于bytesstring,可以自由转换,你可以将字符串s通过bytes(s)转为一个bytes。但需要注意的是通过这种方式访问到的是UTF-8编码的码流,并不是独立的一个个字符。比如中文编码是多字节,变长的,所以你访问到的很有可能只是其中的一个代码点。

类型为数组的状态变量,可以标记为public类型,从而让Solidity创建一个访问器,如果要访问数组的某个元素,指定数字下标就好了。

创建一个数组

可使用new关键字创建一个memory的数组。与stroage数组不同的是,你不能通过.length的长度来修改数组大小属性。我们来看看下面的例子:

pragma solidity ^0.4.0;

contract C {
    function f() {
        //创建一个memory的数组
        uint[] memory a = new uint[](7);
        
        //不能修改长度
        //Error: Expression has to be an lvalue.
        //a.length = 100;
    }
    
    //storage
    uint[] b;
    
    function g(){
        b = new uint[](7);
        //可以修改storage的数组
        b.length = 10;
        b[9] = 100;
    }
}

在上面的代码中,f()方法尝试调整数组a的长度,编译器报错Error: Expression has to be an lvalue.。但在g()方法中我们看到可以修改5

字面量及内联数组

数组字面量,是指以表达式方式隐式声明一个数组,并作为一个数组变量使用的方式。下面是一个简单的例子:

pragma solidity ^0.4.0;

contract C {
    function f() {
        g([uint(1), 2, 3]);
    }
    function g(uint[3] _data) {
        // ...
    }
}

通过数组字面量,创建的数组是memory的,同时还是定长的。元素类型则是使用刚好能存储的元素的能用类型,比如代码里的[1, 2, 3],只需要uint8即可存储。由于g()方法的参数需要的是uint(默认的uint表示的其实是uint256),所以要使用uint(1)来进行类型转换。

还需注意的一点是,定长数组,不能与变长数组相互赋值,我们来看下面的代码:

pragma solidity ^0.4.0;

contract C {
    function f() {
        // The next line creates a type error because uint[3] memory
        // cannot be converted to uint[] memory.
        uint[] x = [uint(1), 3, 4];
}

限制的主要原因是,ABI不能很好的支持数组,已经计划在未来移除这样的限制。(当前的ABI接口,不是已经能支持数组了?)

数组的属性和方法

length属性

数组有一个.length属性,表示当前的数组长度。storage的变长数组,可以通过给.length赋值调整数组长度。memory的变长数组不支持。

不能通过访问超出当前数组的长度的方式,来自动实现上面说的这种情况。memory数组虽然可以通过参数,灵活指定大小,但一旦创建,大小不可调整,对于变长数组,可以通过参数在编译期指定数组大小。

push方法

storage的变长数组和bytes都有一个push(),用于附加新元素到数据末端,返回值为新的长度。

pragma solidity ^0.4.0;

contract C {
    uint[] u;
    bytes b;
    
    function testArryPush() returns (uint){
        uint[3] memory a = [uint(1), 2, 3];
        
        u = a;
        
        return u.push(4);
    }
    
    function testBytesPush() returns (uint){
        b = new bytes(3);
        return b.push(4);
    }
}

限制的情况

当前在外部函数中,不能使用多维数组。

另外,基于EVM的限制,不能通过外部函数返回动态的内容。

pragma solidity ^0.4.0;

contract C { 
    function f() returns (uint[]) { 
    }
}

在上面的例子中,通过web.js调用能返回数据,但在Solidity中不能返回数据。一种临时的解决办法,是使用一个非常大的静态数组。

pragma solidity ^0.4.0;

contract ArrayContract {
    //the orginal length of m_aLotOfIntegers is 2**20
    //run it cause a out of gas,so change it to a much smaller 2**2 for test
    uint[2**2] m_aLotOfIntegers;
    // Note that the following is not a pair of arrays but an array of pairs.
    bool[2][] m_pairsOfFlags;
    // newPairs is stored in memory - the default for function arguments

    function setAllFlagPairs(bool[2][] newPairs) {
        // assignment to a storage array replaces the complete array
        m_pairsOfFlags = newPairs;
    }

    function setFlagPair(uint index, bool flagA, bool flagB) {
        // access to a non-existing index will throw an exception
        m_pairsOfFlags[index][0] = flagA;
        m_pairsOfFlags[index][1] = flagB;
    }

    function changeFlagArraySize(uint newSize) {
        // if the new size is smaller, removed array elements will be cleared
        m_pairsOfFlags.length = newSize;
    }

    function clear() {
        // these clear the arrays completely
        delete m_pairsOfFlags;
        delete m_aLotOfIntegers;
        // identical effect here
        m_pairsOfFlags.length = 0;
    }

    function addFlag(bool[2] flag) returns (uint) {
        return m_pairsOfFlags.push(flag);
    }

    function createMemoryArray(uint size)  {
        // Dynamic memory arrays are created using `new`:
        bool[2][] memory arrayOfPairs = new bool[2][](size);
        m_pairsOfFlags = arrayOfPairs;
    }
}

更多请查看这里的重新梳理: http://me.tryblockchain.org/solidity-array.html

参考资料


  1. 变量声明时定义的数据位置属性。可以是memory的,也可以为storage的,详见:http://me.tryblockchain.org/data-location.org ↩

  2. 函数的可见性说明,详见这里: http://me.tryblockchain.org/solidity-function-advanced1.html ↩

  3. 了解更多关于ABI的信息,https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI#types ↩

  4. 因为字节数组在使用时会padding,所以不节省空间。https://github.com/chriseth/solidity/blob/48588b5c4f21c8bb7c5d4319b05a93ee995b457d/_docs/faq_basic.md#what-is-the-difference-between-bytes-and-byte ↩

  5. http://stackoverflow.com/questions/33839122/in-ethereum-solidity-when-changing-an-array-length-i-get-value-must-be-an-lva ↩

感谢您的支持


你可能感兴趣的:(区块链)