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

📋 学习目标

  • • 理解智能合约的概念和 EVM 工作原理

  • • 掌握 Solidity 语言的基本语法

  • • 学习 Solidity 的数据类型和变量

  • • 理解函数的可见性和修饰符

  • • 使用 Remix IDE 编写第一个智能合约

  • • 在区块链上部署并测试合约


📚 理论部分 (40分钟)

2.1 智能合约简介

什么是智能合约?

智能合约是部署在区块链上的程序代码,它自动执行预定义的规则和条款。

智能合约的特点:

    1. 不可变性 (Immutable)
    • • 一旦部署,代码无法修改

    • • 这是区块链安全的基石

    • • 部署前必须充分测试

    1. 自动执行
    • • 满足条件时自动触发

    • • 无需第三方介入

    • • 消除了人为错误和欺诈

    1. 确定性
    • • 给定相同的输入,必定产生相同的输出

    • • 在所有节点上执行结果一致

    • • 这是共识机制的基础

    1. 透明性
    • • 合约代码公开可见

    • • 任何人都可以审计

    • • 任何人都可以调用

EVM(以太坊虚拟机)

什么是 EVM?

EVM 是执行 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/assert try-catch
Solidity vs JavaScript
特性 JavaScript Solidity
类型 动态类型 静态类型
执行环境 浏览器/Node.js EVM
状态存储 内存/硬盘 区块链状态
代码可变性 可随时修改 部署后不可变
执行成本 免费 需要支付 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 = hex"01a2b3c4";

// 数组
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;
}

修饰符对比:

修饰符 读取状态 修改状态 接收 ETH Gas 成本
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.origin:

复制代码
contract 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 IDE?

Remix 是基于浏览器的 Solidity 开发环境,无需安装,开箱即用。

访问 Remix
    1. 打开 Remix
    1. 了解界面布局

不得不说,第一眼看Remix的主界面,内容非常多,也很凌乱。

    1. 创建工作区
    • • 找到正中间的Home页面

    • • 选择 "Create a new workspace"

    • • 选择 "Blank" 模板

    • • 点击 "Create"


步骤 2: 编写第一个合约 (20分钟)

创建合约文件
    1. 新建文件
    • • 在左侧文件管理器右键

    • • 选择 "New File"

    • • 命名为 HelloWeb3.sol

    1. 编写合约代码

    // 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" 图标

    • • 或使用快捷键 Ctrl+S / Cmd+S

    1. 选择编译器版本
    • • 在 "Compiler" 下拉菜单中选择 0.8.20

    • • 确保与 Pragma 声明匹配

    1. 编译合约
    • • 点击 "Compile HelloWeb3.sol" 按钮

    • • 等待编译完成

    1. 检查结果
    • • ✅ 绿色勾号:编译成功

    • • ❌ 红色叉号:编译失败,查看错误信息

      我们看到编译的时候生成几个json文件,可以点进去看看。

常见编译错误
错误 原因 解决方法
Expected identifier 语法错误 检查代码语法
Type error 类型不匹配 检查变量类型
Undeclared identifier 变量未声明 检查变量名拼写
Function overload clash 函数重载冲突 修改函数签名

步骤 4: 部署和测试 (25分钟)

部署合约
    1. 打开部署面板
    • • 点击左侧 "Deploy & Run Transactions" 图标
    1. 选择环境
    • • 在 "Environment" 下拉菜单中选择 "Remix VM (Cancun)"

    • • 这是 Remix 内置的本地虚拟机

    • • 无需连接钱包,测试使用

    1. 部署合约
    • • 在 "Deploy" 按钮旁确认选中了 HelloWeb3 合约

    • • 点击 "Deploy" 按钮

    • • 等待部署完成

  1. 查看部署结果
  • • 在下方 "Deployed Contracts" 下找到 HelloWeb3

  • • 点击展开合约,可以看到所有公开的函数和变量

测试合约

1. 查看初始值

复制代码
# 点击 message 按钮
# 预期输出: "Hello Web3!"

# 点击 owner 按钮
# 预期输出: 你的部署地址

# 点击 changeCount 按钮
# 预期输出: 0

2. 测试 setMessage 函数

复制代码
# 在 setMessage 输入框输入新消息
# 示例: "Welcome to Web3!"

# 点击按钮调用函数
# 查看控制台输出
# 应该看到事件日志

# 再次点击 message 按钮
# 预期输出: "Welcome to Web3!"

3. 测试访问控制

复制代码
# 切换到另一个账户
# 点击 Remix VM 旁的账户下拉菜单
# 选择不同的账户

# 尝试调用 setMessage
# 预期: 交易失败,显示 "Only owner can call this"

4. 测试 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的基础上又增加了5ETH

6. 测试 getInfo 函数

复制代码
# 点击 getInfo 按钮
# 预期输出: ("Welcome to Web3!", owner地址, 1)
查看事件日志
    1. 打开调试面板
    • • 点击左侧 "Debug" 图标
    1. 查看事件
    • • 点击控制台中的交易

    • • 展开 "logs" 部分

    • • 查看事件参数,拷贝"transaction hash"中的内容到DEBUGGER中,就可以看到debug信息。

了解不同的部署环境

Remix VM

  • • 本地虚拟机

  • • 快速,免费

  • • 适合开发测试

Injected Provider - MetaMask

  • • 连接真实的 MetaMask 钱包

  • • 可以部署到测试网

  • • 需要支付 Gas

Web3 Provider

  • • 连接到外部节点

  • • 需要提供 RPC URL

今天我人只测试和体验了Remix VM,后续有时间我们再体验Injected Provider和Web3 Provider。


✅ 今日产出检查清单

理论知识

  • • 理解智能合约的概念和特点

  • • 了解 EVM 的工作原理

  • • 掌握 Solidity 语言的特点

  • • 理解数据类型(值类型、引用类型)

  • • 掌握函数的可见性修饰符

  • • 理解存储位置(storage、memory、calldata)

实操能力

  • • 熟悉 Remix IDE 的界面和功能

  • • 成功创建和编译智能合约

  • • 在 Remix VM 中部署合约

  • • 测试合约的各种功能

  • • 理解事件的用途

  • • 掌握访问控制的使用

代码能力

  • • 能够定义状态变量

  • • 能够编写构造函数

  • • 能够创建和使用修饰符

  • • 能够定义和触发事件

  • • 能够处理 ETH 接收和发送

  • • 能够实现访问控制


💡 常见问题

Q1: 什么是 wei?

A: 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 wei

Q2: view 和 pure 函数不需要 Gas 吗?

A: 不完全正确。如果外部调用 view/pure 函数,不需要支付 Gas。但如果在合约内部调用,仍然需要 Gas。

Q3: 为什么需要 payable 修饰符?

A: 只有带有 payable 修饰符的函数才能接收 ETH。这是为了防止意外转账和潜在的安全问题。

Q4: 事件和状态变量有什么区别?

A:

  • 事件: 记录日志,不存储在状态中,成本低,用于前端监听

  • 状态变量: 永久存储,成本高,可以随时读取

Q5: 如何选择存储位置?

A:

  • storage: 需要永久保存的数据

  • memory: 函数内部临时使用的数据

  • calldata: 只读的输入参数


📖 课后作业(选做)

    1. 扩展合约功能
    • • 添加一个计数器,记录合约被调用的总次数

    • • 添加一个函数,可以设置最大修改次数

    • • 实现一个简单的投票机制

    1. 测试不同数据类型
    • • 创建一个使用 mapping 的函数

    • • 测试数组的添加和删除

    • • 尝试使用 struct

    1. 优化 Gas 成本
    • • 比较 uint256 和 uint8 的 Gas 差异

    • • 测试使用 calldata vs memory 的区别

    • • 使用事件替代部分状态变量

    1. 记录学习笔记
    • • 记录今天学到的所有语法

    • • 记录遇到的错误和解决方法

    • • 总结最佳实践


🎯 明天预告

明天我们将深入学习智能合约的进阶功能:

  • • 学习继承和多重继承

  • • 掌握错误处理(require、revert、assert)

  • • 理解 ERC-20 代币标准

  • • 实现一个完整的 ERC-20 代币合约

  • • 学习智能合约的安全最佳实践

预习建议:

  • • 阅读关于面向对象编程的基础知识

  • • 了解代币的概念和经济模型

  • • 研究一些知名的 ERC-20 代币


学习时长:约 100 分钟
难度:⭐⭐☆☆☆
下次学习:Day 3 - 智能合约进阶与 ERC-20 代币

详细参见: 春节7天Web3学习计划:我决定用假期给自己"充值"

祝你学习顺利! 🚀

相关推荐
前路不黑暗@2 小时前
Java项目:Java脚手架项目的阿里云短信服务集成(十六)
android·java·spring boot·学习·spring cloud·阿里云·maven
寒秋花开曾相惜2 小时前
(学习笔记)2.2 整数表示(2.2.3 补码编码)
c语言·开发语言·笔记·学习
CappuccinoRose2 小时前
CSS 语法学习文档(十七)
前端·css·学习·布局·houdini·瀑布流布局·csspaintingapi
winfreedoms2 小时前
ros2开发入门——黑马程序员ROS2上课笔记
笔记
啊阿狸不会拉杆2 小时前
《计算机视觉:模型、学习和推理》第 1 章 - 绪论
人工智能·python·学习·算法·机器学习·计算机视觉·模型
tritone2 小时前
初探云原生:在阿贝云免费服务器上学习负载均衡的实践心得
服务器·学习·云原生
Evand J2 小时前
matlab GUI制作界面的一些笔记(入门)
开发语言·笔记·matlab
好奇龙猫2 小时前
【日语学习-日语知识点小记-日本語体系構造-JLPT-N2前期阶段-第一阶段(14):単語文法】
学习
我命由我123453 小时前
Visual Studio - Visual Studio 修改项目的字符集
c语言·开发语言·c++·ide·学习·visualstudio·visual studio