1、Solidity特性
相较于javaScript
- 新增
address
类型; - 语言内嵌框架支持支付;提供了
payable
等关键字,可以在语言层面直接支持支付; - 使用区块链进行数据存储。数据的每个状态都可以永久存储,所以再使用时需要确定变量使用内存,还是区块链存储;
- 运行环境实在去中心化的网络上,所以需要强调合约或函数执行的调用方式
- 不同的异常机制。一旦出现异常,所有的执行都将回回撤,这主要是保证合约执行的原子性,避免中间状态的出现的数据不一致;
2、Solidity关键字
pragma
:杂注,在合约文件最上方。pragma solidity ^版本号;
;address payable
:与地址类型基本相同,不过多了transfer
和send
两个成员变量。address payable
可以隐式的转换为address
,而address
无法转换为address payable
view
:指明函数只读不写,可以从链上读取数据,但不能往链上写数据;constant
:与view
相同,一般只修饰状态变量,不允许赋值(除初始化以外)pure
:指明函数是一个纯计算的函数,不能从链上读取数据,只能读取临时变量;public
:公开类型,如果属性设置为public
类型,则自动生成一个get函数,外部可以访问;constructor
:构造函数,在部署合约时给变量赋初始值;mapping(address => uint)
:go中的map类型event
:event
配合emit
使用,使用event
定于一个事件,在调用的时候添加emit
,在外部就可以监控,放在函数内部似使用。assert
:断言,必须为true
enum
:枚举,不能在函数内部定义,应该在合约内部定义。枚举类型可以转换为任意整型fixed/ufixed
:浮点类型,常用fixed128x20
,其中128为总长度,小数位长度为20bytes1/bytes2 ……
:定长字节数组,里面存储的是16进制数据不是utf8类型,用的时候注意转换- 对于存储型(storage)的数组来说,元素类型可以是任意的(即元素也可以是数组类型,映射类型或者结构体);对于内存型(memory)的数组来说,元素类型不能是映射(mapping)类型
msg.sender.transfer
:要给msg.sender
发送- 高维数组定义的时候跟go是反着的,例如,一个由5个
uint
动态数组组成的数组是uint[][5]
,访问的时候跟go相同,例如,要访问第三个动态数组中的第二个uint
,可以使用x[2][1]
,越界访问将导致调用失败回退。增加新元素,必须使用.push()
或者将.length
增大,变长的storage
数组和bytes
(不包括string
)有一个push()
方法。 - 所有的复杂类型,即数组、结构体和映射类型,都有一个额外属性,“数据位置”,用来说明数据是保存在内存
memory
中还是存储storage
中 immutable
可以在合约部署时,定义常量;
3、数据位置
- 函数参数(包括返回的参数)的数据位置默认都是
memory
,局部变量的数据位置默认是storage
,状态变量的数据位置强制是storage
。另外还存在第三种数据位置calldata
这是一块只读的,且不会永远存储的位置,用来存储函数参数。外部函数的参数(非返回参数)的数据位置被强制指定为calldata
,效果跟memory
差不多。
3.1 数据位置总结
强制指定的数据位置
- 外部函数的参数(不包括返回参数):calldata
- 状态变量:storage
默认数据位置
- 函数参数(包括返回参数):memory
- 引用类型的局部变量:storage
- 值类型的局部变量:栈(stack)
特别要求 - 公开可见(publicly visible)的函数参数一定是memory类型,如果要求是storage类型则必须是private或者internal函数,这是为了防止随意的公开调用占用资源
4、回退函数(fallback)
- 回退函数(fallback function)是合约中的特殊函数;没有名字,不能有参数也不能有返回值
- 如果在一个合约的调用中,没有其他函数与给定的函数标识符匹配(或没有提供调用数据),那么这个函数(fallback函数)会被执行
- 每当合约收到以太币(没有任何数据),回退函数就会执行。此外,为了接受以太币,fallback函数必须标记为payable。如果不存在这样的函数,则合约不能通过常规交易接受以太币(给合约纯转账)
- 在上下文中通常只有很少的gas可以用来完成回退函数的调用,所以使fallback函数的调用尽量廉价很重要
- 回退函数可以使用
fallback()
或receive()
调用
如果调用合约中不存在的方法,首先判断msg.data
是否为空,如果不为空则调用fallback
函数,如果为空则判断是否有receive
方法,如果存在receive
方法则调用receive
,否则调用fallback
函数
5、memory和storage之间的赋值
storage
和memory
之间是值拷贝;- 其他类型向
storage
都是值拷贝; memory
和memory
之间是指针拷贝;storage
和local storage
之间是指针拷贝;storage
是状态变量,它的修改会改变链上数据memory
是局部变量,函数调用结束后会被销毁;- 在合约内定义在函数外定义的是状态变量。在函数内定义的只有函数调用时才会创建,并不会改变链上数据;
6、使用了view依然可以修改
要深入理解
view
:只能从合约上读取数据
pure
:既不读取,也不存储,只是进行运算;
7、delete的用法
8、调用合约的Gas消耗
9、函数可见性
public
修饰的变量和函数,任何用户或者合约都能调用和访问(可以被当前合约调用、被继承被外部调用)。
external
与public 类似,只不过这些函数只能在合约之外调用 - 它们不能被合约内的其他函数调用(只能被外部调用,不能被继承不能被内部调用)。
internal
和 private 类似,可以被继承合约调用。
private
修饰的变量和函数,只能在其所在的合约中调用和访问,不能被继承合约调用。
10、数组
- 定义局部变量的数组时,因为要开辟内存,所以必须指定数组长度,例如:
uint[] memory a = new uint[](5)
; - 局部变量的数组不能使用
push
和pop
等命令,只能通过下标修改; - 如果入参中有数组类型,需要指定
memory
或者calldata
类型(和memory
类似,但是只能用于参数中)。使用calldata
类型时,两个函数直接可以直接传递参数时会节约gas;
11、继承
- 父合约中需要被重写的方法需要使用关键字
virtual
,子合约中重写函数中使用关键字override
- 多线继承需要注意父合约顺序,越基础的越靠前。如果多重继承中需要重写,需要在函数
override
关键字后面加上(x,y)
- 继承中的构造函数
- 继承中调用父合约的方法。一种是
合约名.方法名
另一种是super.方法名
super
会自动寻找父合约。如果使用super.方法名
,方法在多重父合约中都重写了,会依次调用(出栈顺序 子->父->祖)
12、合约之间调用
合约类型(合约地址).方法名
13、Hash运算
keccak256
:智能合约中进行hash运算
abi.encode
:进行bytes
类型的打包,会使用0补齐,更加安全
abi.encodePacker
:进行bytes
类型的打包,不会使用0补齐。会出现碰撞,例如"AA","BB"
和"A","ABB"
的结果是一样的
ecrecover
:会恢复签名结果,验证签名
14、节约gas
- 将入参的存储位置关键字从
memory
改为calldata
- 将循环体中的
i += 1
替换为++i
- 需要重复读写的数据先存储到局部变量,最后写入状态变量
15、transfer、send、call
- transfer:带有2300个Gas,如果失败会调用reverts
- send:带有2300个Gas,返回一个bool值表示是否发送成功
- call:发送所有的Gas,返回一个bool值表示是否发送成功和返回data。如果发送地址是一个智能合约,则通过data带回