智能合约基础语言(六)——Solidity变量类型:其他

智能合约基础语言(六):Solidity变量类型:其他

一、目录

☞映射

☞特殊的运算符delete

☞基本类间的转换

二、变量类型——映射

映射或字典类型,一种键值对的映射关系存储结构。定义方式为mapping(_KeyType => _KeyValue)。键的类型允许除映射外的所有类型,如数组,合约,枚举,结构体。值的类型无限制。

映射可以被视作为一个哈希表,其中所有可能的键已被虚拟化的创建,被映射到一个默认值(二进制表示的零)。但在映射表中,我们并不存储键的数据,仅仅存储它的keccak256哈希值,用来查找值时使用。

因此,映射并没有长度,键集合(或列表),值集合(或列表)这样的概念。

映射类型,仅能用来定义状态变量,或者是在内部函数中作为storage类型的引用。引用是指你可以声明一个,如var storage mappVal的用于存储状态变量的引用的对象,但你没办法使用非状态变量来初始化这个引用。

可以通过将映射标记为public,来让Solidity创建一个访问器。要想访问这样的映射,需要提供一个键值做为参数。如果映射的值类型也是映射,使用访问器访问时,要提供这个映射值所对应的键,不断重复这个过程。

2.1 只能是状态变量

由于在映射中键的数量是任意的,导致映射的大小也是变长的。映射只能声明为storage的状态变量,或被赋值给一个storage的对象引用。我们来看下面的示例:

在上面的示例中,我们声明了storage的状态变量stateVar,可以对其增加新键值对;也能通过引用传递的方式赋值给storage的引用storageRef。

2.2 支持的类型

映射类型的键支持除映射,变长数组,合约,枚举,结构体以外的任意类型。值则允许任意类型,甚至是映射。下面是一个简单的例子代码:

2.3 setter方法

对于映射类型,也能标记为public。以让Solidity为我们自动生成访问器。

在上面的例子中,如果要访问intMapp第二个元素,在一对中括号中输入值1即intMapp[1]。而如果要访问嵌套的映射mapMapp[2][2],则输入两个键对应的值2,2即可。

2.4 getter方法

可以通过将映射标记为public,来让Solidity创建一个访问器。

三、变量类型——特殊的运算符delete

Solidity中有个特殊的操作符delete用于释放空间(特别是对于数组结构体以及映射类型的变量),因为区块链做为一种公用资源,为避免大家滥用。且鼓励主动对空间的回收,释放空间将会返还一些gas给调用者。

delete关键字的作用是对某个类型值a赋予初始值。比如如果删除整数delete a等同于a = 0。

3.1 删除基本类型

对于基本类型,使用delete会设置为对应的初始值:

删除bool类型是false,变长字节数组是0x0。string则是空串。

3.2 删除枚举

删除枚举类型时,会将其值重置为序号为0的值。

上面的例子中,删除light后,light将被置为序号为0的值即RED。

3.3 删除函数

尝试了一下删除函数,会报错Error: Expression has to be an lvalue.,看来不能删除函数。

3.4 删除结构体

删除一个结构体,会将其中的所有成员变量一一置为初值,我们来看一个例子。

在上面的例子中,我们声明了结构体s,调用delete s,结构体的值将变为其对应类型uint,string,bytes的初始值0,空串和0x0。

3.5 删除映射

映射是一个特殊的存在,由于映射的键并不总是能有效遍历(数据结构没有提供接口,也并不总是需要关心所有键是什么),所存的键的数量往往是非常大的,所以我们并不能直接删除一个映射。

如果直接删除一个映射会报错Unary operator delete cannot be applied 但我们可以指定键来删除映射中的某一项:

3.6 删除结构体中的映射

如果删除一个结构体时,其中含有映射类型,会跳过映射类型。我们来看一个删除含映射的结构体示例:

上面的示例中,删除结构体ms,并没有影响其中映射ms.m的值。

3.7 删除数组

对于定长数组,删除时,是将数组内所有元素置为初值。

而对于变长数组时,则是将长度置为0。

3.8 删除数组的一个元素

我们也可以删除数组的一个元素,有一点违反直觉的是,删除一个元素后,数组会留个空隙在那里。比如三个元素的数组,删除了第二个元素,只是将第二个元素置为了初始值,其它没变。

上述的代码运行后,将返回1,0,3。删除只是赋值,并没有移动元素。

3.9 gas使用的考虑

上文中,我们了解到,删除时会忽略映射,以及数组的某个元素被删除后,并不会自动整理数组。这些看起来很不符合常理,其实是基于对gas限制的考虑。因为如果映射或数组非常大的情况下,删除或维护它们将变得非常消耗gas。

不过,清理空间,可以获得gas的返还。但无特别意义的数组的整理和删除,只会消耗更多gas,需要在业务实现上进行权衡, 站在以太坊设计者的角度,因为不清理空间会浪费资源, 而大量遍历和删除操作又会占用cpu以及需要很多节点同步,因此节约空间和减少cpu的消耗都会奖励,两者需要找到一个平衡点才不至于资源浪费消耗太多gas。

3.10 清理的最佳实践

由于本身并未提供对映射这样的大对象的清理,所以存储并遍历它们来进行清理,显得特别消耗gas。一种实践就是能复用就复用,一般不主动清理。下面是一个数组的插入实现,比如增加一个计数器,直接忽略已使用过的位置。

上面的例子中,我们在数组新增时,直接忽略掉已使用过的槽位。而在代码内,我们使用numElements来代替array.length,以获取当前数组所在的位置。

如果这种大对象是在某个事件发生时,一次性使用,然后需要回收的。一个更有效的方式是,在发生某个事件时,创建一个新合约,在新合约完成逻辑,完成后,让合约suicide。清理合约占用空间返还的gas就退还给了调用者,来节省主动遍历删除消耗的额外gas。

3.11 删除的注意事项

删除本质是对一个变量赋初值。所以我们删除storage的引用时会报错,因为storage的引用并没有自己已分配的存储空间,所以不能对storage的引用直接赋初值。

上面的例子中,删除storageRef会报错。

四、变量类型——基本类型间的转换

4.1 隐式转换

如果一个运算符能支持不同类型。编译器会隐式的尝试将一个操作数的类型,转为另一个操作数的类型,赋值同理。

一般来说,值类型间的互相转换只要不丢失信息,语义可通则可转换。下面,我们来看一个整数转换的例子:

上面的例子中,我们将一个uint8的变量a隐式的转换为了uint16。同理它还支持转为uint32,uint128和uint256。

另外,无符号整数可以被转为同样,或更大的字节的类型。但需要注意的是,不能反过来转换。由于address是20字节大小,所以它与int160大小是一样。

4.2 显式转换

编译器不会将语法上不可转换的类型进行隐式转换,此时我们要通过显式转换的方式,比如将一个有符号整数,转为一个无符号整数。

4.3 类型推断

有时为了方便,我们不会显式定义类型。但由于编译器,会自动挑选一个最恰当的类型,所以会常常留下坑,我们来看这个例子:

大家可以想想上述代码运行的结果。

上述代码运行的结果实际为2100。原因是因为var i = 0定义时,通过类型推断,i的实际类型为uint8,所以它会一直循环,如果没有count >= 2100这个判断语句,这个循环将永远不会结束。

4.4 一些常见的转换方案

4.4.1 uint转为bytes

assembly是可以用汇编的方式实现某功能。 将一个uint转换成bytes,可以使用assembly。

上面的转换方式可能是效率最高的方式。

4.4.2 string转为bytes

string可以显式的转为bytes。但如果要转为bytes32,可能只能使用assembly。

猜你喜欢

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