4. 函数与修饰符:函数定义与可见性、view/pure函数、函数修饰符

好,咱们今天聊聊 Solidity 里的函数。这东西说白了就是智能合约的「手脚」——合约能做什么事,全看你怎么定义这些函数。我在早期写合约时,经常在可见性上栽跟头,后来才慢慢摸清门道。

4.1 函数定义与可见性

先看一个最基础的函数长什么样:

function 函数名(参数列表) 可见性 状态可变性 修饰符 returns (返回值类型) {
    // 函数体
}

嗯,这里要注意的是「可见性」。Solidity 提供了四种:

可见性 说明 谁可以调用
public 公开的 内部、外部都能调用
private 私有的 仅当前合约内部
internal 内部的 当前合约及子合约
external 外部的 仅外部调用(内部用 this.f() 也可以)

我个人习惯是:能 private 就 private,能 internal 就 internal。为什么?你想想看,暴露越少,攻击面越小。我在项目中遇到过有人把敏感函数写成 public,结果被恶意调用的惨案。

核心原则:最小权限原则。默认用 private,需要继承时用 internal,必须对外暴露时才用 public 或 external。

4.2 view 与 pure 函数

这两个修饰符很有意思。它们告诉编译器:「我这个函数不会改状态」。

  • view:可以读状态,但不能改。比如读取余额、查询配置。
  • pure:连状态都不读,纯计算。比如数学运算、字符串拼接。

举个例子:

// view 函数:读取状态
function getBalance(address user) public view returns (uint) {
    return balances[user];
}

// pure 函数:纯计算
function add(uint a, uint b) public pure returns (uint) {
    return a + b;
}

我曾经犯过一个低级错误——在 view 函数里尝试修改状态变量,编译器直接报错。嗯,这其实是个好设计,帮你提前发现问题。

小技巧:调用 view/pure 函数不需要 gas(外部调用时)。所以能用 view 就别用普通函数,能 pure 就别用 view。省钱又安全。

4.3 函数修饰符(modifier)

修饰符是 Solidity 里一个很酷的特性。说白了,它就是个「函数包装器」——在函数执行前后插入一些逻辑。

最常见的用法是权限控制:

modifier onlyOwner() {
    require(msg.sender == owner, "你不是合约所有者");
    _;  // 这个下划线代表「继续执行原函数」
}

function withdraw() public onlyOwner {
    // 只有 owner 才能调用
    payable(owner).transfer(address(this).balance);
}

你看,_; 这个符号很关键。它表示「把控制权交还给原函数」。修饰符可以放在函数执行前、执行后,甚至前后都放逻辑。

再举个带参数的修饰符:

modifier minAmount(uint _min) {
    require(msg.value >= _min, "金额不足");
    _;
}

function deposit() public payable minAmount(0.1 ether) {
    // 只有大于等于 0.1 ETH 的转账才能通过
}

注意:修饰符里的 require 失败时,整个交易会回滚。所以别在修饰符里做太重的计算,否则 gas 浪费很心疼。我曾经见过有人把整个业务逻辑塞进修饰符里,结果调试起来欲哭无泪。

4.4 组合使用:可见性 + 状态可变性 + 修饰符

实际项目中,这三个东西经常一起出现。比如:

contract Bank {
    address public owner;
    mapping(address => uint) private balances;

    modifier onlyOwner() {
        require(msg.sender == owner);
        _;
    }

    // public + view:对外可读
    function getBalance(address user) public view returns (uint) {
        return balances[user];
    }

    // external + onlyOwner:仅 owner 可调用
    function emergencyWithdraw() external onlyOwner {
        payable(owner).transfer(address(this).balance);
    }

    // private + pure:内部工具函数
    function _toWei(uint amount) private pure returns (uint) {
        return amount * 1 ether;
    }
}

我个人习惯把私有函数用下划线开头(比如 _toWei),这样一眼就能看出是内部用的。这不是语法要求,但团队协作时很有用。

4.5 避坑指南

  • 我曾经把 external 和 public 搞混,结果内部调用时多花了 gas。记住:内部调用 external 函数需要用 this.f(),这会产生外部调用开销。
  • 我曾经在 view 函数里用了 delete 操作,编译器没报错但逻辑错了。因为 delete 在某些情况下被视为状态修改。
  • 修饰符的执行顺序:多个修饰符时,从左到右执行。比如 function foo() public onlyOwner whenNotPaused,先检查 owner,再检查暂停状态。

总结一下:

  • 可见性:能 private 就别 public,能 internal 就别 external
  • view/pure:能用就用,省 gas 又安全
  • modifier:适合做前置检查,别塞太多逻辑

好了,函数这块其实不难,但细节很多。你写几个合约试试,踩踩坑就记住了。下一章咱们聊聊事件和错误处理,那个也挺有意思的。