solidity中的函数总结

函数是智能合约的基石,包含了丰富的特性和细节。我们将从基础到高级,逐一展开。

1. 函数的基本结构

一个 Solidity 函数的基本定义如下:
function 函数名([参数列表]) [可见性] [状态可变性] [函数修改器(多个)] [返回值]{函数体}

solidity 复制代码
//函数定义的示例
function functionName(parameter1Type parameter1, parameter2Type parameter2, ...)
    public
    pure
    returns (return1Type return1, return2Type return2, ...)
{
    // 函数体
}
  • function: 声明函数的关键字。
  • 参数列表: 接收外部传入的数据,可选。
  • 可见性 : public, internal, private, external ,定义了函数的可见性,缺省时默认为internal
  • 状态可变性 : pure, view, 默认(不写), payable ,定义了函数是否会读取或修改合约状态。
  • 函数修改器:修改器是一种特殊类型的函数,用于在目标函数执行前或执行后插入检查或逻辑
  • returns: 声明返回变量类型和名称(名称可选),可选。
  • 函数体 : 由花括号 {} 包围,包含具体的执行逻辑。

状态可变性修饰符 (pure/view/payable) 和函数修改器在解析时被同等对待,它们之间的相对顺序编译器并不强制


2. 函数的可见性(Visibility)

可见性修饰符决定了谁可以调用这个函数。

  • public:

    • 函数是合约接口的一部分。
    • 可以在内部调用,也可以通过消息调用从外部(包括其他合约)调用。
  • internal:

    • 函数只能在当前合约或其派生合约内部调用。
    • 这是默认的可见性(如果未指定)。
  • private:

    • 函数只能在当前合约内部调用,不能被派生合约继承和调用
    • 用于封装内部实现细节。
  • external:

    • 函数是合约接口的一部分。
    • 只能从外部调用 (通过其他合约或交易)。如果要在内部调用一个 external 函数,必须使用 this.functionName()

示例:

solidity 复制代码
pragma solidity ^0.8.0;

contract VisibilityBase {
    // public - 最开放的可见性
    function publicFunc() public pure returns (string memory) {
        return "Public - 内外皆可调用";
    }
    
    // internal - 默认可见性,家族内部使用
    function internalFunc() internal pure returns (string memory) {
        return "Internal - 仅合约内部和子合约";
    }
    
    // private - 严格保密,仅本合约内部
    function privateFunc() private pure returns (string memory) {
        return "Private - 仅本合约内部";
    }
    
    // external - 专门对外服务
    function externalFunc() external pure returns (string memory) {
        return "External - 主要供外部调用";
    }
    
    // 测试内部调用
    function testInternalCalls() public view {
        publicFunc();      // ✅ OK
        internalFunc();    // ✅ OK  
        privateFunc();     // ✅ OK
        // externalFunc(); // ❌ 错误:不能直接内部调用
        this.externalFunc(); // ✅ OK:通过this外部调用
    }
}

contract VisibilityChild is VisibilityBase {
    // 测试继承调用
    function testInheritanceCalls() public view {
        publicFunc();      // ✅ OK - 继承可用
        internalFunc();    // ✅ OK - 继承可用
        // privateFunc();  // ❌ 错误:private不继承
        this.externalFunc(); // ✅ OK - 外部调用
    }
}

contract ExternalCaller {
    // 测试外部调用
    function callExternal(address baseAddr) public view returns (string memory) {
        VisibilityBase base = VisibilityBase(baseAddr);
        
        base.publicFunc();    // ✅ OK
        base.externalFunc();  // ✅ OK
        // base.internalFunc(); // ❌ 错误:internal不能外部调用
        // base.privateFunc();  // ❌ 错误:private不能外部调用
    }
}

调用的external函数的 最佳实践:

solidity 复制代码
contract BestPractice {
    function externalFunction() external {}
    
    // ✅ 大多数情况:使用 this. 语法
    function normalCase() public {
        this.externalFunction();  // 简洁明了
    }
    
    // ✅ 需要明确性时:使用类型转换
    // 实际应用示例:
    // 假设 addr 可能实现多个接口(如 ERC20 和 ERC721)
    // 使用 IERC20(addr).transfer() 比直接调用更明确
    function whenClarityNeeded() public {
        BestPractice(address(this)).externalFunction();  // 更明确但冗长
    }    
}

3. 状态可变性(Mutability)

函数的状态可变性是函数签名的一部分,它定义了函数如何与区块链的状态进行交互。

它们是关键字,并且是互斥的,也就是说,一个函数一次只能使用一种状态可变性关键字(pure, view, payable 或无标记)

  • view:

    • 承诺不修改任何状态变量。
    • 可以读取状态变量和 immutable 变量。
    • view 函数中修改状态会导致编译错误。
  • pure:

    • 承诺不读取也不修改任何状态变量。
    • 只能使用传入的参数和函数内的局部变量进行计算,不能访问 msg.sender, block.timestamp 等全局变量。
    • pure 函数中读取状态会导致编译错误。
  • payable:

    • 函数可以接收以太币(ETH)。
    • 如果函数需要接收 ETH,必须 标记为 payable,否则交易会被拒绝。
  • 默认(不写):

    • 函数可以读取和修改状态变量。

示例:

solidity 复制代码
contract Mutability {
    uint256 public data = 1;

    function getData() public view returns (uint256) {
        return data; // 只读,不修改
    }

    function double(uint256 x) public pure returns (uint256) {
        return x * 2; // 与状态完全无关
    }

    function updateData(uint256 newData) public {
        data = newData; // 修改状态
    }

    function receiveEther() public payable {
        // 可以接收 ETH,通过 msg.value 获取金额
    }
}

Gas消耗情况对比:


4. 函数修改器(modifier)

函数修改器是一种特殊的语法结构,用于在函数执行前或执行后自动插入检查逻辑或修改行为,特别适合用于权限控制、状态检查、输入验证和重入保护等场景。它可以被多个函数复用,提高代码的可读性和安全性。

函数修改器是一个语法糖(编译期间扩展到函数的实现),可以用私有函数实现类似效果

函数的状态可变性(pure/view/payable)和函数修改器是两个不同维度的概念。状态可变性是函数的固有属性,而函数修改器是附加在函数上的可执行逻辑块。函数修改器不是一种新的状态可变性类别。

基本语法
solidity 复制代码
modifier modifierName(parameters) {
    _; // 下划线表示原函数体的插入位置
}
简单示例: 重入保护
solidity 复制代码
bool private locked;

modifier noReentrancy() {
    require(!locked, "No reentrancy");
    locked = true;
    _;
    locked = false;
}
复杂示例:完整的权限管理
solidity 复制代码
pragma solidity ^0.8.0;

contract AccessControl {
    address public owner;
    mapping(address => bool) public admins;
    mapping(address => bool) public managers;
    bool public paused = false;
    
    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
    
    constructor() {
        owner = msg.sender;
        admins[msg.sender] = true;
        managers[msg.sender] = true;
    }
    
    // 多个修改器组合
    modifier onlyOwner() {
        require(msg.sender == owner, "Only owner");
        _;
    }
    
    //访问控制:只有合约管理员可以调用的修改器
    modifier onlyAdmin() {
        require(admins[msg.sender], "Only admin");
        _;
    }
    
    modifier onlyManager() {
        require(managers[msg.sender], "Only manager");
        _;
    }
    
    //状态检查:用来限制合约未暂停时才能调用的修改器
    modifier whenNotPaused() {
        require(!paused, "Contract paused");
        _;
    }
    
    //输入验证:不能是0地址
    modifier validAddress(address _address) {
        require(_address != address(0), "Invalid address");
        _;
    }
    
    // 使用多个修改器
    function addAdmin(address newAdmin) 
        public 
        onlyOwner 
        validAddress(newAdmin) 
        whenNotPaused 
    {
        admins[newAdmin] = true;
    }
    
    function addManager(address newManager) 
        public 
        onlyAdmin 
        validAddress(newManager)
        whenNotPaused
    {
        managers[newManager] = true;
    }
    
    function pause() public onlyAdmin {
        paused = true;
    }
    
    function unpause() public onlyAdmin {
        paused = false;
    }
    
    function transferOwnership(address newOwner) 
        public 
        onlyOwner 
        validAddress(newOwner)
    {
        emit OwnershipTransferred(owner, newOwner);
        owner = newOwner;
    }
}
注意事项
  1. 下划线位置很重要_; 的位置决定了原函数体何时执行
  2. 修改器顺序:多个修改器从左到右依次执行
  3. Gas 消耗:复杂的修改器会增加 Gas 消耗
  4. 错误处理 :在修改器中使用 requirerevert 来抛出错误
  5. 状态变量访问:修改器可以访问和修改合约的状态变量

5. 特殊函数

getter函数(访问器)

不需要定义,所有public状态变量自动创建getter函数

构造函数(Constructor)
  • 一个使用 constructor 关键字声明的特殊函数。
  • 在合约创建时仅执行一次。链上的字节码没有构造函数,而是执行构造函数之后的结果。
  • 用于初始化合约的初始状态。
solidity 复制代码
contract MyContract {
    address public owner;
    uint256 public createdAt;

    constructor(uint256 _initialValue) {
        owner = msg.sender;
        createdAt = block.timestamp;
        // ... 其他初始化逻辑
    }
}
回调函数(Receive and Fallback)
  • receive(): 一个专门用于接收"空调用"(无数据)的以太币转账的函数。

    复制代码
          // ✅ 这是"空调用" 
         (bool success, ) = target.call{value: 1 ether}("");
    
          // ✅ transfer 和 send 也是空调用
          target.transfer(0.5 ether);
          target.send(0.2 ether);
  • fallback() : 当调用一个不存在的函数时,或者转账但没有 receive() 函数时,会执行此函数。

solidity 复制代码
contract ReceiveFallback {
    event Received(address sender, uint256 amount);
    event FallbackCalled(bytes data);

    // 当向合约发送纯 ETH(无数据)时调用
    receive() external payable {
        emit Received(msg.sender, msg.value);
    }

    // 当调用不存在的函数或发送纯 ETH(无数据)且没有 receive() 时调用
    fallback() external payable {
        emit FallbackCalled(msg.data);
    }
}


Q: 如果to地址存在合约代码,msg.value > 0 且 data 不为空,但在合约中没有找到与 data 中函数选择器匹配的函数(比如存放的是转账备注),那么receive() 函数也不会触发吗?
A: 是的,不会触发。一旦 data 字段不为空,EVM 就进入了"函数调用模式",receive() 这个为"纯转账"设计的专用通道就关闭了。

Q: 是否可以理解为转账备注只能用于向EOA账户的转账呢?
A: 基本上可以这样理解。向合约账户发送带有 data 的 ETH 是一种危险的、通常是无意义的行为。

接收方类型 带有备注 (data 不为空) 的 ETH 转账 结果与建议
EOA(外部账户) 有意义且安全 推荐。ETH 成功转账,备注被永久记录在链上。接收方可以通过区块链浏览器查看备注。这是备注的标准用法。
CA(合约账户) 危险且通常无意义 强烈不推荐 。这是一种"盲发"行为。因为你无法预测合约的 fallback 函数会做什么: 1. 它可能正常接收 ETH,但忽略你的备注。 2. 它可能因为 fallback 函数逻辑复杂而消耗大量 Gas,导致失败。 3. 它可能拒绝接收,导致你的整个交易回滚。 备注信息对合约来说是不可见的 ,除非开发者特意在 fallback 函数中编写了解析 msg.data 的代码,但这极其罕见。

6. 函数参数与返回值

参数
  • 参数可以像变量一样声明,它们的作用域在函数体内。
  • 对于复杂类型(如数组、结构体),可以指定数据位置(memory, calldata, storage)。
返回值
  • 使用 returns 关键字声明。
  • 可以返回多个值。
  • 返回值可以命名,命名后无需在函数体内显式返回变量(但仍可使用 return)。
solidity 复制代码
contract ReturnExample {
    // 返回多个未命名的值
    function returnMultiple() public pure returns (uint256, bool, string memory) {
        return (42, true, "hello");
    }

    // 返回多个命名的值
    // 在函数体内,这些名字作为局部变量存在
    function returnNamed() public pure returns (uint256 number, bool flag, string memory text) {
        number = 100;
        flag = false;
        text = "world";
        // 不需要显式的 return 语句,但也可以使用
        // return (number, flag, text);
    }

    // 解构赋值:调用方可以按需接收返回值
    function callOther() public pure {
        (uint256 num, , string memory txt) = returnNamed(); // 只接收第一个和第三个返回值
    }
}

7. 函数重载(Overloading)

合约中可以存在多个同名函数,但它们的参数类型或数量必须不同。

solidity 复制代码
contract Overloading {
    function add(uint256 a, uint256 b) public pure returns (uint256) {
        return a + b;
    }

    // 重载:参数类型不同
    function add(string memory a, string memory b) public pure returns (string memory) {
        return string(abi.encodePacked(a, b));
    }

    // 重载:参数数量不同
    function add(uint256 a, uint256 b, uint256 c) public pure returns (uint256) {
        return a + b + c;
    }
}

8. 函数调用

内部调用
  • 在当前合约的上下文环境中直接执行,不会产生 EVM 调用(CALL)。内部函数调用高效,在同⼀个 EVM 环境⾥,外部调⽤会切换上下⽂环境
  • 例如:functionA()
外部调用
  • 通过消息调用来执行另一个合约的函数。
  • 会产生一个 EVM 调用(CALL, DELEGATECALL 等),会切换上下文(如 msg.sender 变为当前合约地址)。
  • 例如:otherContract.functionB()
solidity 复制代码
contract Callee {
    function foo() public pure returns (string memory) {
        return "Hello from Callee";
    }
}

contract Caller {
    // 通过接口进行外部调用
    function callAnother(address _calleeAddress) public returns (string memory) {
        Callee callee = Callee(_calleeAddress);
        return callee.foo(); // 这是一个外部调用
    }
}
使用 delegatecall
  • delegatecall 是一种特殊的外部调用,它使用当前合约的存储,但执行目标合约的代码。
  • 常用于实现库和可升级合约模式。
solidity 复制代码
contract Library {
    function setNumber(uint256 _num) public {
        // 注意:这个函数将修改调用者的存储,而不是本合约的存储
        // 存储布局必须与调用者完全一致!
    }
}

contract Caller {
    uint256 public number;
    address public libraryAddress;

    function delegatecallSetNumber(uint256 _num) public {
        (bool success, ) = libraryAddress.delegatecall(
            abi.encodeWithSignature("setNumber(uint256)", _num)
        );
        require(success, "Delegatecall failed");
    }
}
特性 CALL DELEGATECALL STATICCALL
执行上下文 目标合约的代码和存储 调用者合约的存储,目标合约的代码 目标合约的代码和存储
存储修改 修改目标合约的存储 修改调用者合约的存储 ❌ 禁止任何状态修改
msg.sender 调用者合约地址 原始交易发送者地址 调用者合约地址
address(this) 目标合约地址 调用者合约地址 目标合约地址
msg.value ✅ 可传递 ETH ❌ 不能传递 ETH(自动为 0) ❌ 不能传递 ETH(自动为 0)
Gas 成本 较高(2600+ gas) 较高(2600+ gas) 较高(2600+ gas)
主要用途 普通合约间调用,ETH 转账 库模式,可升级合约,代码复用 只读调用,状态查询
状态修改权限 完全权限 完全权限(但修改调用者的存储) ❌ 无权限
回滚影响 仅回滚目标合约的操作 回滚调用者合约的所有操作 仅回滚目标合约的操作

9. 最佳实践与安全考虑

  1. 使用 Checks-Effects-Interactions 模式

    • 先检查条件(Checks)。
    • 然后更新状态变量(Effects)。
    • 最后与其他合约交互(Interactions)。
    • 这是防止重入攻击的关键。
  2. 小心重入攻击

    • 在与其他合约交互(发送 ETH 或调用未知函数)之前,完成所有状态更新。
    • 使用 ReentrancyGuard 修饰器。
  3. 合理使用 payable

    • 只有确实需要接收 ETH 的函数才标记为 payable,以减少攻击面。
  4. 明确的可见性

    • 始终为每个函数显式指定可见性,不要依赖默认的 internal
  5. 参数验证

    • 使用 require 在函数开头验证输入参数和状态条件。

总结

Solidity 函数是一个功能丰富且复杂的主题,涵盖了从基本的定义、可见性、状态可变性,到高级特性如修饰器、重载、特殊函数和底层调用。理解这些概念对于编写安全、高效和可维护的智能合约至关重要。

相关推荐
“抚琴”的人5 小时前
C# 取消机制(CancellationTokenSource/CancellationToken)
开发语言·c#·wpf·1024程序员节·取消机制
介一安全5 小时前
【Frida Android】基础篇12:Native层hook基础——调用原生函数
android·网络安全·逆向·安全性测试·frida·1024程序员节
Cathyqiii6 小时前
Diffusion-TS:一种基于季节性-趋势分解与重构引导的可解释时间序列扩散模型
人工智能·神经网络·1024程序员节
存储国产化前线6 小时前
从浪涌防护到系统可控,天硕工业级SSD重构工业存储安全体系
ssd·固态硬盘·1024程序员节·工业级固态硬盘
瑞禧生物ruixibio6 小时前
4-ARM-PEG-Alkene(2)/Biotin(2),四臂聚乙二醇-烯烃/生物素多功能支链分子
1024程序员节
焦点链创研究所6 小时前
BUYCOIN:以社区共治重构加密交易版图,定义交易所3.0时代
1024程序员节
DO_Community6 小时前
DigitalOcean Gradient™ 平台上线 fal 四款多模态 AI 模型:快速生成图像与音频
1024程序员节
胎粉仔6 小时前
Swift 初阶 —— inout 参数 & 数据独占问题
开发语言·ios·swift·1024程序员节
MeowKnight9587 小时前
【C】使用C语言举例说明逻辑运算符的短路特性
c语言·1024程序员节