智能合约基础语言(五)——Solidity变量类型:引用类型

原文链接:智能合约基础语言(五):Solidity变量类型:引用类型(下)

一、目录

☛数据位置

☛数组

☛结构体

二、引用类型——数据位置

不同于之前值类型,引用类型占的空间更大,超过256字节,因为拷贝它们占用更多的空间。由此我们需要考虑将它们存储在什么位置?内存(memory,数据不是永久存在的)或存储(storage,数据永久的保存在数据块上)

2.1 数据位置分类

▪memory

▪storage

▪calldata

▪stack

2.1.1 memory

存储位置同我们普通程序的内存类似。即分配,即使用,越过作用域即不可被访问,等待被回收。

2.1.2 storage

数据将永远存在于区块链上。

2.1.3 calldata

一般只有外部函数的参数(不包括返回参数)被强制指定为calldata。这种数据位置是只读的,不会持久化到区块链。

2.1.4 语法格式

1. 栈实际是内存中的一个数据结构,每个栈元素占256位,栈的最大深度位1024;

2. 值类型的局部变量存储在栈中;

3. 在栈中保存一个很小的局部变量,gas开销最小,几乎免费使用,但是数量有限。

基于程序的上下文,大多数时候数据位置的选择是默认的,我们可以通过在变量名前声明memory还是storage来定义该变量的数据位置。

2.2 数据位置默认规则

▪ 函数参数、函数返回参数默认为memory

▪ 局部变量(作用域为局部)以及状态变量(作用域为全局)默认storage类型。

2.3 强制的数据位置

▪ 外部函数(External function)的参数(不包括返回参数)强制为:calldata。

▪ 状态变量(State variables)强制为: storage。

▪ 值类型的局部变量是存储在栈上。

2.4 不同数据位置变量间相互赋值

2.4.1 storage

当我们把一个storage类型的变量赋值给另一个storage时,只是修改了它的指针(引用传递)。

在上面的代码中,我们将传入的storage变量,赋值给另一个临时的storage类型的tmp时,并修改tmp.a = "Test",最后我们发现合约的状态变量s也被修改了。

2.4.2 memory给storage赋值

因为局部变量和状态变量的类型都可能是storage。所以我们要分开来说这两种情况。

☞ 2.4.2.1 memory赋值给状态变量

将一个memory类型的变量赋值给一个状态变量时,实际是将内存变量拷贝到存储中。

通过上例,我们发现,在memoryToState()中,我们把tmp赋值给s后,再修改tmp值,并不能产生任何变化。赋值时,完成了值拷贝,后续他们不再有任何的关系。

☞ 2.4.2.2 memory赋值给状态变量

由于在区块链中,storage必须是静态分配存储空间的。局部变量虽然是一个storage的,但它仅仅是一个storage类型的指针。如果进行这样的赋值,实际会产生一个错误。

通过上面的代码,我们可以看到这样的赋值的确不被允许。你可以通过将变量tmp改为memory来完成这样的赋值。

2.4.3 storage转为memory

将storage转为memory,实际是将数据从storage拷贝到memory中。

在上面的例子中,我们看到,拷贝后对tmp变量的修改,完全不会影响到原来的storage变量。

2.4.4 memory转为memory

memory之间是引用传递,并不会拷贝数据。我们来看看下面的代码。

在上面的代码中,memoryToMemory()传递进来了一个memory类型的变量,在函数内将之赋值给tmp,修改tmp的值,发现外部的memory也被改为了other memory。

注意:

1. 对于值类型,总是会进行拷贝

2. 不能将memory的函数参数赋值给storage局部变量

3. 不能通过引用销毁storage

2.5 实例

2.6 不同存储的消耗(gas消耗)

▪ storage 会永久保存合约状态变量,开销最大. 大概5000~20000。

▪ memory 仅保存临时变量,函数调用之后释放,开销很小。

▪ calldata 和memory差不多。

▪ stack 保存很小的局部变量,几乎免费使用,但有数量限制 具体gas消耗值请参考http://yellowpaper.io/。

三、引用类型——数组

数组在所有的语言当中都是一种常见类型。在Solidity中,可以支持编译期定长数组和变长数组。一个类型为T,长度为k的数组,可以声明为T[k],而一个变长的数组则声明为T[]。

3.1 使用字面量创建数组

创建数组时,我们可以使用字面量,隐式创建一个定长数组。

通过上面的代码,我们可以发现。

首先元素类型是刚好能存储的元素的类型,比如代码里的[1, 2, 3],只需要uint8即可存储。但由于我们声明的变量是uint(默认的uint表示的其实是uint256),所以要使用uint(1)来进行显式的类型转换。

其次,字面量方式声明的数组是定长的,且实际长度要与声明的相匹配,否则编译器会报错

Type string memory[1] memory is not implicitly convertible to expected type string memory[2] memory。

3.2 使用new关键字创建数组

对于变长数组,在初始化分配空间前不可使用,可以通过new关键字来初始化一个数组。

我们声明了一个storage的stateVar,和一个memory的memVar。它们不能在使用new关键字初始化前使用下标方式访问,会报错VM Exception: invalid opcode。可以根据情况使用如例子中的new uint;来进行初始化。

3.3 数组属性

3.3.1 length属性

数组有一个length属性,表示当前的数组长度。对于storage的变长数组,可以通过给length赋值调整数组长度。

在上面这个例子中,我们可以看到,通过stateVar.length++语句对数组长度进行自增,我们就得到了一个不断变长的数组。 还可以使用后面提到的push()方法,来隐式的调整数组长度。

3.4 数组函数

变长的storage数组和bytes(不包括string)有一个push()函数。可以将一个新元素附加到数组末端,返回值为当前长度。push函数支持数组的初始化。

3.5 memory数组

对于memory的变长数组,不支持修改length属性,来调整数组大小。memory的变长数组虽然可以通过参数灵活指定大小,但一旦创建,大小不可调整。

如果状态变量的类型为数组,也可以标记为public类型,从而让Solidity创建一个访问器。(public类型的状态变量都有默认的访问器)访问器对于外界使用者表现为一个函数, 因此可以通过调用函数的方式访问某些值。 另外在remix中表现为一个可以点击的按钮, 可以获取对应的public类型的值。

如上面的合约在Remix运行后,需要我们填入的是一个要访问序号的数字,来访问具体某个元素。

3.6 多维数组

我们要创建一个长度为5的数组,每个元素又是一个变长uint数组,将被声明为uint[][5]。 反之, 假如要创建一个变长数组, 每个元素又是一个长度是5的数组, 将被声明为uint[5][],比如下边的例子:

在上面的代码中,我们声明了一个二维数组,它是一个变长的数组,里面的每个元素是一个长度为2的数组。要访问这个数组flags,第一个下标为变长数组的序号,第二个下标为长度为2的数组序号。

3.7 bytes与string

bytes和string是一种特殊的数组。

由于bytes与string,可以自由转换,你可以将字符串s通过bytes(s)转为一个bytes。可以以这种方式获得字符串长度,以及获取字符中字符的UTF-8编码。

四、引用类型——结构体

结构体,Solidity中的自定义类型。我们可以使用Solidity的关键字struct来进行自定义。结构体内可以包含字符串,整型等基本数据类型,以及数组,映射,结构体等复杂类型。数组,映射,结构体也支持自定义的结构体。我们来看一个自定义结构体的定义:

在上面的代码中,我们定义了一个简单的结构体Student,它包含一些基本的数据类型。另外我们还定义了一个稍微复杂一点的结构体Class,它包含了其它结构体Student,以及数组,映射等类型。

数组类型的students和映射类型的index的声明中还使用了结构体。

4.1 结构体定义的限制

我们不能在结构中定义一个自己作为类型,这样限制原因是,自定义类型的大小不允许是无限的。我们来看看下述的代码:

 

在上面的代码中,我们尝试在A类型中定义一个A a;,将会报错Error: Recursive struct definition.。虽然如此,但我们仍然能在类型内用数组,映射来引用当前定义的类型,如变量mappingMemberOfOwn,arrayMemberOfOwn所示。

4.2 初始化

4.2.1 直接初始化

如果我们声明的自定义类型为A,我们可以使用A(变量1,变量2, ...)的方式来完成初始化。来看下面的代码:

上面的代码中,我们按定义依次填入值,即可完成了初始化。需要注意的是,参数要与定义的数量匹配。当你填的参数与预计初始化的参数不一致时,会提示Error: Wrong argument count for function call: 2 arguments given but expected 3. Members that have to be skipped in memory: map。另外,在初始化时,需要忽略映射类型,后面有具体说明。

4.2.2 命名初始化

还可以使用类似JavaScript的命名参数初始化的方式,通过传入参数名和对应值的对象。这样做的好处在于可以不按定义的顺序传入值。我们来看看下面的例子:

上面的例子中,通过在参数对象中,指定键为对应的参数名,值为你想要初化的值,我们即完成了初始化。同样需要注意的是,参数要与定义的个数一致,否则会报类似这样的错误Error: Wrong argument count for function call: 2 arguments given but expected 3. Members that have to be skipped in memory: map。另外,在初始化时,需要忽略映射类型,后面有具体说明。

4.2.3 结构体中映射的初始化

由于映射是一种特殊的数据结构,所以你可能只能在storage变量中使用它。

上面的例子中,我们定义的了一个storage的状态变量storageVar,完成了映射类型的存储空间分配。然后我们就能对映射类型赋值了。

如果你尝试对memory的映射类型赋值,会报错Error: Member "map" is not available in struct StructMappingInitial.A memory outside of storage.。

4.3 结构体的可见性

关于可见性,当前只支持internal的,后续不排除放开这个限制。

4.3.1 继承中使用

结构体由于是不对外可见的,所以你只可以在当前合约,或合约的子类中使用。包含自定义结构体的函数均需要声明为internal的。

在上面的代码中,我们声明了f(S s),由于它包含了struct的S,所以不对外可见,需要标记为internal。你可以在当前类中使用它,如f1()所示,你还可以在子类中使用函数和结构体,如B合约的g()方法所示。

4.3.2 跨合约的临时解决方案

结构体,由于是动态内容。当前不支持在多个合约间互用,目前一种临时的方案如下:

在上面的例子中,我们手动将要返回的结构体拆解为基本类型进行了返回。

五、内存变量的布局

Solidity预留了3个32字节大小的槽位(存储空间):

▪ 0-64:哈希方法的暂存空间(scratch space)

▪ 64-96:当前已分配内存大小(也称空闲内存指针(free memory pointer))

注: 暂存空间是2个32字节大小,作为一个整体单元。

暂存空间可在语句之间使用(如在内联编译时使用)

Solidity总是在空闲内存指针所在位置创建一个新对象,且对应的内存永远不会被释放(也许未来会改变这种做法)。

有一些在Solidity中的操作需要超过64字节的临时空间,这样就会超过预留的暂存空间。他们就将会分配到空闲内存指针所在的地方,但由于他们自身的特点,生命周期相对较短,且指针本身不能更新,内存也许会,也许不会被清零(zerod out)。因此,大家不应该认为空闲的内存一定已经是清零(zeroed out)的。

六、状态变量的存储模型

大小固定的变量(除了映射,变长数组以外的所有类型)在存储(storage)中是依次连续从位置0开始排列的。如果多个变量占用的大小少于32字节,会尽可能的打包到单个storage槽位里,具体规则如下:

▪ 在storage槽中第一项是按低位对齐存储(lower-order aligned) 。

▪ 基本类型存储时仅占用其实际需要的字节。

▪ 如果基本类型不能放入某个槽位余下的空间,它将被放入下一个槽位。

▪ 结构体和数组总是使用一个全新的槽位,并占用整个槽(但在结构体内或数组内的每个项仍遵从上述规则)。

-END-

猜你喜欢

转载自blog.csdn.net/liankuaixy/article/details/82855018