Skip to content

Latest commit

 

History

History
1294 lines (1019 loc) · 35.2 KB

10.inheritance.md

File metadata and controls

1294 lines (1019 loc) · 35.2 KB

10.合约继承

实现继承的方式是通过复制包括多态的代码到子类来实现的。合约继承通过关键字 is 来实现。由于 Solidity 继承的实现方案是代码拷贝,所以合约继承后,部署到网络时,将变成一个合约,代码将从父类拷贝到子类中。

  • 修饰符可以继承
  • 事件不可以继承,但是可以重载
  • fallback 可以继承,但是需要保持原有的 payable/nonpayable
  • receive 可以继承,但是需要保持原有的 payable/nonpayable

1️⃣ 使用 is 实现继承

当一个合约从多个合约继承时,在区块链上只有一个合约被创建,所有基类合约(或称为父合约)的代码被编译到创建的合约中。这意味着对基类合约函数的所有内部调用也只是使用内部函数调用(super.f(..)将使用 JUMP 跳转而不是消息调用)。

  • 继承: 派生合约继承基础合约的属性和方法
  • 基础合约通常也被称为父合约,派生合约通常也称作子合约
  • 下面是: "男人"继承"人"的演示。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Person {
    string internal name;
    uint256 age; // 状态变量默认是internal权限

    event Log(string funName);

    modifier onlyOwner() virtual {
        age = 1;
        _;
    }

    fallback() external payable virtual {
        emit Log("fallback by Person");
    }

    receive() external payable virtual {
        emit Log("receive by Person");
    }
}

contract Man is Person {
    constructor() {
        name = "Anbang";
        age = 18;
    }

    event Log(string funName, address _ads);

    modifier onlyOwner() override {
        age = 99;
        _;
    }

    function getName() external view returns (string memory) {
        return name;
    }

    function getAge() external view returns (uint256) {
        return age;
    }

    function getAge2() external onlyOwner returns (uint256) {
        return age;
    }

    // fallback 和 receive 继承的时候,必须保证 payable/nonpayable 状态不变。
    // Overriding function changes state mutability from "payable" to "nonpayable".
    // fallback() external override {
    //     emit Log("fallback by man");
    // }

    fallback() external payable override {
        emit Log("fallback by man");
    }

    receive() external payable override {
        emit Log("receive by Man", msg.sender);
    }
}
  • 父合约必须写在子合约的前面,
    • 否则会报错: TypeError: Definition of base has to precede definition of derived contract

2️⃣ 子类可以继承父类哪些数据?

子类可以访问父类的权限修饰符只有:public/internal,不能是 external/private

  • 如果父类的状态变量和函数是 privateexternal,则子类不可以继承和访问。
    • 如果子类调用父类 external 修饰的函数,会报错:Cannot call function via contract type name.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract Person {
    string internal name;
    uint256 age; // 状态变量默认是internal权限
    uint256 public hand = 2;
    uint256 private privateState = 99;

    function publicFn() public pure returns (uint256) {
        return 1;
    }

    function internalFn() internal pure returns (uint256) {
        return 2;
    }

    function privateFn() private pure returns (uint256) {
        return 3;
    }
}

contract Man is Person {
    constructor() {
        name = "Anbang";
        age = 18;
    }

    function getInfo()
        external
        view
        returns (
            string memory,
            uint256,
            uint256
        )
    {
        return (name, age, hand);
        // privateState 不可以访问
    }

    function getPublicFn() external pure returns (uint256) {
        return publicFn();
    }

    function getInternalFn() external pure returns (uint256) {
        return internalFn();
    }

    // 不可以访问 privateFn 的方法
    // function getPrivateFn() external pure returns (uint256) {
    //     return privateFn(); // Undeclared identifier.
    // }
}

3️⃣ 多重继承中的重名

  • 一个合约同时继承 2 个合约时,这种情况叫多重继承
  • 多重继承中不允许出现相同的函数名事件名修改器名以及状态变量名等。

如下继承会报错,不允许编译:

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

contract A {
    string internal name;
    event log();
    modifier onlyOwner() {
        _;
    }

    function test() internal {}
}

contract B {
    string internal name;
    event log();
    modifier onlyOwner() {
        _;
    }

    function test() internal {}
}

contract C is A, B {}

多重继承函数中 getter 函数重名也不可以,如下是比较隐蔽的冲突情况

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

contract A {
    uint256 public data = 10;
}

contract B {
    // data函数之所以出错
    // 是因为和 A 中状态变量 data 的 getter 函数重名。
    function data() public returns (uint256) {
        return 1;
    }
}

contract C is A, B {}

当继承时合约出现了一下相同名字会被认为是一个错误:

  • 函数 和 修改器/modifier 同名
  • 函数 和 事件同名
  • 事件和 修改器/modifier 同名
  • 有一种例外情况,状态变量的 getter 函数可以覆盖 external 函数。

4️⃣ 重写函数

solidity 引入了 abstract, virtual, override 几个关键字,用于重写函数。父合约标记为 virtual函数可以在继承合约里重写(overridden)以更改他们的行为。重写的函数需要使用关键字 override 修饰。

继承的方法重写需要注意的点:

  • 父合约方法需要标示为可修改,使用关键字 virtual
  • 子合约方法需要标示为覆盖,使用关键词 override
    • 对于多重继承,如果有多个父合约有相同定义的函数, override 关键字后必须指定所有父合约名。
  • 基础合约中可以包含没有实现代码的函数,也就是纯虚函数,那么基础合约必须声明为 abstract
  • 继承多个合约时,所有同名的可修改函数都需要重写
  • 继承后重写合约方法,各个合约内的函数可见性需要一致
  • 可变性可以按照以下顺序更改为更严格的一种: nonpayable 可以被 viewpure 覆盖。 view 可以被 pure 覆盖。 payable是一个例外,不能更改为任何其他可变性。

1.virtual 和 override

以下例子,B 继承 A,C 继承 B

  • A 是爷爷
  • B 是爸爸
  • C 是孙子
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract A {
    function test1() public pure virtual returns (string memory) {
        return "test1 from A";
    }

    // 使用 public 和 external 都可以
    function test2() external pure virtual returns (string memory) {
        return "test2 from A";
    }

    function test3() public pure virtual returns (string memory) {
        return "test3 from A";
    }
}

contract B is A {
    function test1() public pure virtual override returns (string memory) {
        return "test1 from B";
    }

    function test2() external pure override returns (string memory) {
        return "test2 from B";
    }
}

contract C is B {
    function test1() public pure override returns (string memory) {
        return "test1 from C";
    }
}

对于多重继承,如果有多个父合约有相同定义的函数, override 关键字后必须指定所有父合约名。

pragma solidity >=0.7.0 <0.9.0;

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

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

contract Inherited is Base1, Base2
{
    // 继承自两个基类合约定义的foo(), 必须显示的指定 override
    function foo() public override(Base1, Base2) {}
}

不过如果(重写的)函数继承自一个公共的父合约, override 是可以不用显示指定的。 例如:

pragma solidity >=0.7.0 <0.9.0;

contract A { function f() public pure{} }
contract B is A {}
contract C is A {}
// 不用显示  override
contract D is B, C {}

更正式地说,如果存在父合约是签名函数的所有重写路径的一部分,则不需要重写(直接或间接)从多个基础继承的函数,并且(1)父合约实现了该函数,从当前合约到父合约的路径都没有提到具有该签名的函数,或者(2)父合约没有实现该函数,并且存在从当前合约到该父合约的所有路径中,最多只能提及该函数。

从这个意义上说,签名函数的重写路径是通过继承图的路径,该路径始于所考虑的合约,并终止于提及具有该签名的函数的合约。

如果函数没有标记为 virtual ,那么派生合约将不能更改函数的行为(即不能重写)。

.. note::private 的函数是不可以标记为 virtual 的。

.. note::除接口之外(因为接口会自动作为 virtual ),没有实现的函数必须标记为virtual

.. note::从 Solidity 0.8.8 开始, 在重写接口函数时不再要求 override 关键字,除非函数在多个父合约定义。

如果 getter 函数的参数和返回值都和外部函数一致时,外部(external)函数是可以被 public 的状态变量被重写的,例如:


    pragma solidity >=0.7.0 <0.9.0;

    contract A
    {
        function f() external view virtual returns(uint) { return 5; }
    }

    contract B is A
    {
        uint public override f;
    }

⚠️ : 尽管 public 的状态变量可以重写外部函数,但是 public 的状态变量不能被重写。

2.abstract(抽象合约)

基础合约中可以包含没有实现代码的函数,也就是纯虚函数,那么基础合约必须声明为 abstract

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

abstract contract IERC20 {
    function transfer() external virtual returns (bool);
}

contract ERC20 is IERC20 {
    function transfer() external pure override returns (bool) {
        return true;
    }
}

扩展: 这里的 abstract,也可以使用 interface 来解决。 更多 interface 内容,请参考 interface:接口 详细阅读。

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

interface IERC20 {
    function transfer() external returns (bool);
}

contract ERC20 is IERC20 {
    function transfer() external pure returns (bool) {
        return true;
    }
}

5️⃣ 多级继承的代码书写顺序(线性化)

Solidity 语言的多重继承采用线性继承方式。继承顺序很重要,判断顺序的一个简单规则是按照从“最类似基类”到“最多派生”的顺序指定基类

原则:先写基础合约,再写派生合约。

小技巧:可以先画继承逻辑图,然后按照从上到下,从左到右的顺序来写合约;

上面 virtual 和 override 的介绍例子中,合约继承逻辑图如下:

/**
A
|
B
|
C
 */

B 继承 A,C 继承 B。所以写的时候顺序是: A => B => C

1.继承案例一

/**
 B   A
  \ /
   C
 */

C 继承 A 和 B。

写的时候顺序是: A > B > C / B > A > C 都是可以的

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

contract A {
    function test1() public pure virtual returns (string memory) {
        return "test1 from A";
    }
}

contract B {
    function test2() public pure virtual returns (string memory) {
        return "test2 from B";
    }
}

contract C is A, B {
    function test3() public pure returns (string memory) {
        return "test3 from C";
    }
}

2.继承案例二

/**
    A
  / |
B   |
  \ |
    C
 */

B 继承 A,C 继承 A 和 B。注意:这里是 C 继承 A 和 B,不是 C 继承 B 和 A

写的时候顺序是: A > B > C

代码如下:

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

contract A {
    function test1() public pure virtual returns (string memory) {
        return "test1 from A";
    }

    // 使用 public 和 external 都可以
    function test2() external pure virtual returns (string memory) {
        return "test2 from A";
    }

    function test3() public pure virtual returns (string memory) {
        return "test3 from A";
    }
}

contract B is A {
    function test1() public pure virtual override returns (string memory) {
        return "test1 from B";
    }

    function test2() external pure virtual override returns (string memory) {
        return "test2 from B";
    }
}

// 这里必须是 contract C is A, B
// 不能使用 contract C is B, A
contract C is A, B {
    function test1() public pure override(A, B) returns (string memory) {
        return "test1 from C";
    }

    // overrid内参数顺序无所谓,
    // C 内必须重写 A 和 B,否则会报错
    function test2() public pure override(B, A) returns (string memory) {
        return "test1 from C";
    }
}
  • C 继承 A 和 B 时候,所有 A 和 B 函数相同名字的方法,都需要重写。
    • 否则会报错:
    Derived contract must override function "functionName".
    Two or more base classes define function with same name and parameter types.
    
  • 多重继承时候,需要先写基础合约,再写派生合约
    • 写合约 C 的时候,必须写成contract C is A, B,不能写contract C is B, A
    • 否则会报错:TypeError: Linearization of inheritance graph impossible

3.继承案例三

/**
    A
  / |
B   |
  \ |
    C
    |
    D
 */

B 继承 A,C 继承 A 和 B,D 继承 C。(注意:这里是 C 继承 A 和 B,不是 C 继承 B 和 A),所以写的时候顺序是: A > B > C > D

D 继承 C 时候,没有同名函数的冲突,所以 test1 和 test2 随便是否重写。

代码如下

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

contract A {
    function test1() public pure virtual returns (string memory) {
        return "test1 from A";
    }

    // 使用 public 和 external 都可以
    function test2() external pure virtual returns (string memory) {
        return "test2 from A";
    }

    function test3() public pure virtual returns (string memory) {
        return "test3 from A";
    }
}

contract B is A {
    function test1() public pure virtual override returns (string memory) {
        return "test1 from B";
    }

    function test2() external pure virtual override returns (string memory) {
        return "test2 from B";
    }
}

// 这里必须是 contract C is A, B
// 不能使用 contract C is B, A
contract C is A, B {
    function test1()
        public
        pure
        virtual
        override(A, B)
        returns (string memory)
    {
        return "test1 from C";
    }

    // overrid内参数顺序无所谓,
    function test2()
        public
        pure
        virtual
        override(B, A)
        returns (string memory)
    {
        return "test1 from C";
    }
}

contract D is C {
    function test1() public pure override returns (string memory) {
        return "test1 from D";
    }
}

4.继承案例四

/**
     A
   / |
  /  |
B    C
  \  |
   \ D
    \|
     E
 */

B 继承 A,C 继承 A,D 继承 C,E 继承 B 和 D。所以写的时候顺序是: A > B > C > D

例子如下:

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

contract A {
    function test1() public pure virtual returns (string memory) {
        return "test1 from A";
    }

    // 使用 public 和 external 都可以
    function test2() external pure virtual returns (string memory) {
        return "test2 from A";
    }

    function test3() public pure virtual returns (string memory) {
        return "test3 from A";
    }
}

contract B is A {
    function test1() public pure virtual override returns (string memory) {
        return "test1 from B";
    }

    function test2() external pure virtual override returns (string memory) {
        return "test2 from B";
    }
}

contract C is A {
    function test1() public pure virtual override returns (string memory) {
        return "test1 from C";
    }
}

contract D is C {
    function test1() public pure virtual override returns (string memory) {
        return "test1 from D";
    }
}

contract D is B, D {
    function test1() public pure override(B, D) returns (string memory) {
        return "test1 from E";
    }

    // 必须要重写 test2 ,因为此时 B 和 D 内都有test2方法,但是D内继承A的test2方法,冲突了。
    // 需要要写  override(B, A),不能写 override(B, D),否则会报如下错误
    // Function needs to specify overridden contract "A".
    // Invalid contract specified in override list: "D".

    // 下面是错误的写法
    // function test2() public pure override(B, D) returns (string memory) {

    // 下面是正确的写法
    function test2() public pure override(B, A) returns (string memory) {
        return "test2 from E";
    }
}

E 内必须要重写 test2 ,因为此时 B 和 D 内都有 test2 方法,但是 D 内继承 A 的 test2 方法,需要要写 override(B, A),不能写 override(B, D),否则会报错误:

Function needs to specify overridden contract "A".
Invalid contract specified in override list: "D".

6️⃣ 继承中两种构造函数传参方式

继承的父合约,如果有构造函数并且需要传入参数,我们有以下几种方法进行参数传入

两种传参方法

  • 方法 1: 固定值传参。(该方式不能在部署时动态输入)。
    • 如果我们已经知道基类初始化参数,那么就可以在派生类的继承声明中,直接传递参数给基类的构造函数。
    contract C is A("n"),B("v") {}
    
  • 方法 2: 动态传参
    • 如果我们需要在部署时或者运行时,由调用方传递基类初始化参数。在这种情况下,我们需要编写一个新的构造函数,传递参数给基类。
    • 部署子合约的时候,传入参数到构造函数,该种方法是动态的值,可以部署的时候动态输入
    contract D is A {
      constructor(string memory _name) A(_name) {}
    }
    
  • 混写: 方法 1 和方法 2 可以混合使用
    contract E is A, B("EEEEEEEEEEEEE") {
      constructor(string memory _name) A(_name) {}
    }
    

例子如下:

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

contract A {
    string public nameA;

    constructor(string memory _name) {
        nameA = _name;
    }
}

contract B {
    string public nameB;

    constructor(string memory _name) {
        nameB = _name;
    }
}

// 方法1: 继承时候直接传入参数,该种方法是固定值,不能动态输入
contract C is A("Name From C") {

}

// 方法2: 部署子合约的时候,传入参数到构造函数,该种方法是动态的值,可以部署的时候动态输入
contract D is A {
    constructor(string memory _name) A(_name) {}
}

//  混合使用
contract E is A, B("EEEEEEEEEEEEE") {
    constructor(string memory _name) A(_name) {}
}

7️⃣ 继承中构造函数的执行顺序

多重继承中,构造函数的执行会按照定义时的继承顺序进行,与构造函数中定义顺序无关。

原则: 构造函数的执行顺序按照继承的顺序。

例子:

  1. 如下是先执行 A,再执行 B
contract E is A, B("EEEEEEEEEEEEE") {
    constructor(string memory _name) A(_name) {}
}
  1. 如下是先执行 B,再执行 A
contract E is B("EEEEEEEEEEEEE"),A {
    constructor(string memory _name) A(_name) {}
}

例子如下:

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

contract A {
    event logA(string);

    constructor(string memory _name) {
        emit logA(_name);
    }
}

contract B {
    event logB(string);

    constructor(string memory _name) {
        emit logB(_name);
    }
}

//  混合使用
contract E is A, B("EEE") {
    constructor(string memory _name) A(_name) {}
}

contract F is B("FFF"), A {
    constructor(string memory _name) A(_name) {}
}

部署 E 时候,输入 Anbang,输出的 log 如下:先执行 A,再执行 B

[
	{
		"from": "0xed27012c24FDa47A661De241c4030ecB9D18a76d",
		"topic": "0xb911c6b2723a9b89f3c8a0ce3f2dca6648150807aa6d6959fb18fa31748efcee",
		"event": "logA",
		"args": {
			"0": "Anbang"
		}
	},
	{
		"from": "0xed27012c24FDa47A661De241c4030ecB9D18a76d",
		"topic": "0x2a205cc759862a651d0a138a2245cac3f4b3214b93707a9d0fe4eb716f66f786",
		"event": "logB",
		"args": {
			"0": "EEE"
		}
	}
]

部署 F 时候,输入 Anbang,输出的 log 如下:先执行 B,再执行 A

[
	{
		"from": "0x3D42AD7A3AEFDf99038Cd61053913CFCA4944b95",
		"topic": "0x2a205cc759862a651d0a138a2245cac3f4b3214b93707a9d0fe4eb716f66f786",
		"event": "logB",
		"args": {
			"0": "FFF"
		}
	},
	{
		"from": "0x3D42AD7A3AEFDf99038Cd61053913CFCA4944b95",
		"topic": "0xb911c6b2723a9b89f3c8a0ce3f2dca6648150807aa6d6959fb18fa31748efcee",
		"event": "logA",
		"args": {
			"0": "Anbang"
		}
	}
]

8️⃣ 两种子合约调用父合约的方法

有两种方法可以调用

  1. 直接使用合约名调用 ParentContractName.functionName();
  2. 使用 super 关键字 super.functionName()
    • super 会自动寻找父合约,并执行对应的方法;
    • 如果是多个父级,那么父级都会执行。但有时候又不会,执行顺序的原理,这些需要详细的了解
    • 如果 super 导致 2 个父级同时触发同一个爷爷合约的相同方法;则爷爷的方法只执行一次。一个合约的同一个方法只会执行一次,不会执行多次。

1.直接使用合约名调用

执行顺序:像水中的冒泡一样,由下向上进行执行。

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

contract A {
    event Log(string msg);

    function test1() public virtual {
        emit Log("A.test1");
    }
}

contract B is A {
    function test1() public virtual override {
        emit Log("B.test1");
        A.test1();
    }
}

上面的例子执行顺序是

1. B.test1
2. A.test1

2.使用 super 关键字调用

基础继承

执行顺序:像水中的冒泡一样,由下向上进行执行。

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

contract A {
    event Log(string msg);

    function test1() public virtual {
        emit Log("A.test1");
    }
}

contract C is A {
    function test1() public virtual override {
        emit Log("C.test1");
        super.test1();
    }
}

上面的例子执行顺序是

1. C.test1
2. A.test1

多重继承

写一个如下逻辑的继承

/**
   A
 /   \
B     C
 \   /
   D
 */
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract A {
    event Log(string msg);

    function test1() public virtual {
        emit Log("A.test1");
    }
}

contract B is A {
    function test1() public virtual override {
        emit Log("B.test1");
        A.test1();
    }
}

contract C is A {
    function test1() public virtual override {
        emit Log("C.test1");
        super.test1();
    }
}

contract D is B, C {
    function test1() public override(B, C) {
        emit Log("D.test1");
        // 因为 B 和 C 都是 D 的父级,所以B和C都会执行
        super.test1();
    }
}

执行顺序:像水中的冒泡一样,由下向上进行执行。

1. D.test1
1. C.test1
1. B.test1
1. A.test1 (这里 A 只执行一次)

警告 : 为什么先输出 C,后输出 B ?

上面的例子,如果代码中 B 和 C 换顺序,还是执行的 DCBA。开始怀疑和函数名字的 hash 结果顺序有关系,看完下面的继续研究代码,可以得出结论,复杂继承的时候,supper 方式就像一个疯子一样没有规律可言。我们能做的就是避开使用它。

9️⃣ 多重继承合约不要使用 supper

写一个如下逻辑的继承

/**
   A
 /   \
B     C
| \  /|
|  \/ |
|  /\ |
| /  \|
D     E
 */

代码如下:

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

contract A {
    event Log(string msg);

    // 继承后重写合约方法,可见性需要一致
    function test1() public virtual {
        emit Log("A.test1");
    }
}

contract B is A {
    // 执行 B.test1 后
    // 1. B.test1
    // 2. A.test1
    function test1() public virtual override {
        emit Log("B.test1");
        A.test1();
    }
}

contract C is A {
    // 执行 C.test1 后
    // 1. C.test1
    // 2. A.test1
    function test1() public virtual override {
        emit Log("C.test1");
        super.test1();
    }
}

contract D is B, C {
    // 执行 D.test1 后
    // 1. D.test1
    // 2. C.test1
    // 3. B.test1
    // 4. A.test1
    function test1() public override(B, C) {
        emit Log("D.test1");
        super.test1();
    }
}

// 代码 contract D is B, C 改成 contract E is C, B 后 , C.test1 不执行了。
contract E is C, B {
    // 执行 E.test1 后
    // 1. E.test1
    // 3. B.test1
    // 4. A.test1
    function test1() public override(B, C) {
        emit Log("E.test1");
        // B 和 C 都是 E 的父级,为啥B执行,而C不执行
        super.test1();
    }
}

疑问: 执行 E.test1 后,输出如下结果,这是没有任何规律的,并且丢失数据,在这种多重继承的时候,调用父合约不要 supper,直接使用合约名字是最稳妥的办法,切记切记。

1. E.test1
3. B.test1
4. A.test1

🆗 实战应用

下面的例子进行了详细的说明。

    // SPDX-License-Identifier: GPL-3.0
    pragma solidity >=0.7.0 <0.9.0;

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

    // 使用 is 从另一个合约派生。派生合约可以访问所有非私有成员,包括内部(internal)函数和状态变量,
    // 但无法通过 this 来外部访问。
    contract Destructible is Owned {

     // 关键字`virtual`表示该函数可以在派生类中“overriding”。

         function destroy() virtual public { if (msg.sender == owner)
selfdestruct(owner); } }

    // 这些抽象合约仅用于给编译器提供接口。
    // 注意函数没有函数体。
    // 如果一个合约没有实现所有函数,则只能用作接口。
    abstract contract Config {
        function lookup(uint id) public virtual returns (address adr);
    }

    abstract contract NameReg {
        function register(bytes32 name) public virtual;
        function unregister() public virtual;
     }

    // 可以多重继承。请注意,owned 也是 Destructible 的基类,
    // 但只有一个 owned 实例(就像 C++ 中的虚拟继承)。
    contract Named is Owned, Destructible {
        constructor(bytes32 name) {
            Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
            NameReg(config.lookup(1)).register(name);
        }

        // 函数可以被另一个具有相同名称和相同数量/类型输入的函数重载。
        // 如果重载函数有不同类型的输出参数,会导致错误。
        // 本地和基于消息的函数调用都会考虑这些重载。

//如果要覆盖函数,则需要使用 `override` 关键字。
如果您想再次覆盖此函数,则需要再次指定`virtual`关键字。

        function destroy() public virtual override {
            if (msg.sender == owner) {
                Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
                NameReg(config.lookup(1)).unregister();
                // 仍然可以调用特定的重载函数。
                Destructible.destroy();
            }
        }
    }

    // 如果构造函数接受参数,
    // 则需要在声明(合约的构造函数)时提供,
    // 或在派生合约的构造函数位置以修改器调用风格提供(见下文)。
    contract PriceFeed is Owned, Destructible, Named("GoldFeed") {
        function updateInfo(uint newInfo) public {
            if (msg.sender == owner) info = newInfo;
        }

        // Here, we only specify `override` and not `virtual`.
        // This means that contracts deriving from `PriceFeed`
        // cannot change the behaviour of `destroy` anymore.
        function destroy() public override(Destructible, Named) { Named.destroy(); }

        function get() public view returns(uint r) { return info; }

        uint info;
    }

注意,在上边的代码中,我们调用 Destructible.destroy() 来"转发"销毁请求。 这样做法是有问题的,在下面的例子中可以看到:


    // SPDX-License-Identifier: GPL-3.0
    pragma solidity >=0.7.0 <0.9.0;

    contract owned {
        constructor() { owner = payable(msg.sender); }
        address owner;
    }

    contract Destructible is owned {
        function destroy() public virtual {
            if (msg.sender == owner) selfdestruct(owner);
        }
    }

    contract Base1 is Destructible {
        function destroy() public virtual override  {
            /* 清除操作 1 */
            Destructible.destroy();
        }
    }

    contract Base2 is Destructible {
        function destroy() public { /* 清除操作 2 */ Destructible.destroy(); }
    }

    contract Final is Base1, Base2 {
        function destroy() public override(Base1, Base2) { Base2.destroy(); }
    }

。 解决此问题的方法是使用 super:

调用 Final.destroy() 时会调用 Base2.destroy, 因为我们在最终重写中显式指定了它。 但是此函数将绕过 Base1.destroy, 解决这个问题的方法是使用 super


    // SPDX-License-Identifier: GPL-3.0
    pragma solidity >=0.7.0 <0.9.0;

    contract owned {
        constructor() { owner = payable(msg.sender); }
        address owner;
    }

    contract Destructible is owned {
        function destroy() virtual public {
            if (msg.sender == owner) selfdestruct(owner);
        }
    }

    contract Base1 is Destructible {
        function destroy() public virtual override { /* 清除操作 1 */ super.destroy(); }
    }


    contract Base2 is Destructible {
        function destroy() public  virtual override { /* 清除操作 2 */ super.destroy(); }
    }

    contract Final is Base1, Base2 {
        function destroy() public override(Base1, Base2) { super.destroy(); }
    }

如果 Base2 调用 super 的函数,它不会简单在其基类合约上调用该函数。 相反,它在最终的继承关系图谱的下一个基类合约中调用这个函数,所以它会调用 Base1.destroy() (注意最终的继承序列是------从最终派生合约开始:Final, Base2, Base1, Destructible, ownerd)。 在类中使用 super 调用的实际函数在当前类的上下文中是未知的,尽管它的类型是已知的。 这与普通的虚拟方法查找类似。

#️⃣ 问答题

  • 继承如何实现?
    • 使用 is 实现继承
    • 继承: 派生合约继承基础合约的属性和方法
    • 基础合约通常也被称为父合约,派生合约通常也称作子合约
    • 父合约必须写在子合约的前面,否则会报错
  • 子类可以继承父类哪些数据?
    • 子类可以访问父类的权限修饰符只有:public/internal,不能是 external/private
  • 多重继承中哪些属于重名?
    • 一个合约同时继承 2 个合约时,这种情况叫多重继承
    • 多重继承中不允许出现相同的函数名事件名修改器名以及状态变量名等。多重继承函数中 getter 函数重名也不可以。
    • 当继承时合约出现了一下相同名字会被认为是一个错误:
      • 函数 和 修改器/modifier 同名
      • 函数 和 事件同名
      • 事件和 修改器/modifier 同名
      • 有一种例外情况,状态变量的 getter 函数可以覆盖 external 函数。
  • 如何重写函数?
    • solidity 引入了 abstract, virtual, override 几个关键字,用于重写函数。
    • 父合约方法需要标示为可修改,使用关键字 virtual
    • 子合约方法需要标示为覆盖,使用关键词 override
      • 对于多重继承,如果有多个父合约有相同定义的函数, override 关键字后必须指定所有父合约名。
    • 基础合约中可以包含没有实现代码的函数,也就是纯虚函数,那么基础合约必须声明为 abstract。(abstract,也可以使用 interface 来解决。)
    • 继承多个合约时,所有同名的可修改函数都需要重写
    • 继承后重写合约方法,各个合约内的函数可见性需要一致
    • 可变性可以按照以下顺序更改为更严格的一种: nonpayable 可以被 viewpure 覆盖。 view 可以被 pure 覆盖。 payable是一个例外,不能更改为任何其他可变性。
  • 多级继承的代码书写顺序?
    • 按照从“最类似基类”到“最多派生”的顺序指定基类。
    • 原则:先写基础合约,再写派生合约。
    • 小技巧:可以先画继承逻辑图,然后按照从上到下,从左到右的顺序来写合约;
  • 继承中两种构造函数传参方式。
    • 方法 1: 固定值传参。(该方式不能在部署时动态输入)。
      • 如果我们已经知道基类初始化参数,那么就可以在派生类的继承声明中,直接传递参数给基类的构造函数。
      contract C is A("n"),B("v") {}
      
    • 方法 2: 动态传参
      • 如果我们需要在部署时或者运行时,由调用方传递基类初始化参数。在这种情况下,我们需要编写一个新的构造函数,传递参数给基类。
      • 部署子合约的时候,传入参数到构造函数,该种方法是动态的值,可以部署的时候动态输入
      contract D is A {
        constructor(string memory _name) A(_name) {}
      }
      
    • 混写: 方法 1 和方法 2 可以混合使用
      contract E is A, B("EEEEEEEEEEEEE") {
        constructor(string memory _name) A(_name) {}
      }
      
  • 继承中构造函数的执行顺序
    • 多重继承中,构造函数的执行会按照定义时的继承顺序进行,与构造函数中定义顺序无关。原则: 构造函数的执行顺序按照继承的顺序。
    • 如下是先执行 A,再执行 B
      contract E is A, B("EEEEEEEEEEEEE") {
          constructor(string memory _name) A(_name) {}
      }
      
    • 如下是先执行 B,再执行 A
      contract E is B("EEEEEEEEEEEEE"),A {
          constructor(string memory _name) A(_name) {}
      }
      
  • 子合约调用父合约的方法
    1. 直接使用合约名调用 ParentContractName.functionName();
    2. 使用 super 关键字 super.functionName()
      • super 会自动寻找父合约,并执行对应的方法;
      • 如果是多个父级,那么父级都会执行。但有时候又不会,执行顺序的原理,这些需要详细的了解
      • 如果 super 导致 2 个父级同时触发同一个爷爷合约的相同方法;则爷爷的方法只执行一次。一个合约的同一个方法只会执行一次,不会执行多次。
    3. 执行顺序:像水中的冒泡一样,由下向上进行执行。但是如果多层级的执行,顺序规律还没有找到规律。
  • 聊一聊合约继承
    • 以上的内容精简回答
    • 修饰符可以继承
    • 事件不可以继承,但是可以重载
    • fallback 可以继承,但是需要保持原有的 payable/nonpayable
    • receive 可以继承,但是需要保持原有的 payable/nonpayable