solidity第九课——事件与继承

来到基础课程中的难点与重点课,事件与继承。

一,事件

事件是合约区块链通讯的一种机制。应用“监听”某些事件,并作出反应。solidity中的事件具有两个特点:

1.响应,应用程序(ether.js)可以通过RPC接口订阅和监听这些事件,并在前端做出反应。

2.经济,事件是EVM上比较经济的存储数据的方式。

以代币ERC20合约实例一般格式为:

event Transfer(address indexed from, address indexed to, uint256 value);

我们可以看到,Transfer事件共记录了3个变量fromtovalue,分别对应代币的转账地址,接收地址和转账数量。同时fromto前面带着indexed关键字,每个indexed标记的变量可以理解为检索事件的索引“键”,在以太坊上单独作为一个topic进行存储和索引,程序可以轻松的筛选出特定转账地址和接收地址的转账事件。每个事件最多有3个带indexed的变量。每个 indexed 变量的大小为固定的256比特。事件的哈希以及这三个带indexed的变量在EVM日志中通常被存储为topic。其中topic[0]是此事件的keccak256哈希,topic[1]topic[3]存储了带indexed变量的keccak256哈希。

value 不带 indexed 关键字,会存储在事件的 data 部分中,可以理解为事件的“值”。data 部分的变量不能被直接检索,但可以存储任意大小的数据。因此一般 data 部分可以用来存储复杂的数据结构,例如数组和字符串等等,因为这些数据超过了256比特,即使存储在事件的 topic 部分中,也是以哈希的方式存储。另外,data 部分的变量在存储上消耗的gas相比于 topic 更少。我们可以在函数里释放事件。在下面的例子中,每次用_transfer()函数进行转账操作的时候,都会释放Transfer事件,并记录相应的变量。

    // 定义_transfer函数,执行转账逻辑
    function _transfer(
        address from,
        address to,
        uint256 amount
    ) external {

        _balances[from] = 10000000; // 给转账地址一些初始代币

        _balances[from] -=  amount; // from地址减去转账数量
        _balances[to] += amount; // to地址加上转账数量

        // 释放事件
        emit Transfer(from, to, amount);
    }

二,继承

这里介绍solidity中的继承(inheritance),包括简单继承,多重继承,以及修饰关键词(modifier)和构造函数(constructor)的继承。继承是面向对象编程很重要的组成部分,可以显著减少重复代码。如果把合约看作是对象的话,solidity也是面向对象的语言可以继承。

virtual:父合约里的函数,如果希望子合约重写,需要加上virtual关键字。

override:子合约重写了父合约中的函数,需要加上override关键字。

来看一串代码吧!

contract Doge{
    function catchphrase() public returns(String){
        return "So Wow cryptoDoge";
        }
}

contract BabyDoge is Doge{
    function anotherCatchphrase() public returns(String){
        return "Such Moon BabyDoge";
    }
}

由于BabyDoge是从Doge那里继承来的。意味着当你编译和部署了BabyDoge,它将可以访问catchphrase()和anotherCatchphrase()和其他我们在Doge中的其他公共函数,用继承逻辑表达子类。

solidity中的modifier同样可以继承,用法和函数继承类似,在相应的地方加virtual和override关键字即可。

contract Base1 {
    modifier exactDividedBy2And3(uint _a) virtual {
        require(_a % 2 == 0 && _a % 3 == 0);
        _;
    }
}

contract Identifier is Base1 {

    //计算一个数分别被2除和被3除的值,但是传入的参数必须是2和3的倍数
    function getExactDividedBy2And3(uint _dividend) public exactDividedBy2And3(_dividend) pure returns(uint, uint) {
        return getExactDividedBy2And3WithoutModifier(_dividend);
    }

    //计算一个数分别被2除和被3除的值
    function getExactDividedBy2And3WithoutModifier(uint _dividend) public pure returns(uint, uint){
        uint div2 = _dividend / 2;
        uint div3 = _dividend / 3;
        return (div2, div3);
    }
}

构造函数的继承:

子合约有两种方法继承父合约的构造函数。举个简单的例子,父合约A里面有一个状态变量a,并由构造函数的参数来确定:

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

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

1.在继承时声明父构造函数的参数,例如:contract B is A(1)

2.在子合约的构造函数中声明构造函数的参数,例如:

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

调用父合约的函数

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

1.直接调用:格式为父合约.函数名()

2.super关键字:子合约可以利用super.函数名()来调用最近的父合约函数。solidity继承关系按声明时从右到左的顺序。

function catchphraseSuper() public{
    super.__()
}