Solidity 入门:变量、函数与回调机制
这篇把 Solidity 初学最容易混的几块放在一起:变量类型、函数可见性、数据位置(memory/storage/calldata)、以及 receive / fallback 回调。
你可以把它当作一份“先跑通认知,再写代码”的速查稿。
一、Solidity 是什么
Solidity 是面向 EVM 的静态编译型高级语言,语法受 C++、JavaScript 影响明显。
如果你有前端或后端基础,上手门槛不高,但它和普通服务端语言的最大区别在于:每一步状态写入都和 Gas 成本直接挂钩。
二、合约最小结构
一个最简单的合约通常包含四件事:
- 编译器版本声明(
pragma solidity) - 合约定义(
contract) - 状态变量(state variable)
- 函数(function)
1 | pragma solidity ^0.8.0; |
三、函数:可见性与状态可变性
1) 可见性(visibility)
public:内外都可调external:通常给外部调用internal:仅当前合约及继承合约可调private:仅当前合约可调
2) 状态可变性(mutability)
view:只读状态,不写pure:既不读状态也不写状态payable:允许接收 ETH
1 | function add(uint i, uint j) public pure returns (uint) { |
四、变量与类型:先记住这一层
1) 状态变量 vs 本地变量
- 状态变量:存在链上
storage,写入贵 - 本地变量:函数执行期的临时数据,多在
memory
2) 常量相关
constant:编译期常量,写死immutable:部署时赋值,之后不可改
3) 常见类型
- 值类型:
bool、int/uint、address、bytes1~bytes32、enum - 引用类型:
array、struct、mapping
五、引用类型与数据位置
Solidity 里引用类型的成本差异,核心就看数据位置:
storage:链上持久化,最贵memory:函数调用期间存在calldata:外部函数参数区,只读,通常更省 Gas
1 | function copy(uint[] calldata arrs) public returns (uint len) { |
一个常见优化点:参数只读时优先 calldata,别先拷贝到 memory 再用。
六、数组、结构体、映射:三个高频容器
1) 数组(Array)
- 定长:
uint[10] - 动态:
uint[] - 常用成员:
length、push、pop
注意:在链上循环数组,长度不可控时很容易把 Gas 顶上去。
2) 结构体(Struct)
适合把多个相关字段打包成一个业务对象,例如用户资料、订单记录。
3) 映射(Mapping)
1 | mapping(address => uint) public balances; |
映射读取不存在的 key 会返回默认值(如 0)。
它没有“长度”概念,也不能直接遍历全部 key。
七、地址与转账:address vs address payable
address:20 字节地址address payable:可转账地址,可调用transfer/send
1 | function testTransfer(address payable to) public { |
transfer/send 都有 2300 gas stipend 的历史限制语境。实际开发中,很多团队更倾向 call + 显式检查返回值,配合重入保护一起做。
八、特殊函数:constructor、receive、fallback
1) constructor
部署时执行一次,用于初始化。
链上运行时字节码里不再包含构造逻辑本体,而是构造执行后的结果。
2) receive() external payable
合约接收纯 ETH(空 calldata)时触发。
3) fallback() external [payable]
调用了不存在的函数,或某些不匹配场景时触发。
如果没有 receive,转账时也可能落到 fallback(取决于调用方式与 calldata)。
九、全局变量里最常用的几个
block.number:当前区块号block.timestamp:当前区块时间戳msg.sender:当前调用者msg.value:本次调用携带的 weitx.origin:整条调用链最初发起者(权限判断一般不建议依赖)
十、初学阶段最容易踩的坑
- 循环不设边界:数组过大时函数可能直接超 Gas。
- 数据位置乱用:能
calldata的参数别无脑拷贝。 - 权限控制粗糙:管理员操作要配合 modifier 和事件。
- 回调函数理解不清:收款路径、调用路径没分开。
- 版本差异忽略:例如 0.8+ 默认带溢出检查,和 0.8 前行为不同。
十一、练手建议:做一个简版 Bank 合约
可以用下面这组要求自测:
- 用户可向合约存款(
payable) - 记录每个地址余额(
mapping(address => uint)) - 维护存款 Top N(可先做 Top 3)
- 仅管理员可提取全部 ETH(
onlyOwner)
先把功能跑通,再做两件事:
- 补事件(
Deposit/Withdraw) - 补安全细节(重入防护、错误处理)
如果你刚开始学 Solidity,别急着追“大全”。先把这篇里的函数、类型和回调路径吃透,再进到 token、DEX、治理类合约,速度会快很多。
Solidity 入门:变量、函数与回调机制