繼承

2022-05-16 10:47 更新

Solidity 支持多重繼承,包括多態(tài)性。

多態(tài)性意味著函數(shù)調(diào)用(內(nèi)部和外部)總是在繼承層次結(jié)構(gòu)中最派生的合約中執(zhí)行同名(和參數(shù)類型)的函數(shù)。這必須使用virtualandoverride關(guān)鍵字在層次結(jié)構(gòu)中的每個(gè)函數(shù)上顯式啟用。有關(guān)更多詳細(xì)信息,請(qǐng)參閱函數(shù)覆蓋。

如果您想在扁平繼承層次結(jié)構(gòu)中調(diào)用更高一級(jí)的函數(shù)(見下文),則可以通過(guò)顯式指定合約 usingContractName.functionName()或 using在內(nèi)部調(diào)用繼承層次結(jié)構(gòu)中的函數(shù)。super.functionName()

當(dāng)一個(gè)合約繼承自其他合約時(shí),區(qū)塊鏈上只創(chuàng)建一個(gè)合約,所有基礎(chǔ)合約的代碼都編譯到創(chuàng)建的合約中。這意味著對(duì)基礎(chǔ)合約函數(shù)的所有內(nèi)部調(diào)用也只使用內(nèi)部函數(shù)調(diào)用(super.f(..)將使用 JUMP 而不是消息調(diào)用)。

狀態(tài)變量遮蔽被視為錯(cuò)誤。一個(gè)派生合約只能聲明一個(gè)狀態(tài)變量x,前提是在它的任何基礎(chǔ)中都沒(méi)有同名的可見狀態(tài)變量。

通用的繼承系統(tǒng)和Python 的非常相似 ,尤其是在多重繼承方面,但也有一些區(qū)別。

以下示例中給出了詳細(xì)信息。

// 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;
}

請(qǐng)注意,上面我們調(diào)用Destructible.destroy()“轉(zhuǎn)發(fā)”銷毀請(qǐng)求。這樣做的方式是有問(wèn)題的,如下例所示:

// 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(); }
}

調(diào)用Final.destroy()將調(diào)用Base2.destroy,因?yàn)槲覀冊(cè)谧罱K覆蓋中明確指定它,但此函數(shù)將繞過(guò) Base1.destroy. 解決這個(gè)問(wèn)題的方法是使用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調(diào)用 的函數(shù)super,它不會(huì)簡(jiǎn)單地在其基礎(chǔ)合約之一上調(diào)用此函數(shù)。相反,它會(huì)在最終繼承圖中的下一個(gè)基礎(chǔ)合約上調(diào)用此函數(shù),因此它將調(diào)用Base1.destroy()(請(qǐng)注意,最終繼承順序是 - 從最衍生的合約開始:Final、Base2、Base1、Destructible、owned)。使用 super 時(shí)調(diào)用的實(shí)際函數(shù)在使用它的類的上下文中是未知的,盡管它的類型是已知的。這與普通的虛擬方法查找類似。

函數(shù)覆蓋?

如果標(biāo)記為 ,則可以通過(guò)繼承合同來(lái)更改其行為來(lái)覆蓋基本功能virtual。然后,覆蓋函數(shù)必須override在函數(shù)頭中使用關(guān)鍵字。重寫函數(shù)只能將重寫函數(shù)的可見性從 更改externalpublic??勺冃钥梢园凑找韵马樞蚋臑楦鼑?yán)格的: nonpayable可以被viewand覆蓋pure。view可以被 覆蓋pure。 payable是一個(gè)例外,不能更改為任何其他可變性。

以下示例演示了不斷變化的可變性和可見性:

// 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 {}
}

對(duì)于多重繼承,定義相同函數(shù)的最衍生的基礎(chǔ)合約必須在override關(guān)鍵字之后顯式指定。換句話說(shuō),您必須指定所有定義相同功能且尚未被另一個(gè)基礎(chǔ)合約覆蓋的基礎(chǔ)合約(在通過(guò)繼承圖的某個(gè)路徑上)。此外,如果合約從多個(gè)(不相關(guān)的)基礎(chǔ)繼承相同的功能,它必須顯式覆蓋它:

// 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) {}
}

如果函數(shù)是在通用基礎(chǔ)合約中定義的,或者如果通用基礎(chǔ)合約中有一個(gè)唯一函數(shù)已經(jīng)覆蓋了所有其他函數(shù),則不需要顯式覆蓋說(shuō)明符。

// 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 {}

更正式地說(shuō),如果有一個(gè)基礎(chǔ)合約是簽名的所有覆蓋路徑的一部分,并且(1)該基礎(chǔ)實(shí)現(xiàn)該函數(shù)并且沒(méi)有來(lái)自多個(gè)基礎(chǔ)的路徑,則不需要重寫從多個(gè)基礎(chǔ)繼承的函數(shù)(直接或間接)當(dāng)前與基礎(chǔ)的合約提到了具有該簽名的函數(shù),或者 (2) 該基礎(chǔ)沒(méi)有實(shí)現(xiàn)該功能,并且在從當(dāng)前合約到該基礎(chǔ)的所有路徑中最多有一次提及該功能。

從這個(gè)意義上說(shuō),簽名的覆蓋路徑是通過(guò)繼承圖的路徑,該路徑從所考慮的合同開始,到提及具有該簽名的未覆蓋功能的合同結(jié)束。

如果您不將覆蓋的函數(shù)標(biāo)記為virtual,則派生合約將無(wú)法再更改該函數(shù)的行為。

筆記

具有private可見性的函數(shù)不能virtual。

筆記

沒(méi)有實(shí)現(xiàn)的函數(shù)必須virtual 在接口之外標(biāo)記。在接口中,所有功能都被自動(dòng)考慮virtual。

筆記

從 Solidity 0.8.8 開始,override重寫接口函數(shù)時(shí)不需要關(guān)鍵字,除非函數(shù)在多個(gè)基中定義。

如果函數(shù)的參數(shù)和返回類型與變量的 getter 函數(shù)匹配,則公共狀態(tài)變量可以覆蓋外部函數(shù):

// 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;
}

筆記

雖然公共狀態(tài)變量可以覆蓋外部函數(shù),但它們本身不能被覆蓋。

修改器覆蓋?

函數(shù)修飾符可以相互覆蓋。這與函數(shù)覆蓋的工作方式相同 (除了修飾符沒(méi)有重載)。virtual必須在覆蓋修飾符上使用關(guān)鍵字 ,并且override必須在覆蓋修飾符中使用關(guān)鍵字:

// 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 {_;}
}

在多重繼承的情況下,必須明確指定所有直接基礎(chǔ)合約:

// 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) {_;}
}

構(gòu)造函數(shù)?

構(gòu)造函數(shù)是使用constructor關(guān)鍵字聲明的可選函數(shù),在創(chuàng)建合約時(shí)執(zhí)行,您可以在其中運(yùn)行合約初始化代碼。

在執(zhí)行構(gòu)造函數(shù)代碼之前,如果內(nèi)聯(lián)初始化狀態(tài)變量,則將其初始化為其指定值,否則將其初始化為默認(rèn)值

構(gòu)造函數(shù)運(yùn)行后,合約的最終代碼將部署到區(qū)塊鏈。代碼的部署成本與代碼長(zhǎng)度成線性關(guān)系。此代碼包括作為公共接口一部分的所有函數(shù)以及可通過(guò)函數(shù)調(diào)用從那里訪問(wèn)的所有函數(shù)。它不包括僅從構(gòu)造函數(shù)調(diào)用的構(gòu)造函數(shù)代碼或內(nèi)部函數(shù)。

如果沒(méi)有構(gòu)造函數(shù),合約將假定默認(rèn)構(gòu)造函數(shù),相當(dāng)于. 例如: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() {}
}

您可以在構(gòu)造函數(shù)中使用內(nèi)部參數(shù)(例如存儲(chǔ)指針)。在這種情況下,必須將合約標(biāo)記為abstract,因?yàn)檫@些參數(shù)不能從外部分配有效值,而只能通過(guò)派生合約的構(gòu)造函數(shù)分配。

警告

在 0.4.22 版本之前,構(gòu)造函數(shù)被定義為與合約同名的函數(shù)。此語(yǔ)法已被棄用,并且在 0.5.0 版中不再允許使用。

警告

在 0.7.0 版本之前,您必須將構(gòu)造函數(shù)的可見性指定為 internalpublic。

基本構(gòu)造函數(shù)的參數(shù)?

所有基礎(chǔ)合約的構(gòu)造函數(shù)都將按照下面解釋的線性化規(guī)則進(jìn)行調(diào)用。如果基本構(gòu)造函數(shù)有參數(shù),則派生合約需要指定所有參數(shù)。這可以通過(guò)兩種方式完成:

// 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) {}
}

一種方法是直接在繼承列表 ( ) 中。另一個(gè)是作為派生構(gòu)造函數(shù)()的一部分調(diào)用修飾符的方式。如果構(gòu)造函數(shù)參數(shù)是一個(gè)常量并定義合約的行為或描述它,那么第一種方法會(huì)更方便。如果 base 的構(gòu)造函數(shù)參數(shù)依賴于派生合約的參數(shù),則必須使用第二種方法。參數(shù)必須在繼承列表或派生構(gòu)造函數(shù)的修飾符樣式中給出。在這兩個(gè)地方指定參數(shù)是錯(cuò)誤的。is Base(7)Base(y * y)

如果派生合約沒(méi)有為其所有基礎(chǔ)合約的構(gòu)造函數(shù)指定參數(shù),則必須將其聲明為抽象的。在這種情況下,當(dāng)另一個(gè)合約派生自它時(shí),該其他合約的繼承列表或構(gòu)造函數(shù)必須為所有未指定其參數(shù)的基類提供必要的參數(shù)(否則,該其他合約也必須聲明為抽象)。例如,在上面的代碼片段中,請(qǐng)參見Derived3DerivedFromDerived。

多重繼承和線性化?

允許多重繼承的語(yǔ)言必須處理幾個(gè)問(wèn)題。一是鉆石問(wèn)題。Solidity 類似于 Python,因?yàn)樗褂谩?nbsp;C3 線性化”來(lái)強(qiáng)制基類的有向無(wú)環(huán)圖 (DAG) 中的特定順序。這導(dǎo)致了理想的單調(diào)性屬性,但不允許某些繼承圖。特別是,指令中基類的順序is很重要:您必須按照從“最基類”到“最衍生”的順序??列出直接基類合約。請(qǐng)注意,此順序與 Python 中使用的順序相反。

解釋這一點(diǎn)的另一種簡(jiǎn)化方法是,當(dāng)調(diào)用在不同合約中多次定義的函數(shù)時(shí),以深度優(yōu)先的方式從右到左(在 Python 中從左到右)搜索給定的堿基,在第一次匹配時(shí)停止. 如果已經(jīng)搜索了基本合約,則跳過(guò)它。

在下面的代碼中,Solidity 將給出錯(cuò)誤“繼承圖的線性化不可能”。

// 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請(qǐng)求X覆蓋A (通過(guò)按此順序指定),但本身請(qǐng)求覆蓋,這是無(wú)法解決的矛盾。A, XAX

由于您必須顯式覆蓋從多個(gè)基類繼承的函數(shù)而無(wú)需唯一覆蓋,因此 C3 線性化在實(shí)踐中并不太重要。

繼承線性化特別重要但可能不太清楚的一個(gè)領(lǐng)域是在繼承層次結(jié)構(gòu)中有多個(gè)構(gòu)造函數(shù)時(shí)。構(gòu)造函數(shù)將始終以線性化順序執(zhí)行,而不管繼承合約的構(gòu)造函數(shù)中提供它們的參數(shù)的順序如何。例如:

// 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() {}
}

繼承不同種類的同名成員?

如果合約中的以下任何一對(duì)由于繼承而具有相同的名稱,則為錯(cuò)誤:
  • 一個(gè)函數(shù)和一個(gè)修飾符

  • 一個(gè)函數(shù)和一個(gè)事件

  • 事件和修飾符

作為一個(gè)例外,狀態(tài)變量 getter 可以覆蓋外部函數(shù)。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)