合约中声明和使用的变量都有一个数据位置,指明变量值应该存储在哪里。合约变量的数据位置将会影响Gas消耗量。

1.数据位置的的几种类型:

Solidity 提供4种类型的数据位置。
  • Storage:
    • 该存储位置存储永久数据,这意味着该数据可以被合约中的所有函数访问。可以把它视为计算机的硬盘数据,所有数据都永久存储。
      保存在存储区(Storage)中的变量,以智能合约的状态存储,并且在函数调用之间保持持久性。与其他数据位置相比,存储区数据位置的成本较高。
  • Memory:
    • 内存位置是临时数据,比存储位置便宜。它只能在函数中访问。
      通常,内存数据用于保存临时变量,以便在函数执行期间进行计算。一旦函数执行完毕,它的内容就会被丢弃。你可以把它想象成每个单独函数的内存(RAM)。
  • Calldata:
    • Calldata是不可修改的非持久性数据位置,所有传递给函数的值,都存储在这里。此外,Calldata是外部函数的参数(而不是返回参数)的默认位置。
  • Stack:
    • 堆栈是由EVM (Ethereum虚拟机)维护的非持久性数据。EVM使用堆栈数据位置在执行期间加载变量。堆栈位置最多有1024个级别的限制。

2.变量的数据位置规则:

rule 1:状态变量

状态变量总是存储在存储区中。
此外,不能显式地标记状态变量的位置。

rule 2:函数参数与返回值

函数参数包括返回参数都存储在内存中。
此处,函数参数 uint num1 与 uint num2,返回值 uint result 都存储在内存中。

rule 3:局部变量

值类型的局部变量存储在内存中。但是,对于引用类型,需要显式地指定数据位置。
不能显式覆盖具有值类型的局部变量。

rule 4:外部函数的参数

外部函数的参数(不包括返回参数)存储在Calldata中。

3.赋值的数据位置规则:

数据可以通过两种方式从一个变量复制到另一个变量。一种方法是复制整个数据(按值复制),另一种方法是引用复制。
从一个位置复制数据到另一个位置有一定的默认规则。

rule 1:存储变量赋值给存储变量

将一个状态(存储)变量赋值给另一个状态(存储)变量,将创建一个新的副本。
本例中,stateVar1stateVar2是状态变量。在doSomething函数中,我们将stateVar2的值复制到stateVar1
stateVar1的值是20,但是为了确定它创建了一个新的副本,我们改变了stateVar2的值。因此,如果它不创建副本,那么stateVar1的值应该是30,创建副本则是20。
这同样适用于引用类型状态变量。

rule 2:内存变量复制到存储变量

从内存变量复制到存储变量,总是会创建一个新的副本。
在上面的例子中,我们有一个状态变量和一个局部变量。在函数中,我们把局部变量的值赋给状态变量。
为了检查行为,我们改变了局部变量的值; 返回状态变量的值。这里可以看到,它会返回20,说明从内存变量复制到存储状态变量,会创建一个新的副本。

rule 3:存储变量复制到内存变量

从存储变量复制到内存变量,将创建一个副本。
在这里,将状态变量的值赋给了局部变量,并改变了状态变量的值。
为了检查局部变量的值是否已经更改,返回它的值。可以看到,它会返回10,说明从状态存储变量复制到内存变量,将创建一个副本。

rule 4:内存变量复制到内存变量

对于引用类型的局部变量,从一个内存变量复制到另一个内存变量不会创建副本。
对于值类型的局部变量仍然创建一个新副本。
引用类型:
在上面的示例中,我们在内存中初始化了一个名为localMemoryArray1的数组变量,并赋值为4、5和6。然后,我们将该变量复制到名为localMemoryArray2的新内存变量中。
然后,我们修改了localMemoryArray1中第一个索引的值,并返回了两个数组。这将得到相同的结果,因为它们都指向相同的位置。
让我们对值类型进行相同的检查。下面的函数将创建一个新的副本并返回20。
值类型: