什么是 Solidity 数组?
作者 Alchemy
数组是一种线性数据结构,用于存储固定数量、相同数据类型的元素,这些元素存储在连续且相邻的内存位置中。数组中的每一个元素都由其位置指定,通常称为索引。
数组基于索引系统工作,索引从 0 到 n-1,其中 n 表示数组的总大小。与创建多个相同类型的独立变量不同,开发者只需声明一个具有所需大小的数组,即可通过索引访问其中的元素。
在Solidity(Ethereum 和 EVM 兼容区块链上使用的主要 web3 编程语言)中,数组既可以是固定大小,也可以是动态大小。作为一种面向对象的编程语言,Solidity 支持智能合约之间的继承,即多个合约可以被继承到一个合约中。
如果你想更深入地学习 Solidity 数组,可以注册 Alchemy University 的免费 Solidity 开发者课程。
什么是动态数组和固定数组?
**动态数组的大小在声明时未预先定义,而固定数组则有预先定义的大小。**随着元素被逐步添加,动态数组的大小会发生变化,在运行时才会确定数组的实际大小。
相比之下,固定数组有预先定义的大小,数组中的元素数量不应超过该大小。在极少数情况下,如果没有指定数组大小,则会创建一个“足够大”的数组——即“足够”容纳初始化数据的大小。
存储数组与内存数组
在 Solidity 中,开发者需要考虑两种类型的数组:存储数组和内存数组。
存储数组
存储数组通常声明为状态变量,大小可以是固定的,也可以是动态的。需要注意的是,长度可变的存储数组可以调整大小,这意味着它们可以使用 push() 和 pop() 函数,分别用于向数组添加和删除元素。
内存数组
内存数组声明时以 memory 作为其数据位置。与存储数组类似,内存数组在编译时也可以是固定或动态长度,但在分配内存后无法调整大小。这意味着不能使用 pop() 和 pull() Solidity 函数。
固定大小的内存数组在声明时会自动分配——例如 uint256[5] memory numbers 这样的语句。但是,要分配动态内存数组,需要使用 new 运算符。例如,可以使用 uint256[] memory numbers = new uint256[](5)``;
注意: 使用数组前务必先对其进行初始化,以便获得可用的有效地址。
Solidity 数组与映射(mapping)的比较
Solidity 中的映射与数组类似,都是用于存储一组数据的引用类型。但映射的语法和结构与数组截然不同,这使其能够服务于独特而重要的用途。映射是一张由键和值组成的表,每个键和值都有各自预先定义的类型。可以将映射理解为在 Solidity 智能合约内部初始化一张空表,等待被数据填充。
与数组不同,映射没有可获取的长度,键或值也不需要在初始化阶段就“设定”。此外,映射不能像 Solidity 数组那样进行遍历循环。
不过,从映射中检索数据比从数组中获取相同数据要高效得多。从数组中获取数据需要遍历整个数组,直到找到所需的特定元素,而映射可以直接获取该数据。
从映射中检索数据带来的性能优势,对于在 Ethereum 智能合约中节省 gas 而言相当重要,因为在区块链上编辑数据的交易需要支付 gas 费用。因此,尽可能高效地在智能合约中存储或检索数据,长期来看可以帮助开发者节省 ETH。
总之,如果你需要遍历一组数据,比如使用 for 循环,那么请使用数组。如果不需要遍历数据集,而是可以根据已知的键检索值,那么可以考虑使用映射。
声明和初始化数组
在 Solidity 中声明数组相对简单。需要指定元素的数据类型和元素数量,数组的大小应为正整数,且数据类型在 Solidity 中必须有效。
例如,下面这条基本指令会初始化数组,插入数据后,便可在 console.log 中查看:
数组中的每一项称为元素,每个元素都可以通过“n - 1”这一函数,利用其数字索引进行访问,其中 n 表示元素的编号。
一般来说,与元素相关的编号从 0 开始。这意味着第 10 个元素的索引为 9,第 11 个元素的索引为 10,依此类推。
关于相同数据类型的限制值得牢记,因为数组存储在连续的内存单元中,这意味着每个单元必须是相同的类型,因此大小也相同。
结构体(Structs)
结构体允许程序员定义自己的数据类型。一旦定义了结构体,就可以将其用作状态变量,或用于其他多种函数中,既可以使用位置参数,也可以使用关键字。后一种方式可以避免记住结构体内部成员的顺序。
以下是一个新结构体的示例:
映射(Mappings)
映射可以理解为键值存储,其中每一个可能的键都存在,任何值都可以通过该键一次性设置或获取。KeyType 可以是任意内置值类型(例如 bytes、string,或任意合约/枚举类型)。ValueType 可以是包括映射、数组和结构体在内的任意类型。
映射可以按如下方式声明:
注意:映射变量唯一允许的数据位置是 storage。
成员(Members)
要在数组中查找某个特定成员,意味着需要在数组中搜索直到找到该成员。开发者应了解两个重要函数:length 和 push。
- Length —— 返回数组的大小,也可用于更改动态数组的大小。
- Push —— 使开发者能够直接在动态存储数组末尾追加一个元素,并因此返回数组的新长度。
Solidity 数组的故障排查
开发者有时会遇到编译错误,这些错误可能源于对声明、创建和初始化数组规则的误解。以下是排查 Solidity 数组常见错误的一些高层次方法:
1. Array.length
array.length 函数允许开发者检查数组中当前的元素数量。内存数组的大小在声明时就已固定,而动态数组则是在运行时定义的,因此在操作时需要用到 length。
2. Abi.encode 和 abi.encodePacked
当调用外部合约上的函数时,EVM 会计算一个包含函数签名和参数的字节缓冲区,序列化参数的方式有两种:abi.encode 和 abi.encodePacked。
Abi.encode 按照 ABI 规范对其参数进行编码。实际上,ABI 的设计目的是用于调用合约,参数会被填充到 32 字节。如果要调用合约,通常会使用此函数。
相比之下,abi.encodePacked 使用该类型所需的最小空间对参数进行编码。编码一个 unit8 只会占用 1 字节,当你想节省空间且不打算调用合约时,可以使用此函数。
3. Byte[] 或 bytes
这两种数组类型都可以容纳任意长度的原始字节数据。byte\[\] 与 bytes 之间的区别在于,bytes\[\] 遵循数组类型的规则,而 Solidity 中内存数组的元素总是占用 32 字节的倍数。这意味着,如果某个元素不足 32 字节的倍数,就会被填充,直到达到所需大小。例如,在 byte 数组的情况下,每个元素可能浪费 31 字节,而使用 bytes 数组或 string 则不会出现这种情况。
4. String
这种特殊的动态数组是 UTF-8 数据类型,其功能与其他编程语言中的字符串不同,Solidity 没有提供获取字符串长度、进行两个字符串拼接或比较的函数。可以使用 bytes[] 将字符串快速转换为字节数组。
5. Solidity 数组的 null
尽管在 Python 和 JavaScript 等其他编程语言中存在 undefined、null、nil、none 等固有概念,但 Solidity 中并不存在这些概念。相反,开发者可以称之为零值或默认值概念,因为每个值一旦创建,就会在内存中占据一个位置,因此它必定包含某种内容。
如何进一步学习 Solidity 数组
要进一步学习 Solidity 编程语言,并熟悉数组以及其他基础主题,请前往 Alchemy University 的 Ethereum 训练营。Ethereum 训练营是一个为期 7 周的免费编程课程,配有指导视频和实践项目。
如果开发者是编程领域的新手,那么在开始 Ethereum 训练营之前,Alchemy 的为期 3 周的 JavaScript for Ethereum 课程是一个很好的先修课程。
相关概览

构建区块链应用
Alchemy 将最强大的 Web3 开发者产品和工具与资源、社区及专业支持结合在一起。


