继承
理解继承
继承是面向对象编程中的重要概念之一,它允许一个类(称为子类或派生类)从另一个类(称为父类或基类)继承属性和方法。
Solidity 也支持继承, 当然这里对应的是派生合约(或称子合约)及父合约。
派生合约通过继承获得了父合约的特性,合理的使用继承可以带来这些好处:
- 代码更好重用:派生合约可以直接获得父合约的属性和方法,不需要重复编写相同的代码。
- 方便扩展:在继承的基础上添加新的属性和方法,更方便扩展和定制父合约的功能。
- 提高维护性和可读性:继承可以使合约之间建立清晰的层次关系,使代码更加易于维护和理解。
实际上,正是因为 Solidity 有继承的特性,我们才可以使用大量的第三方合约库(如OpenZepplin)来简化我们的开发。
如下图是一个常见继承图:
在上面的图中,ERC20
是一个父合约,MyTokenA
和 MyTokenB
是继承自 ERC20
的派生合约,它们继承了 ERC20
的属性和方法,但可以拥有自己的值和方法实现。
使用继承
Solidity 使用关键字 is
来表示合约的继承关系:
把Sub
合约部署上链, 可以看到Sub
合约有两个属性,其中 a
继承自 Base
合约。
派生合约会继承父合约内的所有非私有(private)成员:
public | external | internal | private | |
---|---|---|---|---|
可被继承 | ✓ | ✓ | ✓ |
因此内部(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。
说明父合约的构造函数被自动执行了,同时我们也可以做一些验证,会得到结论:父合约的构造函数代码会先调用而后调用派生合约的构造函数。
刚才父合约构造函数是没有参数的,可以自动执行,如果有参数呢?如何自动执行?如何给对父合约构造函数传参呢?
有两种方法:
- 在继承父合约的合约名中指定参数
示例代码如下:
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 构造函数传参进行初始化。
- 在派生构造函数中使用修饰符方式调用父合约
示例代码如下:
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熊。