数组元素可以是任何类型,包括映射或结构体。对类型的限制是映射只能存储在 存储storage 中,并且公开访问函数的参数需要是 ABI 类型。 状态变量标记 public
的数组,Solidity创建一个 getter函数 。 小标数字索引就是 getter函数 的参数。
在前文我们讲述了值类型,也就说再修改值类型的时候,每次都有一个独立的副本,如:string 类型的状态变量,其值是无法修改,而是拷贝出一份该状态的变量,将新值存起来。对于处理稍微复杂地值类型时,拷贝将变得愈发大了,也正是介于此,才考虑到将数据存放在内存(memory)
或是存放在存储(storage)
。
在 Solidity 中,数组(array)和 结构体(struct)属于引用类型
。
更改数据位置或类型转换将始终产生自动进行一份拷贝,而在同一数据位置内(对于 存储(storage) 来说)的复制仅在某些情况下进行拷贝。
数据位置和赋值行为
所有的引用类型,如数组(array0
和结构体(struct)
类型,都有别同于其他类型,那便是引用类型
有额外地属性——数据位置。
数据位置,顾名思义就是数据存放的位置在哪里?是在内存(memory)
中还是在存储(storage)
中。
当然咯,大多时候数据都有默认的存放位置。也可显式地修改其数据存放的位置,只需在类型
后添加memory
或storage
。
而函数参数/形参(包括函数的返回参数)
数据位置默认在memory
,局部变量
数据位置则默认在storage
,但状态变量
数据位置被强制在storage
。
还有一个调用数据(calldata)
存储方式,用于存放外部函数(external)
的参数/形参,其效果跟memory
差不离。
指定数据存放的位置是非常的重要,因为它们将会影响其赋值行为。
- 在 存储storage 和 内存memory 之间两两赋值(或者从 调用数据calldata 赋值 ),都会创建一份独立的拷贝。
- 从 内存memory 到 内存memory 的赋值只创建引用, 这意味着更改内存变量,其他引用相同数据的所有其他内存变量的值也会跟着改变。
- 从 存储storage 到本地存储变量的赋值也只分配一个引用。
- 其他的向 存储storage 的赋值,总是进行拷贝。 这种情况的示例如对状态变量或 存储storage 的结构体类型的局部变量成员的赋值,即使局部变量本身是一个引用,也会进行一份拷贝(译者注:查看下面
ArrayContract
合约 更容易理解)。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.4.0;
contract StorageExam {
uint[] x; // x 的数据存储位置是 storage
// memoryArray 的数据存储位置是 memory
function f(uint[] memory memoryArray) public {
x = memoryArray; // 将整个数组拷贝到 storage 中
uint[] storage y = x; // 分配一个指针(其中 y 的数据存储位置是 storage)
y[7]; // 返回第 8 个元素
y.pop(); // 通过 y 修改 x
delete x; // 删除数组 x,同样也会删除数组 y
// 下面的方法不起作用;它需要在存储中创建一个新的临时未命名数组
// 未命名的数组,但存储是 "静态 "分配的。
// y = memoryArray;
// 同样,"删除y "也是无效的,因为对本地变量的赋值只能从现有的存储对象中进行。
// 它将 "重置 "指针,但是没有任何合理的位置可以让它指向
// delete y;
g(x); // 调用 g 函数,同时移交对 x 的引用
h(x); // 调用 h 函数,同时在 memory 中创建一个独立的临时副本
}
function g(uint[] storage ) internal pure {}
function h(uint[] memory) public pure {}
}
归纳为:
强制指定的数据位置:
- 外部函数的参数(不包括返回参数): calldata
- 状态变量: storage
默认数据位置:
- 函数参数(包括返回参数): memory
- 所有其它局部变量: storage
数组(array)
数组是用来存放一组数据(整数、字符串、地址等),它是一种常见的数据类型,而在 Solidity 中,数组可分为编译时
的固定大小,动态大小
的两种数组。
固定大小数组声明格式:
T[k] // T 为元素类型 k则是数组的大小
uint[7] arr;
address[50] address1;
动态大小数组声明格式:
T[] //T为元素类型 由于是动态分配的 所以只需[]
unit[] arr2;
bytes1[] arr3;
address[] arr4;
bytes arr5; //注意 bytes本身就是数组
一个长度为 5,元素类型为 uint
的动态数组的数组(二维数组),应声明为 uint[][5]
(注意这里跟其它语言比,数组长度的声明位置是反的)。作为对比,如在Java中,声明一个包含5个元素、每个元素都是数组的方式为 int[5][]
在Solidity中, X[3]
总是一个包含三个 X
类型元素的数组,即使 X
本身就是一个数组,这和其他语言也有所不同,比如 C 语言。
数组下标是从 0 开始的,且访问数组时的下标顺序与声明时相反。
如果有一个变量为 uint[][5] memory x
, 要访问第三个动态数组的第7个元素,使用 x[2][6]
,要访问第三个动态数组使用 x[2]
。 同样,如果有一个 T
类型的数组 T[5] a
, T 也可以是一个数组,那么 a[2]
总会是 T
类型。
访问超出数组长度的元素会导致异常(assert 类型异常 )。 可以使用 .push()
方法在末尾追加一个新元素,其中 .push()
追加一个零初始化的元素并返回对它的引用。
bytes
和 string
类型的变量是特殊的数组。 bytes
类似于 bytes1[]
,但它在 调用数据calldata 和 内存memory 中会被“紧打包”(译者注:将元素连续地存在一起,不会按每 32 字节一单元的方式来存放)。 string
与 bytes
相同,但不允许用长度或索引来访问。
Solidity没有字符串操作函数,但是可以使用第三方字符串库,我们可以比较两个字符串通过计算他们的 keccak256-hash ,可使用 keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2))
和使用 string.concat(s1, s2)
来拼接字符串。
我们更多时候应该使用 bytes
而不是 bytes1[]
,因为Gas 费用更低, 在 内存memory 中使用 bytes1[]
时,会在元素之间添加31个填充字节。 而在 存储storage 中,由于紧密包装,这没有填充字节。 作为一个基本规则,对任意长度的原始字节数据使用 bytes
,对任意长度字符串(UTF-8)数据使用 string
。
可以将数组标识为 public
,从而让 Solidity 创建一个 getter。 之后必须使用数字下标作为参数来访问 getter。
创建数组规则
- 使用
new
在内存(memory)中创建动态数组,需声明长度,且在声明后不能修改数组的大小
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.4.16;
contract Arr {
function f(uint len) public pure {
uint[] memory a = new uint[](7);
bytes memory b = new bytes(len);
// 这里我们有 a.length == 7 以及 b.length == len
a[6] = 8;
}
}
- 数组字面常数是一种定长的 内存(memory) 数组类型,它的基础类型是由其中元素的类型决定。 例如,
[1, 2, 3]
的类型是uint8[3] memory
,因为其中的每个字面常数的类型都是uint8
。 正因为如此,有必要将上面这个例子中的第一个元素转换成uint
类型。 目前需要注意的是,定长的 内存memory 数组并不能赋值给变长的 内存memory 数组
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.4.16;
contract C {
function f() public pure {
g([uint(1), 2, 3]);
}
function g(uint[3] _data) public pure {
// ...
}
}
// SPDX-License-Identifier: GPL-3.0
// 这段代码并不能编译。
pragma solidity ^0.4.0;
contract C {
function f() public {
// 这一行引发了一个类型错误,因为 unint[3] memory
// 不能转换成 uint[] memory。
uint[] x = [uint(1), 3, 4];
}
}
数组成员
length
: 数组有一个包含元素数量的length
成员,memory
数组的长度在创建后是固定的。push()
:动态数组
和bytes
拥有push()
成员,可以在数组最后添加一个0
元素。push(x)
:动态数组
和bytes
拥有push(x)
成员,可以在数组最后添加一个x
元素。pop()
:动态数组
和bytes
拥有pop()
成员,可以移除数组最后一个元素。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.4.16;
contract ArrayContract {
uint[2**20] m_aLotOfIntegers;
// 注意下面的代码并不是一对动态数组,
// 而是一个数组元素为一对变量的动态数组(也就是数组元素为长度为 2 的定长数组的动态数组)。
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;
}
}
结构体(struct)
Solidity 中的结构体与 c 语言、golang 很相似,通过构造结构体来定义一种新类型。
// SPDX-License-Identifier: GPL-3.0
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 中。我们先不关注映射类型。
campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0);
}
function contribute(uint campaignID) public 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.transfer(amount);
return true;
}
}
上面的合约只是一个简化版的众筹合约,但它已经足以让我们理解结构体的基础概念。 结构体类型可以作为元素用在映射和数组中,其自身也可以包含映射和数组作为成员变量。
尽管结构体本身可以作为映射的值类型成员,但它并不能包含自身。 这个限制是有必要的,因为结构体的大小必须是有限的。
注意在函数中使用结构体时,一个结构体是如何赋值给一个局部变量(默认存储位置是 存储(storage) )的。 在这个过程中并没有拷贝这个结构体,而是保存一个引用,所以对局部变量成员的赋值实际上会被写入状态。
当然,你也可以直接访问结构体的成员而不用将其赋值给一个局部变量,就像这样, campaigns[campaignID].amount = 0
。