iT邦幫忙

2024 iThome 鐵人賽

DAY 11
0
自我挑戰組

Solidity 初學之路系列 第 11

DAY 11 - 繼承

  • 分享至 

  • xImage
  •  

繼承

繼承可以減少重複的程式碼,可以把合約看作物件,Solidity 也是支持繼承的物件導向程式語言。Solidity 的繼承包括簡單繼承、多重繼承、修飾器繼承和建構子的繼承。

關鍵字

virtualoveride 是繼承的關鍵字。

  • virtual:父合約中的函數,如果希望子合約重寫,則需要加上 virtual 關鍵字。
  • overide:子合約重寫了父合約中的函數,需要加上 override 關鍵字。

簡單繼承

合約之間簡單繼承的語法:contract <子合約名稱> is <父合約名稱>,我們可以在子合約中重寫函數,只要加上 override 關鍵字即可。

contract Grandpa{
    event Log(string msg);
    function f1() public virtual{
        emit Log("Grandpa");
    }
    function f2() public virtual{
        emit Log("Grandpa");
    }
    function grandpa() public virtual{
        emit Log("Grandpa");
    }
}
contract Father is Grandpa{
    function f1() public virtual override{
        emit Log("Father");
    }
    function f2() public virtual override{
        emit Log("Father");
    }

    function father() public virtual{
        emit Log("Father");
    }
}


分別部署父合約 Grandpa、子合約 Father


部署 Father 子合約後,可以發現 Father 子合約中也有 Grandpa 父合約的函數,也繼承了未重寫的 grandpa() 函數。

若父合約 Grandpa 調用 f1() 函數,可以在 log 中找到日誌對應 Grandpa.f1() 的 log 輸出,若子合約 Father 調用 f1() 函數,因為重寫了 f1() 所以日誌輸出和從 Grandpa 父合約調用是不同的 log 輸出結果。

多重繼承

合約可以繼承多個合約。

規則

  1. 繼承時要依輩分最高到最低的順序排列。如:Son 合約,繼承 Grandpa 合約和 Father 合約,那麼就要寫成 contract Son is Grandpa, Father,而不能寫成 contract Son is Father, Grandpa,不然就會報錯。
  2. 如果某個函數在多重繼承的合約裡都存在,例如例子中的 f1f2,在子合約裡必須重寫,不然編譯器無法判斷應該繼承哪個函數會報錯。
  3. 重寫在多個父合約中都重名的函數時,override 關鍵字後面要加上所有父合約名字,例如 override(Grandpa, Father)

contract Son is Grandpa, Father{
    function f1() public virtual override(Grandpa, Father){
        emit Log("Son");
    }

    function f2() public virtual override(Grandpa, Father) {
        emit Log("Son");
    }
}


Son 合約裡面重寫了 f1()f2() 函數,將輸出改為 "Son",並分別從 Grandpa 和 Father 合約繼承了 grandpa()father() 兩個函數。

修飾器繼承

修飾器繼承的用法與函數繼承類似,在對應的地方加上 virtualoverride 關鍵字即可。

  1. 子合約可以直接在程式碼中使用父合約中的修飾器。
contract Base{
    modifier exactDividedBy2And3(uint _a) virtual {
        require(_a % 2 == 0 && _a % 3 == 0);
        _;
    }
}
contract Identifier is Base {
    function getExactDividedBy2And3(uint _dividend) public exactExactDividedBy2And3(_dividend) pure returns(uint, uint) {
        return getExactDividedBy2And3WithoutModifier(_dividend);
    }
    function getExactDividedBy2And3WithoutModifier(uint _dividend) public pure returns(uint, uint){
        uint div2 = _dividend / 2;
        uint div3 = _dividend / 3;
        return (div2, div3);
    }
}


部署 Identifier合約,getExactDividedBy2And3(uint _divided)參數輸入 15,交易會被 revert,因為 Identifier 合約繼承了 Base 合約的修飾器 exactDividedBy2And3(_dividend),檢查到 15 不能被 2 整除,所以 getExactDividedBy2And3() 函數未成功執行。


getExactDividedBy2And3WithoutModifier() 函數沒有裝飾器限制參數必須被 2 或 3 整除,所以可以看到回傳的兩個參數分別是 7 和 5。

  1. 子合約利用 override 關鍵字重寫父合約修飾器。
modifier exactDividedBy2And3(uint _a) override {
    _;
    require(_a % 2 == 0 && _a % 3 == 0);
}

建構子的繼承

abstract contract A{
    uint public a;
    constructor(uint _a){
        a = _a;
    }
}
  1. 在繼承時宣告父建構子的參數,例如:contract B is A(1)
  2. 在子合約的建構子中宣告建構子的參數。
contract C is A {
    constructor(uint _c) A(_c * _c){}
}


選擇 C 合約部署,給定建構子參數為 10。

因為繼承了 A 合約,所以 A 合約的建構子參數被指定為 10 * 10,將 A 合約的狀態變數 a 修改為 100。

呼叫父合約的函數

  1. 直接呼叫:子合約可以直接用 <父合約名>.<函數名>() 呼叫父合約函數。
function callParent() public {
    Grandpa.f2();
}
  1. 利用 super 關鍵字:子合約可以利用 super.<函數名稱>() 來呼叫最近的父合約函數。Solidity 繼承關係依宣告時從右到左的順序是:contract Son is Grandpa, Father,Father 是最近的父合約,super.f2() 將呼叫 Father.f2() 而不是Grandpa.f2()
function callParentSuper() public{
    super.f2();
}


新增 callParent()callParentSuper()Son 合約並重新部署 Son 合約。

可以觀察到子合約函數可以用合約名稱呼叫繼承的父合約的函數。

利用 super 呼叫的是最近的父合約函數。

鑽石繼承(菱形繼承)

在物件導向程式設計中,鑽石繼承(菱形繼承)指一個衍生類別同時有兩個或兩個以上的基底類別。在多重+菱形繼承鏈上使用 super 關鍵字時,需要注意的是使用 super 會呼叫繼承鏈上的每一個合約的相關函數,而不是只呼叫最近的父合約。
我們先寫一個合約 God,再寫 AdamEve 兩個合約繼承 God 合約,最後讓創建合約 people 繼承自 AdamEve,每個合約都有 foobar 兩個函數。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
contract God {
    event Log(string message);
    function foo() public virtual {
        emit Log("God.foo called");
    }
    function bar() public virtual {
        emit Log("God.bar called");
    }
}
contract Adam is God {
    function foo() public virtual override {
        emit Log("Adam.foo called");
        super.foo();
    }
    function bar() public virtual override {
        emit Log("Adam.bar called");
        super.bar();
    }
}
contract Eve is God {
    function foo() public virtual override {
        emit Log("Eve.foo called");
        super.foo();
    }
    function bar() public virtual override {
        emit Log("Eve.bar called");
        super.bar();
    }
}
contract people is Adam, Eve {
    function foo() public override(Adam, Eve) {
        super.foo();
    }
    function bar() public override(Adam, Eve) {
        super.bar();
    }
}

在這個範例中,呼叫合約 people 中的 super.bar() 會依序呼叫 Eve、Adam,最後是 God 合約。雖然 Eve、Adam 都是 God 的子合約,但整個過程中 God 合約只會被呼叫一次。原因是 Solidity 借鑒了 Python 的方式,強制一個由基類構成的 DAG(有向無環圖)使其保證一個特定的順序。


上一篇
DAY 10 - 事件
下一篇
DAY 12 - 抽象合約、介面
系列文
Solidity 初學之路30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言