WTF-Solidity 第13讲:Solidity 合约继承实战——从简单继承、多重继承到钻石继承与 C3 线性化
【免费下载链接】WTF-Solidity WTF Solidity 极简入门教程,供小白们使用。Now supports English! 官网: https://wtf.academy 项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity
本讲围绕 WTF-Solidity 教程第 13 讲(对应 Languages/es/13_Herencia_es/readme.md)展开,系统讲解 Solidity 合约继承的全部核心用法:virtual/override 关键字、简单继承与多重继承、修饰器与构造函数的继承、父合约函数调用,以及容易踩坑的钻石继承与 C3 线性化机制。读者学完后,将能够在自己的合约中正确设计继承层级、安全地覆写函数,并理解 super 在复杂继承链上的真实调用顺序。
继承概述:为什么 Solidity 需要继承
继承是面向对象编程(OOP)的核心概念之一,可以显著减少重复代码。合约可以看作对象,因此 Solidity 天然支持继承:一个合约可以从另一个合约派生,从而形成共享属性与方法的分层结构。在 WTF-Solidity 仓库中,本讲的所有可运行示例位于 Languages/es/13_Herencia_es/ 目录(西班牙语版),中文版源码位于 13_Inheritance/ 目录,包含 Inheritance.sol、ModifierInheritance.sol、DiamondInheritance.sol 等文件,可直接在 Remix 或 Foundry 中编译运行。
继承的两大核心关键字:virtual 与 override
Solidity 继承的规则由两个关键字控制:
- virtual:父合约中的函数,如果希望被子合约重写,必须加上 virtual 关键字;
- override:子合约重写了父合约中的函数,必须加上 override 关键字。
两条易错注意点:
mapping(address => uint256) public override balanceOf;
简单继承:contract Father is Grandfather
先看一个最简单的一对一继承。定义爷爷合约 Grandfather,包含 1 个 Log 事件和 3 个函数 hip()、pop()、grandfather(),输出均为 "Grandfather":
contract Grandfather {
event Log(string msg);
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 中,用 override 关键字覆写 hip() 和 pop(),将输出改为 "Father",并新增一个函数 father():
contract Father is Grandfather{
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"。这一现象在 Remix 部署验证截图中可以直观看到(见上图及 13-2.png)。
多重继承:排序、强制覆写与 override 列表
Solidity 合约可以同时继承多个合约,但有三条硬性规则:
示例(源码见 Languages/es/13_Herencia_es/Inheritance.sol):
contract Son is Grandfather, Father{
function hip() public virtual override(Grandfather, Father){
emit Log("Son");
}
function pop() public virtual override(Grandfather, Father) {
emit Log("Son");
}
}
部署后 Son 合约的 hip()、pop() 输出变为 "Son",而分别从 Grandfather、Father 继承来的 grandfather()、father() 保持原样。
修饰器的继承
Solidity 中的修饰器(modifier)同样可以继承,规则与函数继承完全一致:父合约中的修饰器加 virtual,子合约覆写时加 override。示例源码见 Languages/es/13_Herencia_es/ModifierInheritance.sol:
contract 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);
}
function getExactDividedBy2And3WithoutModifier(uint _dividend) public pure returns(uint, uint){
uint div2 = _dividend / 2;
uint div3 = _dividend / 3;
return (div2, div3);
}
}
Identifier 合约因为继承了 Base1,可以直接在函数声明处使用 exactDividedBy2And3 修饰器。同样也可以用 override 重写修饰器(注意 _ 占位符的位置决定前置检查还是后置检查):
modifier exactDividedBy2And3(uint _a) override {
_;
require(_a % 2 == 0 && _a % 3 == 0);
}
从源码注释可以看到一个实用的验证场景:在不重写修饰器时,传入 9 调用 getExactDividedBy2And3 会因 require 检查不通过而 revert;注释掉重写代码后传入 9 则调用成功。
构造函数的继承:两种传参方式
构造函数同样可以被继承。以父合约 A 为例,它有一个由构造参数初始化的状态变量 a(源码见 Languages/es/13_Herencia_es/ConstructorInheritance.sol):
abstract contract A {
uint public a;
constructor(uint _a) {
a = _a;
}
}
子合约继承父构造函数有两种方式:
contract B is A(1) {}
contract C is A {
constructor(uint _c) A(_c * _c) {}
}
方式二更灵活:子合约可以在调用父构造函数前对参数做任意计算(如 _c * _c)。
调用父合约的函数:直接调用与 super 关键字
子合约调用父合约函数有两种方式:
function callParent() public{
Grandfather.pop();
}
function callParentSuper() public{
// 调用层级中最近一级的父合约函数,即 Father.pop()
super.pop();
}
这两个函数在 Languages/es/13_Herencia_es/Inheritance.sol 的 Son 合约中均已实现,可直接部署验证。
钻石继承与 C3 线性化:super 的真实调用链
钻石继承(菱形继承)指一个派生类同时有两个或两个以上基类。在"多重 + 钻石"继承链上使用 super 时有一个极易踩坑的特性:super 会调用继承链上每一个合约的相关函数,而不仅仅是最近的父合约。
本讲经典示例(源码见 Languages/es/13_Herencia_es/DiamondInheritance.sol)构造了如下继承树:
God
/ \\
Adam Eve
\\ /
people
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.34;
contract God {
event Log(string message);
function foo() public virtual { emit Log("God.foo llamado"); }
function bar() public virtual { emit Log("God.bar llamado"); }
}
contract Adam is God {
function foo() public virtual override { emit Log("Adam.foo llamado"); }
function bar() public virtual override {
emit Log("Adam.bar llamado");
super.bar();
}
}
contract Eve is God {
function foo() public virtual override { emit Log("Eve.foo llamado"); }
function bar() public virtual override {
emit Log("Eve.bar llamado");
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.bar llamado
Adam.bar llamado
God.bar llamado
这与普通多重继承"只调用最近父合约"的行为截然不同。关键点在于:尽管 Eve 和 Adam 都是 God 的子合约,整个调用过程中 God 只会被执行一次。这是因为 Solidity 借鉴了 Python 的做法,强制基类构成一个有向无环图(DAG),并通过 C3 线性化(C3 Linearization) 保证唯一的确定性调用顺序。钻石继承的完整执行结果见仓库截图 13-10.png。
在 Remix 中验证:完整对照实验清单
以下验证步骤与仓库源码一一对应,可在 Remix 中按序执行:
仓库根目录的 foundry.toml 配置了 solc = "0.8.34" 与 src = "src",本讲示例使用的 pragma solidity ^0.8.34 与之兼容,也可直接通过 Foundry 统一编译验证。
总结
本讲完整覆盖了 Solidity 继承的全部基础用法:
- 简单继承(contract Father is Grandfather)与 virtual/override 关键字规则;
- 多重继承的三条规则(排序、强制覆写、override(父合约列表));
- 修饰器与构造函数的继承方式及两种传参写法;
- 父合约函数的两种调用方式(直接调用与 super);
- 钻石继承中 super 沿继承链逐级调用、且公共基类只执行一次的特性,以及其背后的 C3 线性化原理。
理解这些规则不仅有助于编写复用性强的合约,更是阅读 OpenZeppelin 等大型合约库、排查覆写与初始化顺序问题的必备基础。相关可运行源码均在本仓库 Languages/es/13_Herencia_es/ 与 13_Inheritance/ 目录下,建议动手部署并逐项观察 Log 输出,以加深对继承链调用顺序的理解。
【免费下载链接】WTF-Solidity WTF Solidity 极简入门教程,供小白们使用。Now supports English! 官网: https://wtf.academy 项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

