1. 程式人生 > >區塊鏈100講:Solidity語法的合約/抽象合約/介面/庫的定義

區塊鏈100講:Solidity語法的合約/抽象合約/介面/庫的定義

image

以太坊智慧合約語言Solitidy是一種面向物件的語言,本文清楚合約定義,以及派生的抽象合約,介面,庫的定義。

1

合約定義(Contract)

Solidity 合約類似於面嚮物件語言中的類。合約中有用於資料持久化的狀態變數,和可以修改狀態變數的函式。 呼叫另一個合約例項的函式時,會執行一個 EVM 函式呼叫,這個操作會切換執行時的上下文,這樣,前一個合約的狀態變數就不能訪問了。

1.1 建立合約

可以通過以太坊交易“從外部”或從 Solidity 合約內部建立合約。

一些整合開發環境,例如 Remix, 通過使用一些使用者介面元素使建立過程更加流暢。 在以太坊上程式設計建立合約最好使用 JavaScript API web3.js。 現在,我們已經有了一個叫做 web3.eth.Contract 的方法能夠更容易的建立合約。

建立合約時,會執行一次建構函式(與合約同名的函式)。建構函式是可選的。只允許有一個建構函式,這意味著不支援過載。

在內部,建構函式引數在合約程式碼之後通過 ABI 編碼 傳遞,但是如果你使用 web3.js 則不必關心這個問題。

如果一個合約想要建立另一個合約,那麼建立者必須知曉被建立合約的原始碼(和二進位制程式碼)。 這意味著不可能迴圈建立依賴項。

pragma solidity ^0.4.16;

contract OwnedToken {

    // TokenCreator 是如下定義的合約型別.

    // 不建立新合約的話,也可以引用它。

    TokenCreator creator;

    address owner;

    bytes32 name;

    // 這是註冊 creator 和設定名稱的建構函式。

    function OwnedToken(bytes32 _name) public {

        // 狀態變數通過其名稱訪問,而不是通過例如 this.owner 的方式訪問。

        // 這也適用於函式,特別是在建構函式中,你只能像這樣(“內部地”)呼叫它們,

        // 因為合約本身還不存在。

        owner = msg.sender; 

       // 從 `address` 到 `TokenCreator` ,是做顯式的型別轉換

        // 並且假定呼叫合約的型別是 TokenCreator,沒有真正的方法來檢查這一點。

        creator = TokenCreator(msg.sender);

        name = _name;

    }

    function changeName(bytes32 newName) public {

        // 只有 creator (即建立當前合約的合約)能夠更改名稱 —— 因為合約是隱式轉換為地址的,

        // 所以這裡的比較是可行的。

        if (msg.sender == address(creator))

            name = newName;

    }

    function transfer(address newOwner) public {

        // 只有當前所有者才能傳送 token。

        if (msg.sender != owner) return;

        // 我們也想詢問 creator 是否可以傳送。

        // 請注意,這裡呼叫了一個下面定義的合約中的函式。

        // 如果呼叫失敗(比如,由於 gas 不足),會立即停止執行。

        if (creator.isTokenTransferOK(owner, newOwner))

            owner = newOwner;

    }

}

contract TokenCreator {

    function createToken(bytes32 name)

       public

       returns (OwnedToken tokenAddress)

    {

        // 建立一個新的 Token 合約並且返回它的地址。

        // 從 JavaScript 方面來說,返回型別是簡單的 `address` 型別,因為

        // 這是在 ABI 中可用的最接近的型別。

        return new OwnedToken(name);

    }

    function changeName(OwnedToken tokenAddress, bytes32 name)  public {

        // 同樣,`tokenAddress` 的外部型別也是 `address` 。

        tokenAddress.changeName(name);

    }

    function isTokenTransferOK(address currentOwner, address newOwner)

        public

        view

        returns (bool ok)

    {

        // 檢查一些任意的情況。

        address tokenAddress = msg.sender;

        return (keccak256(newOwner) & 0xff) == (bytes20(tokenAddress) & 0xff);

    }

}

2

抽象合約(Abstract Contract)

合約函式可以缺少實現,如下例所示(請注意函式宣告頭由 ; 結尾):

pragma solidity ^0.4.0;

contract Feline {

    function utterance() public returns (bytes32);
}

這些合約無法成功編譯(即使它們除了未實現的函式還包含其他已經實現了的函式),但他們可以用作基類合約:

pragma solidity ^0.4.0;

contract Feline {

    function utterance() public returns (bytes32);

}

contract Cat is Feline {

    function utterance() public returns (bytes32) { return "miaow"; }

}

如果合約繼承自抽象合約,並且沒有通過重寫來實現所有未實現的函式,那麼它本身就是抽象的。

3

介面(Interface)

介面類似於抽象合約,但是它們不能實現任何函式。還有進一步的限制:

  • 無法繼承其他合約或介面。

  • 無法定義建構函式。

  • 無法定義變數。

  • 無法定義結構體

  • 無法定義列舉。
    將來可能會解除這裡的某些限制。

介面基本上僅限於合約 ABI 可以表示的內容,並且 ABI 和介面之間的轉換應該不會丟失任何資訊。

介面由它們自己的關鍵字表示:

pragma solidity ^0.4.11;

interface Token {

    function transfer(address recipient, uint amount) public;
}

4

庫(Libary)

庫與合約類似,它們只需要在特定的地址部署一次,並且它們的程式碼可以通過 EVM 的 DELEGATECALL (Homestead 之前使用 CALLCODE 關鍵字)特性進行重用。 這意味著如果庫函式被呼叫,它的程式碼在呼叫合約的上下文中執行,即 this 指向呼叫合約,特別是可以訪問呼叫合約的儲存。 因為每個庫都是一段獨立的程式碼,所以它僅能訪問呼叫合約明確提供的狀態變數(否則它就無法通過名字訪問這些變數)。 因為我們假定庫是無狀態的,所以如果它們不修改狀態(也就是說,如果它們是 view 或者 pure 函式), 庫函式僅可以通過直接呼叫來使用(即不使用 DELEGATECALL 關鍵字), 特別是,除非能規避 Solidity 的型別系統,否則是不可能銷燬任何庫的。

庫可以看作是使用他們的合約的隱式的基類合約。雖然它們在繼承關係中不會顯式可見,但呼叫庫函式與呼叫顯式的基類合約十分類似 (如果 L 是庫的話,可以使用 L.f() 呼叫庫函式)。此外,就像庫是基類合約一樣,對所有使用庫的合約,庫的 internal 函式都是可見的。 當然,需要使用內部呼叫約定來呼叫內部函式,這意味著所有內部型別,記憶體型別都是通過引用而不是複製來傳遞。 為了在 EVM 中實現這些,內部庫函式的程式碼和從其中呼叫的所有函式都在編譯階段被拉取到呼叫合約中,然後使用一個 JUMP 呼叫來代替 DELEGATECALL。

下面的示例說明如何使用庫(但也請務必看看 using for-https://solidity-cn.readthedocs.io/zh/develop/contracts.html?highlight=view#using-for 有一個實現 set 更好的例子)。

 library Set {

  // 我們定義了一個新的結構體資料型別,用於在呼叫合約中儲存資料。

  struct Data { mapping(uint => bool) flags; }

  // 注意第一個引數是“storage reference”型別,因此在呼叫中引數傳遞的只是它的儲存地址而不是內容。

  // 這是庫函式的一個特性。如果該函式可以被視為物件的方法,則習慣稱第一個引數為 `self` 。

  function insert(Data storage self, uint value)

      public

      returns (bool)

  {

      if (self.flags[value])

          return false; // 已經存在

      self.flags[value] = true;

      return true;

  }

  function remove(Data storage self, uint value)

      public

      returns (bool)

  {

      if (!self.flags[value])

          return false; // 不存在

      self.flags[value] = false;

      return true;

  }

  function contains(Data storage self, uint value)

      public

      view

      returns (bool)

  {

      return self.flags[value];

  }

}

contract C {

    Set.Data knownValues;

    function register(uint value) public {

        // 不需要庫的特定例項就可以呼叫庫函式,

        // 因為當前合約就是“instance”。

        require(Set.insert(knownValues, value));

    }

    // 如果我們願意,我們也可以在這個合約中直接訪問 knownValues.flags。

}

當然,你不必按照這種方式去使用庫:它們也可以在不定義結構資料型別的情況下使用。 函式也不需要任何儲存引用引數,庫可以出現在任何位置並且可以有多個儲存引用引數。

呼叫 Set.contains,Set.insert 和 Set.remove 都被編譯為外部呼叫( DELEGATECALL )。 如果使用庫,請注意實際執行的是外部函式呼叫。 msg.sender, msg.value 和 this 在呼叫中將保留它們的值, (在 Homestead 之前,因為使用了 CALLCODE,改變了 msg.sender 和 msg.value)。

以下示例展示瞭如何在庫中使用記憶體型別和內部函式來實現自定義型別,而無需支付外部函式呼叫的開銷:

 library BigInt {

    struct bigint {

        uint[] limbs;

    }

    function fromUint(uint x) internal pure returns (bigint r) {

        r.limbs = new uint[](1);

        r.limbs[0] = x;

    }

    function add(bigint _a, bigint _b) internal pure returns (bigint r) {

        r.limbs = new uint[](max(_a.limbs.length, _b.limbs.length));

        uint carry = 0;

        for (uint i = 0; i < r.limbs.length; ++i) {

            uint a = limb(_a, i);

            uint b = limb(_b, i); 

           r.limbs[i] = a + b + carry;

            if (a + b < a || (a + b == uint(-1) && carry > 0))

                carry = 1;

            else

                carry = 0;

        }

        if (carry > 0) {

            // 太差了,我們需要增加一個 limb

            uint[] memory newLimbs = new uint[](r.limbs.length + 1);

            for (i = 0; i < r.limbs.length; ++i)

                newLimbs[i] = r.limbs[i];

            newLimbs[i] = carry;

            r.limbs = newLimbs;

        }

    }

    function limb(bigint _a, uint _limb) internal pure returns (uint) {

        return _limb < _a.limbs.length ? _a.limbs[_limb] : 0;

    }

    function max(uint a, uint b) private pure returns (uint) {

        return a > b ? a : b;

    }

}

contract C {

    using BigInt for BigInt.bigint;

    function f() public pure {

        var x = BigInt.fromUint(7);

        var y = BigInt.fromUint(uint(-1));

        var z = x.add(y);

    }

}

由於編譯器無法知道庫的部署位置,我們需要通過連結器將這些地址填入最終的位元組碼中 (請參閱 使用命令列編譯器-https://solidity-cn.readthedocs.io/zh/develop/using-the-compiler.html#commandline-compiler 以瞭解如何使用命令列編譯器來連結位元組碼)。 如果這些地址沒有作為引數傳遞給編譯器,編譯後的十六進位制程式碼將包含 Set____ 形式的佔位符(其中 Set 是庫的名稱)。 可以手動填寫地址來將那 40 個字元替換為庫合約地址的十六進位制編碼。

與合約相比,庫的限制:

  • 沒有狀態變數

  • 不能夠繼承或被繼承

  • 不能接收以太幣

(將來有可能會解除這些限制)

4.1 庫的呼叫保護

如果庫的程式碼是通過 CALL 來執行,而不是 DELEGATECALL 或者 CALLCODE 那麼執行的結果會被回退, 除非是對 view 或者 pure 函式的呼叫。

EVM 沒有為合約提供檢測是否使用 CALL 的直接方式,但是合約可以使用 ADDRESS 操作碼找出正在執行的“位置”。 生成的程式碼通過比較這個地址和構造時的地址來確定呼叫模式。

更具體地說,庫的執行時程式碼總是從一個 push 指令開始,它在編譯時是 20 位元組的零。當部署程式碼執行時,這個常數 被記憶體中的當前地址替換,修改後的程式碼儲存在合約中。在執行時,這導致部署時地址是第一個被 push 到堆疊上的常數, 對於任何 non-view 和 non-pure 函式,排程器程式碼都將對比當前地址與這個常數是否一致。

4.2 Using For

指令 using A for B; 可用於附加庫函式(從庫 A)到任何型別(B)。 這些函式將接收到呼叫它們的物件作為它們的第一個引數(像 Python 的 self 變數)。

using A for *; 的效果是,庫 A 中的函式被附加在任意的型別上。

在這兩種情況下,所有函式都會被附加一個引數,即使它們的第一個引數型別與物件的型別不匹配。 函式呼叫和過載解析時才會做型別檢查。

using A for B; 指令僅在當前作用域有效,目前僅限於在當前合約中,後續可能提升到全域性範圍。 通過引入一個模組,不需要再新增程式碼就可以使用包括庫函式在內的資料型別。

讓我們用這種方式將 庫 中的 set 例子重寫:

// 這是和之前一樣的程式碼,只是沒有註釋。

library Set {

  struct Data { mapping(uint => bool) flags; }

  function insert(Data storage self, uint value)

      public

      returns (bool)

  {

      if (self.flags[value])

        return false; // 已經存在

      self.flags[value] = true;

      return true;

  }

  function remove(Data storage self, uint value)

      public

      returns (bool)

  {

      if (!self.flags[value])

          return false; // 不存在

      self.flags[value] = false;

      return true;

  }

  function contains(Data storage self, uint value)

      public

      view

      returns (bool)

  {

      return self.flags[value];

  }

}

contract C {

    using Set for Set.Data; // 這裡是關鍵的修改

    Set.Data knownValues;

    function register(uint value) public {

        // Here, all variables of type Set.Data have

        // corresponding member functions.

        // The following function call is identical to

        // `Set.insert(knownValues, value)`

        // 這裡, Set.Data 型別的所有變數都有與之相對應的成員函式。

        // 下面的函式呼叫和 `Set.insert(knownValues, value)` 的效果完全相同。

        require(knownValues.insert(value));

    }
}

也可以像這樣擴充套件基本型別:

library Search {

    function indexOf(uint[] storage self, uint value)

        public

        view

        returns (uint)

    {

        for (uint i = 0; i < self.length; i++)

            if (self[i] == value) return i;

        return uint(-1);

    }

}

contract C {

    using Search for uint[];

    uint[] data;

    function append(uint value) public {

        data.push(value);

    }

    function replace(uint _old, uint _new) public {

        // 執行庫函式呼叫

        uint index = data.indexOf(_old);

        if (index == uint(-1)) 

           data.push(_new);

        else

            data[index] = _new;

    }

}

注意,所有庫呼叫都是實際的 EVM 函式呼叫。這意味著如果傳遞記憶體或值型別,都將產生一個副本,即使是 self 變數。 使用儲存引用變數是唯一不會發生拷貝的情況。

本文作者:HiBlock區塊鏈社群技術佈道者輝哥

原文釋出於簡書

以下是我們的社群介紹,歡迎各種合作、交流、學習:)

image