SC Day 15
Solidity - 继承(is) + 接口(interface) + 抽象合约(abstract) + super
### 1. Solidity 继承基础
2026-04-15
第一阶段:基础构建solidityinheritanceinterfaceabstractsuperdiamond-problem
日期: 2026-04-15 方向: Solidity 阶段: 第一阶段:基础构建 标签: #solidity #inheritance #interface #abstract #super #diamond-problem
今日目标
| 类型 | 内容 |
|---|---|
| 学习 | 掌握 Solidity 的继承体系、C3 线性化、接口与抽象合约的设计模式 |
| 实操 | 实现 BaseToken → BurnableToken → MyToken 继承链 |
| 产出 | 完整的多层继承代码 + 钻石问题分析 + 设计模式总结 |
核心概念
1. Solidity 继承基础
Solidity 支持多重继承,使用 is 关键字。子合约继承父合约的所有 public 和 internal 状态变量与函数。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/// @notice 最基础的所有权管理
contract Ownable {
address public owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
constructor() {
owner = msg.sender;
emit OwnershipTransferred(address(0), msg.sender);
}
function transferOwnership(address newOwner) public virtual onlyOwner {
require(newOwner != address(0), "Zero address");
emit OwnershipTransferred(owner, newOwner);
owner = newOwner;
}
}
/// @notice 可暂停功能
contract Pausable is Ownable {
bool public paused;
event Paused(address account);
event Unpaused(address account);
modifier whenNotPaused() {
require(!paused, "Contract is paused");
_;
}
modifier whenPaused() {
require(paused, "Contract is not paused");
_;
}
function pause() public onlyOwner whenNotPaused {
paused = true;
emit Paused(msg.sender);
}
function unpause() public onlyOwner whenPaused {
paused = false;
emit Unpaused(msg.sender);
}
}
2. virtual 和 override 关键字
virtual 标记函数可以被重写,override 标记函数正在重写父合约的函数。
contract Base {
// virtual: 允许子合约重写此函数
function greet() public pure virtual returns (string memory) {
return "Hello from Base";
}
// 没有 virtual 的函数不能被重写
function version() public pure returns (uint256) {
return 1;
}
}
contract Child is Base {
// override: 重写父合约的 virtual 函数
function greet() public pure override returns (string memory) {
return "Hello from Child";
}
// 编译错误!version() 不是 virtual,不能 override
// function version() public pure override returns (uint256) { return 2; }
}
// 如果子合约也想让孙合约重写,需要同时标记 virtual 和 override
contract GrandChild is Child {
// 需要 Child.greet() 也标记为 virtual
// function greet() public pure override returns (string memory) {
// return "Hello from GrandChild";
// }
}
3. 接口 (interface)
接口定义了合约必须实现的函数签名,但不能包含任何实现。
// ====== 接口规则 ======
// 1. 所有函数必须是 external
// 2. 不能有构造函数
// 3. 不能有状态变量
// 4. 不能有函数实现
// 5. 可以继承其他接口
// 6. 可以定义 event、struct、enum、error
interface IERC20 {
// 事件(可以在接口中定义)
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
// 自定义错误(Solidity 0.8.4+)
error InsufficientBalance(uint256 required, uint256 available);
// 函数签名(必须是 external)
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
}
// 接口继承接口
interface IERC20Metadata is IERC20 {
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function decimals() external view returns (uint8);
}
4. 抽象合约 (abstract)
抽象合约介于接口和完整合约之间:可以有部分实现,也可以有未实现的函数。
/// @notice 抽象合约:提供部分实现,留下关键函数给子合约
abstract contract AbstractToken {
string public name;
string public symbol;
uint8 public decimals;
uint256 public totalSupply;
mapping(address => uint256) internal _balances;
// 构造函数(抽象合约可以有)
constructor(string memory _name, string memory _symbol, uint8 _decimals) {
name = _name;
symbol = _symbol;
decimals = _decimals;
}
// 已实现的函数
function balanceOf(address account) public view returns (uint256) {
return _balances[account];
}
// 未实现的函数(必须由子合约实现)
function _beforeTokenTransfer(
address from,
address to,
uint256 amount
) internal virtual;
// 提供基础转账逻辑,但调用了未实现的钩子
function _transfer(address from, address to, uint256 amount) internal virtual {
require(from != address(0), "Transfer from zero");
require(to != address(0), "Transfer to zero");
require(_balances[from] >= amount, "Insufficient balance");
_beforeTokenTransfer(from, to, amount); // 钩子:子合约可在此加逻辑
unchecked {
_balances[from] -= amount;
_balances[to] += amount;
}
}
}
接口 vs 抽象合约 对比
| 特性 | interface | abstract contract |
|---|---|---|
| 函数实现 | 不允许 | 可以有部分实现 |
| 状态变量 | 不允许 | 允许 |
| 构造函数 | 不允许 | 允许 |
| 函数可见性 | 只能 external | 任意(public/internal/...) |
| 继承 | 只能继承 interface | 可以继承任何合约 |
| 用途 | 定义标准/接口规范 | 提供共享逻辑 |
5. super 关键字和 C3 线性化
当多重继承时,super 不是简单地调用 "直接父合约",而是遵循C3 线性化顺序。
// ====== C3 线性化示例 ======
contract A {
event Log(string message);
function foo() public virtual {
emit Log("A.foo");
}
}
contract B is A {
function foo() public virtual override {
emit Log("B.foo");
super.foo(); // 调用 A.foo
}
}
contract C is A {
function foo() public virtual override {
emit Log("C.foo");
super.foo(); // 调用 A.foo
}
}
// D 继承 B 和 C,两者都继承 A(钻石问题)
contract D is B, C {
function foo() public override(B, C) {
emit Log("D.foo");
super.foo(); // 这里 super 是谁?
}
}
// D 的 C3 线性化顺序: D → C → B → A
// 调用 D.foo() 的输出顺序:
// 1. "D.foo"
// 2. super.foo() → C.foo() → "C.foo"
// 3. super.foo() → B.foo() → "B.foo" (不是 A!)
// 4. super.foo() → A.foo() → "A.foo"
// 每个合约的 foo() 只被调用一次!
C3 线性化规则
规则:
1. 子合约在父合约之前
2. 父合约按 is 声明的逆序排列
3. 每个合约只出现一次
示例:contract D is B, C
线性化:D → C → B → A
示例:contract E is C, B
线性化:E → B → C → A(注意顺序变了!)
关键原则:is 列表中越靠后的合约越"基础"
推荐写法:从最基础到最派生
contract MyToken is Ownable, Pausable, ERC20 { ... }
6. 构造函数参数传递
contract Base1 {
uint256 public x;
constructor(uint256 _x) {
x = _x;
}
}
contract Base2 {
uint256 public y;
constructor(uint256 _y) {
y = _y;
}
}
// 方法1:在继承列表中传参(编译时固定)
contract Child1 is Base1(10), Base2(20) {
// x = 10, y = 20
}
// 方法2:在子合约构造函数中传参(运行时动态)
contract Child2 is Base1, Base2 {
constructor(uint256 _x, uint256 _y) Base1(_x) Base2(_y) {
// x = _x, y = _y
}
}
// 混合使用
contract Child3 is Base1(100), Base2 {
constructor(uint256 _y) Base2(_y) {
// x = 100, y = _y
}
}
代码实战:完整的代币继承链
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// ========== 接口层 ==========
interface IERC20 {
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
}
// ========== 基础代币合约 ==========
contract BaseToken is IERC20 {
string public name;
string public symbol;
uint8 public decimals;
uint256 public totalSupply;
mapping(address => uint256) internal _balances;
mapping(address => mapping(address => uint256)) internal _allowances;
constructor(string memory _name, string memory _symbol, uint8 _decimals) {
name = _name;
symbol = _symbol;
decimals = _decimals;
}
function balanceOf(address account) public view returns (uint256) {
return _balances[account];
}
function allowance(address owner_, address spender) public view returns (uint256) {
return _allowances[owner_][spender];
}
function transfer(address to, uint256 amount) public virtual returns (bool) {
_transfer(msg.sender, to, amount);
return true;
}
function approve(address spender, uint256 amount) public virtual returns (bool) {
_approve(msg.sender, spender, amount);
return true;
}
function transferFrom(address from, address to, uint256 amount) public virtual returns (bool) {
_spendAllowance(from, msg.sender, amount);
_transfer(from, to, amount);
return true;
}
// ====== 内部函数(可被子合约重写)======
function _transfer(address from, address to, uint256 amount) internal virtual {
require(from != address(0), "Transfer from zero");
require(to != address(0), "Transfer to zero");
_beforeTokenTransfer(from, to, amount);
uint256 fromBalance = _balances[from];
require(fromBalance >= amount, "Insufficient balance");
unchecked {
_balances[from] = fromBalance - amount;
_balances[to] += amount;
}
emit Transfer(from, to, amount);
}
function _mint(address to, uint256 amount) internal virtual {
require(to != address(0), "Mint to zero");
_beforeTokenTransfer(address(0), to, amount);
totalSupply += amount;
_balances[to] += amount;
emit Transfer(address(0), to, amount);
}
function _approve(address owner_, address spender, uint256 amount) internal virtual {
require(owner_ != address(0), "Approve from zero");
require(spender != address(0), "Approve to zero");
_allowances[owner_][spender] = amount;
emit Approval(owner_, spender, amount);
}
function _spendAllowance(address owner_, address spender, uint256 amount) internal virtual {
uint256 currentAllowance = _allowances[owner_][spender];
if (currentAllowance != type(uint256).max) {
require(currentAllowance >= amount, "Insufficient allowance");
unchecked {
_allowances[owner_][spender] = currentAllowance - amount;
}
}
}
/// @notice 钩子函数:子合约可在转账前插入逻辑
function _beforeTokenTransfer(
address from,
address to,
uint256 amount
) internal virtual {
// 默认为空,子合约重写
}
}
// ========== 可销毁扩展 ==========
contract BurnableToken is BaseToken {
event Burn(address indexed burner, uint256 amount);
constructor(
string memory _name,
string memory _symbol,
uint8 _decimals
) BaseToken(_name, _symbol, _decimals) {}
/// @notice 销毁自己的代币
function burn(uint256 amount) public virtual {
_burn(msg.sender, amount);
}
/// @notice 销毁已授权给你的代币
function burnFrom(address account, uint256 amount) public virtual {
_spendAllowance(account, msg.sender, amount);
_burn(account, amount);
}
function _burn(address account, uint256 amount) internal virtual {
require(account != address(0), "Burn from zero");
_beforeTokenTransfer(account, address(0), amount);
uint256 accountBalance = _balances[account];
require(accountBalance >= amount, "Burn exceeds balance");
unchecked {
_balances[account] = accountBalance - amount;
totalSupply -= amount;
}
emit Transfer(account, address(0), amount);
emit Burn(account, amount);
}
}
// ========== 可暂停扩展 ==========
abstract contract PausableToken is BaseToken {
bool public paused;
address public admin;
event Paused(address account);
event Unpaused(address account);
modifier onlyAdmin() {
require(msg.sender == admin, "Not admin");
_;
}
function pause() external onlyAdmin {
paused = true;
emit Paused(msg.sender);
}
function unpause() external onlyAdmin {
paused = false;
emit Unpaused(msg.sender);
}
/// @notice 重写钩子:暂停时禁止转账
function _beforeTokenTransfer(
address from,
address to,
uint256 amount
) internal virtual override {
require(!paused, "Token transfers paused");
super._beforeTokenTransfer(from, to, amount); // 调用父合约的钩子
}
}
// ========== 带上限的铸造扩展 ==========
abstract contract CappedToken is BaseToken {
uint256 public immutable cap;
constructor(uint256 _cap) {
require(_cap > 0, "Cap must be > 0");
cap = _cap;
}
function _mint(address to, uint256 amount) internal virtual override {
require(totalSupply + amount <= cap, "Cap exceeded");
super._mint(to, amount);
}
}
// ========== 最终合约:组合所有扩展 ==========
contract MyToken is BurnableToken, PausableToken, CappedToken {
constructor()
BurnableToken("MyToken", "MTK", 18)
CappedToken(1_000_000 * 10**18) // 100万上限
{
admin = msg.sender;
_mint(msg.sender, 100_000 * 10**18); // 初始铸造10万
}
/// @notice 管理员铸造新代币
function mint(address to, uint256 amount) external onlyAdmin {
_mint(to, amount);
}
// 必须显式 override 两个父合约的同名函数
function _beforeTokenTransfer(
address from,
address to,
uint256 amount
) internal override(BaseToken, PausableToken) {
// 调用 super 会按 C3 线性化顺序调用所有父合约
super._beforeTokenTransfer(from, to, amount);
}
// CappedToken 重写了 _mint,需要通过 C3 线性化解决
function _mint(address to, uint256 amount) internal override(BaseToken, CappedToken) {
super._mint(to, amount); // 先调用 CappedToken._mint(检查上限),再调用 BaseToken._mint
}
}
关键要点总结
| 要点 | 说明 |
|---|---|
is 实现继承 | 子合约获得父合约的状态和函数 |
virtual | 标记函数可被子合约重写 |
override | 标记函数正在重写父合约函数 |
| C3 线性化 | 确定多重继承时 super 的调用顺序 |
| interface | 纯粹的接口定义,无实现 |
| abstract | 部分实现,留钩子给子合约 |
super | 按 C3 线性化顺序调用父合约 |
| 钩子模式 | _beforeTokenTransfer 等钩子让扩展更灵活 |
常见误区
误区 1:is 顺序不影响行为
// 这两个的 C3 线性化不同!
contract X is A, B { } // X → B → A
contract Y is B, A { } // Y → A → B
// 原则:is 列表中最右边的最"基础"
// 推荐从基础到派生排列
误区 2:忘记在 override 列表中包含所有父合约
// 如果 A 和 B 都有 foo(),子合约必须显式 override 两者
contract C is A, B {
// 编译错误!
// function foo() public override { }
// 正确
function foo() public override(A, B) { }
}
误区 3:认为 super 只调用直接父合约
super.foo() 不是调用 "直接父合约的 foo()",而是调用 C3 线性化中的下一个合约的 foo()。在钻石继承中,这确保每个合约的函数只被调用一次。
误区 4:abstract 合约可以被部署
抽象合约不能直接部署。必须由一个非抽象的子合约实现所有未实现的函数后才能部署。
面试关联
Q: Solidity 的多重继承如何解决钻石问题?
30 秒回答:Solidity 使用 C3 线性化算法将继承图展平为一条线性顺序。super 调用按这个顺序依次执行,确保每个合约的函数只被调用一次。开发者需要在 is 声明中正确排列顺序(从基础到派生),并在 override 中列出所有被覆盖的父合约。
Q: interface 和 abstract contract 什么时候用哪个?
- interface:定义标准规范(如 IERC20),让不同实现互相兼容
- abstract contract:提供共享的基础逻辑(如 BaseToken),减少重复代码
- 实践建议:先定义 interface 作为标准,再用 abstract contract 提供公共实现,最终合约组合两者
Q: 钩子模式 (_beforeTokenTransfer) 的设计意图是什么?
钩子模式允许子合约在不重写核心逻辑的情况下插入自定义行为。例如:暂停检查、黑名单检查、快照记录等。这是模板方法设计模式在智能合约中的应用,既保持了核心逻辑的完整性,又提供了扩展点。
参考资源
| 资源 | 说明 |
|---|---|
| Solidity 继承文档 | 官方文档 |
| C3 线性化算法 | 理解 MRO 原理 |
| OpenZeppelin ERC20 | 继承设计的最佳实践 |
| Solidity Patterns - Template Method | 智能合约设计模式 |