Solidity 学习随记

本文集中记录 Solidity 函数可见性、状态可变性和数据位置。理解这三部分能够避免调用上下文混淆、状态误修改和不必要的 Gas 消耗。

函数可见性

可见性 合约内部调用 外部调用 典型用途
private 仅当前合约 不允许 局部实现细节
internal 当前合约与派生合约 不允许 可继承的内部逻辑
public 允许 允许 同时服务内部与外部
external 不能按函数名直接调用 允许 外部接口入口

external 的正确调用方式

同一合约中的 external 函数不能写成 operation() 进行内部调用。使用 this.operation() 可以调用,但它会经过 ABI 编码并执行一次 EVM 外部调用,msg.sender 也会变为当前合约地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract VisibilityDemo {
function operation(uint256 value) external pure returns (uint256) {
return _calculate(value);
}

function reuseLogic(uint256 value) external pure returns (uint256) {
return _calculate(value);
}

function externalSelfCall(uint256 value) external view returns (uint256) {
return this.operation(value);
}

function _calculate(uint256 value) internal pure returns (uint256) {
return value * 2;
}
}

推荐做法是把可复用逻辑提取到 internal 函数,外部入口只负责权限、参数和业务边界校验。

1
2
3
4
5
6
flowchart LR
A[外部用户] -->|调用 external| B[入口函数]
B --> C[权限与参数检查]
C --> D[internal 核心逻辑]
E[合约内其他入口] --> D
B -->|this 调用| F[新的 EVM CALL]

pure 与 view

  • pure 函数不能读取或修改合约状态。
  • view 函数可以读取状态,但不能修改状态。
  • 普通函数可以读取和修改状态。
  • payable 额外允许函数接收原生资产。

编译器会沿调用链检查状态可变性。一个函数即使没有直接赋值,只要调用的内部函数修改状态,它就不能声明为 viewpure

memory、storage 与 calldata

  • storage 表示链上持久化数据。局部 storage 变量通常是状态数据的引用。
  • memory 表示当前调用期间的临时数据,修改不会自动写回状态。
  • calldata 表示只读调用输入,适合 external 函数参数,通常可减少复制。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
contract DataLocationDemo {
uint256[5] private values = [10, 20, 30, 40, 50];

function operation() external returns (uint256, uint256) {
changeMemory(values);
changeStorage(values);
return (values[3], values[4]);
}

function changeMemory(uint256[5] memory input) internal pure {
input[3] = 8;
}

function changeStorage(uint256[5] storage input) internal {
input[4] = 10;
}
}

changeMemory 接收状态数组的副本,所以 values[3] 仍为 40changeStorage 持有真实状态引用,所以 values[4] 变为 10。最终返回 4010

调用与交易的区别

对节点执行只读调用不会创建交易,也不消耗用户链上 Gas。发送修改状态的交易需要签名、广播、进入区块并成功执行。交易中的 Solidity 返回值通常不会直接出现在普通交易回执中,前端应使用事件、状态读取或交易模拟获得业务结果。

1
2
3
4
5
6
7
8
9
10
11
sequenceDiagram
participant U as 用户
participant N as RPC 节点
participant C as 合约
U->>N: eth_call
N->>C: 本地模拟执行
C-->>U: 直接返回结果
U->>N: eth_sendRawTransaction
N->>C: 区块内执行交易
C-->>N: 状态与事件
N-->>U: 交易回执

常见错误

  1. external 入口当作可直接复用的内部函数。
  2. 使用 this 自调用后忽略 msg.sender 和重入边界变化。
  3. memory 副本的修改误认为会写回状态。
  4. 为修改状态的函数错误添加 viewpure
  5. 依赖交易返回值,而没有设计事件或状态查询接口。
  6. 对大数组进行不必要的 storagememory 复制。

设计建议

外部入口保持简短,把核心业务规则放入内部函数。明确每个参数的数据位置,对外部调用遵循先检查、再更新状态、最后交互的顺序。测试时同时覆盖直接外部调用、代理调用、合约自调用和内部函数复用路径。