阅读(288) (11)

数组

2022-05-12 09:41:27 更新

数组可以具有编译时固定大小,也可以具有动态大小。

固定大小k和元素类型的数组的类型T写为T[k],动态大小的数组写为T[]。

例如,一个由 5 个动态数组组成的数组uint写为 uint[][5]. 与其他一些语言相比,这种表示法是相反的。在 Solidity 中,X[3]总是一个包含三个 type 元素的数组X,即使X它本身就是一个数组。在其他语言(例如 C)中并非如此。

索引从零开始,访问与声明的方向相反。

例如,如果您有一个变量,则使用 访问第三个动态数组中的第七个,并使用 访问第三个动态数组。同样,如果您有一个类型的数组也可以是一个数组,那么总是有 type 。uint[][5] memory xuintx[2][6]x[2]T[5] aTa[2]T

数组元素可以是任何类型,包括映射或结构。类型的一般限制适用,因为映射只能存储在 storage数据位置,公开可见的函数需要ABI 类型的参数。

可以标记状态变量数组public并让 Solidity 创建一个getter。数字索引成为 getter 的必需参数。

访问超出其末尾的数组会导致断言失败。方法.push()和.push(value)可用于在数组末尾追加一个新元素,其中.push()追加一个零初始化元素并返回对它的引用。

bytesstring作为数组

bytes和 类型的变量string是特殊的数组。类型与bytes类似bytes1[],但它紧紧地封装在 calldata 和内存中。string等于bytes但不允许长度或索引访问。

Solidity 没有字符串操作函数,但有第三方字符串库。您还可以使用 keccak256-hash 比较两个字符串, 并使用 连接两个字符串。keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2))string.concat(s1, s2)

您应该使用bytesoverbytes1[]因为它更便宜,因为使用bytes1[]inmemory在元素之间添加了 31 个填充字节。请注意,在 中storage,由于紧密包装而缺少填充,请参见bytes 和 string。作为一般规则,bytes用于任意长度的原始字节数据和string任意长度的字符串 (UTF-8) 数据。如果您可以将长度限制为一定数量的字节,请始终使用其中一种值类型bytes1bytes32因为它们便宜得多。

笔记

如果要访问 string 的字节表示s,请使用 bytes(s).length/ 。请记住,您访问的是 UTF-8 表示的低级字节,而不是单个字符。bytes(s)[7] = 'x';

功能bytes.concatstring.concat

您可以使用 连接任意数量的stringstring.concat。该函数返回一个包含参数内容的单个数组,没有填充。如果要使用其他类型的参数不能隐式转换为,则需要先转换为。string memorystringstring

类似地,该bytes.concat函数可以连接任意数量的bytes或值。该函数返回一个包含参数内容的单个数组,没有填充。如果要使用字符串参数或其他不能隐式转换为的类型,则需要先将它们转换为或/…/ 。bytes1 ... bytes32bytes memorybytesbytesbytes1bytes32

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.12;

contract C {
    string s = "Storage";
    function f(bytes calldata bc, string memory sm, bytes16 b) public view {
        string memory concatString = string.concat(s, string(bc), "Literal", sm);
        assert((bytes(s).length + bc.length + 7 + bytes(sm).length) == bytes(concatString).length);

        bytes memory concatBytes = bytes.concat(bytes(s), bc, bc[:2], "Literal", bytes(sm), b);
        assert((bytes(s).length + bc.length + 2 + 7 + bytes(sm).length + b.length) == concatBytes.length);
    }
}

如果您调用string.concatbytes.concat不使用参数,它们将返回一个空数组。

分配内存数组

可以使用new运算符创建具有动态长度的内存数组。与存储数组相反,无法调整内存数组的大小(例如,.push成员函数不可用)。您要么必须提前计算所需的大小,要么创建一个新的内存数组并复制每个元素。

与 Solidity 中的所有变量一样,新分配的数组的元素始终使用默认值初始化。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract C {
    function f(uint len) public pure {
        uint[] memory a = new uint[](7);
        bytes memory b = new bytes(len);
        assert(a.length == 7);
        assert(b.length == len);
        a[6] = 8;
    }
}

数组文字

数组字面量是一个或多个表达式的逗号分隔列表,用方括号 ( [...]) 括起来。例如. 数组字面量的类型确定如下:[1, a, f(3)]

它始终是一个静态大小的内存数组,其长度是表达式的数量。

数组的基本类型是列表中第一个表达式的类型,这样所有其他表达式都可以隐式转换为它。如果这是不可能的,这是一个类型错误。

有一个所有元素都可以转换为的类型是不够的。其中一个元素必须属于该类型。

在下面的例子中,类型是 ,因为每个常量的类型都是。如果希望结果为类型,则需要将第一个元素转换为.[1, 2, 3]uint8[3] memoryuint8uint[3] memoryuint

// 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) public pure {
        // ...
    }
}

数组字面量无效,因为第一个表达式的类型是,而第二个表达式的类型是,并且它们不能隐式相互转换。为了使它工作,你可以使用,例如。[1, -1]uint8int8[int8(1), -1]

由于不同类型的固定大小的内存数组不能相互转换(即使基类型可以),如果你想使用二维数组字面量,你总是必须明确指定一个通用的基类型:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract C {
    function f() public pure returns (uint24[2][4] memory) {
        uint24[2][4] memory x = [[uint24(0x1), 1], [0xffffff, 2], [uint24(0xff), 3], [uint24(0xffff), 4]];
        // The following does not work, because some of the inner arrays are not of the right type.
        // uint[2][4] memory x = [[0x1, 1], [0xffffff, 2], [0xff, 3], [0xffff, 4]];
        return x;
    }
}

固定大小的内存数组不能分配给动态大小的内存数组,即以下是不可能的:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;

// This will not compile.
contract C {
    function f() public {
        // The next line creates a type error because uint[3] memory
        // cannot be converted to uint[] memory.
        uint[] memory x = [uint(1), 3, 4];
    }
}

计划在将来取消此限制,但由于数组在 ABI 中的传递方式,它会产生一些复杂性。

如果要初始化动态大小的数组,则必须分配各个元素:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract C {
    function f() public pure {
        uint[] memory x = new uint[](3);
        x[0] = 1;
        x[1] = 3;
        x[2] = 4;
    }
}

数组成员

长度

数组有一个length包含其元素数量的成员。内存数组的长度在创建后是固定的(但是是动态的,即它可以依赖于运行时参数)。

推()

动态存储数组 和bytes(not string) 有一个名为的成员函数push(),您可以使用它在数组末尾附加一个零初始化元素。它返回对元素的引用,以便可以像 或一样使用它。x.push().t = 2x.push() = b

推(x)

动态存储数组 和bytes(not string) 有一个名为的成员函数push(x),您可以使用它在数组末尾附加给定元素。该函数不返回任何内容。

流行()

动态存储数组 和bytes(not string) 有一个名为的成员函数pop(),您可以使用该函数从数组末尾删除一个元素。这也隐式调用删除元素上的删除。

笔记

通过调用增加存储数组的长度push() 具有恒定的gas成本,因为存储是零初始化的,而通过调用减少长度pop()的成本取决于被删除元素的“大小”。如果该元素是一个数组,它可能会非常昂贵,因为它包括显式清除已删除的元素,类似于对它们调用delete

笔记

要在外部(而不是公共)函数中使用数组数组,您需要激活 ABI coder v2。

笔记

在拜占庭之前的 EVM 版本中,无法访问函数调用返回的动态数组。如果您调用返回动态数组的函数,请确保使用设置为拜占庭模式的 EVM。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;

contract ArrayContract {
    uint[2**20] aLotOfIntegers;
    // Note that the following is not a pair of dynamic arrays but a
    // dynamic array of pairs (i.e. of fixed size arrays of length two).
    // Because of that, T[] is always a dynamic array of T, even if T
    // itself is an array.
    // Data location for all state variables is storage.
    bool[2][] pairsOfFlags;

    // newPairs is stored in memory - the only possibility
    // for public contract function arguments
    function setAllFlagPairs(bool[2][] memory newPairs) public {
        // assignment to a storage array performs a copy of ``newPairs`` and
        // replaces the complete array ``pairsOfFlags``.
        pairsOfFlags = newPairs;
    }

    struct StructType {
        uint[] contents;
        uint moreInfo;
    }
    StructType s;

    function f(uint[] memory c) public {
        // stores a reference to ``s`` in ``g``
        StructType storage g = s;
        // also changes ``s.moreInfo``.
        g.moreInfo = 2;
        // assigns a copy because ``g.contents``
        // is not a local variable, but a member of
        // a local variable.
        g.contents = c;
    }

    function setFlagPair(uint index, bool flagA, bool flagB) public {
        // access to a non-existing index will throw an exception
        pairsOfFlags[index][0] = flagA;
        pairsOfFlags[index][1] = flagB;
    }

    function changeFlagArraySize(uint newSize) public {
        // using push and pop is the only way to change the
        // length of an array
        if (newSize < pairsOfFlags.length) {
            while (pairsOfFlags.length > newSize)
                pairsOfFlags.pop();
        } else if (newSize > pairsOfFlags.length) {
            while (pairsOfFlags.length < newSize)
                pairsOfFlags.push();
        }
    }

    function clear() public {
        // these clear the arrays completely
        delete pairsOfFlags;
        delete aLotOfIntegers;
        // identical effect here
        pairsOfFlags = new bool[2][](0);
    }

    bytes byteData;

    function byteArrays(bytes memory data) public {
        // byte arrays ("bytes") are different as they are stored without padding,
        // but can be treated identical to "uint8[]"
        byteData = data;
        for (uint i = 0; i < 7; i++)
            byteData.push();
        byteData[3] = 0x08;
        delete byteData[2];
    }

    function addFlag(bool[2] memory flag) public returns (uint) {
        pairsOfFlags.push(flag);
        return pairsOfFlags.length;
    }

    function createMemoryArray(uint size) public pure returns (bytes memory) {
        // Dynamic memory arrays are created using `new`:
        uint[2][] memory arrayOfPairs = new uint[2][](size);

        // Inline arrays are always statically-sized and if you only
        // use literals, you have to provide at least one type.
        arrayOfPairs[0] = [uint(1), 2];

        // Create a dynamic byte array:
        bytes memory b = new bytes(200);
        for (uint i = 0; i < b.length; i++)
            b[i] = bytes1(uint8(i));
        return b;
    }
}