13. 继承
WTF Solidity极简入门: 13. 继承
我最近在重新学 Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新 1-3 讲。
所有代码和教程开源在 github: github.com/AmazingAng/WTF-Solidity
这一讲,我们介绍Solidity
中的继承(inheritance
),包括简单继承,多重继承,以及修饰器(Modifier
)和构造函数(Constructor
)的继承。
继承
继承是面向对象编程很重要的组成部分,可以显著减少重复代码。如果把合约看作是对象的话,Solidity
也是面向对象的编程,也支持继承。
规则
-
virtual
: 父合约中的函数,如果希望子合约重写,需要加上virtual
关键字。 -
override
:子合约重写了父合约中的函数,需要加上override
关键字。
注意:用override
修饰public
变量,会重写与变量同名的getter
函数,例如:
1 | mapping(address => uint256) public override balanceOf; |
简单继承
我们先写一个简单的爷爷合约Yeye
,里面包含1个Log
事件和3个function
: hip()
, pop()
, yeye()
,输出都是”Yeye”。
1 | contract Yeye { |
我们再定义一个爸爸合约Baba
,让他继承Yeye
合约,语法就是contract Baba is Yeye
,非常直观。在Baba
合约里,我们重写一下hip()
和pop()
这两个函数,加上override
关键字,并将他们的输出改为”Baba”
;并且加一个新的函数baba
,输出也是”Baba”
。
1 | contract Baba is Yeye{ |
我们部署合约,可以看到Baba
合约里有4个函数,其中hip()
和pop()
的输出被成功改写成”Baba”
,而继承来的yeye()
的输出仍然是”Yeye”
。
多重继承
Solidity
的合约可以继承多个合约。规则:
-
继承时要按辈分最高到最低的顺序排。比如我们写一个
Erzi
合约,继承Yeye
合约和Baba
合约,那么就要写成contract Erzi is Yeye, Baba
,而不能写成contract Erzi is Baba, Yeye
,不然就会报错。 -
如果某一个函数在多个继承的合约里都存在,比如例子中的
hip()
和pop()
,在子合约里必须重写,不然会报错。 -
重写在多个父合约中都重名的函数时,
override
关键字后面要加上所有父合约名字,例如override(Yeye, Baba)
。
例子:
1 | contract Erzi is Yeye, Baba{ |
我们可以看到,Erzi
合约里面重写了hip()
和pop()
两个函数,将输出改为”Erzi”
,并且还分别从Yeye
和Baba
合约继承了yeye()
和baba()
两个函数。
修饰器的继承
Solidity
中的修饰器(Modifier
)同样可以继承,用法与函数继承类似,在相应的地方加virtual
和override
关键字即可。
1 | contract Base1 { |
Identifier
合约可以直接在代码中使用父合约中的exactDividedBy2And3
修饰器,也可以利用override
关键字重写修饰器:
1 | modifier exactDividedBy2And3(uint _a) override { |
构造函数的继承
子合约有两种方法继承父合约的构造函数。举个简单的例子,父合约A
里面有一个状态变量a
,并由构造函数的参数来确定:
1 | // 构造函数的继承 |
-
在继承时声明父构造函数的参数,例如:
contract B is A(1)
-
在子合约的构造函数中声明构造函数的参数,例如:
1
2
3contract C is A {
constructor(uint _c) A(_c * _c) {}
}
调用父合约的函数
子合约有两种方式调用父合约的函数,直接调用和利用super
关键字。
-
直接调用:子合约可以直接用
父合约名.函数名()
的方式来调用父合约函数,例如Yeye.pop()
1
2
3function callParent() public{
Yeye.pop();
} -
super
关键字:子合约可以利用super.函数名()
来调用最近的父合约函数。Solidity
继承关系按声明时从右到左的顺序是:contract Erzi is Yeye, Baba
,那么Baba
是最近的父合约,super.pop()
将调用Baba.pop()
而不是Yeye.pop()
:1
2
3
4function callParentSuper() public{
// 将调用最近的父合约函数,Baba.pop()
super.pop();
}
钻石继承
在面向对象编程中,钻石继承(菱形继承)指一个派生类同时有两个或两个以上的基类。
在多重+菱形继承链条上使用super
关键字时,需要注意的是使用super
会调用继承链条上的每一个合约的相关函数,而不是只调用最近的父合约。
我们先写一个合约God
,再写Adam
和Eve
两个合约继承God
合约,最后让创建合约people
继承自Adam
和Eve
,每个合约都有foo
和bar
两个函数。
1 | // SPDX-License-Identifier: MIT |
在这个例子中,调用合约people
中的super.bar()
会依次调用Eve
、Adam
,最后是God
合约。
虽然Eve
、Adam
都是God
的子合约,但整个过程中God
合约只会被调用一次。原因是Solidity
借鉴了Python的方式,强制一个由基类构成的DAG(有向无环图)使其保证一个特定的顺序。更多细节你可以查阅Solidity的官方文档。
在Remix上验证
-
合约简单继承示例, 可以观察到Baba合约多了Yeye的函数
-
合约多重继承可以参考简单继承的操作步骤来增加部署Erzi合约,然后观察暴露的函数以及尝试调用来查看日志
-
修饰器继承示例
-
构造函数继承示例
-
调用父合约示例
-
菱形继承示例
总结
这一讲,我们介绍了Solidity
继承的基本用法,包括简单继承,多重继承,修饰器和构造函数的继承、调用父合约中的函数,以及多重继承中的菱形继承问题。