函数

函数调用

函数调用可以分为两类:

  • 内部调用
  • 外部调用

这里的内部与外部,是相对于 合约 来说。两个 合约 之间,各自是独立封闭的,有着各自的存储与代码,可以抽象为:

内外部调用

对于内部调用而言,是通过直接跳转(jump)实现的,而外部调用则是通过 EVM 的消息调用来实现。

从代码上来讲,通过点操作符的调用为外部调用,如 this.f() 或者 c.f() 。而在合约内部不使用点操作符的方式则为内部调用,如 f()

FunctionCall.sol
运行
复制
// 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();
    }
}

函数参数

函数参数的声明与变量声明相同,函数参数声明的变量为函数的局部变量,可见性为整个函数体。

对于不需要使用的函数参数,可以不给出参数名:

ViewFunction.sol
运行
复制
// 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) 的方式声明,且可以通过返回元组的方式声明多个返回值:

ViewFunction.sol
运行
复制
// 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 语句,最终总会将该声明的变量作为返回值返回:

ViewFunction.sol
运行
复制
// 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 的函数,表明函数不会修改合约状态。

ViewFunction.sol
运行
复制
// 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 的函数,表明函数不但不会修改合约状态,也不会读取合约状态。

PureFunction.sol
运行
复制
// 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;
    }
}

函数修饰符

函数修饰符可以实现通过声明的方式,对函数本体的代码进行包裹和定制。

例如,假设我们有一批函数只能通过合约拥有者调用。如果不使用函数修饰符,我们就必须在每一个函数中手动进行检查。如果使用函数修饰符,则只需要给这批函数添加一个检查调用者是否是合约拥有者的修饰符即可:

FunctionModifier.sol
运行
复制
// 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 修饰符用于让函数体执行两次:

FunctionModifier.sol
运行
复制
// 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 次。

FunctionModifier.sol
运行
复制
// 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++;
    }
}

多个函数修饰符

函数可以被多个函数修饰符修饰,只需要通过空格分开书写。最终将根据修饰符的顺序执行函数修饰符代码,并依次嵌套执行,而不是按顺序执行,参阅以下示例代码:

FunctionMultipleModifier.sol
运行
复制
// 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 同时被它们修饰时,最终函数体将被执行 2×3=62\times 3=6 次,而不是2+3=52+3=5次。

因为应用多个函数修饰符时,它们将被嵌套执行,而不是顺序执行。

函数修饰符提前 return

函数修饰符本身不能改变被修饰函数的返回值。但是如果函数修饰符中未执行到函数体代码,而提前 return,则函数体代码将不会被执行,函数将返回对应类型的零值

FunctionModifierRuturn.sol
运行
复制
// 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),即可以定义多个具有相同名字的函数,只要他们的函数参数不同即可:

FunctionModifierRuturn.sol
运行
复制
// 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 标志时,表示该函数可用于接受以太币:

FunctionPayable.sol
运行
复制
// 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 关键字。

FunctionReceive.sol
运行
复制
// 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 函数直接抛出错误,中止未匹配函数的调用:

FunctionFullback.sol
运行
复制
// 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 给当前合约发送以太币,将会调用标志 payablefallback 兜底函数进行兜底:

FunctionFullbackPayable.sol
运行
复制
// 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 函数销毁自身并传入当前合约地址作为参数,强制将它的以太币余额转到当前合约中

函数类型

函数类型变量声明方式如下:

FunctionFullbackPayable.sol
运行
复制
// 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;
    }
}

函数类型变量声明中,可见性只能是 internalexternal,未指定时则认为是 internal 内部函数。对于 payable 函数,只能是 external 外部函数。

函数类型变量转换

函数类型A只有满足以下条件时,可以隐匿转换为函数类型B:

  • 参数类型相同
  • 返回值类型相同
  • internal/external 可见性相同
  • B的状态可变性必须比A的严格,即:
    • A 为 pure 函数类型,则 B 只能是 pure 函数类型
    • A 为 view 函数类型,则 B 可以是 pureview 函数类型
    • 否则, A 未指定状态可变性,此时 B 可以是任意状态可变性
  • 如果 A 是 payable 函数,则 B 也必须是 payable 函数

函数属性

外部函数和公共函数有下面两个属性:

  • address:函数所属的合约的地址
  • selector:返回 ABI 函数选择子(selector)
FunctionAddress.sol
运行
复制
// 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;
    }
}

函数选择子见下节。

函数选择子

考虑以下外部函数调用:

FunctionSelector.sol
运行
复制
// 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 属性与目标函数选择子相同,则该函数即是要调用的外部函数。

以下是获取函数选择子的示例:

FunctionGetSelector.sol
运行
复制
// 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规范》规定。

以下代码展示了如何计算函数选择子:

FunctionComputeSelector.sol
运行
复制
// 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