文章目录
引用类型, 在内存中存储的是对数据的引用,而不是实际的数据本身,实际数据存放在单独的位置。当我们创建引用类型的变量时,实际上分配了一个存储指向数据的地址的内存位置。这与
C
语言中的指针非常相似。
在 Solidity
中引用类型共有三种:数组array
、结构体 struct
和映射 mapping
。
当调用一个函数时,如果参数为引用类型,那么在函数内部对参数数据的修改会影响到原始数据。这是因为操作的是引用,而这个引用指向了原始数据,而不是数据的副本。
一、数组 array
在 Solidity
中,数组
是一种用于存储相同类型元素的数据结构。
例如,声明一个数组变量 uint
numbers[10],那么变量 numbers
存储了 10 个 uint
类型的数据。
数组中的特定元素可以通过索引访问,索引值从 0 开始。
例如,上面声明的数组 numbers
,可以使用 numbers[0]、numbers[1] 访问前两个变量。
Solidity
支持 固定长度数组
和 动态长度数组
两种类型。
1.1 固定长度数组
固定长度数组
在声明时需要指定数组的长度,并且这个长度在编译时就确定了,部署后无法更改。
声明一个 固定长度数组
,需要指定元素类型和数量,语法如下:
type_name arrayName [length];
其中数组长度 length
必须是一个大于零的整数数字,type_name
可以是任何数据类型。
例如,声明一个 uint
类型,长度为 10 的数组 balance,如下所示:
uint balance[10];
初始化一个 固定长度数组
,可以使用下面的语句:
uint balance[3] = [uint(1), 2, 3]; // 初始化固定长度数组
balance[2] = 5; // 设置第 3 个元素的值为 5
这里的数组 balance
的初值,为什么写为 [uint(1), 2, 3],而不是 [1,2,3] 呢?
这是因为在 Solidity
中,如果一个类型的字面量没有显式指定,那么就会由编译器自行推断。
整数字面量的解析规则是根据其值的范围来确定默认的类型。当你提供一个整数字面量时,编译器会尝试确定最小的类型来容纳该值。
如果整数字面量的值在 uint8
的取值范围内(0 到 255),编译器会将其默认解析为 uint8
类型。因为在这种情况下,使用 uint8
类型可以使用更少的存储空间,来提高存储效率。
如果整数字面量的值超出了 uint8
的取值范围,编译器会根据需要自动升级到更大的整数类型,如 uint16
、uint32
。
所以,我们在编写程序的时候,尽量显式指定某些字面量的类型,防止异常情况发生。
我们可以通过数组的索引来访问数组元素,例如:
uint salary = balance[2];
我们可以使用下面的合约,来展示 固定长度数组
的用法。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract StaticArray {
// 初始化固定长度数组为[1,2,3]
uint[3] balance = [uint(1), 2, 3];
function staticArray() external returns (uint length, uint[3] memory array) {
// 获取第 3 个元素的值
uint element = balance[2];
// 设置第 3 个元素的值乘以 2
balance[2] = element * 2;
// 返回数组的长度和数组所有元素
return (balance.length, balance);
}
}
我们将合约代码复制到 Remix
,进行编译,并部署到区块链上:点击 staticArray
,返回结果:数组长度为 3,数组元素为 1,2,6。
1.2 动态长度数组
动态长度数组
与 固定长度数组
相比,就是 动态长度数组
的长度是可以改变的。
声明一个 动态长度数组
,只需要指定元素类型,而不需要指定元素的数量,语法如下:
type_name arrayName[];
动态长度数组
初始化后,是一个长度为 0 的空数组。我们可以使用 push
方法,在数组末尾追加一个元素;使用 pop
方法,截掉末尾的一个元素。
其中,push
有两种用法:一种是带参数 push(x)
,第二种是不带参数 push()
,如果不带参数,那么就会在数组的末尾添加一个具有默认值的元素。
动态长度数组
元素的访问方法,与固定长度数组相同,也是通过索引来访问数组元素。
我们可以使用下面的合约,来展示动态数组的用法。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract DynamicArray {
// 初始化动态长度数组为[1,2,3]
uint[] balance = [uint(1), 2, 3];
function dynamicArray() external returns (uint length, uint[] memory array) {
// 追加两个新元素4、5
balance.push(4);
balance.push(5);
// 删除最后一个元素 5
balance.pop();
// 返回数组的长度和数组所有元素
return (balance.length, balance);
}
}
我们将合约代码复制到 Remix
,进行编译,并部署到区块链上。点击 dynamicArray
,返回结果:数组长度为 4,数组元素为 1,2,3,4。
可以注意到:可变长度数组(动态数组):在声明时不指定数组的长度。用T[]
的格式声明,其中T
是元素的类型,例如:
// 可变长度 Array
uint[] array4;
bytes1[] array5;
address[] array6;
bytes array7;
注意:
bytes
比较特殊,是数组,但是不用加[]
。另外,不能用byte[]
声明单字节数组,可以使用bytes
或bytes1[]
。bytes
比bytes1[]
省gas。
1.3 创建数组的规则
在Solidity里,创建数组有一些规则:
-
对于
memory
修饰的动态数组
,可以用new
操作符来创建,但是必须声明长度,并且声明后长度不能改变。例子:// memory动态数组 uint[] memory array8 = new uint[](5); bytes memory array9 = new bytes(9);
-
数组字面常数(Array Literals)是写作表达式形式的数组,用方括号包着来初始化array的一种方式,并且里面每一个元素的type是以第一个元素为准的,例如
[1,2,3]
里面所有的元素都是uint8
类型,因为在Solidity中,如果一个值没有指定type的话,会根据上下文推断出元素的类型,默认就是最小单位的type,这里默认最小单位类型是uint8
。而[uint(1),2,3]
里面的元素都是uint
类型,因为第一个元素指定了是uint
类型了,里面每一个元素的type都以第一个元素为准。下面的例子中,如果没有对传入
g()
函数的数组进行uint
转换,是会报错的。// 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 _data) public pure { // ... } }
-
如果创建的是动态数组,你需要一个一个元素的赋值。
uint[] memory x = new uint[](3); x[0] = 1; x[1] = 3; x[2] = 4;
1.4 数组成员
length
: 数组有一个包含元素数量的length
成员,memory
数组的长度在创建后是固定的。push()
:动态数组
拥有push()
成员,可以在数组最后添加一个0
元素,并返回该元素的引用。push(x)
:动态数组
拥有push(x)
成员,可以在数组最后添加一个x
元素。pop()
:动态数组
拥有pop()
成员,可以移除数组最后一个元素。
二、结构体 struct
Solidity
中的结构体是一种用户自定义的数据类型,它通过组合多个不同类型的变量成员来创建一个新的复合类型。
例如,一本书包含以下信息:
- Title:标题
- Author:作者
- Book ID:书号
那么,我们就可以定义一个结构体类型,用来表示一本书。
2.1 结构体定义
定义一个结构体类型使用 struct
关键字,它的语法如下:
struct struct_name {
type1 type_name_1;
type2 type_name_2;
type3 type_name_3;
...
}
其中 struct_name
是结构体的名称,typeN
是结构体成员的数据类型,type_name_N
是结构体成员的名字。
比如,我们定义一个结构体类型 Book
,用来表示一本书:
struct Book {
string title;
string author;
uint ID;
}
2.2 使用方法
我们要访问结构体的成员,需要使用成员访问操作符 (.)。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract StructAccess {
struct Book {
string title;
string author;
uint ID;
} // 定义结构体 Book
Book book; // 使用结构体 Book 声明变量
function setBook() public {
book.title = "Learn Solidity"; // 设置结构体的成员 title
book.author = "BinSchool";
book.ID = 1;
}
function getBookAuthor() public view returns(string memory) {
return book.author; // 读取结构体的成员 author
}
}
我们把合约代码复制到 Remix
,进行编译,并部署到区块链上。
我们先点击 setBook
来设置书的信息,然后点击 getBookAuthor
,来获取这本书的作者,返回的调用结果为:
string: BinSchool
2.3 初始化方式
结构体变量共有 3 种初始化数据的方式:按字段顺序初始化、按字段名称初始化、按默认值初始化。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract StructInit {
struct Book {
string title;
string author;
uint ID;
} // 定义结构体 Book
function getBooks() public pure returns(Book memory,Book memory,Book memory) {
// 按字段顺序初始化
Book memory book1 = Book('Learn Java', 'BinSchool', 1);
// 按字段名称初始化
Book memory book2 = Book({title:"Learn JS", author:"BinSchool", ID:2});
// 按默认值初始化
Book memory book3;
book3.ID = 3;
book3.title = 'Learn Solidity';
book3.author = 'BinSchool';
return (book1, book2, book3);
}
}
我们把合约代码复制到 Remix
,进行编译,并部署到区块链上。
点击 getBooks
后,返回的调用结果为:
0: tuple(string,string,uint256): Learn Java,BinSchool,1
1: tuple(string,string,uint256): Learn JS,BinSchool,2
2: tuple(string,string,uint256): Learn Solidity,BinSchool,3
三、映射 mapping
Solidity
中的映射类型 mapping
,用来以键值对的形式存储数据。它的主要作用是提供高效的查找功能,类似于其它编程语言中的哈希表或者字典。
例如,在使用 白名单
的场景中,我们可以通过 映射类型
将用户地址映射到一个布尔值,以标识哪些地址是被允许的,哪些地址是不被允许的,从而高效地控制访问权限。
映射类型是智能合约中最常用的数据类型之一,我们必须熟练掌握。
3.1 映射定义
定义一个映射类型使用 mapping
关键字,它的语法如下:
mapping(key_type => value_type)
mapping
类型是将一个键 (key) 映射到一个值 (value)。
其中:key_type 可以是任何基本数据类型,比如:整型、地址型、布尔型、枚举型,以及 bytes
和 string
,但是部分复杂对象不允许使用,比如:动态数组、结构体、映射。
value_type 可以是任何数据类型。
例如,在合约中声明一个 mapping
类型的变量:
struct MyStruct { uint256 value; } // 定义一个结构体
mapping(address => uint256) a; // 正确
mapping(string => bool[]) b; // 正确
mapping(int => MyStruct) c; // 正确
mapping(address => mapping(address => uint)) d; // 正确
// mapping(uint[] => uint) public e; // 错误
// mapping(MyStruct => addrss) public f; // 错误
// mapping(mapping(string=>int)) => uint) g; // 错误
3.2 使用方法
在智能合约中,mapping
类型的使用非常普遍的。比如,在 ERC20
代币合约中,经常会使用 mapping
类型的变量作为一个内部账本,用来记录每一个钱包地址拥有的代币余额。
下面,我们模拟一个稳定币 USDT
的合约:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract USDT {
mapping(address => uint256) balances; // 保存所有持有 USDT 账户的余额
// 构造函数,合约部署时自动调用
constructor() {
balances[msg.sender] = 100; // 初始设定合约部署者的账户余额为 100 USDT
}
// 查询某一个账户的USDT余额
function balanceOf(address account) public view returns(uint256) {
return balances[account];
}
}
在这个合约中,首先定义了一个保存所有持有 USDT
账户余额的 mapping
类型的变量 balances
,用来作为一个内部账本。
mapping(address => uint256) balances; // 保存所有持有 USDT 账户的余额
获取某个账号地址 0x5B38…ddC 持有 USDT
的数量:
uint256 value = balances[0x5B38...ddC];
设置某个账号地址 0x5B38…ddC 持有 USDT
的数量:
balances[0x5B38...ddC4] = 100 ;
将上方的合约部署者地址 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4,填写到函数 balanceOf
的参数位置,点击 balanceOf
后,返回的结果为 100。
我们使用另外一个地址作为参数,比如 0xAA6663F64d9C6d1EcF9b849Ae677dD3315835666,点击 balanceOf
后,返回的调用结果为 0。
注意:在 mapping
类型的变量中获取一个不存在的键的值,并不会报错,而是会返回值类型的默认值。比如,整型会返回 0,布尔型会返回 false 等。
3.3 映射的规则
-
规则1:映射的
_KeyType
只能选择Solidity内置的值类型,比如uint
,address
等,不能用自定义的结构体。而_ValueType
可以使用自定义的类型。下面这个例子会报错,因为_KeyType
使用了我们自定义的结构体:// 我们定义一个结构体 Struct struct Student{ uint256 id; uint256 score; } mapping(Student => uint) public testVar;
-
规则2:映射的存储位置必须是
storage
,因此可以用于合约的状态变量,函数中的storage
变量和library函数的参数(见例子)。不能用于public
函数的参数或返回结果中,因为mapping
记录的是一种关系 (key - value pair)。 -
规则3:如果映射声明为
public
,那么Solidity会自动给你创建一个getter
函数,可以通过Key
来查询对应的Value
。 -
规则4:给映射新增的键值对的语法为
_Var[_Key] = _Value
,其中_Var
是映射变量名,_Key
和_Value
对应新增的键值对。例子:function writeMap (uint _Key, address _Value) public{ idToAddress[_Key] = _Value; }
3.4 映射的原理
-
原理1: 映射不储存任何键(
Key
)的资讯,也没有length的资讯。 -
原理2: 对于映射使用
keccak256(h(key) . slot)
计算存取value的位置。感兴趣的可以去阅读 WTF Solidity 内部规则: 映射存储布局 -
原理3: 因为Ethereum会定义所有未使用的空间为0,所以未赋值(
Value
)的键(Key
)初始值都是各个type的默认值,如uint的默认值是0。
3.5 优缺点
优点
mapping
可以用来存储数据集,并且提供了高效的查找功能。它可以根据“键”快速定位到特定元素。
我们知道,数组也可以用来存储数据集,但是在数组里面查找特定元素,就必须通过循环语句,遍历整个数组进行匹配,所以查找效率非常低,使用也不方便。
在数据集很大的情况下,通过 mapping
查找数据,效率要比数组高很多。
缺点
mapping
最大的问题是无法直接遍历。你不能使用 for
循环遍历 mapping
中的键值对,它也没有提供获取全部 key
或者 value
的功能。因此,在合约中遍历或迭代 mapping
类型的数据时,通常需要设计其他数据结构来辅助实现。
四、代码示例
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
contract ArrayTypes {
// 固定长度 Array
uint[8] array1;
bytes1[5] array2;
address[100] array3;
// 可变长度 Array
uint[] array4;
bytes1[] array5;
address[] array6;
bytes array7;
// 初始化可变长度 Array
uint[] array8 = new uint[](5);
bytes array9 = new bytes(9);
// 给可变长度数组赋值
function initArray() external pure returns(uint[] memory){
uint[] memory x = new uint[](3);
x[0] = 1;
x[1] = 3;
x[2] = 4;
return(x);
}
function arrayPush() public returns(uint[] memory){
uint[2] memory a = [uint(1),2];
array4 = a;
array4.push(3);
return array4;
}
}
pragma solidity ^0.8.21;
contract StructTypes {
// 结构体 Struct
struct Student{
uint256 id;
uint256 score;
}
Student student; // 初始一个student结构体
// 给结构体赋值
// 方法1:在函数中创建一个storage的struct引用
function initStudent1() external{
Student storage _student = student; // assign a copy of student
_student.id = 11;
_student.score = 100;
}
// 方法2:直接引用状态变量的struct
function initStudent2() external{
student.id = 1;
student.score = 80;
}
// 方法3:构造函数式
function initStudent3() external {
student = Student(3, 90);
}
// 方法4:key value
function initStudent4() external {
student = Student({id: 4, score: 60});
}
}
pragma solidity ^0.8.21;
contract EnumTypes {
// 将uint 0, 1, 2表示为Buy, Hold, Sell
enum ActionSet { Buy, Hold, Sell }
// 创建enum变量 action
ActionSet action = ActionSet.Buy;
// enum可以和uint显式的转换
function enumToUint() external view returns(uint){
return uint(action);
}
}
pragma solidity ^0.8.21;
contract Mapping {
mapping(uint => address) public idToAddress; // id映射到地址
mapping(address => address) public swapPair; // 币对的映射,地址到地址
// 规则1. _KeyType不能是自定义的 下面这个例子会报错
// 我们定义一个结构体 Struct
// struct Student{
// uint256 id;
// uint256 score;
//}
// mapping(Struct => uint) public testVar;
function writeMap (uint _Key, address _Value) public{
idToAddress[_Key] = _Value;
}
}