Gas
我们懂了如何在禁止第三方修改我们的合约的同时,留个自己去修改部分参数的权限。
让我们来看另一种使得 Solidity 编程与众不同的地方:
Gas - 驱动以太坊去中心化应用(DApp)的燃料
在 Solidity 中,用户想要每次执行 DApp 都需要支付一定的 **gas**,gas 可以用以太币购买,因此,用户每次跑 DApp 都得花费以太币。
一个 DApp 收取多少 gas 取决于功能逻辑的复杂程度。每个操作背后,都在计算完成这个操作所需要的计算资源(比如,存储数据就比加法运算贵得多), 一次操作所需要花费的 gas 等于这个操作背后的所有运算花销的总和。
由于运行你的程序需要花费用户的真金白银,在以太坊中代码的编程语言,比其他任何编程语言都更强调优化。同样的功能,使用没优化的代码开发的程序,比起经过精巧优化的代码来,运行花费更高,这显然会给成千上万的用户带来大量不必要的开销。
为什么要用 gas 来驱动?
以太坊就像一个巨大、缓慢、但非常安全的电脑。当你运行一个程序的时候,网络上的每一个节点都在进行相同的运算,以验证它的输出 —— 这就是所谓的“去中心化”
由于数以千计的节点同时在验证着每个功能的运行,这可以确保它的数据不会被被监控,或者被刻意修改。
可能会有用户用无限循环堵塞网络,抑或用密集运算来占用大量的网络资源,为了防止这种事情的发生,以太坊的创建者为以太坊上的资源制定了价格,想要在以太坊上运算或者存储,你需要先付费。
省 gas 的招数:结构封装 (Struct packing)
在第之前的课程中,我们提到除了基本版的 uint 外,还有其他变种 uint:uint8,uint16,uint32等。
通常情况下我们不会考虑使用 uint 变种,因为无论如何定义 uint的大小,Solidity 为它保留256位的存储空间。例如,使用 uint8 而不是uint(uint256)不会为你节省任何 gas。
除非,把 uint 绑定到 struct 里面。
如果一个 struct 中有多个 uint,则尽可能使用较小的 uint, Solidity 会将这些 uint 打包在一起,从而占用较少的存储空间。例如:
struct NormalStruct {
uint a;
uint b;
uint c;
}
struct MiniMe {
uint32 a;
uint32 b;
uint c;
}
// 因为使用了结构打包,`mini` 比 `normal` 占用的空间更少
NormalStruct normal = NormalStruct(10, 20, 30);
MiniMe mini = MiniMe(10, 20, 30);
复制代码所以,当 uint 定义在一个 struct 中的时候,尽量使用最小的整数子类型以节约空间。
并且把同样类型的变量放一起(即在 struct 中将把变量按照类型依次放置),这样 Solidity 可以将存储空间最小化。例如,有两个 struct:
uint c; uint32 a; uint32 b;
和
uint32 a; uint c; uint32 b;
前者比后者需要的gas更少,因为前者把uint32放一起了。
实战演习
在本步中,咱们来修改区块宠物level 、 readyTime 和 strength 的类型,来节约gas。
让我们回到 petincubator.sol:
Pet 结构体属性修改:level 改成 uint32类型,readyTime 改成 uint32类型),strength改成 uint8类型。32位足以保存区块宠物的级别和时间戳了,8位也足以保存区块宠物的体力值(最大为10,最小为0),这样比起使用普通的uint(256位),可以更紧密地封装数据,从而为我们省点 gas。同时我们把level(uint32类型)和readyTime(uint32类型)放一起,因为我们希望同类型数据打成一个包来节省 gas。
注意:Unix时间如果用一个32位的整数进行存储会导致“2038年”问题,当这个32位的unix时间戳不够用,产生溢出,使用这个时间的系统就麻烦了。所以,如果想让我们的应用 跑够20年,我们可以使用64位整数表示时间,但为此应用又得支付更多的 gas。真是个两难的设计啊!
我们需要注意类型转换问题。由于我们修改了readyTime的类型,要注意类型转换的问题。因为 now 返回类型 uint256,所以在传入 now + gapTime 时必须使用 uint32(...) 进行强制类型转换。我们可以用 uint32(now + gapTime) 将它明确转换成一个 uint32 类型的变量。


