智能合约开发从0到100(4)- 引用类型

178 阅读8分钟

引用类型

值类型的变量,每次赋予新的名称都有独立的副本,修改互不影响。

而引用类型可以通过多个不同的名称修改同一个值。

数据位置

类型占的空间更大,超过256字节,因为拷贝它们占用更多的空间。由此我们需要考虑将它们存储在什么位置,在内存(memory,数据不是永久存在)还是存储(永久存在,只要不主动删除或者修改它)

所有的引用类型,都有一个额外注解 数据位置 ,来说明数据存储位置。

有三种位置:

  • 内存memory ,存储位置同我们普通程序的内存一致。即分配,即使用,越过作用域即不可被访问,等待被回收
  • 存储storage,状态变量保存的位置,只要合约存在就一直存储.
  • 调用数据calldata ,用来保存函数参数的特殊数据位置,效果大多类似 内存memory ,但其是不可修改的

如果可以的话,函数参数尽量使用 calldata 作为数据位置,因为它将避免复制,并确保不能修改数据

数据位置与赋值行为

数据位置不仅仅表示数据如何保存,它同样影响着赋值行为:

  • 在 storage 和 memory 之间两两赋值(或者从 调用数据calldata 赋值 ),都会创建一份独立的拷贝。
  • 从 memory 到 memory 的赋值只创建引用, 这意味着更改内存变量,其他引用相同数据的所有其他内存变量的值也会跟着改变。
  • 从 storage 到本地storage变量的赋值也只分配一个引用。
  • 其他的向 存储storage 的赋值,总是进行拷贝。

数组

数组可以在声明时指定长度,也可以动态调整大小(长度)。

一个元素类型为 T,固定长度为 k 的数组可以声明为 T[k],而动态数组声明为 T[]。 举个例子,一个长度为 5,元素类型为 uint 的动态数组的数组(二维数组),应声明为 uint[][5] (注意这里跟其它语言比,数组长度的声明位置是反的)。

在Java中,声明一个包含5个元素、每个元素都是数组的方式为 int[5][]

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

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

数组元素可以是任何类型,包括映射或结构体。对类型的限制是映射只能存储在 storage 中,并且公开访问函数的参数需要是 ABI 类型。

状态变量标记 public 的数组,Solidity创建一个 getter函数 。 小标数字索引就是 getter函数 的参数。

bytesstring 也是数组

bytesstring 类型的变量是特殊的数组。 bytes 类似于 bytes1[],但它在 calldata 和 memory 中会被“紧打包”,即将元素连续地存在一起,不会按每 32 字节一单元的方式来存放

我们更多时候应该使用 bytes 而不是 bytes1[] ,因为Gas 费用更低, 在 memory 中使用 bytes1[] 时,会在元素之间添加31个填充字节。

作为一个基本规则,对任意长度的原始字节数据使用 bytes,对任意长度字符串(UTF-8)数据使用 string

如果使用一个长度限制的字节数组,应该使用一个 bytes1bytes32 的具体类型,因为它们便宜得多。

由于bytesstring,可以自由转换,你可以将字符串s通过bytes(s)转为一个bytes。但需要注意这时你访问的是 UTF-8 形式的低级 bytes 类型,而不是单个的字符。比如中文编码是多字节,变长的,所以你访问到的很有可能只是其中的一个代码点。

函数 bytes.concatstring.concat

可以使用 string.concat 连接任意数量的 string 字符串。 该函数返回一个 string memory ,包含所有参数的内容,无填充方式拼接在一起。

同样, bytes.concat 函数可以连接任意数量的 bytesbytes1 ... bytes32 值。 该函数返回一个 bytes memory ,包含所有参数的内容,无填充方式拼接在一起。

创建内存数组

可使用 new 关键字在 memory 中基于运行时创建动态长度数组。 与 storage 数组相反的是,你 不能 通过修改成员变量 .push 改变 memory 数组的大小。

数组常量

数组常量(字面量)是在方括号中( [...] ) 包含一个或多个逗号分隔的表达式。例如 [1, a, f(3)]

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

数组的基本类型是列表上的第一个表达式的类型,以便所有其他表达式可以隐式地转换为它。如果不可以转换,将出现类型错误。

数组成员

length

数组有 length 成员变量表示当前数组的长度。

push()

它用来添加新的零初始化元素到数组末尾,并返回元素引用

push(x)

用来在数组末尾添加一个给定的元素,这个函数没有返回值.

pop()

它用来从数组末尾删除元素

对存储数组元素的悬空引用

当使用存储数组时,你需要注意避免悬空的引用。 悬空引用是指一个指向不再存在的东西的引用,或者是对象被移除而没有更新引用。 例如,如果你将一个数组元素的引用存储在一个局部的引用中,然后从包含数组中 .pop() 出来,就会发生悬空引用。

数组切片

数组切片是数组连续部分的视图,用法如:x[start:end]startend 是 uint256 类型(或结果为 uint256 的表达式)。 x[start:end] 的第一个元素是 x[start] , 最后一个元素是 x[end - 1]

数组切片没有任何成员。 它们可以隐式转换为其“背后”类型的数组,并支持索引访问。 索引访问也是相对于切片的开始位置。 数组切片没有类型名称,这意味着没有变量可以将数组切片作为类型,它们仅存在于中间表达式中。

结构体

类似c++的结构体

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

// 定义的新类型包含两个属性。
// 在合约外部声明结构体可以使其被多个合约共享。 在这里,这并不是真正需要的。
struct Funder {
    address addr;
    uint amount;
}

contract CrowdFunding {

    // 也可以在合约内部定义结构体,这使得它们仅在此合约和衍生合约中可见。
    struct Campaign {
        address beneficiary;
        uint fundingGoal;
        uint numFunders;
        uint amount;
        mapping (uint => Funder) funders;
    }

    uint numCampaigns;
    mapping (uint => Campaign) campaigns;

    function newCampaign(address payable beneficiary, uint goal) public returns (uint campaignID) {
        campaignID = numCampaigns++; // campaignID 作为一个变量返回

        // 不能使用 "campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0)"
        // 因为RHS(right hand side)会创建一个包含映射的内存结构体 "Campaign"
        Campaign storage c = campaigns[campaignID];
        c.beneficiary = beneficiary;
        c.fundingGoal = goal;
    }

    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 的局部变量。 在这个过程中并没有拷贝这个结构体,而是保存一个引用,所以对局部变量成员的赋值实际上会被写入状态。

映射

映射类型在声明时的形式为 mapping(KeyType => ValueType)。 其中 KeyType 可以是任何基本类型,即可以是任何的内建类型, bytesstring 或合约类型、枚举类型。 而其他用户定义的类型或复杂的类型如:映射、结构体、即除 bytesstring 之外的数组类型是不可以作为 KeyType 的类型的。

ValueType 可以是包括映射类型在内的任何类型。

映射可以视作 哈希表 ,它们在实际的初始化过程中创建每个可能的 key, 并将其映射到字节形式全是零的值:一个类型的 默认值。然而下面是映射与哈希表不同的地方: 在映射中,实际上并不存储 key,而是存储它的 keccak256 哈希值,从而便于查询实际的值。

正因为如此,映射是没有长度的,也没有 key 的集合或 value 的集合的概念。 ,因此如果没有其他信息键的信息是无法被删除

映射只能是 存储storage 的数据位置,因此只允许作为状态变量 或 作为函数内的 存储storage 引用 或 作为库函数的参数。 它们不能用合约公有函数的参数或返回值。

这些限制同样适用于包含映射的数组和结构体。

可以将映射声明为 public ,然后来让 Solidity 创建一个 getter 函数KeyType 将成为 getter 的必须参数,并且 getter 会返回 ValueType

如果 ValueType 是一个映射。这时在使用 getter 时将需要递归地传入每个 KeyType 参数

可迭代映射

映射本身是无法遍历的,即无法枚举所有的键。不过,可以在它们之上实现一个数据结构来进行迭代。