保姆级Solidity教程二:进阶语法,继承、接口、抽象合约、库和异常处理

上一篇文章介绍了Solidity的基本数据类型、函数书写和remix的基本使用方法;这一章准备介绍一些Solidity的继承、重载、接口和异常处理等进阶知识

引入合约

solidity支持利用import关键字导入其他源代码中的合约,让开发更加模块化。

可以在一个.sol文件里写多个合约

// SPDX-License-Identifier: MIT 
pragma solidity ^0.8.7;

// 第一个合约
contract SimpleStorage {
    uint256 favoriteNumber;
    mapping(string => uint256) public nameToFavoriteNumber;
    struct People {
        uint256 favoriteNumber;
        string name;
    }
    // uint256[] public anArray;
    People[] public people;
    function store(uint256 _favoriteNumber) public virtual {
        favoriteNumber = _favoriteNumber;
    }
    function retrieve() public view returns (uint256){
        return favoriteNumber;
    }
    function addPerson(string memory _name, uint256 _favoriteNumber) public {
        people.push(People(_favoriteNumber, _name));
        nameToFavoriteNumber[_name] = _favoriteNumber;
    }
}

// 新的合约,在这个里面创建另一个合约
contract StorageFactory {
  	// 创建一个合约名
    SimpleStorage public simpleStorage;

    function createSimpleStorageContract() public {
        // new关键字会让 solidity 知道 我们需要部署一个新的SimpleStorage合约
        simpleStorage = new SimpleStorage();
    }
}

在部署时 就会显示 SimpleStorage, StorageFactory这两个合约

保姆级Solidity教程二:进阶语法,继承、接口、抽象合约、库和异常处理

或者是创建一个合约文件,然后把上面的SimpleStorage合约引入

// SPDX-License-Identifier: MIT 
pragma solidity ^0.8.7;

import "./SimpleStorage.sol";

// 新的合约,在这个里面创建另一个合约
contract StorageFactory {
  	// 创建一个合约名
    SimpleStorage public simpleStorage;

    function createSimpleStorageContract() public {
        // new关键字会让 solidity 知道 我们需要部署一个新的SimpleStorage合约
        simpleStorage = new SimpleStorage();
    }
}

继承 (inheritance)

继承是一种面向对象编程的概念,允许一个合约(子合约)从另一个合约(父合约)中继承功能和属性。子合约可以继承父合约的状态变量、函数和修饰器等。

继承的合约可以访问所有非 private 的成员。

首先有一个基础的合约SimpleStorage:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.8;

contract SimpleStorage{
  uint256 favoriteNumber;
  
    // mapping 创建一个映射,起一个名字并把它公开
    mapping(string=>uint256) public nameToFavoriteNumber;

    // 定义一个结构体,名为People
    struct People{
        uint256 favoriteNumber;
        string name;
    }

    // 定义一个数组
    People[] public peopleList;

    function store(uint256 _favoriteNumber) public {
        favoriteNumber = _favoriteNumber;
    }

}

再创建一个文件ExtraStorage.sol引入SimpleStorage

is 关键字就是用于继承

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.8;

import "./SimpleStorage.sol";

contract ExtraStorage is SimpleStorage {

}

部署之后就会发现 ExtraStorage 有和 SimpleStorage合约一样的方法和变量

重写

在SimpleStorage有一个函数 store, 是赋值全局变量 favoriteNumber 的

保姆级Solidity教程二:进阶语法,继承、接口、抽象合约、库和异常处理

现在我有一个需求:想在ExtraStorage合约里在赋值的时候 多 +5

这个时候就需要用重写了,重写的时候需要遵守以下两点:

  1. virtual: 父合约中的函数,如果希望子合约重写,需要加上virtual关键字。
  2. override:子合约重写了父合约中的函数,需要加上override关键字。
  1. 在父合约的原函数添后面添加 virtual
    function store(uint256 _favoriteNumber) public virtual {
        favoriteNumber = _favoriteNumber;
    }
  1. 在继承的合约里 ExtraStorage的 函数里用 override 修饰
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.8;

import "./SimpleStorage.sol";

contract ExtraStorage is SimpleStorage {

    function store(uint256 _favoriteNumber) public override{
        favoriteNumber = _favoriteNumber+5;
    }

}

多重继承

solidity的合约可以继承多个合约,书写的时候需要遵守一定的规则

  1. 继承时要按辈分最高到最低的顺序排。比如我们写一个My合约,继承Yeye合约和Baba合约,那么就要写成contract My is Yeye, Baba,而不能写成contract My is Baba, Yeye,不然就会报错。

  2. 如果某一个函数在多个继承的合约里都存在,比如Yeye合约和Baba合约都有 setName()函数,在子合约里必须重写,不然会报错。

  3. 重写在多个父合约中都重名的函数时,override关键字后面要加上所有父合约名字,例如override(Yeye, Baba)

contract Erzi is Yeye, Baba{
    // 继承两个function: hip()和pop(),输出值为Erzi。
    function setName() public virtual override(Yeye, Baba){
        // ...
    }

构造函数的继承

// 构造函数的继承
abstract contract A {
    uint public a;

    constructor(uint _a) {
        a = _a;
    }
}

子合约有两种方法继承父合约的构造函数:

方法一: 在继承时声明父构造函数的参数

contract B is A(1){

}

方法二: 在子合约的构造函数中声明构造函数的参数

contract C is A{
    constructor(uint _c) A(_c * _c) {}
}

调用父合约的函数

子合约有两种方式调用父合约的函数,直接调用和利用super关键字

  1. 直接调用:子合约可以直接用父合约名.函数名()的方式来调用父合约函数,例如Yeye.setName()
    function callParent() public{
        Yeye.setName();
    }
  1. super关键字:子合约可以利用super.函数名()来调用最近的父合约函数。

solidity继承关系按声明是从右到左的顺序contract My is Yeye, Baba,那么Baba是最近的父合约,super.setName()将调用Baba.setName()而不是Yeye.setName()

    function callParentSuper() public{
        // 将调用最近的父合约函数,Baba.setName()
        super.setName();
    }

重载

Solidity 函数重载是指 Solidity 中可以有多个具有相同名称但不同参数的函数。这些函数的名称相同,但是它们的参数数量、类型或顺序不同。 solidity不允许修饰器(modifier)重载。

下面这个例子有两个叫f()的函数,一个参数为uint8,另一个为uint256:

    function f(uint8 _in) public pure returns (uint8 out) {
        out = _in;
    }

    function f(uint256 _in) public pure returns (uint256 out) {
        out = _in;
    }

抽象合约

如果一个智能合约里有一个未实现的函数,即某个函数缺少主体{}中的内容,则必须将该合约标为abstract,不然编译会报错;另外,未实现的函数需要加virtual,以便子合约重写。

abstract contract InsertionSort{
    function insertionSort(uint[] memory a) public pure virtual returns(uint[] memory);
}

接口(interface)

可以把接口比喻为一个合约的一种规范,它指定了合约应该提供哪些功能和行为,但并不涉及具体实现的细节接口定义了一组函数头,包括函数的名称、参数类型和返回类型,但没有函数体。

保姆级Solidity教程二:进阶语法,继承、接口、抽象合约、库和异常处理

interface IERC20 {
    // ...
    function myFunction(uint256 x) external view returns (uint256);
}

接口有哪些特性?

  • 接口不能实现任何函数;
  • 接口无法继承其它合约,但可以继承其它接口;
  • 接口中的所有函数声明必须是external的;
  • 接口不能定义构造函数;
  • 接口不能定义状态变量;

库(library)

库与合约类似,但主要用于重用代码。库包含其他合约可以调用的函数。
把可以反复利用的代码独立出来,就成为一个库。

使用library关键字来定义库。库的定义类似于合约的定义,但没有状态变量。

定义库合约和函数

// 定义一个名为MathLibrary的库。
library MathLibrary {

    function square(uint256 x) external pure returns (uint256) {
       return x * x;
    }

}

库的使用也有一定的限制:

  1. 库不能定义状态变量;

  2. 库不能发送接收以太币;

  3. 库不可以被销毁,因为它是无状态的。

  4. 库不能继承和被继承;

使用

使用指令using A for B;可用于将库 A的所有函数附加到到任何类型B。添加完指令后,库 A中的函数会自动添加为B类型变量的成员,可以直接使用B.functionName()调用。

使用库名+函数名的方式即可调用库合约中的函数。

pragma solidity ^0.8.0;
// 定义一个库合约和方法
library MathLibrary {
    function square(uint256 x) external pure returns (uint256) {
        return x * x;
    }
}

contract ExampleContract {
    //
    using MathLibrary for uint256;
    
    function calculateSquare(uint256 y) external pure returns (uint256) {
        // 调用库合约的函数,y变量将默认作为第一个参数传入square函数。
        return y.square();
    }
}

事件 Event

事件(Event)是一种用于在智能合约中发布通知和记录信息的机制。它可以在合约执行期间发出消息,允许外部应用程序监听并对这些消息做出响应。

使用event关键字来声明一个事件,其后是事件名

//在这里我们定义了一个名为EventName的事件,其有parameter1和parameter2两个参数。
event EventName(
  uint256 parameter1,
  uint256 parameter2
);

触发事件 emit

要广播一个事件,你需要使用 emit 关键字。emit 用于初始化事件,并根据事件的定义设置所有需要的信息,然后广播这个事件。这样,所有订阅了该事件的人或系统就会收到通知。

提交事件使用emit关键字,其后跟事件名和参数即可。

pragma solidity ^0.8.0;

contract EmitExample {
  // 定义事件
  event MessageSent(address sender, string message);

  // 发送消息函数
  function sendMessage(string memory message) public {
    // 触发事件
    emit MessageSent(msg.sender, message);
  }
}

索引 indexed

在Solidity中,事件的参数默认是不可搜索的,也就是说,你不能直接根据事件参数的值来过滤和搜索事件。然而,当你将某个参数标记为indexed时,Solidity会为该参数创建一个额外的索引,使得你可以根据该参数的值进行过滤和搜索。

pragma solidity ^0.8.0;

contract EventExample {
  // 定义事件,其中sender可被搜索
  event MessageSent(address indexed sender, string message);

  // 发送消息函数
  function sendMessage(string memory message) public {
    // 触发事件
    emit MessageSent(msg.sender, message);
  }
}

异常处理机制

solidity有多种抛出异常的方法:requireerrorassert

require

使用方法:require(检查条件,"异常的描述"),当检查条件不成立的时候,就会抛出异常。

    function transferOwner2(uint256 tokenId, address newOwner) public {
        require(_owners[tokenId] == msg.sender, "Transfer Not Owner");
        _owners[tokenId] = newOwner;
    }

当检查条件不成立的时候,就会抛出异常(执行()内的文字) 条件成立则继续往下执行

gas费用会随着描述异常的字符串长度增加

revert

revert语句的作用是立即停止当前函数的执行,并撤销所有对状态的更改。

revert()函数在没有任何参数的情况下使用,用于终止函数的执行并回滚所有状态变化。它会自动返回一个默认的错误消息,指示函数执行失败。

也可以在 revert 关键字后附带一个字符串参数,以提供自定义的错误消息。这样可以在函数终止时提供更具体和详细的错误信息,方便开发者和用户理解发生的错误。

revert();
revert("Custom error message");

自定义 error 错误类型

error: 用于表示合约执行过程中的异常情况。它可以让开发者定义特定的错误状态,以便在合约的执行过程中进行特定的异常处理。

在Solidity中,定义错误类型使用关键字error,随后是参数。

//使用error关键字定义了一个名为MyCustomError的自定义错误类型
//并指定错误消息类型为string 和 uint。
error MyCustomError(string message, uint number);

function process(uint256 value) public pure {

		//检查value是否超过了100。如果超过了限制,我们使用revert语句抛出自定义错误
		//并传递错误消息"Value exceeds limit" 和value。
		if (value >100) revert MyCustomError("Value exceeds limit",value);

}

assert

assert语句应用于检查一些被认为永远不应该发生的情况

assert的作用和revert没有区别,但在gas的消耗上有较大的差异:

  1. assert:使用assert时,它会消耗掉调用者所发送的剩余未使用的gas。

  2. revert和require:与assert不同,当使用revert或require时,Solidity会将未使用的gas退还给调用者。

保姆级Solidity教程二:进阶语法,继承、接口、抽象合约、库和异常处理

try-catch

在处理错误时执行其他逻辑,而不仅仅是终止函数执行。这就需要使用try-catch语句了。

使用try-catch语句来处理可能存在的错误。并且可以使用catch (error err)语句来捕获特定的错误类型:

try recipient.send(amount) {
    // 正常执行的处理逻辑
} catch Error(string memory err) {
    // 捕获特定错误类型为Error的处理逻辑
    // 可以根据错误信息err进行相应的处理
} catch (bytes memory) {
    // 捕获其他错误类型的处理逻辑
    // 处理除了已声明的特定类型之外的所有错误
}

函数修饰符(modifier )

函数修饰符在修饰的函数执行之前被调用,允许在函数执行之前进行额外的检查或操作。modifier的主要使用场景是运行函数前的检查,例如地址,变量,余额等。

保姆级Solidity教程二:进阶语法,继承、接口、抽象合约、库和异常处理

在定义函数修饰符时,通过modifier关键字来定义,其定义方式和函数一样,唯一的区别在于modifier关键字取代了function关键字。_; 表示继续执行被修饰的函数。

定义一个叫做onlyOwner的modifier:

   // 定义modifier
   modifier onlyOwner {
      require(msg.sender == owner); // 检查调用者是否为owner地址
      _; // 如果是的话,继续运行函数主体;否则报错并revert交易
   }

带有onlyOwner修饰符的函数只能被owner地址调用:

   function changeOwner(address _newOwner) external onlyOwner{
      owner = _newOwner; // 只有owner地址运行这个函数,并改变owner
   }

原文链接:https://juejin.cn/post/7339887346433146916 作者:董员外

(0)
上一篇 2024年2月27日 下午4:43
下一篇 2024年2月27日 下午4:54

相关推荐

发表回复

登录后才能评论