Solidity 其它类型及操作

Solidity 的其它复杂类型,包括结构体、映射/字典等。

结构体(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

映射/字典(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);
    }
}

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

左值的相关运算符

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

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

特殊的运算符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的引用赋值

基本类型间的转换

语言中经常会出现类型转换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

类型推断(Type Deduction)

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

uint24 x = 0x123;
var y = x;

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

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