【WEB3.0零基础转行笔记】Solidity编程篇-第2讲:StorageFactory

目录

[2.1 来自anker的信息](#2.1 来自anker的信息)

[2.1.1 核心产品与服务](#2.1.1 核心产品与服务)

[2.1.2 全球DePIN网络](#2.1.2 全球DePIN网络)

[2.1.3 成功案例与企业合作](#2.1.3 成功案例与企业合作)

[2.1.4 总结](#2.1.4 总结)

[2.2 StorageFactory简介](#2.2 StorageFactory简介)

[2.2.1 介绍](#2.2.1 介绍)

[2.2.2 章节概述](#2.2.2 章节概述)

[2.2.3 结论](#2.2.3 结论)

[2.2.4 测试一下自己](#2.2.4 测试一下自己)

核心作用详解

技术实现摘要

[💡 主要应用场景](#💡 主要应用场景)

[⚠️ 潜在考量](#⚠️ 潜在考量)

[1. 唯一标识符:解决"找谁"的问题](#1. 唯一标识符:解决“找谁”的问题)

[2. 确定性寻址:实现精准调用](#2. 确定性寻址:实现精准调用)

[3. 封装与简化用户体验](#3. 封装与简化用户体验)

[4. 访问控制与权限的基石](#4. 访问控制与权限的基石)

[5. 技术实现的必然选择](#5. 技术实现的必然选择)

[⚠️ 潜在风险与注意事项](#⚠️ 潜在风险与注意事项)

[2.3 项目设置](#2.3 项目设置)

[2.3.1 介绍](#2.3.1 介绍)

[2.3.2 StorageFactory设置](#2.3.2 StorageFactory设置)

[2.3.3 结论](#2.3.3 结论)

[2.3.4 测试一下自己](#2.3.4 测试一下自己)

1、核心含义与特征

2、在你的代码中的体现

3、为什么可组合性如此重要?

1、核心限制因素(非文件本身)

2、最佳实践(远比数量上限重要)

[2.4 从合约部署合约](#2.4 从合约部署合约)

[2.4.1 介绍](#2.4.1 介绍)

[2.4.2 创建新变量](#2.4.2 创建新变量)

[2.4.3 结论](#2.4.3 结论)

[2.4.4 测试一下自己](#2.4.4 测试一下自己)

[2.5 Solidity导入](#2.5 Solidity导入)

[2.5.1 介绍](#2.5.1 介绍)

[2.5.2 导入代码](#2.5.2 导入代码)

[2.5.3 指定导入](#2.5.3 指定导入)

[2.5.4 结论](#2.5.4 结论)

[2.5.5 测试一下自己](#2.5.5 测试一下自己)

使用命名导入的优势:

[问题 1:版本严格不兼容,导致编译失败](#问题 1:版本严格不兼容,导致编译失败)

[问题 2:版本范围过宽或过窄,引发意外行为](#问题 2:版本范围过宽或过窄,引发意外行为)

最佳实践建议:

[2.6 利用AI提供帮助(第一部分)](#2.6 利用AI提供帮助(第一部分))

[2.6.1 介绍](#2.6.1 介绍)

[2.6.2 有效提问](#2.6.2 有效提问)

[2.6.3 人工智能回答](#2.6.3 人工智能回答)

[2.6.4 其他资源](#2.6.4 其他资源)

[2.6.5 结论](#2.6.5 结论)

[2.6.6 测试一下自己](#2.6.6 测试一下自己)

[2.7 与合约ABI交互](#2.7 与合约ABI交互)

[2.7.1 介绍](#2.7.1 介绍)

[2.7.2 存储已部署的合约](#2.7.2 存储已部署的合约)

[2.7.3 简单存储交互](#2.7.3 简单存储交互)

[2.7.4 结论](#2.7.4 结论)

[2.7.5 测试一下自己](#2.7.5 测试一下自己)

[2.8 Solidity中的继承](#2.8 Solidity中的继承)

[2.8.1 介绍](#2.8.1 介绍)

[2.8.2 继承](#2.8.2 继承)

[2.8.3 Override and virtual](#2.8.3 Override and virtual)

[2.8.4 结论](#2.8.4 结论)

[2.8.5 测试一下自己](#2.8.5 测试一下自己)

[2.9 小测试](#2.9 小测试)

[2.10 本节总结与回顾](#2.10 本节总结与回顾)

[2.10.1 介绍](#2.10.1 介绍)

[2.10.2 部署和导入](#2.10.2 部署和导入)

[2.10.3 合约互动](#2.10.3 合约互动)

[2.10.4 继承和重写](#2.10.4 继承和重写)

[2.10.5 结论](#2.10.5 结论)

[2.10.6 测试一下自己](#2.10.6 测试一下自己)


2.1 来自anker的信息

了解更多关于 Ankr 的信息,请访问 https://www.ankr.com/

Ankr是一个去中心化的Web3基础设施平台,为开发者和企业提供在区块链生态系统中构建、扩展和赚取收益所需的必备工具。其核心使命是为下一代互联网应用提供高性能、可靠的基础设施。

2.1.1 核心产品与服务

Ankr的服务分为以下几个关键领域:

  • Web3 API 服务:为开发者提供连接到70多条区块链的超快速、可靠接口。它宣称平均响应时间为56毫秒,正常运行时间高达99.99%,是应用程序开发的优选。

  • 扩展服务:提供创建定制区块链(如Rollups和侧链)的完整解决方案。这允许项目启动自己的专用链,并根据特定需求进行扩展。

  • 质押解决方案:提供无缝的质押体验和集成,支持超过9种代币。用户可以通过质押资产赚取奖励,该平台目前管理的总锁定价值超过8300万美元。

2.1.2 全球DePIN网络

Ankr的一个独特优势是其去中心化物理基础设施网络。该网络由遍布全球30多个地区的裸机节点组成。这种架构确保RPC请求能以最短的路径传输,为全球用户提供快速的Web3体验。该网络处理着巨大的规模:

  • 每日处理80亿次RPC请求。

  • 每月服务76万个独立地理位置。

2.1.3 成功案例与企业合作

Ankr为主要的区块链生态系统提供支持。一个重点合作伙伴是Polygon,Ankr的基础设施使其应用速度提升了10倍,每月处理300亿次RPC请求,并支持了超过100万份智能合约。

对于大型组织,Ankr提供定制化的企业解决方案,包括定制基础设施和工程服务。

2.1.4 总结

总而言之,Ankr是Web3的基石,提供从API访问、区块链扩展到质押等一系列完整的服务,所有这些都由一个强大、去中心化的全球网络提供支持。

2.2 StorageFactory简介

2.2.1 介绍

++您可以在Remix Storage Factory 的 GitHub 代码库++中找到本节的代码。在接下来的九节课中,我们将学习三个新的合约:

(1)SimpleStorage.sol - 我们在前一节构建的合约,但进行了一些修改;

Go 复制代码
// SPDX-License-Identifier: MIT
// 这是Solidity的许可证声明,表明此合约代码采用MIT开源许可证,允许自由使用、修改和分发

// 编译器版本声明:指定此合约必须使用0.8.19版本的Solidity编译器进行编译
// 使用固定版本而非范围版本(如^0.8.0)可以确保编译行为完全一致,避免因编译器版本更新导致的不兼容
pragma solidity 0.8.19;

// 以下是几个其他常见的版本声明方式示例(当前已被注释掉):
// pragma solidity ^0.8.0; // 兼容0.8.0及以上版本,但不包括0.9.0
// pragma solidity >=0.8.0 <0.9.0; // 兼容0.8.0到0.9.0之间(不含)的所有版本

// 定义一个名为SimpleStorage的智能合约
contract SimpleStorage {
    // 声明一个无符号256位整数状态变量,用于存储最喜欢的数字
    // 这个变量默认具有internal可见性,且初始值为0
    uint256 myFavoriteNumber;

    // 定义一个结构体Person,用于表示一个人及其相关信息
    struct Person {
        uint256 favoriteNumber; // 该人最喜欢的数字
        string name;            // 该人的姓名
    }

    // 声明一个动态数组示例(当前已被注释掉):
    // uint256[] public anArray;
    
    // 声明一个Person结构体的动态数组,用于存储多个人的信息
    // 添加public可见性会自动生成一个同名的getter函数,可以通过索引访问数组元素
    Person[] public listOfPeople;

    // 声明一个映射,用于通过姓名快速查找对应的最喜欢的数字
    // 映射的键(key)是字符串类型(姓名),值(value)是无符号256位整数(最喜欢的数字)
    // 添加public可见性会自动生成一个getter函数,可以通过姓名查询对应的数字
    mapping(string => uint256) public nameToFavoriteNumber;

    // 存储函数:更新最喜欢的数字
    // - 参数_favoriteNumber: 要存储的新数字
    // - public: 函数可以从外部调用
    // - virtual: 标记此函数为"可虚拟的",意味着它可以被子合约(继承此合约的合约)重写(override)
    function store(uint256 _favoriteNumber) public virtual {
        myFavoriteNumber = _favoriteNumber;
    }

    // 检索函数:获取当前存储的最喜欢的数字
    // - view: 声明此函数为"视图函数",它只读取而不修改合约状态,调用时不消耗gas(在单独调用时)
    // - returns (uint256): 指定函数返回一个无符号256位整数
    function retrieve() public view returns (uint256) {
        return myFavoriteNumber; // 返回myFavoriteNumber状态变量的当前值
    }

    // 添加人员函数:向列表和映射中添加一个新的人员记录
    // - 参数_name: 人员姓名(字符串类型,使用memory修饰符,表示数据临时存储在内存中)
    // - 参数_favoriteNumber: 该人员最喜欢的数字
    function addPerson(string memory _name, uint256 _favoriteNumber) public {
        // 创建一个新的Person结构体实例,并将其添加到listOfPeople数组的末尾
        listOfPeople.push(Person(_favoriteNumber, _name));
        
        // 同时更新映射,建立从姓名到最喜欢数字的快速查找关系
        nameToFavoriteNumber[_name] = _favoriteNumber;
    }
}

// 以下三个是空的合约定义,可能是为后续扩展或示例预留的
contract SimpleStorage2 {
    // 这是一个空的合约,目前没有任何内容
}

contract SimpleStorage3 {
    // 这是一个空的合约,目前没有任何内容
}

contract SimpleStorage4 {
    // 这是一个空的合约,目前没有任何内容
}

(2)AddFiveStorage.sol - SimpleStorage 的一个子合约,它利用(继承了) 父合约的功能;

Go 复制代码
// SPDX-License-Identifier: MIT
// 声明此智能合约采用MIT开源许可证,允许自由使用、修改和分发

// 编译器版本声明:指定必须使用0.8.19版本的Solidity编译器
// 注意:这里使用的是固定版本(0.8.19)而非兼容版本(^0.8.19),确保编译行为完全一致
pragma solidity 0.8.19;

// 导入SimpleStorage合约
// 从SimpleStorage.sol文件中导入SimpleStorage合约,以便继承它
import {SimpleStorage} from "./SimpleStorage.sol";

// AddFiveStorage合约:继承自SimpleStorage的子合约
// 这是一个继承和函数重写(override)的示例,展示了Solidity面向对象的特性
// 关键概念:继承(inheritance) - 子合约可以访问父合约的所有非private成员,并可以重写父合约的虚函数(virtual)
contract AddFiveStorage is SimpleStorage {
    // 重写store函数:修改父合约的存储行为,将输入值加5后再存储
    // 函数签名必须与父合约中的函数完全一致(包括可见性、参数和返回类型)
    // 参数:
    //   - _favoriteNumber: 用户想要存储的数字
    // override修饰符: 明确表示此函数重写了父合约中的同名虚函数(virtual函数)
    // 注意:父合约SimpleStorage中的store函数被声明为virtual,因此可以被重写
    function store(uint256 _favoriteNumber) public override {
        // 核心逻辑:将输入的数字加5后存储到myFavoriteNumber变量中
        // myFavoriteNumber是从SimpleStorage继承来的状态变量
        // 这里重写了父合约的store函数,改变了其原始行为(父合约直接存储输入值)
        myFavoriteNumber = _favoriteNumber + 5;
    }
    
    // 注意:此合约继承了SimpleStorage的所有其他函数和状态变量:
    // 1. retrieve()函数 - 可以正常使用,返回myFavoriteNumber的当前值
    // 2. addPerson()函数 - 可以正常使用,添加人员信息到列表和映射
    // 3. listOfPeople数组和nameToFavoriteNumber映射 - 都可以正常访问
    // 4. 继承的SimpleStorage2、3、4等空合约不会被继承,只有SimpleStorage合约的内容被继承
}

// 继承与重写的重要概念:
// 1. 继承链:AddFiveStorage -> SimpleStorage
// 2. 函数重写条件:
//    a) 父函数必须标记为virtual(SimpleStorage中的store函数确实标记为virtual)
//    b) 子函数必须使用override修饰符
// 3. 状态变量:子合约可以直接访问父合约的非private状态变量(如myFavoriteNumber)
// 4. 函数可见性:重写函数的可见性必须与父函数相同(这里都是public)
// 5. 多继承:Solidity支持多重继承,但这里只展示单继承

// 使用场景示例:
// 这种模式适用于需要修改或扩展现有合约功能的场景,比如:
// 1. 为现有合约添加额外功能
// 2. 修改现有合约的部分逻辑(如这里将存储值加5)
// 3. 创建特定功能的合约变体,而不需要修改原始合约代码

// 注意事项:
// 1. 构造函数:如果父合约有构造函数,子合约需要正确处理
// 2. 初始化:重写函数时要注意不要破坏原有的逻辑或安全假设
// 3. 链上地址:部署AddFiveStorage会创建一个全新的合约地址,与原有的SimpleStorage合约地址不同

(3)StorageFactory.sol - 一个用于部署 SimpleStorage 合约并与之交互的工厂合约;

Go 复制代码
// SPDX-License-Identifier: MIT
// 声明此智能合约采用MIT开源许可证,允许自由使用、修改和分发

// 编译器版本声明:此合约兼容0.8.19及以上版本的Solidity编译器,但不包括0.9.0及以上版本
// ^符号表示"兼容版本",即>=0.8.19 <0.9.0
pragma solidity ^0.8.19;

// 导入其他合约声明
// 从SimpleStorage.sol文件中导入SimpleStorage合约
// 注释掉的代码展示了一次导入多个合约的语法示例
// import {SimpleStorage, SimpleStorage2} from "./SimpleStorage.sol";
import {SimpleStorage} from "./SimpleStorage.sol";

// StorageFactory合约:一个工厂模式合约,用于创建和管理多个SimpleStorage合约实例
contract StorageFactory {
    // 状态变量:SimpleStorage合约实例的动态数组
    // 使用public可见性会自动生成一个getter函数,允许通过索引访问数组中的合约实例
    SimpleStorage[] public listOfSimpleStorageContracts;

    // 创建SimpleStorage合约函数:部署一个新的SimpleStorage合约实例并将其添加到数组中
    // 功能:每次调用此函数都会在以太坊区块链上部署一个全新的SimpleStorage合约
    function createSimpleStorageContract() public {
        // 使用"new"关键字部署一个新的SimpleStorage合约实例
        // 这会创建一个全新的智能合约,拥有自己的存储空间和地址
        SimpleStorage simpleStorageContractVariable = new SimpleStorage();
        
        // 另一种变量命名方式的示例(已被注释掉):
        // SimpleStorage simpleStorage = new SimpleStorage();
        
        // 将新部署的合约实例地址添加到数组中
        // 这样我们可以跟踪和管理所有通过此工厂创建的SimpleStorage合约
        listOfSimpleStorageContracts.push(simpleStorageContractVariable);
    }

    // sfStore函数:在指定索引的SimpleStorage合约中存储一个数字
    // 参数:
    //   - _simpleStorageIndex: 要操作的SimpleStorage合约在数组中的索引位置
    //   - _simpleStorageNumber: 要存储的数字值
    // 原理:通过合约实例直接调用其store函数,无需手动处理ABI编码
    function sfStore(
        uint256 _simpleStorageIndex,
        uint256 _simpleStorageNumber
    ) public {
        // 以下是另一种实现方式的示例(已被注释掉):
        // 这种方式涉及地址类型转换和显式ABI调用
        // 步骤:
        // 1. 从数组中获取合约地址
        // 2. 将地址转换为address类型
        // 3. 通过类型转换和ABI调用store函数
        // SimpleStorage(address(simpleStorageArray[_simpleStorageIndex])).store(_simpleStorageNumber);
        
        // 当前实现:直接通过合约实例调用store函数
        // 这是更简洁直观的方式,Solidity会自动处理类型转换和ABI编码
        listOfSimpleStorageContracts[_simpleStorageIndex].store(
            _simpleStorageNumber
        );
    }

    // sfGet函数:从指定索引的SimpleStorage合约中检索存储的数字
    // 参数:
    //   - _simpleStorageIndex: 要查询的SimpleStorage合约在数组中的索引位置
    // 返回: 该合约中存储的数字值
    // view修饰符: 表明此函数只读取状态而不修改,调用不消耗gas(在单独调用且不修改状态时)
    function sfGet(uint256 _simpleStorageIndex) public view returns (uint256) {
        // 以下是另一种实现方式的示例(已被注释掉):
        // 涉及显式的地址类型转换和ABI调用
        // return SimpleStorage(address(simpleStorageArray[_simpleStorageIndex])).retrieve();
        
        // 当前实现:直接通过合约实例调用retrieve函数
        // 这种方式更简洁,代码可读性更好
        return listOfSimpleStorageContracts[_simpleStorageIndex].retrieve();
    }
}

// 合约设计模式说明:
// 1. 工厂模式:此合约是典型的工厂模式实现,专门用于创建和管理其他合约实例
// 2. 合约间调用:展示了如何通过合约实例直接调用其他合约的函数
// 3. 合约管理:通过数组跟踪所有创建的合约实例,便于批量管理和操作
// 4. 封装性:用户只需与StorageFactory合约交互,无需直接处理每个SimpleStorage合约的部署细节

2.2.2 章节概述

在部署 StorageFactory 合约并执行其函数createSimpleStorageContract 后,我们可以在 Remix 终端中看到出现了一笔新的交易。这是一笔由 StorageFactory 合约执行的 SimpleStorage 合约部署交易。

我们可以通过 store 函数与这个新部署的 SimpleStorage 合约进行交互。我们将通过调用 StorageFactory 合约中的 sfStore 函数来实现。此函数接受两个参数:一个已部署的 SimpleStorage 合约的索引(由于我们只部署了一个合约,因此索引为 '0'),以及要存储的 favoriteNumber 的值。

当输入 '0' 时,sfGet 函数将返回之前函数所设置的数字。

然后,通过点击 get 函数 ListOfSimpleStorageContracts,可以检索到该 SimpleStorage 合约的地址。

内容说明:这段文字描述了一个智能合约(StorageFactory)如何创建并管理另一个合约(SimpleStorage)实例的典型交互流程,常见于Solidity开发教程或演示中,用于展示工厂模式、合约间调用及状态查询。

2.2.3 结论

StorageFactory合约管理着外部合约的多个实例SimpleStorage。它提供动态部署新合约实例的功能,并允许存储和检索每个实例中的值。这些实例在一个数组中维护和组织,从而实现高效的跟踪和交互。

2.2.4 测试一下自己

(1)合约StorageFactory的主要作用是什么?

StorageFactory 合约是一个典型的智能合约工厂 ,其主要作用是作为创建和管理多个 SimpleStorage 合约实例的中央枢纽和封装层。它的核心价值在于实现了区块链上的"工厂模式"。

核心作用详解

(1)批量创建与管理

  • 作用 :通过标准化的函数 createSimpleStorageContract(),可以一键部署全新的、独立的 SimpleStorage 合约。

  • 类比 :就像一个生产产品的流水线,可以按需、批量地制造出多个相同的"产品"(SimpleStorage 合约实例)。

(2)统一交互入口

  • 作用 :用户或外部合约无需直接与每个独立的 SimpleStorage 合约交互 。只需与 StorageFactory 这个单一的入口点进行交互,通过工厂合约提供的 sfStoresfGet 函数来间接操作所有子合约。

  • 好处 :极大地简化了前端或外部合约的逻辑。例如,如果你创建了100个 SimpleStorage 合约,你不需要保存和管理100个地址,只需与一个工厂合约交互,并通过索引来指定操作对象。

(3)地址追踪与索引

  • 作用 :工厂内部维护了一个公共数组 listOfSimpleStorageContracts,自动记录所有它创建的子合约地址。

  • 好处 :实现了对已创建合约的可发现性和可管理性。任何人都可以查询工厂创建了多少合约,或获取特定索引合约的地址进行独立验证。

技术实现摘要

  • 创建createSimpleStorageContract 函数使用 new SimpleStorage() 在链上部署新合约,并将其地址存入数组。

  • 交互sfStoresfGet 函数通过合约实例直接调用 (如 listOfSimpleStorageContracts[index].store(...)),利用 Solidity 的 ABI 封装,简化了底层的地址转换和函数调用编码。

💡 主要应用场景

(1)DApp 服务多实例化:需要为每个用户或每笔交易创建一个独立、隔离的合约实例(如托管合约、游戏会话合约)。

(2)资产管理:用于创建代表唯一资产(如NFT、凭证)的多个合约。

(3)升级与测试:可以部署新版本的逻辑合约,并通过工厂统一切换或管理,而无需迁移用户状态。

⚠️ 潜在考量

(1)Gas成本 :每次调用 createSimpleStorageContract 都会产生部署一个新完整合约的 Gas 费用,成本较高。

(2)工厂单点故障:工厂合约本身的可靠性至关重要。虽然子合约代码是独立的,但它们的创建和索引依赖于工厂合约。

总结来说, StorageFactory 将复杂的多合约部署、寻址和调用逻辑封装起来,提供了一套标准化、可维护的接口,是构建可扩展和模块化DApp的重要设计模式。

(2)为什么在调用函数sfStore时指定索引很重要?

在调用 sfStore 函数时指定索引至关重要 ,因为这是工厂合约能够准确识别和定位你要操作的、由它创建的数十甚至数百个独立子合约的唯一、高效且确定的方式

简单来说,索引是工厂合约管理其"产品目录"的编号。下面从几个关键角度解释其重要性:

1. 唯一标识符:解决"找谁"的问题

(1)核心挑战StorageFactory 可以创建无数个 SimpleStorage 合约,每个都是区块链上一个完全独立、地址不同的合约。

(2)解决方案 :工厂合约内部使用一个数组 SimpleStorage[] public listOfSimpleStorageContracts 来按创建顺序记录所有子合约。索引就是该数组中每个子合约实例的唯一位置编号

(2)类比 :就像酒店前台有一排保险箱(子合约),每个客人(调用者)的财物存在不同的箱子里。前台(工厂合约)必须知道你保险箱的编号(索引),才能帮你打开正确的那个进行操作,而不是打开别人的箱子。

2. 确定性寻址:实现精准调用

当你在 sfStore 中传入索引 _simpleStorageIndex 时,合约内部执行了以下关键步骤:

// 步骤1:通过索引从数组中获取目标子合约的地址(隐式转换)

SimpleStorage targetContract = listOfSimpleStorageContracts[_simpleStorageIndex];

// 步骤2:调用该特定合约的 store 函数

targetContract.store(_simpleStorageNumber);

如果没有索引,合约将无法知道你到底想操作哪一个具体的 SimpleStorage 实例

3. 封装与简化用户体验

(1)对用户友好 :作为外部调用者,你无需记忆或管理一长串复杂的区块链地址。你只需要记住一个简单的整数索引(比如"我想操作我创建的第一个合约,索引是0")。

(2)工厂的统一接口 :无论工厂创建了多少个子合约,与它们交互的接口(sfStore, sfGet)都是相同的,只需改变索引参数。这实现了操作的标准化。

4. 访问控制与权限的基石

虽然当前示例代码没有实现,但索引机制为后续添加权限控制提供了天然的基础 。例如,你可以扩展合约,添加一个映射来记录"哪个地址创建了哪个索引的合约",然后在 sfStore 函数中检查:这样,索引就成为了链接"用户-合约所有权"关系的关键

5. 技术实现的必然选择

从技术实现角度看,使用数组和索引是最高效、最符合逻辑的方案之一:

(1)低Gas消耗:通过索引访问数组是常数时间操作(O(1)),Gas成本低且可预测。

(2)顺序明确:索引天然反映了合约的创建顺序,便于追溯和审计。

(3)可遍历:工厂或外部视图可以轻松遍历所有已创建的合约(例如,用于展示列表)。

⚠️ 潜在风险与注意事项

指定索引虽然关键,但也引入了需要开发者注意的点:

(1)索引越界:如果传入的索引值大于或等于数组当前长度,交易将会回滚(revert)。调用前需要知道有效索引范围。

(2)索引非永久 :如果合约允许删除数组中的元素(当前代码不允许),索引可能会发生变化。但在当前仅追加(push)的设计中,索引是稳定且永久的。

总结来说, sfStore 函数中的索引参数,是将一个"通用"的工厂调用,"路由"到一个"特定"的子合约实例的不可或缺的导航坐标。它是工厂模式得以运作的核心机制,在简化用户交互的同时,保证了合约间调用的精确性和可管理性。

2.3 项目设置

2.3.1 介绍

在这个 StorageFactory 设置中,我们将探讨 可组合性 的含义,展示其部署外部合约并与SimpleStorage交互的能力。

2.3.2 StorageFactory设置

你可以首先访问上一课的 GitHub 仓库,并将 SimpleStorage 合约复制到 Remix 中。该合约允许存储一个喜欢的数字、一个包含人员及其对应喜欢数字的列表、一个映射(mapping),以及多种与它们交互的功能。本课程旨在创建一个能够部署并与 SimpleStorage 合约交互的新合约。

👀❗重要提示:区块链开发的一个基本方面是合约之间无缝且无需许可的交互,这被称为可组合性。这在去中心化金融(DeFi)领域尤为重要,因为复杂的金融产品正是通过通用的智能合约接口轻松交互而构建起来的。

让我们来搭建代码的骨架,它包含 createSimpleStorageContract 函数。该函数将部署一个 SimpleStorage 合约,并将结果保存到一个存储变量中。

Go 复制代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract StorageFactory {

    function createSimplestorageContract() public {
        // How does StorageFactory know what SimpleStorage looks like?
    }
}

我们需要在这两个合约之间建立连接,因为 StorageFactory 需要完整了解 SimpleStorage 合约。一种初步的方法是将 SimpleStorage 合约的代码复制到 StorageFactory 合约的上方。

注意:允许在同一文件中包含多个合约。然而,作为最佳实践,建议每个合约仅使用一个单独的文件。

提示:为了避免混淆,只打开你当前正在处理的文件。

2.3.3 结论

在此设置中,我们将深入研究可组合性 的概念,并开发StorageFactory能够部署和与SimpleStorage合约交互的合约。

2.3.4 测试一下自己

(1)可组合性是什么意思?

可组合性 是区块链和Web3领域的核心概念,常被比作"金融乐高"。它指的是智能合约能够像乐高积木一样,被无需许可地、无缝地相互连接、调用和组合,从而构建出更复杂、功能更强的去中心化应用。

1、核心含义与特征

你可以从以下几个关键特征来理解它:

(1)无需许可

  • 含义 :任何开发者都可以读取和使用区块链上已部署的任何公开的智能合约,无需向原开发者申请授权或支付许可费。

  • 类比:就像你可以直接使用任何公开的乐高积木块,而不需要向乐高公司打报告。

(2)可重用与可拼接

  • 含义:一个合约的功能(如代币交换、借贷、数据存储)可以被其他合约直接调用,作为新应用的构建模块。

  • 例子 :你正在学习的 StorageFactory 合约,它重用SimpleStorage 的存储逻辑,并与之拼接,创造出了"批量创建和管理存储合约"的新功能。

(3)标准化接口

  • 含义:可组合性依赖于通用的标准(如ERC-20代币标准、ERC-721 NFT标准)。只要合约遵循相同的接口,它们就能相互理解、交互。

  • 例子 :正因为 SimpleStorage 合约公开了标准的 storeretrieve 函数,StorageFactory 才能通过索引数组,以统一的方式调用任意一个 SimpleStorage 实例的对应函数。

2、在你的代码中的体现

你刚刚编写的 StorageFactory.sol 就是可组合性的一个完美教学示例:

(1)组合行为StorageFactory 组合SimpleStorage 的核心功能。

(2)交互方式 :通过 import 导入合约,然后使用 new SimpleStorage() 进行部署,最后通过合约实例(如 listOfSimpleStorageContracts[index])直接调用其函数。整个过程无需许可,接口清晰。

(3)产生新功能 :单个的 SimpleStorage 只能自己存一个数字。但通过工厂合约的组合,现在可以实现批量部署、统一管理和索引查询等更强大的功能。

3、为什么可组合性如此重要?

(1)创新加速器:开发者无需从零开始,可以站在巨人的肩膀上快速搭建新应用,极大地提高了创新效率。

(2)构建复杂系统的基础:DeFi(去中心化金融)中复杂的"货币乐高"(如闪电贷、收益聚合器)正是通过将借贷、交易、质押等多个基础合约组合而成的。

(3)促进互操作性:不同项目开发的合约只要遵循标准,就可以轻松协作,形成一个庞大的、相互连接的生态系统,而不是一个个孤岛。

总结来说,可组合性不仅是技术特性,更是一种强大的协作与创新范式。 你正在学习的工厂模式,正是实践这种范式的基础技能。它让你编写的智能合约从一个孤立的功能单元,变成了未来可能被其他开发者广泛使用的"乐高积木"。

(2)一个 .sol 文件中最多可以部署多少个合约?

从技术上讲,一个 .sol 文件可以编写的合约数量没有明确的硬性上限。Solidity 编译器本身不会因为文件里合约太多而拒绝编译。

然而,在实际开发和部署中,真正重要的是 "可以成功部署多少个合约",而这主要受限于以下现实因素,而非文件本身:

1、核心限制因素(非文件本身)

(1)区块链交易 Gas 限制

  • 每个合约的部署都是一笔独立的链上交易,需要消耗 Gas。

  • 单个交易的 Gas 消耗有上限(例如以太坊主网当前的区块 Gas 限制约 3000万)。如果你在一个交易中通过一个"工厂合约"的构造函数部署多个合约,那么所有部署的总 Gas 消耗不能超过这个限制。

  • 结论:你无法在一个交易中无限制地部署合约,最终会达到 Gas 上限。

(2)编译器资源限制

  • 虽然 Solidity 编译器没有明确数量限制,但当单个文件的合约数量和总代码量极其庞大时,可能会遇到编译器的内存耗尽栈溢出错误。

  • 这属于极端情况,通常远在实际开发需求之前。

(3)合约代码大小限制

  • 以太坊虚拟机对单个已部署合约的 字节码大小 有上限(目前是 约 24KB)。

  • 这个限制是针对每个独立部署的合约 的,不是针对 .sol 文件。因此,一个文件里写几十个小合约通常没问题,但如果一个合约本身的逻辑过于复杂,就可能超过此限制而无法部署。

2、最佳实践(远比数量上限重要)

虽然技术上允许,但出于可维护性、安全性和开发效率考虑,强烈建议遵循以下原则:

(1)单一职责,单文件一个 .sol 文件通常只包含一个主合约,以及它直接依赖的库、接口或数据结构。这是社区标准的最佳实践。

(2)使用导入进行模块化 :通过 import 语句来组合多个文件中的合约,就像你的 StorageFactory.sol 导入 SimpleStorage.sol 一样。这使项目结构清晰,易于测试和审计。

(3)特殊情况:在以下少数情况下,可以将多个合约放在一个文件里:

  • 紧密相关的辅助合约:例如,一个主合约和几个仅被它使用的小型库或数据结构合约。

  • 简化部署的合约工厂:有时为了部署方便,会将工厂合约和其要创建的产品合约写在一起。

  • 学习或测试:为了方便演示和教学(就像你之前的课程内容)。

最终结论:不要问"一个文件能放多少个合约",而要问"怎么组织代码最清晰、最安全"。 对于你当前的学习和未来的项目,坚持 "一个核心合约,一个文件" 的原则,并通过 import 来组合功能,这将是最佳路径。

2.4 从合约部署合约

2.4.1 介绍

本课程涵盖了以编程方式部署一个 SimpleStorage 合约并将其保存到一个存储(或状态)变量中的过程。通过本课程,您将全面理解一个合约如何无缝地部署和管理另一个合约。

2.4.2 创建新变量

遵循 type-visibility-name 的格式,我们可以声明一个类型为 SimpleStorage 的状态变量。

Go 复制代码
// SPDX许可证标识符: MIT
pragma solidity ^0.8.19;

// 注意:这里假设已经通过import语句导入了SimpleStorage合约
// 例如: import "./SimpleStorage.sol";
// 如果没有导入,编译器将报错"未定义的类型SimpleStorage"

// StorageFactory合约:一个工厂模式合约,用于创建和管理SimpleStorage合约
contract StorageFactory {
    // 状态变量声明:类型为SimpleStorage,可见性为public,名称为simpleStorage
    // public可见性会自动生成一个同名的getter函数,允许外部查询此变量
    // 注意:变量名simpleStorage(首字母小写)与合约类型SimpleStorage(首字母大写)是不同的标识符
    SimpleStorage public simpleStorage;

    // 创建SimpleStorage合约函数:部署一个新的SimpleStorage实例并存储其引用
    // 功能:当调用此函数时,会在区块链上部署一个全新的SimpleStorage合约
    // 注意:函数名称为createSimplestorageContract,注意大小写("s"是小写,可能与命名约定不符)
    // 更好的命名可能是createSimpleStorageContract以保持一致性
    function createSimplestorageContract() public {
        // 使用new关键字部署一个新的SimpleStorage合约实例
        // new关键字告诉编译器:"请部署一个SimpleStorage类型的新合约到区块链上"
        // 部署完成后,新合约的地址将被赋值给simpleStorage状态变量
        simpleStorage = new SimpleStorage();
        
        // 重要:每次调用此函数,都会部署一个全新的SimpleStorage合约
        // 并且会覆盖之前存储在simpleStorage变量中的地址(因为这里只保存一个引用)
        // 如果需要同时管理多个SimpleStorage合约,应该使用数组或映射来存储
    }
}

👀❗重要提示:左边的 SimpleStorage 和右边的 simpleStorage 由于大小写不同,被视为完全不同的实体。SimpleStorage 指的是合约类型,而 simpleStorage 指的是变量名。

使用 new 关键字时,编译器会识别到部署新合约实例的意图。编译完成后,我们就可以进行部署。

在 Remix 中,你会注意到出现了两个按钮:一个是橙色的 createSimpleStorageContract,另一个是由 public 关键字生成的蓝色 SimpleStorage。

如果我们依次调用这两个按钮------先调用 createSimpleStorageContract,再点击 SimpleStorage------下方显示的地址就会确认我们的 SimpleStorage 合约已被成功部署。

2.4.3 结论

我们刚刚部署了一个可以通过编程方式部署另一个合约的合约,这体现了可组合性原则。这样,合约之间就可以无缝地相互了解和交互。

2.4.4 测试一下自己

(1)new📕 该关键字告诉编译器什么?

使用 new 关键字时,编译器会识别到部署新合约实例的意图。编译完成后,我们就可以进行部署。

(2)创建一个名为 AnimalFactory 的合约,其中包含一个函数 createAnimals。此函数必须能够部署另外两个合约 CowsBirds,这两个合约应为仅包含构造函数方法的简单合约。

Go 复制代码
// SPDX-License-Identifier: MIT
// 声明此智能合约采用MIT开源许可证,允许自由使用、修改和分发

// 编译器版本声明:此合约兼容0.8.19及以上版本的Solidity编译器,但不包括0.9.0及以上版本
pragma solidity ^0.8.19;

// Cows合约:表示牛的合约,存储牛的所有者和年龄信息
contract Cows {
    // 状态变量owner:存储合约部署者的地址
    // 使用public可见性会自动生成一个getter函数,允许外部查询部署者地址
    address public owner;
    
    // 状态变量age:存储牛的年龄
    // 使用public可见性会自动生成一个getter函数,允许外部查询年龄
    uint256 public age;
    
    // 构造函数:在合约部署时执行一次,用于初始化状态变量
    // 参数_age:牛的初始年龄,由部署者传入
    // 构造函数将msg.sender(即调用部署交易的地址)赋给owner,将_age赋给age
    constructor(uint256 _age) {
        owner = msg.sender;    // 记录合约的部署者地址
        age = _age;            // 设置牛的初始年龄
    }
}

// Birds合约:表示鸟的合约,存储鸟的所有者和年龄信息
// 此合约结构与Cows合约类似,遵循相同的设计模式
contract Birds {
    // 状态变量owner:存储合约部署者的地址
    address public owner;
    
    // 状态变量age:存储鸟的年龄
    uint256 public age;
    
    // 构造函数:在合约部署时执行一次,用于初始化状态变量
    // 参数_age:鸟的初始年龄,由部署者传入
    constructor(uint256 _age) {
        owner = msg.sender;    // 记录合约的部署者地址
        age = _age;            // 设置鸟的初始年龄
    }
}

// AnimalFactory合约:动物工厂合约,用于批量创建和管理Cows和Birds合约实例
// 这是一个工厂模式的实现,展示了合约如何动态创建其他合约
contract AnimalFactory {
    // 状态变量cowsArray:Cows合约实例的动态数组
    // 使用public可见性会自动生成一个getter函数,允许通过索引查询数组中的Cows合约地址
    // 数组允许工厂跟踪和管理所有创建的Cows合约实例
    Cows[] public cowsArray;
    
    // 状态变量birdsArray:Birds合约实例的动态数组
    // 使用public可见性会自动生成一个getter函数,允许通过索引查询数组中的Birds合约地址
    Birds[] public birdsArray;
    
    // 以下是被注释掉的单个实例变量示例(如需保留单个最新实例可使用):
    // Cows public cows;    // 存储单个最新Cows合约实例
    // Birds public birds;  // 存储单个最新Birds合约实例
    
    // createAnimalContracts函数:同时创建一个Cows和一个Birds合约实例
    // 这是一个公共函数,任何人都可以调用,但会消耗较多Gas(因为包含两个合约部署操作)
    // 参数_cowAge:要创建的牛的年龄,将传递给Cows合约的构造函数
    // 参数_birdAge:要创建的鸟的年龄,将传递给Birds合约的构造函数
    function createAnimalContracts(uint256 _cowAge, uint256 _birdAge) public {
        // 使用new关键字部署一个新的Cows合约实例
        // 将_cowAge参数传递给Cows构造函数,新合约的部署者是AnimalFactory合约地址
        Cows newCow = new Cows(_cowAge);
        
        // 使用new关键字部署一个新的Birds合约实例
        // 将_birdAge参数传递给Birds构造函数,新合约的部署者是AnimalFactory合约地址
        Birds newBird = new Birds(_birdAge);
        
        // 将新创建的Cows合约实例地址添加到cowsArray数组的末尾
        // 这使得工厂能够跟踪所有创建的Cows合约
        cowsArray.push(newCow);
        
        // 将新创建的Birds合约实例地址添加到birdsArray数组的末尾
        birdsArray.push(newBird);
        
        // 如果只需要保存最新实例而不需要数组,可以使用以下代码(当前已被注释掉):
        // cows = newCow;   // 将新Cows合约实例地址赋给cows状态变量(会覆盖之前的值)
        // birds = newBird; // 将新Birds合约实例地址赋给birds状态变量(会覆盖之前的值)
    }
    
    // createCow函数:单独创建一个Cows合约实例
    // 这是一个公共函数,提供了更灵活的创建方式
    // 参数_age:要创建的牛的年龄,将传递给Cows合约的构造函数
    function createCow(uint256 _age) public {
        // 部署一个新的Cows合约实例并将地址添加到cowsArray数组
        cowsArray.push(new Cows(_age));
    }
    
    // createBird函数:单独创建一个Birds合约实例
    // 这是一个公共函数,提供了更灵活的创建方式
    // 参数_age:要创建的鸟的年龄,将传递给Birds合约的构造函数
    function createBird(uint256 _age) public {
        // 部署一个新的Birds合约实例并将地址添加到birdsArray数组
        birdsArray.push(new Birds(_age));
    }
    
    // 重要注意事项:
    // 1. Gas消耗:每次调用创建函数都会消耗较多Gas,因为涉及合约部署操作
    // 2. 所有权:通过工厂创建的Cows和Birds合约,其owner字段记录的是AnimalFactory合约的地址,而不是最终用户地址
    //    这是因为msg.sender在合约创建时是AnimalFactory合约本身
    // 3. 数组增长:数组会随着创建次数不断增加,需注意区块链存储成本
    // 4. 可扩展性:当前设计允许无限创建合约实例,实际应用中可能需要添加限制或权限控制
    // 5. 检索功能:通过public数组的getter函数,可以查询任意索引处的合约地址
    //    例如:cowsArray(0) 返回第一个创建的Cows合约地址
}

2.5 Solidity导入

2.5.1 介绍

在上一课中,我们将SimpleStorage代码直接集成到了StorageFactory合约中。这样就StorageFactory可以完全访问SimpleStorage合约的功能。在这一课中,我们将探索一种更高效的方式来组织和整理代码,即使用import语句。

2.5.2 导入代码

import关键字允许合约使用其他文件中的代码,而无需将整个代码库直接包含在合约中。以下是该import关键字的两个主要优势:

(1)避免杂乱:它可以防止当前文件被大量代码行堆砌,保持文件整洁有序。

(2)简化维护: 通过将代码保存在独立的文件中,可以更容易地维护和更新各个组件,而不会影响整个代码库。例如,如果我们在 SimpleStorage 中修改某些代码行,就不得不不断地将修改后的内容复制粘贴到 StorageFactory 中。

现在您可以删除之前添加的SimpleStorage代码,并将其替换为import简写形式:

Go 复制代码
import "./SimpleStorage.sol";

🚧警告:所有 Solidity 合约必须使用相同的编译器版本进行编译。确保不同文件中编译器版本的一致性至关重要,因为每个文件都有其自身的pragma语句。

2.5.3 指定导入

让我们暂时假设 SimpleStorage 中包含多个合约,例如 SimpleStorageSimpleStorage1SimpleStorage2,且它们的规模相当大。如果我们像之前那样导入整个文件,该语句会将 import 指令替换为 SimpleStorage.sol 中所有的代码。这将导致 StorageFactory 合约进行不必要的昂贵部署。这可以通过命名导入来避免,它允许你选择性地仅导入你打算使用的特定合约:

Go 复制代码
import { SimpleStorage } from "./SimpleStorage.sol";

您还可以使用命名导入来导入多个合约:

Go 复制代码
import { SimpleStorage, SimpleStorage1 } from "./SimpleStorage.sol";

👀❗重要提示:请尽量使用命名导入而不是导入整个文件。

2.5.4 结论

import 关键字允许合约使用其他文件中的代码,而无需包含整个代码库。但是,如果这些文件中使用了不同的编译器版本,则可能会引入编译问题。

2.5.5 测试一下自己

(1)什么是命名导入?使用它有什么优势?

命名导入是指在导入外部文件时,通过明确指定所需的特定标识符(如合约、函数、变量等),而不是导入整个文件的所有内容。

使用命名导入的优势:

(1)节省部署与执行成本:只导入实际需要的合约或函数,避免将无用代码一并编译和部署到区块链上,从而减少合约大小、降低 Gas 消耗。

(2)避免命名冲突:当多个文件中存在相同名称的合约或函数时,命名导入可以明确指定来源,防止编译错误或意外覆盖。

**(3)提高代码可读性与维护性:**明确列出依赖项,使开发者一目了然地知道当前文件使用了哪些外部组件,便于后续维护和协作。

(4)减少编译时间与资源占用:仅导入必要内容可加快编译速度,尤其在大型项目中效果显著。

(2)在使用 import 语句时, pragma 关键字可能以哪些方式引发问题?请各举一例。

在使用 import 语句时,pragma 关键字(特别是 pragma solidity 版本声明)可能引发的主要问题是版本兼容性冲突。以下是两种常见的问题场景及示例:

问题 1:版本严格不兼容,导致编译失败

如果导入的文件中 pragma solidity 指定的编译器版本范围与主合约的版本范围无交集,编译器会报错并拒绝编译。

示例

  • 主合约 Main.sol 声明:

  • 被导入文件 Legacy.sol 声明:

结果 : 编译器尝试用 0.8.0 编译 Legacy.sol,但该文件可能使用了 0.8.0 中已废弃或变更的语法(如构造函数声明方式),导致编译错误。

问题 2:版本范围过宽或过窄,引发意外行为

即使版本有交集,不恰当的版本范围设定也可能导致:

  • 范围过宽:导入的文件可能意外使用了主合约版本不支持的新特性。

  • 范围过窄:限制了项目的编译器升级灵活性。

示例

  • 主合约 Main.sol 声明:

  • 被导入文件 Library.sol 声明:

潜在风险 : 如果开发者在本地用 0.8.19 编译通过,但部署环境使用 0.8.0 编译主合约,则 Library.sol 中若使用了 0.8.19 特有的功能(如特定内联汇编语法),可能导致编译失败或运行时行为异常。

最佳实践建议

(1)统一版本声明 :在项目中所有文件使用相同或兼容的 pragma solidity 范围,例如统一为 ^0.8.0

(2)使用范围交集 :确保主合约与被导入文件的版本范围存在交集,可通过工具(如 trufflehardhat)验证兼容性。

(3)明确依赖版本 :在项目配置文件(如 package.jsonhardhat.config.js)中锁定编译器版本,避免环境差异。

通过谨慎管理 pragma 版本,可以避免因导入依赖导致的编译失败或部署风险。

2.6 利用AI提供帮助(第一部分)

2.6.1 介绍

如果你遇到(很多)问题,说明你正在进行批判性思考,并且你以正确的方式看待这个问题。在本课中,我们将通过一个人工智能聊天示例,学习如何有效地提问。像ChatGPT和Bard这样的人工智能聊天平台, 其答案的有效性值得关注。

2.6.2 有效提问

例如,假设你不太明白simpleStorage和之间的区别SimpleStorage

Go 复制代码
simpleStorage = new SimpleStorage();

我们可以通过以下方式请 AI 澄清疑问:

1、仅高亮你感到困惑的行并复制它。

2、将这些行以代码块格式粘贴到你的问题中。你可以通过在文本前后添加三个反引号 ``` 来创建代码块。这样可以明确向 AI 表明你指的是某段代码,而不仅仅是普通文本。

这是 AI 提示的示例:

Go 复制代码
Hi, I'm having a hard time understanding the difference between these simple storages on this line:
```// paste the confusing line of code here```
Here is my full code:
```// paste the full code here```

2.6.3 人工智能回答

人工智能可以提供富有洞察力且非常全面的答案。例如,人工智能可能会指出,这simpleStorage是一个类型为的变量SimpleStorage,它是文件中定义的一个合约SimpleStorage.sol

👀❗重要提示:人工智能系统在解决基础编码任务方面效率极高。然而,随着代码库和项目复杂性的增加,人工智能的效能开始下降。高级任务通常需要对上下文有深刻的理解、创新性的问题解决能力以及跨领域的集成,而这些恰恰是当前人工智能能力所欠缺的。

2.6.4 其他资源

尽管人工智能聊天平台优势显著,但它们并非完美无缺,也可能会出错。在此期间,您可以参与其他平台,例如++Stack Exchange++ ,或者与您正在学习的课程主题相关的讨论论坛。例如,当您查询某个问题时SimpleStorage,人工智能的回复可能会提到一个"stored data variable",而该变量在您提供的代码中并不存在。这表明人工智能通常基于上下文进行推理,并且偶尔可能会将问题与不相关的概念联系起来。

2.6.5 结论

人工智能系统非常适合基本的编码任务,但可能难以应对需要深入理解和创新解决方案的复杂项目。如需更精确的帮助,请高亮显示代码块中的特定代码行,或使用 Stack Exchange 等其他资源进行更深入的了解。

2.6.6 测试一下自己

1、请再次复习本 Solidity 课程的第一部分。找出三个你觉得不清楚的概念,并请 AI 为你解释。

2.7 与合约ABI交互

2.7.1 介绍

在本课程中,StorageFactory 合约将升级为能够跟踪记录所有已部署的 SimpleStorage 合约。这也将使我们能够与每个已部署的合约单独进行交互。

2.7.2 存储已部署的合约

在当前的 StorageFactory 版本中,每次调用 createSimpleStorageContract 时,都会部署一个新的 SimpleStorage 合约,并将其覆盖存储在变量 SimpleStorage 中。过去的部署记录无法被跟踪保留。为解决这一问题,我们可以创建一个变量 ListOfSimpleStorageContracts,它是一个 SimpleStorage 合约类型的数组。这样,每当创建一个新合约时,就会将其添加到一个动态数组中。

Go 复制代码
SimpleStorage[] public listOfSimpleStorageContracts;

然后我们可以修改该函数createSimpleStorageContract,将新部署的合约推送到该变量中。

Go 复制代码
function createSimpleStorageContract() public {
    SimpleStorage simpleStorageContractVariable = new SimpleStorage();
    listOfSimpleStorageContracts.push(simpleStorageContractVariable);
}

在 Remix 中,您可以listOfSimpleStorageContracts通过类型索引进行访问uint256,该索引指的是已部署合约在动态数组中的位置。

2.7.3 简单存储交互

StorageFactory 可以通过调用已部署合约的 store 函数与它们进行交互。为此,我们需要创建一个函数 sfStore

Go 复制代码
function sfStore(uint256 _simpleStorageIndex, uint256 _simpleStorageNumber) public {
    //SimpleStorage store function will be called here
}

👀❗重要提示:每次您需要与其他合约进行交互时,您都需要:

(1)合约地址

(2)合约ABI(应用程序二进制接口):一种与部署在区块链上的智能合约二进制版本进行交互的标准化方式。它规定了可用于与合约交互的函数、函数参数以及数据结构。它由编译器生成。

🗒️注意:如果您没有完整的 ABI,则函数选择器就足够了(请参阅课程后面的内容)。

如果你进入 Solidity 的编译选项卡,你会找到一个按钮,可以将 ABI 复制到剪贴板。

🗒️注意:在 Solidity 中,可以将一个地址 (address)类型强制转换为合约(contract)类型。

现在我们可以开始在合约中存储新号码了SimpleStorage

Go 复制代码
function sfStore(uint256 _simpleStorageIndex, uint256 _simpleStorageNumber) public {
    listOfSimpleStorageContracts[_simpleStorageIndex].store( _simpleStorageNumber);
}

然后我们可以使用get函数检索存储的值:

Go 复制代码
function sfGet(uint256 _simpleStorageIndex) public view returns (uint256) {
    // return SimpleStorage(address(simpleStorageArray[_simpleStorageIndex])).retrieve();
    return listOfSimpleStorageContracts[_simpleStorageIndex].retrieve();
}

2.7.4 结论

StorageFactory合约能够创建一个SimpleStorage合约列表,在每个合约中存储一个变量,并读取该变量。

2.7.5 测试一下自己

(1)与外部合同方进行互动需要哪些条件?

(2)通过 StorageFactory 部署 3 个 SimpleStorage 合约的实例。然后通过 sfStore 存储一些数字,并通过 sfGet 获取所有存储的数字。

2.8 Solidity中的继承

2.8.1 介绍

在本课程中,我们将介绍继承和重写的概念,这是两个强大的工具,能够让开发者创建更加模块化、可维护和可重用的智能合约。通过运用这些技术,你可以在现有合约的基础上进行构建,并自定义其函数功能。

2.8.2 继承

我们将通过添加一项新功能来增强 SimpleStorage 合约: 为已存储的 favoriteNumber 加上五 (5)。要实现这一点,我们可以复制现有的 SimpleStorage 合约并在新版本中进行修改。然而,这种做法会导致代码冗余。更好的做法是利用继承机制,该机制允许 AddFiveStorage 合约继承 SimpleStorage 的所有功能。让我们从创建一个新文件 AddFiveStorage.sol 并命名导入 SimpleStorage.sol 开始:

Go 复制代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import {SimpleStorage} from "./SimpleStorage.sol";

contract AddFiveStorage is SimpleStorage {}

is关键字表示继承,并将父合约SimpleStorage与其子合约AddFiveStorage关联起来。

2.8.3 Override and virtual

现在,AddFiveStorage 合约已经继承了 SimpleStorage 的所有方法。我们可以为其添加新的函数,例如:

Go 复制代码
function sayHello() public pure returns(string memory) {
    return "Hello";
}

SimpleStorage我们还可以使用关键字修改现有函数override。假设我们要修改某个store函数,将"5"加到存储的收藏数字上。如果我们直接复制store函数的签名,就会出错:

Go 复制代码
function  store ( uint256 _newFavNumber ) public  {}
Go 复制代码
TypeError: Overriding function is missing "override" specifier.

🗒️注意:要覆盖父合约中的方法,我们必须复制完全相同的函数签名,包括其名称、参数,并添加可见性和override关键字:

Go 复制代码
function store(uint256 _newFavNumber) public override {}

然而,又会弹出另一个错误:

Go 复制代码
TypeError: Trying to override a non-virtual function.

为了解决这个问题,我们需要将该store函数标记SimpleStorage.sol为虚函数,使其能够被子合约重写:

Go 复制代码
function store(uint256 favNumber) public virtual {
    // function body
}

最后,我们可以为 AddFiveStorage 中的 store 函数增加新功能,使其能够在存储的 favoriteNumber 基础上加上"5"

Go 复制代码
function store(uint256 _newFavNumber) public override {
    favoriteNumber = _newFavNumber + 5;
}

2.8.4 结论

在本课中,我们利用继承修改了SimpleStorage合约,而无需重写所有代码。部署合约AddFiveStorage并存储数字 2 后,它将返回favoriteNumber7。这证实了合约store中的函数AddFiveStorage已成功覆盖了原有的store函数SimpleStorage

2.8.5 测试一下自己

(1)为什么我们需要继承来扩展合约的功能?

(2)这两个关键词是如何override一起virtual使用的?

(3) 创建一个合约Squared,重写该store函数并返回最喜欢的数字的平方。

2.9 小测试

  1. 在 Solidity 中重写父合约中的方法时,必须包含哪些内容?

    1. ❌ 函数名称、返回类型和 override 关键字。

    2. ❌ 函数名称、参数、返回类型和 virtual 关键字。

    3. ✅ 函数名称、参数、可见性、返回类型和 override 关键字。

    4. ❌ 函数参数、override 关键字和函数体。

  2. 在 Solidity 中,关键字 overridevirtual 是如何一起使用的?

    1. ❌ virtual 用于在子合约中声明函数,而 override 用于将父合约中的函数变为 final。

    2. ❌ virtual 用于防止函数被重写,而 override 用于定义不能被重写的新函数。

    3. ✅ virtual 用于父合约中,允许重写函数;而 override 用于子合约中,为该函数提供新的实现。

    4. ❌ override 用于父合约中,用于标记可以被重写的函数;而 virtual 用于子合约中,用于重写这些函数。

  3. new 关键字告诉编译器什么?

    1. ❌ 它强制编译器使用最新的字节码覆盖 EVM 中现有的合约。

    2. ❌ 它告诉编译器为结构体和数组等复杂数据类型分配内存。

    3. ❌ 它告诉编译器在循环内为动态合约执行分配 gas。

    4. ✅ 它告诉编译器,编译后将部署一个新的合约实例。

2.10 本节总结与回顾

2.10.1 介绍

本节介绍了如何部署合约、如何导入合约以及如何使用继承来定制合约的功能。

2.10.2 部署和导入

我们深入研究了如何使用new关键字来部署合约的多个实例,从而可以根据需要创建大量的合约实例。

合约也可以导入,这相当于将代码复制到文件中,但优势在于提高了代码的可重用性和模块化程度。最佳实践是使用命名导入,仅从文件中选择我们打算使用的合约。

Go 复制代码
import { Contract  as  MyContract } from  './myOtherContract.sol' ;

2.10.3 合约互动

Solidity 允许您与其他合约进行交互。为此,我们需要合约的地址及其 ABI(应用程序二进制接口):

Go 复制代码
contract AddFiveStorage is SimpleStorage {}

2.10.4 继承和重写

合约也可以通过继承从其他合约中派生函数。这可以通过 is 关键字实现。

若要显式地重写父合约中的函数,需在子合约方法中使用 override 关键字。父合约中的相应函数必须标记为 virtual,以允许该重写操作。

Go 复制代码
//child contract
import './ParentContract.sol';
contract ChildContract is ParentContract {
    function store(uint256 _num) public override {}
}
Go 复制代码
//parent contract
function store(uint256 _num) public virtual {
    // function body
}

2.10.5 结论

在本节中,我们探讨了如何使用 new 关键字部署多个合约实例,并通过合约导入来提高代码的复用性。我们还介绍了如何通过合约地址和 ABI 与其他合约进行交互。此外,我们学习了继承和函数重写的概念,使派生合约能够自定义继承的功能。

**提示:**每当你完成一个章节的学习时,不妨花点时间回顾自己的进步,为自己庆祝,并发个朋友圈分享你的成就。

2.10.6 测试一下自己

🏆 尝试回答第 1 课到第 7 课的所有理论问题,然后再返回完成所有编程任务。

相关推荐
寻寻觅觅☆5 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
时代的凡人6 小时前
0208晨间笔记
笔记
l1t6 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
今天只学一颗糖6 小时前
1、《深入理解计算机系统》--计算机系统介绍
linux·笔记·学习·系统架构
青云计划6 小时前
知光项目知文发布模块
java·后端·spring·mybatis
赶路人儿6 小时前
Jsoniter(java版本)使用介绍
java·开发语言
Victor3566 小时前
MongoDB(9)什么是MongoDB的副本集(Replica Set)?
后端
Victor3567 小时前
MongoDB(8)什么是聚合(Aggregation)?
后端
ceclar1237 小时前
C++使用format
开发语言·c++·算法
码说AI7 小时前
python快速绘制走势图对比曲线
开发语言·python