Solidity 基础语法:用五个小案例,把语法学成肌肉记忆

发布时间:2026/10/10 2:57:12
Solidity 基础语法:用五个小案例,把语法学成肌肉记忆 前两篇我们聊了学习路径和三个实战合约。但有个问题一直悬着很多人的语法是拼凑出来的不是理解出来的。他们能写mapping(address uint256)但说不清为什么不用数组能用modifier但不知道_到底插在哪能写payable但解释不了它和普通函数的本质区别。结果就是——遇到稍微复杂的需求语法就开始拼错。这一篇我用五个递进的小案例把 Solidity 的核心语法拆开揉碎。每个案例都聚焦一组语法写完你就知道为什么这么设计而不只是这么写能跑。案例一变量与类型——从能存到存得对// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; contract TypesDemo { // 值类型赋值时复制 uint256 public count 10; int256 public temperature -5; bool public isActive true; address public owner; bytes32 public hash; // 引用类型赋值时传引用 string public name Solidity; uint256[] public numbers; mapping(address uint256) public balances; // 常量与不可变量 uint256 public constant MAX_SUPPLY 1_000_000; uint256 public immutable deployTime; constructor() { owner msg.sender; deployTime block.timestamp; } function demo() external pure returns (uint256, uint256) { uint8 a 200; uint8 b 100; // uint8 c a b; // 会溢出报错 uint256 c uint256(a) uint256(b); // 显式转换 return (a, c); } }这段代码里藏着四个必须搞清楚的点。第一值类型 vs 引用类型。uint256、bool、address是值类型——赋值时复制一份。string、数组、mapping是引用类型——赋值时传的是位置。这个区别在函数传参和状态变量赋值时极其关键。把storage数组赋值给memory变量会发生一次完整拷贝Gas 消耗可能让你怀疑人生。第二constant和immutable的区别。constant在编译期就确定值必须写死在代码里。immutable在部署时确定可以在构造函数里赋值但之后不能改。两者都比普通状态变量省 Gas——因为它们的值被直接写进了字节码不占用 storage slot。MAX_SUPPLY用constantdeployTime用immutable这是标准用法。第三数字分隔符。1_000_000和1000000完全等价但前者不容易看错。写大额代币数量时这个习惯能救命。第四类型转换必须显式。uint8加uint8可能溢出到uint256Solidity 不让你隐式转换。uint256(a)这种写法是强制的虽然啰嗦但避免了大量隐蔽的 bug。注意demo函数标了pure——它不读也不写链上状态。pure和view的区别是view可以读状态变量pure连读都不行。编译器靠这个来优化你写错了它会直接报错。案例二函数与可见性——搞清楚谁能调// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; contract FunctionDemo { uint256 private secret 42; address public owner; constructor() { owner msg.sender; } // public: 内部外部都能调 function publicFunc() public pure returns (string memory) { return anyone; } // external: 只能外部调参数在 calldata 里更省 gas function externalFunc(uint256[] calldata data) external pure returns (uint256) { return data.length; } // internal: 只有本合约和子合约能调 function internalFunc() internal pure returns (uint256) { return 1; } // private: 只有本合约能调 function privateFunc() private pure returns (uint256) { return 2; } // view: 读状态但不改 function getSecret() external view returns (uint256) { return secret; } // pure: 不读也不改 function add(uint256 a, uint256 b) external pure returns (uint256) { return a b; } // payable: 可以接收 ETH function deposit() external payable { require(msg.value 0, Must send ETH); } function callInternal() external pure returns (uint256) { return internalFunc() privateFunc(); } }这里最容易被低估的是external和public的区别。external函数的参数如果标了calldata数据直接从交易输入读取不经过内存拷贝。public函数做不到这一点因为public函数可能被内部调用内部调用时参数必须在内存里。所以只被外部调用的函数一律用externalcalldata这是一个能省真金白银的习惯。internal和private的实际安全边界很有限。记住链上一切都是公开的。private变量别人照样能读private函数别人虽然不能直接调但如果合约逻辑有漏洞照样能间接利用。可见性修饰符是代码组织工具不是安全机制。payable的本质是允许这个函数接收 ETH。没有payable的函数别人给它转 ETH 会直接回滚。msg.value只在payable函数里有意义。写 DeFi 合约时几乎每个涉及资金的入口都必须是payable。一个细节callInternal是pure的因为它调用的两个函数都是pure。编译器会沿着调用链检查状态可变性一个pure函数如果调了view函数会直接编译失败。案例三控制流与错误处理——出错时怎么办// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; contract ControlFlowDemo { mapping(address uint256) public balances; address public owner; error Unauthorized(address caller); error InsufficientBalance(uint256 requested, uint256 available); error InvalidAmount(); constructor() { owner msg.sender; } modifier onlyOwner() { if (msg.sender ! owner) revert Unauthorized(msg.sender); _; } function withdraw(uint256 amount) external { // 方式一require 字符串旧写法 // require(amount 0, Amount must be positive); // 方式二custom error推荐 if (amount 0) revert InvalidAmount(); uint256 balance balances[msg.sender]; if (balance amount) { revert InsufficientBalance(amount, balance); } // 先改状态再转账防重入 balances[msg.sender] balance - amount; (bool success, ) msg.sender.call{value: amount}(); if (!success) revert InsufficientBalance(amount, balance); } function loopDemo(uint256 n) external pure returns (uint256) { uint256 sum 0; for (uint256 i 0; i n; i) { if (i 5) continue; // 跳过 if (i 100) break; // 提前退出 sum i; } return sum; } function ternaryDemo(uint256 x) external pure returns (string memory) { return x 0 ? positive : non-positive; } receive() external payable { balances[msg.sender] msg.value; } }这个案例的核心是错误处理因为它是 Solidity 里最容易写错的部分。require、revert、assert三者怎么选现在的标准答案很清晰assert只用于不可能发生的内部错误比如不变量被破坏它消耗所有 Gas。require和revert用于正常的错误检查。而自定义 error revert是 2026 年的首选——Gas 更低错误信息可以带参数前端能精确解析。.call{value: amount}()为什么不用.transfer().transfer()只转发 2300 Gas在 Gas 价格波动的环境下很容易失败。.call转发全部 Gas是现在的标准做法。但它也带来了重入风险所以必须先改状态再转账——这就是检查-生效-交互Checks-Effects-Interactions模式。receive和fallback的区别。receive只在收到纯 ETH 转账calldata 为空时触发。fallback在调用不存在的函数时触发。两个都是external payable但用途完全不同。写合约接收 ETH通常只需要receive。loopDemo里的continue和break是标准 C 风格。但要注意链上循环有 Gas 上限遍历一个无上限的数组是自找麻烦。这也是为什么很多合约用mapping 计数器而不是数组。案例四数据结构——struct、mapping、数组怎么配合// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; contract DataStructureDemo { struct User { string name; uint256 balance; uint256[] history; } mapping(address User) public users; address[] public userList; // 嵌套 mapping记录 A 是否授权 B mapping(address mapping(address bool)) public approvals; // 数组的三种形态 uint256[] public dynamicArray; uint256[3] public fixedArray; uint256[] public initializedArray [1, 2, 3]; function createUser(string calldata name) external { require(bytes(users[msg.sender].name).length 0, User exists); User storage u users[msg.sender]; u.name name; userList.push(msg.sender); } function addHistory(uint256 value) external { users[msg.sender].history.push(value); } function getHistory(address user) external view returns (uint256[] memory) { return users[user].history; } function deleteUser() external { delete users[msg.sender]; // 注意userList 里的地址不会被删掉需要额外处理 } function setApproval(address spender, bool status) external { approvals[msg.sender][spender] status; } }这个案例里最关键的一句话是delete不会把数组元素真正删掉。delete users[msg.sender]会把 struct 里的值重置为零值userList里那个地址依然在。这在很多合约里是 bug 的来源——你以为删干净了其实数组里还留着一个空壳地址。为什么用mapping而不是数组因为mapping的查找是 O(1)数组遍历是 O(n)。在链上O(n) 可能意味着几千美元的 Gas。凡是按键查找的场景都用mapping。只有需要枚举所有元素时才额外维护一个数组。struct 里能不能放 mapping可以但这样的 struct 不能作为memory变量使用只能作为storage。因为mapping没有内存布局的概念。fixedArray的长度是编译期固定的。uint256[3]这种写法在需要固定长度的场景比如存储三个坐标时有用但它不能push。dynamicArray才能push和pop。案例五事件与继承——合约的对外接口和代码复用// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; contract Base { event BaseEvent(address indexed sender, uint256 value); function baseFunc() public virtual pure returns (string memory) { return base; } } contract Derived is Base { event DerivedEvent(address indexed sender); // 重写父类函数必须标 override function baseFunc() public pure override returns (string memory) { return derived; } // 调用父类实现 function callParent() external pure returns (string memory) { return super.baseFunc(); } function emitEvents(uint256 value) external { emit BaseEvent(msg.sender, value); emit DerivedEvent(msg.sender); } } abstract contract AbstractBase { function mustImplement() public virtual returns (uint256); } contract Implementer is AbstractBase { function mustImplement() public pure override returns (uint256) { return 42; } }indexed关键字是事件里最重要的细节。一个事件最多有三个indexed参数。被标记的参数可以被链下高效过滤——比如你想查某个地址的所有转账记录如果from是indexed索引器可以直接筛出来如果不是就得扫描所有事件再过滤慢得没法用。virtual和override是显式的继承契约。父类函数想被重写必须标virtual。子类重写必须标override。如果父类有多个写override(A, B)。这种必须明说的设计避免了 Java 那种不小心覆盖了父类方法的坑。super.baseFunc()调用的是父类实现。在多重继承中super会按 C3 线性化顺序调用这个顺序有时候反直觉写复杂继承时要小心。抽象合约abstract不能部署它只提供接口。接口interface更极端——所有函数都是external不能有状态变量不能有构造函数。写和其他合约交互的代码时定义 interface 是最常见的做法。语法学完之后怎么检验自己真的会了给你一个自测题写一个合约管理一个待办事项列表支持添加、完成、删除、查询每个待办有创建时间戳和完成状态只有创建者能操作自己的待办。如果你能不用查资料15 分钟内写出来并且逻辑正确说明这一篇的语法你已经内化了。如果卡在某个地方——比如不知道怎么存多个待办、不知道怎么判断是否是创建者、不知道怎么返回整个列表——那就回到对应的案例再读一遍。语法的真正掌握不在于看得懂而在于写得对。下一篇我们会进入 Ethernaut用攻击者的视角来检验这些语法到底学扎实没有。那些关卡会逼着你发现external用错会怎样、delegatecall有多危险、随机数为什么不能上链。语法是工具安全是目的——两者必须一起学。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询