——从简单继承到钻石继承的完整实战)
WTF Solidity 极简入门13. 合约继承Inheritance——从简单继承到钻石继承的完整实战【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity继承inheritance是 Solidity 面向对象编程的核心能力也是降低合约代码冗余、构建可复用合约体系的基础。本文以 WTF-Solidity 教程第 13 讲 Languages/ja/13_Inheritance_ja/readme.md 为骨架结合仓库内 Inheritance.sol、ModifierInheritance.sol 与 DiamondInheritance.sol 三份源码系统讲解简单继承、多重继承、修饰器与构造函数的继承、父合约函数调用以及多重继承中最易踩坑的钻石继承菱形继承问题。读完本文你将掌握virtual/override/super三个关键字的正确用法理解 Solidity 继承线性化的执行顺序并能在 Remix 中独立验证全部示例。继承是什么继承是面向对象编程的重要组成部分可以显著减少重复代码。如果把合约看作对象那么 Solidity 同样是面向对象的编程语言原生支持继承子合约派生合约可以复用父合约基合约中定义的函数、状态变量、事件与修饰器并可按需改写其中的行为。这一机制让开发者可以把公共逻辑如权限校验、余额管理沉淀在基类合约中由多个子合约共享。两个核心关键字Solidity 中与继承相关的两个关键关键字virtual父合约中的函数如果希望子合约重写需要加上virtual关键字明确允许被覆盖override子合约重写了父合约中的函数需要加上override关键字显式声明我覆盖了父类实现。注意点 1如果一个函数既覆盖了父合约函数又希望被更下层的子合约继续覆盖则需要同时标记为virtual override。这也是下文多个示例中函数签名的标准写法。注意点 2用override修饰public状态变量会重写与该变量同名的getter函数。例如 OpenZeppelin 风格的 ERC20 实现中常见写法mapping(address uint256) public override balanceOf;此时重写的是编译器自动生成的balanceOf()getter 函数而不是状态存储本身这一写法广泛用于在继承体系中统一代币余额的访问接口。简单继承先从一个简单的爷爷合约Grandfather开始对应源码 Inheritance.sol它包含 1 个Log事件和 3 个函数hip()、pop()、grandfather()输出都是Grandfather且 3 个函数均声明为public virtual。contract Grandfather { event Log(string msg); // 对以下 3 个函数应用继承hip()、pop()、grandfather()日志值为 Grandfather function hip() public virtual{ emit Log(Grandfather); } function pop() public virtual{ emit Log(Grandfather); } function grandfather() public virtual { emit Log(Grandfather); } }接着定义一个爸爸合约Father语法为contract Father is Grandfather非常直观。在Father中重写hip()和pop()加上override关键字将输出改为Father同时新增一个father()函数输出也是Fathercontract Father is Grandfather{ // 对以下 2 个函数应用继承hip()、pop()将日志值改为 Father function hip() public virtual override{ emit Log(Father); } function pop() public virtual override{ emit Log(Father); } function father() public virtual{ emit Log(Father); } }部署Father合约后可以看到它共有 4 个函数——hip()与pop()的输出已被成功改写为Father而从父合约继承的grandfather()输出仍保持Grandfather。这正是简单继承的全部行为继承带来复用override带来定制。多重继承Solidity 合约可以同时继承多个合约规则比简单继承更严格继承顺序按辈分从高到低排列多重继承时父合约必须按辈分最高到最低的顺序书写。例如contract Son is Grandfather, Father合法若写成contract Son is Father, Grandfather则会报错。重名函数必须重写如果某个函数在多个父合约中都存在例如hip()和pop()子合约必须重写它否则编译器报错。列出所有父合约重写多个父合约中同名函数时override关键字后必须附上所有父合约的名字例如override(Grandfather, Father)。示例见 Inheritance.solcontract Son is Grandfather, Father{ // 对以下 2 个函数应用继承hip()、pop()将日志值改为 Son function hip() public virtual override(Grandfather, Father){ emit Log(Son); } function pop() public virtual override(Grandfather, Father) { emit Log(Son); } }部署后调用hip()和pop()输出均变为Son而分别从Grandfather与Father继承来的grandfather()和father()保持原样。override(Grandfather, Father)的写法既满足了编译器的合法性检查也向阅读者明示了该函数的完整来源。修饰器的继承修饰器Modifier同样支持继承用法与函数继承类似在父合约中加virtual在子合约重写时加override对应源码 ModifierInheritance.solcontract Base1 { modifier exactDividedBy2And3(uint _a) virtual { require(_a % 2 0 _a % 3 0); _; } } contract Identifier is Base1 { // 计算一个数分别被 2 除和被 3 除的值但传入的参数必须是 2 和 3 的倍数 function getExactDividedBy2And3(uint _dividend) public exactDividedBy2And3(_dividend) pure returns(uint, uint) { return getExactDividedBy2And3WithoutModifier(_dividend); } // 计算一个数分别被 2 除和被 3 除的值 function getExactDividedBy2And3WithoutModifier(uint _dividend) public pure returns(uint, uint){ uint div2 _dividend / 2; uint div3 _dividend / 3; return (div2, div3); } }由于继承了Base1Identifier可以直接在函数上使用exactDividedBy2And3修饰器做前置校验调用getExactDividedBy2And3(6)正常返回(3, 2)而传入 9 这类非 2 和 3 公倍数的值会触发require回滚。修饰器同样可以被重写。仓库源码 ModifierInheritance.sol 中保留了一段注释示例将校验逻辑从_之前移到_之后即可把前置校验改写为后置校验modifier exactDividedBy2And3(uint _a) override { _; require(_a % 2 0 _a % 3 0); }改动_的位置即可控制校验发生在函数体执行前还是执行后这是修饰器继承最灵活的应用点之一。构造函数的继承构造函数constructor也会被继承。先看父合约A它有一个状态变量a由构造函数参数决定Inheritance.sol// 构造函数的继承 abstract contract A { uint public a; constructor(uint _a) { a _a; } }注意这里使用了abstract关键字因为构造函数带有参数合约A无法被直接部署只能作为抽象基类被继承这正是抽象合约的典型用途。子合约有两种方式继承父合约的构造函数在继承时直接声明父构造函数参数contract B is A(1) {}部署B后状态变量a恒为 1无需额外构造函数。在子合约构造函数中声明父构造函数参数contract C is A { constructor(uint _c) A(_c * _c) {} }部署C时传入_c父构造函数收到_c * _c即a会被初始化为传入参数的平方。这种方式适合父合约初始化参数需要由子合约动态计算如换算、派生的场景。调用父合约的函数子合约有直接调用和super关键字两种方式调用父合约函数Inheritance.sol。方式一直接调用——用父合约名.函数名()的语法精确指定调用某个父合约的实现function callParent() public{ Grandfather.pop(); }方式二super关键字——用super.函数名()调用继承层级中最近的父合约函数。Solidity 继承关系按声明时从右到左的优先级contract Son is Grandfather, Father中Father比Grandfather更近。因此Son中调用super.pop()会执行Father.pop()而不是Grandfather.pop()function callParentSuper() public{ // 调用继承层级中上一层的函数Father.pop() super.pop(); }在简单/多重继承场景下可以把super理解为向上找一层但到了钻石继承场景其行为会变得更加微妙见下节。钻石继承钻石继承菱形继承指一个派生类同时有两个或两个以上的基类而这些基类又共享同一个更高层的祖先。在多重钻石继承链条上使用super时务必注意super会调用继承链条上每一个合约的相关函数而不是只调用最近的父合约。仓库提供了完整的钻石继承示例 DiamondInheritance.sol继承树如下God / \ Adam Eve \ / people基合约God定义foo()与bar()Adam与Eve继承God并各自override这两个函数people同时继承Adam与Eve// SPDX-License-Identifier: MIT pragma solidity ^0.8.34; contract God { event Log(string message); function foo() public virtual { emit Log(God.foo called); } function bar() public virtual { emit Log(God.bar called); } } contract Adam is God { function foo() public virtual override { emit Log(Adam.foo called); } function bar() public virtual override { emit Log(Adam.bar called); super.bar(); } } contract Eve is God { function foo() public virtual override { emit Log(Eve.foo called); } function bar() public virtual override { emit Log(Eve.bar called); super.bar(); } } contract people is Adam, Eve { function foo() public override(Adam, Eve) { super.foo(); } function bar() public override(Adam, Eve) { super.bar(); } }在这个例子中调用people中的super.bar()会依次触发Eve.bar()→Adam.bar()→God.bar()形成一条完整的调用链。虽然Eve和Adam都是God的子合约但整个过程中God合约的bar()只会被调用一次不会出现菱形重复执行。背后的原理是Solidity 借鉴了 Python 的 MRO方法解析顺序思想对由基类构成的有向无环图DAG强制应用 C3 线性化C3 Linearization从而保证一个确定且无重复的调用顺序。对于继承链中每个函数的具体调用次序可依据该线性化规则逐一推演这也是官方文档中多重继承与线性化一节讨论的主题。提示钻石继承中override后列出全部父合约如override(Adam, Eve)是编译器的强制要求它同时帮助开发者显式确认函数在所有分支上的覆盖关系避免遗漏。在 Remix 上验证以下截图均来自仓库 Languages/ja/13_Inheritance_ja/img/展示在 Remix IDE 中验证上述行为的真实结果。1. 简单继承部署Father合约后可在已部署合约列表中看到它同时拥有hip()、pop()、grandfather()、father()四个函数其中hip()、pop()输出为Father继承来的grandfather()输出仍为Grandfather。2. 多重继承参照简单继承的操作步骤继续部署Son合约观察其暴露的函数并分别调用hip()、pop()查看日志输出验证override(Grandfather, Father)的效果。3. 修饰器继承部署Identifier合约分别以 6 和 9 调用getExactDividedBy2And3前者正常返回后者因无法通过exactDividedBy2And3修饰器校验而回滚可直观验证继承修饰器的生效过程。4. 构造函数继承分别部署B与C合约并查看公共状态变量a的值确认contract B is A(1)与constructor(uint _c) A(_c * _c)两种初始化方式的结果差异。5. 调用父合约函数部署Son合约分别调用callParent()触发Grandfather.pop()与callParentSuper()触发Father.pop()对比日志即可理解直接调用与super调用的区别。6. 钻石继承部署people合约并调用bar()观察交易日志中依次出现的调用记录验证super沿继承链依次执行Eve、Adam、God的bar()且God仅被调用一次。实操建议Remix 中可以先把 Inheritance.sol、ModifierInheritance.sol、DiamondInheritance.sol 三个文件直接导入工作区编译部署三个合约文件均使用pragma solidity ^0.8.34与仓库根目录 foundry.toml 中配置的solc 0.8.34一致也可在本地用forge直接编译验证。总结本讲覆盖了 Solidity 继承的完整知识体系virtual与override两个关键字的语义与搭配规则、public状态变量 getter 的覆盖写法、简单继承的复用与改写、多重继承的三条硬性规则、修饰器与构造函数的继承方式、直接调用与super调用的区别以及钻石继承中 C3 线性化决定的多级调用链。配合仓库中的三份示例源码在 Remix 中逐一验证即可牢固掌握 Solidity 面向对象编程中这一最基础也最重要的机制。【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考