继承
继承的机制和 python 的非常相似,但是存在差异。一般而言使用过 C++, 基本已经掌握。
当合约继承其他的合约时,只会在区块链上生成一个合约,所有相关的合约都会编译进这个合约,调用机制和写在一个合约上一致。
继承时,全局变量无法覆盖,如果出现可见的同名变量会编译错误。通过例子来体会细节,重点理解语法,而不是程序逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; contract Owned { constructor() { owner = payable(msg.sender); }//构造函数中的msg.sender 是部署者 address payable owner; } // `is` 是继承的关键词. 子合约可以接受父合约所有非 private 的东西. contract Destructible is Owned { // `virtual` 表示函数可以被重写 function destroy() virtual public { if (msg.sender == owner) selfdestruct(owner);//只有调用函数的人是部署者,才能执行自毁操作 } } // abstract用于提取合约的 接口,重写后实现更多的功能 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; } // 允许从多个合约继承. contract Named is Owned, Destructible { constructor(bytes32 name) { Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);//从地址创建 满足接口的 Condig 合约实例,用于调用 NameReg(config.lookup(1)).register(name);//这里并未重写 lookup函数,因此返回值都是默认零值,这里创建0地址上的NameReg合约实例,然后注册管理者 } // 将重写的函数需要使用overridden的标识,并且被重写的函数之前有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; } // 如果从多个合约继承了同名的可重写函数,需要在override后面指明所有同名函数所在的合约。 function destroy() public override(Destructible, Named) { Named.destroy(); } function get() public view returns(uint r) { return info; } uint info; } |
但是,继承是从右到左深度优先搜索来寻找同名函数(搜索的顺序是按 ”辈分“ 从小到大,而且继承多个合约时也要按着从右到左的顺序填上,如下图继承链是 D, C, B, A),一旦找到同名函数就停止,不会执行后面重复出现的重名函数。所以如果继承了多个合约,希望把上一级父合约的同名函数都执行一遍,就需要 super
关键词。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
// SPDX-License-Identifier: MIT pragma solidity ^0.8.10; /* Inheritance tree A / \ B C \ / D */ contract A { event Log(string message); function foo() public virtual { emit Log("A.foo called"); } function bar() public virtual { emit Log("A.bar called"); } } contract B is A { function foo() public virtual override { emit Log("B.foo called"); A.foo(); } function bar() public virtual override { emit Log("B.bar called"); super.bar(); } } contract C is A { function foo() public virtual override { emit Log("C.foo called"); A.foo(); } function bar() public virtual override { emit Log("C.bar called"); super.bar(); } } contract D is B, C { // Try: // - Call D.foo and check the transaction logs. // Although D inherits A, B and C, it only called C and then A. // - Call D.bar and check the transaction logs // D called C, then B, and finally A. // Although super was called twice (by B and C) it only called A once. function foo() public override(B, C) { super.foo(); } function bar() public override(B, C) { super.bar(); } } |
更多的介绍请见官方文档。
函数重写
父合约中被标记为virtual
的非 private 函数可以在子合约中用override
重写。
重写可以改变函数的标识符,规则如下:
- 可见性只能单向从
external
更改为public。
nonpayable
可以被view
和pure
覆盖。view
可以被pure
覆盖。payable
不可被覆盖。
如果有多个父合约有相同定义的函数, override
关键字后必须指定所有父合约的名字,且这些父合约没有被继承链上的其他合约重写。
接口会自动作为 virtual
。
注意:特殊的,如果 external
函数的参数和返回值和 public
全局变量一致的话,可以把函数重写全局变量。
1 2 3 4 5 6 7 8 9 10 11 12 |
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.0 <0.9.0; contract A { function f() external view virtual returns(uint) { return 5; } } contract B is A { uint public override f; } |
注意:函数修饰器也支持重写,且和函数重写规则一致。
1 2 3 4 5 6 7 8 9 10 11 12 |
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.0 <0.9.0; contract Base { modifier foo() virtual {_;} } contract Inherited is Base { modifier foo() override {_;} } |
抽象合约
如果合约至少有一个函数没有完成 (例如:function foo(address) external returns (address);
),则该合约会被视为抽象合约,需要用 abstract
标明。
1 2 3 4 5 6 7 8 9 10 |
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.0 <0.9.0; abstract contract Feline { function utterance() public pure virtual returns (bytes32); } contract Cat is Feline { function utterance() public pure override returns (bytes32) { return "miaow"; } } |
如果子合约没有重写父合约中所有未完成的函数,那么子合约也需要标注abstract
注意:声明函数类型的变量和未实现的函数的不同:
1 2 |
function(address) external returns (address) foo;//函数类型变量 function foo(address) external returns (address);//抽象合约的函数 |
抽象合约可以将定义合约和实现合约的过程分离开,具有更佳的可拓展性。
接口
接口和抽象合约的作用很类似,但是它的每一个函数都没有实现,而且不可以作为其他合约的子合约,只能作为父合约被继承。
接口中所有的函数必须是external
,且不包含构造函数和全局变量。接口的所有函数都会隐式标记为external
,可以重写。多次重写的规则和多继承的规则和一般函数重写规则一致。
1 2 3 4 5 6 7 |
pragma solidity >=0.6.2 <0.9.0; interface Token { enum TokenType { Fungible, NonFungible } struct Coin { string obverse; string reverse; } function transfer(address recipient, uint amount) external; } |
库
库与合约类似,但是它们只在某个合约地址部署一次,并且通过 EVM 的DELEGATECALL
(为了实现上下文更改)来实现复用。
当库中的函数被调用时,它的代码在当前合约的上下文中执行,并且只可以访问调用时显式提供的调用合约的状态变量。库本身没有状态记录(如 全局变量)。
如果库被继承的话,库函数在子合约是可见的,也可以直接使用,和普通的继承相同(属于库的内部调用方式)。为了改变状态,内部的库(即不是通过地址引入的库)所有data a rea
的传参需要都是传递一个引用(库函数使用storage
标识),在 EVM 中,编译也是直接把库包含进调用合约。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.0 <0.9.0; struct Data { mapping(uint => bool) flags; } library Set { // 注意到这里使用的是storage引用类型 function insert(Data storage self, uint value) public returns (bool) { if (self.flags[value]) return false; // 如果已经存在停止插入 self.flags[value] = true; return true; } function remove(Data storage self, uint value) public returns (bool) { if (!self.flags[value]) return false; // 如果不存在就比用移除 self.flags[value] = false; return true; } function contains(Data storage self, uint value) public view returns (bool) { return self.flags[value]; } } contract C { Data knownValues; function register(uint value) public { require(Set.insert(knownValues, value)); } // In this contract, we can also directly access knownValues.flags, if we want. } |
库具有以下特性:
- 没有状态变量
- 不能够继承或被继承
- 不能接收以太币
- 不可以被销毁
Using For
using A for B;
可用于附加库函数(从库 A
)到任何类型(B
)
using A for *;
的效果是,库 A
中的函数被附加在任意的类型上,这个类型可以使用 A 内的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
pragma solidity >=0.6.0 <0.9.0; // 这是和之前一样的代码,只是没有注释。 struct Data { mapping(uint => bool) flags; } library Set { function insert(Data storage self, uint value) public returns (bool) { if (self.flags[value]) return false; // 已经存在 self.flags[value] = true; return true; } function remove(Data storage self, uint value) public returns (bool) { if (!self.flags[value]) return false; // 不存在 self.flags[value] = false; return true; } function contains(Data storage self, uint value) public view returns (bool) { return self.flags[value]; } } contract C { using Set for Data; // 这里是关键的修改 Data knownValues; function register(uint value) public { // Here, all variables of type Data have // corresponding member functions. // The following function call is identical to // `Set.insert(knownValues, value)` // 这里, Data 类型的所有变量都有与之相对应的成员函数。 // 下面的函数调用和 `Set.insert(knownValues, value)` 的效果完全相同。 require(knownValues.insert(value)); } } |
引用存储变量或者 internal 库调用 是唯一不会发生拷贝的情况。