(八)合约的高级特性(完) | Word Count: 2.3k | Reading Time: 9mins | Post Views:
区块链基础 (一)初步理解智能合约 (二)代码结构和合约特性 (三)控制结构 (四)类型 (五)字面量和内置单位、函数 (六)应用二进制接口 (七)特殊函数 (八)合约的高级特性(完)
继承
继承的机制和 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 pragma solidity >=0.7 .0 <0.9 .0 ; contract Owned { constructor ( ) { owner = payable (msg.sender ); } address payable owner; } contract Destructible is Owned { 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; } contract Named is Owned , Destructible { constructor (bytes32 name ) { Config config = Config (0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970 ); NameReg (config.lookup (1 )).register (name); } 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; } 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 pragma solidity ^0.8 .10 ; 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 { 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 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 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 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 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 { Data knownValues; function register (uint value ) public { require (Set .insert (knownValues, value)); } }
库具有以下特性:
没有状态变量
不能够继承或被继承
不能接收以太币
不可以被销毁
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 { require (knownValues.insert (value)); } }
引用存储变量或者 internal 库调用 是唯一不会发生拷贝的情况。