Solidity 入门:变量、函数与回调机制

这篇把 Solidity 初学最容易混的几块放在一起:变量类型、函数可见性、数据位置(memory/storage/calldata)、以及 receive / fallback 回调。

你可以把它当作一份“先跑通认知,再写代码”的速查稿。


一、Solidity 是什么

Solidity 是面向 EVM 的静态编译型高级语言,语法受 C++、JavaScript 影响明显。
如果你有前端或后端基础,上手门槛不高,但它和普通服务端语言的最大区别在于:每一步状态写入都和 Gas 成本直接挂钩


二、合约最小结构

一个最简单的合约通常包含四件事:

  1. 编译器版本声明(pragma solidity
  2. 合约定义(contract
  3. 状态变量(state variable)
  4. 函数(function)
1
2
3
4
5
6
7
8
9
10
11
12
13
pragma solidity ^0.8.0;

contract Counter {
uint public counter;

constructor() {
counter = 0;
}

function count() public {
counter = counter + 1;
}
}

三、函数:可见性与状态可变性

1) 可见性(visibility)

  • public:内外都可调
  • external:通常给外部调用
  • internal:仅当前合约及继承合约可调
  • private:仅当前合约可调

2) 状态可变性(mutability)

  • view:只读状态,不写
  • pure:既不读状态也不写状态
  • payable:允许接收 ETH
1
2
3
4
5
6
7
8
9
10
11
function add(uint i, uint j) public pure returns (uint) {
return i + j;
}

function addTo(uint y) public view returns (uint) {
return counter + y;
}

function deposit() public payable {
deposited += msg.value;
}

四、变量与类型:先记住这一层

1) 状态变量 vs 本地变量

  • 状态变量:存在链上 storage,写入贵
  • 本地变量:函数执行期的临时数据,多在 memory

2) 常量相关

  • constant:编译期常量,写死
  • immutable:部署时赋值,之后不可改

3) 常见类型

  • 值类型:boolint/uintaddressbytes1~bytes32enum
  • 引用类型:arraystructmapping

五、引用类型与数据位置

Solidity 里引用类型的成本差异,核心就看数据位置:

  • storage:链上持久化,最贵
  • memory:函数调用期间存在
  • calldata:外部函数参数区,只读,通常更省 Gas
1
2
3
4
function copy(uint[] calldata arrs) public returns (uint len) {
numbers = arrs; // calldata -> storage(发生拷贝)
return numbers.length;
}

一个常见优化点:参数只读时优先 calldata,别先拷贝到 memory 再用。


六、数组、结构体、映射:三个高频容器

1) 数组(Array)

  • 定长:uint[10]
  • 动态:uint[]
  • 常用成员:lengthpushpop

注意:在链上循环数组,长度不可控时很容易把 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
2
3
4
5
function testTransfer(address payable to) public {
if (address(this).balance >= 10) {
to.transfer(10);
}
}

transfer/send 都有 2300 gas stipend 的历史限制语境。实际开发中,很多团队更倾向 call + 显式检查返回值,配合重入保护一起做。


八、特殊函数:constructorreceivefallback

1) constructor

部署时执行一次,用于初始化。
链上运行时字节码里不再包含构造逻辑本体,而是构造执行后的结果。

2) receive() external payable

合约接收纯 ETH(空 calldata)时触发。

3) fallback() external [payable]

调用了不存在的函数,或某些不匹配场景时触发。
如果没有 receive,转账时也可能落到 fallback(取决于调用方式与 calldata)。


九、全局变量里最常用的几个

  • block.number:当前区块号
  • block.timestamp:当前区块时间戳
  • msg.sender:当前调用者
  • msg.value:本次调用携带的 wei
  • tx.origin:整条调用链最初发起者(权限判断一般不建议依赖)

十、初学阶段最容易踩的坑

  1. 循环不设边界:数组过大时函数可能直接超 Gas。
  2. 数据位置乱用:能 calldata 的参数别无脑拷贝。
  3. 权限控制粗糙:管理员操作要配合 modifier 和事件。
  4. 回调函数理解不清:收款路径、调用路径没分开。
  5. 版本差异忽略:例如 0.8+ 默认带溢出检查,和 0.8 前行为不同。

十一、练手建议:做一个简版 Bank 合约

可以用下面这组要求自测:

  • 用户可向合约存款(payable
  • 记录每个地址余额(mapping(address => uint)
  • 维护存款 Top N(可先做 Top 3)
  • 仅管理员可提取全部 ETH(onlyOwner

先把功能跑通,再做两件事:

  • 补事件(Deposit / Withdraw
  • 补安全细节(重入防护、错误处理)

如果你刚开始学 Solidity,别急着追“大全”。先把这篇里的函数、类型和回调路径吃透,再进到 token、DEX、治理类合约,速度会快很多。

Author

Felix Tao

Posted on

2023-08-22

Updated on

2023-08-22

Licensed under