使用Solidity开发智能合约(三)
本文我们将看到数据存储如何在Solidity中工作。
以太坊虚拟机(EVM)
EVM的内部工作原理
当我们安装以太坊客户端时,它附带了EVM,这是专门为运行智能合约而创建的轻量级操作系统。EVM的体系结构基于堆栈计算机的模型,这意味着指令集被设计用于堆栈而不是寄存器。
代码执行开始如下:当交易导致智能合约代码执行时,将实例化EVM,并在EVM的ROM中加载要调用的合约的代码。程序计数器设置为零,从合同帐户的存储中加载存储,存储器全部设置为零,并且所有块和环境变量都已设置。然后代码被执行。
资料位置
现在让我们回到Solidity docs中提到的内存关键字。从0.5.0版本开始,所有复杂的类型必须给它们存储在何处明确的数据位置,有三个数据位置:memory,storage,和calldata。
注意:唯一可以省略数据位置的地方是状态变量,因为状态变量将始终存储在帐户的存储器中。
1。storage
输入的数据storage将永久存储。该存储是键值存储。
中的数据storage写在区块链中(因此它们会改变状态),这就是为什么使用存储非常昂贵的原因。
占用256位插槽将花费20,000瓦斯。
更改一个已经占用的插槽的值将花费5,000瓦斯。
清理存储插槽时(即,将非零字节设置为零),将退还一定量的汽油。
存储将数据保存在256位大小(32字节=字)的字段中。即使未完全占用每个插槽,也会产生成本。
2。memory
memory是一个字节数组,其插槽大小为256位(32字节)。此处仅在函数执行期间存储数据。之后,将其删除。它们不会保存到区块链中。
读或写一个字(256位)需要3汽油。
为了避免给矿工带来太多工作,在进行22次作业后,每次作业的成本开始增加。
3。calldata
calldata是存储函数参数的不可修改的,非持久的区域,其行为基本上类似于memory。
calldata外部函数的参数是必需的,但也可以用于其他变量。
它避免了复制,还确保了数据不能被修改。
带有calldata数据位置的数组和结构也可以从函数中返回,但是不可能分配这种类型。
数据位置和分配行为
如果您不想发生意外行为,那么了解数据位置分配的工作原理非常重要。
在分配之间应用以下规则:
之间的分配storage和memory(或calldata)总是创建一个独立的副本。
从memory到分配memory仅创建引用。这意味着对一个内存变量的更改在引用相同数据的所有其他内存变量中也可见。
从storage本地存储变量的赋值也仅分配一个引用。
所有其他作业storage始终复制。这种情况的示例是分配给状态变量或storage结构类型的局部变量成员,即使局部变量本身只是一个引用也是如此。
让我们使用Remix debugger对其进行更详细的研究。
// SPDX-License-Identifier: GPL-3.0pragma solidity ^0.7.0;contract DataLocationTest { uint[] stateVar = [1,4,5]; function foo() public{ // case 1 : from storage to memory uint[] memory y = stateVar; // copy the content of stateVar to y // case 2 : from memory to storage y[0] = 12; y[1] = 20; y[2] = 24; stateVar = y; // copy the content of y to stateVar // case 3 : from storage to storage uint[] storage z = stateVar; // z is a pointer to stateVar z[0] = 38; z[1] = 89; z[2] = 72; } }
创建一个新文件,复制上面的代码,然后部署合同。
现在尝试调用该函数foo。您将在控制台中看到事务的详细信息,并且在其旁边有一个调试按钮。点击它。
您现在应该看到如下所示的调试器区域:
要遍历代码,请单击我以红色选择的箭头。
您应该注意到的第一件事是,正如我们在EVM部分中提到storage的stateVar,加载了的内容,当然没有局部变量。
当您跨过时,应该看到该变量y出现在局部变量(Solidity locals)部分中。继续进行下去,您会注意到,为了分配必要的内存空间并从中加载每个单词storage,然后将其复制到中,它需要很多字节码memory。这意味着要支付更多的天然气,因此从 storage到分配memory非常昂贵。
让我们研究第二种情况:从memory到赋值storage。当您完成存储在其中的副本的修改memory并且想要将更改保存回时,可以使用它 storage。它还消耗大量的气体。如果我们从调试器计算步骤详细信息部分中指示的剩余气体差,则为17,083瓦斯。该操作采用了四个SSTORE操作码:第一个用于存储保持不变的阵列大小(消耗了800个气体),另外三个用于更新阵列的值(每个消耗了5,000个气体)。
现在让我们看一下情况三:从存储到存储的分配。这次将创建一个新的局部变量,并包含与相同的内容stateVar。如果我们看一下代码执行,我们会注意到Solidity所做的就是推入包含数组长度的存储的第一个插槽的地址。对于动态数组,包含数组长度的插槽位置用于计算包含数组数据的插槽位置。
如果现在比较将数据复制到的成本memory,然后将其更新并复制回存储(21,629气体)与直接创建参考并直接更新状态(5,085气体)的成本,那么很明显第二种方法要便宜得多。
但是,如果我们像这样直接更新状态变量,该怎么办:
这也是可能的。但是,如果您要处理映射和嵌套数据类型(我们将在后面介绍),则使用storage指针可以导致代码更具可读性。
总结
在本篇文章中,我们可以看到的数据位置是如何工作的,你可以用这三个地点:memory,storage,和calldata。
在下一篇系列文章中,我们将继续学习Solidity中的变量,我们将重点放在引用类型上,该引用类型应显式指定数据位置。
微信扫描关注公众号,及时掌握新动向
2.本文版权归属原作所有,仅代表作者本人观点,不代表比特范的观点或立场
2.本文版权归属原作所有,仅代表作者本人观点,不代表比特范的观点或立场