引用类型
值类型的变量,每次赋予新的名称都有独立的副本,修改互不影响。
而引用类型可以通过多个不同的名称修改同一个值。
数据位置
类型占的空间更大,超过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函数 的参数。
bytes 和 string 也是数组
bytes 和 string 类型的变量是特殊的数组。 bytes 类似于 bytes1[],但它在 calldata 和 memory 中会被“紧打包”,即将元素连续地存在一起,不会按每 32 字节一单元的方式来存放
我们更多时候应该使用 bytes 而不是 bytes1[] ,因为Gas 费用更低, 在 memory 中使用 bytes1[] 时,会在元素之间添加31个填充字节。
作为一个基本规则,对任意长度的原始字节数据使用 bytes,对任意长度字符串(UTF-8)数据使用 string 。
如果使用一个长度限制的字节数组,应该使用一个 bytes1 到 bytes32 的具体类型,因为它们便宜得多。
由于bytes与string,可以自由转换,你可以将字符串s通过bytes(s)转为一个bytes。但需要注意这时你访问的是 UTF-8 形式的低级 bytes 类型,而不是单个的字符。比如中文编码是多字节,变长的,所以你访问到的很有可能只是其中的一个代码点。
函数 bytes.concat 和 string.concat
可以使用 string.concat 连接任意数量的 string 字符串。 该函数返回一个 string memory ,包含所有参数的内容,无填充方式拼接在一起。
同样, bytes.concat 函数可以连接任意数量的 bytes 或 bytes1 ... bytes32 值。 该函数返回一个 bytes memory ,包含所有参数的内容,无填充方式拼接在一起。
创建内存数组
可使用 new 关键字在 memory 中基于运行时创建动态长度数组。 与 storage 数组相反的是,你 不能 通过修改成员变量 .push 改变 memory 数组的大小。
数组常量
数组常量(字面量)是在方括号中( [...] ) 包含一个或多个逗号分隔的表达式。例如 [1, a, f(3)] 。
它总是一个静态大小的内存数组,其长度为表达式的数量。
数组的基本类型是列表上的第一个表达式的类型,以便所有其他表达式可以隐式地转换为它。如果不可以转换,将出现类型错误。
数组成员
length
数组有 length 成员变量表示当前数组的长度。
push()
它用来添加新的零初始化元素到数组末尾,并返回元素引用
push(x)
用来在数组末尾添加一个给定的元素,这个函数没有返回值.
pop()
它用来从数组末尾删除元素
对存储数组元素的悬空引用
当使用存储数组时,你需要注意避免悬空的引用。 悬空引用是指一个指向不再存在的东西的引用,或者是对象被移除而没有更新引用。 例如,如果你将一个数组元素的引用存储在一个局部的引用中,然后从包含数组中 .pop() 出来,就会发生悬空引用。
数组切片
数组切片是数组连续部分的视图,用法如:x[start:end] , start 和 end 是 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 可以是任何基本类型,即可以是任何的内建类型, bytes 和 string 或合约类型、枚举类型。 而其他用户定义的类型或复杂的类型如:映射、结构体、即除 bytes 和 string 之外的数组类型是不可以作为 KeyType 的类型的。
ValueType 可以是包括映射类型在内的任何类型。
映射可以视作 哈希表 ,它们在实际的初始化过程中创建每个可能的 key, 并将其映射到字节形式全是零的值:一个类型的 默认值。然而下面是映射与哈希表不同的地方: 在映射中,实际上并不存储 key,而是存储它的 keccak256 哈希值,从而便于查询实际的值。
正因为如此,映射是没有长度的,也没有 key 的集合或 value 的集合的概念。 ,因此如果没有其他信息键的信息是无法被删除
映射只能是 存储storage 的数据位置,因此只允许作为状态变量 或 作为函数内的 存储storage 引用 或 作为库函数的参数。 它们不能用合约公有函数的参数或返回值。
这些限制同样适用于包含映射的数组和结构体。
可以将映射声明为 public ,然后来让 Solidity 创建一个 getter 函数。 KeyType 将成为 getter 的必须参数,并且 getter 会返回 ValueType 。
如果 ValueType 是一个映射。这时在使用 getter 时将需要递归地传入每个 KeyType 参数
可迭代映射
映射本身是无法遍历的,即无法枚举所有的键。不过,可以在它们之上实现一个数据结构来进行迭代。