Web3学习笔记:Day2-Solidity基础语法

📅 发布时间:2026/7/17 7:55:01 👁️ 浏览次数:
Web3学习笔记:Day2-Solidity基础语法
学习目标• 理解智能合约的概念和 EVM 工作原理• 掌握 Solidity 语言的基本语法• 学习 Solidity 的数据类型和变量• 理解函数的可见性和修饰符• 使用 Remix IDE 编写第一个智能合约• 在区块链上部署并测试合约 理论部分 (40分钟)2.1 智能合约简介什么是智能合约智能合约是部署在区块链上的程序代码它自动执行预定义的规则和条款。智能合约的特点1.不可变性 (Immutable)• 一旦部署代码无法修改• 这是区块链安全的基石• 部署前必须充分测试2.自动执行• 满足条件时自动触发• 无需第三方介入• 消除了人为错误和欺诈3.确定性• 给定相同的输入必定产生相同的输出• 在所有节点上执行结果一致• 这是共识机制的基础4.透明性• 合约代码公开可见• 任何人都可以审计• 任何人都可以调用EVM以太坊虚拟机什么是 EVMEVM 是执行 Solidity 字节码的运行环境类似于 Java 虚拟机 (JVM)。EVM 的特点特性说明图灵完备可以执行任何计算逻辑隔离环境每个合约在独立沙箱中运行Gas 限制每个操作都有计算成本无状态不保存状态每次执行从头开始确定性执行结果可预测EVM 执行流程1. 接收交易 ↓ 2. 验证 Gas 费用 ↓ 3. 加载合约代码 ↓ 4. 执行字节码 ↓ 5. 更新状态 ↓ 6. 返回结果 ↓ 7. 消耗 Gas为什么需要 Gas• 防止无限循环攻击• 为计算资源定价• 激励矿工/验证者打包交易• 防止网络滥用2.2 Solidity 语言特点Solidity 简介Solidity 是面向智能合约的高级编程语言受 C、Python 和 JavaScript 影响。Solidity 的核心特性特性说明类比静态类型编译时检查类型错误Java、C面向合约以合约为核心组织代码面向对象编程Gas 敏感每行代码都有计算成本按量计费继承支持支持多重继承Java、Python事件驱动通过 event 记录日志JavaScript 事件异常处理require/revert/asserttry-catchSolidity vs JavaScript特性JavaScriptSolidity类型动态类型静态类型执行环境浏览器/Node.jsEVM状态存储内存/硬盘区块链状态代码可变性可随时修改部署后不可变执行成本免费需要支付 Gas并发模型单线程异步单线程同步Solidity 的独特概念•Gas: 每个操作都有成本•Storage: 永久存储成本高•Memory: 临时存储成本低•Calldata: 只读数据成本最低•Events: 日志记录不存储在状态中2.3 基础语法详解数据类型值类型 (Value Types)值类型在赋值时会创建副本。// 布尔类型 bool isTrue true; bool isFalse false; // 整数类型 uint256 amount 100; // 无符号整数 (uint uint256) uint8 smallNumber 255; // 8位无符号整数 (0-255) int256 temperature -10; // 有符号整数 int32 shortInt -1000; // 32位有符号整数 // 地址类型 address user 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb; address payable recipient 0x123...; // 可接收 ETH 的地址 // 字节类型 bytes32 data hello; // 固定大小字节数组 bytes1 singleByte 0x01; // 单字节 // 枚举类型 enum Status { Active, Inactive, Pending } Status currentStatus Status.Active;整数类型详解// 无符号整数 uint8 // 8位, 范围: 0 到 2^8 - 1 uint16 // 16位 uint32 // 32位 uint64 // 64位 uint128 // 128位 uint256 // 256位 (默认) // 有符号整数 int8 // 8位, 范围: -2^7 到 2^7 - 1 int16 // 16位 int32 // 32位 int64 // 64位 int128 // 128位 int256 // 256位 (默认)引用类型 (Reference Types)引用类型在赋值时不会创建副本而是引用同一数据。// 字符串 string name Alice; string description This is a long description...; // 动态字节数组 bytes dynamicData hex01a2b3c4; // 数组 uint[] numbers; // 动态数组 uint[10] fixedNumbers; // 固定大小数组 string[] names [Alice, Bob]; // 初始化数组 // 映射 (类似哈希表/字典) mapping(address uint256) balances; // 地址 → 余额 mapping(address mapping(address uint256)) allowances; // 嵌套映射 // 结构体 struct User { string name; uint256 age; address wallet; } User user User(Alice, 25, 0x123...);映射详解// 定义映射 mapping(address uint256) public balances; // 设置值 balances[0x123...] 100; // 读取值 uint256 balance balances[0x123...]; // 检查是否存在 (需要额外的映射) mapping(address bool) public hasBalance; hasBalance[0x123...] true;变量作用域contract Example { // 状态变量 - 永久存储在区块链上 uint256 public globalVar; string public name; // 常量 - 编译时确定不占用存储空间 uint256 constant MAX_SUPPLY 1000000; // 不可变变量 - 构造时设置之后不可变 address immutable owner; constructor() { owner msg.sender; } function example() public { // 局部变量 - 函数执行完销毁 uint256 localVar 10; string memory tempStr hello; // memory - 临时存储函数执行完释放 string memory tempName Alice; // storage - 永久存储修改状态变量 globalVar 20; name tempName; // calldata - 只读不能修改 function(string calldata) external { // _input 是 calldata只能读取 } } }存储位置对比存储位置持久性Gas 成本可修改性storage永久高可读可写memory临时低可读可写calldata临时最低只读函数可见性contract Example { // public - 任何人都可以调用 function publicFunc() public returns (uint256) { return 1; } // external - 只能从外部调用内部调用需要 this.externalFunc() function externalFunc() external returns (uint256) { return 2; } // internal - 只能在合约内部和继承合约中调用 function internalFunc() internal returns (uint256) { return 3; } // private - 只能在合约内部调用 function privateFunc() private returns (uint256) { return 4; } function test() public { // 可以调用 uint256 a publicFunc(); uint256 b internalFunc(); uint256 c privateFunc(); // 不能直接调用 external 函数 // uint256 d externalFunc(); // 错误 // 需要通过 this 调用 uint256 d this.externalFunc(); } }可见性对照表修饰符合约内部继承合约外部调用public✅✅✅external❌❌✅internal✅✅❌private✅❌❌函数修饰符状态修改修饰符// view - 只读不修改状态 function getBalance() public view returns (uint256) { return balances[msg.sender]; } // pure - 不读取也不修改状态 function add(uint a, uint b) public pure returns (uint) { return a b; } // payable - 可以接收 ETH function deposit() public payable { balances[msg.sender] msg.value; }修饰符对比修饰符读取状态修改状态接收 ETHGas 成本view✅❌❌低pure❌❌❌最低payable✅✅✅取决于操作(无)✅✅❌取决于操作何时使用 view/pure// ✅ 使用 view - 只读取状态变量 function getTotalSupply() public view returns (uint256) { return totalSupply; } // ✅ 使用 pure - 纯计算不涉及状态 function calculate(uint256 a, uint256 b) public pure returns (uint256) { return a * b / 2; } // ❌ 不要使用 view - 修改了状态 function updateBalance() public view { // 错误 balances[msg.sender] 100; } // ❌ 不要使用 pure - 读取了状态变量 function getBalance() public pure returns (uint256) { // 错误 return balances[msg.sender]; }特殊变量contract SpecialVariables { // msg - 消息对象 function example() public payable { address sender msg.sender; // 调用者地址 uint256 value msg.value; // 发送的 ETH 数量wei bytes calldata data msg.data; // 完整的调用数据 uint256 gas msg.gas; // 剩余 gas // block - 区块信息 uint256 blockNumber block.number; // 当前区块号 uint256 timestamp block.timestamp; // 当前区块时间戳 uint256 chainId block.chainid; // 当前链 ID address coinbase block.coinbase; // 矿工地址 // tx - 交易信息 address origin tx.origin; // 交易发起者地址 uint256 gasPrice tx.gasprice; // Gas 价格 } // 使用示例 function deposit() public payable { require(msg.value 0, Must send ETH); balances[msg.sender] msg.value; } function getBlockInfo() public view returns (uint256, uint256) { return (block.number, block.timestamp); } }msg.sender vs tx.origincontract A { function callB() public { B(address(0x123...)).call(); } } contract B { function call() public { address sender msg.sender; // 合约 A 的地址 address origin tx.origin; // 用户的地址 } }⚠️ 安全警告不要使用tx.origin进行身份验证容易被钓鱼攻击️ 实操部分 (60分钟)步骤 1: 使用 Remix IDE (10分钟)什么是 Remix IDERemix 是基于浏览器的 Solidity 开发环境无需安装开箱即用。访问 Remix1.打开 Remix• 访问https://remix.ethereum.org/2.了解界面布局不得不说第一眼看Remix的主界面内容非常多也很凌乱。3.创建工作区• 找到正中间的Home页面• 选择 Create a new workspace• 选择 Blank 模板• 点击 Create步骤 2: 编写第一个合约 (20分钟)创建合约文件1.新建文件• 在左侧文件管理器右键• 选择 New File• 命名为HelloWeb3.sol2.编写合约代码// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; /** * title HelloWeb3 * dev 第一个智能合约示例 * author Your Name */ contract HelloWeb3 { // 状态变量 string public message; address public owner; uint256 public changeCount; // 事件 event MessageChanged( string oldMessage, string newMessage, address changedBy, uint256 timestamp ); event DepositReceived( address indexed from, uint256 amount, uint256 timestamp ); // 构造函数 constructor() { owner msg.sender; message Hello Web3!; changeCount 0; emit MessageChanged( , Hello Web3!, msg.sender, block.timestamp ); } // 修饰符 modifier onlyOwner() { require(msg.sender owner, Only owner can call this); _; } // 读函数 /** * dev 获取完整信息 */ function getInfo() public view returns ( string memory, address, uint256 ) { return (message, owner, changeCount); } /** * dev 获取合约余额 */ function getBalance() public view returns (uint256) { return address(this).balance; } // 写函数 /** * dev 修改消息 * param _newMessage 新的消息 */ function setMessage(string memory _newMessage) public onlyOwner { string memory oldMessage message; message _newMessage; changeCount; emit MessageChanged( oldMessage, _newMessage, msg.sender, block.timestamp ); } /** * dev 增加计数器 */ function increment() public { changeCount; } // 余额函数 /** * dev 接收 ETH 的函数 */ receive() external payable { emit DepositReceived(msg.sender, msg.value, block.timestamp); } /** * dev 兜底函数 */ fallback() external payable {} /** * dev 提取合约中的 ETH */ function withdraw() public onlyOwner { uint256 balance address(this).balance; require(balance 0, No balance to withdraw); payable(owner).transfer(balance); } /** * dev 销毁合约 */ function destroy() public onlyOwner { selfdestruct(payable(owner)); } }代码解析1. SPDX 许可标识// SPDX-License-Identifier: MIT• 告诉编译器代码使用的开源许可证• MIT 是最宽松的许可证• 必须放在第一行2. Pragma 声明pragma solidity ^0.8.20;• 指定 Solidity 编译器版本•^0.8.20表示兼容 0.8.20 及以上但不包括 0.9.0•0.8.20 0.9.0是另一种写法3. 注释/// dev 开发者注释 /// param 参数说明 /// return 返回值说明 /// notice 用户可见的注释4. 事件event MessageChanged(...);• 记录日志不存储在状态中• 降低 Gas 成本• 便于前端监听5. 修饰符modifier onlyOwner() { require(msg.sender owner, Only owner can call this); _; // 函数代码插入的位置 }步骤 3: 编译合约 (5分钟)编译步骤1.打开编译器• 点击左侧 Solidity Compiler 图标• 或使用快捷键CtrlS/CmdS2.选择编译器版本• 在 Compiler 下拉菜单中选择0.8.20• 确保与 Pragma 声明匹配3.编译合约• 点击 Compile HelloWeb3.sol 按钮• 等待编译完成4.检查结果• ✅ 绿色勾号编译成功• ❌ 红色叉号编译失败查看错误信息我们看到编译的时候生成几个json文件可以点进去看看。常见编译错误错误原因解决方法Expected identifier语法错误检查代码语法Type error类型不匹配检查变量类型Undeclared identifier变量未声明检查变量名拼写Function overload clash函数重载冲突修改函数签名步骤 4: 部署和测试 (25分钟)部署合约1.打开部署面板• 点击左侧 Deploy Run Transactions 图标2.选择环境• 在 Environment 下拉菜单中选择 Remix VM (Cancun)• 这是 Remix 内置的本地虚拟机• 无需连接钱包测试使用3.部署合约• 在 Deploy 按钮旁确认选中了HelloWeb3合约• 点击 Deploy 按钮• 等待部署完成4.查看部署结果• 在下方 Deployed Contracts 下找到HelloWeb3• 点击展开合约可以看到所有公开的函数和变量测试合约1. 查看初始值# 点击 message 按钮 # 预期输出: Hello Web3! # 点击 owner 按钮 # 预期输出: 你的部署地址 # 点击 changeCount 按钮 # 预期输出: 02. 测试 setMessage 函数# 在 setMessage 输入框输入新消息 # 示例: Welcome to Web3! # 点击按钮调用函数 # 查看控制台输出 # 应该看到事件日志 # 再次点击 message 按钮 # 预期输出: Welcome to Web3!3. 测试访问控制# 切换到另一个账户 # 点击 Remix VM 旁的账户下拉菜单 # 选择不同的账户 # 尝试调用 setMessage # 预期: 交易失败显示 Only owner can call this4. 测试 ETH 接收通过 receive 函数合约中定义了receive()函数可以直接向合约地址转账# 步骤 1: 在 Deploy 面板找到 Value 输入框 # 输入金额: 5选择单位: ether # 步骤 2: 在 Deployed Contracts 中展开合约 # 找到最下方的合约地址按钮低地址按钮 # 步骤 3: 点击该地址按钮触发 receive() 函数 # 步骤 4: 点击 getBalance 按钮查看余额 # 预期输出: 5000000000000000000 (5 ETH in wei) # 步骤 5: 查看控制台的事件日志 # 应该看到 DepositReceived 事件 # 步骤 6: 查看余额 # 应该看到少了5ETH少了5ETH。5. 测试提取 ETH# 切换回 owner 账户 # 点击 withdraw 按钮 # 等待交易完成 # 点击 getBalance 按钮 # 预期输出: 0 # 查看当前账户余额 # 应该增加了 5 ETH在94.99的基础上又增加了5ETH6. 测试 getInfo 函数# 点击 getInfo 按钮 # 预期输出: (Welcome to Web3!, owner地址, 1)查看事件日志1.打开调试面板• 点击左侧 Debug 图标2.查看事件• 点击控制台中的交易• 展开 logs 部分• 查看事件参数拷贝“transaction hash”中的内容到DEBUGGER中就可以看到debug信息。了解不同的部署环境Remix VM• 本地虚拟机• 快速免费• 适合开发测试Injected Provider - MetaMask• 连接真实的 MetaMask 钱包• 可以部署到测试网• 需要支付 GasWeb3 Provider• 连接到外部节点• 需要提供 RPC URL今天我人只测试和体验了Remix VM后续有时间我们再体验Injected Provider和Web3 Provider。✅ 今日产出检查清单理论知识• 理解智能合约的概念和特点• 了解 EVM 的工作原理• 掌握 Solidity 语言的特点• 理解数据类型值类型、引用类型• 掌握函数的可见性修饰符• 理解存储位置storage、memory、calldata实操能力• 熟悉 Remix IDE 的界面和功能• 成功创建和编译智能合约• 在 Remix VM 中部署合约• 测试合约的各种功能• 理解事件的用途• 掌握访问控制的使用代码能力• 能够定义状态变量• 能够编写构造函数• 能够创建和使用修饰符• 能够定义和触发事件• 能够处理 ETH 接收和发送• 能够实现访问控制 常见问题Q1: 什么是 weiA:wei 是以太币的最小单位。1 ETH 10^18 wei。这是为了避免浮点数精度问题。1 ETH 1,000,000,000,000,000,000 wei 1 Gwei 1,000,000,000 wei (10^9) 1 Ether 10^18 weiQ2: view 和 pure 函数不需要 Gas 吗A:不完全正确。如果外部调用 view/pure 函数不需要支付 Gas。但如果在合约内部调用仍然需要 Gas。Q3: 为什么需要 payable 修饰符A:只有带有 payable 修饰符的函数才能接收 ETH。这是为了防止意外转账和潜在的安全问题。Q4: 事件和状态变量有什么区别A:•事件: 记录日志不存储在状态中成本低用于前端监听•状态变量: 永久存储成本高可以随时读取Q5: 如何选择存储位置A:•storage: 需要永久保存的数据•memory: 函数内部临时使用的数据•calldata: 只读的输入参数 课后作业选做1.扩展合约功能• 添加一个计数器记录合约被调用的总次数• 添加一个函数可以设置最大修改次数• 实现一个简单的投票机制2.测试不同数据类型• 创建一个使用 mapping 的函数• 测试数组的添加和删除• 尝试使用 struct3.优化 Gas 成本• 比较 uint256 和 uint8 的 Gas 差异• 测试使用 calldata vs memory 的区别• 使用事件替代部分状态变量4.记录学习笔记• 记录今天学到的所有语法• 记录遇到的错误和解决方法• 总结最佳实践 明天预告明天我们将深入学习智能合约的进阶功能• 学习继承和多重继承• 掌握错误处理require、revert、assert• 理解 ERC-20 代币标准• 实现一个完整的 ERC-20 代币合约• 学习智能合约的安全最佳实践预习建议• 阅读关于面向对象编程的基础知识• 了解代币的概念和经济模型• 研究一些知名的 ERC-20 代币学习时长约 100 分钟难度⭐⭐☆☆☆下次学习Day 3 - 智能合约进阶与 ERC-20 代币详细参见 春节7天Web3学习计划我决定用假期给自己充值祝你学习顺利