文章目录
引用类型
引用类型的值可以通过多个不同的名称进行修改。这与值类型形成对比,在值类型中,每当使用一个值类型的变量时,都会获得一个独立的副本。因此,引用类型比值类型需要更谨慎地处理。目前,引用类型包括结构体(structs)、数组(arrays)和映射(mappings)。如果使用引用类型,必须明确提供数据存储的位置:memory(其生命周期仅限于外部函数调用期间)、storage(存储状态变量的位置,其生命周期与合约的生命周期一致)或 calldata(一个特殊的数据存储区域,其中包含函数参数)。
如果赋值或类型转换导致数据存储位置发生变化,则会自动触发复制操作,而在同一数据存储位置内部进行赋值时,仅在某些情况下会触发复制(对于 storage 类型)。
数据存储位置
每个引用类型都有一个额外的注释,即“数据存储位置”,用于指明其存储位置。数据存储位置包括 memory、storage 和 calldata。calldata 是一个不可修改、不可持久化的区域,其中存储了函数参数,其行为大多数情况下类似于 memory。
注意
transient 作为引用类型的数据存储位置目前尚不受支持。 如果可能,尽量使用 calldata 作为数据存储位置,因为这样可以避免复制,同时确保数据不可修改。具有 calldata 存储位置的数组和结构体可以作为函数的返回值,但无法直接分配此类类型。
注意
在函数体中声明或作为返回参数的 calldata 位置的数组和结构体必须在使用或返回之前进行赋值。在某些使用非平凡控制流的情况下,编译器可能无法正确检测初始化。在这些情况下,一个常见的解决方法是先将受影响的变量赋值给自身,然后再进行正确的初始化。
注意
在 0.6.9 版本之前,外部函数的引用类型参数数据存储位置仅限于 calldata,公共函数为 memory,内部和私有函数则可以是 memory 或 storage。而现在,所有可见性(visibility)的函数都允许使用 memory 和 calldata。
注意
构造函数的参数不能使用 calldata 作为数据存储位置。
注意
在 0.5.0 版本之前,数据存储位置可以省略,并且会根据变量类型、函数类型等默认使用不同的位置。但从 0.5.0 版本开始,所有复杂类型都必须显式指定数据存储位置。
分配行为
数据存储位置不仅与数据的持久性相关,还会影响赋值的语义:
- 在 storage 和 memory(或 calldata)之间的赋值总是会创建一个独立的副本。
- 在 memory 之间的赋值仅创建引用。这意味着对一个 memory 变量的修改会影响所有引用同一数据的 memory 变量。
- 从 storage 赋值给本地 storage 变量时,也只是赋值引用。
- 其他所有对 storage 的赋值都会进行复制。例如,对状态变量的赋值,或对 storage 结构体类型的本地变量的成员赋值,即使本地变量本身只是一个引用。
举个例子:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
contract C {
// x 的数据存储位置是 storage。
// 这是唯一可以省略数据存储位置的地方。
uint[] x;
// 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; // 合法,清空数组,同时影响 y
// 以下操作无法执行,因为它需要在 storage 中创建一个新的临时/匿名数组,
// 但 storage 是静态分配的:
// y = memoryArray;
// 同样,“delete y” 也是不合法的,因为对指向 storage 对象的本地变量的赋值
// 只能来自已有的 storage 对象。
// 它会“重置”指针,但没有合理的位置可以指向。
// 更多细节请参考“delete” 运算符的文档。
// delete y;
g(x); // 调用 g,传递 x 的引用
h(x); // 调用 h,创建一个独立的临时副本存储在 memory
}
function g(uint[] storage) internal pure {}
function h(uint[] memory) public pure {}
}
数组
数组可以是编译时固定大小的,也可以是动态大小的。
固定大小为 k,元素类型为 T 的数组写作 T[k]
,而动态大小的数组写作 T[]
。
例如,一个包含 5 个 uint
动态数组的数组写作 uint[][5]
。该表示法与某些其他语言相反。在 Solidity 中,X[3]