第三讲:Solidity语言入门(下)——函数定义与可见性、修饰符、事件、错误处理

好,咱们接着往下聊。上一讲我们把Solidity的基础数据类型和存储位置捋了一遍,今天要聊的这几个概念,可以说是写智能合约的“四梁八柱”——函数怎么定义、谁能调用、怎么加权限、怎么报错、怎么通知前端。嗯,这些搞明白了,你写合约才算真正入了门。

一、函数定义与可见性

先说说函数。Solidity里的函数,说白了就是一段可以被调用的逻辑代码。但跟普通编程语言不一样,它多了几个“可见性”的修饰符——publicprivateinternalexternal

我刚开始学的时候,老搞混internalprivate。后来踩了个坑才明白:private是连子合约都不能调,internal是允许子合约调用的。你想想看,如果你写了一个基础合约,想让子合约复用某个函数,那就得用internal

二、可见性详解

修饰符 合约内部 子合约 外部调用
public
private
internal
external

这里有个小细节:external函数在接收大量数据时,比public更省gas。为什么?因为public会把参数复制到内存里,而external直接从calldata读取。我在项目中处理过一个大数组的合约,换成external后gas省了将近20%。

// 函数定义示例
contract Demo {
    uint public data = 100;

    function setData(uint _newData) public {
        data = _newData;
    }

    function getData() external view returns (uint) {
        return data;
    }

    function internalFunc() internal pure returns (string memory) {
        return "只能内部或子合约调用";
    }

    function privateFunc() private pure returns (string memory) {
        return "只有本合约能调用";
    }
}

三、修饰符(modifier)

修饰符这东西,说白了就是函数的“前置检查器”。你可以在函数执行前,先跑一段逻辑——比如检查调用者是不是管理员、检查参数是否合法。

我个人习惯把修饰符当作“权限门禁”。每次写合约,第一个写的修饰符永远是onlyOwner。为什么?因为90%的合约漏洞都出在权限控制上。

contract AccessControl {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

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

    function sensitiveAction() public onlyOwner {
        // 只有owner能调用
    }
}

注意那个_;,它代表“把控制权交还给原函数”。如果你在_;前面写逻辑,那就是前置检查;写在后面,就是后置处理。嗯,这个用法在防重入攻击时特别有用。

避坑指南:修饰符里的require如果失败了,整个交易会回滚。我曾经在修饰符里写了一个复杂的计算逻辑,结果gas消耗暴涨。记住:修饰符里尽量只做简单的检查,别搞复杂运算。

四、事件(Event)

事件是Solidity里一个非常巧妙的设计。合约里的状态变量,前端是读不到的——除非你主动“发射”一个事件。事件会被记录在交易日志里,前端可以通过监听事件来获取数据。

说白了,事件就是合约跟外部世界喊话的“喇叭”。

contract EventDemo {
    event Transfer(address indexed from, address indexed to, uint value);

    function transfer(address _to, uint _value) public {
        // 转账逻辑...
        emit Transfer(msg.sender, _to, _value);
    }
}

这里有个关键点:indexed关键字。加了indexed的参数,可以被前端过滤搜索。最多只能有三个indexed参数。我在做NFT市场时,就靠indexed参数快速定位某个用户的交易记录,效率非常高。

小技巧:事件里的参数,能加indexed就尽量加。虽然会多花一点点gas,但换来的是前端查询的极大便利。你想想看,如果事件没有索引,前端就得遍历所有区块——那速度简直不敢想。

五、错误处理(require/revert/assert)

智能合约一旦部署,就不能随便改。所以错误处理特别重要。Solidity提供了三种报错方式:requirerevertassert

我刚开始写合约时,三种混着用,结果出了bug很难排查。后来总结了一套规则:

  • require:检查外部输入是否合法。比如“转账金额不能为0”、“调用者必须是管理员”。失败时退还剩余gas。
  • revert:跟require类似,但更灵活。可以在复杂的if-else里用。
  • assert:检查内部逻辑是否出错。比如“余额计算后不能为负数”。失败时消耗所有gas。
contract ErrorHandling {
    uint public balance;

    function deposit(uint _amount) public {
        require(_amount > 0, "存款金额必须大于0");
        balance += _amount;
    }

    function withdraw(uint _amount) public {
        if (_amount > balance) {
            revert("余额不足");
        }
        balance -= _amount;
    }

    function checkInvariant() public view {
        // 内部逻辑检查:余额永远不会溢出
        assert(balance >= 0);
    }
}

警告:千万不要在assert里写外部输入检查!因为assert失败会消耗所有gas,如果用户因为输入错误被扣光gas,那体验就太差了。我见过一个项目,把用户输入检查写在assert里,结果用户每次输错都要付全额的gas——这设计简直反人类。

六、实战组合:一个带权限的转账合约

来,咱们把今天学的知识点串起来,写一个完整的例子:

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

    event Transfer(address indexed from, address indexed to, uint value);
    event Log(string message);

    modifier onlyOwner() {
        require(msg.sender == owner, "只有owner能操作");
        _;
    }

    constructor() {
        owner = msg.sender;
    }

    function mint(address _to, uint _amount) public onlyOwner {
        require(_amount > 0, "数量必须大于0");
        require(_to != address(0), "地址不能为0");

        balances[_to] += _amount;
        emit Transfer(address(0), _to, _amount);
    }

    function transfer(address _to, uint _amount) public {
        require(_amount > 0, "数量必须大于0");
        require(balances[msg.sender] >= _amount, "余额不足");

        balances[msg.sender] -= _amount;
        balances[_to] += _amount;
        emit Transfer(msg.sender, _to, _amount);
    }
}

这个合约里,我们用onlyOwner修饰符控制了铸币权限,用require做了输入检查,用emit发射了事件。嗯,一个标准的ERC20雏形就有了。

好了,今天的内容就到这。函数可见性、修饰符、事件、错误处理——这四个概念你搞明白了,写合约的基本功就算扎实了。下一讲咱们聊聊映射和结构体,那才是真正开始构建复杂数据的时候。


公众号:蓝海资料掘金营,微信deep3321