- 一、函数可见性的四种类型
- 二、修饰器(modifier)的作用
- 三、Foundry 示例
- 四、
modifier
示例 - 五、
pure
和view
函数修饰符 —— 状态访问语义的标识 - 六、函数可见性的设计建议
- 七、课后练习
- 下一课预告
在 Solidity 中,函数的可见性不仅决定了“谁可以调用”,更深层地影响到合约之间的交互方式、函数的 ABI 暴露、安全性设计和 gas 成本。本课还将介绍如何使用函数修饰器(modifier)实现访问控制与逻辑封装。
一、函数可见性的四种类型
Solidity 中的函数(和状态变量)支持 4 种可见性:
可见性 | 外部调用 | 合约内部调用 | 派生合约调用 | ABI 导出 | 典型用途 |
---|---|---|---|---|---|
public |
✅ | ✅ | ✅ | ✅ | 用户或其他合约调用 |
external |
✅ | ❌(需 this.f() ) |
✅ | ✅ | 节省 gas 的入口函数 |
internal |
❌ | ✅ | ✅ | ❌ | 内部逻辑、继承使用 |
private |
❌ | ✅ | ❌ | ❌ | 完全私有逻辑 |
示例代码:
contract Visibility {
// 可被任何人调用
function publicFn() public pure returns (string memory) {
return "public";
}
// 只能从外部调用:Visibility(address).externalFn()
function externalFn() external pure returns (string memory) {
return "external";
}
// 合约内 & 子合约可调用
function internalFn() internal pure returns (string memory) {
return "internal";
}
// 仅当前合约内部可调用
function privateFn() private pure returns (string memory) {
return "private";
}
}
继承中的可见性
子合约可以:
- ✅ 继承并
override
public
与internal
函数 - ❌ 无法
override
或访问private
函数
contract Base {
function visible() internal virtual {}
function secret() private {}
}
contract Child is Base {
function useVisible() public {
visible(); // ✅
// secret(); // ❌ compile error
}
}
external
是不是更安全?
不是。external
函数依旧公开访问,只是:
- gas 成本更低(尤其是动态数组传参)
- 不能合约内部直接调用(除非使用
this.
前缀)
function update(uint[] calldata data) external {
// ...
}
二、修饰器(modifier)的作用
Modifier 是 Solidity 的语法糖,允许在函数执行前或后添加逻辑 —— 非常适合权限控制、状态检查、reentrancy 防御等场景。
常见用途:
- 权限控制(如 onlyOwner)
- 重入保护
- 状态锁定
- 函数执行顺序约束
示例:访问控制修饰器
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
function sensitiveAction() public onlyOwner {
// 只有 owner 才能执行
}
_;
表示“继续执行被修饰的函数”modifier
中的逻辑优先于主函数体执行。modifier
的执行是链式调用(按声明顺序,即从左往右执行),且可以在同一函数上附加多个 modifiers。
三、Foundry 示例
我们来编写一个带有函数可见性示例的测试用例:
合约:Visibility.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Visibility {
address public owner = msg.sender;
function publicFunc() public pure returns (string memory) {
return "public";
}
function externalFunc() external pure returns (string memory) {
return "external";
}
function internalFunc() internal pure returns (string memory) {
return "internal";
}
function privateFunc() private pure returns (string memory) {
return "private";
}
function callInternal() public pure returns (string memory) {
return internalFunc();
}
function callPrivate() public pure returns (string memory) {
return privateFunc();
}
}
测试:test/Visibility.t.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../src/Visibility.sol";
contract VisibilityTest is Test {
Visibility vis;
function setUp() public {
vis = new Visibility();
}
function testPublic() public {
assertEq(vis.publicFunc(), "public");
}
function testExternal() public {
// 只能外部调用
string memory val = Visibility(address(vis)).externalFunc();
assertEq(val, "external");
}
function testInternalAccess() public {
// 内部函数间接调用
assertEq(vis.callInternal(), "internal");
}
function testPrivateAccess() public {
assertEq(vis.callPrivate(), "private");
}
}
执行测试命令:
➜ counter git:(main) ✗ forge test --match-path test/Visibility.t.sol -vvv
[⠊] Compiling...
[⠒] Compiling 3 files with Solc 0.8.30
[⠑] Solc 0.8.30 finished in 544.36ms
Compiler run successful!
Ran 4 tests for test/Visibility.t.sol:VisibilityTest
[PASS] testExternal() (gas: 10240)
[PASS] testInternalAccess() (gas: 10329)
[PASS] testPrivateAccess() (gas: 10285)
[PASS] testPublic() (gas: 10293)
Suite result: ok. 4 passed; 0 failed; 0 skipped; finished in 5.25ms (4.99ms CPU time)
Ran 1 test suite in 189.08ms (5.25ms CPU time): 4 tests passed, 0 failed, 0 skipped (4 total tests)
四、modifier
示例
合约:ModifierVault.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract ModifierVault {
address public owner;
uint public balance;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
function deposit() public payable {
balance += msg.value;
}
function withdraw() public onlyOwner {
payable(msg.sender).transfer(balance);
balance = 0;
}
}
测试:test/ModifierVault.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../src/ModifierVault.sol";
contract ModifierTest is Test {
ModifierVault vault;
address alice = address(0x1);
address bob = address(0x2);
// 添加 receive 用于接收 ETH
receive() external payable {}
function setUp() public {
vault = new ModifierVault();
vm.deal(alice, 1 ether);
}
function testOnlyOwnerCanWithdraw() public {
// alice 向 vault 存款
vm.prank(alice);
vault.deposit{value: 1 ether}();
// 使用合约拥有者 address(this) 执行提款
vault.withdraw();
// 验证余额清零
assertEq(vault.balance(), 0);
}
function test_RevertWhen_NonOwnerWithdraws() public {
vm.prank(bob);
vm.expectRevert("Not owner"); // 或自定义错误选择器
vault.withdraw();
}
}
执行测试命令:
➜ counter git:(main) ✗ forge test --match-path test/ModifierVault.t.sol -vvv
[⠊] Compiling...
[⠒] Compiling 1 files with Solc 0.8.30
[⠑] Solc 0.8.30 finished in 558.83ms
Compiler run successful!
Ran 2 tests for test/ModifierVault.t.sol:ModifierTest
[PASS] testOnlyOwnerCanWithdraw() (gas: 36840)
[PASS] test_RevertWhen_NonOwnerWithdraws() (gas: 13505)
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 5.30ms (1.59ms CPU time)
Ran 1 test suite in 190.26ms (5.30ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)
五、pure
和 view
函数修饰符 —— 状态访问语义的标识
在 Solidity 中,函数除了权限可见性(public
/ private
等),还可以声明它们对状态变量的访问行为。这就是 pure
和 view
修饰符的用途:
修饰符 | 能否读取状态变量 | 能否修改状态变量 | 能否发送交易 | 示例用途 |
---|---|---|---|---|
pure |
❌ | ❌ | ❌ | 数学运算,纯逻辑函数 |
view |
✅ | ❌ | ❌ | 查询、只读状态 |
无修饰符 | ✅ | ✅ | ✅ | 默认允许任何操作 |
pure
修饰符:完全不接触状态的函数
pure
修饰符用于标记那些 不读取也不修改任何合约状态(包括 msg.sender
, block.timestamp
等) 的函数。
function add(uint a, uint b) public pure returns (uint) {
return a + b;
}
特性:
- 编译器静态检查,不允许访问任何状态变量或全局变量
- 理论上可以在链下完全复现,无需部署或调用链上数据
view
修饰符:只读函数
view
表示该函数 读取状态变量,但不允许写入(修改)状态。
uint public totalSupply = 100;
function getSupply() public view returns (uint) {
return totalSupply;
}
特性:
- 常用于 getter 函数
- 只能读取状态,不能写入
- 使用 view 函数是读取链上状态数据的标准方法
编译器强制规则举例
uint public counter = 0;
function get() public pure returns (uint) {
return counter; // ❌ 编译器报错:pure 函数不能读取状态变量
}
function read() public view returns (uint) {
return counter; // ✅ 允许读取
}
function write() public {
counter += 1; // ✅ 可读可写
}
测试示例(Foundry)
合约:src/MathLib.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MathLib {
uint public base = 10;
function addPure(uint a, uint b) public pure returns (uint) {
return a + b;
}
function addView(uint x) public view returns (uint) {
return base + x;
}
}
测试代码:test/MathLib.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../src/MathLib.sol";
contract MathTest is Test {
MathLib math;
function setUp() public {
math = new MathLib();
}
function testPureAdd() public view {
assertEq(math.addPure(2, 3), 5);
}
function testViewAdd() public view {
assertEq(math.addView(5), 15);
}
}
执行测试:
➜ counter git:(main) ✗ forge test --match-path test/MathLib.t.sol -vvv
[⠊] Compiling...
[⠒] Compiling 2 files with Solc 0.8.30
[⠑] Solc 0.8.30 finished in 534.57ms
Compiler run successful!
Ran 2 tests for test/MathLib.t.sol:MathTest
[PASS] testPureAdd() (gas: 9940)
[PASS] testViewAdd() (gas: 11665)
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 5.33ms (3.01ms CPU time)
Ran 1 test suite in 189.73ms (5.33ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)
六、函数可见性的设计建议
场景 | 建议使用 | 原因 |
---|---|---|
用户或外部合约调用的入口函数 | external |
节省 gas,明确只供外部访问 |
需要外部也可内部复用的函数 | public |
可在合约内部和外部同时调用 |
仅合约内部或子合约调用的辅助函数 | internal |
控制作用域,避免被误用或暴露接口 |
私有状态修改/校验/哈希计算等内部逻辑 | private |
强限制,仅当前合约可访问,增强封装性 |
不访问任何状态或区块变量,仅进行计算 | pure |
完全独立,节省 gas,适用于纯逻辑计算 |
读取状态变量或全局上下文(如 block) | view |
只读合约状态,不能修改,适用于查询或只读函数场景 |
七、课后练习
- 编写一个
Bank
合约,要求只有owner
能调用withdraw()
。 - 在
Bank
合约中添加一个internal
函数来计算利息。 - 使用 Foundry 为这两个函数编写测试用例,确保权限控制生效。
下一课预告
第 8 课:Solidity 中的继承与接口 —— 模块化不是“复制粘贴”的借口
在下一课中,我们将学习 Solidity 中的合约继承、接口、抽象合约等代码复用机制,掌握智能合约的模块化和解耦技巧。

声明:本作品采用署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)进行许可,使用时请注明出处。
Author: mengbin
blog: mengbin
Github: mengbin92
腾讯云开发者社区:孟斯特
—