1. 简介
合约是存放在以太坊区块链具有特定地址的代码和数据集合。 合约账户之间可以相互传递消息以实现图灵完备运算。 合约以以太坊特定的二进制字节码通过以太坊虚拟机(EVM)运行于区块链上。目前,合约通常是以Solidity(一门长得很像js的语言)高级语言编写,编译后传到区块链上运行。可使用浏览器版IDE Remix进行智能合约的开发。
2. Solidity高级语言
一个完整的智能合约往往会包含状态变量、函数、函数修改器、事件、结构体等基本要素,就像一个合约就像Java程序里面的类一样,支持继承。下面我们会逐一介绍solidity这门语言所特有的点及注意事项。
2.1. 基本类型
Solidity主要包括了值类型和引用类型两种。值类型与一般开发语言类似,有布尔、整型、定长字节数据、函数等。引用类型有数组、不定长字节数组、结构体、字符串等。像布尔值、整型、结构体、字典映射等均与c++或者java语言类似,用法可以参考官方文档,这里不在赘述。但我们在开发时需要注意几点:
1) 引入新的值类型:地址(address)
2) 对于复杂的数据类型数组和结构体,需要注意额外属性——存储位置
3) 函数可见性分为public,private,internal,external。可用view,payable,pure等关键字修饰。
4) 小数问题。
5) 数组可变与不变及基础操作。
下面简单解释下这几点。
2.1.1. 地址类型:
地址就相当于一个以太坊的账户,是一个串160位定长的16进制字符串。
地址具有balance属性,值代该地址账户下的以太币余额,基础单位是位(wei)而非ether,1 ether = 1e18wei。
地址拥有send,call,delegatecall,callcode函数,后面新加入了transfer方法。send和transfer函数用于向指定地址发送以太币。如:
to.transfer(msg.value)
to表示接受者的地址,而msg.value表示本次消息附带的以太币数量,值得注意的是msg是一个神奇的全局变量,它为合约提供了一些区块链的属性。需要注意的是send如果传输失败会返回一个返回值false,成功则返回true。而transfer会将传输作为一个事物进行传输,一旦失败则会滚。其余三个函数主要用于同一些支持ABI协议的进行交互。
call方法返回一个bool值,以表明执行成功还是失败。正常结束返回true,异常终止返回false。我们无法解析返回结果,因为这样我们得事前知道返回的数据的编码和数据大小(这里的潜在假设是不知道对方使用的协议格式,所以也不会知道返回的结果如何解析,有点祼协议测试的感觉)。
同样我们也可以使用delegatecall(),它与call方法的区别在于,仅仅是代码会执行,而其它方面,如(存储,余额等)都是用的当前的合约的数据。delegatecall()方法的目的是用来执行另一个合约中的工具库。所以开发者需要保证两个合约中的存储变量能兼容,来保证delegatecall()能顺利执行。
在homestead阶段之前,仅有一个受限的多样的callcode()方法可用,但并未提供对msg.sender,msg.value的访问权限。
在使用call时应该足够谨慎,因为这个能影响合约的安全性。
2.1.2. 存储位置:
数组和结构体可选的存储位置有memory和storage两处。
Memory即为内存存储,分配并使用、不可变长、使用完被回收;storage则会永久的存储在区块链上,主要用于记录长期保留的状态。
在不声明存储位置的情况下,会默认选择指定的位置,也可通过关键字storage和memory声明。
默认的函数参数,包括返回的参数,他们是memory的。默认的局部变量是storage的。而默认的状态变量(合约声明的公有变量)是storage。
2.1.3. 函数可见性及关键字
下面我们通过一个实际的例子来进行讲解。
上图中我们定义了四个函数和一个永久存储在区块链上的状态变量a。可以看到,我们一共定义了public,private,external,internal四个类型的函数,他们之间的区别在于:
Public:
函数默认声明为public,允许内部及外部访问,可以被继承。
Private:
只能被当前合约访问,不可被继承,需要注意,即使声明为private,数据依然能被所有人查看,只是禁止了其他合约对函数的访问和数据的修改。
External:
可以提供对外访问,可被子类继承,但不能进行内部调用。
Internal:
可以内部进行访问和调用,可以继承。
可以看到,我们在四个函数可见性声明的后面还加了view, pure, payable等关键字。
主要是对函数作用进行声明,如view可以理解为它将此函数声明为了一个常量函数,承诺不会对区块链上的状态变量进行更改,老一点的版本中可能看到的是constant。
Pure声明的函数表示该函数不仅不会修改状态变量而且也不访问链上数据,可以作为一些工具类使用,如提供计算方法等。
Payable声明表示该函数存在交易,会产生以太币的转移。
当需要更改链上数据又不需要交易时,可像第四个函数_internal一样不做声明。
2.1.4. 小数
目前solidity暂不支持定长浮点型。
倘若计算结果是浮点型,则会转为ufixed(无符号浮点型)或者fixed(有符号浮点型)类型。如1/4可以得到一个ufixed0x8,但1/3是无限的,在solidity里面只能用ufixed0x256的最大长度来表示。
2.1.5. 数组
Solidity中数组分为可变数组和定长数组,由2.1.2可知,数组分为存在合约内部的和存在于内存中两类,存储与内存中的数组是不允许创建变长数组的。如:
uint[] memory a;
这样的声明是不被允许的。
数组可通过 .length方法获取长度和更改长度,.push方法加入新元素。但要注意,定长数组是无法通过length和push方法更改长度或插入的。如:
这样的操作方式是不被允许的。
接下来我们介绍下声明数组的问题。
在storage中,声明定长数组,并初始化变量:
uint[2] a = [1,2];
在storage中,声明变长数组,并初始化变量:
uint[] a = [1,2];
二维数组初始化:
uint[2][3] a = [[1,1],[2,2],[3,3]];
这里我们一定要注意,这个数组声明和java等语言的行列刚好是相反的。
在memory中,声明定长数组:
uint[] memory a = new uint[](2);
在memory中,声明定长数组并初始化:
uint[2] memory a = [uint(1),2];
而uint[] memory a = [uint(1),2];这种方式是错误的,大家会奇怪为什么要在第一个元素一前面加上uint的类型,这是因为我们前面声明的是uint的数组,而在solidity中有uint8,uint248等多种不同位数的无符号int型常量,在不声明的情况下,默认使用uint256,而我们初始化的数组元素是1和2,系统会判定为uint8,所以需要我们在第一个元素1前面强制转化为uint型。当然,我们也可以如下的声明方式:
uint8[2] memory a = [1,2];
但要注意,那么a数组里面就不可以出现2进制里面大于8位的数了。
原文发布时间为:2018-03-20
本文作者:can@链博科技