函数
函数调用
函数调用可以分为两类:
- 内部调用
- 外部调用
这里的内部与外部,是相对于 合约
来说。两个 合约
之间,各自是独立封闭的,有着各自的存储与代码,可以抽象为:
对于内部调用而言,是通过直接跳转(jump)实现的,而外部调用则是通过 EVM
的消息调用来实现。
从代码上来讲,通过点操作符的调用为外部调用,如 this.f()
或者 c.f()
。而在合约内部不使用点操作符的方式则为内部调用,如 f()
:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract C {
function f() public pure {}
}
contract FunctionCall {
function f() public pure {}
function caller() public {
// 内部调用
f();
// 外部调用
this.f();
C c = new C();
// 外部调用
c.f();
}
}
函数参数
函数参数的声明与变量声明相同,函数参数声明的变量为函数的局部变量,可见性为整个函数体。
对于不需要使用的函数参数,可以不给出参数名:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract FunctionArg {
// 只使用中间的参数 a
function getMiddle(int, int a, int) public pure returns (int) {
return a;
}
}
函数返回值
函数返回值由 retunrs (Type)
的方式声明,且可以通过返回元组的方式声明多个返回值:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract FunctionReturns {
function getPosition() public pure returns (int, int) {
return (1, 2);
}
function getX() public pure returns (int) {
// 使用解构赋值,获取多个返回值,第二个返回值在此处不使用,可忽略
(int x, ) = getPosition();
return x;
}
}
函数返回值可以带上变量名,此时相当于声明了一个函数的局部变量,可见性为整个函数体。此时函数体内可以省略 return
语句,最终总会将该声明的变量作为返回值返回:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract FunctionReturns {
function getPosition() public pure returns (int x, int y) {
x = 1;
y = 2;
}
function getX() public pure returns (int x) {
(x, ) = getPosition();
}
}
状态可变性
在合约内定义的函数还可以指定其状态可变性,即当前函数是否允许读取或更改合约状态。
view 视图函数
标志为 view
的函数,表明函数不会修改合约状态。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract ViewFunction {
int private number;
function getNumber() public view returns (int) {
return number;
}
}
修改合约状态的行为如下::
- 修改状态变量
- 发出事件
- 创建其他合约
- 使用 selfdestruct
- 通过调用发送以太币
- 调用非
view
或 非pure
的函数 - 使用底层调用
- 在内联汇编中使用某些不允许的操作码
包含以上行为的函数不可以被声明为 view
函数
pure 纯函数
标志为 pure 的函数,表明函数不但不会修改合约状态,也不会读取合约状态。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract PureFunction {
function add(int a, int b) public pure returns (int) {
return a + b;
}
}
函数修饰符
函数修饰符可以实现通过声明的方式,对函数本体的代码进行包裹和定制。
例如,假设我们有一批函数只能通过合约拥有者调用。如果不使用函数修饰符,我们就必须在每一个函数中手动进行检查。如果使用函数修饰符,则只需要给这批函数添加一个检查调用者是否是合约拥有者的修饰符即可:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract FunctionModifier {
address owner;
constructor() {
owner = msg.sender;
}
// 定义一个函数修饰符
modifier onlyOwner() {
// 检查调用者必须是合约拥有者
require(msg.sender == owner, unicode"调用者非合约拥有者");
_; // 该下划线为函数代码占用符,表示被修饰的函数代码应当被插在此处
}
function fn1() public onlyOwner {}
function fn2() public onlyOwner {}
function fn3() public onlyOwner {}
// ...
}
需要注意的是,在函数修饰符内,使用了一个函数代码占位符 _
,表示被修饰的函数的代码应当被插在该位置。
查看以下代码示例,runTwice
修饰符用于让函数体执行两次:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;
contract FunctionModifierRunTwice {
int256 public a;
modifier runTwice() {
// 连着使用两个函数代码占位符,导致函数代码被执行两次
_;
_;
}
// 由于被 runTwice 修饰,实际上将导致每调用一次,a 将被增加 2
function addOne() public runTwice {
a++;
}
}
函数修饰符参数
函数修饰符也可以接受参数。查看以下代码示例,我们通过传入参数 n
,来让函数体执行 n
次。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;
contract FunctionModifierRun {
int256 public a;
modifier run(uint n) {
for (uint i = 0; i < n; i++) _;
}
// 使用修饰符 run(10) 来让函数体 a++ 执行 10 次,实现 + 10
function addTen() public run(10) {
a++;
}
// 使用修饰符 run(n) 来让函数体 a++ 执行 n 次,实现 + n
function add(uint n) public run(n) {
a++;
}
}
多个函数修饰符
函数可以被多个函数修饰符修饰,只需要通过空格分开书写。最终将根据修饰符的顺序执行函数修饰符代码,并依次嵌套执行
,而不是按顺序执行,参阅以下示例代码:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;
contract FunctionMultipleModifier {
int256 public a;
modifier runTwice() {
// 在本例子,此处函数体为 function addOne() public runTriple { a++; }
_;
// 在本例子,此处函数体为 function addOne() public runTriple { a++; }
_;
}
modifier runTriple() {
// 在本例中,此处函数体为 function addOne() public { a++; }
_;
// 在本例中,此处函数体为 function addOne() public { a++; }
_;
// 在本例中,此处函数体为 function addOne() public { a++; }
_;
}
function addOne() public runTwice runTriple {
a++;
}
}
以上代码中,runTwice
用于执行 2 次函数体,而 runTriple
用于执行 3 次函数体。当函数 addOne
同时被它们修饰时,最终函数体将被执行 次,而不是次。
因为应用多个函数修饰符时,它们将被嵌套执行,而不是顺序执行。
函数修饰符提前 return
函数修饰符本身不能改变被修饰函数的返回值。但是如果函数修饰符中未执行到函数体代码,而提前 return
,则函数体代码将不会被执行,函数将返回对应类型的零值
:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;
contract FunctionModifierRuturn {
int256 public a;
modifier returnModifier() {
return;
_;
}
function addOne() public returnModifier returns (int) {
// 由于 returnModifier 会提前返回,以下函数体代码将不会被执行
a++;
return a;
}
}
函数描述符的可见性
函数描述符可以定义在 库
中,也可以定义在 合约
中。但定义在库中的描述符,仅库内函数可见。定义在合约中的描述符,仅合约内函数可见。
函数重载
Solidity
支持函数重载(overload),即可以定义多个具有相同名字的函数,只要他们的函数参数不同即可:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;
contract FunctionOverload {
int256 public a;
modifier fnModifier() {
_;
}
function addOne() public fnModifier returns (int) {
a++;
return a;
}
function addOne(int256 n) internal returns (uint) {
a += n;
return uint(a);
}
}
请注意,函数重载时,只要参数不同即可,不要求可见性修饰符,函数修饰符以及返回值是否相同。如上例中,第二个 addOne
的可见性修饰符 internal
,返回值 (uint)
都不同于第一个 addOne
,而且也没有加函数修饰符 fnModifier
。
可支付函数
当一个函数带有 payable
标志时,表示该函数可用于接受以太币:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;
contract FunctionPayable {
function pay() public payable {
require(msg.value == 1 ether, unicode"只能支付1个以太币");
}
}
payable
标志使得我们可以在调用 pay
函数时,传入以太币,该以太币将存入合约地址。如果 pay
函数抛出错误,则打断整个过程,以太币也将被退回。例如在上面例子中,pay
函数只接受1个以太币,多于或少于都将拒绝。
receive 函数
当使用 send
以及 transfer
方法给合约发送以太币时,将被 receive
函数接受。
receive
函数的签名必须是:receive() external payable {}
,注意前面不需要加 function
关键字。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;
contract FunctionReceive {
receive() external payable {
require(msg.value == 1 ether, unicode"只能支付1个以太币");
}
}
一个合约只能有一个 receive
函数。由于 receive
函数签名是固定的,因此它不允许被重载。
如果 receive
函数抛出错误,则打断整个过程,以太币也将被退回。例如在上面例子中,receive
函数只接受1个以太币,多于或少于都将拒绝。
fallback 函数
fallback 函数是一个兜底函数。当外部发起一个调用,但是外部要调用的函数签名与合约中的任何函数都不匹配时,将调用 fallback
函数进行兜底处理,例如拒绝该交易、发出错误事件等等。以下 fallback
函数直接抛出错误,中止未匹配函数的调用:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;
contract FunctionFullback {
error NoMatchFunction();
fallback() external {
revert NoMatchFunction();
}
}
fallback
必须是以下两种签名之一:
fallback() external [payable]
fallback(bytes calldata) external [payable] returns (bytes memory)
第二种签名允许 fallback
函数解析传入的参数数据,以及返回字节型数据。由于 fallback
函数只能有一个,因此也不支持重载。
fallback
签名中,payable
是可选的。当合约中没有定义 receive
函数时,使用 send/transfer
给当前合约发送以太币,将会调用标志 payable
的 fallback
兜底函数进行兜底:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;
contract FunctionFullbackPayable {
fallback() external payable {}
}
如果 payable fallback
函数抛出错误,则打断整个过程,以太币也将被退回。
合约不可接收以太币
如果合约中没有 receive
函数,没有 payable fallback
函数,也没有任何其他 payable
函数,则该合约无法接收以太币。
但是并不意味着合约余额不可能增加,以下是两种可能的例外情况:
- 即使合约没有任何
payable
函数,仍可以接受挖矿区块奖励 - 另一个带有以太币余额的合约,调用
selfdestruct
函数销毁自身并传入当前合约地址作为参数,强制将它的以太币余额转到当前合约中
函数类型
函数类型变量声明方式如下:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;
contract FunctionType2 {
function test() public pure {
function() fn1;
// 带参数
function(uint, int) fn2;
// 带返回值
function() returns (int) fn3;
// 带可见性声明
function() external fn4;
// 带状态可变性声明
function() pure fn5;
// 带可支付声明
function() external payable fn6;
}
}
函数类型变量声明中,可见性只能是 internal
或 external
,未指定时则认为是 internal
内部函数。对于 payable
函数,只能是 external
外部函数。
函数类型变量转换
函数类型A只有满足以下条件时,可以隐匿转换为函数类型B:
- 参数类型相同
- 返回值类型相同
internal/external
可见性相同- B的状态可变性必须比A的严格,即:
- A 为
pure
函数类型,则 B 只能是pure
函数类型 - A 为
view
函数类型,则 B 可以是pure
或view
函数类型 - 否则, A 未指定状态可变性,此时 B 可以是任意状态可变性
- A 为
- 如果 A 是
payable
函数,则 B 也必须是payable
函数
函数属性
外部函数和公共函数有下面两个属性:
address
:函数所属的合约的地址selector
:返回ABI
函数选择子(selector)
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;
contract FunctionAddress {
function test() public view returns (address) {
return this.test.address;
}
}
函数选择子见下节。
函数选择子
考虑以下外部函数调用:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;
contract Callee {
function callee(uint x, uint y) external pure returns (uint, uint) {
return (0, 1);
}
}
contract FunctionSelector {
function caller() public {
Callee c = new Callee();
c.callee(0, 1);
}
}
当执行外部调用 c.callee(0, 1)
时,我们需要从合约c中查找签名为 callee(uint256,uint256)
的函数然后调用它。
实际上在 EVM
调用中,我们并不会直接使用该签名去目标合约中查找对应函数,因为函数签名本身长度不定,且可能非常长。因此,我们引入了一个 函数选择子
的概念。我们将函数签名通过 hash
之后取前 4 个字节作为它的 函数选择子
。
在进行外部调用时,我们就不需要传输函数的签名字符串,而只需要传输4个字节长的函数选择子即可。
如果目标合约中存在某一个函数的 selector
属性与目标函数选择子相同,则该函数即是要调用的外部函数。
以下是获取函数选择子的示例:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;
contract FunctionGetSelector {
function test() public pure returns (bytes4) {
return this.test.selector;
}
}
函数选择子的这种编码方式,由《ABI规范》规定。
以下代码展示了如何计算函数选择子:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;
contract Callee {
function callee(
uint256 x,
uint256 y
) external pure returns (uint256, uint256) {
return (0, 1);
}
}
contract FunctionSelector {
function getSelector() public pure returns (bytes4, bytes4) {
return (
Callee.callee.selector,
bytes4(keccak256("callee(uint256,uint256)"))
);
}
}
以上签名生成的函数选择子为 0x8ecc4601
。