for循环减少写入
在之前的内容中,我们提到过,函数中使用的数组是运行时在内存中通过 for 循环实时构建,而不是预先建立在存储中的。
为什么要这样做呢?
为了实现 getPetsByOwner 函数,一种“无脑式”的解决方案是在 PetIncubator 中存入”主人“和”区块宠物基地“的映射。
mapping (address => uint[]) public ownerToPets
然后我们每次创建新区块宠物时,执行 ownerToPets[owner].push(PetId) 将其添加到主人的区块宠物数组中。而 getPetsByOwner 函数也非常简单:
function getPetsByOwner(address _owner) external view returns (uint[]) {
return ownerToPets[_owner];
}这个做法有问题
做法倒是简单。可是如果我们需要一个函数来把一头区块宠物转移到另一个主人名下(我们一定会在后面的课程中实现的),又会发生什么?
这个“换主”函数要做到:
1.将区块宠物push到新主人的 ownerToPets 数组中,
2.从旧主的 ownerToPets 数组中移除区块宠物,
3.将旧主区块宠物数组中“换主区块宠物”之后的的每头区块宠物都往前挪一位,把挪走“换主区块宠物”后留下的“空槽”填上,
4.将数组长度减1。
但是第三步实在是太贵了!因为每挪动一头区块宠物,我们都要执行一次写操作。如果一个主人有20头区块宠物,而第一头被挪走了,那为了保持数组的顺序,我们得做19个写操作。
由于写入存储是 Solidity 中最费 gas 的操作之一,使得换主函数的每次调用都非常昂贵。更糟糕的是,每次调用的时候花费的 gas 都不同!具体还取决于用户在原主基地中的区块宠物头数,以及移走的区块宠物所在的位置。以至于用户都不知道应该支付多少 gas。
注意:当然,我们也可以把数组中最后一个区块宠物往前挪来填补空槽,并将数组长度减少一。但这样每做一笔交易,都会改变区块宠物基地的秩序。
由于从外部调用一个 view 函数是免费的,我们也可以在 getPetsByOwner 函数中用一个for循环遍历整个区块宠物数组,把属于某个主人的区块宠物挑出来构建出区块宠物数组。如此我们只是调用view函数,我们的 transfer 函数将便宜得多,因为我们不需要挪动存储里的区块宠物数组重新排序。
使用 for 循环
for循环的语法在 Solidity 和 JavaScript 中类似。
来看一个创建偶数数组的例子:
function getEvens() pure external returns(uint[]) {
uint[] memory evens = new uint[](5);
// 在新数组中记录序列号
uint counter = 0;
// 在循环从1迭代到10:
for (uint i = 1; i <= 10; i++) {
// 如果 `i` 是偶数...
if (i % 2 == 0) {
// 把它加入偶数数组
evens[counter] = i;
//索引加一, 指向下一个空的‘even’
counter++;
}
}
return evens;
}这个函数将返回一个形为 [2,4,6,8,10] 的数组。
实战演习
我们回到 getPetsByOwner 函数, 通过一条 for 循环来遍历 DApp 中所有的区块宠物, 将给定的‘用户id'与每头区块宠物的‘主人’进行比较,并在函数返回之前将它们推送到我们的result 数组中。
声明一个变量 counter,属性为 uint,设其值为 0 。我们用这个变量作为 result 数组的索引。
声明一个 for 循环,定义语句中第一项定义一个 uint 类型的 i 且赋值为 0;判断语句为 i < pets.length;最后使 i++。
在每一轮 for 循环中,用一个 if 语句来检查 petToOwner [i] 是否等于 _owner。这会比较两个地址是否匹配。
在 if 语句中:
通过将 result[counter] 赋值为 i,将区块宠物ID添加到 result 数组中。
将counter加1(使用i++)。
就是这样 - 这个函数能返回 _owner 所拥有的区块宠物数组,不花一分钱 gas。


