引用类型比上述的值类型需要更加小心地处理。因为复制拷贝他们可能开销相当大, 我们必须考虑把它们存储在内存(这不是持久化)或者存储器(状态变量存放的地方)。
每一个复杂类型,即数组Array
和结构体Struct
,有一个额外的注解——“数据存储位置”,关系到它是放在在内存还是存储器中。根据上下文会产生一个默认存储位置,但是可以通过storage
或memory
关键字修饰该类型来改变。
memory
storage
storage
。其实还有第三个数据存储位置calldata
,它是不可修改的非持久化的函数参数存储区域。外部函数的函数参数(不包括返回值)被强存储在calldata
,其表现与memory
很相似。
数据存储位置很重要,因为它们会改变赋值的方式:
pragma solidity ^0.4.0;
contract C {
uint[] x; // x的数据存储位置是storage
// memoryArray的数据存储位置是memory
function f(uint[] memoryArray) public {
x = memoryArray; // 可以将整个数组拷贝到storage
var y = x; // 赋值一个指针,y的数据存储位置是storage
y[7]; // 这样写没有问题, 返回第8个元素
y.length = 2; // 这样写没有问题, 通过y修改x
delete x; // 这样写没有问题, 清空一个数组, 并且改变y
// 以下不起作用; 它需要在storage中创建一个新的临时的
// 未命名数组, 但是storage需要"静态"分配:
// y = memoryArray;
// T这个也不起作用, 因为这会"重置"指针, 但是没有明显
// 的位置可以指向.
// delete y;
g(x); // 调用g,移交对x的引用
h(x); // 调用h并在内存中创建一个独立的临时副本
}
function g(uint[] storage storageArray) internal {}
function h(uint[] memoryArray) public {}
}
calldata
storage
memory
storage
数组可以声明时指定长度,也可以是动态长度。对storage
存储的数组来说,元素类型可以是任意的,类型可以是数组,映射类型,结构体等。但对于memory
的数组来说。如果作为public
函数的参数,它不能是映射类型的数组,只能是支持ABI的类型。
一个元素类型为T
,固定长度为k
的数组,可以声明为T[k]
,而一个动态大小(变长)的数组则声明为T[]
。
还可以声明一个多维数组,如声明一个类型为uint
的数组长度为5的变长数组(5个元素都是变长数组),可以声明为uint[][5]
。(注意,相比非区块链语言,多维数组的长度声明是反的。)
为了访问第三动态数组中的第二个元素, 必须使用x[2][1]
(下标是从0开始的,访问模式和声明模式正好相反, 即x[2]
是从右边剔除了一阶)
bytes
和string
是一种特殊的数组。bytes
类似byte[]
,但在外部函数作为参数调用中,bytes
会进行压缩打包。string
类似bytes
,但目前不提供长度和下标的访问方式。
所以应该尽量使用bytes
而不是byte[]
,因为开销更小。
注解 |
---|
如果你想访问字符串s 的某个字节,要使用bytes(s).length/bytes(s)[7]='x'; 。注意此时通过下标访问获取到的不是对应字符,而是UTF-8编码! |
类型为数组的状态变量,可以标记为public
,从而让Solidity创建一个getter,如果要访问数组的某个元素,指定下标就好了。
可使用new
关键字创建一个memory
的数组。与stroage
数组不同的是,你不能通过.length
的长度来修改数组大小属性。
pragma solidity ^0.4.16;
contract C {
function f(uint len) public pure {
uint[] memory a = new uint[](7);
bytes memory b = new bytes(len);
// Here we have 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 {
// ...
}
}
通过数组字面量,创建的数组是memory
的,同时还是定长的。元素类型则是使用刚好能存储的元素的能用类型,比如[1, 2, 3]
的类型是uint8[3] memory
,因为这些常量中的每一个的类型都是uint8
。
因此,有必要将上例中的第一个元素转换为uint
。 请注意,目前固定大小的memory
数组不能分配给动态大小的memory
数组,即以下情况是不可能的:
// 这将不能编译
pragma solidity ^0.4.0;
contract C {
function f() public {
// 下一行发生一个type error
// 因为固定大小的uint[3],不能转换为动态大小的uint[]。
uint[] x = [uint(1), 3, 4];
}
}
已经计划在未来移除这样的限制,但由于数组在ABI中的传递方式,所以现在还比较棘手。
数组有一个.length
属性,表示当前数组中元素的个数。动态数组可以通过改变.length
来在storage里来调整size。memory里的动态数组不支持。
不能通过访问超出当前数组的长度的下标来自动实现改变数组长度。‘
memory数组虽然可以通过参数,灵活指定大小,但一旦创建,大小不可调整。但如果是动态数组,则取决于运行时参数。
storage的动态数组和bytes
都有一个push
方法(string
没有),用于附加新元素到数据末端,返回值为新的length
。
警告 |
---|
当前在外部函数中,不能使用多维数组。 |
警告 |
---|
由于EVM的局限,不可能在外部函数调用中返回动态内容。contract C { function f() returns (uint[]) { ... } } 里的合约函数f 使用web3.js调用,将有返回值,但使用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 {
m_pairsOfFlags = newPairs;
}
function setFlagPair(uint index, bool flagA, bool flagB) public {
// 访问不存在的index会抛出异常
m_pairsOfFlags[index][0] = flagA;
m_pairsOfFlags[index][1] = flagB;
}
function changeFlagArraySize(uint newSize) public {
// 如果新size更小, 移除的元素会被销毁
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 arrays ("bytes") are different as they are stored without padding,
// but can be treated identical to "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) {
// memory动态数组通过new创建:
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提供struct
来定义自定义类型,自定义的类型是引用类型。
我们看看下面的例子:
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 returns (uint campaignID) {
campaignID = numCampaigns++; // campaignID 作为一个变量返回
// 创建一个结构体实例,存储在storage ,放入mapping里
campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0);
}
function contribute(uint campaignID) public payable {
Campaign storage c = campaigns[campaignID];
// 用mapping对应项创建一个结构体引用
// 也可以用 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.transfer(amount);
return true;
}
}
上面是一个简化版的众筹合约,但它可以让我们理解structs
的基础概念,struct
可以用于映射和数组中作为元素。其本身也可以包含映射和数组等类型。
不能声明一个struct
同时将自身struct
作为成员,这个限制是基于结构体的大小必须是有限的。但struct
可以作为mapping
的值类型成员。
注意在函数中,将一个struct
赋值给一个局部变量(默认是storage
类型),实际是拷贝的引用,所以修改局部变量值的同时,会影响到原变量。
当然,也可以直接通过访问成员修改值,而不用一定赋值给一个局部变量,如campaigns[campaignID].amount = 0
上一篇:深入理解Solidity——值类型
下一篇:深入理解Solidity——映射