文章目录
引用类型, 在内存中存储的是对数据的引用,而不是实际的数据本身,实际数据存放在单独的位置。当我们创建引用类型的变量时,实际上分配了一个存储指向数据的地址的内存位置。这与
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;
}
}
3445

被折叠的 条评论
为什么被折叠?



