上链ON-CHAIN BLOCK 09 / 26
卷三 · 合约工程BLOCK 09

Solidity 语言核心


Solidity 是静态类型、编译型、面向合约的语言。对写过 Kotlin 的你,它的语法门槛很低——真正要重新建立的是直觉:这里的每一行代码都在花钱,每一个 public 函数都是全世界的攻击面。这一章逐项对照 Kotlin,带你快速吃透语言核心,并标出那些"看着像 Kotlin、行为却不一样"的陷阱。

类型:定长、无浮点、会溢出回退

三个和 Kotlin 最不同的地方,先记住:

SolidityKotlin 对应注意
uint256 / int256约等于 BigInteger,但定长默认用 256 位最省 gas
boolBoolean
address无直接对应(20 字节地址)address payable 才能收 ETH
bytes32 / bytesByteArray定长 bytes32 比动态 bytes 省 gas
stringStringUTF-8,链上存字符串很贵
mapping(K => V)Map<K,V>不可遍历、无 size、无法删整个 map
T[] / T[n]List / Array动态数组操作贵,谨慎循环
structdata class纯数据聚合
enumenum class底层就是 uint8
风险记账

mapping 不是 HashMap 的等价物,而更像"一个无限大、默认全 0 的稀疏数组"。它不能遍历、拿不到长度、不能一次清空。想"遍历所有持有者"?你得自己额外维护一个地址数组。很多新手把 mapping 当 Kotlin 的 Map 用,结果发现没法列出所有 key——这是设计使然,因为遍历海量存储会 gas 爆炸。

函数:可见性 + 状态可变性 两个维度

Solidity · 函数签名剖析
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 的招牌特性,把重复的前置/后置检查抽出来。最常见的是权限校验:

Solidity · modifier
address public owner;

modifier onlyOwner() {
    require(msg.sender == owner, "not owner");
    _;   // 占位符:被修饰函数的函数体在这里执行
}

function withdraw() public onlyOwner {  // 挂上修饰器
    // 只有 owner 能走到这里
}
Android 类比

modifier 的心智模型接近拦截器 / 注解 + 切面:像给某些方法加一个 @RequiresPermission,进方法前先跑一段校验逻辑。那个 _; 就是"在此处调用原方法体"。msg.sender 是调用者地址,相当于"当前请求是谁发起的",是链上权限判断的基石。

错误处理:require / revert / custom error

Solidity 没有 try/catch 那套(调外部合约时有,但语义不同)。核心是三件套,共同点是:一旦触发,整笔交易的所有状态改动全部回滚,像事务一样原子。

Solidity · 三种回退
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。

Android 类比

把 revert 想成一个"自动全量回滚的事务"。它比 Kotlin 抛异常更彻底:异常你还能 catch 后让部分副作用留下,而合约 revert 会把这笔交易里已经改过的每一格状态都恢复原样,仿佛从没执行过(gas 照扣)。这个"要么全成功要么全回滚"的原子性,是链上安全的重要护栏。

继承与接口

Solidity 支持多继承(用 C3 线性化解决菱形问题),以及接口 interface。你会大量继承 OpenZeppelin 的标准实现:

Solidity · 继承标准库
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 和读取"私有"变量的关键。

本章小结

◆ 返回目录