← 返回
Web3与WASM 2026.03.09

存儲位置與拷貝機制:storage、memory、calldata

Web3与WASM

學習目標

理解 EVM 中三種數據存儲位置的特點,以及引用類型在不同存儲位置之間賦值時的拷貝規則。

前置知識

已學習值類型和引用類型(數組、結構體、映射、字符串)。


三種存儲位置

storage —— 持久化存儲

  • 類似數據庫,數據永久保存在區塊鏈上
  • 成員變量(狀態變量)默認存儲在 storage
  • 讀寫 gas 成本最高

memory —— 臨時內存

  • 函數執行期間存在,函數返回後銷燬
  • 局部變量默認存儲在 memory
  • gas 成本適中

calldata —— 調用數據

  • 來自 transaction 的 msg.data只讀
  • 不可修改,gas 成本最低
  • 適用於 external 函數的參數

存儲位置對引用類型的影響

關鍵規則:

  • 不同 location 的同一引用類型,編譯器視爲不同類型
  • public / external 函數參數只能是 memorycalldata
  • internal / 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 不可寫入

上一篇引用類型詳解