复合类型

枚举

枚举中的枚举值依次对应 0, 1, 2……:

ValueTypeEnum.sol
运行
复制
// 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).mintype(EnumType).max 可以用来获取枚举值的取值范围,例如上面 Fruit 定义了三个枚举值,因此其取值范围为 [0,2][0, 2]

枚举类型和整数可以互相显式转换。但是需要注意,如果转换前的整数不在枚举值取值范围内,将引发 panic 错误。

自定义类型

Solidity 支持将基本值类型自定义为另一种类型:

ValueTypeEnum.sol
运行
复制
// 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.wrapNewType.unwrap 用于在 NewTypeType 之间相互转换。

定长字节数组

Solidity 定义了 32 个定长字节数组:bytes1, bytes2, …, bytes32。每一个相当于对应长度的字节数组。如 bytes32 相当于一个长度为 32 的字节数组。

相比直接使用字节数组 bytes1[]bytes32 的长度刚好是一个字(256位),可以刚好放在一个 256 位的槽中。而 bytes1[] 作为一个数组,每一个元素都需要占用一个 256 位的槽,因此有 248 位被浪费。如果长度可以确定,应当尽量使用定长字节数组,可以节省成本。

定长字节数组还可以通过 bytesN[i] 的索引方式访问 bytesN 中的第 i 位(从0开始)。

数组

Solidity 支持静态数组(长度已知)以及动态数组(长度可变):

ValueTypeArray.sol
运行
复制
// 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 内存区的动态数组,更像是一个数组指针:

ValueTypeDynamicArray.sol
运行
复制
// 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 来拼接字节数组:

ValueTypeBytes.sol
运行
复制
// 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 个字符。因此相比 bytesstring 类型是不支持 .length 属性以及 str[i] 索引访问的。

可以使用 string.concat 来拼接字符串:

ValueTypeString.sol
运行
复制
// 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;
    }
}

bytesstring 之间可以相互转换:

  • bytes 转 string:string(bytesArray)
  • string 转 bytes:bytes(str)

数组切片

数组切片的语法为:arr[start:end]

  • 相当于从 arr 中,取出索引大于等于 start,小于 end 的元素,构成一个新的数组
  • 未给出 start 时,默认为 0
  • 未给出 end 时,默认为 arr.length
ValueTypeArraySlice.sol
运行
复制
// 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 结构体

ComplexTypeStruct.sol
运行
复制
// 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 类型

mappingSolidity 中的字典类型。

语法为 mapping(K => V) dict

  • K 为关键字类型,可以为 bytes、string、或者其他值类型。但不可以是自定义类型、引用类型,包括数组、struct 以及 mapping 类型。
  • V 可以是任何类型

需要注意,只有 storage 存储区允许定义 mapping 类型变量。

mapping 类型可以看作一个超大的散列表。Solidity 默认该散列表包含了所有的 K 的可能取值,其对应的 V 值初始默认为零值。因此 mapping 类型没有 length 属性用于获取 mapping 的大小。同时我们也无法直接遍历整个 mapping 类型。

如果需要遍历 mapping,则需要自己使用一个数组 K keys[] 将所有赋值过的关键字保存下来,然后通过遍历该数组来遍历目标 mapping 变量:

ComplexTypeMapping.sol
运行
复制
// 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;
    }
}

合约类型

合约类似于面向对象编程中的 ,每一个合约本身是一个独立的类型。合约与地址之间可以显式转换:

ValueTypeContractAddr.sol
运行
复制
// 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); // 地址转合约
    }
}

合约转为地址,该地址即为合约的部署地址。地址转合约,则将该地址视为合约的部署地址,从该地址处获取合约代码,以实现合约调用。

如果合约派生于另一个合约,则可以被隐式地转换为祖先合约:

ValueTypeContract.sol
运行
复制
// 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)

元组最常见的使用是作为函数返回值,以实现返回多个值:

Tuple.sol
运行
复制
// 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 返回元组中的值,该方式称为 解构赋值

元组的解构赋值还有其他一些技巧用法:

Tuple.sol
运行
复制
// 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);
    }
}

函数类型

《函数类型》 一章。