智慧合約編寫之Solidity的基礎特性

買賣虛擬貨幣
如前篇介紹,目前大部分的聯盟鏈平臺,包括FISCO BCOS,都採用Solidity作為智慧合約開發語言,因此熟悉並上手Solidity十分必要。作為一門面向區塊鏈平臺設計的圖靈完備的程式語言,Solidity支援函式呼叫、修飾符、過載、事件、繼承等多種特性,在區塊鏈社羣中,擁有廣泛的影響力和踴躍的社羣支援。但對於剛接觸區塊鏈的人而言,Solidity是一門陌生的語言。智慧合約編寫階段將從Solidity基礎特性、高階特性、設計模式以及程式設計攻略分別展開,帶讀者認識Solidity並掌握其運用,更好地進行智慧合約開發。本篇將圍繞Solidity的基礎特性,帶大家上手開發一個最基本的智慧合約。智慧合約程式碼結構任何程式語言都有其規範的程式碼結構,用於表達在一個程式碼檔案中如何組織和編寫程式碼,Solidity也一樣。
本節,我們將透過一個簡單的合約示例,來了解智慧合約的程式碼結構。pragma solidity ^0.4.25;contract Sample{    //State variables    address private _admin;    uint private _state;
    //Modifier    modifier onlyAdmin(){        require(msg.sender == _admin, "You are not admin");              _;    }    //Events
    event SetState(uint value);    //Constructor    constructor() public{        _admin = msg.sender;    }    //Functions
    function 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)。

在logs中,我們看到SetState事件被丟擲,裡面的引數也記錄了事件傳入的值4。

如果我們換一個賬戶來執行,那麼呼叫會失敗,因為onlyAdmin修飾符會阻止使用者呼叫。

getState

呼叫getState後,可以直接看到所得到的值為4,正好是我們先前setState所傳入的值:

Solidity資料型別

在前文的示例中,我們用到了uint等資料型別。由於Solidity型別設計比較特殊,這裡也會簡單介紹一下Solidity的資料型別。

整型系列

Solidity提供了一組資料型別來表示整數, 包含無符號整數與有符號整數。每類整數還可根據長度細分,具體細分型別如下。

 定長bytes系列

Solidity提供了bytes1到bytes32的型別,它們是固定長度的位元組陣列。

使用者可以讀取定長bytes的內容。

    function bytesSample() public{

        bytes32 barray;
        //Initialize baarray
        //read brray[0]
        byte b = barray[0];
    }

並且,可以將整數型別轉換為bytes。

        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]);
    }

變長bytes

從上文中,讀者可瞭解定長byte陣列。此外,Solidity還提供了一個變長byte陣列:bytes。使用方式類似陣列,後文會有介紹。

string

Solidity提供的string,本質是一串經UTF-8編碼的位元組陣列,它相容於變長bytes型別。

目前Solidity對string的支援不佳,也沒有字元的概念。使用者可以將string轉成bytes。

    function stringSample() public view returns(bytes){
        string memory str = "abc";
        bytes memory b = bytes(str);
        //0x616263
        return b;
    }

要注意的是,當將string轉換成bytes時,資料內容本身不會被複製,如上文中,str和b變數指向的都是同一個字串abc。

address 

address表示賬戶地址,它由私鑰間接生成,是一個20位元組的資料。同樣,它也可以被轉換為bytes20。

    function addressSample() public view returns(bytes20){

        address me = msg.sender;
        bytes20 b = bytes20(me);
        return b;
    }

mapping

mapping表示對映, 是極其重要的資料結構。它與java中的對映存在如下幾點差別:

它無法迭代keys,因為它只儲存鍵的雜湊,而不儲存鍵值,如果想迭代,可以用開源的可迭代雜湊類庫

如果一個key未被儲存在mapping中,一樣可以正常讀取到對應value,只是value是空值(位元組全為0)。所以它也不需要put、get等操作,使用者直接去操作它即可。

contract Sample{

    mapping(uint=>string) private values;

    function mappingSample() public view returns(bytes20){
        //put a key value pair
        values[10] = "hello";

        //read value
        string value = values[10];

    }

}

陣列

如果陣列是狀態變數,那麼支援push等操作:

contract Sample{

    string[] private arr;

    function arraySample() public view {
        arr.push("Hello");
        uint len = arr.length;//should be 1
        string value = arr[0];//should be Hello
      }

}

陣列也可以以區域性變數的方式使用,但稍有不同:

function arraySample() public view returns(uint){
        //create an empty array of length 2
        uint[] memory p = new uint[](2);
        p[3] = 1;//THIS WILL THROW EXCEPTION 
        return p.length;
}

struct

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開發智慧合約的基本知識。讀者可以嘗試執行該合約,感受智慧合約的開發。

若想更深入學習智慧合約示例,推薦官方網站示例供讀者學習,也可關注本專題後續系列文章:

https://solidity.readthedocs.io/en/v0.6.2/solidity-by-example.html。

在官網的示例中,提供了投票、競拍、微支付通道等多個案例,這些案例貼近實際生活,是很好的學習資料。

免責聲明:

  1. 本文版權歸原作者所有,僅代表作者本人觀點,不代表鏈報觀點或立場。
  2. 如發現文章、圖片等侵權行爲,侵權責任將由作者本人承擔。
  3. 鏈報僅提供相關項目信息,不構成任何投資建議

推荐阅读

;