智慧合約基礎語言(五):Solidity變數型別:引用型別
1
目錄
☛資料位置
☛陣列
☛結構體
2
引用型別——資料位置
不同於之前值型別,引用型別佔的空間更大,超過256位元組,因為拷貝它們佔用更多的空間。由此我們需要考慮將它們儲存在什麼位置?記憶體(memory,資料不是永久存在的)或儲存(storage,資料永久的儲存在資料塊上)
2.1 資料位置分類
▪memory
▪storage
▪calldata
▪stack
2.1.1 memory
儲存位置同我們普通程式的記憶體類似。即分配,即使用,越過作用域即不可被訪問,等待被回收。
2.1.2 storage
資料將永遠存在於區塊鏈上。
2.1.3 calldata
一般只有外部函式的引數(不包括返回引數)被強制指定為calldata。這種資料位置是隻讀的,不會持久化到區塊鏈。
2.1.4 語法格式
1. 棧實際是記憶體中的一個數據結構,每個棧元素佔256位,棧的最大深度位1024;
2. 值型別的區域性變數儲存在棧中;
3. 在棧中儲存一個很小的區域性變數,gas開銷最小,幾乎免費使用,但是數量有限。
基於程式的上下文,大多數時候資料位置的選擇是預設的,我們可以通過在變數名前宣告memory還是storage來定義該變數的資料位置。
2.2 資料位置預設規則
▪ 函式引數、函式返回引數預設為memory
▪ 區域性變數(作用域為區域性)以及狀態變數(作用域為全域性)預設storage型別。
2.3 強制的資料位置
▪ 外部函式(External function)的引數(不包括返回引數)強制為:calldata。
▪ 狀態變數(State variables)強制為: storage。
▪ 值型別的區域性變數是儲存在棧上。
2.4 不同資料位置變數間相互賦值
2.4.1 storage
當我們把一個storage型別的變數賦值給另一個storage時,只是修改了它的指標(引用傳遞)。
在上面的程式碼中,我們將傳入的storage變數,賦值給另一個臨時的storage型別的tmp時,並修改tmp.a = "Test",最後我們發現合約的狀態變數s也被修改了。
2.4.2 memory給storage賦值
因為區域性變數和狀態變數的型別都可能是storage。所以我們要分開來說這兩種情況。
☞ 2.4.2.1 memory賦值給狀態變數
將一個memory型別的變數賦值給一個狀態變數時,實際是將記憶體變數拷貝到儲存中。
通過上例,我們發現,在memoryToState()中,我們把tmp賦值給s後,再修改tmp值,並不能產生任何變化。賦值時,完成了值拷貝,後續他們不再有任何的關係。
☞ 2.4.2.2 memory賦值給狀態變數
由於在區塊鏈中,storage必須是靜態分配儲存空間的。區域性變數雖然是一個storage的,但它僅僅是一個storage型別的指標。如果進行這樣的賦值,實際會產生一個錯誤。
通過上面的程式碼,我們可以看到這樣的賦值的確不被允許。你可以通過將變數tmp改為memory來完成這樣的賦值。
2.4.3 storage轉為memory
將storage轉為memory,實際是將資料從storage拷貝到memory中。
在上面的例子中,我們看到,拷貝後對tmp變數的修改,完全不會影響到原來的storage變數。
2.4.4 memory轉為memory
memory之間是引用傳遞,並不會拷貝資料。我們來看看下面的程式碼。
在上面的程式碼中,memoryToMemory()傳遞進來了一個memory型別的變數,在函式內將之賦值給tmp,修改tmp的值,發現外部的memory也被改為了other memory。
注意:
1. 對於值型別,總是會進行拷貝
2. 不能將memory的函式引數賦值給storage區域性變數
3. 不能通過引用銷燬storage
2.5 例項
2.6 不同儲存的消耗(gas消耗)
▪ storage 會永久儲存合約狀態變數,開銷最大. 大概5000~20000。
▪ memory 僅儲存臨時變數,函式呼叫之後釋放,開銷很小。
▪ calldata 和memory差不多。
▪ stack 儲存很小的區域性變數,幾乎免費使用,但有數量限制 具體gas消耗值請參考http://yellowpaper.io/。
3
引用型別——陣列
陣列在所有的語言當中都是一種常見型別。在Solidity中,可以支援編譯期定長陣列和變長陣列。一個型別為T,長度為k的陣列,可以宣告為T[k],而一個變長的陣列則宣告為T[]。
3.1 使用字面量建立陣列
建立陣列時,我們可以使用字面量,隱式建立一個定長陣列。
通過上面的程式碼,我們可以發現。
首先元素型別是剛好能儲存的元素的型別,比如程式碼裡的[1, 2, 3],只需要uint8即可儲存。但由於我們宣告的變數是uint(預設的uint表示的其實是uint256),所以要使用uint(1)來進行顯式的型別轉換。
其次,字面量方式宣告的陣列是定長的,且實際長度要與宣告的相匹配,否則編譯器會報錯
Type string memory[1] memory is not implicitly convertible to expected type string memory[2] memory。
3.2 使用new關鍵字建立陣列
對於變長陣列,在初始化分配空間前不可使用,可以通過new關鍵字來初始化一個數組。
我們聲明瞭一個storage的stateVar,和一個memory的memVar。它們不能在使用new關鍵字初始化前使用下標方式訪問,會報錯VM Exception: invalid opcode。可以根據情況使用如例子中的new uint;來進行初始化。
3.3 陣列屬性
3.3.1 length屬性
陣列有一個length屬性,表示當前的陣列長度。對於storage的變長陣列,可以通過給length賦值調整陣列長度。
在上面這個例子中,我們可以看到,通過stateVar.length++語句對陣列長度進行自增,我們就得到了一個不斷變長的陣列。 還可以使用後面提到的push()方法,來隱式的調整陣列長度。
3.4 陣列函式
變長的storage陣列和bytes(不包括string)有一個push()函式。可以將一個新元素附加到陣列末端,返回值為當前長度。push函式支援陣列的初始化。
3.5 memory陣列
對於memory的變長陣列,不支援修改length屬性,來調整陣列大小。memory的變長陣列雖然可以通過引數靈活指定大小,但一旦建立,大小不可調整。
如果狀態變數的型別為陣列,也可以標記為public型別,從而讓Solidity建立一個訪問器。(public型別的狀態變數都有預設的訪問器)訪問器對於外界使用者表現為一個函式, 因此可以通過呼叫函式的方式訪問某些值。 另外在remix中表現為一個可以點選的按鈕, 可以獲取對應的public型別的值。
如上面的合約在Remix執行後,需要我們填入的是一個要訪問序號的數字,來訪問具體某個元素。
3.6 多維陣列
我們要建立一個長度為5的陣列,每個元素又是一個變長uint陣列,將被宣告為uint[][5]。 反之, 假如要建立一個變長陣列, 每個元素又是一個長度是5的陣列, 將被宣告為uint[5][],比如下邊的例子:
在上面的程式碼中,我們聲明瞭一個二維陣列,它是一個變長的陣列,裡面的每個元素是一個長度為2的陣列。要訪問這個陣列flags,第一個下標為變長陣列的序號,第二個下標為長度為2的陣列序號。
3.7 bytes與string
bytes和string是一種特殊的陣列。
由於bytes與string,可以自由轉換,你可以將字串s通過bytes(s)轉為一個bytes。可以以這種方式獲得字串長度,以及獲取字元中字元的UTF-8編碼。
4
引用型別——結構體
結構體,Solidity中的自定義型別。我們可以使用Solidity的關鍵字struct來進行自定義。結構體內可以包含字串,整型等基本資料型別,以及陣列,對映,結構體等複雜型別。陣列,對映,結構體也支援自定義的結構體。我們來看一個自定義結構體的定義:
在上面的程式碼中,我們定義了一個簡單的結構體Student,它包含一些基本的資料型別。另外我們還定義了一個稍微複雜一點的結構體Class,它包含了其它結構體Student,以及陣列,對映等型別。
陣列型別的students和對映型別的index的宣告中還使用了結構體。
4.1 結構體定義的限制
我們不能在結構中定義一個自己作為型別,這樣限制原因是,自定義型別的大小不允許是無限的。我們來看看下述的程式碼:
在上面的程式碼中,我們嘗試在A型別中定義一個A a;,將會報錯Error: Recursive struct definition.。雖然如此,但我們仍然能在型別內用陣列,對映來引用當前定義的型別,如變數mappingMemberOfOwn,arrayMemberOfOwn所示。
4.2 初始化
4.2.1 直接初始化
如果我們宣告的自定義型別為A,我們可以使用A(變數1,變數2, ...)的方式來完成初始化。來看下面的程式碼:
上面的程式碼中,我們按定義依次填入值,即可完成了初始化。需要注意的是,引數要與定義的數量匹配。當你填的引數與預計初始化的引數不一致時,會提示Error: Wrong argument count for function call: 2 arguments given but expected 3. Members that have to be skipped in memory: map。另外,在初始化時,需要忽略對映型別,後面有具體說明。
4.2.2 命名初始化
還可以使用類似JavaScript的命名引數初始化的方式,通過傳入引數名和對應值的物件。這樣做的好處在於可以不按定義的順序傳入值。我們來看看下面的例子:
上面的例子中,通過在引數物件中,指定鍵為對應的引數名,值為你想要初化的值,我們即完成了初始化。同樣需要注意的是,引數要與定義的個數一致,否則會報類似這樣的錯誤Error: Wrong argument count for function call: 2 arguments given but expected 3. Members that have to be skipped in memory: map。另外,在初始化時,需要忽略對映型別,後面有具體說明。
4.2.3 結構體中對映的初始化
由於對映是一種特殊的資料結構,所以你可能只能在storage變數中使用它。
上面的例子中,我們定義的了一個storage的狀態變數storageVar,完成了對映型別的儲存空間分配。然後我們就能對對映型別賦值了。
如果你嘗試對memory的對映型別賦值,會報錯Error: Member "map" is not available in struct StructMappingInitial.A memory outside of storage.。
4.3 結構體的可見性
關於可見性,當前只支援internal的,後續不排除放開這個限制。
4.3.1 繼承中使用
結構體由於是不對外可見的,所以你只可以在當前合約,或合約的子類中使用。包含自定義結構體的函式均需要宣告為internal的。
在上面的程式碼中,我們聲明瞭f(S s),由於它包含了struct的S,所以不對外可見,需要標記為internal。你可以在當前類中使用它,如f1()所示,你還可以在子類中使用函式和結構體,如B合約的g()方法所示。
4.3.2 跨合約的臨時解決方案
結構體,由於是動態內容。當前不支援在多個合約間互用,目前一種臨時的方案如下:
在上面的例子中,我們手動將要返回的結構體拆解為基本型別進行了返回。
5
記憶體變數的佈局
Solidity預留了3個32位元組大小的槽位(儲存空間):
▪ 0-64:雜湊方法的暫存空間(scratch space)
▪ 64-96:當前已分配記憶體大小(也稱空閒記憶體指標(free memory pointer))
注: 暫存空間是2個32位元組大小,作為一個整體單元。
暫存空間可在語句之間使用(如在內聯編譯時使用)
Solidity總是在空閒記憶體指標所在位置建立一個新物件,且對應的記憶體永遠不會被釋放(也許未來會改變這種做法)。
有一些在Solidity中的操作需要超過64位元組的臨時空間,這樣就會超過預留的暫存空間。他們就將會分配到空閒記憶體指標所在的地方,但由於他們自身的特點,生命週期相對較短,且指標本身不能更新,記憶體也許會,也許不會被清零(zerod out)。因此,大家不應該認為空閒的記憶體一定已經是清零(zeroed out)的。
6
狀態變數的儲存模型
大小固定的變數(除了對映,變長陣列以外的所有型別)在儲存(storage)中是依次連續從位置0開始排列的。如果多個變數佔用的大小少於32位元組,會盡可能的打包到單個storage槽位裡,具體規則如下:
▪ 在storage槽中第一項是按低位對齊儲存(lower-order aligned) 。
▪ 基本型別儲存時僅佔用其實際需要的位元組。
▪ 如果基本型別不能放入某個槽位餘下的空間,它將被放入下一個槽位。
▪ 結構體和陣列總是使用一個全新的槽位,並佔用整個槽(但在結構體內或陣列內的每個項仍遵從上述規則)。
-END-
鏈塊學院區塊鏈系列網課已上線
學習路徑已為您規劃好
離區塊鏈工程師就差這一張圖的距離了~
本文完,獲取更多資訊,敬請關注區塊鏈工程師。
來源:鏈塊學院
本文由布洛克專欄作者釋出,代表作者觀點,版權歸作者所有,不代表布洛克科技觀點
——TheEnd——
關注“布洛克科技”