欢迎光临
我们一直在努力

Solidity入门(7)- 合约继承

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

文章目录

  • 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;
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)是面向对象编程的核心特性,它允许一个合约(子合约)继承另一个合约(父合约)的属性和方法。

使用继承的做法:

// 基础合约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 单继承基础语法

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

基本语法:

contract Parent {
// 父合约代码
}

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

关键字说明:

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

简单示例:

// 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
}
}

子合约获得了什么:

// 部署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 {
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关键字用于调用父合约的函数,即使子合约已经重写了该函数。

基本概念:

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指向唯一的父合约。

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不是指向单个父合约,而是按照继承顺序调用下一个合约。

关键理解:

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 是共同的基类,放在最后。

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:表示这个函数可以被重写
    function getValue() public virtual returns (uint256) {
    return 100;
    }
    }

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

    6.2 函数签名必须匹配

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

    必须相同的部分:

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

    可以不同的部分:

    • 可见性(可以更开放,不能更严格)
    • 状态修饰符(可以更严格,不能更宽松) 示例:

    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

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

    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";
    // }
    }

    语法规则:

    // 单继承:简单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在重写中调用父函数

    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)是包含至少一个未实现函数的合约。

    定义语法:

    abstract contract 合约名 {
    // 至少一个未实现的函数
    }

    基本示例:

    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)是纯粹的接口定义,只声明函数签名,不包含任何实现。

    定义语法:

    interface 接口名 {
    // 只有函数声明
    }

    基本示例:

    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是最经典的接口定义示例。

    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

    // 定义接口
    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:不需要导入完整合约代码
    赞(0)
    未经允许不得转载:171主机测评 » Solidity入门(7)- 合约继承
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址