Solidity笔记
原项目参考:https://github.com/AmazingAng/WTF-Solidity
12、事件
事件(event)是EVM上日志的抽象,它具有两个特点:
- 响应:应用程序(
ethers.js)可以通过RPC接口订阅和监听这些事件,并在前端做响应。 - 经济:事件是
EVM上比较经济的存储数据的方式,每个大概消耗2,000gas;相比之下,链上存储一个新变量至少需要20,000gas。
12.1、声明事件
- 由
event关键字开头,接着是事件名称,括号里面写好事件需要记录的变量类型和变量名。以ERC20代币合约的Transfer事件为例:
// Transfer事件共记录了3个变量from,to和value,分别对应代币的转账地址,接收地址和转账数量,其中from和to前面带有indexed关键字,他们会保存在以太坊虚拟机日志的topics中,方便之后检索。
event Transfer(address indexed from, address indexed to, uint256 value);
12.2、释放事件
event Transfer(address indexed from, address indexed to, uint256 value);
mapping(address => uint256) public _balances;
// 用_transfer()函数进行转账操作的时候,都会释放Transfer事件,并记录相应的变量
function _transfer(address from, address to, uint256 amount) external {
_balances[from] = 10000000;
_balances[from] -= amount;
_balances[to] += amount;
// 释放事件
emit Transfer(from, to, amount);
}
12.3、EVM日志Log
- 以太坊虚拟机(EVM)用日志
Log来存储Solidity事件,每条日志记录都包含主题topics和数据data两部分。
12.4、主题topics
日志的第一部分是主题数组,用于描述事件,长度不能超过4。它的第一个元素是事件的签名(哈希)。对于上面的Transfer事件,它的签名就是:
keccak256("Transfer(address,address,uint256)")
//0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
除了事件签名,主题还可以包含至多3个indexed参数,也就是Transfer事件中的from和to。
indexed标记的参数可以理解为检索事件的索引“键”,方便之后搜索。每个 indexed 参数的大小为固定的256比特,如果参数太大了(比如字符串),就会自动计算哈希存储在主题中。
12.5、数据data
事件中不带 indexed的参数会被存储在 data 部分中,可以理解为事件的“值”。data 部分的变量不能被直接检索,但可以存储任意大小的数据。因此一般 data 部分可以用来存储复杂的数据结构,例如数组和字符串等等,因为这些数据超过了256比特,即使存储在事件的 topics 部分中,也是以哈希的方式存储。另外,data 部分的变量在存储上消耗的gas相比于 topics 更少。
13、继承
继承是面向对象编程很重要的组成部分,可以显著减少重复代码。如果把合约看作是对象的话,solidity也是面向对象的编程,也支持继承。
13.1、规则
virtual: 父合约中的函数,如果希望子合约重写,需要加上virtual关键字。override:子合约重写了父合约中的函数,需要加上override关键字。
注意:用override修饰public变量,会重写与变量同名的getter函数,例如:
mapping(address => uint256) public override balance0f;
13.2、简单继承
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
// 先写一个简单合约
contract Yeye {
event Log(string msg);
// 定义三个function hip() pop() man()
function hip() public virtual {
emit Log("Yeye");
}
function pop() public virtual {
emit Log("Yeye");
}
function yeye() public virtual {
emit Log("Yeye");
}
}
// 再定义一个子合约,继承Yeye
contract Baba is Yeye {
// 使用override重写hip()和pop()
function hip() public virtual override {
emit Log("Baba");
}
function pop() public virtual override {
emit Log("Babe");
}
function foo() public virtual {
emit Log("Baba");
}
function baba() public virtual {
emit Log("Baba");
}
}
13.3、多重继承
- 继承时要按辈分最高到最低的顺序排。比如我们写一个
Erzi合约,继承Yeye合约和Baba合约,那么就要写成contract Erzi is Yeye, Baba,而不能写成contract Erzi is Baba, Yeye,不然就会报错。 - 如果某一个函数在多个继承的合约里都存在,比如例子中的
hip()和pop(),在子合约里必须重写,不然会报错。 - 重写在多个父合约中都重名的函数时,
override关键字后面要加上所有父合约名字,例如override(Yeye, Baba)。
// 再定义一个子合约,继承上面两个合约
contract Erzi is Yeye, Baba {
function hip() public virtual override (Yeye, Baba) {
emit Log("Erzi");
}
function pop() public virtual override (Yeye, Baba) {
emit Log("Erzi");
}
function foo() public virtual override (Baba) {
emit Log("Erzi");
}
}
13.4、修饰器的继承
- 修饰器(
Modifier)同样可以继承,用法与函数继承类似,在相应的地方加virtual和override关键字即可。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
contract Base1 {
modifier exactDividedBy2And3(uint _a) virtual {
require(_a % 2== 0 && _a % 3 == 0);
_;
}
}
contract Identifier is Base1 {
// 一个分别被2和3整除的值,传入时需要经过检查
function getDivided2a3(uint _num) public exactDividedBy2And3(_num) pure returns(uint, uint) {
return getDivided2a3NoModifier(_num);
}
function getDivided2a3NoModifier(uint _num) public pure returns(uint, uint) {
uint d1 = _num / 2;
uint d2 = _num / 3;
return (d1, d2);
}
}
Identifier合约可以直接在代码中使用父合约中的exactDividedBy2And3修饰器,也可以利用override关键字重写修饰器:
contract Identifier is Base1 {
modifier exactDividedBy2And3(uint _a) virtual override {
require(_a % 2==0);
_;
}
// ...
}
13.5、构造函数的继承
子合约有两种方法继承父合约构造函数
- 在继承时声明父构造函数的参数,例如:
contract B is A(1) - 在子合约的构造函数中声明构造函数的参数,例如:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
// 父合约A有一个状态变量a
abstract contract A {
uint public a;
constructor(uint _a) {
a = _a;
}
}
contract C is A {
constructor(uint _c) A(_c * _c) {}
}
// 如果派生合约未能给其继承的基合约指定构造函数参数时,那么,该派生合约必须声明为抽象合约(abstract contract)。
// 在 Solidity 0.8.x版本以上,抽象合约的抽象函数需加上virtual修饰,而对于的派生合约中的函数实现也得加上override修饰,否则编译过不了。
abstract contract Animal {
function eat() public virtual;
}
contract Cat is Animal {
function eat() public override {}
}
13.6、调用父合约的函数
子合约有两种方式调用父合约的函数,直接调用和利用super关键字。
- 直接调用:子合约可以直接用
父合约名.函数名()的方式来调用父合约函数,例如Yeye.pop()。
function callParent() public{
Yeye.pop();
}
super关键字:子合约可以利用super.函数名()来调用最近的父合约函数。solidity继承关系按声明时从右到左的顺序是:contract Erzi is Yeye, Baba,那么Baba是最近的父合约,super.pop()将调用Baba.pop()而不是Yeye.pop():
function callParentSuper() public{
// 将调用最近的父合约函数,Baba.pop()
super.pop();
}
13.7、钻石继承
在面向对象编程中,钻石继承(菱形继承)指一个派生类同时有两个或两个以上的基类。
在多重+菱形继承链条上使用
super关键字时,需要注意的是使用super会调用继承链条上的每一个合约的相关函数,而不是只调用最近的父合约。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
/* 继承树:
Animal
/ \
pig dog
\ /
people
*/
contract Animal {
event Log(string message);
function foo() public virtual {
emit Log("Animal.foo called");
}
function bar() public virtual {
emit Log("Animal.bar called");
}
}
contract Pig is Animal {
function foo() public virtual override {
emit Log("Pig.foo called");
}
function bar() public virtual override {
emit Log("Pig.bar called");
super.bar();
}
}
contract Dog is Animal {
function foo() public virtual override {
emit Log("Dog.foo called");
Animal.foo();
}
function bar() public virtual override {
emit Log("Dog.bar called");
super.bar();
}
}
contract People is Pig, Dog {
function foo() public virtual override(Pig, Dog) {
super.foo();
}
function bar() public virtual override(Pig, Dog) {
super.bar();
}
}