Solidity 语言核心
Solidity 是静态类型、编译型、面向合约的语言。对写过 Kotlin 的你,它的语法门槛很低——真正要重新建立的是直觉:这里的每一行代码都在花钱,每一个 public 函数都是全世界的攻击面。这一章逐项对照 Kotlin,带你快速吃透语言核心,并标出那些"看着像 Kotlin、行为却不一样"的陷阱。
类型:定长、无浮点、会溢出回退
三个和 Kotlin 最不同的地方,先记住:
- 整数是定长的:
uint256(默认)、uint8…int256。uint256是 EVM 的原生字长,最省 gas。 - 没有浮点数。金融系统里浮点误差是灾难,所以链上用整数 + 固定小数位表示(如代币用 18 位小数,1 个币 =
10^18个最小单位)。 - 0.8 起溢出自动 revert:
uint8的 255+1 不再像 C 那样绕回 0,而是整笔交易回退。要绕回得显式写unchecked { }。
| Solidity | Kotlin 对应 | 注意 |
|---|---|---|
uint256 / int256 | 约等于 BigInteger,但定长 | 默认用 256 位最省 gas |
bool | Boolean | — |
address | 无直接对应(20 字节地址) | address payable 才能收 ETH |
bytes32 / bytes | ByteArray | 定长 bytes32 比动态 bytes 省 gas |
string | String | UTF-8,链上存字符串很贵 |
mapping(K => V) | Map<K,V> | 不可遍历、无 size、无法删整个 map |
T[] / T[n] | List / Array | 动态数组操作贵,谨慎循环 |
struct | data class | 纯数据聚合 |
enum | enum class | 底层就是 uint8 |
mapping 不是 HashMap 的等价物,而更像"一个无限大、默认全 0 的稀疏数组"。它不能遍历、拿不到长度、不能一次清空。想"遍历所有持有者"?你得自己额外维护一个地址数组。很多新手把 mapping 当 Kotlin 的 Map 用,结果发现没法列出所有 key——这是设计使然,因为遍历海量存储会 gas 爆炸。
函数:可见性 + 状态可变性 两个维度
function transfer(address to, uint256 amt) public // 可见性:谁能调 returns (bool) { … } // 返回值 function balanceOf(address a) public view // view:只读状态,call 免费 returns (uint256) { … } function add(uint256 a, uint256 b) public pure // pure:连状态都不读 returns (uint256) { return a + b; }
| 可见性 | 谁能调 | Kotlin 类比 |
|---|---|---|
public | 内部 + 外部 + 其他合约 | public |
external | 只能从外部调(省 gas) | 类似只暴露给外部的 API |
internal | 本合约 + 继承的子合约 | protected |
private | 仅本合约 | private |
private 只是访问控制,不是保密!链上所有数据都是公开可读的——即使一个变量标了 private,任何人都能直接读它的 storage slot 拿到值(第 10 章你会看到怎么读)。永远不要把密码、私钥、随机种子存进合约还以为 private 能藏住。这是新手最危险的误解之一。
修饰器:可复用的前置检查
modifier 是 Solidity 的招牌特性,把重复的前置/后置检查抽出来。最常见的是权限校验:
address public owner; modifier onlyOwner() { require(msg.sender == owner, "not owner"); _; // 占位符:被修饰函数的函数体在这里执行 } function withdraw() public onlyOwner { // 挂上修饰器 // 只有 owner 能走到这里 }
modifier 的心智模型接近拦截器 / 注解 + 切面:像给某些方法加一个 @RequiresPermission,进方法前先跑一段校验逻辑。那个 _; 就是"在此处调用原方法体"。msg.sender 是调用者地址,相当于"当前请求是谁发起的",是链上权限判断的基石。
错误处理:require / revert / custom error
Solidity 没有 try/catch 那套(调外部合约时有,但语义不同)。核心是三件套,共同点是:一旦触发,整笔交易的所有状态改动全部回滚,像事务一样原子。
require(amount > 0, "amount must be positive"); // 校验输入,最常用 if (balance[msg.sender] < amount) revert InsufficientBalance(amount, balance[msg.sender]); // 自定义错误,省 gas 且带数据 assert(totalSupply == sumOfBalances); // 检查"绝不该发生"的不变量,用于 bug 兜底
推荐用 custom error(error InsufficientBalance(uint256 need, uint256 have);):比字符串 require 更省 gas,还能携带结构化数据供前端解析。assert 只用于"逻辑上绝不可能为假"的不变量检查,触发它意味着代码有 bug。
把 revert 想成一个"自动全量回滚的事务"。它比 Kotlin 抛异常更彻底:异常你还能 catch 后让部分副作用留下,而合约 revert 会把这笔交易里已经改过的每一格状态都恢复原样,仿佛从没执行过(gas 照扣)。这个"要么全成功要么全回滚"的原子性,是链上安全的重要护栏。
继承与接口
Solidity 支持多继承(用 C3 线性化解决菱形问题),以及接口 interface。你会大量继承 OpenZeppelin 的标准实现:
import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; contract MyToken is ERC20, Ownable { constructor() ERC20("MyToken", "MTK") Ownable(msg.sender) {} function mint(address to, uint256 amt) external onlyOwner { _mint(to, amt); // 复用父合约逻辑 } }
Solidity 语法你两小时就能读懂,但四个直觉必须重装:①整数定长、无浮点、溢出回退;②private 不保密,链上皆公开;③mapping 不可遍历;④任何回退都是全交易原子回滚。把这几条刻进肌肉记忆,能帮你躲开一大半新手 bug。下一章我们钻到语言之下,看这些变量到底存在哪个 storage slot——那是省 gas 和读取"私有"变量的关键。
本章小结
- 类型定长、无浮点、0.8 起溢出自动 revert;金额用整数 + 固定小数位表示。
- 函数有可见性(public/external/internal/private)和状态可变性(view/pure)两个维度。
- modifier 是可复用前置检查,onlyOwner + msg.sender 是权限控制基石。
- require/revert/custom error 触发即全交易原子回滚;private 只是访问控制,不是保密。