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有多危险、随机数为什么不能上链。语法是工具安全是目的——两者必须一起学。

相关新闻

同城跑腿系统:骑手端同步和下单收款怎么拆

同城跑腿系统:骑手端同步和下单收款怎么拆

同城跑腿系统联调时,常见做法是支付一通就对外宣称上线。更稳的做法是把「下单与订单状态」和「收款回调」拆阶段验收:前者不依赖真实通道,后者用沙箱 profile,避免支付未过却改订单写入口。结论 订单状态推进应由领域事件驱动&am…

2026/10/10 2:56:07 阅读更多 →
TwinCAT3.1.4026 C++使用入门

TwinCAT3.1.4026 C++使用入门

TwinCAT C使用入门1、准备工作1.1 软件环境1.2 硬件环境2、安装流程2.1 Visual Studio安装2.2 TwinCAT 安装3、配置签名3.1 TwinCAT证书制作3.2 Windows开启测试模式4、C项目设置4.1 项目编译4.2 模块加载执行1、准备工作 1.1 软件环境 TwinCAT Package Manager; …

2026/10/10 2:56:07 阅读更多 →
LikeShop 购物车模块深度优化:登录态合并、商品失效与批量结算二开

LikeShop 购物车模块深度优化:登录态合并、商品失效与批量结算二开

一、原生购物车体系数据表拆解1.1 购物车表(ls_cart)LikeShop 的购物车数据存储在 ls_cart 表中,表结构简洁,核心字段如下:字段名数据类型注释idint主键user_idint用户idgoods_idint商品idgoods_numsmallint购买数量it…

2026/10/10 2:56:07 阅读更多 →

最新新闻

MATLAB联合CST建模:超表面仿真自动化工作流实战

MATLAB联合CST建模:超表面仿真自动化工作流实战

最近不少做超表面的同学都在折腾CST仿真,尤其是想把MATLAB联合CST建模这条路彻底走通,用来处理超透镜、轨道角动量、吸收器、极化转换器、EIT(类电磁诱导透明)这些常见方向。这篇文章不打算讲教科书推导,只写我在真实仿…

2026/10/10 3:48:27 阅读更多 →
PostgreSQL权限管理实战:角色体系、层级授权与行级安全

PostgreSQL权限管理实战:角色体系、层级授权与行级安全

1. 权限分配这件事,先搞懂 PostgreSQL 的角色体系做数据库运维这些年,我发现一个特别有意思的现象:很多人对 PostgreSQL 的权限管理第一反应是“这不就是 grant 一下嘛”,可真到了线上环境,经常被各种“没权限”“权限…

2026/10/10 3:48:26 阅读更多 →
1996-2024年各省农业总产值无缺失面板数据:从处理到分析完整指南

1996-2024年各省农业总产值无缺失面板数据:从处理到分析完整指南

做农业数据分析的人应该都有过这种经历:想研究各省农业生产的长期变化,打开官方数据库发现要么年份对不上,要么某些省份某几年突然缺了一块,要么当年价格和可比价格混在一起,搞不清谁是谁。最后大量时间花在找数据、拼…

2026/10/10 3:48:26 阅读更多 →
SCSI磁盘实战指南:从协议原理到Linux诊断调优

SCSI磁盘实战指南:从协议原理到Linux诊断调优

1. 项目概述:为什么“SCSI磁盘”这个老词还在工程师的日常对话里反复出现你可能在服务器机房巡检时听到运维同事说“这台存储柜挂了两块SCSI盘,热备没切过去”,也可能在旧系统迁移文档里看到“需兼容SCSI-3 SPI协议的磁盘阵列”,甚…

2026/10/10 3:48:26 阅读更多 →
PostgreSQL自定义函数规范:从命名到性能排查的完整指南

PostgreSQL自定义函数规范:从命名到性能排查的完整指南

1. 为什么自定义函数必须讲规范1.1 从一次线上事故说起先说一个我亲眼见过的教训。某公司的订单系统,早期为了赶业务进度,开发人员在 PostgreSQL 里写自定义函数时完全放飞自我——函数名有的叫get_data、有的叫f_order,参数类型混用 varchar…

2026/10/10 3:48:26 阅读更多 →
PyTorch+LSTM电影评论情感分析实战:从预处理到模型部署

PyTorch+LSTM电影评论情感分析实战:从预处理到模型部署

简介:一份评审分达99分的基于深度学习的电影评论情感分析项目资源包,适合计算机相关专业课程设计、期末大作业及入门实战,重点解决从数据爬取到模型训练演示中缺完整代码、缺数据集、缺文档的常见问题。资源围绕豆瓣短评设计,约5万…

2026/10/10 3:47:26 阅读更多 →

日新闻

卫星轨道分类全解析:从LEO到GEO的选型逻辑与工程实践

卫星轨道分类全解析:从LEO到GEO的选型逻辑与工程实践

1. 从“卫星轨道分类”这个标题说起:为什么值得花时间搞懂第一次接触“卫星轨道分类”这个概念,很多人会觉得它离自己很远——不就是天上的星星怎么转吗?但如果你正在做航天任务规划、遥感数据接收、星座设计,甚至只是准备一场航天…

2026/10/10 0:00:39 阅读更多 →
Spring AOP 核心原理与实战:从概念到日志切面落地

Spring AOP 核心原理与实战:从概念到日志切面落地

1. 从一个真实痛点说起:为什么你的代码里到处都是重复逻辑刚入行那会儿,我写过一个用户管理模块,注册、登录、改密码、注销四个接口。每个接口里都塞了几乎一样的日志打印、参数校验、事务开启和提交。当时觉得没什么,能跑就行。直…

2026/10/10 0:00:40 阅读更多 →
Python招聘数据采集与分析可视化:从采集清洗到薪资技能城市可视化全链路

Python招聘数据采集与分析可视化:从采集清洗到薪资技能城市可视化全链路

简介:这是一套面向计算机相关专业学生与项目实战学习者的Python数据采集与分析可视化完整项目,以Boss直聘岗位数据为对象,适合用作毕业设计、课程设计或期末大作业。资源包共38个文件,约246KB,以13个py源码文件为核心&…

2026/10/10 0:00:40 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:32 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 1:36:08 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 10:11:06 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 21:13:17 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 21:32:20 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 6:17:20 阅读更多 →