3、函数与修饰符:函数定义与可见性、view/pure函数、modifier修饰符、构造函数
好,咱们今天来聊聊 Solidity 里的函数和修饰符。这部分内容,说白了就是智能合约的「行为逻辑」所在。你写的合约能不能干活,干得漂不漂亮,全看这里。
我个人习惯把函数比作合约的「手脚」,而修饰符就是给这些手脚加的「规矩」。咱们一个一个来看。
3.1 函数的可见性:谁可以调用?
函数的可见性,决定了谁能调用它。Solidity 提供了四种可见性修饰符:public、internal、external、private。嗯,这里要注意,它们的作用范围是递减的。
| 修饰符 | 合约内部 | 子合约 | 外部(交易/其他合约) |
|---|---|---|---|
public |
✅ | ✅ | ✅ |
internal |
✅ | ✅ | ❌ |
external |
❌(需用 this.f()) |
❌ | ✅ |
private |
✅ | ❌ | ❌ |
你可能会问,external 和 public 有什么区别?说白了,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 对吧?view 和 pure 就是干这个的。
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; // 编译会报错
// }
}
view 或 pure,可以先不写。编译器会帮你检查,如果它发现你没改状态,会建议你加上。我个人习惯是,能加就加,这样调用方一看就知道这函数不花钱。
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 里有 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) {}
}
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 保证安全。而 view 和 pure 函数则放在前端直接调用,不花用户一分钱。
好了,函数和修饰符这块就聊到这儿。记住一句话:可见性决定谁能调用,view/pure 决定是否花钱,modifier 决定调用前要过什么关,构造函数决定合约的初始状态。把这四个点吃透了,写合约就顺手多了。