3、函数与修饰符:函数定义与可见性、view/pure函数、modifier修饰符、构造函数

好,咱们今天来聊聊 Solidity 里的函数和修饰符。这部分内容,说白了就是智能合约的「行为逻辑」所在。你写的合约能不能干活,干得漂不漂亮,全看这里。

我个人习惯把函数比作合约的「手脚」,而修饰符就是给这些手脚加的「规矩」。咱们一个一个来看。

3.1 函数的可见性:谁可以调用?

函数的可见性,决定了谁能调用它。Solidity 提供了四种可见性修饰符:publicinternalexternalprivate。嗯,这里要注意,它们的作用范围是递减的。

修饰符 合约内部 子合约 外部(交易/其他合约)
public
internal
external ❌(需用 this.f()
private

你可能会问,externalpublic 有什么区别?说白了,external 函数在接收大量数据时更省 gas。因为它的参数可以直接从 calldata 读取,不用先拷贝到内存里。我在项目中遇到过,一个接收大数组的函数,用 external 比用 public 能省下不少 gas。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract VisibilityExample {
    uint private secretNumber = 42;
    uint internal internalData = 100;

    // public:谁都能调用
    function publicFunc() public pure returns (string memory) {
        return "Anyone can call me";
    }

    // external:只能外部调用
    function externalFunc() external pure returns (string memory) {
        return "Only external calls";
    }

    // internal:合约内部和子合约可以调用
    function internalFunc() internal view returns (uint) {
        return internalData;
    }

    // private:只有本合约可以调用
    function privateFunc() private view returns (uint) {
        return secretNumber;
    }

    // 测试调用 internal 函数
    function callInternal() public view returns (uint) {
        return internalFunc();
    }
}
⚠️ 注意: 别以为 private 就真的「私密」了。区块链上的数据都是公开的,private 只是阻止其他合约代码直接访问,但链上数据本身任何人都能读到。我曾经见过有人把私钥存在 private 变量里,结果...嗯,你懂的。

3.2 view 和 pure 函数:不花钱的调用

这两个修饰符,说白了就是告诉编译器:「我这个函数不会改链上状态」。你想想看,如果函数只是读数据,那就不需要消耗 gas 对吧?viewpure 就是干这个的。

  • view:承诺不修改状态,但可以读取状态变量。
  • pure:承诺既不修改也不读取状态变量,只依赖传入的参数。
contract ViewPureExample {
    uint public data = 100;

    // view 函数:可以读状态,但不能改
    function getData() public view returns (uint) {
        return data;
    }

    // pure 函数:不能读也不能改状态
    function add(uint a, uint b) public pure returns (uint) {
        return a + b;
    }

    // 错误示例:pure 函数里不能读状态变量
    // function wrongPure() public pure returns (uint) {
    //     return data; // 编译会报错
    // }
}
💡 小技巧: 如果你不确定一个函数该不该加 viewpure,可以先不写。编译器会帮你检查,如果它发现你没改状态,会建议你加上。我个人习惯是,能加就加,这样调用方一看就知道这函数不花钱。

3.3 modifier 修饰符:给函数加「规矩」

Modifier 是 Solidity 里一个非常强大的特性。它允许你在函数执行前后插入一段逻辑。最常见的用法就是做权限检查。

我举个例子,你写了一个只有管理员才能调用的函数,难道每个函数里都写一遍 require(msg.sender == owner)?那也太累了。Modifier 就是来解决这个问题的。

contract ModifierExample {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    // 定义一个 modifier
    modifier onlyOwner() {
        require(msg.sender == owner, "Not the owner");
        _;  // 这个下划线表示「继续执行原函数」
    }

    // 使用 modifier
    function sensitiveAction() public onlyOwner {
        // 只有 owner 能执行到这里
    }

    // modifier 还可以带参数
    modifier greaterThan(uint _minValue) {
        require(_minValue > 0, "Value must be greater than 0");
        _;
    }

    function setValue(uint _value) public greaterThan(_value) {
        // _value 必须大于 0
    }
}

你想想看,_; 这个符号很关键。它代表「原函数体」插入的位置。你可以在 _; 前后都写逻辑,实现「前置检查」和「后置处理」。

🔑 核心要点: Modifier 的执行顺序是:先执行 modifier 中 _; 之前的代码,然后执行原函数,最后执行 _; 之后的代码。如果 modifier 里有 return,它会直接返回,原函数不会执行。

3.4 构造函数:合约的「出生证明」

构造函数在合约部署时执行一次,之后就不会再被调用了。它通常用来初始化状态变量,比如设置 owner 地址。

在 Solidity 0.7.0 之前,构造函数的名字必须和合约名一样。现在统一用 constructor 关键字,清爽多了。

contract ConstructorExample {
    address public owner;
    uint public initialValue;

    // 构造函数可以带参数
    constructor(uint _initialValue) {
        owner = msg.sender;
        initialValue = _initialValue;
    }

    // 构造函数也可以继承
}

// 继承时调用父合约构造函数
contract ChildContract is ConstructorExample(100) {
    // 或者通过构造函数参数传递
    // constructor(uint _val) ConstructorExample(_val) {}
}
⚠️ 注意: 构造函数一旦部署就无法修改。如果你在构造函数里写死了某个地址,后面想改就难了。我曾经犯过一个错,在构造函数里直接把合约的 ownership 转给了某个地址,结果那个地址的私钥丢了...嗯,那合约就成了「孤儿合约」。

3.5 实战建议:如何组合使用?

在实际项目中,这些特性往往是组合使用的。我给你一个典型的模式:

contract TypicalPattern {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

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

    // 公开的只读函数
    function getOwner() public view returns (address) {
        return owner;
    }

    // 只有 owner 才能调用的敏感操作
    function updateConfig(uint _newValue) external onlyOwner {
        // 更新配置
    }

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

你发现没有?external + onlyOwner 是一个很常见的组合。外部调用省 gas,modifier 保证安全。而 viewpure 函数则放在前端直接调用,不花用户一分钱。

好了,函数和修饰符这块就聊到这儿。记住一句话:可见性决定谁能调用,view/pure 决定是否花钱,modifier 决定调用前要过什么关,构造函数决定合约的初始状态。把这四个点吃透了,写合约就顺手多了。