Solidity-引用类型

所谓引用类型,实际上是提供了一个指向内容的指针赋值给引用类型的变量。这么做的原因,在于其数据内容本身就是复合类型的,不像内置的基础类型,是可以直接表达出来的。

对于引用类型的变量,在进行参数传递或者变量赋值的时候,会存在这样一个问题:传递过去的到底是这个值的拷贝还是这个值的引用。其造成的影响就是,修改传递过去的内容是否会对原内容产生影响。

因此,本节讨论的主题就是:引用类型有哪些?影响其深拷贝的因素有哪些?删除时需要注意什么。

1 数据位置

对于引用类型,需要指定其数据位置,这关乎到数据存储在哪里,生命周期有多长。 数据位置有三种:

  • memory: EVM 中的临时存储空间,生命周期限制在一个函数调用中。
  • storage: EVM 中的进行持久化存储空间。生命周期在整个合约的生命周期内。
  • calldata:用于函数参数,有点像 memory,生命周期都是函数中,但是,值不可修改。

注意,如果可以,尽可能使用 calldata 作为存储位置,它将避免数据被修改。

0.6.9 之前,引用类型数据位置的使用在函数可见性上有所限制,比如 calldata 被限制只能使用在外部函数中。现在 memorycalldata 的使用,不再有函数可见性上的限制。

0.5.0 之前,数据位置可以被省略,根据不同的变量类型/函数类型等会有不同的默认的数据位置。但是,对于所有的复合类型(由基础类型组合而成)来说,都必须指定一个显式的数据位置。


图片来源


1.1 赋值行为和数据位置

赋值行为与数据位置有关:

  • storagememory(或者 calldata) 之间赋值会涉及到数据拷贝。
  • 将一个 memory 赋值到另外一个 memory,仅仅是创建了一个引用。这就意味着其中一个引发的改变,在另外一个 memory 中也是可见的。两个 memory 指向同一块存储空间。
  • 将一个 storage 赋值给一个本地 storage 变量(local storage variable),实际上赋值的也是一个引用。
  • 除上述,所有其它到 storage 的赋值,通常都是 copy。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;

contract C {
// 这里,数据 x 的存储的位置是 storage,
// 这有这里,数据位置(data location)是可以省略的。
uint[] x;

// memoryArray 的存储位置是 memory,这就意味着它的生命周期只是在这个函数调用中
function f(uint[] memory memoryArray) public {
x = memoryArray; // 将 memoryArray 拷贝给 x,x 的存储位置是 storage
uint[] storage y = x; // y 是一个 local storage,将 storage 赋值给 local storage,属于引用赋值
y[7]; // 将返回 x 中第八个元素
y.pop(); // 通过 y 来修改 x,x 和 y 指向同一块存储区域
delete x; // 清空数组,此时,y 也被清理
// 下述方式,是不可取的。。
// y = memoryArray;
// This does not work either, since it would "reset" the pointer, but there
// is no sensible location it could point to.
// delete y;
g(x); // g 在调用时,传递的是 x 的引用,这就意味着在 g 中可以修改 x
h(x); // h 在调用时,将拷贝 x 进行参数传递
}

function g(uint[] storage) internal pure {}
function h(uint[] memory) public pure {}
}

这里需要注意的是:The type of the local variable x is uint[] storage, but since storage is not dynamically allocated, it has to be assigned from a state variable before it can be used.
来源


2 引用类型

引用类型有三种,数组,结构体以及mapping。


2.1 数组

我们可以创建一个在编译时就确定的固定(长度)数组,也可以使用动态数组。
固定长度为 k 类型为 T 的数组即 T[k],动态长度的数组可以写成 T[]
比如说,一个拥有 5 个动态数组的数组,可以写成 uint[][5]需要注意的是,这种写法和有些语言是相反的。在 Solidity 中,X[3] 表示数组中存在三个类型为 X 的元素,即使 X 本身是一个数组。这种使用,和某些语言(比如 C) 是不一样的。

索引是从零开始的,访问可以相对位置。

举个例子,如果有一个 uint[][5] memory x,你可以通过 x[2][6] 访问第三个动态数组中的第七个元素,如果要访问第三个动态数组,可以使用 x[2]。同样的,如果你有一个 T[5] a 数组,那么,a[2] 的类型就是 T

数组中的元素可以是任意类型,包括结构体和map。但是对于 map 类型的数组,会存在一个限制,其实,这本身是对 map 自身的限制,即,mapping 应该存储在 storage 区域,否则会编译不通过。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
contract TestContract is ReentrancyGuard {
// 这种是可行的
mapping (uint256=>address) [] test ;
int [] array ;
function a()public{
// 这种是不行的,编译不通过
// 报错:"Type mapping(uint256 => address) is only valid in storage because it contains a (nested) mapping."
// mapping (uint256=>address) [] memory test ;
// 这种操作也是不被允许的
// 报错:"TypeError: Unary operator delete cannot be applied to type int256[] storage pointer"
// int [] storage array1 = array;
// delete array1;
}
}

2.1.1 bytes 和 string

变量类型 bytesstring 都是特殊的数组。bytesbytes1[] 比较相似,但是,在 calldatamemory 中,它是会被 ,string 等于 bytes,但是不允许通过索引访问。

Solidity 中没有关于 string 的操作方法,但是仍然有一些第三方库可以使用。你也可以通过 keccak256(abi.encodePacked(s1))==keccak256(abi.encodePacked(s2)) 比较两个字符串。

根据官方文档的描述:

You should use bytes over bytes1[] because it is cheaper, since using bytes1[] in memory adds 31 padding bytes between the elements.

虽然,bytesbytes1[] 相类似,但是,在 memory 中,bytes 是压缩存储的,而 bytes1[] 会根据填充算法,补充 31 个字节。因此,在 memorybytes 要高效些。


2.1.2 为数组分配 memory

如果数组的位置是 memory,那么,数组的大小需要事先确定好。与位置在 storage 中的数组不同,memory 下,无法执行 ‘.push’ 方法。

1
2
3
4
5
6
7
8
9
10
11
pragma solidity >=0.4.16 <0.9.0;

contract C {
function f(uint len) public pure {
uint[] memory a = new uint[](7);
bytes memory b = new bytes(len);
assert(a.length == 7);
assert(b.length == len);
a[6] = 8;
}
}

2.1.3 数组字面值

数组字面值,是用方括号包裹起来的以逗号分隔的一个或者多个表达式列,形式为 [1,a,f(3)]。这种数组字面值类型需要满足下述需要:

  • 数组的大小是静态的,其大小为其中表达式的个数;
  • 数组中的元素类型,是由数组中第一个元素的类型确定的,其他表达式将进行类型的隐式转换,如果无法转换,将报错。

在下述示例中,[1,2,3] 的类型是 uint8[3] memory,如果要用到 uint[3] memory,只需要将第一个元素进行强制转换即可。

1
2
3
4
5
6
7
8
9
10
11
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract C {
function f() public pure {
g([uint(1), 2, 3]);
}
function g(uint[3] memory) public pure {
// ...
}
}

数组字面 [1,-1] 就是不合法的,因为第一个表达式是 uint8,而第二个表达式的类型是 int8,为此,需要进行类型转换:[int8(1),-1]

如果是二位数组,该怎么办呢?内部数组元素的类型也需要保持一致。

1
2
3
4
5
6
7
8
9
10
11
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract C {
function f() public pure returns (uint24[2][4] memory) {
uint24[2][4] memory x = [[uint24(0x1), 1], [0xffffff, 2], [uint24(0xff), 3], [uint24(0xffff), 4]];
// The following does not work, because some of the inner arrays are not of the right type.
// uint[2][4] memory x = [[0x1, 1], [0xffffff, 2], [0xff, 3], [0xffff, 4]];
return x;
}
}

固定大小的数组,是无法直接赋值给动态数组的:

1
2
3
4
5
6
7
8
9
10
11
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;

// This will not compile.
contract C {
function f() public {
// The next line creates a type error because uint[3] memory
// cannot be converted to uint[] memory.
uint[] memory x = [uint(1), 3, 4];
}
}

2.1.4 数组成员函数
  • length:返回数组当前长度。静态数组的长度是固定的;动态数组的长度依赖于运行时。

  • push():动态的 storage 数组和 storage bytes 拥有此函数。它将返回元素的引用。因此,可以这么使用 x.push().t = 2 或者 x.push() = b

  • push(x):动态的 storage 数组和 storage bytes 拥有此函数。在数组尾部增加一个 x,该函数不返回任何东西。

  • pop():动态的 storage 数组和 storage bytes 拥有此函数。返回并删除数组的最后一个元素。这个函数将在最后一个数组元素上隐式调用调用 delete

注意,push 将消耗常量的 gas,因为这个过程将涉及到元素初始化。


2.2 结构体

以结构的形式定义一种新的数据类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;

// 定义一个拥有两个成员的类型
// 在合约外部定义该结构体,该结构体可以在多个合约中共享。这里,其实并没有这种必要性。
struct Funder {
address addr;
uint amount;
}

contract CrowdFunding {
// 结构体也可以定义在合约内部,这样,其可见性只有合约及其派生合约中。
struct Campaign {
address payable beneficiary;
uint fundingGoal;
uint numFunders;
uint amount;
mapping (uint => Funder) funders;
}

uint numCampaigns;
mapping (uint => Campaign) campaigns;

function newCampaign(address payable beneficiary, uint goal) public returns (uint campaignID) {
campaignID = numCampaigns++; // campaignID 是返回的变量
// 我们不能使用 "campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0)"
// 因为,"Campaign" 包含由 mapping
Campaign storage c = campaigns[campaignID];
c.beneficiary = beneficiary;
c.fundingGoal = goal;
}

function contribute(uint campaignID) public payable {
Campaign storage c = campaigns[campaignID];
// 创建一个临时的 memory 中的 Funder,
// 并将其拷贝给 c.funders[i]
// Note that you can also use Funder(msg.sender, msg.value) to initialise.
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;
}
}

2.3 mapping

使用 mapping(_KeyType => _ValueType) 来表达映射关系。
_KeyType 可以是任意内置类型,比如 bytesstring,合约或者是枚举类型。
其他用户定义的以及一些复合类型,比如 mapping,结构体,数组,是不允许作为 key 的。_ValueType 可以是任何类型,包括 mapping,结构体,以及数组。

mapping 类型的数据位置,只能是在 storage 中,作为一个状态变量存在;在函数内部,其可以作为 storage 的引用出现;作为参数时,还需要出现在一些库函数中。
mapping 无法作为合约中公共函数的参数的类型,也无法作为其返回参数的类型。这个限制,也适用于包含了 mapping 的数组和结构体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.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(address(this));
}
}

2.3.1 mapping 遍历

mapping 是无法直接遍历的,因为无法穷举 key。
变通的做法,通过定义结构体可以实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.8 <0.9.0;

struct IndexValue { uint keyIndex; uint value; }
struct KeyFlag { uint key; bool deleted; }

struct itmap {
mapping(uint => IndexValue) data;
KeyFlag[] keys;
uint size;
}

library IterableMapping {
function insert(itmap storage self, uint key, uint value) internal returns (bool replaced) {
uint keyIndex = self.data[key].keyIndex;
self.data[key].value = value;
if (keyIndex > 0)
return true;
else {
keyIndex = self.keys.length;
self.keys.push();
self.data[key].keyIndex = keyIndex + 1;
self.keys[keyIndex].key = key;
self.size++;
return false;
}
}

function remove(itmap storage self, uint key) internal returns (bool success) {
uint keyIndex = self.data[key].keyIndex;
if (keyIndex == 0)
return false;
delete self.data[key];
self.keys[keyIndex - 1].deleted = true;
self.size --;
}

function contains(itmap storage self, uint key) internal view returns (bool) {
return self.data[key].keyIndex > 0;
}

function iterate_start(itmap storage self) internal view returns (uint keyIndex) {
return iterate_next(self, type(uint).max);
}

function iterate_valid(itmap storage self, uint keyIndex) internal view returns (bool) {
return keyIndex < self.keys.length;
}

function iterate_next(itmap storage self, uint keyIndex) internal view returns (uint r_keyIndex) {
keyIndex++;
while (keyIndex < self.keys.length && self.keys[keyIndex].deleted)
keyIndex++;
return keyIndex;
}

function iterate_get(itmap storage self, uint keyIndex) internal view returns (uint key, uint value) {
key = self.keys[keyIndex].key;
value = self.data[key].value;
}
}

// How to use it
contract User {
// Just a struct holding our data.
itmap data;
// Apply library functions to the data type.
using IterableMapping for itmap;

// Insert something
function insert(uint k, uint v) public returns (uint size) {
// This calls IterableMapping.insert(data, k, v)
data.insert(k, v);
// We can still access members of the struct,
// but we should take care not to mess with them.
return data.size;
}

// Computes the sum of all stored data.
function sum() public view returns (uint s) {
for (
uint i = data.iterate_start();
data.iterate_valid(i);
i = data.iterate_next(i)
) {
(, uint value) = data.iterate_get(i);
s += value;
}
}
}

3 delete 操作符

delete a 意味着将为 a 赋值一个初始值。
对于整型来说,a 的初始值就是 0。但是,当 a 是数组时,如果是静态数组,其中的每个元素将被初始化;如果是动态数组,数组的长度将归零;delete a[x] 将仅仅删除索引为 x 的数组 item。

对于结构体而言,delete a 意味着 a 中的所有成员将被重置。换句话说,delete a 之后,a 的值就像是声明但是为赋值的状态。

delete 对 mapping 不产生影响(没法给 key 初始化)。因此,如果在结构体中有一个 mapping,则 mapping 并不会被重置。然而,对于却可以 delete a[x],此时,a 是 mapping,x 是 key。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.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 的长度被重置为 0,这种改变同样会影响到 y
// 然而 "delete y" 就是不合法的,y 的赋值初始化只能由一个已经存在的 storage 对象来做
assert(y.length == 0);
}
}

参考链接

Types

© 2024 YueGS