智能合約編寫之Solidity的基礎特性
如前篇介紹,目前大部分的聯盟鏈平臺,包括 FISCO BCOS,都采用 Solidity 作為智能合約開發語言,因此熟悉并上手 Solidity 十分必要。
作為一門面向區塊鏈平臺設計的圖靈完備的編程語言,Solidity 支持函數調用、修飾符、重載、事件、繼承等多種特性,在區塊鏈社區中,擁有廣泛的影響力和踴躍的社區支持。但對于剛接觸區塊鏈的人而言,Solidity 是一門陌生的語言。
智能合約編寫階段將從 Solidity 基礎特性、高級特性、設計模式以及編程攻略分別展開,帶讀者認識 Solidity 并掌握其運用,更好地進行智能合約開發。
本篇將圍繞 Solidity 的基礎特性,帶大家上手開發一個最基本的智能合約。
智能合約代碼結構
任何編程語言都有其規范的代碼結構,用于表達在一個代碼文件中如何組織和編寫代碼,Solidity 也一樣。
本節,我們將通過一個簡單的合約示例,來了解智能合約的代碼結構。
pragma solidity ^0.4.25;contract Sample{//State variablesaddress private _admin;uint private _state;//Modifiermodifier onlyAdmin(){require(msg.sender == _admin, "You are not admin");_;}//Eventsevent SetState(uint value);//Constructorconstructor() public{_admin = msg.sender;}//Functionsfunction setState(uint value) public onlyAdmin{_state = value;emit SetState(value);}function getValue() public view returns (uint){return _state;}}
上面這段程序包括了以下功能:
-
通過構造函數來部署合約
-
通過
setValue函數設置合約狀態 -
通過
getValue函數查詢合約狀態
整個合約主要分為以下幾個構成部分:
-
狀態變量 -
_admin、_state,這些變量會被永久保存,也可以被函數修改 -
構造函數 - 用于部署并初始化合約
-
事件 -
SetState, 功能類似日志,記錄了一個事件的發生 -
修飾符 -
onlyAdmin, 用于給函數加一層“外衣” -
函數 -
setState、getState,用于讀寫狀態變量
下面將逐一介紹上述構成部分。
狀態變量
狀態變量是合約的骨髓,它記錄了合約的業務信息。用戶可以通過函數來修改這些狀態變量,這些修改也會被包含到交易中;交易經過區塊鏈網絡確認后,修改即為生效。
uint private _state;
狀態變量的聲明方式為:[類型] [訪問修飾符-可選] [字段名]。
構造函數
構造函數用于初始化合約,它允許用戶傳入一些基本的數據,寫入到狀態變量中。
在上述例子中,設置了 _admin 字段,作為后面演示其他功能的前提。
constructor() public{_admin = msg.sender;}
和 Java 不同的是,構造函數不支持重載,只能指定一個構造函數。
函數
函數被用來讀寫狀態變量。對變量的修改將會被包含在交易中,經區塊鏈網絡確認后才生效。生效后,修改會被永久的保存在區塊鏈賬本中。
函數簽名定義了函數名、輸入輸出參數、訪問修飾符、自定義修飾符。
function setState(uint value) public onlyAdmin;
函數還可以返回多個返回值:
function functionSample() public view returns(uint, uint){return (1,2);}
在本合約中,還有一個配備了 view 修飾符的函數。這個 view 表示了該函數不會修改任何狀態變量。
與 view 類似的還有修飾符 pure,其表明該函數是純函數,連狀態變量都不用讀,函數的運行僅僅依賴于參數。
function add(uint a, uint b) public pure returns(uint){return a+b;}
如果在 view 函數中嘗試修改狀態變量,或者在 pure 函數中訪問狀態變量,編譯器均會報錯。
事件
事件類似于日志,會被記錄到區塊鏈中,客戶端可以通過 web3 訂閱這些事件。
定義事件:
event SetState(uint value);
構造事件:
emit SetState(value);
這里有幾點需要注意:
-
事件的名稱可以任意指定,不一定要和函數名掛鉤,但推薦兩者掛鉤,以便清晰地表達發生的事情。
-
構造事件時,也可不寫
emit,但因為事件和函數無論是名稱還是參數都高度相關,這樣操作很容易筆誤將事件寫成函數調用,因此不推薦不寫。function setState(uint value) public onlyAdmin{_state = value;emit SetState(value);// 下面這樣寫也可以,但不推薦,因為很容易筆誤寫成 setState// SetState(value);}
-
Solidity 編程風格應采用一定的規范。關于編程風格,建議參考:https://learnblockchain.cn/docs/solidity/style-guide.html#id16
修飾符
修飾符是合約中非常重要的一環。它掛在函數聲明上,為函數提供一些額外的功能,例如檢查、清理等工作。
在本例中,修飾符 onlyAdmin 要求函數調用前,需要先檢測函數的調用者是否為函數部署時設定的那個管理員(即合約的部署人)。
//Modifermodifier onlyAdmin(){require(msg.sender == _admin, "You are not admin");_;}...//Functionsfunction setState(uint value) public onlyAdmin{...}
值得注意的是,定義在修飾符中的下劃線 “_”,表示函數的調用,指代的是開發者用修飾符修飾的函數。在本例中,表達的是 setState 函數調用的意思。
智能合約的運行
了解了上述的智能合約示例的結構,就可以直接上手運行,運行合約的方式有多種,大家可以任意采取其中一種:
-
方法一:可以使用 FISCO BCOS 控制臺的方式來部署合約,具體請參考:https://fisco-bcos-documentation.readthedocs.io/zh_CN/latest/docs/installation.html#id7
-
方法二:使用 FISCO BCOS 開源項目 WeBASE 提供的在線 ide WEBASE-front 運行
-
方法三:通過在線 ide remix 來進行合約的部署與運行,remix 的地址為:http://remix.ethereum.org/
本例中使用 remix 作為運行示例。
編譯
首先,在 remix 的在線 ide 中鍵入代碼后,通過編譯按鈕來編譯。成功后會在按鈕上出現一個綠色對勾:
部署
編譯成功后就可進行部署環節,部署成功后會出現合約實例。
setState
合約部署后,我們來調用 setState(4)。在執行成功后,會產生一條交易收據,里面包含了交易的執行信息。
在這里,用戶可以看到交易執行狀態(status)、交易執行人(from)、交易輸入輸出(decoded input、decoded output)、交易開銷(execution cost)以及交易日志(logs)。
在交易日志中,我們看到 SetState 事件被拋出,里面的參數也記錄了事件傳入的值 4。
如果我們換一個賬戶來執行,那么調用會失敗,因為 onlyAdmin 修飾符會阻止用戶調用。
getState
調用 getState 后,可以直接看到所得到的值為 4,正好是我們先前 setState 所傳入的值:
Solidity 數據類型
在前文的示例中,我們用到了 uint 等數據類型。由于 Solidity 類型設計比較特殊,這里也會簡單介紹一下 Solidity 的數據類型。
整型系列
Solidity 提供了一組數據類型來表示整數, 包含無符號整數與有符號整數。每類整數還可根據長度細分,具體細分類型如下。
|
類型 |
長度(位) |
有符號 |
|
uint |
256 |
否 |
|
uint8 |
8 |
否 |
|
uint16 |
16 |
否 |
|
... |
... |
否 |
|
uint256 |
256 |
否 |
|
int |
256 |
是 |
|
int8 |
8 |
是 |
|
int16 |
16 |
是 |
|
... |
... |
是 |
|
int256 |
256 |
是 |
定長字節系列
Solidity 提供了 bytes1 到 bytes32 的類型,它們是固定長度的字節數組。
用戶可以讀取定長字節的內容。
function bytesSample() public{bytes32 barray;//Initialize baarray//read brray[0]byte b = barray[0];}
并且,可以將整數類型轉換為字節。
uint256 s = 1;bytes32 b = bytes32(s);
這里有一個關鍵細節,Solidity 采取大端序編碼,高地址存的是整數的小端。例如,b[0] 是低地址端,它存整數的高端,所以值為 0;取 b[31] 才是 1。
function bytesSample() public pure returns(byte, byte){uint256 value = 1;bytes32 b = bytes32(value);//Should be (0, 1)return (b[0], b[31]);}
變長字節
從上文中,讀者可了解定長字節數組。此外,Solidity 還提供了一個變長字節數組:bytes。使用方式類似數組,后文會有介紹。
字符串
Solidity 提供的字符串,本質是一串經 UTF-8 編碼的字節數組,它兼容于變長字節類型。
目前 Solidity 對字符串的支持不佳,也沒有字符的概念。用戶可以將字符串轉成字節。
function stringSample() public view returns(bytes){string memory str = "abc";bytes memory b = bytes(str);//0x616263return b;}
要注意的是,當將 string 轉換成 bytes 時,數據內容本身不會被拷貝,如上文中,str 和 b 變量指向的都是同一個字符串 "abc"。
地址類型
address 表示賬戶地址,它由私鑰間接生成,是一個 20 字節的數據。同樣,它也可以被轉換為 bytes20。
function addressSample() public view returns(bytes20){address me = msg.sender;bytes20 b = bytes20(me);return b;}
映射
mapping 表示映射,是極其重要的數據結構。它與 Java 中的映射存在如下幾點差別:
-
它無法迭代鍵名,因為它只保存鍵的哈希,而不保存鍵值,如果想迭代,可以用開源的可迭代哈希類庫
-
如果一個鍵名未被保存在映射中,一樣可以正常讀取到對應的鍵值,只是值是空值(字節全為
0)。所以它也不需要put、get等操作,用戶直接去操作它即可。
contract Sample{mapping(uint=>string) private values;function mappingSample() public view returns(bytes20){//put a key value pairvalues[10] = "hello";//read valuestring value = values[10];}}
數組
如果數組是狀態變量,那么支持 push 等操作:
contract Sample{string[] private arr;function arraySample() public view {arr.push("Hello");uint len = arr.length;//should be 1string value = arr[0];//should be Hello}}
數組也可以以局部變量的方式使用,但稍有不同:
function arraySample() public view returns(uint){//create an empty array of length 2uint[] memory p = new uint[](2);p[3] = 1;//THIS WILL THROW EXCEPTIONreturn p.length;}
結構
Solidity 允許開發者自定義結構對象。結構體既可以作為狀態變量存儲,也可以在函數中作為局部變量存在。
struct Person{uint age;string name;}Person private _person;function structExample() {Person memory p = Person(1, "alice");_person = p;}
本節中只介紹了比較常見的數據類型,更完整的列表可參考 Solidity 官方網站:https://solidity.readthedocs.io/en/v0.6.3/types.html
全局變量
示例合約代碼的構造函數中,包含 msg.sender。它屬于全局變量。在智能合約中,全局變量或全局方法可用于獲取和當前區塊、交易相關的一些基本信息,如塊高、塊時間、合約調用者等。
比較常用的全局變量是 msg 變量,表示調用上下文,常見的全局變量有以下幾種:
-
msg.sender:合約的直接調用者。由于是直接調用者,所以當處于“用戶 A->合約 1->合約 2”調用鏈下,若在合約 2內使用msg.sender,得到的會是合約 1 的地址。如果想獲取用戶 A,可以用tx.origin。 -
tx.origin:交易的"始作俑者",整個調用鏈的起點。 -
msg.calldata:包含完整的調用信息,包括函數標識、參數等。calldata的前 4 字節就是函數標識,與msg.sig相同。 -
msg.sig:msg.calldata的前 4 字節,用于標識函數。 -
block.number:表示當前所在的區塊高度。 -
now:表示當前的時間戳。也可以用block.timestamp表示。
這里只列出了部分常見全局變量,完整版本請參考:https://solidity.readthedocs.io/en/v0.4.24/units-and-global-variables.html 。
結語
本文以一個簡單的示例合約作為引入,介紹了運用 Solidity 開發智能合約的基本知識。讀者可以嘗試運行該合約,感受智能合約的開發。
































