函数

2022-05-16 10:46 更新

可以在合约内部和外部定义函数。

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

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;

function sum(uint[] memory arr) pure returns (uint s) {
    for (uint i = 0; i < arr.length; i++)
        s += arr[i];
}

contract ArrayExample {
    bool found;
    function f(uint[] memory arr) public {
        // This calls the free function internally.
        // The compiler will add its code to the contract.
        uint s = sum(arr);
        require(s >= 10);
        found = true;
    }
}

笔记

在合约之外定义的功能仍然总是在合约的上下文中执行。他们仍然可以访问变量this,可以调用其他合约,向它们发送以太币并销毁调用它们的合约等等。与合约中定义的函数的主要区别在于,自由函数不能直接访问存储变量和不在其范围内的函数。

函数参数和返回变量

函数将类型化参数作为输入,并且与许多其他语言不同,它还可以返回任意数量的值作为输出。

功能参数

函数参数的声明方式与变量相同,未使用的参数名称可以省略。

例如,如果您希望您的合约接受一种带有两个整数的外部调用,您可以使用如下内容:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract Simple {
    uint sum;
    function taker(uint a, uint b) public {
        sum = a + b;
    }
}

函数参数可以用作任何其他局部变量,也可以分配给它们。

笔记

外部函数不能接受多维数组作为输入参数。如果您通过添加到源文件来启用 ABI coder v2,则此功能是可能的。pragma abicoder v2;

内部函数可以在不启用该功能的情况下接受多维数组。

返回变量

函数返回变量在 returns关键字之后使用相同的语法声明。

例如,假设您要返回两个结果:作为函数参数传递的两个整数的总和和乘积,那么您可以使用以下内容:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract Simple {
    function arithmetic(uint a, uint b)
        public
        pure
        returns (uint sum, uint product)
    {
        sum = a + b;
        product = a * b;
    }
}

返回变量的名称可以省略。返回变量可以用作任何其他局部变量,并使用其默认值初始化并具有该值,直到它们被(重新)分配。

您可以显式分配给返回变量,然后像上面一样保留函数,或者您可以直接使用 语句提供返回值(单个或多个):return

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract Simple {
    function arithmetic(uint a, uint b)
        public
        pure
        returns (uint sum, uint product)
    {
        return (a + b, a * b);
    }
}

如果使用 earlyreturn离开具有返回变量的函数,则必须在 return 语句中提供返回值。

笔记

您不能从非内部函数返回某些类型,尤其是多维动态数组和结构。如果您通过添加 到源文件来启用 ABI coder v2,则可以使用更多类型,但 类型仍仅限于单个合约内,您无法转移它们。pragma abicoder v2;mapping

返回多个值

当一个函数有多种返回类型时,该语句可用于返回多个值。组件的数量必须与返回变量的数量相同,并且它们的类型必须匹配,可能在隐式转换之后。return (v0, v1, ..., vn)

状态可变性

查看函数

可以声明函数view,在这种情况下它们承诺不修改状态。

笔记

如果编译器的 EVM 目标是 Byzantium 或更新的(默认), 则在调用函数STATICCALL时使用操作码view,这会强制状态保持不变,作为 EVM 执行的一部分。使用库view函数 DELEGATECALL,因为没有组合DELEGATECALLand STATICCALL。这意味着库view函数没有阻止状态修改的运行时检查。这不应该对安全性产生负面影响,因为库代码通常在编译时是已知的,并且静态检查器执行编译时检查。

以下语句被视为修改状态:

  1. 写入状态变量。

  2. 发射事件

  3. 创建其他合同

  4. 使用selfdestruct.

  5. 通过调用发送以太币。

  6. 调用任何未标记view或的函数pure

  7. 使用低级调用。

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

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;

contract C {
    function f(uint a, uint b) public view returns (uint) {
        return a * (b + 42) + block.timestamp;
    }
}

笔记

constanton 函数曾经是 的别名view,但在 0.5.0 版中已删除。

笔记

Getter 方法是自动标记的view

笔记

在 0.5.0 版本之前,编译器不使用函数的STATICCALL操作码viewview这通过使用无效的显式类型转换启用了函数中的状态修改。通过使用 STATICCALLforview函数,可以防止在 EVM 级别上修改状态。

纯函数

可以声明函数pure,在这种情况下它们承诺不读取或修改状态。特别是,应该可以pure在编译时评估一个函数,只给它的输入 和msg.data,但不知道当前的区块链状态。这意味着从immutable变量中读取可能是非纯操作。

笔记

如果编译器的 EVM 目标是 Byzantium 或更新的(默认),STATICCALL则使用操作码,这不保证不读取状态,但至少不修改状态。

除了上面解释的状态修改语句列表之外,以下内容被认为是从状态中读取的:

  1. 从状态变量中读取。

  2. 访问address(this).balance<address>.balance

  3. block访问, tx,的任何成员msg(和 除外msg.sigmsg.data

  4. 调用任何未标记的函数pure

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

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;

contract C {
    function f(uint a, uint b) public pure returns (uint) {
        return a * (b + 42);
    }
}

纯函数能够使用revert()和函数在发生错误时require()恢复潜在的状态变化。

恢复状态更改不被视为“状态修改”,因为只有先前在没有vieworpure限制的代码中所做的状态更改才会被恢复,并且该代码可以选择捕获revert而不传递它。

这种行为也符合STATICCALL操作码。

警告

不可能在 EVM 级别阻止函数读取状态,只能阻止它们写入状态(即只能view在 EVM 级别强制执行,pure不能)。

笔记

在 0.5.0 版本之前,编译器不使用函数的STATICCALL操作码purepure这通过使用无效的显式类型转换启用了函数中的状态修改。通过使用 STATICCALLforpure函数,可以防止在 EVM 级别上修改状态。

笔记

在 0.4.17 版本之前,编译器没有强制执行pure不读取状态。它是一种编译时类型检查,可以规避在合约类型之间进行无效的显式转换,因为编译器可以验证合约的类型不做状态改变操作,但不能检查将要被在运行时调用实际上就是那种类型。

特殊功能

接收以太功能

一个合约最多可以有一个receive函数,使用声明 (不带关键字)。此函数不能有参数,不能返回任何内容,并且必须具有 可见性和状态可变性。它可以是虚拟的,可以覆盖并且可以具有修饰符。receive() external payable { ... }functionexternalpayable

接收函数在调用带有空 calldata 的合约时执行。.send()这是在普通 Ether 传输(例如 via或.transfer())上执行的函数。如果不存在这样的功能,但存在应付回退功能 ,则回退功能将在普通的以太币转账中被调用。如果既不存在接收 Ether 也不存在应付回退功能,则合约无法通过常规交易接收 Ether 并引发异常。

在最坏的情况下,该receive功能只能依赖 2300 gas 可用(例如何时使用sendtransfer使用),几乎没有空间执行除基本日志记录之外的其他操作。以下操作将消耗比 2300 气体津贴更多的气体:

  • 写入存储

  • 创建合同

  • 调用消耗大量gas的外部函数

  • 发送以太币

警告

当 Ether 直接发送到合约(没有函数调用,即发送方使用sendor transfer)但接收合约没有定义接收 Ether 函数或应付回退函数时,将抛出异常,发送回 Ether(这是不同的在 Solidity v0.4.0 之前)。如果你希望你的合约接收 Ether,你必须实现一个接收 Ether 函数(不推荐使用支付回退函数来接收 Ether,因为回退被调用并且不会因发送方的接口混淆而失败)。

警告

没有接收 Ether 功能的合约可以接收 Ether 作为coinbase 交易的接收者(又名矿工块奖励)或作为selfdestruct.

合约无法对此类以太币转账做出反应,因此也无法拒绝它们。这是 EVM 的设计选择,Solidity 无法解决它。

这也意味着它address(this).balance可能高于在合约中实施的一些人工会计的总和(即在接收以太函数中更新了一个计数器)。

下面你可以看到一个使用 function 的 Sink 合约示例receive

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;

// This contract keeps all Ether sent to it with no way
// to get it back.
contract Sink {
    event Received(address, uint);
    receive() external payable {
        emit Received(msg.sender, msg.value);
    }
}

后备功能

一个合约最多可以有一个fallback函数,使用 or声明 (都没有关键字)。此功能必须具有可见性。回退函数可以是虚拟的,可以覆盖并且可以具有修饰符。fallback () external [payable]fallback (bytes calldata input) external [payable] returns (bytes memory output)functionexternal

如果没有其他函数与给定的函数签名匹配,或者根本没有提供数据并且没有接收 Ether 函数,则在调用合约时执行回退函数。fallback 函数总是接收数据,但为了也接收 Ether,它必须被标记payable

如果使用带参数的版本,input将包含发送到合约的完整数据(等于msg.data),并且可以在 中返回数据output。返回的数据不会经过 ABI 编码。相反,它将在没有修改的情况下返回(甚至没有填充)。

在最坏的情况下,如果还使用支付回退函数代替接收函数,它只能依赖 2300 gas 可用( 有关此含义的简要描述,请参阅接收以太函数)。

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

警告

如果不存在接收 Ether 功能payable,则还为普通 Ether 传输执行回退功能 。如果您定义了一个应付回退函数来区分 Ether 传输和接口混淆,那么建议始终定义一个接收 Ether 函数。

笔记

如果要解码输入数据,可以检查函数选择器的前四个字节,然后可以abi.decode与数组切片语法一起使用来解码 ABI 编码的数据: 请注意,这只能作为最后的手段使用,并且应该使用适当的功能。(c, d) = abi.decode(input[4:], (uint256, uint256));

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.2 <0.9.0;

contract Test {
    uint x;
    // This function is called for all messages sent to
    // this contract (there is no other function).
    // Sending Ether to this contract will cause an exception,
    // because the fallback function does not have the `payable`
    // modifier.
    fallback() external { x = 1; }
}

contract TestPayable {
    uint x;
    uint y;
    // This function is called for all messages sent to
    // this contract, except plain Ether transfers
    // (there is no other function except the receive function).
    // Any call with non-empty calldata to this contract will execute
    // the fallback function (even if Ether is sent along with the call).
    fallback() external payable { x = 1; y = msg.value; }

    // This function is called for plain Ether transfers, i.e.
    // for every call with empty calldata.
    receive() external payable { x = 2; y = msg.value; }
}

contract Caller {
    function callTest(Test test) public returns (bool) {
        (bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()"));
        require(success);
        // results in test.x becoming == 1.

        // address(test) will not allow to call ``send`` directly, since ``test`` has no payable
        // fallback function.
        // It has to be converted to the ``address payable`` type to even allow calling ``send`` on it.
        address payable testPayable = payable(address(test));

        // If someone sends Ether to that contract,
        // the transfer will fail, i.e. this returns false here.
        return testPayable.send(2 ether);
    }

    function callTestPayable(TestPayable test) public returns (bool) {
        (bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()"));
        require(success);
        // results in test.x becoming == 1 and test.y becoming 0.
        (success,) = address(test).call{value: 1}(abi.encodeWithSignature("nonExistingFunction()"));
        require(success);
        // results in test.x becoming == 1 and test.y becoming 1.

        // If someone sends Ether to that contract, the receive function in TestPayable will be called.
        // Since that function writes to storage, it takes more gas than is available with a
        // simple ``send`` or ``transfer``. Because of that, we have to use a low-level call.
        (success,) = address(test).call{value: 2 ether}("");
        require(success);
        // results in test.x becoming == 2 and test.y becoming 2 ether.

        return true;
    }
}

函数重载

一个合约可以有多个同名但参数类型不同的函数。这个过程称为“重载”,也适用于继承的函数。下面的例子展示 f了合约范围内函数的重载A

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract A {
    function f(uint value) public pure returns (uint out) {
        out = value;
    }

    function f(uint value, bool really) public pure returns (uint out) {
        if (really)
            out = value;
    }
}

外部接口中也存在重载函数。如果两个外部可见函数的不同在于它们的 Solidity 类型而不是它们的外部类型,这是一个错误。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

// This will not compile
contract A {
    function f(B value) public pure returns (B out) {
        out = value;
    }

    function f(address value) public pure returns (address out) {
        out = value;
    }
}

contract B {
}

上面的两个f函数重载最终都接受了 ABI 的地址类型,尽管它们在 Solidity 中被认为是不同的。

重载解析和参数匹配

通过将当前作用域中的函数声明与函数调用中提供的参数匹配来选择重载函数。如果所有参数都可以隐式转换为预期类型,则选择函数作为重载候选者。如果不完全是一个候选人,则决议失败。

笔记

重载解析不考虑返回参数。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract A {
    function f(uint8 val) public pure returns (uint8 out) {
        out = val;
    }

    function f(uint256 val) public pure returns (uint256 out) {
        out = val;
    }
}

调用f(50)会产生类型错误,因为50可以隐式转换为uint8 和uint256类型。另一方面f(256)将解决f(uint256)重载,因为256不能隐式转换为uint8.

以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号