十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Solidity 函数完全指南:从 Free Functions、view/pure 到 receive/fallback 与重载解析

Solidity 函数完全指南:从 Free Functions、view/pure 到 receive/fallback 与重载解析 Solidity 函数完全指南从 Free Functions、view/pure 到 receive/fallback 与重载解析【免费下载链接】soliditySolidity, the Smart Contract Programming Language项目地址: https://gitcode.com/GitHub_Trending/so/solidity本文档主体对应仓库文档docs/contracts/functions.rst并辅以编译器源码libsolidity/analysis/ViewPureChecker.cpp、libsolidity/codegen/ExpressionCompiler.cpp、libsolidity/analysis/TypeChecker.cpp、libsolidity/codegen/ContractCompiler.cpp佐证底层实现原理。本篇指南系统讲解 Solidity 中函数的全部核心语法与语义函数可以定义在合约内部也可以定义在合约外部free functions参数与返回值如何声明、如何返回多个值view/pure状态可变性约束在编译器与 EVM 层面如何被检查与强制合约接收 Ether 时receive与fallback特殊函数的执行时机与 2300 gas 限制以及函数重载的解析规则。读完本篇你将能够正确设计函数签名、规避状态可变性误用、正确处理原生 Ether 转账并理解重载解析失败的根本原因。函数定义的位置合约内与合约外Free Functions函数既可以定义在合约内部也可以定义在合约外部。定义在合约外部的函数也被称为free functions自由函数它们总是隐式具有internal可见性关于可见性的完整说明见 docs/contracts/visibility-and-getters.rst。它们的代码会被包含在所有调用它们的合约中这一点与 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; } }需要注意定义在合约外的函数仍然总是在某个合约的上下文中执行。它们依然可以调用其他合约、向它们发送 Ether、甚至销毁调用它们的合约selfdestruct等。与合约内函数的主要区别在于free functions无法直接访问this变量、存储变量以及不在其作用域内的函数。从编译器实现角度看free function 与普通合约内函数的处理路径并无本质区别——它们同样经过 libsolidity/analysis/ViewPureChecker.cpp 等分析阶段的检查其internal可见性由编译器在解析阶段确定。函数参数与返回变量函数接收带类型的参数作为输入并且与许多其他语言不同Solidity 函数可以返回任意数量的值。函数参数Function Parameters函数参数的声明方式与变量声明相同未使用的参数名可以省略。例如要让合约接受一个带有两个整数参数的外部调用// 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; } }函数参数可以像任何其他局部变量一样使用也可以被赋值。返回变量Return Variables返回变量在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); } }如果你使用提前return离开一个声明了返回变量的函数则必须在return语句中同时提供返回值。注意非 internal 函数不能返回某些类型。这些类型包括mapping、internal 函数类型、数据位置为storage的引用类型以及在 ABI coder v1 下的多维数组和结构体。上述限制不适用于库函数因为库函数使用不同的内部 ABI。返回多个值Returning Multiple Values当一个函数有多个返回类型时可以使用return (v0, v1, ..., vn)语句返回多个值。返回值的数量必须与返回变量的数量一致且类型必须匹配允许经过隐式转换。状态可变性State MutabilitySolidity 函数可以用view或pure声明向编译器承诺不修改状态或既不读也不改状态。编译器通过 libsolidity/analysis/ViewPureChecker.cpp 中的ViewPureChecker在分析阶段逐语句推断函数体实际所需的最小可变性并将声明的可变性与推断结果比对若函数体内存在读取状态或环境的表达式而函数声明为pure则报类型错误2527_errorFunction declared as pure, but this expression (potentially) reads from the environment or state...若函数体内存在修改状态的表达式而函数声明为view/pure则报类型错误8961_errorFunction cannot be declared as ... because this expression (potentially) modifies the state.。该检查器还会给出警告2018_error当推断出的最小可变性低于函数声明时提示 Function state mutability can be restricted to ...。View 函数声明为view的函数承诺不修改状态。当编译器的 EVM 目标为 Byzantium 或更新版本默认时调用view函数会使用STATICCALL操作码从 EVM 执行层面强制状态不被修改。对于库的view函数则使用DELEGATECALL因为不存在DELEGATECALL与STATICCALL组合的操作码这意味着库的view函数没有运行时阻止状态修改的检查。这通常不会带来安全负面影响因为库代码一般在编译期就是已知的静态检查器会进行编译期检查。以下语句被视为修改状态写入状态变量storage 与 transient storage触发事件见 docs/contracts/events.rst创建其他合约见 docs/contracts/creating-contracts.rst使用selfdestruct通过调用发送 Ether调用任何未标记为view或pure的函数使用底层调用low-level calls使用包含特定操作码的内联汇编。// 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; } }几点补充说明函数上的constant曾经是view的别名但在 0.5.0 版本中被移除了Getter 方法会被自动标记为view在 0.5.0 之前的版本中编译器不对view函数使用STATICCALL这曾允许通过无效的显式类型转换在view函数中修改状态使用STATICCALL后状态修改在 EVM 层面被阻止。从代码生成层面看libsolidity/codegen/ExpressionCompiler.cpp 中的调用生成逻辑会按优先级选择操作码// Order is important here, STATICCALL might overlap with DELEGATECALL. if (isDelegateCall) m_context Instruction::DELEGATECALL; else if (useStaticCall) m_context Instruction::STATICCALL; else m_context Instruction::CALL;即委托调用库函数场景走DELEGATECALLview/pure的外部调用走STATICCALL普通调用走CALL。Pure 函数声明为pure的函数承诺既不读取状态也不修改状态。特别地一个pure函数应当可以在只给定其输入和msg.data、而对当前区块链状态一无所知的情况下于编译期求值。这也意味着读取immutable变量可能不是 pure 操作——从 ViewPureChecker.cpp 的源码可以看到只有被赋值为字面量RationalNumber的immutable才被视为 pure否则其可读性要求会提升为view。若 EVM 目标为 Byzantium 或更新版本默认pure函数同样使用STATICCALL它不保证不读取状态但至少保证不修改状态。在上一节修改状态清单的基础上以下操作被视为读取状态读取状态变量storage 与 transient storage访问address(this).balance或address.balance访问block、tx、msg的任何成员msg.sig与msg.data除外调用任何未标记为pure的函数使用包含特定操作码的内联汇编。// 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); } }Pure 函数可以使用revert()和require()在出错时回滚潜在的状态更改。回滚状态更改不被视为状态修改因为只有先前在不受view/pure限制的代码中做出的状态更改才会被回滚而那段代码可以选择捕获revert而不继续传播它。这一行为也与STATICCALL操作码的语义一致。警告在 EVM 层面无法阻止函数读取状态只能阻止它们写入状态——即只有view能在 EVM 层面被强制执行pure不能。历史版本差异说明0.5.0 之前编译器不对pure函数使用STATICCALL曾可通过无效显式类型转换在pure函数中修改状态0.4.17 之前编译器不强制pure不读取状态。pure本质上是编译期类型检查可以通过合约类型之间的无效显式转换绕过——编译器能验证该类型的合约不会做状态更改操作但无法检查运行时被调用的合约是否真的属于该类型。特殊函数Special FunctionsReceive Ether 函数一个合约最多只能有一个receive函数声明语法为receive() external payable { ... }不带function关键字。该函数不能有参数、不能有返回值并且必须具有external可见性和payable状态可变性。它可以声明为virtual、可以被override覆盖也可以有修饰器modifiers。receive函数在以空 calldata 调用合约时执行即在普通 Ether 转账例如通过.send()或.transfer()时执行。如果合约没有receive函数但存在 payable 的 fallback 函数则普通 Ether 转账会调用 fallback 函数如果两者都不存在合约将无法通过不表示 payable 函数调用的交易接收 Ether并会抛出异常。在最坏情况下例如使用send或transfer时receive函数只能依赖2300 gas几乎没有空间执行除基础日志外的其他操作。以下操作消耗的 gas 会超过 2300 gas 补贴写入存储创建合约调用一个消耗大量 gas 的外部函数发送 Ether。警告一send()和transfer()已被弃用并计划移除详见 docs/contracts/units-and-global-variables.rst 中关于send与transfer的说明。警告二当 Ether 被直接发送给合约不经过函数调用即发送方使用send或transfer而接收合约既没有定义 receive 函数也没有 payable fallback 函数时会抛出异常并将 Ether 退回这在 Solidity v0.4.0 之前的行为不同。如果你希望合约接收 Ether就必须实现 receive 函数不推荐用 payable fallback 接收 Ether因为 fallback 会被调用且不会因发送方的接口混淆而失败。警告三没有 receive 函数的合约仍可作为 coinbase 交易矿工区块奖励的接收方或作为selfdestruct的目标接收 Ether。合约无法对此类转账做出反应也无法拒绝它们。这是 EVM 的设计选择Solidity 无法绕过。这也意味着address(this).balance可能高于合约中手工记账的总和例如在 receive 函数中维护一个计数器。下面是一个使用receive函数的 Sink回收站合约示例——它接收所有发送给它的 Ether且没有取回途径// 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 函数一个合约最多只能有一个fallback函数声明语法为fallback () external [payable]或fallback (bytes calldata input) external [payable] returns (bytes memory output)都不带function关键字。该函数必须具有external可见性可以声明为virtual、可以被override覆盖也可以有修饰器。fallback 函数在以下两种情况下执行调用合约时没有其他函数匹配给定的函数签名没有提供任何数据且合约没有 receive Ether 函数。fallback 函数总是接收数据但要想同时接收 Ether必须标记为payable。如果使用带参数版本的 fallbackinput将包含发送给合约的完整数据等于msg.data并可通过output返回数据。返回的数据不会经过 ABI 编码而是原样返回甚至不做填充。在最坏情况下如果 payable fallback 被当作 receive 使用它也只能依赖 2300 gas含义见上节。与任何函数一样只要有足够的 gas 传入fallback 函数也可以执行复杂操作。警告如果没有 receive 函数payablefallback 函数也会在普通 Ether 转账时被执行。建议在定义 payable fallback 时始终同时定义 receive 函数以区分 Ether 转账与接口混淆。提示如果需要在 fallback 中解码输入数据可以检查前四个字节的函数选择器然后用abi.decode结合数组切片语法解码 ABI 编码数据(c, d) abi.decode(input[4:], (uint256, uint256));。这只应作为最后手段应优先使用规范函数。完整示例Test、TestPayable与Caller三个合约展示了 fallback/receive 的分派行为// 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. // This will report a warning (deprecation) 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; } }从编译器实现看libsolidity/codegen/ContractCompiler.cpp 生成的外部入口分派逻辑清晰地体现了上述语义先检查 calldata 大小若为空且存在etherReceiver即 receive 函数则直接跳转执行 receive 并STOP若 calldata 非空或不存在 receive则进入 fallback 分支若 fallback 不是payable且需要校验 callvalue会先插入appendCallValueCheck()如果合约既没有 fallback 也没有 receive则appendRevert(Contract does not have fallback nor receive functions)有 fallback 但函数签名未命中时则appendRevert(Unknown signature and no fallback defined)。此外libsolidity/analysis/ContractLevelChecker.cpp 会校验合约中 receive/fallback 的数量与签名合法性。函数重载Function Overloading一个合约可以有多个同名但参数类型不同的函数这被称为重载并且同样适用于继承的函数。下面的例子展示了在合约A作用域内重载函数f// 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 类型上不同、但在外部类型上相同则属于错误。例如下面的代码无法编译因为B类型在 ABI 中就是address两个f重载最终都接受 ABI 层面的address类型尽管在 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 { }重载解析与参数匹配Overload Resolution and Argument Matching重载函数通过在当前作用域中将函数声明与函数调用中提供的实参进行匹配来选择。如果一个函数的所有实参都能被隐式转换为期望类型则该函数成为重载候选。如果候选恰好只有一个解析成功否则解析失败。注意返回参数不参与重载解析。// 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。从源码实现看libsolidity/analysis/TypeChecker.cpp 的重载解析过程会遍历所有候选声明调用functionType-canTakeArguments(*annotation.arguments)过滤出能接受实参的候选若最终候选唯一则解析成功否则分别报错 No matching declaration found after argument-dependent lookup.无匹配错误码9322_error或 No unique declaration found after argument-dependent lookup.候选不唯一错误码4487_error。若根本找不到候选声明则报7593_errorNo candidates for overload resolution found.。延伸阅读docs/contracts/visibility-and-getters.rstpublic/internal/private/external可见性与 Getter 自动生成规则docs/contracts/function-modifiers.rst函数修饰器modifiers的声明与组合docs/contracts/events.rst事件被视为修改状态的操作之一docs/contracts/creating-contracts.rstnew创建合约与create/create2docs/contracts/libraries.rst库函数的内部 ABI 与限制豁免docs/contracts/errors.rstrevert()/require()与自定义错误docs/types/value-types.rst各类型的默认值docs/types/conversion.rst隐式与显式类型转换规则。【免费下载链接】soliditySolidity, the Smart Contract Programming Language项目地址: https://gitcode.com/GitHub_Trending/so/solidity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表