复合类型
枚举
枚举中的枚举值依次对应 0, 1, 2……:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract ValueTypeEnum {
enum Fruit {
Apple, // => 0,默认值
Banana, // => 1
Watermelon // => 2
}
function test()
public
pure
returns (Fruit, uint, Fruit, uint, Fruit, Fruit, Fruit)
{
Fruit defaultFruit;
Fruit a = Fruit.Watermelon;
return (
defaultFruit, // 0: 默认为 Apple <=> 0
uint(defaultFruit), // 0: Apple <=> 0
a, // 2: Watermelon <=> 2
uint(a), // 2: Watermelon <=> 2
Fruit(1), // 1: Banana <=> 1
type(Fruit).min, // 0: 最小取值为 0 <=> Apple
type(Fruit).max // 2: 最大取值为 2 <=> Watermelon
);
}
}
枚举中最多只能定义 256
个枚举值,枚举也可以定义在合约之外。
type(EnumType).min
和 type(EnumType).max
可以用来获取枚举值的取值范围,例如上面 Fruit
定义了三个枚举值,因此其取值范围为 。
枚举类型和整数可以互相显式转换。但是需要注意,如果转换前的整数不在枚举值取值范围内,将引发 panic
错误。
自定义类型
Solidity
支持将基本值类型自定义为另一种类型:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
// 定义一个新类型 EvenInt,其实际存储类型为 int
type EvenInt is int;
contract EvenIntContract {
function test() public pure returns (int) {
EvenInt a = EvenInt.wrap(1234); // 将 int 包裹为 EvenInt
int b = EvenInt.unwrap(a); // 将 EvenInt 解包为 int
return b;
}
}
使用 type NewType is Type
来将基本值类型 Type
定义为新类型 NewType
。
使用 NewType.wrap
与 NewType.unwrap
用于在 NewType
和 Type
之间相互转换。
定长字节数组
Solidity
定义了 32 个定长字节数组:bytes1
, bytes2
, …, bytes32
。每一个相当于对应长度的字节数组。如 bytes32
相当于一个长度为 32 的字节数组。
相比直接使用字节数组 bytes1[]
,bytes32
的长度刚好是一个字(256位),可以刚好放在一个 256 位的槽中。而 bytes1[]
作为一个数组,每一个元素都需要占用一个 256 位的槽,因此有 248 位被浪费。如果长度可以确定,应当尽量使用定长字节数组,可以节省成本。
定长字节数组还可以通过 bytesN[i]
的索引方式访问 bytesN
中的第 i
位(从0开始)。
数组
Solidity
支持静态数组(长度已知)以及动态数组(长度可变):
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract ValueTypeArray {
int[] public storageArray; // 只有 storage 存储区,才支持长度可变的动态数组
function test() public returns (int[5] memory, uint, uint) {
int[5] memory memoryArray; // 长度为 5 的 int 数组
storageArray.push(10); // storage 动态数组,支持 push
storageArray.push(); // 默认插入零值
storageArray.pop(); // 使用 pop 弹出元素
memoryArray[0] = 10; // 静态数组不支持 push,使用索引方式赋值
return (
memoryArray,
memoryArray.length, // 支持使用 .length 获取长度
storageArray.length // 支持使用 .length 获取长度
);
}
}
数组类型仅有以下三个成员方法:
push(x)
:用于推入一个元素,仅动态数组可用
。当 x 未给出时,则默认推入一个零值pop()
:用于弹出一个元素并返回,仅动态数组可用
length
:获取数组长度
数组支持以索引方式 arr[i]
访问数组元素。
数组元素类型可以是任何类型,也包括数组,如 int[2][2]
表示一个数组的数组。
需要注意,对于保存在 memory
内存区中的动态数组
,必须预先确定数组大小,当 Solidity
为其分配好内存之后,就不能用 push/pop
来调整其大小了。因此 memory
内存区的动态数组,更像是一个数组指针:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract ValueTypeDynamicArray {
int[] public storageArray; // storage 存储区动态数组
function test() public returns (uint, uint) {
storageArray.push(10); // storage 动态数组,支持 push
// memory 内存区动态数组,未指向任何数组,长度为0
int[] memory memoryArray;
uint initLength = memoryArray.length;
// 非法,memory 内存区动态数组不支持 push/pop
// memoryArray.push(10);
// 但是可以指向一个新的内存区数组
memoryArray = new int[](1);
memoryArray[0] = 0;
// 手动扩容,分配一块更大的数组
int[] memory tmp = new int[](2);
tmp[0] = memoryArray[0]; // 把 memoryArray 中的元素拷贝到新数组
memoryArray = tmp; // memoryArray 指向新的数组
memoryArray[1] = 1; // 成功扩容
return (initLength, memoryArray.length);
}
}
特殊的数组:bytes 与 string
bytes 与 bytes1[]
bytes
类似于 bytes1[]
,但是在 memory
内存区与 calldata
中,bytes
是按顺序紧密存储的。例如 bytes1[32]
数组,每个元素将占用32个字节,虽然有效数据只占1个字节,但总体占用将高达 32*32=1024 个字节。而使用 bytes
类型,则只占用 32 个字节。
可以使用 bytes.concat
来拼接字节数组:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract ValueTypeBytes {
function test() public pure returns (bytes memory) {
bytes memory byteArray = hex"01";
byteArray = bytes.concat(byteArray, hex"02", "03");
return byteArray;
}
}
bytes vs string
string
类似于 bytes
,但是 string
中保存的是经过 utf-8
编码后的字节。由于 utf-8
是变长的 unicode
编码,即不同的字符在 utf-8
编码中其长度不同。这就导致了我们没有办法通过索引 str[i]
来定位到第 i
个字符。因此相比 bytes
,string
类型是不支持 .length
属性以及 str[i]
索引访问的。
可以使用 string.concat
来拼接字符串:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract ValueTypeString {
function concat() public pure returns (string memory) {
string memory str = "01";
str = string.concat(str, "02", "03");
return str;
}
}
bytes
跟 string
之间可以相互转换:
- bytes 转 string:
string(bytesArray)
- string 转 bytes:
bytes(str)
数组切片
数组切片的语法为:arr[start:end]
:
- 相当于从 arr 中,取出索引大于等于
start
,小于end
的元素,构成一个新的数组 - 未给出
start
时,默认为 0 - 未给出
end
时,默认为arr.length
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract ValueTypeArraySlice {
function slice(bytes calldata echo) public pure returns (bytes memory) {
bytes memory a = hex"01";
a = bytes.concat(a, echo[:5]);
return a;
}
}
需要注意,数组切片目前只有 calldata
数组支持。
数组切片本身不是一种数据类型,因此不能把数组切片赋值给一个变量,它只能在表达式中做为中间值使用。
结构体
Solidity
支持 struct 结构体
:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract ComplexTypeStruct {
struct People {
string name;
uint8 age;
People[] sons;
}
struct People2 {
string name;
uint8 age;
mapping(uint => People2) sons;
uint[] sonsKeys;
}
}
struct
不支持直接嵌套自身,但是如果是包含自身类型的引用则可以,如上面的动态数组 People[] sons
,或者字典类型 mapping(uint => People2) sons
。
下面属于直接嵌套自身,元素大小无法确定:
struct People3 {
People3 son;
People3[2] sons;
}
mapping 类型
mapping
是 Solidity
中的字典类型。
语法为 mapping(K => V) dict
:
- K 为关键字类型,可以为 bytes、string、或者其他值类型。但不可以是自定义类型、引用类型,包括数组、struct 以及 mapping 类型。
- V 可以是任何类型
需要注意,只有 storage
存储区允许定义 mapping
类型变量。
mapping
类型可以看作一个超大的散列表。Solidity
默认该散列表包含了所有的 K
的可能取值,其对应的 V
值初始默认为零值。因此 mapping
类型没有 length
属性用于获取 mapping
的大小。同时我们也无法直接遍历整个 mapping
类型。
如果需要遍历 mapping
,则需要自己使用一个数组 K keys[]
将所有赋值过的关键字保存下来,然后通过遍历该数组来遍历目标 mapping 变量:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/utils/Strings.sol";
contract ComplexTypeMapping {
mapping(string => int) scores; // 保存姓名与成绩
string[] names; // 保存所有姓名
function addScore(string calldata name, int score) public {
scores[name] = score;
names.push(name);
}
function getScoresTable() public view returns (string memory) {
string memory res = "";
// 遍历 names 数组,间接遍历 scores
for(uint i = 0; i < names.length; i++) {
res = string.concat(res, names[i], Strings.toString(uint(scores[names[i]])), "
");
}
return res;
}
}
合约类型
合约类似于面向对象编程中的 类
,每一个合约本身是一个独立的类型。合约与地址之间可以显式转换:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract A {}
contract ValueTypeContract {
function test() public {
A a = new A();
address aAddr = address(a); // 合约转地址
A aRef = A(aAddr); // 地址转合约
}
}
合约转为地址,该地址即为合约的部署地址。地址转合约,则将该地址视为合约的部署地址,从该地址处获取合约代码,以实现合约调用。
如果合约派生于另一个合约,则可以被隐式地转换为祖先合约:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Grandpa {}
contract Dad is Grandpa {}
contract Son is Dad {}
contract ValueTypeContract {
function test() public payable {
Son s = new Son();
Grandpa p = s; // 隐式转换为 Grandpa
}
}
元组类型
Solidity
中还内置了一种中间类型 tuple 元组
。Python
程序员应该对此比较熟悉。
元组与数组切片一样,只能做为中间类型,即不存在类型为元组的变量。
元组实际上是通过括号将多个变量括起,做为一个整体,例如:(x, y)
。
元组最常见的使用是作为函数返回值,以实现返回多个值:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Tuple {
// 返回一个 (int, int) 型的二元元组
function position() internal pure returns (int x, int y) {
return (1, 2);
}
function test() internal pure {
// 解构赋值
(int x, int y) = position();
}
}
我们使用 (int x, int y) = position()
来取得 position
返回元组中的值,该方式称为 解构赋值
。
元组的解构赋值还有其他一些技巧用法:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract MemoryAssigment {
// 返回一个 (int, int) 型的二元元组
function position() internal pure returns (int x, int y) {
return (1, 2);
}
function test() internal pure {
// 解构赋值
(int x, int y) = position();
// 如果只需要其中一个值,也可以忽略另一个解构出来的值
(x, ) = position();
// 也可以这样赋值
(x, y) = (3, 4);
// 甚至用于交换两个变量的值
(x, y) = (y, x);
}
}
函数类型
见 《函数类型》 一章。