🐹4 函数

[TOC]

函数

函数是合约的重要组成部分,合约就是通过暴露函数的调用来提供 API 式的服务,对外暴露一个可以调用的函数,就是用户通过操作 DAPP 与部署在链上的合约的交互。

函数定义

函数定义使用 function 关键字,函数定义必须声明其可见性、函数类型、形参、返回值。

  • 函数可以接受任意数量形参,函数返回值可以返回任意数量参数。

自由函数与内部函数

  • 在 Solidity 0.7.0 之前只能在合约内部定义函数,0.7.0 之后可以在合约内部和外部定义函数。

  • 合约之外的函数称为“自由函数”,始终具有隐式的 internal 可见性。 自由函数的代码会包含在所有调用它们的合约中,类似于内部库函数。

  • 自由函数仍然在合约的上下文内执行,可以访问变量 this ,也可以调用其它合约,发送以太币或销毁调用它们合约等其它行为。

  • 自由函数与在合约中定义的函数的主要区别为:自由函数不能直接访问存储变量和不在他们的作用域范围内函数。

function 函数名(形参...) 可见性 函数类型 returns(返回值...){}

函数可见性

Solidity 中函数调用分内部调用和外部调用,内部调用不会产生实际的 EVM 调用称为消息调用,而外部调用则会产生一个 EVM 调用。

  • 函数可见性有 external、public、internal 和 private 四种类型。

external

  • external 可见性的是外部函数,外部函数作为合约接口的一部分,可以从其他合约和交易中调用。 一个外部函数 f 不能从内部调用(即 f() 不起作用,但 this.f() 可以)。

  • 当收到大量数据的时候,外部函数有时候会更有效率,因为数据不会从 calldata 复制到内存。

public

  • public 可见性的函数是合约接口的一部分,可以在内部直接调用或通过消息调用。

internal

  • internal 可见性的函数只能是内部访问,从当前合约内部或从它派生的合约访问,不能使用 this 调用。

private

  • private 可见性的函数仅在当前定义它的合约中使用,派生合约无法调用,不能使用 this 调用。

函数类型

函数类型有 payable 支付、view 视图函数、pure 纯函数、receive 接收函数 和 fallback 回退函数等五种。

  • 编译器的 EVM 有4个操作符: REVERT 、RETURNDATASIZE、RETURNDATACOPY 、STATICCALL;函数类型的实现是基于这些操作符的。

payable 支付

  • 接收 ether 函数上要增加payable标识,payable 函数才能正常接收msg.value。

  • 需要支付 gas 的函数,如函数修改了状态变量,可以不显式使用 payable 修饰。

view 视图函数

  • 将函数声明为 view 类型,要保证函数中不修改状态。

  • 操作码 STATICCALL 用于 view 类型的函数, 这些函数强制在 EVM 执行过程中保持不修改状态。

  • constant 之前是 view 的别名,在0.5.0之后移除了。

pure 纯函数

  • 将函数声明为 pure 类型,函数中不读取也不修改状态。

  • 对于 pure 类型函数,编译器的 EVM 使用操作码 STATICCALL , 这并不保证状态未被读取, 但至少不被修改。

  • 纯函数能够使用 revert() 和 require() 在发生错误时去还原潜在状态更改,还原状态更改不被视为 “状态修改”, 因为它只还原以前在没有 view 或 pure 限制的代码中所做的状态更改, 并且代码可以选择捕获 revert 并不传递还原。

receive 接收以太函数

  • 一个合约最多有一个 receive 函数, 声明函数为: receive() external payable { ... }

  • 不需要 function 关键字,也没有参数和返回值并且必须是 external 可见性和 payable 修饰. 它可以是 virtual 的,可以被重载也可以有 修改器 modifier 。

  • 在对合约没有任何附加数据调用(通常是对合约转账)是会执行 receive 函数. 例如 通过 .send()或 r.transfer() 如果 receive 函数不存在, 但是有payable 的 fallback 回退函数 那么在进行纯以太转账时,fallback 函数会调用。

  • 如果两个函数都没有,这个合约就没法通过常规的转账交易接收以太(会抛出异常)。

  • receive 函数可能只有 2300 gas 可以使用(当使用 send 或 transfer 时), 除了基础的日志输出之外,进行其它操作的余地很小。写入存储、创建合约、调用消耗大量 gas 的外部函数、发送以太币等操作都会消耗 2300 gas。

fallback 回退函数

  • 一个合约最多有一个 fallback 函数。函数声明为: fallback () external [payable] 或 fallback (bytes calldata _input) external [payable] returns (bytes memory _output)

  • 没有 function 关键字。必须是 external 可见性,它可以是 virtual 的,可以被重载也可以有修改器 modifier。

  • 如果在一个对合约调用中,没有其他函数与给定的函数标识符匹配 fallback 会被调用,或者在没有 receive 函数时,而没有提供附加数据对合约调用,那么 fallback 函数会被执行。

  • fallback 函数始终会接收数据,但为了同时接收以太时,必须标记为 payable。

  • 如果使用了带参数的版本,_input 将包含发送到合约的完整数据(等于 msg.data ),并且通过 _output 返回数据。 返回数据不是 ABI 编码过的数据,它返回不经过修改的数据。

  • 如果回退函数在接收以太时调用,可能只有 2300 gas 可以使用,这与 receive 接收函数一致。

  • 与任何其他函数一样,只要有足够的 gas 传递给它,回退函数就可以执行复杂的操作。

修改状态的行为:

  • 修改状态变量。

  • 产生事件。

  • 创建其它合约。

  • 使用 selfdestruct 自毁。

  • 通过调用发送以太币。

  • 调用任何没有标记为 view 或者 pure 的函数。

  • 使用低级调用。

  • 使用包含特定操作码的内联汇编。

读取状态的行为:

  • 读取状态变量。

  • 访问 address(this).balance 或者.balance。

  • 访问 block,tx, msg 中任意成员 (除 msg.sig 和 msg.data 之外)。

  • 调用任何未标记为 pure 的函数。

  • 使用包含某些操作码的内联汇编。

函数修改器 modifier

使用函数修改器 modifier 可以改变函数的行为,例如用于在执行函数之前自动检查某个条件,其实函数修改器就像拦截器,可以在函数执行前执行一些操作,也可在函数执行之后执行一些操作。

  • 修改器 modifier 是可以被继承的,被标记为 virtual 可以被派生合约覆盖(重写)。

  • 只能使用在当前合约或在基类合约中定义的修改器 modifier, 定义在库中的修改器只能在库函数使用。

  • 函数可以使用多个修改器,使用空格隔开,修改器会依次检查执行。

修改器重写

  • 父合约使用 virtual 修饰的修改器可以被重写,重写的修改器使用 override 修饰。

函数重载

多个不同形参的同名函数称为重载(overloading),这也适用于继承函数。

  • 重载函数也存在于外部接口中,如果两个外部可见函数仅区别于 Solidity 内的类型而不是它们的外部类型则会导致错误。

  • 尽可能避免函数重载,在 Solidity 中同名函数形参是 uint 和 uint8 是可以通过编译的,但是调用时就是不稳定的因素。

重载解析和参数匹配

  • 通过将当前范围内的函数声明与函数调用中提供的参数相匹配,可以选择重载函数。 如果所有参数都可以隐式地转换为预期类型,则选择函数作为重载候选项。如果一个候选都没有,解析失败。

内部调用函数

合约中的函数在合约中调用称为内部调用函数。

  • 内部调用函数支持递归调用,但是每个内部函数调用至少使用一个堆栈槽,最多有1024堆栈槽可用,因此要避免过多的递归调用。

  • 函数调用在 EVM 中被解释为简单的跳转,当前内存不会被清除。

  • 函数之间通过传递内存引用进行内部调用非常高效,只有在同一合约的函数可以内部调用。

外部调用函数(消息调用)

通过 this.fun();合约实例.fun(); 方式调用,函数会通过一个消息调用来进行 外部调用,而不是内部调用函数那样直接的跳转。

  • 调用其它合约的函数一定是外部调用,外部调用时所有的形参都需要被复制到内存。

  • 合约间的函数调用不会创建自己的交易, 它是作为整个交易的一部分的消息调用。

  • 与其它合约的交互存在风险,交互时当前合约会将控制权移交给被调用合约,而被调用合约能做任何事。被调用合约可以通过它自己的函数改变调用合约的状态变量。

Last updated