可支付回退函数
合约可以有一个未命名的函数。这个函数不能有参数也不能有返回值。 如果在一个到合约的调用中,没有其他函数与给定的函数标识符匹配(或没有提供调用数据),那么这个函数(fallback 函数)会被执行。即使 fallback 函数不能有参数,仍然可以使用 msg.data
来获取随调用提供的任何有效数据。
除此之外,每当合约收到以太币(没有任何数据),这个函数就会执行。此外,为了接收以太币,fallback 函数必须标记为 payable
。 如果不存在这样的函数,则合约不能通过普通转账交易接收以太币。
在最坏的情况下,回退函数只有 2300 gas 可以使用(如,当使用 send 或 transfer 时), 除了基础的日志输出之外,进行其他操作的余地很小。下面的操作消耗会操作 2300 gas :
- 写入存储
- 创建合约
- 调用消耗大量 gas 的外部函数
- 发送以太币
与任何其他函数一样,只要有足够的 gas 传递给它,回退函数就可以执行复杂的操作。
如果调用方打算调用不可用的函数, 也会执行回退函数。如果要实现回退函数仅用于接收以太, 则应添加类似 require(msg.data.length == 0)
检查以防止哪些无效的调用。
一个没有定义 fallback 函数的合约,直接接收以太币(没有函数调用,即使用 send
或 transfer
)会抛出一个异常, 并返还以太币(在 Solidity v0.4.0 之前行为会有所不同)。所以如果你想让你的合约接收以太币,必须实现 fallback 函数。
一个没有 payable fallback 函数的合约,可以作为 coinbase 交易 (又名 矿工区块回报 )的接收者或者作为 selfdestruct
的目标来接收以太币。一个合约不能对这种以太币转移做出反应,因此也不能拒绝它们。这是 EVM 在设计时就决定好的,而且 Solidity 无法绕过这个问题。
这也意味着 address(this).balance
可以高于合约中实现的一些手工记帐的总和(例如在回退函数中更新的累加器记帐)。
1 | pragma solidity >=0.5.0 <0.7.0; |
数据位置
所有的引用类型,如 数组 和 结构体 类型,都有一个额外注解 数据位置
,来说明数据存储位置。 有三种位置: 内存memory 、 存储storage 以及 调用数据calldata 。 调用数据calldata 仅对外部合约函数的参数有效,同时也是必须的。 调用数据calldata 是不可修改的、非持久的函数参数存储区域,效果大多类似 内存memory 。
在版本0.5.0之前,数据位置可以省略,并且根据变量的类型,函数类型等有默认数据位置,但是所有复杂类型现在必须提供明确的数据位置。
数据位置不仅仅表示数据如何保存,它同样影响着赋值行为:
- 在 存储storage 和 内存memory 之间两两赋值(或者从 调用数据calldata 赋值 ),都会创建一份独立的拷贝。
- 从 内存memory 到 内存memory 的赋值只创建引用, 这意味着更改内存变量,其他引用相同数据的所有其他内存变量的值也会跟着改变。
- 从 存储storage 到本地存储变量的赋值也只分配一个引用。
- 其他的向 存储storage 的赋值,总是进行拷贝。 这种情况的示例如对状态变量或 存储storage 的结构体类型的局部变量成员的赋值,即使局部变量本身是一个引用,也会进行一份拷贝(译者注:查看下面
ArrayContract
合约 更容易理解)
1 | pragma solidity >=0.4.0 <0.7.0; |
数组
bytes
和 string
类型的变量是特殊的数组。 bytes
类似于 byte[]
,但它在 调用数据calldata 和 内存memory 中会被“紧打包”,将元素连续地存在一起,不会按每 32 字节一单元的方式来存放。string
与 bytes
相同,但不允许用长度或索引来访问。
我们更多时候应该使用 bytes
而不是 byte[]
,因为Gas 费用更低, byte[]
会在元素之间添加31个填充字节。作为一个基本规则, 对任意长度的原始字节数据使用 bytes
,对任意长度字符串(UTF-8)数据使用 string
。
如果使用一个长度限制的字节数组,应该使用一个 bytes1
到 bytes32
的具体类型,因为它们便宜得多。
地址类型
地址类型有两种形式,他们大致相同:
address
:保存一个20字节的值(以太坊地址的大小)。address payable
:可支付地址,与address
相同,不过有成员函数transfer
和send
。
区别
简单来说是 address payable
可以接受以太币的地址,而一个普通的 address
则不能。
大部分情况下你不需要关心 address
与 address payable
之间的区别,并且到处都使用 address
。 例如,如果你在使用 取款模式, 你可以(也应该)保存地址为 address
类型, 因为可以在msg.sender
对象上调用 transfer
函数, 因为 msg.sender
是 address payable
。
address
和 address payable
的区别是在 0.5.0 版本引入的,同样从这个版本开始,合约类型不在继承自地址类型,不过如果合约有可支付的回退( payable fallback )函数,合约类型仍然可以显示转换为 address
或 address payable
。
类型转换
允许从 address payable
到 address
的隐式转换,而从 address
到 address payable
的转换是不可以的( 执行这种转换的唯一方法是使用中间类型,先转换为 uint160
),如:
1 | address payable ap = address(uint160(addr)); |
地址字面常量可以隐式转换为 address payable
。
address
可以显式和整型、整型字面常量、bytes20
及合约类型相互转换。转换时需注意:不允许以 address payable(x)
形式转换。 如果 x
是整型或定长字节数组、字面常量或具有可支付的回退( payable fallback )函数的合约类型,则转换形式 address(x)
的结果是 address payable
类型。 如果 x
是没有可支付的回退( payable fallback )函数的合约类型,则 address(x)
将是 address
类型。 在外部函数签名(定义)中,address
可用来表示 address
和 address payable
类型。
如果将使用较大字节数组类型转换为 address
,例如 bytes32
,那么 address
将被截断。 为了减少转换歧义,0.4.24及更高编译器版本要求我们在转换中显式截断处理。 以地址 0x111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFFCCCC
为例, 如果使用 address(uint160(bytes20(b)))
结果是 0x111122223333444455556666777788889999aAaa
, 而使用 address(uint160(uint256(b)))
结果是 0x777788889999AaAAbBbbCcccddDdeeeEfFFfCcCc
。
成员变量和函数
balance
和transfer()
可以使用 balance
属性来查询一个地址的余额, 也可以使用 transfer
函数向一个可支付地址(payable address)发送 以太币Ether (以 wei 为单位):
1 | address x = 0x123; // 此地址为地址字面常量,可以隐式转换为 `address payable` 。 |
如果当前合约的余额不够多,则 transfer
函数会执行失败,或者如果以太转移被接收帐户拒绝, transfer
函数同样会失败而进行回退。
如果 x
是一个合约地址,且有可支付的回退( payable fallback )函数时,该函数会跟 transfer
函数调用一起执行(这是 EVM 的一个特性,无法阻止)。 如果在执行过程中用光了 gas 或者因为任何原因执行失败,以太币Ether 交易会被打回,当前的合约也会在终止的同时抛出异常。
send
send
是 transfer
的低级版本。如果执行失败,当前的合约不会因为异常而终止,但 send
会返回 false
。
在使用 send
的时候会有些风险:如果调用栈深度是 1024 会导致发送失败(这总是可以被调用者强制),如果接收者用光了 gas 也会导致发送失败。 所以为了保证 以太币Ether 发送的安全,一定要检查 send
的返回值,使用 transfer
或者更好的办法: 使用接收者自己取回资金的模式。
地址字面常量
比如像 0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF
这样的通过了地址校验和测试的十六进制字面常量会作为 address payable
类型。
十六进制字面常量长度在 39 到 41 个数字之间,而没有通过校验测试(我们也会收到一个警告)的十六进制字面常量则会视为正常的有理数字面常量。
混合大小写的地址校验和格式定义在 EIP-55 中。
合约类型
每一个 contract]定义都有他自己的类型。您可以隐式地将合约转换为从他们继承的合约。
合约和 address
的数据表示是相同的。在版本0.5.0之前,合约直接从地址类型派生的。
合约可以显式转换为 address
类型。只有当合约具有可支付回退函数时,才能显式和 address payable
类型相互转换 转换仍然使用 address(x)
执行,而不是使用 address payable(x)
。
调用其他合约
- 对于已部署的合约,声明一个合约类型的局部变量(MyContract c),则可以调用该合约的函数。
- 对于未部署的合约, 需要知道其代码,然后创建合约对象再调用。
其他注意
- 合约不支持任何运算符。
- 合约类型的成员是合约的外部函数及 public 的 状态变量。
- 对于合约
C
可以使用type(C)
获取合约的类型信息。
事件 Events
Solidity 事件是EVM的日志功能之上的抽象。 应用程序可以通过以太坊客户端的RPC接口订阅和监听这些事件。
事件在合约中可被继承。当他们被调用时,会使参数被存储到交易的日志中 —— 一种区块链中的特殊数据结构。这些日志与地址相关联,被并入区块链中,只要区块可以访问就一直存在(在 Frontier 和 Homestead 版本中会被永久保存,在 Serenity 版本中可能会改动)。 日志和事件在合约内不可直接被访问(甚至是创建日志的合约也不能访问)。
对日志的 SPV(Simplified Payment Verification)证明是可能的,如果一个外部实体提供了一个带有这种证明的合约,它可以检查日志是否真实存在于区块链中。 但需要留意的是,由于合约中仅能访问最近的 256 个区块哈希,所以还需要提供区块头信息。