提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 1. 为什么需要继承
- 1.1 代码复用的问题
- 1.2 继承的解决方案
- 1.3 继承的实际应用场景
- 2. 单继承
- 2.1 单继承基础语法
- 2.2 访问权限
- 3. 多重继承
- 3.1 多重继承基础
- 3.2 C3线性化算法
- 4. super关键字
- 4.1 super的作用
- 4.2 单继承中的super
- 4.3 多重继承中的super
- 5. 构造函数继承
- 5.1 构造函数执行顺序
- 5.2 构造函数参数传递
- 5.3 多重继承的构造函数
- 6. 函数重写
- 6.1 virtual和override关键字
- 6.2 函数签名必须匹配
- 6.3 多重继承中的override
- 6.4 使用super在重写中调用父函数
- 7. 抽象合约
- 7.1 什么是抽象合约
- 8. 接口
- 8.1 什么是接口
- 8.3 ERC20接口标准
- 8.4 接口用于合约交互
1. 为什么需要继承
1.1 代码复用的问题
在没有继承机制的情况下,开发者会遇到严重的代码复用问题。
问题场景:
假设你要创建三个代币合约:稳定币、治理代币、奖励代币。它们都需要:
- ERC20基本功能(transfer、approve等)
- 权限控制(只有owner可以执行某些操作)
- 暂停功能(紧急情况下暂停转账)
没有继承的做法:
// 稳定币合约 contract StableCoin{// 复制粘贴ERC20代码 mapping(address=>uint256)public balanceOf;functiontransfer(address to, uint256 amount)public{}// 复制粘贴权限控制代码 address public owner;modifieronlyOwner(){}// 复制粘贴暂停功能代码 bool public paused;modifierwhenNotPaused(){}functionpause()public{}}// 治理代币合约 contract GovernanceToken{// 又复制粘贴一遍所有代码... mapping(address=>uint256)public balanceOf;functiontransfer(address to, uint256 amount)public{}address public owner;//... 完全重复的代码}// 奖励代币合约 contract RewardToken{// 再次复制粘贴...}这种做法的问题:
代码冗余:
三个合约有90%的代码相同
- 浪费存储空间
- 增加部署成本
维护困难:
- 发现bug需要修改三个地方
- 容易遗漏
- 一致性难以保证
升级麻烦:
- 添加新功能需要修改所有合约
- 无法批量更新
- 测试成本高
容易出错:
- 复制粘贴可能出错
- 某个合约可能用旧版本代码
- 难以追踪哪个版本最新
1.2 继承的解决方案
继承(Inheritance)是面向对象编程的核心特性,它允许一个合约(子合约)继承另一个合约(父合约)的属性和方法。
使用继承的做法:
// 基础合约1:ERC20功能 contract BaseERC20{mapping(address=>uint256)public balanceOf;functiontransfer(address to, uint256 amount)public virtual returns(bool){require(balanceOf[msg.sender]>=amount);balanceOf[msg.sender]-=amount;balanceOf[to]+=amount;returntrue;}}// 基础合约2:权限控制 contract Ownable{address public owner;constructor(){owner=msg.sender;}modifieronlyOwner(){require(msg.sender==owner,"Not owner");_;}}// 基础合约3:暂停功能 contract Pausable{bool public paused;modifierwhenNotPaused(){require(!paused,"Paused");_;}functionpause()internal{paused=true;}}// 子合约:继承所有功能 contract StableCoin is BaseERC20, Ownable, Pausable{// 自动获得所有父合约的功能 // 只需要添加特有功能functionemergencyPause()public onlyOwner{pause();}}contract GovernanceToken is BaseERC20, Ownable, Pausable{// 同样继承所有功能}contract RewardToken is BaseERC20, Ownable, Pausable{// 同样继承所有功能}继承的优势:
代码复用:
- 公共功能只写一次
- 多个子合约共享
- 减少90%以上的重复代码
易于维护:
- bug修复只改一处
- 所有子合约自动受益
- 保证一致性
模块化设计:
- 功能分离清晰
- 每个合约职责单一
- 易于理解和测试
灵活扩展:
- 子合约可以添加新功能
- 可以重写父合约函数
- 组合不同功能模块
1.3 继承的实际应用场景
场景1:代币项目
// 基础代币 → 标准代币 → 项目代币
ERC20 → ERC20Burnable → MyToken
场景2:权限管理
// 基础权限 → 角色管理 → 具体合约
Ownable → AccessControl → ProjectContract
场景3:安全功能
// 基础合约 → 安全增强 → 最终合约
BaseContract → ReentrancyGuard + Pausable → SecureContract
场景4:可升级合约
// 存储合约 → 逻辑合约 → 代理合约
Storage → Logic → Proxy
2. 单继承
2.1 单继承基础语法
单继承是最简单的继承形式,一个子合约只继承一个父合约。
基本语法:
contract Parent{// 父合约代码}contract Child is Parent{// 子合约代码 // 自动继承Parent的所有内容}关键字说明:
- is:表示继承关系
- Child:子合约(派生合约)
- Parent:父合约(基础合约)
简单示例:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.19;contract Parent{uint256 public value;functiongetValue()public view returns(uint256){returnvalue;}functionsetValue(uint256 _value)public{value=_value;}}contract Child is Parent{// 自动继承: // - value状态变量 // - getValue()函数 // - setValue()函数 // 添加新功能functiondoubleValue()public view returns(uint256){returnvalue *2;// 可以直接访问父合约的value}}子合约获得了什么:
// 部署Child合约后,可以调用: Child child=new Child();child.value();// 继承自Parent child.getValue();// 继承自Parent child.setValue(100);// 继承自Parent child.doubleValue();// Child自己的函数2.2 访问权限
子合约对父合约的访问权限取决于可见性修饰符。
- public:最开放,子合约和外部都能访问
- internal:只有子合约能访问,外部不能
- private:最严格,连子合约都不能访问
3. 多重继承
3.1 多重继承基础
Solidity支持多重继承,一个合约可以同时继承多个父合约。
基本语法:
contract Child is Parent1, Parent2, Parent3{// 同时继承多个父合约}继承顺序:
- 从左到右列出父合约
- 顺序很重要
- 影响super调用和函数解析
基础示例:
contract Parent1{functionfoo()public virtual returns(string memory){return"Parent1";}}contract Parent2{functionbar()public virtual returns(string memory){return"Parent2";}}// 多重继承 contract Child is Parent1, Parent2{// 自动获得foo()和bar()functiontest()public view returns(string memory, string memory){return(foo(), bar());}}3.2 C3线性化算法
Solidity使用C3线性化算法(C3 Linearization)确定继承顺序。
基本规则:
- 从左到右:Child is A, B, C,A最先,C最后
- 深度优先:先处理父合约的父合约
- 保持单调性:不能打乱已有的顺序
简单示例:
继承声明:contract C is A, B
继承顺序:C → A → B
调用链:
C.foo()
→ A.foo()
→ B.foo()
4. super关键字
4.1 super的作用
super关键字用于调用父合约的函数,即使子合约已经重写了该函数。
基本概念:
contract Parent{functiongreet()public virtual returns(string memory){return"Hello from Parent";}}contract Child is Parent{functiongreet()public override returns(string memory){// 调用父合约的greet()string memory parentGreeting=super.greet();returnstring.concat("Hello from Child, ", parentGreeting);}}调用过程:
Child.greet()
↓
获取super.greet()的返回值:“Hello from Parent”
↓
拼接:“Hello from Child, Hello from Parent”
↓
返回最终结果
4.2 单继承中的super
在单继承中,super指向唯一的父合约。
contract Counter{uint256 public count;functionincrement()public virtual{count+=1;}}contract DoubleCounter is Counter{functionincrement()public override{// 先调用父合约的increment(+1) super.increment();// 再自己加一次(再+1) count+=1;// 最终效果:每次+2}}4.3 多重继承中的super
在多重继承中,super不是指向单个父合约,而是按照继承顺序调用下一个合约。
关键理解:
contract A{functionfoo()public virtual returns(string memory){return"A";}}contract B is A{functionfoo()public virtual override returns(string memory){returnstring.concat("B->", super.foo());}}contract C is A{functionfoo()public virtual override returns(string memory){returnstring.concat("C->", super.foo());}}contract D is B, C{functionfoo()public override(B, C)returns(string memory){returnstring.concat("D->", super.foo());}}调用链分析:
1)、Solidity 从右到左处理继承列表。
2)、C 在继承列表中更靠右,所以在 D 之后首先出现。
3)、B 在 C 之后。
4)、A 是共同的基类,放在最后。
D.foo()执行 ↓ super.foo()→ 指向 C(线性化顺序的下一个) ↓ C.foo()执行 ↓ C 中的 super.foo()→ 指向 B(不是 A!) ↓ B.foo()执行 ↓ B 中的 super.foo()→ 指向 A ↓ A.foo()返回"A"↓ B.foo()返回"B->A"↓ C.foo()返回"C->B->A"↓ D.foo()返回"D->C->B->A"重要提示: super 的含义:在多重继承中,super 不是指"直接父合约",而是指C3 线性化顺序中的下一个合约。 在 D 的上下文中,C 虽然直接继承自 A,但 C 的 super 会指向 B 这是因为在整个继承链中,C 的下一个是 B,而不是直接跳到 A
5. 构造函数继承
5.1 构造函数执行顺序
构造函数总是按照继承顺序执行,从父到子。
执行规则:
- 父合约优先:所有父合约构造函数先执行
- 按继承顺序:从左到右
- 最后是子合约:子合约构造函数最后执行
示例:
contract A{uint256 public valueA;constructor(){valueA=1;// 第1个执行}}contract B{uint256 public valueB;constructor(){valueB=2;// 第2个执行}}contract C is A, B{uint256 public valueC;constructor(){valueC=3;// 第3个执行}}执行顺序:
部署C合约
↓
- A的构造函数执行(valueA = 1)
↓- B的构造函数执行(valueB = 2)
↓- C的构造函数执行(valueC = 3)
↓
完成
5.2 构造函数参数传递
当父合约的构造函数需要参数时,有两种传递方式。
方式1:在继承声明时传递(固定值)
contract Parent{uint256 public value;constructor(uint256 _value){value=_value;}}// 在继承声明时传递固定值 contract Child is Parent(100){constructor(){// Parent构造函数接收100}}特点:
- 适合固定值
- 代码简洁
- 不够灵活
方式2:在子构造函数中传递(动态值)
contract Parent{uint256 public value;constructor(uint256 _value){value=_value;}}// 在子构造函数中传递动态值 contract Child is Parent{constructor(uint256 _value)Parent(_value){// 通过参数传递给Parent}}特点:
- 更灵活
- 可以传递动态值
- 推荐使用
5.3 多重继承的构造函数
多个父合约都需要参数时,必须全部初始化。
contract A{uint256 public valueA;constructor(uint256 _a){valueA=_a;}}contract B{uint256 public valueB;constructor(uint256 _b){valueB=_b;}}contract C is A, B{uint256 public valueC;// 方式1:混合传递 constructor(uint256 _a, uint256 _c)A(_a)// 动态传递给A B(200)// 固定值传递给B{valueC=_c;}// 方式2:全部动态传递(推荐) constructor(uint256 _a, uint256 _b, uint256 _c)A(_a)B(_b){valueC=_c;}}执行顺序保持不变:
无论如何传递参数,执行顺序始终是:
A() → B() → C()
6. 函数重写
6.1 virtual和override关键字
函数重写(Function Overriding)允许子合约修改父合约函数的行为。
基本规则:
- 父合约:函数必须标记为virtual
- 子合约:重写函数必须标记为override
- 两者必须配对:缺一不可
基础示例:
contract Parent{// virtual:表示这个函数可以被重写functiongetValue()public virtual returns(uint256){return100;}}contract Child is Parent{// override:表示重写父合约的函数functiongetValue()public override returns(uint256){return200;// 修改返回值}}6.2 函数签名必须匹配
重写函数必须与父合约函数签名完全一致。
必须相同的部分:
- 函数名
- 参数类型
- 参数顺序
- 返回类型
可以不同的部分:
- 可见性(可以更开放,不能更严格)
- 状态修饰符(可以更严格,不能更宽松)
示例:
contract Parent{functionfoo(uint256 a)public virtual returns(uint256){returna;}}contract Child is Parent{// 正确:签名完全相同functionfoo(uint256 a)public override returns(uint256){returna *2;}// 错误:参数不同 //functionfoo(uint256 a, uint256 b)public override returns(uint256){//returna + b;//}// 错误:返回类型不同 //functionfoo(uint256 a)public override returns(string memory){//return"hello";//}// 正确:internal → public(更开放)functionfoo()public override returns(uint256){return2;}// 错误:internal → private(更严格) //functionfoo()private override returns(uint256){//return2;//}}6.3 多重继承中的override
当多个父合约有同名函数时,必须明确指定重写哪些。
contract A{functionfoo()public virtual returns(string memory){return"A";}}contract B{functionfoo()public virtual returns(string memory){return"B";}}contract C is A, B{// 必须明确指定:override(A, B)functionfoo()public override(A, B)returns(string memory){return"C";}// 错误:不明确 //functionfoo()public override returns(string memory){//return"C";//}}语法规则:
// 单继承:简单overridefunctionfoo()public override returns(string memory){}// 多重继承:明确指定functionfoo()public override(Parent1, Parent2)returns(string memory){}// 如果继续被继承,还要加virtualfunctionfoo()public virtual override(Parent1, Parent2)returns(string memory){}6.4 使用super在重写中调用父函数
contract Logger{event Log(string message);functionlog(string memory message)public virtual{emit Log(message);}}contract TimestampLogger is Logger{functionlog(string memory message)public override{// 先调用父合约的log super.log(message);// 再添加时间戳日志 emit Log(string.concat("Timestamp: ", uint2str(block.timestamp)));}functionuint2str(uint _i)internal pure returns(string memory){if(_i==0)return"0";uint j=_i;uint len;while(j!=0){len++;j /=10;}bytes memory bstr=new bytes(len);uint k=len;while(_i!=0){k=k-1;uint8 temp=(48+ uint8(_i - _i /10*10));bytes1 b1=bytes1(temp);bstr[k]=b1;_i /=10;}returnstring(bstr);}}7. 抽象合约
7.1 什么是抽象合约
抽象合约(Abstract Contract)是包含至少一个未实现函数的合约。
定义语法:
abstract contract 合约名{// 至少一个未实现的函数}基本示例:
abstract contract Animal{// 抽象函数:只有声明,没有实现functionmakeSound()public virtual returns(string memory);// 普通函数:可以有实现functionsleep()public pure returns(string memory){return"Zzz...";}// 可以有状态变量 uint256 public age;}// 实现抽象合约 contract Dog is Animal{// 必须实现makeSoundfunctionmakeSound()public pure override returns(string memory){return"Woof!";}}contract Cat is Animal{functionmakeSound()public pure override returns(string memory){return"Meow!";}}抽象合约的特点:
- 不能直接部署:必须被继承
- 可以有实现:部分函数可以有实现
- 可以有状态变量:可以定义状态变量
- 可以有构造函数:可以有构造函数
- 强制实现:子合约必须实现所有抽象函数
8. 接口
8.1 什么是接口
接口(Interface)是纯粹的接口定义,只声明函数签名,不包含任何实现。
定义语法:
interface 接口名{// 只有函数声明}基本示例:
interface ICounter{// 所有函数必须是externalfunctiongetCount()external view returns(uint256);functionincrement()external;functiondecrement()external;// 可以定义事件 event CountChanged(uint256 newCount);}// 实现接口 contract Counter is ICounter{uint256 private count;event CountChanged(uint256 newCount);functiongetCount()external view override returns(uint256){returncount;}functionincrement()external override{count++;emit CountChanged(count);}functiondecrement()external override{count--;emit CountChanged(count);}}8.2 接口的特点和限制
接口的特点:
使用interface关键字
- 不能有实现:所有函数都是声明
- 不能有状态变量:不能定义storage变量
- 不能有构造函数
- 所有函数必须external
- 可以继承其他接口
- 可以定义事件
接口vs合约对比:
8.3 ERC20接口标准
ERC20是最经典的接口定义示例。
interface IERC20{// 查询函数functiontotalSupply()external view returns(uint256);functionbalanceOf(address account)external view returns(uint256);functionallowance(address owner, address spender)external view returns(uint256);// 操作函数functiontransfer(address to, uint256 amount)external returns(bool);functionapprove(address spender, uint256 amount)external returns(bool);functiontransferFrom(address from, address to, uint256 amount)external returns(bool);// 事件 event Transfer(address indexed from, address indexed to, uint256 value);event Approval(address indexed owner, address indexed spender, uint256 value);}8.4 接口用于合约交互
接口最重要的应用是合约间交互。
场景:合约A调用合约B
// 定义接口 interface IToken{functiontransfer(address to, uint256 amount)external returns(bool);functionbalanceOf(address account)external view returns(uint256);}// 使用接口与其他合约交互 contract Exchanger{functionswapTokens(address tokenAddress, address recipient, uint256 amount)public{// 通过接口与代币合约交互 IToken token=IToken(tokenAddress);// 检查余额 uint256 balance=token.balanceOf(address(this));require(balance>=amount,"Insufficient balance");// 执行转账 bool success=token.transfer(recipient, amount);require(success,"Transfer failed");}}接口的优势:
- 解耦:不需要知道合约的完整代码
- 标准化:统一的接口规范
- 互操作性:不同合约可以互相调用
- 节省gas:不需要导入完整合约代码