Solidity入门(7)- 合约继承

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • [1. 为什么需要继承](#1. 为什么需要继承)
    • [1.1 代码复用的问题](#1.1 代码复用的问题)
    • [1.2 继承的解决方案](#1.2 继承的解决方案)
    • [1.3 继承的实际应用场景](#1.3 继承的实际应用场景)
  • [2. 单继承](#2. 单继承)
    • [2.1 单继承基础语法](#2.1 单继承基础语法)
    • [2.2 访问权限](#2.2 访问权限)
  • [3. 多重继承](#3. 多重继承)
    • [3.1 多重继承基础](#3.1 多重继承基础)
    • [3.2 C3线性化算法](#3.2 C3线性化算法)
  • [4. super关键字](#4. super关键字)
    • [4.1 super的作用](#4.1 super的作用)
    • [4.2 单继承中的super](#4.2 单继承中的super)
    • [4.3 多重继承中的super](#4.3 多重继承中的super)
  • [5. 构造函数继承](#5. 构造函数继承)
    • [5.1 构造函数执行顺序](#5.1 构造函数执行顺序)
    • [5.2 构造函数参数传递](#5.2 构造函数参数传递)
    • [5.3 多重继承的构造函数](#5.3 多重继承的构造函数)
  • [6. 函数重写](#6. 函数重写)
    • [6.1 virtual和override关键字](#6.1 virtual和override关键字)
    • [6.2 函数签名必须匹配](#6.2 函数签名必须匹配)
    • [6.3 多重继承中的override](#6.3 多重继承中的override)
    • [6.4 使用super在重写中调用父函数](#6.4 使用super在重写中调用父函数)
  • [7. 抽象合约](#7. 抽象合约)
    • [7.1 什么是抽象合约](#7.1 什么是抽象合约)
  • [8. 接口](#8. 接口)
    • [8.1 什么是接口](#8.1 什么是接口)
    • [8.3 ERC20接口标准](#8.3 ERC20接口标准)
  • [8.4 接口用于合约交互](#8.4 接口用于合约交互)

1. 为什么需要继承

1.1 代码复用的问题

在没有继承机制的情况下,开发者会遇到严重的代码复用问题。

问题场景:

假设你要创建三个代币合约:稳定币、治理代币、奖励代币。它们都需要:

  • ERC20基本功能(transfer、approve等)
  • 权限控制(只有owner可以执行某些操作)
  • 暂停功能(紧急情况下暂停转账)

没有继承的做法:

bash 复制代码
// 稳定币合约
contract StableCoin {
    // 复制粘贴ERC20代码
    mapping(address => uint256) public balanceOf;
    function transfer(address to, uint256 amount) public { }
    
    // 复制粘贴权限控制代码
    address public owner;
    modifier onlyOwner() { }
    
    // 复制粘贴暂停功能代码
    bool public paused;
    modifier whenNotPaused() { }
    function pause() public { }
}

// 治理代币合约
contract GovernanceToken {
    // 又复制粘贴一遍所有代码...
    mapping(address => uint256) public balanceOf;
    function transfer(address to, uint256 amount) public { }
    address public owner;
    // ... 完全重复的代码
}

// 奖励代币合约
contract RewardToken {
    // 再次复制粘贴...
}

这种做法的问题:

代码冗余:

  • 三个合约有90%的代码相同

    • 浪费存储空间
    • 增加部署成本
  • 维护困难:

    • 发现bug需要修改三个地方
    • 容易遗漏
    • 一致性难以保证
  • 升级麻烦:

    • 添加新功能需要修改所有合约
    • 无法批量更新
    • 测试成本高
  • 容易出错:

    • 复制粘贴可能出错
    • 某个合约可能用旧版本代码
    • 难以追踪哪个版本最新

1.2 继承的解决方案

继承(Inheritance)是面向对象编程的核心特性,它允许一个合约(子合约)继承另一个合约(父合约)的属性和方法。

使用继承的做法:

bash 复制代码
// 基础合约1:ERC20功能
contract BaseERC20 {
    mapping(address => uint256) public balanceOf;
    
    function transfer(address to, uint256 amount) public virtual returns (bool) {
        require(balanceOf[msg.sender] >= amount);
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        return true;
    }
}

// 基础合约2:权限控制
contract Ownable {
    address public owner;
    
    constructor() {
        owner = msg.sender;
    }
    
    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }
}

// 基础合约3:暂停功能
contract Pausable {
    bool public paused;
    
    modifier whenNotPaused() {
        require(!paused, "Paused");
        _;
    }
    
    function pause() internal {
        paused = true;
    }
}

// 子合约:继承所有功能
contract StableCoin is BaseERC20, Ownable, Pausable {
    // 自动获得所有父合约的功能
    // 只需要添加特有功能
    
    function emergencyPause() 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 单继承基础语法

单继承是最简单的继承形式,一个子合约只继承一个父合约。

基本语法:

bash 复制代码
contract Parent {
    // 父合约代码
}

contract Child is Parent {
    // 子合约代码
    // 自动继承Parent的所有内容
}

关键字说明:

  • is:表示继承关系
  • Child:子合约(派生合约)
  • Parent:父合约(基础合约)

简单示例:

bash 复制代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract Parent {
    uint256 public value;
    
    function getValue() public view returns (uint256) {
        return value;
    }
    
    function setValue(uint256 _value) public {
        value = _value;
    }
}

contract Child is Parent {
    // 自动继承:
    // - value状态变量
    // - getValue()函数
    // - setValue()函数
    
    // 添加新功能
    function doubleValue() public view returns (uint256) {
        return value * 2;  // 可以直接访问父合约的value
    }
}

子合约获得了什么:

bash 复制代码
// 部署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支持多重继承,一个合约可以同时继承多个父合约。

基本语法:

bash 复制代码
contract Child is Parent1, Parent2, Parent3 {
    // 同时继承多个父合约
}

继承顺序:

  • 从左到右列出父合约
  • 顺序很重要
  • 影响super调用和函数解析
    基础示例:
bash 复制代码
contract Parent1 {
    function foo() public virtual returns (string memory) {
        return "Parent1";
    }
}

contract Parent2 {
    function bar() public virtual returns (string memory) {
        return "Parent2";
    }
}

// 多重继承
contract Child is Parent1, Parent2 {
    // 自动获得foo()和bar()
    
    function test() 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关键字用于调用父合约的函数,即使子合约已经重写了该函数。

基本概念:

bash 复制代码
contract Parent {
    function greet() public virtual returns (string memory) {
        return "Hello from Parent";
    }
}

contract Child is Parent {
    function greet() public override returns (string memory) {
        // 调用父合约的greet()
        string memory parentGreeting = super.greet();
        return string.concat("Hello from Child, ", parentGreeting);
    }
}

调用过程:

Child.greet()

获取super.greet()的返回值:"Hello from Parent"

拼接:"Hello from Child, Hello from Parent"

返回最终结果

4.2 单继承中的super

在单继承中,super指向唯一的父合约。

bash 复制代码
contract Counter {
    uint256 public count;
    
    function increment() public virtual {
        count += 1;
    }
}

contract DoubleCounter is Counter {
    function increment() public override {
        // 先调用父合约的increment(+1)
        super.increment();
        // 再自己加一次(再+1)
        count += 1;
        // 最终效果:每次+2
    }
}

4.3 多重继承中的super

在多重继承中,super不是指向单个父合约,而是按照继承顺序调用下一个合约。

关键理解:

bash 复制代码
contract A {
    function foo() public virtual returns (string memory) {
        return "A";
    }
}

contract B is A {
    function foo() public virtual override returns (string memory) {
        return string.concat("B->", super.foo());
    }
}

contract C is A {
    function foo() public virtual override returns (string memory) {
        return string.concat("C->", super.foo());
    }
}

contract D is B, C {
    function foo() public override(B, C) returns (string memory) {
        return string.concat("D->", super.foo());
    }
}

调用链分析:

1)、Solidity 从右到左处理继承列表。

2)、C 在继承列表中更靠右,所以在 D 之后首先出现。

3)、B 在 C 之后。

4)、A 是共同的基类,放在最后。

bash 复制代码
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 构造函数执行顺序

构造函数总是按照继承顺序执行,从父到子。

执行规则:

  • 父合约优先:所有父合约构造函数先执行
  • 按继承顺序:从左到右
  • 最后是子合约:子合约构造函数最后执行
    示例:
bash 复制代码
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合约

  1. A的构造函数执行(valueA = 1)
  2. B的构造函数执行(valueB = 2)
  3. C的构造函数执行(valueC = 3)

    完成

5.2 构造函数参数传递

当父合约的构造函数需要参数时,有两种传递方式。

方式1:在继承声明时传递(固定值)

bash 复制代码
contract Parent {
    uint256 public value;
    
    constructor(uint256 _value) {
        value = _value;
    }
}

// 在继承声明时传递固定值
contract Child is Parent(100) {
    constructor() {
        // Parent构造函数接收100
    }
}

特点:

  • 适合固定值
  • 代码简洁
  • 不够灵活

方式2:在子构造函数中传递(动态值)

bash 复制代码
contract Parent {
    uint256 public value;
    
    constructor(uint256 _value) {
        value = _value;
    }
}

// 在子构造函数中传递动态值
contract Child is Parent {
    constructor(uint256 _value) Parent(_value) {
        // 通过参数传递给Parent
    }
}

特点:

  • 更灵活
  • 可以传递动态值
  • 推荐使用

5.3 多重继承的构造函数

多个父合约都需要参数时,必须全部初始化。

bash 复制代码
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
  • 两者必须配对:缺一不可
    基础示例:
bash 复制代码
contract Parent {
    // virtual:表示这个函数可以被重写
    function getValue() public virtual returns (uint256) {
        return 100;
    }
}

contract Child is Parent {
    // override:表示重写父合约的函数
    function getValue() public override returns (uint256) {
        return 200;  // 修改返回值
    }
}

6.2 函数签名必须匹配

重写函数必须与父合约函数签名完全一致。

必须相同的部分:

  • 函数名
  • 参数类型
  • 参数顺序
  • 返回类型

可以不同的部分:

  • 可见性(可以更开放,不能更严格)
  • 状态修饰符(可以更严格,不能更宽松)
    示例:
bash 复制代码
contract Parent {
    function foo(uint256 a) public virtual returns (uint256) {
        return a;
    }
}

contract Child is Parent {
    // 正确:签名完全相同
    function foo(uint256 a) public override returns (uint256) {
        return a * 2;
    }
    
    // 错误:参数不同
    // function foo(uint256 a, uint256 b) public override returns (uint256) {
    //     return a + b;
    // }
    
    // 错误:返回类型不同
    // function foo(uint256 a) public override returns (string memory) {
    //     return "hello";
    // }


    // 正确:internal → public(更开放)
    function foo() public override returns (uint256) {
        return 2;
    }
    
    // 错误:internal → private(更严格)
    // function foo() private override returns (uint256) {
    //     return 2;
    // }
}

6.3 多重继承中的override

当多个父合约有同名函数时,必须明确指定重写哪些。

bash 复制代码
contract A {
    function foo() public virtual returns (string memory) {
        return "A";
    }
}

contract B {
    function foo() public virtual returns (string memory) {
        return "B";
    }
}

contract C is A, B {
    // 必须明确指定:override(A, B)
    function foo() public override(A, B) returns (string memory) {
        return "C";
    }
    
    // 错误:不明确
    // function foo() public override returns (string memory) {
    //     return "C";
    // }
}

语法规则:

bash 复制代码
// 单继承:简单override
function foo() public override returns (string memory) { }

// 多重继承:明确指定
function foo() public override(Parent1, Parent2) returns (string memory) { }

// 如果继续被继承,还要加virtual
function foo() public virtual override(Parent1, Parent2) returns (string memory) { }

6.4 使用super在重写中调用父函数

bash 复制代码
contract Logger {
    event Log(string message);
    
    function log(string memory message) public virtual {
        emit Log(message);
    }
}

contract TimestampLogger is Logger {
    function log(string memory message) public override {
        // 先调用父合约的log
        super.log(message);
        
        // 再添加时间戳日志
        emit Log(
            string.concat(
                "Timestamp: ",
                uint2str(block.timestamp)
            )
        );
    }
    
    function uint2str(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;
        }
        return string(bstr);
    }
}

7. 抽象合约

7.1 什么是抽象合约

抽象合约(Abstract Contract)是包含至少一个未实现函数的合约。

定义语法:

bash 复制代码
abstract contract 合约名 {
    // 至少一个未实现的函数
}

基本示例:

bash 复制代码
abstract contract Animal {
    // 抽象函数:只有声明,没有实现
    function makeSound() public virtual returns (string memory);
    
    // 普通函数:可以有实现
    function sleep() public pure returns (string memory) {
        return "Zzz...";
    }
    
    // 可以有状态变量
    uint256 public age;
}

// 实现抽象合约
contract Dog is Animal {
    // 必须实现makeSound
    function makeSound() public pure override returns (string memory) {
        return "Woof!";
    }
}

contract Cat is Animal {
    function makeSound() public pure override returns (string memory) {
        return "Meow!";
    }
}

抽象合约的特点:

  • 不能直接部署:必须被继承
  • 可以有实现:部分函数可以有实现
  • 可以有状态变量:可以定义状态变量
  • 可以有构造函数:可以有构造函数
  • 强制实现:子合约必须实现所有抽象函数

8. 接口

8.1 什么是接口

接口(Interface)是纯粹的接口定义,只声明函数签名,不包含任何实现。

定义语法:

bash 复制代码
interface 接口名 {
    // 只有函数声明
}

基本示例:

bash 复制代码
interface ICounter {
    // 所有函数必须是external
    function getCount() external view returns (uint256);
    function increment() external;
    function decrement() external;
    
    // 可以定义事件
    event CountChanged(uint256 newCount);
}

// 实现接口
contract Counter is ICounter {
    uint256 private count;
    
    event CountChanged(uint256 newCount);
    
    function getCount() external view override returns (uint256) {
        return count;
    }
    
    function increment() external override {
        count++;
        emit CountChanged(count);
    }
    
    function decrement() external override {
        count--;
        emit CountChanged(count);
    }
}

8.2 接口的特点和限制

接口的特点:

使用interface关键字

  • 不能有实现:所有函数都是声明
  • 不能有状态变量:不能定义storage变量
  • 不能有构造函数
  • 所有函数必须external
  • 可以继承其他接口
  • 可以定义事件

接口vs合约对比:

8.3 ERC20接口标准

ERC20是最经典的接口定义示例。

bash 复制代码
interface IERC20 {
    // 查询函数
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function allowance(address owner, address spender) 
        external view returns (uint256);
    
    // 操作函数
    function transfer(address to, uint256 amount) external returns (bool);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(
        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

bash 复制代码
// 定义接口
interface IToken {
    function transfer(address to, uint256 amount) external returns (bool);
    function balanceOf(address account) external view returns (uint256);
}

// 使用接口与其他合约交互
contract Exchanger {
    function swapTokens(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:不需要导入完整合约代码
相关推荐
小明的小名叫小明2 小时前
Solidity入门(8)-库合约Library
区块链
新华经济3 小时前
合规+AI双驱动,Decode Global 2025重构全球服务新生态
人工智能·重构·区块链
voidmort3 小时前
web3.py实现NFT合约全流程
区块链·web3.py
myan3 小时前
RWA 将改变华尔街——Uweb纽约游学总结(下)
区块链
九河云4 小时前
血液中心 “冷链箱 IoT + 区块链”:让每一袋血浆的 2-8℃曲线被法院采证,断链纠纷降为 0
物联网·区块链
Rockbean5 小时前
3分钟Solidity: 5.2 发送以太币(传输、发送、调用)
web3·区块链·solidity
IvorySQL9 小时前
活动回顾|Oracle 到 PostgreSQL 迁移技术网络研讨会
postgresql·oracle·区块链
怪只怪满眼尽是人间烟火9 小时前
离线环境下部署区块链FISCO BCOS v2.11.0
linux·运维·区块链
古城小栈9 小时前
AI + 区块链:去中心化智能的未来形态
人工智能·去中心化·区块链