跳到主要内容

继承

理解继承

继承是面向对象编程中的重要概念之一,它允许一个类(称为子类或派生类)从另一个类(称为父类或基类)继承属性和方法。

Solidity 也支持继承, 当然这里对应的是派生合约(或称子合约)及父合约。

派生合约通过继承获得了父合约的特性,合理的使用继承可以带来这些好处:

  1. 代码更好重用:派生合约可以直接获得父合约的属性和方法,不需要重复编写相同的代码。
  2. 方便扩展:在继承的基础上添加新的属性和方法,更方便扩展和定制父合约的功能。
  3. 提高维护性和可读性:继承可以使合约之间建立清晰的层次关系,使代码更加易于维护和理解。

实际上,正是因为 Solidity 有继承的特性,我们才可以使用大量的第三方合约库(如OpenZepplin)来简化我们的开发。

如下图是一个常见继承图:

solidity-继承

在上面的图中,ERC20 是一个父合约,MyTokenAMyTokenB 是继承自 ERC20 的派生合约,它们继承了 ERC20 的属性和方法,但可以拥有自己的值和方法实现。

使用继承

Solidity 使用关键字 is 来表示合约的继承关系:

Sub合约部署上链, 可以看到Sub合约有两个属性,其中 a 继承自 Base 合约。

Solidity - 继承 is

派生合约会继承父合约内的所有非私有(private)成员:

publicexternalinternalprivate
可被继承

因此内部(internal)函数和公开的状态变量在派生合约里是可以直接使用,派生合约也会继承 external 方法,但不能在内部访问。

需要注意的是,在部署派生合约时,父合约不会连带被部署,可以理解为,在编译时,编译器会把父合约的代码拷贝进派生合约。因此,不能在派生合约再次声明父合约中已经存在的状态变量。

不过父合约函数可以重写,派生合约可以通过重写(overide)函数来更改父合约中函数的行为。

函数重写(overriding)

只有父合约中的虚函数(使用了virtual 关键字修饰的函数)可以在派生被重写,以更改它们在父合约中的行为,重写函数需要使用关键字override修饰表示正在重写父合约的某个函数。

以下一段代码,试一试调用重写的foo 之后,a 的结果是:

a 的结果是1、2 还是 3 呢?

结果是 1 ,是因为当函数被重写后,父合约的函数就会被遮蔽。

重写的函数也可以再次重写,当需要被重写时,需要使用 virtual 来修饰, 例如:

contract Sub is Base {
function foo() public virtual override {
a += 1;
}
}

contract Inherited is Sub {
function foo() public virtual override {
a += 3;
}
}

使用 super 调用父合约函数

刚才我们说,当函数被重写后,父合约的函数就会被遮蔽。

有时候,我们会现在父合约函数的基础上,添加一些实现,要如何做呢?

我们可以在重写的函数中显式的用 super调用父合约的函数,例如:

pragma solidity >=0.8.0;

contract Base {
uint public a;
function foo() virtual public {
a += 2;
}
}

contract Sub is Base {

function foo() public override {
super.foo(); // 或者 Base.foo();
a += 1;
}
}

想想看此时调用 Sub 的foo 函数后, a 的结果是多少?

大家自行验证哦~

继承下构造函数

构造函数的处理与函数重写处理的方式不一样,当派生合约继承父合约时,如果父合约实现了构造函数,在部署派生合约时,父合约的构造函数也会执行。

这是一个例子:

在部署 Sub 合约后,可以查看到 a 为1,b 为2。

说明父合约的构造函数被自动执行了,同时我们也可以做一些验证,会得到结论:父合约的构造函数代码会先调用而后调用派生合约的构造函数。

刚才父合约构造函数是没有参数的,可以自动执行,如果有参数呢?如何自动执行?如何给对父合约构造函数传参呢?

有两种方法:

  1. 在继承父合约的合约名中指定参数

示例代码如下:

contract Base {
uint public a;
constructor(uint _a) {
a = _a;
}
}

contract Sub is Base(1) {
uint public b ;
constructor() {
b = 2;
}
}

代码 contract Sub is Base(1) 对Base 构造函数传参进行初始化。

  1. 在派生构造函数中使用修饰符方式调用父合约

示例代码如下:

contract Sub is Base {
uint public b ;

constructor() Base(1) {
b = 2;
}
}

或者是:

    constructor(uint _b) Base(_b / 2)  {
b = _b;
}

此时利用部署合约Sub的参数,传入到合约 Base 中。

多重继承

Solidity也支持多重继承,即可以从多个父合约继承,直接在is后面接多个父合约即可,例如:

contract Sub is Base1, Base2 {

}

如果多个父合约之间也有继承关系,那么 is 后面的合约的书写顺序就很重要,正确的顺序应该是:父合约在前,子合约在后,例如:下面的代码将无法编译:

contract X {}
contract A is X {}
// 编译出错
contract C is A, X {}

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

示例代码如下:

pragma solidity >=0.8.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) {}
}

抽象合约

有一些父合约,我们创建他们,只是为了在合约之间建立清晰的结构关系,而不是真实的部署这些父合约。

我们可以在这些不想被部署的合约前加上 abstract 关键字,表示这是一个抽象合约。

下面是抽象合约的示例代码:

abstract contract Base {
uint public a;
}

抽象合约由于不需要部署,因此可以声明没有具体实现的纯虚函数,纯虚函数声明用";"结尾,而不是"{ }",例如:

pragma solidity >=0.8.0;

abstract contract Base {
function get() virtual public;
}

这段代码声明了一个 get() 函数,没有函数体,这个函数需要有派生的合约来实现。

小结

本文,我们学习了继承的概念,继承的特性让我们更好重用代码,也使代码更加易于维护和理解。但也要尽量少使用复杂的多重继承。

我们只要掌握以下关键字,就知道在 Solidity 代码中如何使用继承了:

  • is : 继承某合约
  • abstract: 表示该合约不可被部署
  • super:调用父合约函数

  • virtual: 表示函数可以被重写

  • override: 表示重写了父合约函数


DeCert.me 码一个未来,DeCert 让每一位开发者轻松构建自己的可信履历。

DeCert.me 由登链社区 @UpchainDAO 孵化,欢迎 Discord 频道 一起交流。

本教程来自贡献者 @Tiny熊