阅读(215) (14)

继承

2022-05-13 11:11:47 更新

Solidity 支持多重继承,包括多态性。

多态性意味着函数调用(内部和外部)总是在继承层次结构中最派生的合约中执行同名(和参数类型)的函数。这必须使用virtualandoverride关键字在层次结构中的每个函数上显式启用。有关更多详细信息,请参阅函数覆盖

如果您想在扁平继承层次结构中调用更高一级的函数(见下文),则可以通过显式指定合约 usingContractName.functionName()或 using在内部调用继承层次结构中的函数。super.functionName()

当一个合约继承自其他合约时,区块链上只创建一个合约,所有基础合约的代码都编译到创建的合约中。这意味着对基础合约函数的所有内部调用也只使用内部函数调用(super.f(..)将使用 JUMP 而不是消息调用)。

状态变量遮蔽被视为错误。一个派生合约只能声明一个状态变量x,前提是在它的任何基础中都没有同名的可见状态变量。

通用的继承系统和Python 的非常相似 ,尤其是在多重继承方面,但也有一些区别

以下示例中给出了详细信息。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;


contract Owned {
    constructor() { owner = payable(msg.sender); }
    address payable owner;
}


// Use `is` to derive from another contract. Derived
// contracts can access all non-private members including
// internal functions and state variables. These cannot be
// accessed externally via `this`, though.
contract Destructible is Owned {
    // The keyword `virtual` means that the function can change
    // its behaviour in derived classes ("overriding").
    function destroy() virtual public {
        if (msg.sender == owner) selfdestruct(owner);
    }
}


// These abstract contracts are only provided to make the
// interface known to the compiler. Note the function
// without body. If a contract does not implement all
// functions it can only be used as an interface.
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;
}


// Multiple inheritance is possible. Note that `Owned` is
// also a base class of `Destructible`, yet there is only a single
// instance of `Owned` (as for virtual inheritance in C++).
contract Named is Owned, Destructible {
    constructor(bytes32 name) {
        Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
        NameReg(config.lookup(1)).register(name);
    }

    // Functions can be overridden by another function with the same name and
    // the same number/types of inputs.  If the overriding function has different
    // types of output parameters, that causes an error.
    // Both local and message-based function calls take these overrides
    // into account.
    // If you want the function to override, you need to use the
    // `override` keyword. You need to specify the `virtual` keyword again
    // if you want this function to be overridden again.
    function destroy() public virtual override {
        if (msg.sender == owner) {
            Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
            NameReg(config.lookup(1)).unregister();
            // It is still possible to call a specific
            // overridden function.
            Destructible.destroy();
        }
    }
}


// If a constructor takes an argument, it needs to be
// provided in the header or modifier-invocation-style at
// the constructor of the derived contract (see below).
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 payable owner;
}

contract Destructible is owned {
    function destroy() public virtual {
        if (msg.sender == owner) selfdestruct(owner);
    }
}

contract Base1 is Destructible {
    function destroy() public virtual override { /* do cleanup 1 */ Destructible.destroy(); }
}

contract Base2 is Destructible {
    function destroy() public virtual override { /* do cleanup 2 */ Destructible.destroy(); }
}

contract Final is Base1, Base2 {
    function destroy() public override(Base1, Base2) { Base2.destroy(); }
}

调用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 payable owner;
}

contract Destructible is owned {
    function destroy() virtual public {
        if (msg.sender == owner) selfdestruct(owner);
    }
}

contract Base1 is Destructible {
    function destroy() public virtual override { /* do cleanup 1 */ super.destroy(); }
}


contract Base2 is Destructible {
    function destroy() public virtual override { /* do cleanup 2 */ super.destroy(); }
}

contract Final is Base1, Base2 {
    function destroy() public override(Base1, Base2) { super.destroy(); }
}

如果Base2调用 的函数super,它不会简单地在其基础合约之一上调用此函数。相反,它会在最终继承图中的下一个基础合约上调用此函数,因此它将调用Base1.destroy()(请注意,最终继承顺序是 - 从最衍生的合约开始:Final、Base2、Base1、Destructible、owned)。使用 super 时调用的实际函数在使用它的类的上下文中是未知的,尽管它的类型是已知的。这与普通的虚拟方法查找类似。

函数覆盖

如果标记为 ,则可以通过继承合同来更改其行为来覆盖基本功能virtual。然后,覆盖函数必须override在函数头中使用关键字。重写函数只能将重写函数的可见性从 更改externalpublic。可变性可以按照以下顺序更改为更严格的: nonpayable可以被viewand覆盖pureview可以被 覆盖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
{
    // Derives from multiple bases defining foo(), so we must explicitly
    // override it
    function foo() public override(Base1, Base2) {}
}

如果函数是在通用基础合约中定义的,或者如果通用基础合约中有一个唯一函数已经覆盖了所有其他函数,则不需要显式覆盖说明符。

// 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 {}
// No explicit override required
contract D is B, C {}

更正式地说,如果有一个基础合约是签名的所有覆盖路径的一部分,并且(1)该基础实现该函数并且没有来自多个基础的路径,则不需要重写从多个基础继承的函数(直接或间接)当前与基础的合约提到了具有该签名的函数,或者 (2) 该基础没有实现该功能,并且在从当前合约到该基础的所有路径中最多有一次提及该功能。

从这个意义上说,签名的覆盖路径是通过继承图的路径,该路径从所考虑的合同开始,到提及具有该签名的未覆盖功能的合同结束。

如果您不将覆盖的函数标记为virtual,则派生合约将无法再更改该函数的行为。

笔记

具有private可见性的函数不能virtual

笔记

没有实现的函数必须virtual 在接口之外标记。在接口中,所有功能都被自动考虑virtual

笔记

从 Solidity 0.8.8 开始,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;
}

笔记

虽然公共状态变量可以覆盖外部函数,但它们本身不能被覆盖。

修改器覆盖

函数修饰符可以相互覆盖。这与函数覆盖的工作方式相同 (除了修饰符没有重载)。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) {_;}
}

构造函数

构造函数是使用constructor关键字声明的可选函数,在创建合约时执行,您可以在其中运行合约初始化代码。

在执行构造函数代码之前,如果内联初始化状态变量,则将其初始化为其指定值,否则将其初始化为默认值

构造函数运行后,合约的最终代码将部署到区块链。代码的部署成本与代码长度成线性关系。此代码包括作为公共接口一部分的所有函数以及可通过函数调用从那里访问的所有函数。它不包括仅从构造函数调用的构造函数代码或内部函数。

如果没有构造函数,合约将假定默认构造函数,相当于. 例如: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() {}
}

您可以在构造函数中使用内部参数(例如存储指针)。在这种情况下,必须将合约标记为abstract,因为这些参数不能从外部分配有效值,而只能通过派生合约的构造函数分配。

警告

在 0.4.22 版本之前,构造函数被定义为与合约同名的函数。此语法已被弃用,并且在 0.5.0 版中不再允许使用。

警告

在 0.7.0 版本之前,您必须将构造函数的可见性指定为 internalpublic

基本构造函数的参数

所有基础合约的构造函数都将按照下面解释的线性化规则进行调用。如果基本构造函数有参数,则派生合约需要指定所有参数。这可以通过两种方式完成:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

contract Base {
    uint x;
    constructor(uint x_) { x = x_; }
}

// Either directly specify in the inheritance list...
contract Derived1 is Base(7) {
    constructor() {}
}

// or through a "modifier" of the derived constructor...
contract Derived2 is Base {
    constructor(uint y) Base(y * y) {}
}

// or declare abstract...
abstract contract Derived3 is Base {
}

// and have the next concrete derived contract initialize it.
contract DerivedFromDerived is Derived3 {
    constructor() Base(10 + 10) {}
}

一种方法是直接在继承列表 ( ) 中。另一个是作为派生构造函数()的一部分调用修饰符的方式。如果构造函数参数是一个常量并定义合约的行为或描述它,那么第一种方法会更方便。如果 base 的构造函数参数依赖于派生合约的参数,则必须使用第二种方法。参数必须在继承列表或派生构造函数的修饰符样式中给出。在这两个地方指定参数是错误的。is Base(7)Base(y * y)

如果派生合约没有为其所有基础合约的构造函数指定参数,则必须将其声明为抽象的。在这种情况下,当另一个合约派生自它时,该其他合约的继承列表或构造函数必须为所有未指定其参数的基类提供必要的参数(否则,该其他合约也必须声明为抽象)。例如,在上面的代码片段中,请参见Derived3DerivedFromDerived

多重继承和线性化

允许多重继承的语言必须处理几个问题。一是钻石问题。Solidity 类似于 Python,因为它使用“ C3 线性化”来强制基类的有向无环图 (DAG) 中的特定顺序。这导致了理想的单调性属性,但不允许某些继承图。特别是,指令中基类的顺序is很重要:您必须按照从“最基类”到“最衍生”的顺序​​列出直接基类合约。请注意,此顺序与 Python 中使用的顺序相反。

解释这一点的另一种简化方法是,当调用在不同合约中多次定义的函数时,以深度优先的方式从右到左(在 Python 中从左到右)搜索给定的碱基,在第一次匹配时停止. 如果已经搜索了基本合约,则跳过它。

在下面的代码中,Solidity 将给出错误“继承图的线性化不可能”。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;

contract X {}
contract A is X {}
// This will not compile
contract C is A, X {}

这样做的原因是C请求X覆盖A (通过按此顺序指定),但本身请求覆盖,这是无法解决的矛盾。A, XAX

由于您必须显式覆盖从多个基类继承的函数而无需唯一覆盖,因此 C3 线性化在实践中并不太重要。

继承线性化特别重要但可能不太清楚的一个领域是在继承层次结构中有多个构造函数时。构造函数将始终以线性化顺序执行,而不管继承合约的构造函数中提供它们的参数的顺序如何。例如:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

contract Base1 {
    constructor() {}
}

contract Base2 {
    constructor() {}
}

// Constructors are executed in the following order:
//  1 - Base1
//  2 - Base2
//  3 - Derived1
contract Derived1 is Base1, Base2 {
    constructor() Base1() Base2() {}
}

// Constructors are executed in the following order:
//  1 - Base2
//  2 - Base1
//  3 - Derived2
contract Derived2 is Base2, Base1 {
    constructor() Base2() Base1() {}
}

// Constructors are still executed in the following order:
//  1 - Base2
//  2 - Base1
//  3 - Derived3
contract Derived3 is Base2, Base1 {
    constructor() Base1() Base2() {}
}

继承不同种类的同名成员

如果合约中的以下任何一对由于继承而具有相同的名称,则为错误:
  • 一个函数和一个修饰符

  • 一个函数和一个事件

  • 事件和修饰符

作为一个例外,状态变量 getter 可以覆盖外部函数。