在 VS Code 中正式学习 Solidity

我们现在不需要继续折腾环境。直接打开左侧的 contracts/Counter.sol,你会看到当前的合约:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.34;

contract Counter {
    uint public x;

    event Increment(uint by);

    function inc() public {
        x++;
        emit Increment(1);
    }

    function incBy(uint by) public {
        require(by > 0, "incBy: increment should be positive");
        x += by;
        emit Increment(by);
    }
}

第一课我们从 uint public x; 开始。

  • uint:表示无符号整数,不能表示负数。

  • public:允许外部读取这个变量,Solidity 会自动生成读取函数。

  • x:变量名。

  • 这个变量是状态变量,不是普通的局部变量。修改它会改变合约存储的状态。

在 Counter.sol 中找到:

uint public x;

暂时不要修改它。请在 VS Code 中把鼠标悬停在 uint 上,会看到Solidity 扩展显示类型提示。

理解 Counter.sol

1. 状态变量是什么?

先看这一行:

uint public x;

它可以拆成三个部分:

代码

含义

uint

无符号整数类型,不能为负数

public

自动生成外部读取函数

x

变量名称

因为 x 声明在合约内部、函数外部,所以它是状态变量。它的值保存在合约的链上存储中。

例如,合约部署后,x 初始值为 0。调用 inc() 后,x 变为 1。

这里有一个重要区别:读取状态变量通常不需要发送链上交易;修改状态变量则需要执行交易并消耗 Gas。

2. 函数如何修改状态?

看这段代码:

function inc() public {
    x++;
    emit Increment(1);
}

执行顺序是:

  1. function inc() 声明一个名为 inc 的函数。

  2. public 允许外部账户或其他合约调用它。

  3. x++ 将状态变量 x 增加 1。

  4. emit Increment(1) 发出一个事件,记录本次增加了 1。

注意:事件日志不是状态变量。 事件适合让应用程序、区块浏览器或索引服务获取发生过的操作;它不能像普通状态变量那样通过合约的读取函数直接查询历史记录。

3. 亲自做一次修改

现在在 VS Code 的 contracts/Counter.sol 中,找到:

function inc() public {
    x++;
    emit Increment(1);
}

在它下面添加这个函数:

function incByTwo() public {
    x += 2;
    emit Increment(2);
}

保存文件。

这个练习会让你理解:一个函数可以一次增加 2,同时发出对应的事件。

4. 用测试验证修改

回到 VS Code 终端,执行:

npx hardhat test

如果成功,你会看到原有的 5 项测试仍然通过。不过,原有测试还没有专门验证新函数,因此我们下一步要为 incByTwo() 添加一项测试。

小测验

如果 x 的初始值是 0,调用一次 incByTwo() 后,x 的值是多少?

A. 0

B. 1

C. 2

D. Gas

为新函数编写测试

现在我们要让测试自动验证 incByTwo() 的行为,而不是只凭肉眼检查代码。

打开 test/Counter.ts,在现有的两个 it(...) 测试内部同级的位置,添加下面这个测试:

it("Should increase x by 2 when calling incByTwo()", async function () {
  const counter = await viem.deployContract("Counter");

  await counter.write.incByTwo();

  assert.equal(await counter.read.x(), 2n);
});

注意:这段测试要求你已经在 contracts/Counter.sol 中添加并保存了 incByTwo() 函数。

运行测试

在 VS Code 容器终端执行:

npx hardhat test

预期结果是 6 passing:原来的 5 项测试,加上新写的 1 项测试。

6 passing (3 solidity, 3 nodejs)

理解交易、读取和 Gas

这一步先不部署到真正的以太坊网络,我们继续在本地学习。

1. 先理解三个概念

操作

例子

是否消耗链上 Gas

读取状态

查询 x 当前是多少

通过 RPC 只读调用时不消耗链上 Gas

修改状态

x += 2

消耗 Gas

发送事件

emit Increment(2)

消耗 Gas,事件数据会写入交易日志

注意:即使是读取操作,如果在智能合约内部执行,并且属于一笔修改状态的交易,也可能产生 Gas 消耗。

2. 我们来测量一次交易的 Gas

在 VS Code 的容器终端中,进入项目目录:

cd /workspace

打开 test/Counter.ts,找到 incByTwo() 对应的测试。如果你已经添加了这个测试,就在它里面加入 Gas 统计。

将原来的测试改成下面这样:

it("Should increase x by 2 when calling incByTwo()", async function () {
  const counter = await viem.deployContract("Counter");

  const hash = await counter.write.incByTwo();       

  const receipt = await publicClient.waitForTransactionReceipt({
    hash,          
  });

  console.log("incByTwo 消耗的 Gas:", receipt.gasUsed.toString());

  assert.equal(await counter.read.x(), 2n);
});

这里有几个新知识点:

  • hash:交易的哈希值,相当于这笔交易的唯一标识。

  • waitForTransactionReceipt():等待交易执行完成并取得回执。

  • receipt.gasUsed:这笔交易实际消耗的 Gas 数量。

  • assert.equal(...):确认执行结果符合预期。

3. 运行测试

在终端执行:

npx hardhat test

找到输出中的 incByTwo 消耗的 Gas。本地模拟网络的 Gas 数值可能因编译器和执行环境而异。

输出结果“:

Running node:test tests

  Counter
    ✔ Should emit the Increment event when calling the inc() function
incByTwo 消耗的 Gas: 44875
    ✔ Should increase x by 2 when calling incByTwo()
    ✔ The sum of the Increment events should match the current value


6 passing (3 solidity, 3 nodejs)

root@ad7657016973:/workspace# 

这节课的小测验

假设 x 当前是 0,调用 incByTwo() 后,以下哪项正确?

A. x 变成 2,但不产生交易

B. x 变成 2,产生一笔修改状态的交易,并消耗 Gas

C. x 仍然是 0,因为 public 函数不能修改状态