-
solidity的自定义结构体深入详解
结构体,solidity中的自定义类型,我们可以使用关键字struct
来进行自定义,结构体内可以包含字符串,整型等基本数据类型,以及数组,映射,结构体等复杂类型。数组,映射,结构体也支持自定义的结构体:pragma solidity ^0.4.18; contract SmpleStruct { struct student { string name; int num; //student a; error student[] students; mapping(string => string)index; } struct Class { string className; //学生列表 student[] students; mapping(string => string)index; } }
结构体定义的限制
不能在结构体中定义一个自己作为类型,这样限制原因是,自定义类型的大小不允许是无限的,但是我们仍然能在类型内用数组,映射来引用当前定义的类型-
初始化
在初始化时,需要忽略映射类型- 直接初始化
如果声明的结构体类型为A
,可以使用A(变量1,变量2,...)
的方式来完成初始化,用法和go语言中的struct类似 - 命名初始化
还可以使用类似JavaScript的命名参数初始化额方式,通过传入参数名和对应值的对象,这样做的好处在于可以好处在于可以不按定义的顺序传入值 - 结构体中映射的初始化
由于映射是一种特殊的数据结构
参考资料
只能在storage
变量中使用,这里的storage
为状态变量,如果尝试对memory
的映射赋值,则会报错
[图片上传失败...(image-7f3e2-1540205314127)]
- 直接初始化
-
结构体的可见性
当前只支持internal
的,后续不排除开发这个限制
参考资料- 继承中使用
结构体由于不对外可见的,所以只可以在当前合约或继承的子合约中使用,但包含定义结构体的函数均需声明为internal
的
- 继承中使用
-
跨合约的临时解决方案
手动的将要返回的结构体拆解为基本类型进行返回pragma solidity ^0.4.18; contract TestA { struct A {string para1;int para2} function call(B b) internal { A memory a = A("test",1); b.g(a.para1,a.para2); } } contract TestB { function g(string s,int i) public { //要实现的内容 } }
-
solidity的映射类型深入详解
映射
是一种引用类型,存储键值对,提供根据键查找值,与其他语言的字典,map
等类似,但也有非常大的不同,尤其它在区块链中独特的储存模型-
只能是状态变量
由于在映射中键的数量是任意的,导致映射的大小也是变长的。映射只能声明为storage
的状态变量 ,或被赋值给一个storage的对象引用:pragma solidity ^0.4.18; contract Test { mapping(uint =>uint) stateVar; function fTest() public returns(uint) { //mapping(uint => uint) memory memVar; //TypeError:只能为数组或结构体类型指定内存 //mapping(uint =>uint) b; //TypeError:未被初始化,映射只能分配给状态变量 mapping(uint =>uint) b = stateVar; //可以被赋值为storage的引用 } }
支持的类型
映射类型的键
支持除映射,变长数组,合约,枚举,结构体以外的任意类型。值
则允许任意类型,甚至是映射-
setter方法
对于映射类型,也能标记为public
,以让solidity自动生成访问器pragma solidity ^0.4.18; contract Test{ mapping(uint => uint) public a; mapping(uint => mapping(uint => string)) b; //需要注意的是public不能不写,不然不会自动生成访问器 function set() { a[1] = 1; b[1][1] = "aaa"; } }
映射的储存模型
由于状态变量是储存在区块链上的,所以储存空间需要预先分配,但映射的储存值是可以动态增改的,那么最终是如何支持的呢?
关于状态的储存模型中提到,实际储存时是以哈希键值对的方式。其中哈希是由键值和映射的储存槽位序号拼接后计算的哈希值(映射只占一个槽位序号),也就是说值是存到由keccak256(k . p)
计算的哈希串里,这里的k
表示的是映射要查找的键,p
表示映射在整个合约中相对序号位置
这里没有看明白,所以附上原文链接,如果想看的可以看一下原文链接与其他语言映射的不同
由于映射的储存模型决定了,映射实际不存在一个映射的键大小,没有一个键集合的概念,但是可以通过扩展默认映射来实现这样的功能
官方提供的映射扩展示例
-
-
solidity的delete深入详解
solidity中有个特殊的操作符delete
用于释放空间,因为区块链作为一个公用资源,为避免大家滥用,且鼓励主动对空间的回收,释放空间将会返还一些gas
delete
关键字的作用是对某个类型值a
赋予初始值。比如如果删除整数delete a
等同于a = 0
- 删除基本类型
- 对于基本类型,如
:bool
,uint
,address
,bytes
,string
,使用delete
会设置成对应的初始值,bool
类型是false
,变长字节数组是0x0
,string
则是空串
- 对于基本类型,如
-
删除枚举
删除枚举类型时,会将其值重置为序号为0的值pragma solidity ^0.4.18; contract DeleteEnum { eum Lignt{red,green,yellow} Light light; function f() returns(Light) { light = Light.green;//此时light=1 delete light;//此时light=0 return light; } }
删除函数
函数不可以删除,会报错
- 删除复杂类型
删除结构体
删除一个结构体,会将其中的所有成员一一置为初值-
删除映射
映射是一个特殊的存在,由于映射的键并不总是能有效遍历(数据结构没有提供接口,也并不总是需要关心所有键是什么),所存的键的数量往往是非常大的,所以我们并不能删除一个映射,但是我们可以指定键来删除映射中的某一项mapping(address => uint) map; map[msg.sender] = 100; delete map[msg.sender];
删除结构体中的映射
如果删除一个结构体时,其中含有映射类型,会跳过映射类型
- 删除数组
- 删除整个数组
对于定长数组,删除时,是将数组内所有元素置为初值,而对于变长数组时,则是将长度置为0 - 删除数组的一个元素
我们也可以删除数组的一个元素,这里需要注意的是,删除一个元素并不会改变数组长度,即和删除其他类型一样,都是赋类型的零值
,并不会移动元素
-
gas使用的考虑
gas相关
上文中,我们了解到,删除时会忽略映射,以及数组的某个元素被删除后,并不会自动整理数组。这些看起来很不符合常理,其实是基于对gas
限制的考虑。因为如果映射或数组非常大的情况下,删除或维护它们将变得非常消耗gas
不过,清理空间,可以获得gas
的返还,但无特别意义的数组的整理和删除,只会消耗更多gas
,需要在业务实现上进行权衡-
清理的最佳实践
由于本身并未提供对映射这样的大对象的清理,所有储存并遍历它们来清理,显得特别消耗gas
,一种实践就是能复用就复用,一般不主动清理,下面是一个数组的插入实现,比如增加一个计数器,直接忽略已使用过的位置:uint numElements = 0; uint[] arrary; function insert(uint value) { if(numElements == arrary.length) { arrary.length += 1; } //需要注意的是,solidity中也有前自增和后自增的区别 arrary[numElements++] = value; } function clear() { numElements = 0; }
上面的例子中,我们在数组新增时,直接忽略已使用过的槽位。而在代码内,我们使用
numElements
来代替arrary.length
,以获取当前数组所在的位置。
如果这种大对象是在某个事件发生时,一次性使用,然后需要回收。一个更更有效的方式是,在发生某个事件时,创建一个新合约,在新合约完成逻辑,完成后,让合约suicide
。清理合约占用空间返还的gas
就退还给了调用者,来节省主动遍历消除的额外gas
。 -
删除的注意事项
删除的本质是对一个变量置其类型的零值
,所以我们删除storage
的引用时会报错pragma solidity ^0.4.18; contract Test { struct S{} S s; function fTest() { S storage storageVar = s; //delete storageVar; //Error:一元运算符不能应用于删除储存指针 delete s; } }
删除
storageVar
会报错- 参考资料
delte语法介绍
- 删除基本类型
-
solidity中的基本类型转换
-
隐式转换
如果一个运算符能支持不同类型,编译器会隐式的尝试将一个操作数的类型,转为另一个操作数的类型,赋值同理。
一般来说,值类型间的互换只要不丢失信息,语义可通则可转换,如:pragma solidity ^0.4.18; contract Test { function conversion() public returns(uint16) { uint8 a = 1; //隐式转换 uint16 b = a; return b; } }
另外,无符号整数可以被转换为同样,或更大的字节类型,由于
address
是20字节大小,所以它与int160
大小是一样的,所以也可以转换的- 显示转换
编译器不会将语法上不可转换的类型进行隐式转换,此时需要强转,如:int8 a =1;uint16 b =uint16(a);
- 类型推断:var
如:var i = 0;
,通过类型推断,i
的实际类型为uint8
,而且后续不会改变,即在第一次类型推断之后,该变量的类型就已经确定了 - 一些常见的转换方案
-
uint转为bytes
将一个uint转换为bytes,可以使用assembly
function toBytes(uint256 x) public returns(bytes b) { b = new bytes(32); assembly{mstore(add(b,32),x)} }
-
string转为bytes
string
可以显示的转为bytes
,但如果要转为bytes32
,可能只能使用assembly
,字符串转为bytespragma solidity ^0.4.18; contract stringTobytes{ function sTb1(string memory source) returns(bytes result) { return bytes(source); } //要记住,字符串是UTF8,所以在将它们转换为字节后,每个字节不一定是字符 function sTb2(string memory source) returns(bytes32 result) { assembly{ result := mload(add(source,32)) } } }
-
-
-
solidity变量的声明及作用域
solidity中的作用域规则同于Javascript
题外话【补遗】:
值类型
:储存值的变量;引用类型
:储存地址的变量,通俗来说:“值类型是现金,引用类型是存折”。链接声明及默认值
变量声明后均有一个初值,是对应类型的“零态”,即对应的类型的字节表示的全0
,使用中需要特别小心的是这与其他语言的默认值如null
或undefined
有所不同,因为有时0
也是一种业务值-
引用类型的初始化
对于值类型,声明变量后,即赋值为默认值,可正常使用。而对于引用类型是否仍需同其它语言一样进行显式初始化,进行内存分配,才能进一步使用呢,我们来分别看一下。-
动态数组
对于数组,声明后,仍需分配内存后访问,下面的代码会报越界错误pragma solidity ^0.4.18; contract inital{ function f() returns(byte,uint8){ bytes memory bs; uint[] memory arr; return (bs[0],arr[0]); } }
需要主动分配内存,进行初始化
pragma solidity ^0.4.18; contract Arraryinitalok { function f() returns(bytes1,uint8) { bytes memory bs = new bytes(1); uint8[] memory arr = new uint8[](1); return (bs[0],arr[0]); } }
上述代码通过
new
关键字进行了内存分配,现在即可正常访问第一个数组元素了- 映射
映射的声明后,不用显式初始化即可使用,只是里面不会有任何值,[回顾
]:映射只能是状态变量
或者是storage
对象的引用 - 枚举
枚举类型不用显示初始化,默认值将为0,即顺位第一值,枚举也只能是状态变量 - 结构体
结构体声明后,不用显式初始化即可使用。当没有显式初始化时,其成员值均为默认值。 - delete操作符,具体可以看上文
-
-
作用域
变量无论在函数内什么位置定义,其作用域均为整个函数,而非大多数据语言常见的块级作用域
下面的例子会报错,重复定义
pragma solidity ^0.4.18; contract scopeErr { function f() { {uint8 a = 0;} //{uint8 a = 1;} } }
我们来看一个稍微隐蔽点的例子
pragma solidity ^0.4.18; contract Test { function fTest() returns(uint8) { for(var i=0;i<10;i++) { //do sth } return i; } }
学过其他语言的,会认为这样是会报错的,但是在solidity中,无论变量在函数中任意位置定义,整个函数都是可以访问的,所以
i
变量虽然是在for循环中被定义,但它的的作用域仍是整个函数,所以在函数内的任何位置均可以访问到这个变量
我们来看一个更加极端的例子pragma solidity ^0.4.18; contract Test { function f() returns(uint8){ if (false) { uint8 a =10; } return a; } }
上述的代码中,虽然变量的声明看似没有执行到,但是由于变量作用域是整个函数,且由于第一部分讲到,任何变量声明后均有其默认值。故上例中将返回默认值
0
-
solidity的getter访问器
对于所有public
的状态变量(回顾:只有struct是强制internal),solidity语言编译器,提供了自动为状态变量(定义在函数外部)生成对应的getter访问器
的特性,暂不支持setter
(因为不能很好控制访问权限的改值函数,不一定实用)
状态变量所创建的访问器函数,与变量同名。以internal
访问时,按状态变量的方式使用,若以external
的方式访问时,则需要通过访问器函数pragma solidty ^0.4.18; contract Test{ uint public data = 10; function f() returns(uint,uint) { //分别以internal,external的方式访问 return (data,this.data()) } } contract test { //注意外部合约调用时,必须要先初始创建合约,不然不能访问 Test t = new Test(); function f() returns(address,uint) { return (t,t.data()); } }
上面的例子中,编译器会自动生成
data()
函数,在合约内我们可以直接以internal
的方式访问data
状态变量,但是在合约外我们只能用data()
的方式来访问,因为访问器函数的可见性是external
-
枚举
枚举的访问器与基本类型类似,均是生成与状态变量同名的函数pragma solidity ^0.4.18; contract Test { enum Color{red,green,yellow} Color public color = Color.green; function f() returns(Color,Color) { return (color,this.color()); } }
-
数组
前面的访问器函数,是一个与状态变量同名的无参函数。那么访问器一定是无参的吗,其实不然,对于数组,我们必须提供序号来使用访问器,下面我们来看一个数组的访问器示例:pragma solidity ^0.4.18; contract ArraryGetter{ uint[] public arr = new uint[](1); function f() returns(uint,uint) { return (arr[0],this.arr(0)); } }
我们可以发现,对于数组访问器的使用,需要增加序号值作为参数,如果不传序号值,会报错
-
映射
对于映射类型,它的访问器也是有参数的,参数为映射定义的键类型。我们来看一下映射的访问器示例:pragma solidity ^0.4.18; contract MappingGetter { mapping(uint => uint) public data; function f() returns(uint,uint) { data[25] = 100; return (data[25],this.data(25)); } }
回顾,映射类型只能定义为状态变量或storage
变量的引用,映射也是可变长的-
结构体
结构体的访问器也是同名的函数,访问器返回结果是结构体中的每个变量pragma solidity ^0.4.18; contract Test{ struct S { uint a; string b; bytes32 c; } S public s = S(10,"hello",hex"1234"); function f() returns (uint,bytes32) { var(a,b,c) = this.s(); return (a,c); } }
通过上例,我们可以看出,结构体的访问器分别返回了
s
中的a,b,c
三个变量值,但由于solidity不支持通过external
的访问变长内容,故上面的代码,通过f()
不能返回b
的值,会报错,但是通过Remix,则可以访问到结构体中的所有变量值-
一个复杂的例子
下面是一个集合结构体,数组,映射的一个复杂例子,但访问器访问方式遵循前述的规则:pragma solidity ^0.4.18; contract Complex { struct Data { uint a; bytes3 b; mapping(uint => uint) map; } mapping(uint => mapping(bool =>Data[])) public data; Data[] internal arr; function f() returns(uint,bytes3) { //初始化时跳过mapping类型 Data memory d = Data(1,0x123); arr.push(d); data[0][true] = arr; return this.data(0,true,0); } }
上面代码中,
data
状态变量的访问器,有三个参数。第一个参数是第一层映射的键uint
,第二个参数是映射内嵌映射的键类型bool
,由于两层映射最终映射到的是数组。故而,第三个参数是数组的序号值,访问器返回的类型是结构体类型Data
,所以上述代码将返回1,0x000123
需要注意的是,访问器返回结果结构体内的映射map
,由于访问器没有较好的方式来提供访问,所以直接被忽略 -