智慧合約基礎語言(六)——Solidity變數型別:其他
智慧合約基礎語言(六):Solidity變數型別:其他
一、目錄
☞對映
☞特殊的運算子delete
☞基本類間的轉換
二、變數型別——對映
對映或字典型別,一種鍵值對的對映關係儲存結構。定義方式為mapping(_KeyType => _KeyValue)。鍵的型別允許除對映外的所有型別,如陣列,合約,列舉,結構體。值的型別無限制。
對映可以被視作為一個雜湊表,其中所有可能的鍵已被虛擬化的建立,被對映到一個預設值(二進位制表示的零)。但在對映表中,我們並不儲存鍵的資料,僅僅儲存它的keccak256雜湊值,用來查詢值時使用。
因此,對映並沒有長度,鍵集合(或列表),值集合(或列表)這樣的概念。
對映型別,僅能用來定義狀態變數,或者是在內部函式中作為storage型別的引用。引用是指你可以宣告一個,如var storage mappVal的用於儲存狀態變數的引用的物件,但你沒辦法使用非狀態變數來初始化這個引用。
可以通過將對映標記為public,來讓Solidity建立一個訪問器。要想訪問這樣的對映,需要提供一個鍵值做為引數。如果對映的值型別也是對映,使用訪問器訪問時,要提供這個對映值所對應的鍵,不斷重複這個過程。
2.1 只能是狀態變數
由於在對映中鍵的數量是任意的,導致對映的大小也是變長的。對映只能宣告為storage的狀態變數,或被賦值給一個storage的物件引用。我們來看下面的示例:
在上面的示例中,我們聲明瞭storage的狀態變數stateVar,可以對其增加新鍵值對;也能通過引用傳遞的方式賦值給storage的引用storageRef。
2.2 支援的型別
對映型別的鍵支援除對映,變長陣列,合約,列舉,結構體以外的任意型別。值則允許任意型別,甚至是對映。下面是一個簡單的例子程式碼:
2.3 setter方法
對於對映型別,也能標記為public。以讓Solidity為我們自動生成訪問器。
在上面的例子中,如果要訪問intMapp第二個元素,在一對中括號中輸入值1即intMapp[1]。而如果要訪問巢狀的對映mapMapp[2][2],則輸入兩個鍵對應的值2,2即可。
2.4 getter方法
可以通過將對映標記為public,來讓Solidity建立一個訪問器。
三、變數型別——特殊的運算子delete
Solidity中有個特殊的操作符delete用於釋放空間(特別是對於陣列結構體以及對映型別的變數),因為區塊鏈做為一種公用資源,為避免大家濫用。且鼓勵主動對空間的回收,釋放空間將會返還一些gas給呼叫者。
delete關鍵字的作用是對某個型別值a賦予初始值。比如如果刪除整數delete a等同於a = 0。
3.1 刪除基本型別
對於基本型別,使用delete會設定為對應的初始值:
刪除bool型別是false,變長位元組陣列是0x0。string則是空串。
3.2 刪除列舉
刪除列舉型別時,會將其值重置為序號為0的值。
上面的例子中,刪除light後,light將被置為序號為0的值即RED。
3.3 刪除函式
嘗試了一下刪除函式,會報錯Error: Expression has to be an lvalue.,看來不能刪除函式。
3.4 刪除結構體
刪除一個結構體,會將其中的所有成員變數一一置為初值,我們來看一個例子。
在上面的例子中,我們聲明瞭結構體s,呼叫delete s,結構體的值將變為其對應型別uint,string,bytes的初始值0,空串和0x0。
3.5 刪除對映
對映是一個特殊的存在,由於對映的鍵並不總是能有效遍歷(資料結構沒有提供介面,也並不總是需要關心所有鍵是什麼),所存的鍵的數量往往是非常大的,所以我們並不能直接刪除一個對映。
如果直接刪除一個對映會報錯Unary operator delete cannot be applied 但我們可以指定鍵來刪除對映中的某一項:
3.6 刪除結構體中的對映
如果刪除一個結構體時,其中含有對映型別,會跳過對映型別。我們來看一個刪除含對映的結構體示例:
上面的示例中,刪除結構體ms,並沒有影響其中對映ms.m的值。
3.7 刪除陣列
對於定長陣列,刪除時,是將陣列內所有元素置為初值。
而對於變長陣列時,則是將長度置為0。
3.8 刪除陣列的一個元素
我們也可以刪除陣列的一個元素,有一點違反直覺的是,刪除一個元素後,陣列會留個空隙在那裡。比如三個元素的陣列,刪除了第二個元素,只是將第二個元素置為了初始值,其它沒變。
上述的程式碼執行後,將返回1,0,3。刪除只是賦值,並沒有移動元素。
3.9 gas使用的考慮
上文中,我們瞭解到,刪除時會忽略對映,以及陣列的某個元素被刪除後,並不會自動整理陣列。這些看起來很不符合常理,其實是基於對gas限制的考慮。因為如果對映或陣列非常大的情況下,刪除或維護它們將變得非常消耗gas。
不過,清理空間,可以獲得gas的返還。但無特別意義的陣列的整理和刪除,只會消耗更多gas,需要在業務實現上進行權衡, 站在以太坊設計者的角度,因為不清理空間會浪費資源, 而大量遍歷和刪除操作又會佔用cpu以及需要很多節點同步,因此節約空間和減少cpu的消耗都會獎勵,兩者需要找到一個平衡點才不至於資源浪費消耗太多gas。
3.10 清理的最佳實踐
由於本身並未提供對對映這樣的大物件的清理,所以儲存並遍歷它們來進行清理,顯得特別消耗gas。一種實踐就是能複用就複用,一般不主動清理。下面是一個數組的插入實現,比如增加一個計數器,直接忽略已使用過的位置。
上面的例子中,我們在陣列新增時,直接忽略掉已使用過的槽位。而在程式碼內,我們使用numElements來代替array.length,以獲取當前陣列所在的位置。
如果這種大物件是在某個事件發生時,一次性使用,然後需要回收的。一個更有效的方式是,在發生某個事件時,建立一個新合約,在新合約完成邏輯,完成後,讓合約suicide。清理合約佔用空間返還的gas就退還給了呼叫者,來節省主動遍歷刪除消耗的額外gas。
3.11 刪除的注意事項
刪除本質是對一個變數賦初值。所以我們刪除storage的引用時會報錯,因為storage的引用並沒有自己已分配的儲存空間,所以不能對storage的引用直接賦初值。
上面的例子中,刪除storageRef會報錯。
四、變數型別——基本型別間的轉換
4.1 隱式轉換
如果一個運算子能支援不同型別。編譯器會隱式的嘗試將一個運算元的型別,轉為另一個運算元的型別,賦值同理。
一般來說,值型別間的互相轉換隻要不丟失資訊,語義可通則可轉換。下面,我們來看一個整數轉換的例子:
上面的例子中,我們將一個uint8的變數a隱式的轉換為了uint16。同理它還支援轉為uint32,uint128和uint256。
另外,無符號整數可以被轉為同樣,或更大的位元組的型別。但需要注意的是,不能反過來轉換。由於address是20位元組大小,所以它與int160大小是一樣。
4.2 顯式轉換
編譯器不會將語法上不可轉換的型別進行隱式轉換,此時我們要通過顯式轉換的方式,比如將一個有符號整數,轉為一個無符號整數。
4.3 型別推斷
有時為了方便,我們不會顯式定義型別。但由於編譯器,會自動挑選一個最恰當的型別,所以會常常留下坑,我們來看這個例子:
大家可以想想上述程式碼執行的結果。
上述程式碼執行的結果實際為2100。原因是因為var i = 0定義時,通過型別推斷,i的實際型別為uint8,所以它會一直迴圈,如果沒有count >= 2100這個判斷語句,這個迴圈將永遠不會結束。
4.4 一些常見的轉換方案
4.4.1 uint轉為bytes
assembly是可以用匯編的方式實現某功能。 將一個uint轉換成bytes,可以使用assembly。
上面的轉換方式可能是效率最高的方式。
4.4.2 string轉為bytes
string可以顯式的轉為bytes。但如果要轉為bytes32,可能只能使用assembly。