0x00 数据类型
值类型
fixed/unfixed
:有符号和无符号的定长浮点型fixedMxN
:带符号的定长浮点型,其中M表示按类型取的位数,N表示小数点。M应该能被8整除,N可以是0到80。fixed
默认为fixed128x18
。ufixedMxN
:无符号的定长浮点型,其中M表示按类型取的位数,N表示小数点。
地址类型
地址类型表示以太坊地址,长度为20字节。地址可以使用.balance
放啊获得余额,也可以使用.transfer
方法将余额转到另一个地址。
0x01 变量
Solidity支持三种类型的变量:
- 状态变量:变量值永久保存在合约存储空间中的变量;
- 局部变量:变量值仅在函数执行过程中有效的变量,函数退出后变量失效。
- 全局变量:保存在全局命名空间,用于获取区块链相关信息的特殊变量。
Solidity是一种静态类型语言,这意味着需要在声明期间指定变量类型。每个变量声明时都有一个基于其类型的默认值,不存在undefined
和null
的概念。
0x02 变量作用域
状态变量可以有三种作用域类型:
- Public:公共状态变量可以在内部访问,也可以通过消息访问。对于公共状态变量,将自动生成一个
getter
函数。 - Internal:内部状态变量只能从当前合约或其派生合约内访问。
- Private:私有状态变量只能从当前合约内部访问,派生合约内不能访问。
0x03 数据位置(data location)
Solidity提供四种类型的数据位置:
- Storage
- Memory
- Calldata
- Stack
Storage
该存储位置存储永久数据,这意味着该数据可以被合约中的所有函数访问。可类比为计算机的硬盘数据,所有数据都永久存储。与其他数据位置相比,存储区数据位置的成本较高。
Memory
Memory是临时数据,比存储位置便宜,只能在函数中访问。可类比成每个单独函数的内存RAM。
Calldata
Calldata是不可修改的非持久性数据位置,所有传递给函数的值都存储在这里。此外,Calldata是外部函数参数的默认位置。
Stack
堆栈是由EVM维护的非持久性数据。EVM使用堆栈数据位置在执行期间加载变量,该位置最多有1024个级别限制。
0x04 变量数据位置规则
状态变量
状态变量总是存储在Storage中,不能显式地标记状态变量的位置。1
2
3
4
5
6
7
8
9
10
11pragam solidity ^0.5.0;
contract DataLocation {
// storage
uint stateVariable;
uint[] stateArray;
uint storage stateVariable; // Error
uint[] memory stateArray; // Error
}
函数参数与返回值
函数参数包括返回参数都存储在Memory中。
局部变量
值类型的局部变量存储在Memory中。但是对于引用类型,需要显式地指定数据位置。不能显式地覆盖具有值类型的局部变量。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25pragma solidity ^0.5.0;
contract Locations {
/* 此处是状态变量 */
// 存储在Storage中
bool flag;
uint number;
address account;
function doSomething() public {
/* 此处是局部变量 */
// 由于是值类型,故被存储在Memory中
bool flag2;
uint number2;
address account2;
// 引用类型需要显式指定数据位置
uint[] memory localArray;
// 不能显式覆盖具有值类型的局部变量
bool memory flag2; // Error
uint Storage number2; // Error
}
}
外部函数的参数
外部函数的参数(不包括返回参数)存储在Calldata中。
0x05 赋值的数据位置规则
- 将一个状态存储变量赋值给另一个状态存储变量,将创建一个新的副本。
- 从内存变量复制到存储变量,总是会创建一个新的副本。
- 从存储变量复制到内存变量,将创建一个副本。
- 对于引用类型的局部变量,从一个内存变量复制到另一个内存变量不会创建副本。而对于值类型的局部变量仍然创建一个新副本。
0x06 经典结构
字符串
可以使用string()
构造函数将bytes转换为字符串。1
2bytes memory bstr = new bytes(10);
string message = string(bstr);
数组
对于Storage数组,元素类型可以是任意的,而对于Memory数组,元素类型不能是映射类型,若它是一个公共函数的参数,那么元素类型必须是ABI类型。相比于byte[]
,bytes
应优先使用。
可以使用new
关键字在内存中创建动态数组。与存储数组相反,不能通过设置.length
成员来调整内存动态数组的长度。
结构体
使用struct
关键字定义结构体,包含多个成员。使用成员访问操作符.
访问结构的任何成员。
映射
与数组和结构体一样,映射也属于引用类型。声明语法如下:1
mapping(_KeyType => _ValueType)
其中,_KeyType
可以是任何内置类型,但不允许使用引用类型或复杂对象,_ValueType
是任何类型。
注意,映射的数据位置只能是Storage,通常用于状态变量。映射可标记为public
,Solidity将自动为它创建getter。
特殊变量/全局变量
函数
函数是一组可重用代码的包装,接受输入,返回输出。
Solidity中,定义函数的语法如下,函数由关键字function
声明,后面跟函数名、参数、可见性和返回值的定义。1
2
3function function-name(parameter-list) scope returns(){
}
Solidity中,函数可以返回多个值。
函数修饰符
函数修饰符用于修改函数的行为,可以创建带参数修饰符和不带参数修饰符:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15contract Owner {
// 定义修饰符 onlyOwner 不带参数
modifier onlyOwner {
require(msg.sender == owner);
_;
}
// 定义修饰符 costs 带参数
modifier costs(uint price) {
if (msg.value >= price) {
_;
}
}
}
View函数
视图函数不会修改状态。可以通过在函数声明中添加view
关键字来声明视图函数,getter方法是默认的视图函数。
Pure函数
纯函数不读取或修改状态。若发生错误,纯函数可以使用revert()
和require()
函数来还原潜在的状态更改。可以通过在函数声明中添加pure
关键在来声明纯函数。
Fallback函数
回退函数是合约中的特殊函数,具有以下特点:
- 当合约中不存在的函数被调用时,将调用fallback函数。
- 被标记为外部函数。
- 它没有名字,没有参数,不能返回任何东西。
- 每个合约定义一个fallback函数。
- 如果没有被标记为payable,则当合约收到无数据的以太币转账时,将抛出异常。
语法如下:1
2
3
4// 没有名字,没有参数,不返回,标记为external,可以标记为payable
function() external {
// statements
}
函数重载
同一作用域内,相同函数名可定义多个函数。这些函数的参数(参数类型或参数数量)必须不同,仅返回值不同不被允许。
数学函数与加密函数
0x07 常用模式
Withdrawal Mode
当在智能合约中,直接向一个地址转账时,如该地址是一个合约地址,合约中可以编写代码,拒绝接受付款,导致交易失败。为避免这种情况,通常会使用提款模式。提款模式是让收款方主动来提取款项,而不是直接转账给收款方。
Restricted Visit
使用限制访问修饰符可以限制合约状态修改者或调用合约函数,常用限制访问操作如下:
- onlyBy:根据地址限制函数的调用者;
- onlyAfter:限制该函数只能在特定的时间段之后调用;
- costs:调用方法只能在提供特定值的情况下完成。
0x08 智能合约
Solidity中,合约类似于类,包含以下部分: - 构造函数:使用
constructor
关键字声明的特殊函数,每个合约执行一次,在创建合约时调用。 - 状态变量:用于存储合约状态的变量。
- 函数:智能合约中的函数,可以修改状态变量来改变合约的状态。
可见性
合约中的函数和变量具有可见性,包括以下四种关键词:
external
:外部函数由其他合约调用,要在合约内部调用外部函数可以使用this.function_name()
的方式。状态变量不能标记为外部变量。public
:公共函数/变量可以在外部和内部直接使用。对于公共状态变量,Solidity为其自动创建一个getter函数。internal
:内部函数/变量只能在内部或派生合约中使用。private
:私有函数/变量只能在内部使用,派生合约无法使用。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45pragma solidity ^0.5.0;
contract C {
//private state variable
uint private data;
//public state variable
uint public info;
//constructor
constructor() public {
info = 10;
}
//private function
function increment(uint a) private pure returns(uint) { return a + 1; }
//public function
function updateData(uint a) public { data = a; }
function getData() public view returns(uint) { return data; }
function compute(uint a, uint b) internal pure returns (uint) { return a + b; }
}
//External Contract
contract D {
function readData() public returns(uint) {
C c = new C();
c.updateData(7);
return c.getData();
}
}
//Derived Contract
contract E is C {
uint private result;
C private c;
constructor() public {
c = new C();
}
function getComputedResult() public {
result = compute(3, 5);
}
function getResult() public view returns(uint) { return result; }
function getData() public view returns(uint) { return c.info(); }
}
继承
Solidity中的合约继承可类比面向对象语言中的类继承,支持单继承和多继承。继承的主要特点为:
- 派生合约可以访问父合约的所有非私有成员,包括内部方法和状态变量。但是不允许使用
this
。 - 如果函数签名保持不变,则允许函数重写。如果输出参数不同,编译将失败。
- 可以使用
super
关键字或父合同名称调用父合同的函数。 - 在多重继承的情况下,使用
super
的父合约函数调用,优先选择被最多继承的合约。
构造函数
构造函数是使用construct
关键字声明的特殊函数,用于初始化合约的状态变量,构造函数是可选的,可以省略。
构造函数具有以下重要特性:
- 一个合约只能有一个构造函数。
- 构造函数在创建合约时执行一次,用于初始化合约状态。
- 在执行构造函数之后,合约最终代码被部署到区块链。合约最终代码包括公共函数和可通过公共函数访问的代码。构造函数代码或仅由构造函数使用的任何内部方法不包括在最终代码中。
- 构造函数可以是公共的,也可以是内部的。
- 内部构造函数将合约标记为抽象合约。
- 如果没有定义构造函数,则使用默认构造函数。
- 如果基合约具有带参数的构造函数,则每个派生/继承的合约也都必须包含参数。
- 不允许直接或间接地初始化基合约构造函数。
- 如果派生合约没有将参数传递给基合约构造函数,则派生合约将成为抽象合约。
抽象合约
类似Java中的抽象类,抽象合约至少包含一个没有实现的函数。通常抽象合约作为父合约,被用来继承,在继承合约中实现抽象函数。抽象合约也可以包含有实现的函数。
若派生合约没有抽象函数,则该派生合约也将被标记为抽象合约。
接口
接口类似于抽象合约,使用interface
关键字创建,只能包含抽象函数,不能包含函数实现。接口关键特性如下:
- 接口的函数只能是外部类型;
- 接口不能有构造函数;
- 接口不能有状态变量。
- 接口可以包含
enum
,struct
定义,可以使用interface_name
来访问它们。
库
库的主要作用是代码重用,库中包含了可以被合约调用的函数。主要特征如下:
- 如果库函数不修改状态,则可以直接调用它们。这意味着纯函数或视图函数只能从库外部调用。
- 库不能被销毁,因为它被认为是无状态的。
- 库不能有状态变量,不能继承任何其他元素,不能被继承。
Using For
using A for B
指令可用于将库A的函数附加到给定类型B。这些函数将把调用者类型作为第一个参数(使用self
标识)。
内联汇编
使用内联汇编,可以在Solidity源程序中嵌入汇编代码,对EVM具有更细粒度的控制,在编写库函数时很有用。
汇编代码嵌入使用以下语法:1
assembly { ... }
举例如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25pragma solidity ^0.5.0;
library Sum {
function sumUsingInlineAssembly(uint[] memory _data) public pure returns (uint o_sum) {
for (uint i = 0; i < _data.length; ++i) {
assembly {
o_sum := add(o_sum, mload(add(add(_data, 0x20), mul(i, 0x20))))
}
}
}
}
contract Test {
uint[] data;
constructor() public {
data.push(1);
data.push(2);
data.push(3);
data.push(4);
data.push(5);
}
function sum() external view returns(uint){
return Sum.sumUsingInlineAssembly(data);
}
}
事件
事件是智能合约发出的信号,可以被索引,以便以后可以搜索事件记录。
Solidity中,可以使用event
关键字定义事件,然后可以在函数中使用emit
关键字触发事件:1
2
3
4
5// 声明一个事件
event Deposit(address indexed _from, bytes32 indexed _id, uint _value);
// 触发事件
emit Deposit(msg.sender, _id, msg.value);
按照惯例,事件名称以大写字母开头,以区别于函数。一个事件最多有3个参数可以标记为索引。可以使用索引参数有效地过滤事件。
事件构建在Ethereum中,底层的日志接口之上,具有以下局限性:
- 日志结构最多有四个主题和一个数据字段;
- 日志包括记录在日志中的事件,不能从Ethereum虚拟机中访问,意味着合约无法读取自己的或其他合约的日志及事件。
关于事件总结如下: - Solidity 提供了一种记录交易期间事件的方法。
- 智能合约前端(DApp)可以监听这些事件。
- 索引(indexed)参数为过滤事件提供了一种高效的方法。
- 事件受其构建基础日志机制的限制。
错误处理
错误处理使用以下一些重要方法:
assert(bool condition)
:如果不满足条件,此方法调用将导致一个无效的操作码,对状态所做的任何更改将被还原。这个方法是用来处理内部错误的。require(bool condition)
:如果不满足条件,此方法调用将恢复到原始状态。此方法用于检查输入或外部组件的错误。require(bool condition, string memory message)
:如果不满足条件,此方法调用将恢复到原始状态。此方法用于检查输入或外部组件的错误。它提供了一个提供自定义消息的选项。revert()
:此方法将中止执行并将所做的更改还原为执行前状态。revert(string memory reason)
:此方法将中止执行并将所做的更改还原为执行前状态。它提供了一个提供自定义消息的选项。