存储位置与拷贝机制:storage、memory、calldata
学习目标
理解 EVM 中三种数据存储位置的特点,以及引用类型在不同存储位置之间赋值时的拷贝规则。
前置知识
已学习值类型和引用类型(数组、结构体、映射、字符串)。
三种存储位置
storage —— 持久化存储
- 类似数据库,数据永久保存在区块链上
- 成员变量(状态变量)默认存储在 storage
- 读写 gas 成本最高
memory —— 临时内存
- 函数执行期间存在,函数返回后销毁
- 局部变量默认存储在 memory
- gas 成本适中
calldata —— 调用数据
- 来自 transaction 的
msg.data,只读 - 不可修改,gas 成本最低
- 适用于 external 函数的参数
存储位置对引用类型的影响
关键规则:
- 不同 location 的同一引用类型,编译器视为不同类型
public/external函数参数只能是memory或calldatainternal/private函数参数可以是storage
综合示例
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract DataLocations {
struct MyStruct {
uint256 foo;
string text;
}
mapping(address => MyStruct) myStructs;
function examples(uint[] calldata y, string calldata s) external returns (uint[] memory) {
myStructs[msg.sender] = MyStruct({foo: 123, text: "bar"});
// storage 引用:修改会持久化
MyStruct storage myStruct = myStructs[msg.sender];
myStruct.text = "foo";
// memory 副本:修改不影响存储
MyStruct memory readOnly = myStructs[msg.sender];
readOnly.foo = 456; // 不会影响 myStructs
_internal(y);
uint[] memory memArr = new uint[](3);
memArr[0] = 234;
return memArr;
}
function _internal(uint[] calldata y) private pure {
uint x = y[0];
}
}
值拷贝 vs 引用拷贝
这是 Solidity 中最容易混淆的知识点之一,务必反复理解。
核心概念:成员变量的特殊性
在 EVM 中,成员变量(状态变量)指向固定的 storage 数据块(slot),不能像一般引用变量那样切换指向的数据块。因此对成员变量赋值,只能是值拷贝。
判定算法
对于赋值操作 x = a,按以下规则判定是值拷贝还是引用拷贝:
1. 如果 x 是成员变量 → 值拷贝
2. 如果 x 是局部变量:
- x 与 a 的 location 相同 → 引用拷贝
- x 与 a 的 location 不同 → 值拷贝
检查算法
当判定为值拷贝时,编译器还会进行以下检查:
1. 检查类型中是否包含 mapping → 有则编译错误(mapping 不支持拷贝)
2. 检查 x 是否为 calldata → 是则编译错误(calldata 只读)
3. 通过检查 → 执行值拷贝
一句话总结
当赋值被解释为引用拷贝时,如果不与更高优先级的设计选择相冲突,则为引用拷贝;否则为值拷贝。
常见场景速查
| 赋值场景 | 拷贝类型 | 说明 |
|---|---|---|
成员变量 = storage引用 |
值拷贝 | 成员变量始终值拷贝 |
成员变量 = memory变量 |
值拷贝 | 成员变量始终值拷贝 |
storage局部变量 = 成员变量 |
引用拷贝 | 同为 storage,指向同一数据 |
memory局部变量 = memory变量 |
引用拷贝 | 同为 memory,指向同一数据 |
memory局部变量 = storage变量 |
值拷贝 | 不同 location |
storage局部变量 = memory变量 |
编译错误 | storage 局部变量只能引用已有 storage 数据 |
关于默认值与初始化
- 成员变量:自动初始化为默认值(
uint→ 0,bool→ false,address→ 0x0 等) - memory 局部变量:自动初始化为默认值
- storage 局部变量:必须经过赋值才能使用,不会自动初始化
历史安全漏洞:早期 Solidity 版本中,未赋值的 storage 局部变量会默认指向 slot 0,可能意外覆盖其他状态变量的数据,造成严重安全问题。现代编译器已修复此问题。
完整 storage 交互示例
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract DataLocations {
uint[] public arr;
mapping(uint => address) map;
struct MyStruct {
uint foo;
}
mapping(uint => MyStruct) myStructs;
function f() public {
// 将 storage 变量传递给 internal 函数
_f(arr, map, myStructs[1]);
// storage 局部变量:引用拷贝,指向 myStructs[1]
MyStruct storage myStruct = myStructs[1];
// memory 局部变量:独立副本
MyStruct memory myMemStruct = MyStruct(0);
}
function _f(
uint[] storage _arr,
mapping(uint => address) storage _map,
MyStruct storage _myStruct
) internal {
// 操作 storage 引用,修改会持久化
}
function g(uint[] memory _arr) public returns (uint[] memory) {
// 操作 memory 数组,函数返回后销毁
}
function h(uint[] calldata _arr) external {
// 操作 calldata 数组(只读,gas 更低)
}
}
小结
| 存储位置 | 生命周期 | 可写 | Gas 成本 | 典型用途 |
|---|---|---|---|---|
| storage | 永久 | 是 | 最高 | 状态变量 |
| memory | 函数执行期间 | 是 | 中等 | 局部变量、函数参数 |
| calldata | 函数执行期间 | 否 | 最低 | external 函数参数 |
拷贝规则核心:
- 赋值给成员变量 → 始终值拷贝
- 局部变量间赋值 → 同 location 引用拷贝,不同 location 值拷贝
- mapping 不可值拷贝,calldata 不可写入
上一篇:引用类型详解
主题测试文章,只做测试使用。发布者:Walker,转转请注明出处:https://walker-learn.xyz/archives/7490