目录
Solidity中,继承是扩展合约功能的一种方式。Solidity支持单继承和多继承。
合约继承的重要特点:
- 派生合约可以访问父合约的所有非私有成员,包括内部方法和状态变量。但是不允许使用
this
。 - 如果函数输出参数保持不变,则允许函数重写。如果输出参数不同,编译将失败。
- 可以使用
super
关键字或父合约名称调用父合约的函数。
1. 继承的本质
- 继承的实现方案是代码拷贝,所以合约继承后,部署到网络时,将变成一个合约。
- 代码将从父类拷贝到子类中。
2. 继承的定义
- 继承使用
is
,一个合约可以继承多个合约,用逗号分开。 - 如果继承的合约之间也有父子关系,那么合约要按照先父后子的顺序排序。
比如:
contract A {}
contract B is A {} // B继承了A
contract C is A, B {} // 先A,再B
contract C is B, A {} // 错误
3. 继承的可见性
- 子合约不能访问父合约的
private
私有成员。 - 子合约可以访问父合约所有的非私有成员(包括
internal
的函数和状态变量)。
4. 父合约传参
继承的子合约,必须提供父合约构造方法需要的所有参数。
有两种方式实现,如下:
contract Base {
uint x;
function Base(uint _x) {
x = _x;
}
}
// 方式一,在定义继承的时候传参数
contract DerivedA is Base(7) {}
// 方式二,在合约构造函数中传参数
contract DerivedB is Base { // 仅指定继承关系而不提供参数,请不要加括号
function DerivedB(uint _y) Base(_y) {} // 0.4.x之前旧版本的构造函数写法
constructor(uint _y) Base(_y) {} // 0.5.x之后新版本的构造函数写法
}
5. 重写
在子类中允许重写函数,但不允许重写返回参数类型。
下面的代码:
pragma solidity ^0.4.24;
contract Base{
function data() public pure returns(uint){
return 1;
}
}
contract InheritOverride is Base{
function data(uint) public pure {}
function data() public pure returns(uint){}
// function data() public pure returns(string){} // 报错
}
上面代码中的 function data() returns(string){} 将导致报错:
// 本文实测,报错信息为:
TypeError: Overriding function return types differ.
因为不能修改返回类型。
6. 最远继承原则
在继承链中,由于继承实现是代码复制。如果出现函数重写,最终使用的是继承链上哪个合约定义的代码呢?
实际执行时,依据的是最远继承原则(most derived)。
示例:
pragma solidity ^0.4.24;
contract Base1 {
function data() public view returns(uint){
return 1;
}
}
contract Base2 {
function data() public view returns(uint){
return 2;
}
}
contract Derived1 is Base1,Base2 {
function call() public view returns(uint) {
return data(); // 本文实测,返回 2
}
}
contract Derived2 is Base2,Base1 {
function call() public view returns(uint) {
return data(); // 本文实测,返回 1
}
}
7. super关键字
有些时候,我们希望继承链条上每一个函数都能被调用;这个时候,需要用到super关键字。
示例:
pragma solidity ^0.4.24;
contract owned {
address owner;
constructor() public {
owner = msg.sender;
}
}
contract mortal is owned {
function kill() public {
if (msg.sender == owner) selfdestruct(owner);
}
}
contract Base1 is mortal {
function kill() public { /* do cleanup 1 */
mortal.kill();
}
}
contract Base2 is mortal {
function kill() public { /* do cleanup 2 */
mortal.kill();
}
}
contract Final is Base1, Base2 {}
在 Final 中调用 kill() ,将仅仅触发 Base2.kill() 被调用,因为它是最远继承合约,从而跳过Base1.kill()。
如果我们想也触发 Base1.kill(),解决方案是使用 super:
pragma solidity ^0.4.24;
contract owned {
address owner;
constructor() public {
owner = msg.sender;
}
}
contract mortal is owned {
function kill() public {
if (msg.sender == owner) selfdestruct(owner);
}
}
contract Base1 is mortal {
function kill() public { /* do cleanup 1 */
super.kill();
}
}
contract Base2 is mortal {
function kill() public { /* do cleanup 2 */
super.kill();
}
}
contract Final is Base1, Base2 {}
8. 版本 0.6.x 之后的更新
8.1 显式使用 virtual
与 override
在 solidity 0.5 版本中,所有函数都是隐式虚函数,从而可以在继承结构中进一步重写。这在大型继承中尤其危险,在这种情况下,这种歧义可能导致意外的行为和错误。
例如:
pragma solidity ^0.5.17;
contract A {
uint public x;
function setValue(uint _x) public {
x = _x;
}
}
contract B {
uint public y;
function setValue(uint _y) public {
y = _y;
}
}
contract C is A, B {}
合约 C
中,调用 setValue
会调用最后派生合约 B
的实现(因为 B 是继承关系的最后一个),合约编译部署正常;
使用 0.6.0 版编译时,编译器会报这样一个错误:
TypeError: Derived contract must override function "setValue". Two or more base classes define function with same name and parameter types. contract C is A, B {} ^-------------------^
意思是:因为父合约定义具有相同名称和参数类型的函数,派生合约必须重写(override)函数“setValue”。
在上面多重继承的示例中,有同一个函数是从多个父合约(合约A和B)继承。在这种情况下,必须要重写,并且必须 override
修饰符中列出父合约。
要注意重要的一点,override(A,B)
中的顺序无关紧要, 它不会改变 super
的行为, super
仍然由继承图的线性化决定,即继承关系由 contract C is A, B { ... }
声明的顺序决定。
将上述示例合约修改为:
//SPDX-License-Identifier: MIT
pragma solidity ^0.6.10;
contract A {
uint public x;
function setValue(uint _x) public virtual { //增加 virtual
x = _x;
}
}
contract B {
uint public y;
function setValue(uint _y) public virtual { //增加 virtual
y = _y;
}
}
contract C is A, B {
function setValue(uint _x) public override(A,B) { // 重写 setValue
A.setValue(_x);
}
}
本文实测,编译部署正常;
结论:只有标记为 virtual
的函数才可以重写它们。任何重写的函数都必须标记为override
。如果重写后依旧是可重写的,则仍然需要标记为 virtual
。
8.2 不再有状态变量遮蔽
在 0.5 版本编译器中允许继承具有相同名称的可见状态变量。
示例:
pragma solidity ^0.5.17;
contract A {
uint public x;
function setValue1(uint _x) public { x = _x; }
}
contract B is A {
uint public x;
function setValue2(uint _x) public { x = _x; }
}
在上面的例子中,A、B 各自有自己的 x
, 因此:
调用 B.setValue2(100)的结果将是将 B.x 设置为 100,而调用 B.setValue1(200)的设置将是将 A.x 设置为 200。
现在 0.6.x 版本禁止这种用法,并会引发编译器错误提示:
DeclarationError: Identifier already declared // 意思是变量已经声明
结论:合约继承时不要使用相同名称的可见状态变量。
8.3 接口可以继承
这个是 solidity 0.6 新增的功能,允许接口继承接口。
派生的接口是的所有接口函数的组合。实现合约必须实现的所有继承接口的函数。
示例:
//SPDX-License-Identifier: MIT
pragma solidity ^0.6.10;
interface X {
function setValue(uint _x) external;
}
interface Y is X {
function getValue() external returns (uint);
}
contract Z is Y {
uint x;
function setValue(uint _x) external override {
x = _x;
}
function getValue() external override returns (uint) {
return x;
}
}
注意:如果合约未实现所有函数,则必须将合约标记为 abstract
。
abstract contract Z is Y {
uint x;
function setValue(uint _x) external override { x = _x; }
}