引用类型
复杂类型,比如一些并不是总能适应与256bits的类型,必须比我们见过的值类型更加慎重的处理。因为复制他们需要较昂贵的花费,所有我们不得不思考我们是否希望将其存储在内存里(并不长久的)或者存储里(状态变量就在这里保存)
数据位置
每一种复杂类型,比如数组或者结构体,都有一个附加的注释——“数据位置”,有关于它在哪里存放的。有3种存放数据的地方:内存(memory),存储(storage)和 calldata。其中calldate仅仅对于外部(external)合约函数是合法的并且对于参数类型有要求。calldata是一块不可修改,不能长久的存放函数参数的区域,它的特征表现更近似与内存(memory)。
0.5.0以前的版本所有数据的存储位置是可以根据其类型进行省略的,但是之后所有复杂类型必须指明一个确定的数据位置。
数据位置非常重要,因为他们改变了赋值的操作方式:在内存与存储之间赋值,或者存储向状态变量,(甚至是从其他状态变量)赋值都会创建一份独立的copy。然而状态变量向局部存储变量(local storage variables)赋值是仅仅传递一个引用,而且这个引用总是指向状态变量,因此后者改变的同时前者也会改变。
另一方面,从一个内存存储的引用类型向另一个内存存储的引用类型赋值并不会创建拷贝。
pragma solidity ^0.4.0;
contract C {
uint[] x;// the data location of x is storage
// the data location of memoryArray is memory
function f(uint[] memoryArray) public {
x = memoryArray;//工作,将整个数组拷贝到存储中
var y = x;//工作,赋予一个指针,y的数据位置在存储
y[7];// fine returns the 8th element
y.length = 2; // 通过y来改变x
delete x; // fine 清除掉x数组,也修改了y
// the following does not work;it would need to create a new
// temporary/unnamed array in storage, but storage is "statically"
// allocated : y = memoryArray;
// This doesn't work either , since it would "reset" the pointer,but there
// is no sensible location it could point to,
// delete y;
g(x); // calls g,handing over a reference to x
h(x); // calls h and creates an independent,temporary copy in memory
}
function g(uint[] storage storageArray) internal {}
function h(uint[] memoryArray) public {}
}
总结
强制指定的数据位置:
- 外部函数的参数(不包含返回参数) : calldata
- 状态变量: storage
默认数据位置:
- 函数参数(包括返回参数):memory
- 所有其它局部变量:storage
数组
数组可以在声明是指定长度,也可以动态的调整大小。
对于存储的数组来说,元素类型可以是任意的(即元素也可以是数组类型,映射类型或结构体)。
对于内存的数组来说,元素类型不能是映射类型,如果作为 public 函数的参数,它只能是ABI类型。
一个元素类型为T,固定长度为 k 的数组可以声明为 T[k], 而动态数组声明为T[]。举个例子(注意这里与其他语言相比,数组长度的声明是反的),一个存储5个元素类型为uint的动态数组的数组,应声明为 uint[ ][5]。要想获取第3个动态数组中的第二 个uint值,需要使用 x[2][1] (数组下标是从0开始的,且访问数组是的下标顺序与声明时相反,也就是说,x[2]是从右减少一级)
bytes 和 string 是两个特殊的数组。 bytes 近似与 byte[] , 但它在calldata中会被打包得很紧(将元素连续的存放起来,不会按每32字节一单元的方式来存放)。
string和bytes相同,但是(暂时)不允许用长度或索引来访问。
note:
如果想要访问一字节表示的 string s , 请使用bytes(s).length
/bytes(s)[7] = 'x';
注意这时你访问的是UTF-8形式的低级bytes类型,而非单个的字符。
可以将数组标识为 public , 从而让Solidity 创建一个 getter。
之后必须使用数字下标作为参数来访问getter。
创建内存数组
可以使用 new 关键字在内存中创建变长数组。
与存储数组相反的是,你 不能 通过修改成员变量 .length 改变内存数组的大小。
pragma solidity ^0.4.16;
contract C{
function f(uint len) public pure {
uing[] memeory a = new uint[](7);
bytes memory b = new bytes(len);
// 这里我们有a.length == 7 and b.length == len
a[6] = 8;
}
}
数组字面常数/内联数组
数组字面常数是写作表达式形式的数组,并且不会立即赋值给变量.
pragma solidity ^0.4.16;
contract C {
function f() public pure {
g([uint(1),2,3]);
}
function g(uint[3] _data) public pure {
//...
}
}
数组字面常数是一种定长的内存数组类型,它的基础类型由其中元素的普通类型决定。
例如,[1,2,3]的类型是 uint8[3] memory, 因为这种的每个字面常数的类型都是uint8.
正因为如此,有必要将上面这个例子中的第一个元素转换成uint类型。
目前需要注意的是,定长的内存数组并不能赋值给变长的内存数组,下面是个反例:
// 该代码无法编译
pragma solidity ^0.4.0;
contract C{
function f() public{
// 该行引发了一个类型错误,因为uint3 memory
// 不能转换成 uint[] memory
uint[] x = [uint(1),3,4];
}
}
成员
length:
数组由 length 成员变量表示当前数组的长度。
动态数组可以在storage中通过改变成员变量 .length 改变数组大小.
并不能通过访问超出当前数组巡航毒的方式实现自动扩展数组的长度.
一经创建,memory数组大小就是固定的(但另一方面却是动态的,也就是说它依赖与运行时的参数)(设定好后.length的值就固定了)
push:
变长的存储(storage)数组以及bytes类型(而非String 类型)都有一个叫做 push 的成员函数,它用来附加新的元素到数组末尾.这个函数将返回新的数组长度.
warning:
在外部函数中目前还不能使用多维数组
由于EVM的限制,不能通过外部函数调用返回动态的内容:
比如如果通过web3.js 调用 contract C{function f() returns(uint[]) { … } }中的 f 函数,它会返回一些内容,但通过Solidity 不可以.
目前唯一的变通方法是使用大型的静态数组.
pragma solidity ^0.4.16;
contract ArrayContract {
uint[2**20] m_aLotOfIntegers;
// 注意下面的代码并不是一对动态数组,
// 而是一个数组元素为一对变量的动态数组
bool[2][] m_pairsOfFlags;
// newPairs 存储在 memory 中 —— 函数参数默认的存储位置
function setAllFlagPairs(bool[2][] newPairs) public {
// 向一个storage的数组赋值就会替代整个数组
m_pairsOfFlags = newPairs;
}
function setFlagPair(uint index, bool flagA,bool flagB) public{
//访问一个不存在的数组下标会引发一个异常
m_pairsOfFlags[index][0] = flagA;
m_pairsOfFlags[index][1] = flagB;
}
function changeFlagArraySize(uint newSize) public {
// 如果newSize 更小,那么超出的元素会被清除
m_pairsOfFlags.length = newSize;
}
function clear() public {
// 这些代码会将数组全部清空
delete m_pairsOfFlags;
delete m_aLotOfIntegers;
// 这里也是实现同样的功能
m_pairsOfFlags.length = 0;
}
bytes m_byteData;
function byteArrays(bytes data) public {
// 字节的数组(语言 意义中的byte的复数"bytes")不一样,因为他们不是填充式存储的
// 但可以当做和 "uint8[]"一样对待
m_byteData = data;
m_byteData.length += 7;
m_byteData[3] = byte(8);
delete m_byteData[2];
}
function addFlag(bool[2] flag) public returns (uint) {
return m_pairsOfFlags.push(flag);
}
function createMemoryArray(uint size) public pure returns (bytes) {
// 使用 'new' 创建动态 memory 数组;
uint[2][] memory arrayOfPairs = new uint[2][](size);
// 创建一个动态字节数组;
bytes memory b = new bytes(200);
for (uint i = 0;i < b.length; i++)
b[i] = byte(i);
return b;
}
}
结构体
solidity支持通过构造结构体的形式定义新的类型,以下是一个结构体使用的示例:
pragma solidity ^0.4.11;
contract CrowdFunding {
// 定义的新类型包含两个属性
struct Funder {
address addr;
uint amount;
}
struct Campaign {
address beneficiary;
uint fundingGoal;
uint numFunders;
uint amount;
mapping (uint => Funder) funders;
}
uint numCampaigns;
mapping (uint => Campaign) campaigns;
function newCampaign (address beneficiary, uint goal) public retrurns (uint campaignID) {
campaignID = numCampaigns++; // campaignID作为一个变量返回
// 创建新的结构体示例,存储在 storage 中,我们先不关注映射类型
campaigns[campaignID] = Campaign(beneficiary, goal, 0 ,0);
}
function contribute(uint campaignID) pubulic payable {
Campaign storage c = campaigns[campaignID];
// 以给定的值初始化,创建一个新的临时 memory 结构体
// 并将其拷贝到 storage 中.
// 注意你也可以使用 Funder(msg.sender,msg.value) 来初始化.
c.funders[c.numFunders++] = Funder({addr: msg.sender, amount: msg.value});
c.amount += msg.value;
}
function checkGoalReached(uint campaignID) public returns (bool reached) {
Campaign storage c = campaigns[campaignID];
if (c.amount < c.fundingGoal)
return false;
uint amount = c.amount;
c.amount = 0;
c.beneficiary.transfei(amount);
return true;
}
}
上面的合约是一个简化版的众筹合约,但已足够让我们理解结构体的基础概念.结构体类型可以作为元素用在映射和数组中,其自身也可以包含映射和数组作为成员变量.
尽管结构体本身可以作为映射的值类型成员,但它并不能包含自身.
这个限制有必要因为结构体的大小必须是有限的.
注意在函数中使用结构体时,一个结构体是如何赋值给一个局部变量(默认存储位置是存储)的.
在这个过程中并没有拷贝这个结构体,而是保存一个引用,所以对局部变量成员的赋值实际上是会被写入状态的.
当然,你也可以直接访问结构体的成员而不用将其赋值给一个局部变量,就像这样,
campaigns[campaignID].amount = 0
映射
映射类型在声明时的形式为 mapping(_KeyType => _ValueType)
.
其中,_KeyType 可以是除了映射,变长数组,合约,枚举以及结构体以外的几乎所有类型.
_ValueType 可以是包括映射类型在内的任何类型.
映射可以视作哈希表,它们在实际的初始化过程中创建每个可能的 key,
并将其映射到字节形式全是0的值:一个类型的 默认值. 然而下面是映射与哈希表不同的地方:
在映射中,实际上并不存储 key,而是存储它的 keccak256 哈希值,以便查询实际的值.
正因为如此,映射是没有长度的,也没有key的集合或value的集合的概念.
只有状态变量(或者在internal函数中的对于存储变量的引用)可以使用映射类型.
可以将映射声明为public,然后来让Solidity创建一个getter.
_KeyType 将成为 getter 的必须参数,并且getter会返回 _ValueType.
_ValueType 也可以是一个映射.这时在使用 getter 时将需要递归地传入 每个 _KeyType 参数.
pragma solidity ^0.4.0 ;
contract MappingExample {
mapping(address => uint) public balances;
function update(uint newBalance) public {
balances[msg.sender] = newBalance;
}
}
contract MappingUser {
function f() public returns (uint){
MappingExample m = new MappingExample();
m.update(100);
return m.balances(this);
}
}
note:
递归不支持迭代,但可以在此之上实现一个这样的数据结构
参见 可迭代的映射
涉及LValues的运算符
如果 a 是一个LValue(即一个变量或者其他可以被复制的东西),以下运算符都可以使用简写:
a += e
或者其他的 -= ,*=,/=,等等
a++ 和 a–相当于 a+1 和 a-1,但是表达式本身的值等于 a 在计算之前的值.
与之相反, –a 和 ++a 虽然最终a的结果与前面相同,但是表达式返回的值是计算之后的值.
删除
delete a 的结果是将 a 的类型在初始化时的值赋值给 a.即对于整形变量来说,相当于 a = 0
但 delete也适用于数组, 对于动态数组来说,是将数组的长度设为0,而对于静态数组来说,是将数组中的所有元素重置.
如果对象是结构体,则将结构体中的所有属性重置.
delete 对整个映射是无效的(因为映射的key可以是任意的,通常也是未知的).
因此在你删除一个结构体时,结果将重置所有的非映射属性,这个过程是递归进行的,除非它们是映射.
然而,单个key及其映射的值是可以被删除的.
理解 delete a 的效果就像是给 a 赋值一样重要,换句话说,这相当于在 a 中存储了一个新的对象.
pragma solidity ^0.4.0;
contract DeleteExample {
uint data;
uint[] dataArray;
function f() public {
uint x = data;
delete x; // 将 x 设为 0,并不影响data
delete data; // 将 data 设为 0,并不影响 x,因为它仍然有个副本
uint[] storage y = dataArray;
delete dataArray;
// 将dataArray.length 设为0,但由于 uint[] 是一个复杂的对象,y 也将受到影响
// 因为它是一个存储位置是 storage 的对象的别名
// 另一方面:"delete y"是非法的,引用了 storage 对象的局部变量只能由已有的
// storage对象赋值
}
}
基本类型之间的转换
隐式转换
如果一个运算符用在两个不同类型的变量之间,那么编译器将隐式地将其中一个类型转换为另一个类型(不同类型之间的赋值也是一样).
一般来说,只要值类型之间的转换在语义上行得通,而且转换的过程中没有信息丢失,那么隐式转换基本都可以实现的:
uint8 可以转换成 uint16,uint128 转换成 int256, 但 int8 不能转换成uint256(因为uint不能涵盖像 -1 这样的值)
更进一步说,无符号整形可以转换成跟她大小相等 或 更大的字节类型,但反之不能.任何可以转换成uint160 的类型都可以转换成 address 类型.
显示转换
如果某些情况下编译器不支持隐式转换,但是你很清楚自己想要做什么,这种情况可以考虑显示转换.
注意这可能会发生一些无法预料的后果,因此一定要进行测试,确保结果如你所想.
如下例,将一个 int8 类型的负数转化成 uint:
int8 y = -3;
uint x = uint(y);
这段代码的最后,x的值将是 0xfffff..fd(64个16进制字符),因为这是-3 的256 位补码形式.
如果一个类型显示转换成更小的类型,相应的高位被舍弃
uint32 a = 0x12345678;
uint16 b = uint16(a); //此时 b 的值是 0x5678
类型推断
为了方便起见,没有必要每次都精确指定一个变量的类型,编译器会根据分配该变量的第一个表达式的类型自动推断该变量的类型
uint24 x = 0x123;
var y = x;
这里 y 的类型将是 uint24.不能对函数参数或者返回参数使用var
警告:
类型只能从第一次赋值中推断出来,因此一下代码中的循环是无限的
原因是 i 的类型 uint8,而这个类型变量的最大值比 2000 小
for (var i = 0; i < 2000; i++) { ….}