Solidity 函数 + 事件 + 错误处理
函数可见性(public/private/internal/external)、view/pure、自定义修饰符、事件系统、错误处理四种方式
日期: 2026-04-12 方向: Solidity 阶段: 第一阶段:基础构建 标签: #solidity #functions #events #error-handling #modifiers
今日目标
| 类型 | 内容 |
|---|---|
| 学习 | 函数可见性(public/private/internal/external)、view/pure、自定义修饰符、事件系统、错误处理四种方式 |
| 实操 | 编写一个完整的 SimpleStorage 合约,包含所有上述特性 |
| 产出 | 可部署的 SimpleStorage 合约 + 事件监听示例 |
一、函数可见性 (Visibility)
Solidity 函数有四种可见性,决定了谁可以调用这个函数。这是智能合约安全的第一道防线。
1.1 四种可见性详解
contract VisibilityDemo {
// ============ public ============
// 谁都能调用:外部账户、其他合约、本合约内部
// 编译器自动为 public 状态变量生成 getter 函数
uint public count = 0;
function publicFunc() public returns (uint) {
count += 1;
return count;
}
// ============ external ============
// 只能从外部调用(其他合约或 EOA)
// 不能在合约内部直接调用(除非用 this.externalFunc())
// 对于接收大量 calldata 参数时比 public 更省 gas
function externalFunc(uint[] calldata data) external returns (uint) {
return data.length;
}
// ============ internal ============
// 只能在本合约和子合约中调用
// 外部无法调用
// 类似于面向对象语言中的 protected
function internalFunc() internal view returns (uint) {
return count;
}
// ============ private ============
// 只能在本合约中调用
// 子合约也不能调用!
function privateFunc() private pure returns (string memory) {
return "I am private";
}
// 演示调用关系
function demo() public returns (uint) {
publicFunc(); // ✅ 可以调用 public
// externalFunc(); // ❌ 不能直接调用 external
// this.externalFunc(new uint[](0)); // ✅ 可以通过 this 调用(但浪费 gas)
internalFunc(); // ✅ 可以调用 internal
privateFunc(); // ✅ 可以调用 private
return count;
}
}
contract Child is VisibilityDemo {
function childDemo() public view returns (uint) {
// publicFunc(); // ✅ 继承了 public
internalFunc(); // ✅ 继承了 internal
// privateFunc(); // ❌ 无法访问父合约的 private
return 0;
}
}
1.2 可见性选择指南
| 可见性 | 使用场景 | Gas 优化 |
|---|---|---|
external | 只被外部调用的函数 | calldata 参数比 memory 省 gas |
public | 需要内部和外部都能调用 | 自动生成 getter |
internal | 只在合约家族内使用的工具函数 | 不暴露给外部 |
private | 绝对不想被子合约覆写的内部逻辑 | 最限制 |
最佳实践:遵循最小权限原则——能用 private 就不用 internal,能用 internal 就不用 public,对外接口优先用 external。
二、view 和 pure 函数
2.1 view 函数
view 函数承诺不修改状态,但可以读取状态。调用 view 函数不消耗 gas(除非从合约内部调用)。
uint public balance = 100;
// view: 读取状态变量但不修改
function getBalance() public view returns (uint) {
return balance; // ✅ 可以读
// balance = 200; // ❌ 不能写
}
// 以下操作在 view 函数中被禁止:
// 1. 修改状态变量
// 2. 发射事件 (emit)
// 3. 创建其他合约
// 4. 使用 selfdestruct
// 5. 发送 ETH
// 6. 调用非 view/pure 函数
2.2 pure 函数
pure 函数承诺既不读取也不修改状态,它只依赖输入参数和本地变量。
// pure: 不读取也不修改状态
function add(uint a, uint b) public pure returns (uint) {
return a + b;
}
function calculateHash(string memory input) public pure returns (bytes32) {
return keccak256(abi.encodePacked(input));
}
// pure 函数中被禁止的额外操作(除了 view 的限制):
// 1. 读取状态变量
// 2. 读取 address(this).balance
// 3. 读取 block/tx/msg 的成员(msg.sig 和 msg.data 除外)
// 4. 调用非 pure 函数
2.3 三者对比
修改状态 读取状态 Gas 消耗
普通函数 ✅ ✅ 要 gas
view 函数 ❌ ✅ 外部调用免费*
pure 函数 ❌ ❌ 外部调用免费*
* 从合约内部调用 view/pure 函数仍然消耗 gas(因为是在一笔交易内)
三、自定义修饰符 (Modifiers)
修饰符(modifier)是 Solidity 独有的语法糖,用于在函数执行前后插入检查逻辑。它极大地减少了代码重复。
3.1 基础修饰符
contract ModifierDemo {
address public owner;
bool public paused;
constructor() {
owner = msg.sender;
}
// 最常用的修饰符:权限检查
modifier onlyOwner() {
require(msg.sender == owner, "Not the owner");
_; // 这个下划线代表"执行被修饰的函数体"
}
// 带参数的修饰符
modifier minimumAmount(uint _min) {
require(msg.value >= _min, "Insufficient amount");
_;
}
// 状态检查修饰符
modifier whenNotPaused() {
require(!paused, "Contract is paused");
_;
}
// 使用修饰符
function withdraw() public onlyOwner whenNotPaused {
// 只有 owner 在合约未暂停时才能执行
payable(owner).transfer(address(this).balance);
}
function deposit() public payable minimumAmount(0.01 ether) {
// 必须发送至少 0.01 ETH
}
function pause() public onlyOwner {
paused = true;
}
function unpause() public onlyOwner {
paused = false;
}
}
3.2 修饰符执行顺序
modifier checkA() {
// Step 1: checkA 的前置逻辑
require(msg.sender != address(0), "A check");
_;
// Step 4: checkA 的后置逻辑(少用)
}
modifier checkB() {
// Step 2: checkB 的前置逻辑
require(block.timestamp > 0, "B check");
_;
// Step 3: checkB 的后置逻辑
}
// 多个修饰符从左到右执行
function doSomething() public checkA checkB {
// 这里是函数体
// 执行顺序:checkA前 → checkB前 → 函数体 → checkB后 → checkA后
}
3.3 重入锁修饰符
// 这是安全领域最重要的修饰符之一
bool private _locked;
modifier nonReentrant() {
require(!_locked, "Reentrant call");
_locked = true;
_;
_locked = false;
}
function withdraw(uint amount) public nonReentrant {
require(balances[msg.sender] >= amount);
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
balances[msg.sender] -= amount;
}
四、事件 (Events)
事件是智能合约与外部世界通信的重要机制。事件数据存储在交易日志(logs)中,不存储在合约 storage 里,因此 gas 成本远低于状态变量。
4.1 事件定义与触发
contract EventDemo {
// 定义事件
event Transfer(
address indexed from, // indexed: 可被过滤搜索
address indexed to, // 最多 3 个 indexed 参数
uint256 value // 非 indexed: 存储在 data 部分
);
event Approval(
address indexed owner,
address indexed spender,
uint256 value
);
// 匿名事件(少用)
event AnonymousEvent(uint data) anonymous;
// 触发事件
mapping(address => uint) public balances;
function transfer(address to, uint amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[to] += amount;
// emit 关键字触发事件
emit Transfer(msg.sender, to, amount);
}
}
4.2 indexed 参数详解
event Transfer(
address indexed from, // Topic[1] - 可以作为过滤条件
address indexed to, // Topic[2] - 可以作为过滤条件
uint256 value // Data - 不能直接过滤
);
// 事件在 EVM 中的底层结构:
// Topic[0] = keccak256("Transfer(address,address,uint256)") -- 事件签名
// Topic[1] = from address (indexed)
// Topic[2] = to address (indexed)
// Data = abi.encode(value) (non-indexed)
// indexed 的限制:
// - 最多 3 个 indexed 参数
// - indexed 的 string/bytes/array 会被哈希存储,无法恢复原始值
// - 非 indexed 参数更便宜但不能被用作搜索条件
4.3 前端监听事件
// 使用 ethers.js v6 监听事件
import { ethers } from "ethers";
const provider = new ethers.BrowserProvider(window.ethereum);
const contract = new ethers.Contract(contractAddress, abi, provider);
// 监听新事件(实时)
contract.on("Transfer", (from, to, value, event) => {
console.log(`Transfer: ${from} → ${to}, amount: ${ethers.formatEther(value)}`);
console.log("Block:", event.log.blockNumber);
console.log("Tx Hash:", event.log.transactionHash);
});
// 查询历史事件
const filter = contract.filters.Transfer(myAddress, null); // from = myAddress
const events = await contract.queryFilter(filter, -1000); // 最近 1000 个区块
events.forEach(e => {
console.log("Past transfer:", e.args.from, e.args.to, e.args.value);
});
4.4 事件的 Gas 成本
| 操作 | Gas 成本 |
|---|---|
| 基础事件触发 | ~375 gas |
| 每个 indexed topic | ~375 gas |
| 每字节 data | ~8 gas |
| 存储 32 字节到 storage | ~20,000 gas |
事件比 storage 便宜约 50 倍!这就是为什么很多链下数据通过事件记录而非状态变量。
五、错误处理
Solidity 有四种错误处理方式,在不同场景下选择最适合的方式至关重要。
5.1 require
// require: 检查输入条件和外部调用返回值
// 失败时 revert 并退还剩余 gas
function transfer(address to, uint amount) public {
require(to != address(0), "Cannot transfer to zero address");
require(amount > 0, "Amount must be positive");
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[to] += amount;
}
// require 的特点:
// - 检查不通过时 revert 并返回错误消息
// - 退还未使用的 gas
// - 适用于:输入验证、权限检查、外部调用检查
5.2 revert
// revert: 无条件回滚,通常与 if 配合使用
function withdraw(uint amount) public {
if (amount == 0) {
revert("Amount cannot be zero");
}
if (balances[msg.sender] < amount) {
revert("Insufficient balance");
}
// 复杂条件判断时,revert 比嵌套 require 更清晰
if (amount > maxWithdraw && msg.sender != owner) {
revert("Exceeds max withdrawal for non-owner");
}
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
5.3 assert
// assert: 检查内部不变量(invariants)
// 用于检测代码 bug,不应该在正常情况下触发
function internalCheck() internal view {
// assert 消耗所有剩余 gas(Solidity 0.8.0 之前)
// 0.8.0 之后也使用 revert,但语义上仍表示"不应该发生"
assert(totalSupply == sumOfAllBalances());
// 适用场景:
// 1. 算术运算后的不变量检查
// 2. 数据结构一致性验证
// 3. 不应该到达的代码分支
}
function divide(uint a, uint b) public pure returns (uint) {
require(b > 0, "Division by zero"); // 用户输入检查用 require
uint result = a / b;
assert(result * b <= a); // 内部不变量用 assert
return result;
}
5.4 自定义错误 (Custom Errors) - 推荐!
从 Solidity 0.8.4 开始,自定义错误是最 gas 高效的错误处理方式。
// 定义自定义错误
error InsufficientBalance(address account, uint256 requested, uint256 available);
error Unauthorized(address caller);
error ZeroAddress();
error AmountTooSmall(uint256 amount, uint256 minimum);
error TransferFailed();
contract CustomErrorDemo {
mapping(address => uint) public balances;
address public owner;
constructor() {
owner = msg.sender;
}
function transfer(address to, uint amount) public {
if (to == address(0)) revert ZeroAddress();
uint balance = balances[msg.sender];
if (balance < amount) {
revert InsufficientBalance(msg.sender, amount, balance);
}
balances[msg.sender] = balance - amount;
balances[to] += amount;
}
function adminAction() public {
if (msg.sender != owner) {
revert Unauthorized(msg.sender);
}
// ... admin logic
}
}
5.5 四种方式对比
| 方式 | Gas 成本 | 适用场景 | 信息 |
|---|---|---|---|
require(cond, "msg") | 中等 | 输入验证 | 字符串 |
revert("msg") | 中等 | 复杂条件 | 字符串 |
assert(cond) | 中等 | 内部不变量 | 无消息 |
revert CustomError() | 最低 | 所有场景 | 结构化数据 |
Gas 成本对比:
// require 带字符串: 部署时和触发时都更贵
require(x > 0, "Value must be positive");
// 错误消息存储在合约字节码中,增加部署成本
// 触发时需要 ABI 编码字符串
// 自定义错误: 只存储 4 字节选择器
error ValueNotPositive();
if (x == 0) revert ValueNotPositive();
// 部署更小,触发更便宜
// 节省约 ~50% 的错误处理 gas
六、代码实战:完整 SimpleStorage 合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/// @title SimpleStorage - 综合演示函数、事件和错误处理
/// @author SC Day3 Learning
/// @notice 一个带权限管理的键值存储合约
// ============ 自定义错误 ============
error SimpleStorage__NotOwner(address caller);
error SimpleStorage__NotAdmin(address caller);
error SimpleStorage__KeyEmpty();
error SimpleStorage__ValueEmpty();
error SimpleStorage__KeyNotFound(string key);
error SimpleStorage__ContractPaused();
error SimpleStorage__AlreadyAdmin(address admin);
error SimpleStorage__NotExistingAdmin(address admin);
contract SimpleStorage {
// ============ 状态变量 ============
address public immutable owner;
bool public paused;
uint256 public totalEntries;
mapping(string => string) private store;
mapping(string => bool) private keyExists;
mapping(address => bool) public isAdmin;
string[] private allKeys;
// ============ 事件 ============
event ValueSet(
address indexed setter,
string key,
string value,
uint256 timestamp
);
event ValueDeleted(
address indexed deleter,
string key,
uint256 timestamp
);
event AdminAdded(address indexed admin, address indexed addedBy);
event AdminRemoved(address indexed admin, address indexed removedBy);
event ContractPaused(address indexed pausedBy);
event ContractUnpaused(address indexed unpausedBy);
// ============ 修饰符 ============
modifier onlyOwner() {
if (msg.sender != owner) {
revert SimpleStorage__NotOwner(msg.sender);
}
_;
}
modifier onlyAdmin() {
if (!isAdmin[msg.sender] && msg.sender != owner) {
revert SimpleStorage__NotAdmin(msg.sender);
}
_;
}
modifier whenNotPaused() {
if (paused) {
revert SimpleStorage__ContractPaused();
}
_;
}
modifier validKey(string memory _key) {
if (bytes(_key).length == 0) {
revert SimpleStorage__KeyEmpty();
}
_;
}
// ============ 构造函数 ============
constructor() {
owner = msg.sender;
isAdmin[msg.sender] = true;
emit AdminAdded(msg.sender, msg.sender);
}
// ============ 外部函数(external)============
/// @notice 设置键值对
/// @param _key 键名
/// @param _value 值
function set(string calldata _key, string calldata _value)
external
whenNotPaused
onlyAdmin
validKey(_key)
{
if (bytes(_value).length == 0) {
revert SimpleStorage__ValueEmpty();
}
if (!keyExists[_key]) {
keyExists[_key] = true;
allKeys.push(_key);
totalEntries++;
}
store[_key] = _value;
emit ValueSet(msg.sender, _key, _value, block.timestamp);
}
/// @notice 删除键值对
function remove(string calldata _key)
external
whenNotPaused
onlyAdmin
validKey(_key)
{
if (!keyExists[_key]) {
revert SimpleStorage__KeyNotFound(_key);
}
delete store[_key];
keyExists[_key] = false;
totalEntries--;
emit ValueDeleted(msg.sender, _key, block.timestamp);
}
/// @notice 批量设置(展示 external + calldata 的 gas 优势)
function batchSet(
string[] calldata _keys,
string[] calldata _values
) external whenNotPaused onlyAdmin {
require(_keys.length == _values.length, "Length mismatch");
require(_keys.length > 0, "Empty arrays");
for (uint i = 0; i < _keys.length; i++) {
// 内部调用用 internal 函数
_setInternal(_keys[i], _values[i]);
}
}
// ============ 公共函数(public)============
/// @notice 查询值(view 函数 - 不消耗 gas)
function get(string memory _key) public view validKey(_key) returns (string memory) {
if (!keyExists[_key]) {
revert SimpleStorage__KeyNotFound(_key);
}
return store[_key];
}
/// @notice 检查键是否存在
function exists(string memory _key) public view returns (bool) {
return keyExists[_key];
}
/// @notice 获取所有键
function getAllKeys() public view returns (string[] memory) {
return allKeys;
}
// ============ 内部函数(internal)============
function _setInternal(string memory _key, string memory _value) internal {
if (bytes(_key).length == 0 || bytes(_value).length == 0) return;
if (!keyExists[_key]) {
keyExists[_key] = true;
allKeys.push(_key);
totalEntries++;
}
store[_key] = _value;
emit ValueSet(msg.sender, _key, _value, block.timestamp);
}
// ============ 纯函数(pure)============
/// @notice 生成键的哈希(pure - 不读取也不修改状态)
function hashKey(string memory _key) public pure returns (bytes32) {
return keccak256(abi.encodePacked(_key));
}
/// @notice 拼接两个字符串
function concat(string memory a, string memory b)
public
pure
returns (string memory)
{
return string.concat(a, ":", b);
}
// ============ 管理函数 ============
function addAdmin(address _admin) external onlyOwner {
if (isAdmin[_admin]) {
revert SimpleStorage__AlreadyAdmin(_admin);
}
isAdmin[_admin] = true;
emit AdminAdded(_admin, msg.sender);
}
function removeAdmin(address _admin) external onlyOwner {
if (!isAdmin[_admin]) {
revert SimpleStorage__NotExistingAdmin(_admin);
}
if (_admin == owner) {
revert SimpleStorage__NotOwner(msg.sender);
}
isAdmin[_admin] = false;
emit AdminRemoved(_admin, msg.sender);
}
function togglePause() external onlyOwner {
paused = !paused;
if (paused) {
emit ContractPaused(msg.sender);
} else {
emit ContractUnpaused(msg.sender);
}
}
// ============ 内部不变量检查 ============
/// @notice 验证合约状态一致性(用于调试)
function checkInvariant() public view returns (bool) {
uint256 existingCount = 0;
for (uint i = 0; i < allKeys.length; i++) {
if (keyExists[allKeys[i]]) {
existingCount++;
}
}
// assert 用于检查内部不变量
assert(existingCount == totalEntries);
return true;
}
}
七、关键要点总结
| 要点 | 说明 |
|---|---|
| external > public | 外部接口优先用 external,calldata 更省 gas |
| view/pure 免 gas | 外部直接调用不消耗 gas(RPC call,非 transaction) |
| modifier 减少重复 | 把检查逻辑提取到修饰符,DRY 原则 |
| 事件比 storage 便宜 | 用事件记录链下数据,存储成本低 50 倍 |
| 自定义错误最省 gas | 比 require("string") 节省 ~50% gas |
| indexed 最多 3 个 | 事件参数 indexed 用于链下过滤搜索 |
| require = 输入验证 | 检查用户输入和外部条件 |
| assert = 不变量 | 检查代码 bug,不应该在正常情况下触发 |
八、常见误区
误区 1:view/pure 函数在合约内部调用也免 gas
function writeThenRead() public {
balances[msg.sender] = 100; // 这消耗 gas
uint b = getBalance(); // ← 这个 view 调用也消耗 gas!
// 因为是在一笔交易中调用,getBalance 的执行也是交易的一部分
}
// 只有从外部直接调用 view/pure(通过 eth_call)才免 gas
误区 2:modifier 中 _ 的位置无所谓
modifier lockAndUnlock() {
locked = true;
_; // 函数体在这里执行
locked = false; // 函数体执行完后执行这行
}
// _ 的位置决定了函数体的执行时机
// 前置检查:_ 在最后
// 后置清理:_ 在中间
误区 3:事件数据可以在合约中读取
// ❌ 事件只写不读!
// 合约内部无法读取自己触发过的事件
// 事件是给链下(前端/索引器)使用的
emit Transfer(from, to, amount);
// 之后你无法在合约中查询 "最近一次 Transfer 的 to 是谁"
误区 4:require 和 assert 在 0.8.0+ 行为相同
// 从 0.8.0 开始,两者都使用 revert opcode
// 但语义不同:
// require: "条件不满足" → 输入/前提问题
// assert: "不变量被破坏" → 代码 bug
// 静态分析工具会区别对待它们
九、面试关联
Q: external 和 public 有什么区别?为什么 external 更省 gas?
A: external 函数的参数可以使用 calldata(只读内存区域,直接引用交易数据),而 public 函数的参数必须复制到 memory。对于大型数组参数,calldata 避免了内存复制,因此更省 gas。但 external 函数不能被合约内部直接调用(除非通过 this.func(),这会产生外部调用开销)。
Q: 为什么 Solidity 需要 view 和 pure 修饰符?
A: 这是 Solidity 的设计契约(design by contract)思想。view/pure 让编译器和开发者明确函数的副作用:view 保证不写状态,pure 保证不读也不写。这不仅帮助编译器优化(外部 view/pure 调用可以通过 eth_call 执行而不需要发交易),也让代码审计更容易判断函数是否可能修改状态。
Q: 自定义错误(custom error)相比 require 有什么优势?
A: 三个核心优势:(1) Gas 效率——自定义错误在 ABI 编码时只有 4 字节选择器 + 参数,而 require 的字符串消息存储在字节码中并在运行时编码,成本更高。(2) 结构化信息——可以携带类型化的参数(如 InsufficientBalance(address, uint256, uint256)),前端可以精确解析。(3) NatSpec 文档——可以用 @param 注释错误参数,IDE 和文档工具可以展示。
Q: 如何防止重入攻击?
A: 三种策略:(1) Checks-Effects-Interactions 模式——先做检查,再更新状态,最后进行外部调用。(2) nonReentrant 修饰符(ReentrancyGuard)——使用一个锁变量,函数执行时锁定,执行后解锁。(3) 使用 pull payment 模式——不主动给用户发 ETH,让用户自己来提取。最佳实践是三者结合使用。
十、参考资源
| 资源 | 链接 | 说明 |
|---|---|---|
| Solidity 函数文档 | https://docs.soliditylang.org/en/latest/contracts.html#functions | 官方函数文档 |
| Solidity 事件文档 | https://docs.soliditylang.org/en/latest/contracts.html#events | 官方事件文档 |
| Solidity 错误处理 | https://docs.soliditylang.org/en/latest/control-structures.html#error-handling | 官方错误处理文档 |
| OpenZeppelin ReentrancyGuard | https://docs.openzeppelin.com/contracts/4.x/api/security#ReentrancyGuard | 重入锁参考实现 |
| SWC Registry | https://swcregistry.io/ | 智能合约弱点分类 |
| Solidity by Example - Events | https://solidity-by-example.org/events/ | 事件代码示例 |