文章目录

合约
继承
Solidity 支持多重继承,并且具有多态性特性。
多态性意味着函数调用(无论是内部调用还是外部调用)始终会执行继承体系中最底层合约中具有相同名称和参数类型的函数。为了启用多态性,必须在继承体系中的每个函数上显式使用 virtual 和 override 关键字。
我们可以通过显式指定合约名来调用继承体系中更高层次的函数,例如:ContractName.functionName(),或者使用 super.functionName() 来调用继承结构中的上一级函数(参考下文“扁平化继承结构”)。
当一个合约继承其他合约时,区块链上只会创建一个合约实例,所有父合约的代码会被编译到这个实例中。这意味着对父合约函数的所有内部调用实际上是普通的内部函数调用(例如,super.f(…) 会使用 JUMP 操作码,而不是消息调用)。
状态变量遮蔽(shadowing)会被视为错误:如果某个父合约中已经声明了变量 x,子合约中不能再声明同名变量。
Solidity 的继承系统在整体上类似于 Python 的继承,尤其是在处理多重继承时,但也存在一些差异。
具体示例如下:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Owned {
address payable owner;
constructor() { owner = payable(msg.sender); }
}
// 使用 `is` 关键字派生自另一个合约。
// 派生合约可以访问所有非 private 的成员,包括 internal 函数和状态变量。
// 这些成员无法通过 `this` 关键字从外部访问。
contract Emittable is Owned {
event Emitted();
// 使用 `virtual` 关键字表示该函数可以被子类重写。
function emitEvent() virtual public {
if (msg.sender == owner)
emit Emitted();
}
}
// 这些抽象合约只是为了让编译器知道接口。注意没有函数体的声明。
// 如果一个合约未实现所有函数,就只能作为接口使用。
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` 同时是 `Emittable` 的父类,
// 但在最终合约中只会有一个 `Owned` 实例(类似 C++ 的虚继承)。
contract Named is Owned, Emittable {
constructor(bytes32 name) {
Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
NameReg(config.lookup(1)).register(name);
}
// 函数可以通过相同的名称和参数类型进行重写。
// 如果返回值类型不一样,将导致编译错误。
// 无论是内部调用还是消息调用,都会考虑重写关系。
// 如果我们要重写函数,必须使用 `override` 关键字。
// 如果这个函数还需要被其他合约重写,则还需要再次声明 `virtual`。
function emitEvent() public virtual override {
if (msg.sender == owner) {
Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
NameReg(config.lookup(1)).unregister();
// 仍然可以调用被重写的父类函数。
Emittable.emitEvent();
}
}
}
// 如果构造函数需要参数,则必须在派生合约的构造器中提供,
// 可以在构造函数声明中传参,也可以通过修饰器风格传参。
contract PriceFeed is Owned, Emittable, Named("GoldFeed") {
uint info;
function updateInfo(uint newInfo) public {
if (msg.sender == owner) info = newInfo;
}
// 这里只声明 `override` 而不是 `virtual`,
// 意味着继承自 PriceFeed 的合约不能再重写 emitEvent。
function emitEvent() public override(Emittable, Named) {
Named.emitEvent();
}
function get() public view returns(uint r) { return info; }
}
上述代码中我们通过 Emittable.emitEvent() 的方式来“转发”事件触发请求。这种做法实际上是有问题的,原因如下例所示:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Owned {
address payable owner;
constructor() { owner = payable(msg.sender); }
}
contract Emittable is Owned {
event Emitted();
function emitEvent() virtual public {
if (msg.sender == owner) {
emit Emitted();
}
}
}
contract Base1 is Emittable {
event Base1Emitted();
function emitEvent() public virtual override {
/* 这里,我们触发一个事件来模拟 Base1 的逻辑 */
emit Base1Emitted();
Emittable.emitEvent();
}
}
contract Base2 is Emittable {
event Base2Emitted();
function emitEvent() public virtual override {
/* 这里,我们触发一个事件来模拟 Base2 的逻辑 */
emit Base2Emitted();
Emittable.emitEvent();
}
}
contract Final is Base1, Base2 {
event FinalEmitted();
function emitEvent() public override(Base1, Base2) {
/* 这里,我们触发一个事件来模拟 Final 的逻辑 */
emit FinalEmitted();
Base2.emitEvent();
}
}
调用 Final.emitEvent() 实际上会执行 Base2.emitEvent,因为在最终的重写中我们明确指定了它。但该调用会绕过 Base1.emitEvent,从而导致事件触发顺序为:
FinalEmitted -> Base2Emitted -> Emitted,而不是预期的:FinalEmitted -> Base2Emitted -> Base1Emitted -> Emitted。
正确的解决方式是使用 super 关键字:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Owned {
address payable owner;
constructor() { owner = payable(msg.sender); }
}
contract Emittable is Owned {
event Emitted();
function emitEvent() virtual public {
if (msg.sender == owner) {
emit Emitted();
}
}
}
contract Base1 is Emittable {
event Base1Emitted();
function emitEvent() public virtual override {
/* 这里,我们触发一个事件来模拟 Base1 的逻辑 */
emit Base1Emitted();
super.emitEvent();
}
}
contract Base2 is Emittable {
event Base2Emitted();
function emitEvent() public virtual override {
/* 这里,我们触发一个事件来模拟 Base2 的逻辑 */
emit Base2Emitted();
super.emitEvent();
}
}
contract Final is Base1, Base2 {
event FinalEmitted();
function emitEvent() public override(Base1, Base2) {
/* 这里,我们触发一个事件来模拟 Final 的逻辑 */
emit FinalEmitted();
super.emitEvent();
}
}
如果 Final 使用 super 调用某个函数,它并不会简单地调用某个特定父合约中的该函数。相反,super 会沿着最终继承图中的下一个父合约查找并调用对应的函数。因此,它实际上会调用 Base1.emitEvent()(注意最终的继承顺序是从最底层合约开始:Final → Base2 → Base1 → Emittable → Owned)。
在使用 super 时,具体被调用的函数在当前合约上下文中并不是静态确定的,尽管其类型是确定的。这种行为类似于常规的虚方法查找。
函数重写(Function Overriding)
基函数可以通过继承的合约进行重写,以修改其行为,前提是该函数被标记为 virtual。重写函数必须在函数签名中使用 override 关键字。
在重写时,函数的可见性只能从 external 放宽为 public。函数的可变性(mutability)也可以按照更严格的顺序进行调整,具体规则如下:
-
nonpayable 可以被重写为 view 或 pure;
-
view 可以被重写为 pure;
-
payable 是一个特例,不能被重写为其他可变性类型。
以下示例展示了如何在重写时更改可见性和可变性:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Base
{
function foo() virtual external view {}
}
contract Middle is Base {}
contract Inherited is Middle
{
function foo() override public pure {}
}
在多重继承中,如果多个基合约定义了同名函数,则最派生合约在重写该函数时,必须在 override 关键字后显式指定所有相关的基合约名称。
换句话说,必须列出所有定义了该函数,且在继承路径中尚未被其他基合约重写的合约。
此外,如果一个合约从多个不相关的基合约继承了同一个函数,也必须显式进行重写并明确指定重写的来源,如下例所示:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
contract Base1
{
function foo() virtual public {}
}
contract Base2
{
function foo() virtual public {}
}
contract Inherited is Base1, Base2
{
// 从多个基合约继承了 foo(),因此我们必须显式地重写它
function foo() public override(Base1, Base2) {}
}
如果某个函数是在一个公共的基合约中定义,或者该函数在某个公共基合约中已唯一地重写了所有其他同名函数,那么在派生合约中无需显式指定 override 的基合约名称:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
contract A { function f() public pure{} }
contract B is A {}
contract C is A {}
// 不需要显式的重写
contract D is B, C {}
更正式地说,如果存在某个基合约,它出现在所有重写路径中,并且满足以下任一条件:
-
该基合约实现了该函数,且从当前合约到该基合约的所有路径中没有其他合约声明了相同签名的函数;
-
该基合约未实现该函数,且从当前合约到该基合约的所有路径中至多只有一个合约声明了该签名的函数;
那么,在这种情况下,不需要在当前合约中显式重写从多个基合约继承而来的该函数,无论是直接还是间接继承。
在这种语义下,某个函数签名的“重写路径”指的是:从当前合约出发,沿继承图向上查找,直到遇到一个声明了该函数但未被重写的合约为止的路径。
如果被重写的函数未被标记为 virtual,则派生合约将无法改变其行为。
注意:
1.具有 private 可见性的函数不能被声明为 virtual。
2.没有函数体的函数(即未实现函数),在接口之外必须显式标记为 virtual;在接口中,所有函数默认都被视为 virtual。
3.自 Solidity 0.8.8 起,在重写接口函数时,如果该函数仅出现在一个接口中,则不需要使用 override 关键字。只有当多个基合约中都定义了该函数时,才需要显式地使用 override。
此外,公共状态变量可以重写一个具有相同参数和返回类型的外部函数,只要该函数的签名与该变量的 getter 函数匹配,如下例所示:
// 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;
}
注意:
虽然公共状态变量可以用来重写外部函数(前提是其 getter 的签名与函数匹配),但公共状态变量自身不能被重写。
修饰符重写(Modifier Overriding)
函数修饰符也可以被重写,其机制与函数重写类似(区别在于修饰符不支持重载)。被重写的修饰符必须标记为 virtual,而进行重写的修饰符则必须使用 override 关键字:
// 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 {_; }
}
在多重继承的情况下,所有直接基合约必须显式指定:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
contract Base1
{
modifier foo() virtual {_; }
}
contract Base2
{
modifier foo() virtual {_; }
}
contract Inherited is Base1, Base2
{
modifier foo() override(Base1, Base2) {_; }
}
构造函数(Constructors)
构造函数是一个可选函数,使用 constructor 关键字声明,并在合约创建时执行,用于运行合约的初始化代码。
在构造函数执行之前,如果状态变量在声明时已被初始化,它们会被赋予指定的初始值;否则,将使用默认值进行初始化。
构造函数执行完成后,合约的最终代码将被部署到区块链上。部署过程会消耗额外的 gas,且消耗量与代码长度呈线性关系。部署到链上的代码包含所有公共接口相关的部分,以及通过函数调用可以访问的所有函数,但不包括构造函数代码或仅在构造函数中调用的内部函数。
如果未显式定义构造函数,编译器将默认生成一个空构造函数,等同于 constructor() {}。例如:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
abstract contract A {
uint public a;
constructor(uint a_) {
a = a_;
}
}
contract B is A(1) {
constructor() {}
}
我们可以在构造函数中使用内部参数(如存储指针)。在这种情况下,合约必须标记为抽象合约,因为这些参数不能从外部赋值,只能通过派生合约的构造函数来赋值。
在版本 0.4.22 之前,构造函数是通过与合约同名的函数来定义的。这种语法已经被弃用,并且在 0.5.0 版本中不再被支持。
在版本 0.7.0 之前,构造函数的可见性必须显式指定为 internal 或 public。
基合约构造函数的参数
基合约的构造函数会按照合约线性化的规则依次调用。如果基合约的构造函数有参数,派生合约需要为每个基合约构造函数提供相应的参数。派生合约可以通过两种方式来完成这一操作:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Base {
uint x;
constructor(uint x_) { x = x_; }
}
// 方式一:直接在继承列表中指定...
contract Derived1 is Base(7) {
constructor() {}
}
// 或者通过派生构造函数中的“修饰符”来指定...
contract Derived2 is Base {
constructor(uint y) Base(y * y) {}
}
// 或者声明为抽象合约...
abstract contract Derived3 is Base {
}
// 由下一个具体的派生合约来初始化它。
contract DerivedFromDerived is Derived3 {
constructor() Base(10 + 10) {}
}
一种方式是在继承列表中直接指定参数(如 is Base(7))。另一种方式是在派生合约的构造函数中通过修饰符调用基合约构造函数(如 Base(y * y))。第一种方式较为简便,适用于构造函数的参数为常量且定义了合约的行为或描述。第二种方式则适用于基合约的构造函数参数依赖于派生合约的参数。需要注意,参数只能通过继承列表或修饰符的方式提供,不能两者同时指定,否则会导致错误。
如果派生合约没有为所有基合约的构造函数指定参数,它必须被声明为抽象合约。在这种情况下,当另一个合约继承时,继承列表或构造函数必须为所有尚未提供参数的基类提供所需的参数,否则该合约也必须声明为抽象合约。例如,在下面的代码片段中,可以看到 Derived3 和 DerivedFromDerived 的用法。
多重继承与线性化
允许多重继承的语言必须解决一些问题,其中之一就是钻石问题。Solidity 和 Python 类似,都使用 C3 线性化 来强制按照特定顺序排列基类的继承图(DAG)。C3 线性化保证了继承结构的单调性,但也限制了某些类型的继承图。特别的是,基类在 is 指令中的顺序非常重要:必须按照从“最基础”到“最派生”的顺序列出直接基合约。值得注意的是,这个顺序与 Python 中使用的顺序是相反的。
一种简化的方式来理解这一点是,当调用一个在多个合约中多次定义的函数时,Solidity 会从右到左(Python 中是从左到右)深度优先地搜索给定的基合约,直到找到第一个匹配的函数。如果某个合约已经被搜索过了,那么就会跳过该合约。
以下代码会导致 Solidity 报错:“Linearization of inheritance graph impossible”,因为存在继承冲突:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract X {}
contract A is X {}
// 这将无法编译
contract C is A, X {}
出现这种情况的原因是,合约 C 请求在继承中先覆盖 A(通过指定 A, X 这个顺序),但 A 本身又要求覆盖 X,这导致了一个无法解决的矛盾。
由于必须显式重写从多个基合约继承来的函数,而没有明确的重写时,C3 线性化在实际操作中并不会产生太大影响。
一个特别重要但可能不太容易理解的继承线性化应用场景是,当继承层次结构中包含多个构造函数时。构造函数总是按照继承图中的线性化顺序执行,尽管它们的参数在继承合约构造函数中的传递顺序可能不同,如下例所示:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Base1 {
constructor() {}
}
contract Base2 {
constructor() {}
}
// 构造函数按以下顺序执行:
// 1 - Base1
// 2 - Base2
// 3 - Derived1
contract Derived1 is Base1, Base2 {
constructor() Base1() Base2() {}
}
// 构造函数按以下顺序执行:
// 1 - Base2
// 2 - Base1
// 3 - Derived2
contract Derived2 is Base2, Base1 {
constructor() Base2() Base1() {}
}
// 构造函数仍然按以下顺序执行:
// 1 - Base2
// 2 - Base1
// 3 - Derived3
contract Derived3 is Base2, Base1 {
constructor() Base1() Base2() {}
}
继承相同名称的不同成员类型
由于多重继承的特性,Solidity 中有一些情况下多个合约可能会共享相同名称的定义,主要包括以下几种情况:
1.函数重载(Overloading of functions)
这意味着在同一个合约中,可以定义多个函数,它们具有相同的名称,但参数类型或数量不同。这是函数重载的一种表现形式,Solidity 允许这样做以增加函数的灵活性和可重用性。
2.虚函数重写(Overriding of virtual functions)
如果基合约中的函数被标记为 virtual,则派生合约可以通过 override 关键字重写该函数。当多个基合约定义了相同的虚拟函数时,派生合约可以选择性地重写这些函数,提供不同的实现方式。
3.外部虚函数由状态变量的 getter 重写(Overriding of external virtual functions by state variable getters)
外部虚函数可以由公共状态变量的 getter 函数重写。这意味着,如果合约中的状态变量被声明为 public,Solidity 会自动为其生成一个外部函数作为 getter,从而允许外部访问该变量。当基合约定义了虚函数和相应的 getter 函数时,派生合约的 getter 函数可以重写这些外部虚函数。
4.虚修饰符重写(Overriding of virtual modifiers)
虚修饰符(如 virtual)也可以在多个继承合约中被重写。修饰符的重写是通过 override 关键字实现的,允许派生合约修改父合约中的修饰符行为,以适应不同的需求。
5.事件重载(Overloading of events)
事件允许在不同的函数或合约中重载。虽然事件名称相同,但它们的参数类型和数量可以不同,从而实现事件的重载。这为合约提供了灵活性,允许相同事件在不同的上下文中触发不同的参数。

被折叠的 条评论
为什么被折叠?



