Now and Nawoo

NFTの制作記録、技術メモ → C#, Solidity, Blockchain, Bitcoin, Ethereum, NFT

Solidityメモ: 多重継承

Solidityの多重継承がよくわからなかったので整理。 (C#には多重継承が無いので。。。)

多重継承

contract A {}
contract B {}
contract C is A {} // これは普通の継承
contract D is A, B {} // これが多重継承

ダイヤモンド継承

contract A {}
contract B is A {}
contract C is A {}
contract D is B, C {} // この場合 A-B-C-D の順番
contract E is C, B {} // この場合 A-C-B-E の順番

この順番は、superでひとつ上の関数を呼び出す時に重要になる。

Multiple Inheritance and Linearization(多重継承と線形化) — Solidity 0.8.7 documentation

親と祖父を継承

contract A {}
contract B is A {}
contract C is A, B {}
contract C is B, A {} // これはエラー (ベースクラスを先に書く必要がある)

親(B)と祖父(A)の両方を継承しているパターン。 親だけでいいんじゃないのと思うのだがよく見かける。何か意味があるのだろうか? contract MyNFT is ERC721, ERC721Enumerable {} とか

virtualとoverride

関数を子でオーバーライドさせたいなら、関数にvirtualを付ける。 オーバーライドした方は、overrideを付ける。

親の関数をオーバーライドしつつ、子にオーバーライドさせたいなら、virtual override と書く。 ここはC#とは違うところなので注意。(C#ならoverrideだけでよい)

contract A {
    function hoge() public virtual {}
}
contract B is A {
    function hoge() public virtual override {}
}
contract C is B {
    function hoge() public override {}
}

多重継承とoverride

同じ関数がAとBの両方にある場合は、Cはオーバーライドしなければならない。このとき override (A, B) のように書く必要がある。

contract A {
    function hoge() public virtual {}
}
contract B {
    function hoge() public virtual {}
}
contract C is A, B {
    function hoge() public override (A, B) {
        super.hoge(); // B.hoge()を呼ぶ
       // A.hoge(); // 直接A.hoge()を呼ぶこともできる
    }
}

Cにhoge関数がないと下記エラーが出る。

TypeError: Derived contract must override function "hoge". Two or more base classes define function with same name and parameter types.

ダイヤモンド継承とoverride

A-(B-C)-D タイプのダイアモンド継承では、同じ関数がAとBにあり、Cにない場合でも、Dはオーバーライドしなければならない。このときは override (A, B) と書く。

contract A {
    function hoge() public virtual {...}
}
contract B is A {
    function hoge() public override virtual {...}
}
contract C is A {
    // hoge関数を持たない
}
contract D is B, C {
    function hoge() public override (A, B) {
        super.hoge(); // B.hoge()を呼ぶ
    }
}

実践編: ERC721EnumerableとERC721Burnableを継承する

ERC721EnumerableとERC721Burnableは、どちらもERC721を継承している。 そのため、この2つを多重継承する場合には、 _beforeTokenTransfersupportsInterface関数をオーバーライドしなければならない。 これらの関数はERC721とERC721Enumerableには存在するが、ERC721Burnableには存在しない。

上の説明の

  • A = ERC721
  • B = ERCEnumerable
  • C = ERCBurnable
  • D = MyNFT

と考えるとわかりやすい。

contract MyNFT is ERC721Enumerable, ERC721Burnable {
    constructor() ERC721("MyNFT", "MYNFT") {} // ※

    function _beforeTokenTransfer(address from, address to, uint256 tokenId)
        internal
        override(ERC721, ERC721Enumerable)
    {
        super._beforeTokenTransfer(from, to, tokenId);
    }
    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC721, ERC721Enumerable)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}

※ ちなみに ERC721EnumerableとERC721Burnable はコンストラクタを持たず、その親のERC721は引数ありのコンストラクタを持つため、 このような書き方になる。