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();
}
}