前言
写本文的目的是记录自己对于区块链技术的学习,方便日后查漏补缺,solidity是一门智能合约语言,是静态类型语言,支持继承等特性。solidity中不存在 undefine和null的概念,每个新声明的变量都会根据其类型赋予默认值。引用类型包括结构,数组和映射。
一、数据位置
如果使用引用类型,则必须明确指明数据存储哪种类型的位置空间里 :
- memory 即数据在内存中,因此数据仅在其生命周期内(函数调用期间)有效。
- calldata 用来保存函数参数的特殊数据位置,是一个只读位置,不可修改的、非持久的函数参数存储区域,效果大多类似 内存memory .
- storage 状态变量保存的位置,只要合约存在就一直存储
更改数据位置或类型转换将始终产生自动进行一份拷贝,而在同一数据位置内(对于storage来说)的复制仅在某些情况下进行拷贝。
尽量使用calldata作为数据位置,因为它将避免复制,并确保不能修改数据。 函数的返回值中也可以使用calldata数据位置的数组和结构,但是无法给其分配空间。
在0.6.9版本之前,引用类型参数的数据位置有限制,外部函数中使用calldata,公共函数中使用memory,以及内部和私有函数中的memory或storage。 现在memory和calldata在所有函数中都被允许使用,无论其可见性如何。
在版本0.5.0之前,数据位置可以省略,并且根据变量的类型,函数类型等有默认数据位置,但是所有复杂类型现在必须提供明确的数据位置。
数据位置与赋值行为
数据位置不仅仅表示数据如何保存,它同样影响着赋值行为:
- 在storage和memory之间两两赋值(或者从calldata赋值 ),都会创建一份独立的拷贝。
- 从memory到memory的赋值只创建引用, 这意味着更改内存变量,其他引用相同数据的所有其他内存变量的值也会跟着改变。
- 从storage到本地存储变量的赋值也只分配一个引用。
- 其他的向storage的赋值,总是进行拷贝。 这种情况的示例如对状态变量或storage的结构体类型的局部变量成员的赋值,即使局部变量本身是一个引用,也会进行一份拷贝
二、数组
数组可以在声明时指定长度,也可以动态调整大小。
一个元素类型为T
,固定长度为k
的数组可以声明为T[k]
,而动态数组声明为T[]。
多维数组:T[][],如其他语言不同的是,在Solidity中, X[3]
总是一个包含三个 X
类型元素的数组,即使 X
本身就是一个数组,例如:uint[][5],代表的是长度为5,元素类型为动态数组,即一维为静态数组,二维为动态数组,而不是像其他语言一样,方括号中的数字对应着维度数。但是索引的时候,和其他语言一样,第一个括号代表着一维,第二个括号代表着二维。
数组元素可以是任何类型,包括映射或结构体。对类型的限制是映射只能存储在storage中,并且公开访问函数的参数需要是ABI类型。
状态变量标记 public
的数组,Solidity创建一个getter函数。 小标数字索引就是getter函数的参数。访问超出数组长度的元素会导致异常(Panic类型异常 )。
bytes
和string
也是数组
bytes和string类型的变量是特殊的数组。bytes类似于bytes1[],但它在calldata和memory中会被“紧打包”。string
与bytes
相同,但不允许用长度或索引来访问。
函数 bytes.concat
和 string.concat
可以使用string.concat
连接任意数量的string
字符串。 该函数返回一个string memory
,包含所有参数的内容,无填充方式拼接在一起。
bytes.concat
函数可以连接任意数量的 bytes
或 bytes1...bytes32
值。 该函数返回一个bytes memory
,包含所有参数的内容,无填充方式拼接在一起。如果使用string或者其他不能隐式转换为bytes的类型,需要先转换他。
调用不使用参数调用string.concat
或bytes.concat
将返回空数组。
创建内存数组
可使用new关键字在memory中创建动态长度数组。 与storage 数组相反的是,不能通过修改成员变量push改变memory数组的大小。必须提前计算所需的大小或者创建一个新的内存数组并复制每个元素。
uint[] memory a = new uint[](7);
数组常量
数组常量的类型通过以下的方式确定:它总是一个静态大小的memory数组,其长度为表达式的数量,数组的基本类型是列表上的第一个表达式的类型,以便所有其他表达式可以隐式地转换为它。如果不可以转换,将出现类型错误。
数组常量[1,-1]
是无效的,因为第一个表达式类型是 uint8
而第二个类似是 int8
他们不可以隐式的相互转换。 为了确保可以运行,你是可以使用例如:[int8(1),-1]
。
数组成员
length:数组有length成员变量表示当前数组的长度。 一经创建,memory 数组的大小就是固定的.
push():动态的storage 数组以及 bytes
类型(string
类型不可以)都有一个 push()
的成员函数,它用来添加新的零初始化元素到数组末尾,并返回元素引用。
push(x):动态的 存储storage 数组以及bytes
类型(string
类型不可以)都有一个push(x)
的成员函数,用来在数组末尾添加一个给定的元素,这个函数没有返回值。
pop():变长的 存储storage 数组以及 bytes
类型( string
类型不可以)都有一个pop()
的成员函数, 它用来从数组末尾删除元素。 同样的会在移除的元素上隐含调用delete ,这个函数没有返回值。
通过
push()
增加 存储storage 数组的长度具有固定的 gas 消耗,因为 存储storage 总是被零初始化,而通过pop()
减少长度则依赖移除与元素的大小(size)。如果元素是数组,则成本是很高的,因为它包括已删除的元素的清理,类似于在这些元素上调用delete。delete:将删除的元素初始化,即在其存储的位置全填充0.
如果需要在外部(external)函数中使用多维数组,这需要启用ABI coder v2。 公有(public)函数中是支持的使用多维数组。pragma experimental ABI coder v2/pragma ABI coder v2
在拜占庭(在2017-10-16日4370000区块上进行硬分叉升级)之前的EVM版本中,无法访问从函数调用返回的动态数组,如果要调用返回的动态数组的函数,要确保EVM在拜占庭模式上运行。
对存储数组元素的悬空引用
悬空引用是指一个指向不再存在的东西的引用,或者是对象被移除而没有更新引用。 例如,如果你将一个数组元素的引用存储在一个局部的引用中,然后从包含数组中pop()
出来,就会发生悬空引用。
编译器假定未使用的存储空间总是被清零的,数组在一块连续的区域中。所以:
uint[][] s; // 保存s最后一个元素的指向。 uint[] storage ptr = s[s.length - 1]; // 移除 s 最后一个元素 s.pop(); // 向不再属于数组的元素写入数据 ptr.push(0x42); // 现在添加元素到 ``s`` 不会添加一个空元素, 而是数组长度为 1, ``0x42`` 作为其元素。 s.push();
三、数组切片
目前数组切片,仅可使用于 calldata 数组.
用法如:x[start:end]
,start
和end
是 uint256 类型(或结果为 uint256 的表达式)。x[start:end]
的第一个元素是x[start]
,最后一个元素是x[end-1]
。如果start
比end
大或者end
比数组长度还大,将会抛出异常。
start
和end
都可以是可选的:start
默认是0,而end
默认是数组长度。
数组切片没有任何成员。 它们可以隐式转换为数组,并支持索引访问。 索引访问也是相对于切片的开始位置。 数组切片没有类型名称,这意味着没有变量可以将数组切片作为类型,它们仅存在于中间表达式中。
四、结构体
结构体本身可以作为映射的值类型成员,但它并不能包含自身。 因为结构体的大小必须是有限的。
函数中使用结构体时,一个结构体是如何赋值给一个存储位置是storage的局部变量的。 在这个过程中并没有拷贝这个结构体,而是保存一个引用,所以对局部变量成员的赋值实际上会被写入状态。
struct Campaign { address beneficiary; uint fundingGoal; uint numFunders; uint amount; mapping (uint => Funder) funders; }在 Solidity 0.7.0 之前,在 内存memory 结构体包含仅 存储storage 的类型(例如映射)可以允许
campaigns[campaignID]=Campaign(beneficiary,goal,0,0)
赋值,它会直接忽略映射类型。如果直接campaigns[campaignID]=Campaign(beneficiary,goal,0,0)
赋值,会创建一个memory类型的mapping,而mapping仅为storage.
五、映射
映射类型在声明时的形式为 mapping(KeyType=>ValueType),
其中 KeyType
可以是任何基本类型,即可以是任何的内建类型, bytes
和 string
或合约类型、枚举类型。 而其他用户定义的类型或复杂的类型如:映射、结构体、即除 bytes
和 string
之外的数组类型是不可以作为 KeyType
的类型的。ValueType
可以是包括映射类型在内的任何类型。
在实际的初始化过程中创建每个可能的 key, 并将其映射到字节形式全是零的值:一个类型的默认值。在映射中,实际上并不存储 key,而是存储它的 keccak256
哈希值,从而便于查询实际的值。正因为如此,映射是没有长度的,也没有 key 的集合或 value 的集合的概念。 ,因此如果没有其他信息键的信息是无法被删除
映射只能是storage 的数据位置,因此只允许作为状态变量或作为函数内的storage引用或作为库函数的参数。 它们不能用合约公有函数的参数或返回值。
六、三元运算符
三元运算符是一个表达是形式:<expression>?<trueExpression>:<falseExpression>
运算结果类型是由两个操作数的类型决定的,方法与上面一样,如果需要的话,首先转换为它们的最小可容纳类型(mobile type )。因此,255+(true?1:0)
将由于算术溢出而被回退。原因是(true?1:0)
是 uint8
类型,这迫使加法也要在uint8
中执行。 而256超出了这个类型所允许的范围。
像1.5+1.5
这样的表达式是有效的,但1.5+(true?1.5:2.5)
则无效。这是因为前者是以无限精度来进行有理表达式运算,只有它的最终结果值才是重要的。 后者涉及到将小数有理数转换为整数,这在目前是不允许的。
七、delete
delete a
的结果是将 a
类型初始值赋值给 a
。即对于整型变量来说,相当于 a = 0
,delete 也适用于数组,对于动态数组来说,是将重置为数组长度为0的数组,而对于静态数组来说,是将数组中的所有元素重置为初始值。对数组而言, delete a[x]
仅删除数组索引 x
处的元素,其他的元素和长度不变,这以为着数组中留出了一个空位。