玩以太坊链上项目的必备技能(类型-引用类型-Solidity之旅三)
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
在前文我们讲述了值类型也就说再修改值类型的时候每次都有一个独立的副本如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
类型。
数组元素可以是任何类型包括映射或结构体。对类型的限制是映射只能存储在 存储storage 中并且公开访问函数的参数需要是 ABI 类型。
状态变量标记 public
的数组Solidity创建一个 getter函数 。 小标数字索引就是 getter函数 的参数。
访问超出数组长度的元素会导致异常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
。