智能合約開發
資料位置
想像合約有三張不同的桌子可以放資料。儲存(storage)是螺絲鎖在地板上的檔案櫃:它在交易之間持續存在,而且使用成本遠遠最高。記憶體(memory)是一張便條紙,呼叫一結束就被擦得乾乾淨淨。Calldata 則是隨呼叫送達、唯讀的引數信封,你完全無法更動它。在 Solidity 裡,每一個參考型別變數(陣列、結構體、字串、bytes、映射)都必須標註其中一種位置,因為位元組存放在哪裡,會同時改變程式碼的成本與語意。
值型別(uint、bool、address)存在 EVM 堆疊上,不需要標註。參考型別則必須指明位置。儲存變數是合約的持久狀態,成本最高:一次冷 SLOAD 約 2,100 gas,而寫入一個全新的儲存槽約 20,000 gas。記憶體是按位元組定址的暫存空間,其成本隨擴展而呈二次方成長。Calldata 是最便宜的區域:它不可變且直接從交易輸入讀取,因此把大型陣列以 calldata 傳入可避免複製到記憶體。映射則永遠只能存在於儲存中。
微妙之處在於賦值語意。把儲存的值複製到記憶體變數(或反之)會產生一份獨立的複本。但宣告為 storage 的區域變數其實是指向真實狀態的指標(別名):透過它寫入就等於寫入鏈上。初學者以為那是複本,結果發現自己「暫時」的修改竟永久改變了合約狀態。對外部函數的輸入選用 calldata 而非 memory,並避免不必要的儲存到記憶體複製,是每位 Solidity 開發者最早學到的 gas 課題之一。
function f(uint[] calldata input) external {
uint[] memory temp = input; // calldata -> memory: a real copy
Item storage item = items[0]; // storage pointer (an alias!)
item.count++; // this writes to chain state
}三種位置、三種行為、差異極大的 gas 成本。
區域的 storage 變數不是複本,而是指向實際狀態的別名。寫入它就會改動鏈上資料;幾乎每位初學者都至少被它坑過一次。
另見