🐼8 合约和事件
[TOC]
合约
Solidity 合约类似于面向对象语言中的类。
合约中有用于数据持久化的状态变量,和可以修改状态变量的函数。
调用另一个合约实例的函数时,会执行一个 EVM 函数调用,这个操作会切换执行时的上下文,这样,前一个合约的状态变量就不能访问了。
创建合约时,合约的构造函数(constructor)只会执行一次,构造函数执行完毕后,合约的最终代码会部署到区块链上,代码包括所有公共和外部函数以及所有可以通过函数调用访问的函数。部署的代码没有包括构造函数代码或构造函数调用的内部函数。
构造函数参数在合约代码之后通过 ABI 编码传递,但是使用 web3.js 则不必关心这个问题。
一个合约想要创建另一个合约,那么创建者必须知道被创建合约的源代码(和二进制代码),所以不可能循环创建依赖项。
合约的继承
Solidity 使用
is关键字声明继承,可以多重继承。
继承多个合约,在区块链上也只有一个合约被创建,所有父类合约的代码被编译到创建的合约中。通过
super.f()对父类合约函数的调用是内部函数调用, 是使用JUMP跳转而不是消息调用。继承多个合约,各个父类不能有相同的状态变量、函数(函数名+函数参数),否则无法通过编译。
子合约不能覆盖父类的状态变量,覆盖被视为错误(DeclarationError: Identifier already declared.),因此子合约不可以声明在父合约中可见的(除private)状态变量具有相同的名称。
合约的构造函数 constructor
构造函数是使用 constructor 关键字声明的一个可选函数, 它在创建合约时执行一次, 可以在其中运行合约初始化代码。
状态变量初始化在构造函数执行之前,构造函数中也可以直接调用成员函数,但是通过
this.函数();调用无法通过编译,因为构造函数执行完成之前还没有实例,如果构造函数中调用的成员函数内部使用了this.可以通过编译但是部署会失败。构造函数运行后, 将合约的最终代码部署到区块链。代码的部署需要 gas 与代码的长度线性相关,此代码包括所有函数部分是公有接口以及可以通过函数调用访问的所有函数。它不包括构造函数代码或仅从构造函数调用的内部函数。
如果没有编写构造函数, 合约将采用默认的空构造函数
constructor() {}。如果想让合约在首次被挖出时就做些事情可以使用构造函数。在构造函数中写的任何内容都会在首次被挖出时执行,例如在构造函数初始化一些参数等。
在 0.4.22 版本之前, 构造函数定义为合约的同名函数
constructor demo(){},语法在0.5.0之后弃用了。在 0.7.0 版本之前, 需要通过 internal 或 public 指定构造函数的可见性。
函数重写与 super
父合约标记为 virtual 的函数可以在继承合约里重写,重写的函数需要使用关键字 override 修饰。
重写函数只能将覆盖函数的可见性从 external 更改为 public 。
函数类型可以更改为更严格的一种: nonpayable 可以被 view 和 pure 覆盖, view 可以被 pure 覆盖,payable 不能更改为任何其他可变性。
被重写的函数依然可以通过
super.调用。
父类构造函数的参数
父构造函数有参数, 派生合约需要指定所有参数,可以通过两种方式来实现。
第一种方式,直接在继承列表中调用父类构造函数
is Base(参数)。第二种方式,像修改器 modifier 的使用方法一样,作为子合约构造函数定义头的一部分
Base(参数)。如果构造函数参数是常量并且定义或描述了合约的行为,使用第一种方法比较方便。 如果基类构造函数的参数依赖于子合约,那么必须使用第二种方式。
参数必须在两种方式中选择使用一种,两种方式都使用会发生错误(Base constructor arguments given twice)。
如果子合约没有给所有父类合约指定参数,则这个合约将是抽象合约。
多重继承与线性化
Solidity 实现多重继承借鉴了 Python 的方式并且使用 “C3 线性化”强制一个由父类构成的 DAG(有向无环图)保持一个特定的顺序,达到唯一化的结果。
父类在 is 后面的顺序很重要,列出父类合约的顺序从 “最高” 到 “最低” ,此顺序与 Python 中使用的顺序相反。
当一个在不同的合约中多次定义函数被调用时,给定的父类以从右到左 (Python 是从左到右) 按深度优先的方式进行搜索,在第一次匹配的时候停止。 如果父类合约已经搜索过, 则跳过该合约。
由于必须显式覆盖从多个父类继承的函数,因此C3线性化在实践中并不是太重要。
当继承层次结构中有多个构造函数时,继承线性化特别重要。
构造函数将始终以线性化顺序执行,无论在继承合约的构造函数中提供其参数的顺序如何。
抽象合约 abstract
如果未实现合约中的函数,则需要将合约标记为 abstract。 即使实现了所有函数,合约也可能被标记为abstract。
抽象合约不能直接实例化,抽象合约本身可以有实现的函数,也可以实现所有函数。
合约继承自抽象合约,并且没有通过重写来实现所有未实现的函数, 那它依然需要标记为抽象 abstract 合约。
抽象合约将合约的定义与其实现脱钩,从而提供了更好的可扩展性和自文档性,并简化了诸如 Template 方法的模式并消除了代码重复。抽象合约的使用方式与接口 interface 中定义方法的使用方式相同。
合约中的 msg
在合约中经常看到
msg.sender,这个msg全局变量是合约的调用者。
msg在构造函数中时是合约的创建者,因为构造函数是在合约创建时(部署上链)执行,而在外部函数调用中谁调用谁就是msg,如发起转账transfer的用户,发起转账者就是msg。
msg主要成员:
msg.data
bytes
完整的 calldata
msg.sender
address
消息发送者(当前调用者)
msg.sig
bytes4
calldata 的前 4 字节(也就是函数标识符)
msg.value
uint
随消息发送的 wei 的数量
事件 Event
事件是以太坊虚拟机(EVM)日志基础设施提供的一个便利接口。当被发送事件(调用)时,会触发参数存储到交易的日志中(一种区块链上的特殊数据结构)。这些日志与合约的地址关联,并记录到区块链中。
区块链是打包一系列交易的区块组成的链条,每一个交易“收据”会包含0到多个日志记录,日志代表着智能合约所触发的事件。应用程序可以通过以太坊客户端的 RPC 接口订阅和监听这些事件。
事件在合约中可被继承。当他们被调用时,会使参数被存储到交易的日志中(区块链中的一种特殊数据结构)。
这些日志与地址相关联,被并入区块链中,只要区块可以访问就一直存在,在 Serenity 版本(以太坊2.0)中可能会改动。
日志和事件在合约内不可直接被访问,包括创建日志的合约也不能访问。
外部实体需要该日志实际上存在于区块链中的证明,可以请求日志的 Merkle 证明, 但是由于合约中仅能访问最近的 256 个区块哈希,所以还需要提供区块头信息。
一个外部实体提供了一个带有这种证明的合约,它可以检查日志是否真实存在于区块链中。
事件可用于记录智能合约函数被调用时的操作日志,前提是函数中具有触发事件的代码,并且执行过程中没有触发 require、assert、revert 异常。事件通过 Web3 回调的方式进行监听。调用事件时可以将事件的参数永久存储在区块链的交易日志中,事件可以根据参数值进行过滤,支持最多三个参数的过滤,以方便监听事件时设置过滤条件,对需要过滤的参数添加关键字 indexed,DApp 可以根据过滤条件进行过滤。
监听事件应用
可以通过 Web3 监听事件实现代币跨链功能,也可以将事件做过滤器功能。如果使用 web3.js 进行事件监听要使用 websocket 连接方式。
Last updated