1.1 storage, memory, calldata, stack區分編程
在 Solidity 中,有兩個地方能夠存儲變量 :存儲(storage)以及內存(memory)。Storage變量是指永久存儲在區塊鏈中的變量。Memory 變量則是臨時的,當外部函數對某合約調用完成時,內存型變量即被移除。數組
內存(memory)位置還包含2種類型的存儲數據位置,一種是calldata,一種是棧(stack)。安全
(1) calldata數據結構
這是一塊只讀的,且不會永久存儲的位置,用來存儲函數參數。 外部函數的參數(非返回參數)的數據位置被強制指定爲 calldata ,效果跟 memory 差很少。app
(2) 棧(stack)編程語言
另外,EVM是一個基於棧的語言,棧實際是在內存(memory)的一個數據結構,每一個棧元素佔爲256位,棧最大長度爲1024。 值類型的局部變量是存儲在棧上。函數
不一樣存儲的消耗(gas消耗)是不同的,說明以下:學習
storage 會永久保存合約狀態變量,開銷最大;區塊鏈
memory 僅保存臨時變量,函數調用以後釋放,開銷很小;優化
stack 保存很小的局部變量,無償使用,但有數量限制(16個變量);
calldata的數據包含消息體的數據,其計算須要增長n*68的GAS費用;
storage 存儲結構是在合約建立的時候就肯定好了的,它取決於合約所聲明狀態變量。可是內容能夠被(交易)調用改變。
Solidity 稱這個爲狀態改變,這也是合約級變量稱爲狀態變量的緣由。也能夠更好的理解爲何狀態變量都是storage存儲。
memory 只能用於函數內部,memory 聲明用來告知EVM在運行時建立一塊(固定大小)內存區域給變量使用。
storage 在區塊鏈中是用key/value的形式存儲,而memory則表現爲字節數組
1.2 棧(stack)的延伸閱讀
EVM是一個基於棧的虛擬機。這就意味着對於大多數操做都使用棧,而不是寄存器。基於棧的機器每每比較簡單,且易於優化,但其缺點就是比起基於寄存器的機器所須要的opcode更多。
因此EVM有許多特有的操做,大多數都只在棧上使用。好比SWAP和DUP系列操做等,具體請參見EVM文檔。如今咱們試着編譯以下合約:
pragma solidity ^0.4.13; contract Something{ function foo(address a1, address a2, address a3, address a4, address a5, address a6){ address a7; address a8; address a9; address a10; address a11; address a12; address a13; address a14; address a15; address a16; address a17; } }
你將看到以下錯誤:
CompilerError: Stack too deep, try removing local variables.
這個錯誤是由於當棧深超過16時發生了溢出。官方的「解決方案」是建議開發者減小變量的使用,並使函數儘可能小。固然還有其餘幾種變通方法,好比把變量封裝到struct或數組中,或是採用關鍵字memory(不知道出於何種緣由,沒法用於普通變量)。既然如此,讓咱們試一試這個採用struct的解決方案:
pragma solidity ^0.4.13; contract Something{ struct meh{ address x; } function foo(address a1, address a2, address a3, address a4, address a5, address a6){ address a7; address a8; address a9; address a10; address a11; address a12; address a13; meh memory a14; meh memory a15; meh memory a16; meh memory a17; } }
結果呢?
CompilerError: Stack too deep, try removing local variables.
咱們明明採用了memory關鍵字,爲何仍是有問題呢?關鍵在於,雖然此次咱們沒有在棧上存放17個256bit整數,但咱們試圖存放13個整數和4個256bit內存地址。
這當中包含一些Solidity自己的問題,但主要問題仍是EVM沒法對棧進行隨機訪問。據我所知,其餘一些虛擬機每每採用如下兩種方法之一來解決這個問題:
鼓勵使用較小的棧深,但能夠很方便地實現棧元素和內存或其餘存儲(好比.NET中的本地變量)的交換;
實現pick或相似的指令用於實現對棧元素的隨機訪問;
然而,在EVM中,棧是惟一免費的存放數據的區域,其餘區域都須要支付gas。所以,這至關於鼓勵儘可能使用棧,由於其餘區域都要收費。正由於如此,咱們纔會遇到上文所述的基本的語言實現問題。
Solidity 類型分爲兩類: 值類型(Value Type) 及 引用類型(Reference Types)。 Solidity 提供了幾種基本類型,能夠用來組合出複雜類型。
(1)值類型(Value Type) 是指 變量在賦值或傳參時老是進行值拷貝,包含:
布爾類型(Booleans)
整型(Integers)
定長浮點型(Fixed Point Numbers)
定長字節數組(Fixed-size byte arrays)
有理數和整型常量(Rational and Integer Literals)
字符串常量(String literals)
十六進制常量(Hexadecimal literals)
枚舉(Enums)
函數(Function Types)
地址(Address)
地址常量(Address Literals)
(2)引用類型(Reference Types)
是指賦值時咱們能夠值傳遞也能夠引用即地址傳遞,包括:
不定長字節數組(bytes)
字符串(string)
數組(Array)
結構體(Struts)
引用類型是一個複雜類型,佔用的空間一般超過256位, 拷貝時開銷很大。
全部的複雜類型,即 數組 和 結構 類型,都有一個額外屬性:「數據位置」,說明數據是保存在內存(memory ,數據不是永久存在)中仍是存儲(storage,永久存儲在區塊鏈中)中。 根據上下文不一樣,大多數時候數據有默認的位置,但也能夠經過在類型名後增長關鍵字( storage )或 (memory) 進行修改。
變量默認存儲位置:
函數參數(包含返回的參數)默認是memory;
局部變量(local variables)默認是storage;
狀態變量(state variables)默認是storage;
局部變量:局部做用域(越過做用域即不可被訪問,等待被回收)的變量,如函數內的變量。 狀態變量:合約內聲明的公共變量
數據位置指定很是重要,由於他們影響着賦值行爲。
在memory和storage之間或與狀態變量之間相互賦值,老是會建立一個徹底獨立的拷貝。
而將一個storage的狀態變量,賦值給一個storage的局部變量,是經過引用傳遞。因此對於局部變量的修改,同時修改關聯的狀態變量。
另外一方面,將一個memory的引用類型賦值給另外一個memory的引用,不會建立拷貝(即:memory之間是引用傳遞)。
注意: 不能將memory賦值給局部變量。 對於值類型,老是會進行拷貝。
下面引用一段合約代碼做說明:
pragma solidity ^0.4.0; contract C { uint[] x; // x 的數據存儲位置是 storage // memoryArray 的數據存儲位置是 memory function f(uint[] memoryArray) public { x = memoryArray; // 將整個數組拷貝到 storage 中,可行 var y = x; // 分配一個指針(其中 y 的數據存儲位置是 storage),可行 y[7]; // 返回第 8 個元素,可行 y.length = 2; // 經過 y 修改 x,可行 delete x; // 清除數組,同時修改 y,可行 // 下面的就不可行了;須要在 storage 中建立新的未命名的臨時數組, / // 但 storage 是「靜態」分配的: // y = memoryArray; // 下面這一行也不可行,由於這會「重置」指針, // 但並無可讓它指向的合適的存儲位置。 // delete y; g(x); // 調用 g 函數,同時移交對 x 的引用 h(x); // 調用 h 函數,同時在 memory 中建立一個獨立的臨時拷貝 } function g(uint[] storage storageArray) internal {} function h(uint[] memoryArray) public {}
3.1 定位固定大小的值
在這個存模型中,到底是怎麼樣存儲的呢?對於具備固定大小的已知變量,在內存中給予它們保留空間是合理的。Solidity編程語言就是這樣作的。
contract StorageTest { uint256 a; uint256[2] b; struct Entry { uint256 id; uint256 value; } Entry c; }
在上面的代碼中:
a存儲在下標0處。(solidity表示內存中存儲位置的術語是「下標(slot)」。)
b存儲在下標1和2(數組的每一個元素一個)。
c從插槽3開始並消耗兩個插槽,由於該結構體Entry存儲兩個32字節的值。
這些下標位置是在編譯時肯定的,嚴格基於變量出如今合同代碼中的順序。
3.2 查找動態大小的值
使用保留下標的方法適用於存儲固定大小的狀態變量,但不適用於動態數組和映射(mapping),由於沒法知道須要保留多少個槽。
若是您想將計算機RAM或硬盤驅動器做爲比喻,您可能會但願有一個「分配」步驟來查找可用空間,而後執行「釋放」步驟,將該空間放回可用存儲池中。
可是這是沒必要要的,由於智能合約存儲是一個天文數字級別的規模。存儲器中有2^256個位置可供選擇,大約是已知可觀察宇宙中的原子數。您能夠隨意選擇存儲位置,而不會遇到碰撞。您選擇的位置相隔太遠以致於您能夠在每一個位置存儲儘量多的數據,而無需進入下一個位置。
固然,隨機選擇地點不會頗有幫助,由於您沒法再次查找數據。Solidity改成使用散列函數來統一併可重複計算動態大小值的位置。
3.3 動態大小的數組
動態數組須要一個地方來存儲它的大小以及它的元素。
contract StorageTest { uint256 a; // slot 0 uint256[2] b; // slots 1-2 struct Entry { uint256 id; uint256 value; } Entry c; // slots 3-4 Entry[] d; }
在上面的代碼中,動態大小的數組d存在下標5的位置,可是存儲的惟一數據是數組的大小。數組d中的值從下標的散列值hash(5)開始連續存儲。
下面的Solidity函數計算動態數組元素的位置:
function arrLocation(uint256 slot, uint256 index, uint256 elementSize) public pure returns (uint256) { return uint256(keccak256(slot)) + (index * elementSize); }
3.4 映射(Mappings)
一個映射mapping須要有效的方法來找到與給定的鍵相對應的位置。計算鍵的哈希值是一個好的開始,但必須注意確保不一樣的mappings產生不一樣的位置。
contract StorageTest { uint256 a; // slot 0 uint256[2] b; // slots 1-2 struct Entry { uint256 id; uint256 value; } Entry c; // slots 3-4 Entry[] d; // slot 5 for length, keccak256(5)+ for data mapping(uint256 => uint256) e; mapping(uint256 => uint256) f; }
在上面的代碼中,e的「位置」 是下標6,f的位置是下標7,但實際上沒有任何內容存儲在這些位置。(不知道多長鬚要存儲,而且獨立的值須要位於其餘地方。)
要在映射中查找特定值的位置,鍵和映射存儲的下標會一塊兒進行哈希運算。
如下Solidity函數計算值的位置:
function mapLocation(uint256 slot, uint256 key) public pure returns (uint256) { return uint256(keccak256(key, slot)); }
請注意,當keccak256函數有多個參數時,在哈希運算以前先將這些參數鏈接在一塊兒。因爲下標和鍵都是哈希函數的輸入,所以不一樣mappings之間不會發生衝突。
3.5 複雜類型的組合
動態大小的數組和mappings能夠遞歸地嵌套在一塊兒。當發生這種狀況時,經過遞歸地應用上面定義的計算來找到值的位置。這聽起來比它更復雜。
contract StorageTest { uint256 a; // slot 0 uint256[2] b; // slots 1-2 struct Entry { uint256 id; uint256 value; } Entry c; // slots 3-4 Entry[] d; // slot 5 for length, keccak256(5)+ for data mapping(uint256 => uint256) e; // slot 6, data at h(k . 6) mapping(uint256 => uint256) f; // slot 7, data at h(k . 7) mapping(uint256 => uint256[]) g; // slot 8 mapping(uint256 => uint256)[] h; // slot 9 }
要找到這些複雜類型中的項目,咱們可使用上面定義的函數。要找到g123:
// first find arr = g[123] arrLoc = mapLocation(8, 123); // g is at slot 8 // then find arr[0] itemLoc = arrLocation(arrLoc, 0, 1);
要找到h2:
// first find map = h[2] mapLoc = arrLocation(9, 2, 1); // h is at slot 9 // then find map[456] itemLoc = mapLocation(mapLoc, 456);
3.6 總結
每一個智能合約都以2^256個32字節值的數組形式存儲,所有初始化爲零。
零沒有明確存儲,所以將值設置爲零會回收該存儲。
Solidity中,肯定佔內存大小的值從第0號下標開始放。
Solidity利用存儲的稀疏性和散列輸出的均勻分佈來安全地定位動態大小的值。
下表顯示瞭如何計算不一樣類型的存儲位置。「下標」是指在編譯時遇到狀態變量時的下一個可用下標,而點表示二進制串聯:
本文做者:HiBlock區塊鏈社區技術佈道者輝哥
原文發佈於簡書
如下是咱們的社區介紹,歡迎各類合做、交流、學習:)