在 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;它可以拆成三个部分:
因为 x 声明在合约内部、函数外部,所以它是状态变量。它的值保存在合约的链上存储中。
例如,合约部署后,x 初始值为 0。调用 inc() 后,x 变为 1。
这里有一个重要区别:读取状态变量通常不需要发送链上交易;修改状态变量则需要执行交易并消耗 Gas。
2. 函数如何修改状态?
看这段代码:
function inc() public {
x++;
emit Increment(1);
}执行顺序是:
function inc()声明一个名为inc的函数。public允许外部账户或其他合约调用它。x++将状态变量x增加1。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 消耗。
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 函数不能修改状态