Renaissance Labs

Posted on Mar 01, 2022Read on Mirror.xyz

Learn Solidity Series 9: basis tips

Version claim

合约文件开头需要声明编译器的版本号,目的是为了该合约在未来版本的升级中引入了不兼容的编译器,其语法为:

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;

Code note

与Java类似,使用双斜杠//代表单行注释。在双斜杠中间添加星号代表多行注释,比如:

// 单行注释

/*
多行注释
多行注释
...
*/

也可以添加文档注释,其语法为:

/**
* @dev 
* @param
* @return 
*/
// eg:
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;
    }

} 

Data type

boolean

布尔类型使用bool关键字。它的值是true或false。布尔类型支持的运算符有:

名称 符号

逻辑与&&

逻辑或||

逻辑非!

等于==

不等于!=

Integer

整型使用int或uint表示。uint代表的是无符号整型。它们都支持uint8、uint16,…, uint256(以8位为步长递增)。如果没有指定多少位,默认为int256或uint256。

整型支持的运算符:

名称 符号

比较运算符<= < == != >= >

位运算符& | ^ ~

算术运算符+ - * / ++ -- % ** << >>

注意:如果除数为0或者对0取模都会引发运行时异常。

fixed float

定长浮点型使用fixed或ufixed表示。但是目前solidity支持声明定长浮点型的变量,不能够对该类型的变量进行赋值。

address type

地址类型的关键字使用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状态变量的改变做好准备。

fixed byte array

定长字节数组使用bytes1、bytes2、…、bytes32来表示。如果没有指定长度,默认为bytes1。定长字节数组支持的运算符有:

名称 符号

比较运算符 <= < == != >= >

位运算符 & | ^ ~ << >>

索引访问 x[i]代表访问第i个字节的数据

定长字节数组包含length属性,用于查询字节数组的长度。虽然可以使用byte[]表示字节数组,但是一般推荐使用bytes代替。

constant

  1. 地址字面常量

    像0x692a70d2e424a56d2c6c27aa97d1a86395877b3a这样通过了地址校验和测试的十六进制字面常量属于address类型。

  2. 数值字面常量

    solidity支持整数和任意精度小数的字面常量。1.或.1都是有效的小数字面常量。也可以使用科学计数法表示,比如:2e10。如果数值字面常量赋值给了数值类型的变量,就会转换成非字面常量类型。

uint128 a = 2.5 + 1 + 0.5; // 编译成功
uint128 b = 1; 
uint128 c = 2.5 + b + 0.5; // 编译报错

上面代码的第二行,将数值字面常量1赋值给了变量b。因为变量b是非字面常量,不同类型的字面常量和非字面常量无法进行运算,所以第三行报错。上面代码的第二行,将数值字面常量1赋值给了变量b。因为变量b是非字面常量,不同类型的字面常量和非字面常量无法进行运算,所以第三行报错。

  1. 字符串字面常量

字符串字面常量使用单引号或双引号引起来的一串字符。字符串字面常量可以隐式地转换成bytes类型。比如:

bytes3 b = "abc";
bytes4 b = "abcd";
bytes4 b = "abcde"; // 报错,因为bytes4只能存储4个字节数据

function type

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);
    }
    
}

Other type

array

如果声明数组的时候指定数组长度,那么数组的大小就固定下来;如果声明数组时候没有指定长度,那么该数组的长度可以发生变化。

// 定义固定长度的数组
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函数,它会返回一些动态内容。

Tips:可以把bytes看作是byte的数组形式,也可以将bytes当作string类型使用。但是,string无法通过length或索引来访问字符串中的每个字符,如果需要访问字符串中的某个字符,可以先把字符串转换成bytes形式,比如:uint size = bytes(“123”).length; byte c = bytes(“123”)[0]。

Struct

可以使用结构体用来存储复杂的数据结构。其定义格式:

struct 结构体名称 {
    变量类型 变量名;
    ...
}

结构体中变量类型可以是任意类型,但是它不能够包含自身。

如果函数中将结构体类型数据赋给一个局部变量,这个过程并没有创建这个结构体对象的副本,而且将该结构体对象的引用赋给了局部变量(即引用传递)。

Enum

与结构体类似的是,枚举也是solidity中的一种自定义类型,其定义格式为:

enum 类型名称 {
    枚举值1,
    枚举值2,
    ...
}

枚举类型中至少要包含一个枚举值。枚举值可以是任意有效的标识符(比如说:必须是字母或下划线开头)。

enum Week {
    Monday,
    Tuesday,
    ...
}

枚举值的类型默认为uint8。第一个枚举值会被解析成0,第二个枚举值会被解析成1,以此类推。枚举类型无法与其他类型进行隐式转换,只能进行显示转换。

Weekend weekend = Weekend(0);

上面代码将数值0转换成枚举类型,对应Weekend.Monday枚举值。如果数值超过了最大枚举值,则运行合约时显式转换报错。

mapping

与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;
    }
}

tuple

元组类似于定长数组,它是一个元素数量固定的对象列表。但是与数组不同的是,元组中元素类型可以不一样。一般来说,可以在函数中通过元组返回多个值,使用元素类型的变量来接收函数的返回值。

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();
}

operator

算数运算符:+ - * / % ++ --
逻辑运算符:&& || !
比较运算符:> < >= <= == !=
位运算符:& | ^(异或) ~(取反) <<(左移) >>(右移) >>>(右移后左边补0)
赋值运算符:= += -= *= /= %=
三目运算符:condition expression ? value1 : value2

lvalue

左值变量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对映射无效。

Type convert

如果两个相同类型但精度不同的变量进行运算,那么低精度会自动转换成高精度的类型。


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关键字。

Flow control

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;
    }  
    
}