Solidity 教學: 繼承 Inheritance

本篇預計閱讀時間 10 分鐘, 實作時間 10 分鐘。

在開始之前先來思考底下的練習題,閱讀後即可練習完成。

練習題 

  • 練習題 1

    • 當宣告 A is X, Y, Z 時,___ 是最末端繼承合約

  • 練習題 2

    • 在 Solidity 中,宣告多重繼承時使用的是 _____ 演算法。

  • 練習題 3

    • 在繼承合約之後,可透過 ____ 來訪問繼承中最源頭的合約。

 

Solidity 中繼承有以下的特性:

  • 當進行多重繼承(Multiple Inheritance)使用 C3 Linearization 演算法

  • 具有多態性質(Polymorphism)

  • 使用 is 關鍵字來做為繼承語法:

    • 當宣告 A is X, Y, Z 時,Z 是最末端繼承合約

    • 可使用 super 來訪問(access)最源頭的合約

  • 被繼承的合約會被佈署成單一合約,不會佈署多個合約上鏈

C3 Linearization 演算法用於決定多重繼承中的順序,其中包含子合約不可以改變父合約的方法搜尋順序、局部優先原則(A 繼承 B 和 C,C 繼承 B,那麼 A 讀取父合約方法時優先使用 C)。這會強制將繼承轉換為一個有向無環圖的特定順序,同時我們在進行繼承時的宣告順序也很重要(is 後放的合約順序)。

 

Inheritance

繼承是一個能夠使用其他 Contract 的一種方法。當一個合約從多個合約繼承之後,在區塊鏈上實際佈署(創造)的還是只有一個合約。

Solidity 某種程度上是以「複製」程式碼的行為來繼承合約的。

派生(子)合約:合約在繼承父合約之後,可以訪問父合約中的所有非私有成員,包括父合約內的變數和(可視性為 public 或 internal 的)函式。

//導入 // First import the contract

import B from 'path/to/B.sol';

//繼承//Then make your contract inherit from it

contract A is B {
// 呼叫父合約的建構子//Then call the constructor of the B 

contractconstructor() B() {}
}

如果我們同時宣告一樣的函數在不同 contract:


 

contract B {
    function foo() external {...}
}
contract A is B {function foo() external {...}
}

當我們 call 了 foo() 在 A 這個 contract 裡面,則屬於 A 的 A.foo() 會被執行。

但要注意以下這種情況屬於不同的函數:

contract B {
    function foo(uint data) external {...}
}
contract A is B {
    function foo() external {...}
}

當我們呼叫了 foo(1) 在 A 這個 contract 裡面,則 B.foo() 會被執行,因為只有 B 的 foo(uint) 有宣告 uint。

所以要注意的是所謂的「一模一樣的函數」,是需要連 returns 參數都一模一樣!

Modifier

我們可以使用 modifier 來對一個函數進行補充敘述,或者在滿足 modifier 定義的特定條件後才執行函數的內容。在父合約定義 modifier 後,可以在子合約定義某些函式時進行調用。

此外,modifier 可以有參數也可以沒有參數。

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.11;

contract Owner {
    address owner;
    constructor() public {
        owner = msg.sender;
}

    modifier onlyOwner {
        require(msg.sender == owner);
        _;
}

modifier costs(uint price) {
    if (msg.value >= price) {
        _;
    }
  }
}

contract Register is Owner {
  mapping (address => bool) registeredAddresses;
  uint price;
  constructor(uint initialPrice) public { price = initialPrice; }

  function register() public payable costs(price) {
registeredAddresses[msg.sender] = true;
}
  function changePrice(uint _price) public onlyOwner {
    price = _price;
  }
}

modifier 內容最後的 _; 代表的是定義這些敘述為 modifier

 

合約互動

我們可以使用 address()contract 型態轉為 address 型態。

如果今天要使用同一個檔案裏面其他合約的函數,可以使用以下方法:

//SPDX-License-Identifier: MIT
contract A {
  function foo() view external returns(uint) {...}
}
contract B {
  function callFoo(address addrA) external {
    uint result = A(addrA).foo();
  }
}

甚至可以在一個合約裡面宣告其他合約。

//SPDX-License-Identifier: MIT
contract A {
  constructor(uint a) {...}
  function foo() external {...}
}

contract B {
  function createA(uint a) external {
    A AInstance = new A(a); // 記得傳遞建構子參數 pass constructor argument(s) if any
  }
}

Function Overriding

  • virtual: 當一個父合約的函數、modifier 或狀態變數被宣告 virtual 時,表示他在之後繼承的合約中可以被改變(覆寫)。

  • override: 當一個子合約的函數、modifier 或狀態變數被宣告 override 時,表示他正在改變(覆寫)父合約的函數。

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.11;

contract Base {
    function foo() virtual public {}
}
contract Middle is Base {}

contract Inherited is Middle {
    function foo() public override {}
}

Function Overriding 有以下規定和重點:

  • 在多重繼承的時候,最根本的合約需要特別宣告 override 在函數中。

  • 如果這個函數的可視性為 private 則不能是 virtual

  • 在介面中所有函數都會自動被視為 virtual,也就是說在可視性允許的情況下,Solidity 預計未來這些函式將會被實作函式 body(某種程度上的 override)。

  • 可以同時 override 兩個父合約的函式,範例如下:

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.11;
contract Base1 {
    function foo() virtual public {}
}

contract Base2 {function foo() virtual public {}
}

contract Inherited is Base1, Base2 {
// Derives from multiple bases defining foo(),
// so we must explicitly override it
function foo() public override(Base1, Base2) {}}

Polymorphism

還記得 Solidity 教學: 函式 Function 時提到的 Function Overloading 以及上述 Function Overriding 嗎?

其實 Polymorphism 在繼承中最主要的兩種形式 Function Polymorphism 和 Contract Polymorphism 便分別指稱了以上兩項內容。

Function Polymorphism 指的是在同一個合約中,宣告相同的函式名稱,藉由不同的參數內容來決定我們呼叫之後,程式碼最終使用的是哪段函式內容,也就是 Function Overloading。

Contract Polymorphism 指的是在不同合約中,宣告相同的函式名稱,藉由繼承之間的定義來決定我們呼叫之後,程式碼最終使用的是哪段函式內容,也就是 FunctionOverriding。

多重繼承與 super

當一個合約從多個合約繼承時,在區塊鏈上只會有一個合約被建立,所有父合約的程式碼都被編譯到我們建立的子合約中。

在多重繼承的情況下 super 這個關鍵字在 Solidity 中可以調用最初的父合約。使用 super 的函數調用優先於大多數派生合約。

當我們擁有一個 contract A 並在其中有一個函數 f(),並且它的父合約之中也有一個函數 f()。此時 A 會覆寫(overrides)B 的 f()。這代表 myInstanceOfA.f() 會呼叫 A 之中的 f(),且 B 之中的 f() 將不可再被調用,但如果我們想要調用合約 B 的 f() 則可以再 A 之中使用 super.f()

或者我們也可以顯式地(explicitly)宣告父合約的函數。

以下是多重繼承的例子:

首先建立一個 c 合約,這個合約定義了一個變數 u。然後 b 合約去繼承 c 合約。這裡就不要定義變量了,使用的時 c 合約的變數。然後 a 合約繼承了 b 合約。

 

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.11;

contract C {
  uint public u;
  function f() public virtual {
    u = 1;
  }
}

contract B is C {
  function f() public virtual override{
    u = 2;
  }
}

contract A is B {
  function f() public override{  // will set u to 3
    u = 3;
  }

  function f1() public {
    // will set u to 2
    super.f();
}

  function f2() public {
    // will set u to 2
    B.f();
    // 使用 super 去呼叫的話,是呼叫 b 合約的,而不是 c 合約的。}
  function f3() public {
    // will set u to 1
    C.f();
    // 如果要呼叫 c 合約中的函式,需要使用函式名。
  }
}

需要注意的是父合約的 State Variables 不可以在子合約被更改,同時也不可以在子合約宣告在父合約們中已經宣告同樣名稱的 State Variables。

 

練習題解答

  • 練習題解答 1

    • 當宣告 A is X, Y, Z 時,Z 是最末端繼承合約

  • 練習題解答 2

    • 在 Solidity 中,宣告多重繼承時使用的是C3線性演算法。

  • 練習題解答 3

    • 在繼承合約之後,可透過 super 來訪問繼承中最源頭的合約。