Cover photo

继承

继承是面向对象编程最重要的组成部分,可以显著的减少重复代码。如果把合约当做对象的话,solidity也是面向对象的编程,也支持继承。

  • virtual:父合约中的函数,如果希望子合约重写,需要加上virtual关键字。

  • override:子合约重写父合约中的函数,需要加上override关键字再。

注意:用override修饰public变量,会重写与变量同名的getter函数,例如:

    mapping(address => uint256) public override balanceOf;

我们先简单写一个爷爷合约Yeye,里面包含一个Log事件和三个函数,hip()pop()yeye(),输出都是Yeye

    contract Yeye {

    event Log(string msg);

    //定义3个函数:hip(),pop(),man(),Log值为Yeye
    function hip() public virtual {
        emit Log("Yeye");
    }

    function pop() public virtual{
        emit Log("Yeye");
    }

    function yeye() public virtual {
        emit Log("Yeye");
    }

}

我们在定义一个爸爸合约Baba,让它继承Yeye合约,语法就是contract Baba is Yeye,非常直观。在Baba合约里,我们重写一下hip()pop()这两个函数,加上override关键字,并将它们输出改为Baba,并且加一个新函数baba(),输出也是Baba

    contract Baba is Yeye{
    // 继承两个function: hip()和pop(),输出改为Baba。
    function hip() public virtual override{
        emit Log("Baba");
    }

    function pop() public virtual override{
        emit Log("Baba");
    }

    function baba() public virtual{
        emit Log("Baba");
    }
}

我们部署合约,可以看到Baba合约有4个函数,其中hip()pop()的输出被成功改写成Baba,而继承来的yeye()的输出仍然是Yeye

solidity的合约可以继承多个合约,规则

  1. 继承时按备份最高到最低的顺序排。比如我们写一个Erzi合约,继承Yeye合约和Baba合约,那么就写成contract Erzi is Yeye, Baba,而不是写成contract Erzi is Baba, Yeye,不然会报错。

  2. 如果某一个函数在多个继承的合约里存在,比如合约里的hip()pop(),在子合约里必须重写,不然会报错。

  3. 重写在多个父合约都重名的函数时,override关键字后面要加上所有父合约名字,例如`override(Yeye, Baba)。

    contract Erzi is Yeye, Baba{
    // 继承两个function: hip()和pop(),输出改为Erzi。
    function hip() public virtual override(Yeye, Baba){
        emit Log("Erzi");
    }

    function pop() public virtual override(Yeye, Baba) {
        emit Log("Erzi");
    }

    function callParent() public{
        Yeye.pop();
    }

    function callParentSuper() public{
        super.pop();
    }
}

我们可以看到,Erzi合约里面重写了hip()pop()两个函数,并且还分别从YeyeBaba合约继承了yeye()baba()两个函数。

solidity中的修饰器(modifier)同样可以继承,用法与函数类似,相应的地方加上overridevirtual关键字即可。

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

    contract Base1 {
        modifier exactDividedBy2And3(uint _a) virtual {
            require(_a % 2 == 0 && _a % 3 == 0);
            _;
        }
    }

    contract Identifier is Base1 {

        //计算一个数分别被2除和被3除的值,但是传入的参数必须是2和3的倍数
        function getExactDividedBy2And3(uint _dividend) public exactDividedBy2And3(_dividend) pure returns(uint, uint) {
            return getExactDividedBy2And3WithoutModifier(_dividend);
        }

        //计算一个数分别被2除和被3除的值
        function getExactDividedBy2And3WithoutModifier(uint _dividend) public pure returns(uint, uint){
            uint div2 = _dividend / 2;
            uint div3 = _dividend / 3;
            return (div2, div3);
        }

        //重写Modifier: 不重写时,输入9调用getExactDividedBy2And3,会revert,因为无法通过检查
        //删掉下面三行注释重写Modifier,这时候输入9调用getExactDividedBy2And3, 会调用成功
        // modifier exactDividedBy2And3(uint _a) override {
        //     _;
        // }
    }

Identifier合约可以直接在代码中使用父合约中的exactDividedBy2And3修改器,也可以利用override关键字重写修改器。

    modifier exactDividedBy2And3(uint _a) override {
        _;
        require(_a % 2 == 0 && _a % 3 == 0);
    }

子合约有两种方法继承父合约的构造函数。举个简单的例子,父合约A里面有一个状态变量a,并有构造函数的参数来确定:

    abstract contract A {
    uint public a;

    constructor(uint _a) {
        a = _a;
    }
}
  1. 在继承时声明父构造函数参数,例如:contract B is A(1)

  2. 在子合约的构造函数中声明构造函数的参数。

    contract B is A(1) {
    }

    contract C is A {
        constructor(uint _c) A(_c * _c) {}
    }

子合约有两种方法调用父合约的函数,直接调用和利用super关键字。

  1. 直接调用:子合约可以直接用父合约.函数名的方式来调用父合约函数,例如Yeye.pop()

    function callParent() public{
        Yeye.pop();
    }
  1. super关键字:子合约可以利用super.函数名()来调用最近的父合约函数。solidity继承关系声明时从右到左的顺序是contract Erzi is Yeye, Baba,那么Baba是最近的父合约,super.pop()将调用Baba.pop()而不是Yeye.pop()

    function callParentSuper() public{
        // 将调用最近的父合约函数,Baba.pop()
        super.pop();
    }

在面向对象编程中,钻石继承(菱形继承)值一个派生类同时又两个或两个以上的基类。

在多重加菱形继承链条上使用关键字super时,需要注意的是使用super会调用继承链条上的每一个合约的相关函数,而不是只调用最近的父合约。

我们先写一个God合约,再写两个AdamEve两个合约继承God合约,最后让创建合约people继承AdamEve,每个合约都有foobar两个函数。

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

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

        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");
            God.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()会依次调用EveAdm,最后是God合约。