Solidity智能合约开发(入门篇)
一、基础语法1.1 版本声明合约文件开头需要声明编译器的版本号,目的是为了该合约在未来版本的升级中引入了不兼容的编译器,其语法为:pragma solidity 版本号版本号应该遵循“0.x.0”或者“x.0.0”的形式,比如:// 代表不允许低于0.4.17版本的编译器,也不允许使用高于0.5.0版本的编译器pragma solidity ^0.4.17// 代表支持0.4.17以上的版本,但是
一、版本声明
合约文件开头需要声明编译器的版本号,目的是为了该合约在未来版本的升级中引入了不兼容的编译器,其语法为:
pragma solidity 版本号
版本号应该遵循“0.x.0”或者“x.0.0”的形式,比如:
// 代表不允许低于0.4.17版本的编译器,也不允许使用高于0.5.0版本的编译器
pragma solidity ^0.4.17
// 代表支持0.4.17以上的版本,但是不能超过0.9.0版本
pragma solidity >=0.4.17 < 0.9.0;
二、代码注释
与Java类似,使用双斜杠//
代表单行注释。在双斜杠中间添加星号代表多行注释,比如:
// 单行注释
/*
多行注释
多行注释
...
*/
也可以添加文档注释,其语法为:
/**
* @dev
* @param
* @return
*/
比如:
pragma solidity >=0.4.17 <0.9.0
contract Calculator {
/**
* @dev 实现两个数的乘法运算
* @param a 乘数
* @param b 被乘数
* @return 返回乘法的结果
*/
function mul(uint a, uint b) public pure returns(uint) {
return a * b;
}
}
三、数据类型
3.1 值类型
3.1.1 布尔类型
布尔类型使用bool关键字。它的值是true或false。布尔类型支持的运算符有:
名称 | 符号 |
---|---|
逻辑与 | && |
逻辑或 | || |
逻辑非 | ! |
等于 | == |
不等于 | != |
3.1.2 整型
整型使用int或uint表示。uint代表的是无符号整型。它们都支持uint8、uint16,…, uint256(以8位为步长递增)。如果没有指定多少位,默认为int256或uint256。
整型支持的运算符:
名称 | 符号 |
---|---|
比较运算符 | <= < == != >= > |
位运算符 | & | ^ ~ |
算术运算符 | + - * / ++ -- % ** << >> |
注意:如果除数为0或者对0取模都会引发运行时异常。
3.1.3 定长浮点型
定长浮点型使用fixed或ufixed表示。但是目前solidity支持声明定长浮点型的变量,不能够对该类型的变量进行赋值。
3.1.4 地址类型
地址类型的关键字使用address表示,一般用于存储合约或账号,其长度一般为20个字节。地址类型的变量可以使用> >= < <= == !=
运算符。
地址类型的变量还可以使用以下成员变量和成员方法:
成员变量 | 描述 | 示例 |
---|---|---|
balance | 地址余额 | x.balance |
transfer | 向当前地址转账 | x.transfer(10) |
send | 向当前地址转账,与transfer不同的是,如果执行失败,该方法不会因为异常而终止合约执行 | x.send(10) |
call、delegatecall | 调用合约函数,第一个参数是函数签名。与call不同的是,delegatecall只使用存储在库合约中的代码 | x.call(“mul”, 2, 3) |
值得注意的是,当一个合约A调用另外一个合约B的时候,就相当于将控制权交给了合约B。合约B也有可能会反过来调用合约A。因此调用结束时,需要对合约A状态变量的改变做好准备。
3.1.5 定长字节数组
定长字节数组使用bytes1、bytes2、…、bytes32来表示。如果没有指定长度,默认为bytes1。定长字节数组支持的运算符有:
名称 | 符号 |
---|---|
比较运算符 | <= < == != >= > |
位运算符 | & | ^ ~ << >> |
索引访问 | x[i]代表访问第i个字节的数据 |
定长字节数组包含length属性,用于查询字节数组的长度。
虽然可以使用byte[]表示字节数组,但是一般推荐使用bytes代替。
3.1.6 字面常量
- 地址字面常量
像0x692a70d2e424a56d2c6c27aa97d1a86395877b3a这样通过了地址校验和测试的十六进制字面常量属于address类型。
- 数值字面常量
solidity支持整数和任意精度小数的字面常量。1.
或.1
都是有效的小数字面常量。也可以使用科学计数法表示,比如:2e10。
如果数值字面常量赋值给了数值类型的变量,就会转换成非字面常量类型。
uint128 a = 2.5 + 1 + 0.5; // 编译成功
uint128 b = 1;
uint128 c = 2.5 + b + 0.5; // 编译报错
上面代码的第二行,将数值字面常量1
赋值给了变量b。因为变量b是非字面常量,不同类型的字面常量和非字面常量无法进行运算,所以第三行报错。
- 字符串字面常量
字符串字面常量使用单引号或双引号引起来的一串字符。字符串字面常量可以隐式地转换成bytes类型。比如:
bytes3 b = "abc";
bytes4 b = "abcd";
bytes4 b = "abcde"; // 报错,因为bytes4只能存储4个字节数据
3.1.7 函数类型
solidity支持将一个函数赋值给一个变量,也可以作为参数传递给其他函数,或者作为函数的返回值。
声明语法:function (参数类型) [internal|external] [pure|constant|view|payable] [returns (返回值类型)]
pragma solidity ^0.4.16;
contract A {
// 声明函数变量,并将mul函数赋给变量func
function (uint, uint) pure returns (uint) func = mul;
// 函数声明
function mul(uint a, uint b) public pure returns(uint) {
return a * b;
}
}
注意事项:
- 如果函数类型不需要返回,那么需要删除整个returns部分;
- 如果没有指定
internal|external
,默认是internal内部函数。内部函数可以在当前合约或子合约中使用;
pragma solidity ^0.4.16;
contract A {
function (uint, uint) pure returns (uint) func;
}
contract B is A {
function mul(uint a, uint b) public pure returns(uint) {
return a * b;
}
function test() public {
// 对父合约的函数变量进行赋值
func = mul;
// 通过父合约函数变量调用mul函数
func(10, 20);
}
}
如果是external外部函数,可以通过函数调用传递,也可以通过函数返回。
pragma solidity ^0.4.16;
contract A {
struct Request {
bytes data;
// 外部函数类型
function (bytes memory) external callback;
}
Request[] requests;
// 该函数的第二个参数类型为外部函数类型
function query(bytes memory data, function (bytes memory) external callback) public {
requests.push(Request(data, callback));
}
}
contract Test {
function getResponse(bytes result) public {
// TODO ...
}
function test() public {
// 创建合约A的实例
A a = new A();
// 调用合约A实例的query函数,第二个参数是函数类型
a.query("abc", this.getResponse);
}
}
3.2 其他类型
3.2.1 数组
如果声明数组的时候指定数组长度,那么数组的大小就固定下来;如果声明数组时候没有指定长度,那么该数组的长度可以发生变化。
- 声明数组的方式:
// 定义固定长度的数组
uint[3] a;
// 定义长度可变的数组
uint[] a;
uint[] a = new int[](3);
// 通过字面量定义数组,数组长度也是可变的
uint[] c = [uint(1), 2, 3];
- 注意事项:
1)如果是状态变量的数组类型,不能手动指定memory或storage,默认为storage数组;
2)如果是局部变量的数组类型,可以指定为memory或storage,但是如果指定为storage数组,那么就必须对数组变量进行初始化;
contract SimpleContract {
int[] aa;
int[3] bb;
int[] cc = new int[](3);
int[] dd = [uint(1), 2, 3];
public test() public {
int[] memory a1;
int[] storage b1 = aa;
}
}
➡ memory和storage关键字的作用是什么?
memory和storage代表数据的存储位置,即数据是保存在内存中还是存储中(这里的存储可以简单理解为电脑的磁盘)。如果是状态变量和局部变量,默认为storage位置;如果是形参,默认位置为memory。另外如果是外部函数参数,其数据位置为calldata,效果跟memory差不多。数据位置的指定非常重要,它会影响着赋值行为。比如说,如果是状态变量赋值给storage的局部变量,实际上传递的是该变量的一个引用。
还有一个地方值得注意的是,定长数组只能赋给定长数组,并且两个数组的大小必须相同;定长数组不能赋值给变长数组,如果下面代码编译报错:
对于storage动态数组,可以通过它的length
属性改变数组的大小,所以下面第15行代码编译通过。但是对于memory动态数组,则无法通过length
属性改变数组的大小,所以下面第14行代码报错:
上面第14行代码会提示TypeError: Expression has to be an lvalue.
。lvalue可以理解为等号左边的值。上面错误提示信息提示我们,等号左边的值a1.length
必须是一个lvalue,即可以被修改的值。明显a1.length
不是一个lvalue。
添加数组元素可以通过下标方式添加,比如说:
function test() public pure {
int[] memory arr;
arr[0] = 100;
}
如果是变长的storage数组以及bytes类型,也可以通过push
方法添加数组元素,该方法会返回数组的最新长度。
contract SimpleContract {
int[] aa; // Define an array of variable length
function test() public {
int[] memory bb;
bb[0] = 100;
// 因为aa是一个变长的storage数组,因此可以通过push方法添加元素
aa.push(100);
// 下面代码报错,因为bb的位置不是storage
bb.push(100);
}
}
另外一个值得注意的地方是,由于EVM限制,solidity不支持通过外部函数调用返回动态内容。比如下面代码:
function test() public pure returns(int[] memory) {
int[] memory arr;
arr[0] = 100;
return arr;
}
EVM编译合约时候不会报错,但是当我们调用该合约示例的test函数时候,提示下面错误信息call to SimpleContract.test errored: VM error: invalid opcode.
。
但是,如果是通过web3调用test函数,它会返回一些动态内容。
小知识:可以把bytes看作是byte的数组形式,也可以将bytes当作string类型使用。但是,string无法通过
length
或索引
来访问字符串中的每个字符,如果需要访问字符串中的某个字符,可以先把字符串转换成bytes形式,比如:uint size = bytes(“123”).length; byte c = bytes(“123”)[0]。
3.2.3 结构体
可以使用结构体用来存储复杂的数据结构。其定义格式:
struct 结构体名称 {
变量类型 变量名;
...
}
结构体中变量类型可以是任意类型,但是它不能够包含自身。
如果函数中将结构体类型数据赋给一个局部变量,这个过程并没有创建这个结构体对象的副本,而且将该结构体对象的引用赋给了局部变量(即引用传递)。
3.2.4 枚举
与结构体类似的是,枚举也是solidity中的一种自定义类型,其定义格式为:
enum 类型名称 {
枚举值1,
枚举值2,
...
}
枚举类型中至少要包含一个枚举值。枚举值可以是任意有效的标识符(比如说:必须是字母或下划线开头)。
enum Week {
Monday,
Tuesday,
...
}
枚举值的类型默认为uint8。第一个枚举值会被解析成0,第二个枚举值会被解析成1,以此类推。枚举类型无法与其他类型进行隐式转换,只能进行显示转换。
Weekend weekend = Weekend(0);
上面代码将数值0
转换成枚举类型,对应Weekend.Monday
枚举值。如果数值超过了最大枚举值,则运行合约时显式转换报错。
3.2.5 映射
与Java的Map集合类型,映射类型用于存储的是一对有关系的数据,其定义格式为:
mapping(_keyType => _valueType) 变量名;
_keyType不可以是映射、变长数组、枚举、结构体类型;_valueType可以是任意类型。
在映射中,实际上存储的是key的keccak256哈希值。映射没有长度,也没有key和value的集合概念。
如果局部变量使用mapping类型,那么该变量的数据位置必须是storage,并且必须要对该变量进行初始化。
contract SimpleContract {
mapping(string => string) m1;
function test() public {
mapping(string => string) storage m2 = m1;
}
}
3.2.6 元组类型
元组类似于定长数组,它是一个元素数量固定的对象列表。但是与数组不同的是,元组中元素类型可以不一样。一般来说,可以在函数中通过元组返回多个值,使用元素类型的变量来接收函数的返回值。
function f() public pure returns(uint, bool) {
return (10, false);
}
function test() public pure {
(uint a, bool b) = f();
}
元组之间可以进行赋值。
function test() public pure {
uint x = 10;
uint y = 20;
(x, y) = (y, x);
}
上面代码通过元组之间赋值交互x和y的值。
如果元组只有一个元素,那么元素后的逗号不能省略。
function f() public pure returns(uint, bool) {
return (10, false);
}
function test() public pure {
(uint a, ) = f();
}
四、运算符
4.1 基本运算符
- 算数运算符:
+ - * / % ++ --
- 逻辑运算符:
&& || !
- 比较运算符:
> < >= <= == !=
- 位运算符:
& | ^(异或) ~(取反) <<(左移) >>(右移) >>>(右移后左边补0)
- 赋值运算符:
= += -= *= /= %=
- 三目运算符:
condition expression ? value1 : value2
4.2 左值变量
左值变量lvalue
,即一个可以赋值给它的变量。如果表达式中包含左值变量,其运算符都可以简写。比如:
function test() public {
int a = 10;
int b = 5;
a += b;a -= b;a *= b;a /= b;
a++;a--;
}
与左值紧密相关的一个关键字delete
。比如说delete a
,如果a是一个整型,那么delete a
代表将a的值恢复成初始状态;如果a是动态数组,那么delete a
相当于将数组的长度length设置为0;如果a是静态数组,那么delete a
会将数组中每个元素进行重置;如果a是结构体类型,delete a
会将结构体中每个属性进行重置。
注意:delete对映射无效。
4.3 类型转换
如果两个相同类型但精度不同的变量进行运算,那么低精度会自动转换成高精度的类型。
function test() public {
int8 a = 10;
int b = 20;
int c = a + b;
}
上面代码中变量a的类型是int8,变量b的类型是int256,因此它们两个变量相加,低精度会自动转换成高精度,因此得到的结果是int256。同样地,低精度变量也可以赋给高精度变量,所以下面代码也是没问题的。
function test() public {
int8 a = 10;
int b = a;
}
但是如果需要将高精度转换成低精度的类型,那么就需要强制类型转换,如下所示:
function test() public {
int a = 10;
int8 b = int8(a);
}
需要注意的是,显式类型转换可能会导致一些无法预料的结果,比如说:
function test() public {
int a1 = -10;
uint a2 = uint(a); // 115792089237316195423570985008687907853269984665640564039457584007913129639926
uint32 b1 = 0x12345678;
uint16 b2 = uint16(b1); // 22136,即0x5678的十进制表示形式
}
运行上面代码,最终结果与我们的预期不一样。因此如果使用显式类型转换时候,必须要清楚转换的过程。
与javascript类似,定义变量时候也可以使用var关键字,比如说:
int a = 10;
var b = a;
上面代码中,因为变量a的类型为int256,因此变量b的类型也是int256。而且一旦变量b的类型确定后,它的类型就不会再发生改变。因此下面程序编译失败:
bool c = false;
b = c;
另外,solidity不支持对函数形参或反参使用var关键字。
五、流程控制
solidity支持javascript的大部分语法,比如if…else、while…do、do…while、for等等。
pragma solidity ^0.4.16;
contract SimpleContract {
function test1(int week) public pure returns(string) {
if (week == 1) {
return "Monday";
} else if (week == 2) {
return "Tuesday";
} else if (week == 3) {
return "Wednesday";
} else if (week == 4) {
return "Thursday";
} else if (week == 5) {
return "Friday";
} else if (week == 6) {
return "Saturday";
} else if (week == 7) {
return "Sunday";
} else {
return "invalid week";
}
}
function test2() public pure returns(int) {
int sum = 0;
for (int i = 0; i < 100; i++) {
sum += i;
}
return sum;
}
function test3() public pure returns(int) {
int i = 0;
int sum = 0;
while (i < 100) {
sum += i;
i++;
}
return sum;
}
function test4() public pure returns(int) {
int i = 0;
int sum = 0;
do {
if (i % 2 == 0) continue;
sum += i;
i++;
} while(i < 100);
return sum;
}
}
小结
这部分主要介绍了solidity语言的基础语法,下一部分继续介绍solidity的高级语法。
更多推荐
所有评论(0)