存儲位置與拷貝機制: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