侧边栏壁纸
博主头像
秋码记录

一个游离于山间之上的Java爱好者 | A Java lover living in the mountains

  • 累计撰写 137 篇文章
  • 累计创建 297 个标签
  • 累计创建 45 个分类

玩以太坊链上项目的必备技能(类型-引用类型-Solidity之旅三)

数组元素可以是任何类型,包括映射或结构体。对类型的限制是映射只能存储在 存储storage 中,并且公开访问函数的参数需要是 ABI 类型。 状态变量标记 public 的数组,Solidity创建一个 getter函数 。 小标数字索引就是 getter函数 的参数。

在前文我们讲述了值类型,也就说再修改值类型的时候,每次都有一个独立的副本,如:string 类型的状态变量,其值是无法修改,而是拷贝出一份该状态的变量,将新值存起来。对于处理稍微复杂地值类型时,拷贝将变得愈发大了,也正是介于此,才考虑到将数据存放在内存(memory)或是存放在存储(storage)

在 Solidity 中,数组(array)和 结构体(struct)属于引用类型

更改数据位置或类型转换将始终产生自动进行一份拷贝,而在同一数据位置内(对于 存储(storage) 来说)的复制仅在某些情况下进行拷贝。

数据位置和赋值行为

所有的引用类型,如数组(array0结构体(struct)类型,都有别同于其他类型,那便是引用类型有额外地属性——数据位置。

数据位置,顾名思义就是数据存放的位置在哪里?是在内存(memory)中还是在存储(storage)中。

当然咯,大多时候数据都有默认的存放位置。也可显式地修改其数据存放的位置,只需在类型后添加memorystorage

函数参数/形参(包括函数的返回参数)数据位置默认在memory局部变量数据位置则默认在storage,但状态变量数据位置被强制在storage

还有一个调用数据(calldata)存储方式,用于存放外部函数(external)的参数/形参,其效果跟memory差不离。

指定数据存放的位置是非常的重要,因为它们将会影响其赋值行为。

  • 在 存储storage 和 内存memory 之间两两赋值(或者从 调用数据calldata 赋值 ),都会创建一份独立的拷贝。
  • 从 内存memory 到 内存memory 的赋值只创建引用, 这意味着更改内存变量,其他引用相同数据的所有其他内存变量的值也会跟着改变。
  • 从 存储storage 到本地存储变量的赋值也只分配一个引用。
  • 其他的向 存储storage 的赋值,总是进行拷贝。 这种情况的示例如对状态变量或 存储storage 的结构体类型的局部变量成员的赋值,即使局部变量本身是一个引用,也会进行一份拷贝(译者注:查看下面 ArrayContract 合约 更容易理解)。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.4.0;

contract StorageExam {
    uint[] x; // x 的数据存储位置是 storage

    // memoryArray 的数据存储位置是 memory
    function f(uint[] memory memoryArray) public {
        x = memoryArray; // 将整个数组拷贝到 storage 中
        uint[] storage y = x;  // 分配一个指针(其中 y 的数据存储位置是 storage)
        y[7]; // 返回第 8 个元素
        y.pop(); // 通过 y 修改 x
        delete x; // 删除数组 x,同样也会删除数组 y

        // 下面的方法不起作用;它需要在存储中创建一个新的临时未命名数组
        // 未命名的数组,但存储是 "静态 "分配的。
        // y = memoryArray;
        // 同样,"删除y "也是无效的,因为对本地变量的赋值只能从现有的存储对象中进行。
        // 它将 "重置 "指针,但是没有任何合理的位置可以让它指向
        // delete y;

        g(x); // 调用 g 函数,同时移交对 x 的引用
        h(x); // 调用 h 函数,同时在 memory 中创建一个独立的临时副本
    }

    function g(uint[] storage ) internal pure {}
    function h(uint[] memory) public pure {}
}

归纳为:

强制指定的数据位置:

  • 外部函数的参数(不包括返回参数): calldata
  • 状态变量: storage

默认数据位置:

  • 函数参数(包括返回参数): memory
  • 所有其它局部变量: storage

数组(array)

数组是用来存放一组数据(整数、字符串、地址等),它是一种常见的数据类型,而在 Solidity 中,数组可分为编译时的固定大小,动态大小的两种数组。

固定大小数组声明格式:

T[k] // T 为元素类型  k则是数组的大小
uint[7] arr;
address[50] address1;

动态大小数组声明格式:

T[] //T为元素类型  由于是动态分配的 所以只需[]
unit[] arr2;
bytes1[] arr3;
address[] arr4;
bytes arr5;  //注意 bytes本身就是数组

一个长度为 5,元素类型为 uint 的动态数组的数组(二维数组),应声明为 uint[][5] (注意这里跟其它语言比,数组长度的声明位置是反的)。作为对比,如在Java中,声明一个包含5个元素、每个元素都是数组的方式为 int[5][]

在Solidity中, X[3] 总是一个包含三个 X 类型元素的数组,即使 X 本身就是一个数组,这和其他语言也有所不同,比如 C 语言。

数组下标是从 0 开始的,且访问数组时的下标顺序与声明时相反。

如果有一个变量为 uint[][5] memory x, 要访问第三个动态数组的第7个元素,使用 x[2][6],要访问第三个动态数组使用 x[2]。 同样,如果有一个 T 类型的数组 T[5] a , T 也可以是一个数组,那么 a[2] 总会是 T 类型。

img

访问超出数组长度的元素会导致异常(assert 类型异常 )。 可以使用 .push() 方法在末尾追加一个新元素,其中 .push() 追加一个零初始化的元素并返回对它的引用。

bytesstring 类型的变量是特殊的数组。 bytes 类似于 bytes1[],但它在 调用数据calldata 和 内存memory 中会被“紧打包”(译者注:将元素连续地存在一起,不会按每 32 字节一单元的方式来存放)。 stringbytes 相同,但不允许用长度或索引来访问。

Solidity没有字符串操作函数,但是可以使用第三方字符串库,我们可以比较两个字符串通过计算他们的 keccak256-hash ,可使用 keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2)) 和使用 string.concat(s1, s2) 来拼接字符串。

我们更多时候应该使用 bytes 而不是 bytes1[] ,因为Gas 费用更低, 在 内存memory 中使用 bytes1[] 时,会在元素之间添加31个填充字节。 而在 存储storage 中,由于紧密包装,这没有填充字节。 作为一个基本规则,对任意长度的原始字节数据使用 bytes,对任意长度字符串(UTF-8)数据使用 string

可以将数组标识为 public,从而让 Solidity 创建一个 getter。 之后必须使用数字下标作为参数来访问 getter。

创建数组规则

  • 使用new在内存(memory)中创建动态数组,需声明长度,且在声明后不能修改数组的大小
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.4.16;

contract Arr {
    function f(uint len) public pure {
        uint[] memory a = new uint[](7);
        bytes memory b = new bytes(len);
        // 这里我们有 a.length == 7 以及 b.length == len
        a[6] = 8;
    }
}
  • 数组字面常数是一种定长的 内存(memory) 数组类型,它的基础类型是由其中元素的类型决定。 例如,[1, 2, 3] 的类型是 uint8[3] memory,因为其中的每个字面常数的类型都是 uint8。 正因为如此,有必要将上面这个例子中的第一个元素转换成 uint 类型。 目前需要注意的是,定长的 内存memory 数组并不能赋值给变长的 内存memory 数组
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.4.16;

contract C {
    function f() public pure {
        g([uint(1), 2, 3]);
    }
    function g(uint[3] _data) public pure {
        // ...
    }
}
// SPDX-License-Identifier: GPL-3.0
// 这段代码并不能编译。
pragma solidity ^0.4.0;

contract C {
    function f() public {
        // 这一行引发了一个类型错误,因为 unint[3] memory
        // 不能转换成 uint[] memory。
        uint[] x = [uint(1), 3, 4];
    }
}

数组成员

  • length: 数组有一个包含元素数量的length成员,memory数组的长度在创建后是固定的。
  • push(): 动态数组bytes拥有push()成员,可以在数组最后添加一个0元素。
  • push(x): 动态数组bytes拥有push(x)成员,可以在数组最后添加一个x元素。
  • pop(): 动态数组bytes拥有pop()成员,可以移除数组最后一个元素。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.4.16;

contract ArrayContract {
    uint[2**20] m_aLotOfIntegers;
    // 注意下面的代码并不是一对动态数组,
    // 而是一个数组元素为一对变量的动态数组(也就是数组元素为长度为 2 的定长数组的动态数组)。
    bool[2][] m_pairsOfFlags;
    // newPairs 存储在 memory 中 —— 函数参数默认的存储位置

    function setAllFlagPairs(bool[2][] newPairs) public {
        // 向一个 storage 的数组赋值会替代整个数组
        m_pairsOfFlags = newPairs;
    }

    function setFlagPair(uint index, bool flagA, bool flagB) public {
        // 访问一个不存在的数组下标会引发一个异常
        m_pairsOfFlags[index][0] = flagA;
        m_pairsOfFlags[index][1] = flagB;
    }

    function changeFlagArraySize(uint newSize) public {
        // 如果 newSize 更小,那么超出的元素会被清除
        m_pairsOfFlags.length = newSize;
    }

    function clear() public {
        // 这些代码会将数组全部清空
        delete m_pairsOfFlags;
        delete m_aLotOfIntegers;
        // 这里也是实现同样的功能
        m_pairsOfFlags.length = 0;
    }

    bytes m_byteData;

    function byteArrays(bytes data) public {
        // 字节的数组(语言意义中的 byte 的复数 ``bytes``)不一样,因为它们不是填充式存储的,
        // 但可以当作和 "uint8[]" 一样对待
        m_byteData = data;
        m_byteData.length += 7;
        m_byteData[3] = byte(8);
        delete m_byteData[2];
    }

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

    function createMemoryArray(uint size) public pure returns (bytes) {
        // 使用 `new` 创建动态 memory 数组:
        uint[2][] memory arrayOfPairs = new uint[2][](size);
        // 创建一个动态字节数组:
        bytes memory b = new bytes(200);
        for (uint i = 0; i < b.length; i++)
            b[i] = byte(i);
        return b;
    }
}

结构体(struct)

Solidity 中的结构体与 c 语言、golang 很相似,通过构造结构体来定义一种新类型。

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

contract CrowdFunding {
    // 定义的新类型包含两个属性。
    struct Funder {
        address addr;
        uint amount;
    }

    struct Campaign {
        address beneficiary;
        uint fundingGoal;
        uint numFunders;
        uint amount;
        mapping (uint => Funder) funders; //这是 映射  后续会讲到
    }

    uint numCampaigns;
    mapping (uint => Campaign) campaigns; //这是 映射  后续会讲到

    function newCampaign(address beneficiary, uint goal) public returns (uint campaignID) {
        campaignID = numCampaigns++; // campaignID 作为一个变量返回
        // 创建新的结构体示例,存储在 storage 中。我们先不关注映射类型。
        campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0);
    }

    function contribute(uint campaignID) public payable {
        Campaign storage c = campaigns[campaignID];
        // 以给定的值初始化,创建一个新的临时 memory 结构体,
        // 并将其拷贝到 storage 中。
        // 注意你也可以使用 Funder(msg.sender, msg.value) 来初始化。
        c.funders[c.numFunders++] = Funder({addr: msg.sender, amount: msg.value});
        c.amount += msg.value;
    }

    function checkGoalReached(uint campaignID) public returns (bool reached) {
        Campaign storage c = campaigns[campaignID];
        if (c.amount < c.fundingGoal)
            return false;
        uint amount = c.amount;
        c.amount = 0;
        c.beneficiary.transfer(amount);
        return true;
    }
}

上面的合约只是一个简化版的众筹合约,但它已经足以让我们理解结构体的基础概念。 结构体类型可以作为元素用在映射和数组中,其自身也可以包含映射和数组作为成员变量。

尽管结构体本身可以作为映射的值类型成员,但它并不能包含自身。 这个限制是有必要的,因为结构体的大小必须是有限的。

注意在函数中使用结构体时,一个结构体是如何赋值给一个局部变量(默认存储位置是 存储(storage) )的。 在这个过程中并没有拷贝这个结构体,而是保存一个引用,所以对局部变量成员的赋值实际上会被写入状态。

当然,你也可以直接访问结构体的成员而不用将其赋值给一个局部变量,就像这样, campaigns[campaignID].amount = 0

img

花了不到1块5,玩了下全网最火的ChatGPT
« 上一篇 2022-12-08
玩以太坊链上项目的必备技能(类型-映射类型-Solidity之旅四)
下一篇 » 2022-12-10

相关推荐

  • 玩以太坊链上项目的必备技能(类型-值类型-Solidity之旅二) 2022-12-06 21:52:25 +0800 +0800
  • 玩以太坊链上项目的必备技能(初识智能合约语言-Solidity之旅一) 2022-12-05 22:34:42 +0800 +0800
  • 在构建 Web3 前,需先知道什么是区块链,毕竟 Web3 是基于区块链 2022-12-01 20:30:51 +0800 +0800
  • Web3 来了,让我们展开双手拥抱它吧! 2022-11-30 20:28:25 +0800 +0800
  • Go 语言中的 if 分支结构(Let's Go 十五) 2022-09-28 22:12:00 +0800 +0800