实现继承的方式是通过复制包括多态的代码到子类来实现的。合约继承通过关键字 is
来实现。由于 Solidity 继承的实现方案是代码拷贝,所以合约继承后,部署到网络时,将变成一个合约,代码将从父类拷贝到子类中。
- 修饰符可以继承
- 事件不可以继承,但是可以重载
fallback
可以继承,但是需要保持原有的payable/nonpayable
receive
可以继承,但是需要保持原有的payable/nonpayable
当一个合约从多个合约继承时,在区块链上只有一个合约被创建,所有基类合约(或称为父合约)的代码被编译到创建的合约中。这意味着对基类合约函数的所有内部调用也只是使用内部函数调用(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
- 否则会报错:
子类可以访问父类的权限修饰符只有:public/internal
,不能是 external/private
。
- 如果父类的状态变量和函数是
private
和external
,则子类不可以继承和访问。- 如果子类调用父类
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.
// }
}
- 一个合约同时继承 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
函数。
solidity 引入了 abstract
, virtual
, override
几个关键字,用于重写函数。父合约标记为 virtual
函数可以在继承合约里重写(overridden)以更改他们的行为。重写的函数需要使用关键字 override
修饰。
继承的方法重写需要注意的点:
- 父合约方法需要标示为可修改,使用关键字
virtual
, - 子合约方法需要标示为覆盖,使用关键词
override
- 对于多重继承,如果有多个父合约有相同定义的函数, override 关键字后必须指定所有父合约名。
- 基础合约中可以包含没有实现代码的函数,也就是纯虚函数,那么基础合约必须声明为
abstract
。 - 继承多个合约时,所有同名的可修改函数都需要重写
- 继承后重写合约方法,各个合约内的函数可见性需要一致
- 可变性可以按照以下顺序更改为更严格的一种:
nonpayable
可以被view
和pure
覆盖。view
可以被pure
覆盖。payable
是一个例外,不能更改为任何其他可变性。
以下例子,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;
}
基础合约中可以包含没有实现代码的函数,也就是纯虚函数,那么基础合约必须声明为 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;
}
}
Solidity 语言的多重继承采用线性继承方式。继承顺序很重要,判断顺序的一个简单规则是按照从“最类似基类”到“最多派生”的顺序指定基类。
原则:先写基础合约,再写派生合约。
小技巧:可以先画继承逻辑图,然后按照从上到下,从左到右的顺序来写合约;
上面 virtual 和 override 的介绍例子中,合约继承逻辑图如下:
/**
A
|
B
|
C
*/
B 继承 A,C 继承 B。所以写的时候顺序是: A => B => C
/**
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";
}
}
/**
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
。
- 写合约 C 的时候,必须写成
/**
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";
}
}
/**
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".
继承的父合约,如果有构造函数并且需要传入参数,我们有以下几种方法进行参数传入
- 方法 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) {}
}
多重继承中,构造函数的执行会按照定义时的继承顺序进行,与构造函数中定义顺序无关。
原则: 构造函数的执行顺序按照继承的顺序。
例子:
- 如下是先执行 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) {}
}
例子如下:
// 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"
}
}
]
有两种方法可以调用
- 直接使用合约名调用
ParentContractName.functionName()
; - 使用 super 关键字
super.functionName()
- super 会自动寻找父合约,并执行对应的方法;
- 如果是多个父级,那么父级都会执行。但有时候又不会,执行顺序的原理,这些需要详细的了解
- 如果 super 导致 2 个父级同时触发同一个爷爷合约的相同方法;则爷爷的方法只执行一次。一个合约的同一个方法只会执行一次,不会执行多次。
执行顺序:像水中的冒泡一样,由下向上进行执行。
// 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
执行顺序:像水中的冒泡一样,由下向上进行执行。
// 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 方式就像一个疯子一样没有规律可言。我们能做的就是避开使用它。
写一个如下逻辑的继承
/**
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
可以被view
和pure
覆盖。view
可以被pure
覆盖。payable
是一个例外,不能更改为任何其他可变性。
- solidity 引入了
- 多级继承的代码书写顺序?
- 按照从“最类似基类”到“最多派生”的顺序指定基类。
- 原则:先写基础合约,再写派生合约。
- 小技巧:可以先画继承逻辑图,然后按照从上到下,从左到右的顺序来写合约;
- 继承中两种构造函数传参方式。
- 方法 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) {} }
- 方法 1: 固定值传参。(该方式不能在部署时动态输入)。
- 继承中构造函数的执行顺序
- 多重继承中,构造函数的执行会按照定义时的继承顺序进行,与构造函数中定义顺序无关。原则: 构造函数的执行顺序按照继承的顺序。
- 如下是先执行 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) {} }
- 子合约调用父合约的方法
- 直接使用合约名调用
ParentContractName.functionName()
; - 使用 super 关键字
super.functionName()
- super 会自动寻找父合约,并执行对应的方法;
- 如果是多个父级,那么父级都会执行。但有时候又不会,执行顺序的原理,这些需要详细的了解
- 如果 super 导致 2 个父级同时触发同一个爷爷合约的相同方法;则爷爷的方法只执行一次。一个合约的同一个方法只会执行一次,不会执行多次。
- 执行顺序:像水中的冒泡一样,由下向上进行执行。但是如果多层级的执行,顺序规律还没有找到规律。
- 直接使用合约名调用
- 聊一聊合约继承
- 以上的内容精简回答
- 修饰符可以继承
- 事件不可以继承,但是可以重载
fallback
可以继承,但是需要保持原有的payable/nonpayable
receive
可以继承,但是需要保持原有的payable/nonpayable