Solidity8.0-入门

学习资料

1
2
3
4
5
6
7
https://wtf.academy/solidity-start/HelloWeb3
https://www.osgeo.cn/solidity/index.html
https://www.bilibili.com/video/BV1oZ4y1B7WS/?spm_id_from=333.337.search-card.all.click&vd_source=c11905ea80357896daa3f5d07f472904


开发工具:
https://remix.ethereum.org/

Solidity简述

Solidity是以太坊虚拟机(EVM)智能合约的语言。同时solidity是玩链上项目必备的技能:区块链项目大部分是开源的,如果你能读懂代码,就可以规避很多亏钱项目。

Solidity具有两个特点:

  • 基于对象:学会之后,能帮你挣钱找对象。
  • 高级:不会solidity,在币圈显得很low。

进入remix,我们可以看到最左边的菜单有三个按钮,分别对应文件(写代码的地方),编译(跑代码),部署(部署到链上)。

我们点新建(Create New File)按钮,就可以创建一个空白的solidity合约。

hello world

基础语法

1
2
3
4
5
6
//注释使用//
// SPDX-License-Identifier: GPL-3.0 //版权声明,注释形式比如MIT,版权开源协议(不写会警告)
pragma solidity >=0.7.0 <0.9.0; //声明合约文件的版本,^表示X.X.X以上的版本都可以编译,不跨越大版本也支持大于小于,不写其他符号就只能使用指定版本
contract HelloWorld{ //声明合约
string public myString="whoami"; //定义变量,关键词为string,可视范围public(公开可视),定义之后是一个只读的方法
}

编译和发布

使用在线网站对源代码编辑和修改之后可以编译和部署

1
https://remix.ethereum.org/

在上一步操作过程中编译成功之后,下面开始部署,每一份智能合约都会经历编译部署环节。

当最后的按钮是蓝色的表示是一个只读的方法,点击按钮即可得到变量的值。

变量的作用域

数据位置

solidity数据存储位置有三类:storagememorycalldata。不同存储位置的gas成本不同。storage类型的数据存在链上,类似计算机的硬盘,消耗gas多;memorycalldata类型的临时存在内存里,消耗gas少。大致用法:

  1. storage:合约里的状态变量默认都是storage,存储在链上。
  2. memory:函数里的参数和临时变量一般用memory,存储在内存中,不上链。
  3. calldata:和memory类似,存储在内存中,不上链。与memory的不同点在于calldata变量不能修改(immutable),一般用于函数的参数。例子:
1
2
3
4
5
function fCalldata(uint[] calldata _x) public pure returns(uint[] calldata){
//参数为calldata数组,不能被修改
// _x[0] = 0 //这样修改会报错
return(_x);
}

数据位置和赋值规则

在不同存储类型相互赋值时候,有时会产生独立的副本(修改新变量不会影响原变量),有时会产生引用(修改新变量会影响原变量)。规则如下:

  • storage(合约的状态变量)赋值给本地storage(函数里的)时候,会创建引用,改变新变量会影响原变量。例子:
1
2
3
4
5
6
7
uint[] x = [1,2,3]; // 状态变量:数组 x

function fStorage() public{
//声明一个storage的变量 xStorage,指向x。修改xStorage也会影响x
uint[] storage xStorage = x;
xStorage[0] = 100;
}
  • storage赋值给memory,会创建独立的复本,修改其中一个不会影响另一个;反之亦然。例子:
1
2
3
4
5
6
7
8
9
10
uint[] x = [1,2,3]; // 状态变量:数组 x

function fMemory() public view{
//声明一个Memory的变量xMemory,复制x。修改xMemory不会影响x
uint[] memory xMemory = x;
xMemory[0] = 100;
xMemory[1] = 200;
uint[] memory xMemory2 = x;
xMemory2[0] = 300;
}
  • memory赋值给memory,会创建引用,改变新变量会影响原变量。

  • 其他情况,变量赋值给storage,会创建独立的复本,修改其中一个不会影响另一个。

变量

Solidity中变量按作用域划分有三种,分别是状态变量(state variable),局部变量(local variable)和全局变量(global variable)

状态变量

状态变量是数据存储在链上的变量,所有合约内函数都可以访问 ,gas消耗高,状态变量在合约内、函数外声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.7;
contract Variables {
uint public state1;//定义变量
uint public state2=123;//初始化,给予初始值
//如果不写修改方法,myuint2将会一直是123,任何时候去读取都是固定值
//定义一个函数,external外部可见
function foo() external{
//在函数中定义的变量只有在调用这个函数的时候才会在虚拟机中产生
//在函数内部的变量叫局部变量
uint notstate = 456;
}
}

只要不写修改的方法则会永远保存在链上。(类似C全局变量)

局部变量

局部变量是仅在函数执行过程中有效的变量,函数退出后,变量无效。局部变量的数据存储在内存里,不上链,gas低。局部变量在函数内声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.7;
contract Variables {
uint public state1;//定义变量
uint public state2=123;//初始化,给予初始值
address public addr;
//如果不写修改方法,myuint2将会一直是123,任何时候去读取都是固定值
//定义一个函数,external外部可见
function foo() external {
//在函数中定义的变量只有在调用这个函数的时候才会在虚拟机中产生
//在函数内部的变量叫局部变量
state1=789;
addr=address(1);
}
}

上述代码部署之后可以看到一个函数和部分值的初始值

点击黄色的函数名称按钮代表写入方法,会改变链的状态

可以看到载入方法之后,状态变量的值发生变化,局部变量不会有变化而且不会产生数据

全局变量

全局变量是全局范围工作的变量,都是solidity预留关键字。不用定义就能显示内容的变量。他们可以在函数内不声明直接使用:

1
2
3
4
5
6
7
8
9
10
11
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.7;
contract Variables {
function global() external view returns(address,uint, uint, bytes memory){
address sender = msg.sender;//账户内容,上一个调用这个函数的地址,可能是人或合约
uint timestamp= block.timestamp;//当前时间时间戳
uint blockNum = block.number;//当前区块编号
bytes memory data = msg.data;
return(sender,timestamp, blockNum, data);
}
}

保存部署之后点击按钮即可查看全局变量的值

在上面例子里,我们使用了3个常用的全局变量:msg.sender, block.numbermsg.data,他们分别代表请求发起地址,当前区块高度,和请求数据。下面是一些常用的全局变量,更完整的列表请看这个链接

  • blockhash(uint blockNumber): (bytes32)给定区块的哈希值 – 只适用于256最近区块, 不包含当前区块。
  • block.coinbase: (address payable) 当前区块矿工的地址
  • block.gaslimit: (uint) 当前区块的gaslimit
  • block.number: (uint) 当前区块的number
  • block.timestamp: (uint) 当前区块的时间戳,为unix纪元以来的秒
  • gasleft(): (uint256) 剩余 gas
  • msg.data: (bytes calldata) 完整call data
  • msg.sender: (address payable) 消息发送者 (当前 caller)
  • msg.sig: (bytes4) calldata的前四个字节 (function identifier)
  • msg.value: (uint) 当前交易发送的wei

变量初始值

solidity中,声明但没赋值的变量都有它的初始值或默认值。

值类型初始值

  • boolean: false

  • string: ""

  • int: 0

  • uint: 0

  • enum: 枚举中的第一个元素

  • address: 0x0000000000000000000000000000000000000000 (或 address(0))

  • ```
    function

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    - `internal`: 空白方程
    - `external`: 空白方程

    可以用`public`变量的`getter`函数验证上面写的初始值是否正确:

    ```solidity
    bool public _bool; // false
    string public _string; // ""
    int public _int; // 0
    uint public _uint; // 0
    address public _address; // 0x0000000000000000000000000000000000000000

    enum ActionSet { Buy, Hold, Sell}
    ActionSet public _enum; // 第一个元素 0

    function fi() internal{} // internal空白方程
    function fe() external{} // external空白方程

引用类型初始值

  • 映射mapping: 所有元素都为其默认值的mapping
  • 结构体struct: 所有成员设为其默认值的结构体
  • 数组array
    • 动态数组: []
    • 静态数组(定长): 所有成员设为其默认值的静态数组

可以用public变量的getter函数验证上面写的初始值是否正确:

1
2
3
4
5
6
7
8
9
10
// Reference Types
uint[8] public _staticArray; // 所有成员设为其默认值的静态数组[0,0,0,0,0,0,0,0]
uint[] public _dynamicArray; // `[]`
mapping(uint => address) public _mapping; // 所有元素都为其默认值的mapping
// 所有成员设为其默认值的结构体 0, 0
struct Student{
uint256 id;
uint256 score;
}
Student public student;

delete操作符

delete a会让变量a的值变为初始值。

1
2
3
4
5
// delete操作符
bool public _bool2 = true;
function d() external {
delete _bool2; // delete 会让_bool2变为默认值,false
}

变量被声明但没有赋值的时候,它的值默认为初始值。不同类型的变量初始值不同,delete操作符可以删除一个变量的值并代替为初始值。

常数

solidity中两个关键字,constant(常量)和immutable(不变量)。状态变量声明这个两个关键字之后,不能在合约后更改数值;并且还可以节省gas。另外,只有数值变量可以声明constantimmutablestringbytes可以声明为constant,但不能为immutable。让不应该变的变量保持不变。这样的做法能在节省gas的同时提升合约的安全性。

常量(constant)

constant变量必须在声明的时候初始化,之后再也不能改变。尝试改变的话,编译不通过。

1
2
3
4
5
// constant变量必须在声明的时候初始化,之后不能改变
uint256 constant CONSTANT_NUM = 10;
string constant CONSTANT_STRING = "0xAA";
bytes constant CONSTANT_BYTES = "WTF";
address constant CONSTANT_ADDRESS = 0x0000000000000000000000000000000000000000;

不变量(immutable)

immutable变量可以在声明时或构造函数中初始化,因此更加灵活。

1
2
3
4
5
// immutable变量可以在constructor里初始化,之后不能改变
uint256 public immutable IMMUTABLE_NUM = 9999999999;
address public immutable IMMUTABLE_ADDRESS;
uint256 public immutable IMMUTABLE_BLOCK;
uint256 public immutable IMMUTABLE_TEST;

你可以使用全局变量例如address(this)block.number ,或者自定义的函数给immutable变量初始化。在下面这个例子,我们利用了test()函数给IMMUTABLE_TEST初始化为9

1
2
3
4
5
6
7
8
9
10
11
// 利用constructor初始化immutable变量,因此可以利用
constructor(){
IMMUTABLE_ADDRESS = address(this);
IMMUTABLE_BLOCK = block.number;
IMMUTABLE_TEST = test();
}

function test() public pure returns(uint256){
uint256 what = 9;
return(what);
}

小结

要求实现计数器的智能合约,对状态变量加或者减操作

1
2
3
4
5
6
7
8
9
10
11
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.7;
contract Counter {
uint public count;
function inc() external {
count +=1;
}
function dec() external {
count -=1;
}
}

编译部署之后可以看到两个橙色按钮,点击即可实现对count数值变化

数据类型

Solidity中的变量类型

  • 数值类型(Value Type):包括布尔型,整数型等等,这类变量赋值时候直接传递数值。

  • 引用类型(Reference Type):包括数组和结构体,这类变量占空间大,赋值时候直接传递地址(类似指针)。

  • 映射类型(Mapping Type): Solidity里的哈希表。

  • 函数类型(Function Type):Solidity文档里把函数归到数值类型,但我觉得他跟其他类型差别很大,所以单独分一类。

数值类型

布尔型(bool)

布尔型是二值变量,取值为true或false。

1
2
// 布尔值
bool public _bool = true;

布尔值的运算符,包括:

  • ! (逻辑非)
  • && (逻辑与, “and” )
  • || (逻辑或, “or” )
  • == (等于)
  • != (不等于)
1
2
3
4
5
6
// 布尔运算
bool public _bool1 = !_bool; //取非
bool public _bool2 = _bool && _bool1; //与
bool public _bool3 = _bool || _bool1; //或
bool public _bool4 = _bool == _bool1; //相等
bool public _bool5 = _bool != _bool1; //不相等

上面的代码中:变量_bool的取值是true_bool1_bool的非,为false_bool && _bool1false_bool || _bool1true_bool == _bool1false_bool != _bool1true

值得注意的是:&&||运算符遵循短路规则,这意味着,假如存在f(x) || g(y)的表达式,如果f(x)trueg(y)不会被计算,即使它和f(x)的结果是相反的

整型(int)

整型是solidity中的整数,最常用的包括

1
2
3
4
// 整型
int public _int = -1; // 整数,包括负数
uint public _uint = 1; // 正整数
uint256 public _number = 20220330; // 256位正整数

常用的整型运算符包括:

  • 比较运算符(返回布尔值): <=<==!=>=>
  • 算数运算符: +-, 一元运算 -+*/%(取余),**(幂)
1
2
3
4
5
// 整数运算
uint256 public _number1 = _number + 1; // +,-,*,/
uint256 public _number2 = 2**2; // 指数
uint256 public _number3 = 7 % 2; // 取余数
bool public _numberbool = _number2 > _number3; // 比大小

地址类型(address)

地址类型(address)存储一个 20 字节的值(以太坊地址的大小)。地址类型也有成员变量,并作为所有合约的基础。有普通的地址和可以转账ETH的地址(payable)。payable的地址拥有balance和transfer()两个成员,方便查询ETH余额以及转账。

1
2
3
4
5
// 地址
address public _address = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71;
address payable public _address1 = payable(_address); // payable address,可以转账、查余额
// 地址类型的成员
uint256 public balance = _address1.balance; // balance of address

定长字节数组(bytes)

字节数组bytes分两种,一种定长(byte, bytes8, bytes32),另一种不定长。定长的属于数值类型,不定长的是引用类型(之后讲)。 定长bytes可以存一些数据,消耗gas比较少。

1
2
3
4
// 固定长度的字节数组
bytes32 public _byte32 = "MiniSolidity";
bytes1 public _byte = _byte32[0];
//_byte变量存储_byte32的第一个字节,为0x4d。

MiniSolidity变量以字节的方式存储进变量_byte32,转换成16进制为:0x4d696e69536f6c69646974790000000000000000000000000000000000000000

枚举 (enum)

枚举(enum)是solidity中用户定义的数据类型。它主要用于为uint分配名称,使程序易于阅读和维护。它与C语言中的enum类似,使用名称来代替从0开始的uint:

1
2
3
4
// 用enum将uint 0, 1, 2表示为Buy, Hold, Sell
enum ActionSet { Buy, Hold, Sell }
// 创建enum变量 action
ActionSet action = ActionSet.Buy;

它可以显式的和uint相互转换,并会检查转换的正整数是否在枚举的长度内,不然会报错:

1
2
3
4
// enum可以和uint显式的转换
function enumToUint() external view returns(uint){
return uint(action);
}

enum的一个比较冷门的变量,几乎没什么人用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.7;
contract ValueType{
bool public b = true; //公开可见的bool值,变量名为b,值为true或者false

uint public u = 123; //无符号整数,正整数,默认为uint256,值为0到2**256-1;
//uint8,值为0到2**8-1;
//uint16,值为0到2**16-1;除此之外uint32,uint40,unint48,往下面+8都有的

int public i =-123; //正负数都可以表示,默认为int256,值为-2**255到2**255-1
//uint8,值为-2**7到2**7-1;

//(为什么是255,如果是256将会翻倍,所有这样的话int和uint在内存中的空间其实一样大)

int public minint=type(int).min; //查看有符号的整数的最小值
int public maxint=type(int).max; //查看有符号的整数的最大值
//有符号的整数会落在上面这个区间之间

address public addr=0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
//地址类型//16进制数值,比较短,可以算出来

bytes32 public b32=0x33cc998f29bb694187d0944de3a196c7360d1f3af801da895a9b1fd54877b729;
//比特
}

引用类型

**引用类型(Reference Type)**:包括数组(array),结构体(struct)和映射(mapping),这类变量占空间大,赋值时候直接传递地址(类似指针)。由于这类变量比较复杂,占用存储空间大,我们在使用时必须要声明数据存储的位置。

数组 array

数组(Array)是solidity常用的一种变量类型,用来存储一组数据(整数,字节,地址等等)。数组分为固定长度数组和可变长度数组两种:

  • 固定长度数组:在声明时指定数组的长度。用T[k]的格式声明,其中T是元素的类型,k是长度,例如:
1
2
3
4
// 固定长度 Array
uint[8] array1;
bytes1[5] array2;
address[100] array3;
  • 可变长度数组(动态数组):在声明时不指定数组的长度。用T[]的格式声明,其中T是元素的类型,例如(bytes比较特殊,是数组,但是不用加[]):
1
2
3
4
5
// 可变长度 Array
uint[] array4;
bytes1[] array5;
address[] array6;
bytes array7;
创建数组的规则

在solidity里,创建数组有一些规则:

  • 对于memory修饰的动态数组,可以用new操作符来创建,但是必须声明长度,并且声明后长度不能改变。例子:
1
2
3
// memory动态数组
uint[] memory array8 = new uint[](5);
bytes memory array9 = new bytes(9);
  • 数组字面常数(Array Literals)是写作表达式形式的数组,用方括号包着来初始化array的一种方式,并且里面每一个元素的type是以第一个元素为准的,例如[1,2,3]里面所有的元素都是uint8类型,因为在solidity中如果一个值没有指定type的话,默认就是最小单位的该type,这里int的默认最小单位类型就是uint8。而[uint(1),2,3]里面的元素都是uint类型,因为第一个元素指定了是uint类型了,我们都以第一个元素为准。下面的合约中,对于f函数里面的调用,如果我们没有显式对第一个元素进行uint强转的话,是会报错的,因为如上所述我们其实是传入了uint8类型的array,可是g函数需要的却是uint类型的array,就会报错了。
1
2
3
4
5
6
7
8
9
10
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract C {
function f() public pure {
g([uint(1), 2, 3]);
}
function g(uint[3] memory) public pure {
// ...
}
}
  • 如果创建的是动态数组,你需要一个一个元素的赋值。
1
2
3
4
uint[] memory x = new uint[](3);
x[0] = 1;
x[1] = 3;
x[2] = 4;
数组成员
  • length: 数组有一个包含元素数量的length成员,memory数组的长度在创建后是固定的。
  • push(): 动态数组bytes拥有push()成员,可以在数组最后添加一个0元素。
  • push(x): 动态数组bytes拥有push(x)成员,可以在数组最后添加一个x元素。
  • pop(): 动态数组bytes拥有pop()成员,可以移除数组最后一个元素。

结构体 struct

olidity支持通过构造结构体的形式定义新的类型。创建结构体的方法:

1
2
3
4
5
// 结构体
struct Student{
uint256 id;
uint256 score;
}
1
Student student; // 初始一个student结构体

给结构体赋值的两种方法:

  • 方法1:在函数中创建一个storage的struct引用
1
2
3
4
5
6
7
//  给结构体赋值
// 方法1:在函数中创建一个storage的struct引用
function initStudent1() external{
Student storage _student = student; // assign a copy of student
_student.id = 11;
_student.score = 100;
}
  • 方法2:直接引用状态变量的struct
1
2
3
4
5
 // 方法2:直接引用状态变量的struct
function initStudent2() external{
student.id = 1;
student.score = 80;
}

映射类型

映射Mapping

在映射中,人们可以通过键(Key)来查询对应的值(Value),比如:通过一个人的id来查询他的钱包地址。

声明映射的格式为mapping(_KeyType => _ValueType),其中_KeyType_ValueType分别是KeyValue的变量类型。例子:

1
2
mapping(uint => address) public idToAddress; // id映射到地址
mapping(address => address) public swapPair; // 币对的映射,地址到地址

映射的规则

  • 规则1:映射的_KeyType只能选择solidity默认的类型,比如uintaddress等,不能用自定义的结构体。而_ValueType可以使用自定义的类型。下面这个例子会报错,因为_KeyType使用了我们自定义的结构体:
1
2
3
4
5
6
// 我们定义一个结构体 Struct
struct Student{
uint256 id;
uint256 score;
}
mapping(Student => uint) public testVar;
  • 规则2:映射的存储位置必须是storage,因此可以用于合约的状态变量,函数中的storage变量,和library函数的参数(见例子)。不能用于public函数的参数或返回结果中,因为mapping记录的是一种关系 (key - value pair)。
  • 规则3:如果映射声明为public,那么solidity会自动给你创建一个getter函数,可以通过Key来查询对应的Value
  • 规则4:给映射新增的键值对的语法为_Var[_Key] = _Value,其中_Var是映射变量名,_Key_Value对应新增的键值对。例子:
1
2
3
function writeMap (uint _Key, address _Value) public{
idToAddress[_Key] = _Value;
}

映射的原理

  • 原理1: 映射不储存任何键(Key)的资讯,也没有length的资讯。
  • 原理2: 映射使用keccak256(key)当成offset存取value。
  • 原理3: 因为Ethereum会定义所有未使用的空间为0,所以未赋值(Value)的键(Key)初始值都是0。

函数类型

solidity官方文档里把函数归到数值类型。我们先看一下solidity中函数的形式:

1
function <function name> (<parameter types>) {internal|external|public|private} [pure|view|payable] [returns (<return types>)]

从前往后一个一个看(方括号中的是可写可不写的关键字):

  1. function:声明函数时的固定用法,想写函数,就要以function关键字开头。
  2. <function name>:函数名。
  3. (<parameter types>):圆括号里写函数的参数,也就是要输入到函数的变量类型和名字。
  4. {internal|external|public|private}:函数可见性说明符,一共4种。没标明函数类型的,默认internal
    • public: 内部外部均可见。(也可用于修饰状态变量,public变量会自动生成 getter函数,用于查询数值).
    • private: 只能从本合约内部访问,继承的合约也不能用(也可用于修饰状态变量)。
    • external: 只能从合约外部访问(但是可以用this.f()来调用,f是函数名)
    • internal: 只能从合约内部访问,继承的合约可以用(也可用于修饰状态变量)。
  5. [pure|view|payable]:决定函数权限/功能的关键字。payable(可支付的)很好理解,带着它的函数,运行的时候可以给合约转入ETHpureview的介绍需要区分。
  6. [returns ()]:函数返回的变量类型和名称。

Pure和View

合约的状态变量存储在链上,gas fee很贵,如果不改变链上状态,就不用付gas。包含pure跟view关键字的函数是不改写链上状态的,因此用户直接调用他们是不需要付gas的(合约中非pure/view函数调用它们则会改写链上状态,需要付gas)。

在以太坊中,以下语句被视为修改链上状态:

  1. 写入状态变量。
  2. 释放事件。
  3. 创建其他合同。
  4. 使用selfdestruct.
  5. 通过调用发送以太币。
  6. 调用任何未标记viewpure的函数。
  7. 使用低级调用(low-level calls)。
  8. 使用包含某些操作码的内联汇编。

三种不同的关键字的区别。

  • pure,中文意思是“纯”,在solidity里理解为“纯纯牛马”。包含pure关键字的函数,不能读取也不能写入存储在链上的状态变量。
  • view,“看”,在solidity里理解为“看客”。包含view关键字的函数,能读取但也不能写入状态变量。
  • 不写pure也不写view,函数既可以读取也可以写入状态变量。

代码实现

  • 智能合约跟大部分语言一样,有局部变量在合约中称为状态变量
  • 存在返回值并需要指定类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.7;
contract FunctionIntro{
//状态变量类似局部变量,函数外的那种感觉,函数只处理传来的参数
//external外部函数,只能在外部读取的函数
//外部可视:在合约内部的其他函数无法调用该函数,只能够通过外部读取
//pure纯函数,纯函数表示这个函数既不能读也不能写状态变量,只能够有用局部变量(输入的参数),也就是不做链上的读写操作
//returns(uint)每个函数需要返回值和类型,这里记得加s
function add(uint x,uint y) external pure returns(uint){
return x + y;//函数内容
}
function sub(uint x,uint y) external pure returns(uint){
return x - y;//函数内容
}
}

上述代码被部署之后会在部署结果的下方生成两个按钮,点击即可调用传递参数,并且根据函数中的分割符进行参数传递

view函数

只要读取了链上的东西就需要用到view

1
2
3
4
5
6
7
8
9
10
11
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.7;
contract FunctionIntro{
uint public num;
function viewFunc() external view returns(uint){
return num;//函数内容
}
function viewaddFunc(uint x) external view returns(uint){
return num+x;//函数内容
}
}

pure函数

只有局部变量或者什么都没有

1
2
3
4
5
6
7
8
9
10
11
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.7;
contract FunctionIntro{
uint public num;
function pureFunc() external pure returns(uint){
return 1;//函数内容
}
function add(uint x,uint y) external pure returns(uint){
return x + y;//函数内容
}//因为只处理了参数,并没有读取状态变量
}

函数输出

Solidity函数输出,包括:返回多种变量,命名式返回,以及利用解构式赋值读取全部和部分返回值。

返回值 return和returns

Solidity有两个关键字与函数输出相关:return和returns,他们的区别在于:

  • returns加在函数名后面,用于声明返回的变量类型及变量名;
  • return用于函数主体中,返回指定的变量。
1
2
3
4
// 返回多个变量
function returnMultiple() public pure returns(uint256, bool, uint256[3] memory){
return(1, true, [uint256(1),2,5]);
}

上面这段代码中,我们声明了returnMultiple()函数将有多个输出:returns(uint256, bool, uint256[3] memory),接着我们在函数主体中用return(1, true, [uint256(1),2,5])确定了返回值。

命名式返回

可以在returns中标明返回变量的名称,这样solidity会自动给这些变量初始化,并且自动返回这些函数的值,不需要加return。

1
2
3
4
5
6
// 命名式返回
function returnNamed() public pure returns(uint256 _number, bool _bool, uint256[3] memory _array){
_number = 2;
_bool = false;
_array = [uint256(3),2,1];
}

在上面的代码中,我们用returns(uint256 _number, bool _bool, uint256[3] memory _array)声明了返回变量类型以及变量名。这样,我们在主体中只需要给变量_number_bool_array赋值就可以自动返回了。

也可以在命名式返回中用return来返回变量:

1
2
3
4
// 命名式返回,依然支持return
function returnNamed2() public pure returns(uint256 _number, bool _bool, uint256[3] memory _array){
return(1, true, [uint256(1),2,5]);
}

解构式赋值

solidity使用解构式赋值的规则,支持读取函数的全部或部分返回值。

  • 读取所有返回值:声明变量,并且将要赋值的变量用,隔开,按顺序排列。
1
2
3
4
uint256 _number;
bool _bool;
uint256[3] memory _array;
(_number, _bool, _array) = returnNamed();
  • 读取部分返回值:声明要读取的返回值对应的变量,不读取的留空。下面这段代码中,我们只读取_bool,而不读取返回的_number_array
1
(, _bool2, ) = returnNamed();

本节代码

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
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.7;
contract ValueType{
// 返回多个变量
function returnMultiple() public pure returns(uint256, bool, uint256[3] memory){
return(1, true, [uint256(1),2,5]);
}
// 命名式返回
function returnNamed() public pure returns(uint256 _number, bool _bool, uint256[3] memory _array){
_number = 2;
_bool = false;
_array = [uint256(3),2,1];
}


// 命名式返回,依然支持return
function returnNamed2() public pure returns(uint256 _number, bool _bool, uint256[3] memory _array){
return(1, true, [uint256(1),2,5]);
}

function retrunReturn() public pure{
uint256 _number;
bool _bool;
uint256[3] memory _array;
(_number, _bool, _array) = returnNamed();
bool _bool2;
(, _bool2, ) = returnNamed();
}
}

运行截图

控制流

Solidity的控制流

Solidity的控制流与其他语言类似,主要包含以下几种:

  • if-else
1
2
3
4
5
6
7
function ifElseTest(uint256 _number) public pure returns(bool){
if(_number == 0){
return(true);
}else{
return(false);
}
}
  • for循环
1
2
3
4
5
6
7
function forLoopTest() public pure returns(uint256){
uint sum = 0;
for(uint i = 0; i < 10; i++){
sum += i;
}
return(sum);
}
  • while循环
1
2
3
4
5
6
7
8
9
function whileTest() public pure returns(uint256){
uint sum = 0;
uint i = 0;
while(i < 10){
sum += i;
i++;
}
return(sum);
}
  • do-while循环
1
2
3
4
5
6
7
8
9
function doWhileTest() public pure returns(uint256){
uint sum = 0;
uint i = 0;
do{
sum += i;
i++;
}while(i < 10);
return(sum);
}
  • 三元运算符 三元运算符是solidity中唯一一个接受三个操作数的运算符,规则条件? 条件为真的表达式:条件为假的表达式。 此运算符经常用作 if 语句的快捷方式。
1
2
3
4
5
// 三元运算符 ternary/conditional operator
function ternaryTest(uint256 x, uint256 y) public pure returns(uint256){
// return the max of x and y
return x >= y ? x: y;
}

另外还有continue(立即进入下一个循环)和break(跳出当前循环)关键字可以使用。

实现插入排序

排序算法解决的问题是将无序的一组数字,例如[2, 5, 3, 1],从小到大依次排列好。插入排序(InsertionSort)是最简单的一种排序算法,也是很多人学习的第一个算法。它的思路很简答,从前往后,依次将每一个数和排在他前面的数字比大小,如果比前面的数字小,就互换位置。

python实现

可以先看一下插入排序的python代码:

1
2
3
4
5
6
7
8
9
# Python program for implementation of Insertion Sort
def insertionSort(arr):
for i in range(1, len(arr)):
key = arr[i]
j = i-1
while j >=0 and key < arr[j] :
arr[j+1] = arr[j]
j -= 1
arr[j+1] = key
solidity 实现

如果直接修改上面的代码会报错:”solidity insertion sort”,然后发现:solidity中最常用的变量类型是uint,也就是正整数,取到负值的话,会报underflow错误。而在插入算法中,变量j有可能会取到-1,引起报错。

这里,我们需要把j加1,让它无法取到负值。正确代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 插入排序 正确版
function insertionSort(uint[] memory a) public pure returns(uint[] memory) {
// note that uint can not take negative value
for (uint i = 1;i < a.length;i++){
uint temp = a[i];
uint j=i;
while( (j >= 1) && (temp < a[j-1])){
a[j] = a[j-1];
j--;
}
a[j] = temp;
}
return(a);
}

构造函数和修饰器

构造函数

构造函数(constructor)是一种特殊的函数,每个合约可以定义一个,并在部署合约的时候自动运行一次。它可以用来初始化合约的一些参数,例如初始化合约的owner地址:

1
2
3
4
5
6
address owner; // 定义owner变量

// 构造函数
constructor() {
owner = msg.sender; // 在部署合约的时候,将owner设置为部署者的地址
}

注意⚠️:构造函数在不同的solidity版本中的语法并不一致,在Solidity 0.4.22之前,构造函数不使用 constructor 而是使用与合约名同名的函数作为构造函数而使用,由于这种旧写法容易使开发者在书写时发生疏漏(例如合约名叫 Parents,构造函数名写成 parents),使得构造函数变成普通函数,引发漏洞,所以0.4.22版本及之后,采用了全新的 constructor 写法。

构造函数的旧写法代码示例:

1
2
3
4
5
6
pragma solidity =0.4.21;
contract Parents {
// 与合约名Parents同名的函数就是构造函数
function Parents () public {
}
}

修饰器

修饰器(modifier)是solidity特有的语法,类似于面向对象编程中的decorator,声明函数拥有的特性,并减少代码冗余。它就像钢铁侠的智能盔甲,穿上它的函数会带有某些特定的行为。modifier的主要使用场景是运行函数前的检查,例如地址,变量,余额等。

我们来定义一个叫做onlyOwner的modifier:

1
2
3
4
5
// 定义modifier
modifier onlyOwner {
require(msg.sender == owner); // 检查调用者是否为owner地址
_; // 如果是的话,继续运行函数主体;否则报错并revert交易
}

代有onlyOwner修饰符的函数只能被owner地址调用,比如下面这个例子:

1
2
3
function changeOwner(address _newOwner) external onlyOwner{
owner = _newOwner; // 只有owner地址运行这个函数,并改变owner
}

我们定义了一个changeOwner函数,运行他可以改变合约的owner,但是由于onlyOwner修饰符的存在,只有原先的owner可以调用,别人调用就会报错。这也是最常用的控制智能合约权限的方法。

OppenZepplin的Ownable标准实现

OppenZepplin是一个维护solidity标准化代码库的组织,他的Ownable标准实现如下: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol

控制合约权限代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.7;
contract Variables {
address public owner; // 定义owner变量

// 构造函数
constructor() {
owner = msg.sender; // 在部署合约的时候,将owner设置为部署者的地址
}

// 定义modifier
modifier onlyOwner {
require(msg.sender == owner); // 检查调用者是否为owner地址
_; // 如果是的话,继续运行函数主体;否则报错并revert交易
}

function changeOwner(address _newOwner) external onlyOwner{
owner = _newOwner; // 只有owner地址运行这个函数,并改变owner
}

}

点击 owner 按钮查看当前 owner 变量。

以 owner 地址的用户身份,调用 changeOwner 函数,交易成功。

以非 owner 地址的用户身份,调用 changeOwner 函数,交易失败,因为modifier onlyOwner 的检查语句不满足。

事件

Solidity中的事件(event)是EVM上日志的抽象,它具有两个特点:

  • 响应:应用程序(ether.js)可以通过RPC接口订阅和监听这些事件,并在前端做响应。
  • 经济:事件是EVM上比较经济的存储数据的方式,每个大概消耗2,000 gas;相比之下,链上存储一个新变量至少需要20,000 gas

规则

事件的声明由event关键字开头,然后跟事件名称,括号里面写好事件需要记录的变量类型和变量名。以ERC20代币合约的Transfer事件为例:

1
event Transfer(address indexed from, address indexed to, uint256 value);

我们可以看到,Transfer事件共记录了3个变量fromtovalue,分别对应代币的转账地址,接收地址和转账数量。

同时fromto前面带着indexed关键字,每个indexed标记的变量可以理解为检索事件的索引“键”,在以太坊上单独作为一个topic进行存储和索引,程序可以轻松的筛选出特定转账地址和接收地址的转账事件。每个事件最多有3个带indexed的变量。每个 indexed 变量的大小为固定的256比特。事件的哈希以及这三个带indexed的变量在EVM日志中通常被存储为topic。其中topic[0]是此事件的keccak256哈希,topic[1]topic[3]存储了带indexed变量的keccak256哈希。

value 不带 indexed 关键字,会存储在事件的 data 部分中,可以理解为事件的“值”。data 部分的变量不能被直接检索,但可以存储任意大小的数据。因此一般 data 部分可以用来存储复杂的数据结构,例如数组和字符串等等,因为这些数据超过了256比特,即使存储在事件的 topic 部分中,也是以哈希的方式存储。另外,data 部分的变量在存储上消耗的gas相比于 topic 更少。

我们可以在函数里释放事件。在下面的例子中,每次用_transfer()函数进行转账操作的时候,都会释放Transfer事件,并记录相应的变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 定义_transfer函数,执行转账逻辑
function _transfer(
address from,
address to,
uint256 amount
) external {

_balances[from] = 10000000; // 给转账地址一些初始代币

_balances[from] -= amount; // from地址减去转账数量
_balances[to] += amount; // to地址加上转账数量

// 释放事件
emit Transfer(from, to, amount);
}

事件的logs可以在下面的地址中查询:

1
https://rinkeby.etherscan.io/tx/0x8cf87215b23055896d93004112bbd8ab754f081b4491cb48c37592ca8f8a36c7

点击Logs按钮,就能看到事件明细:Topics里面有三个元素,[0]是这个事件的哈希,[1][2]是我们定义的两个indexed变量的信息,即转账的转出地址和接收地址。Data里面是剩下的不带indexed的变量,也就是转账数量。

继承

继承是面向对象编程很重要的组成部分,可以显著减少重复代码。如果把合约看作是对象的话,solidity也是面向对象的编程,也支持继承。

规则

  • virtual: 父合约中的函数,如果希望子合约重写,需要加上virtual关键字。
  • override:子合约重写了父合约中的函数,需要加上override关键字。

简单继承

我们先写一个简单的爷爷合约Yeye,里面包含1个Log事件和3个function: hip(), pop(), yeye(),输出都是”Yeye”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
contract Yeye {
event Log(string msg);

// 定义3个function: hip(), pop(), man(),Log值为Yeye。
function hip() public virtual{
emit Log("Yeye");
}

function pop() public virtual{
emit Log("Yeye");
}

function yeye() public virtual {
emit Log("Yeye");
}
}

我们再定义一个爸爸合约Baba,让他继承Yeye合约,语法就是contract Baba is Yeye,非常直观。在Baba合约里,我们重写一下hip()pop()这两个函数,加上override关键字,并将他们的输出改为”Baba”;并且加一个新的函数baba,输出也是”Baba”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
contract Baba is Yeye{
// 继承两个function: hip()和pop(),输出改为Baba。
function hip() public virtual override{
emit Log("Baba");
}

function pop() public virtual override{
emit Log("Baba");
}

function baba() public virtual{
emit Log("Baba");
}
}

我们部署合约,可以看到Baba合约里有4个函数,其中hip()pop()的输出被成功改写成”Baba”,而继承来的yeye()的输出仍然是”Yeye”

多重继承

solidity的合约可以继承多个合约。规则:

继承时要按辈分最高到最低的顺序排。比如我们写一个Erzi合约,继承Yeye合约和Baba合约,那么就要写成contract Erzi is Yeye, Baba,而不能写成contract Erzi is Baba, Yeye,不然就会报错。 如果某一个函数在多个继承的合约里都存在,比如例子中的hip()pop(),在子合约里必须重写,不然会报错。 重写在多个父合约中都重名的函数时,override关键字后面要加上所有父合约名字,例如override(Yeye, Baba)。 例子:

1
2
3
4
5
6
7
8
9
contract Erzi is Yeye, Baba{
// 继承两个function: hip()和pop(),输出值为Erzi。
function hip() public virtual override(Yeye, Baba){
emit Log("Erzi");
}

function pop() public virtual override(Yeye, Baba) {
emit Log("Erzi");
}

我们可以看到,Erzi合约里面重写了hip()pop()两个函数,将输出改为”Erzi”,并且还分别从YeyeBaba合约继承了yeye()baba()两个函数。

修饰器的继承

Solidity中的修饰器(Modifier)同样可以继承,用法与函数继承类似,在相应的地方加virtualoverride关键字即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
contract Base1 {
modifier exactDividedBy2And3(uint _a) virtual {
require(_a % 2 == 0 && _a % 3 == 0);
_;
}
}

contract Identifier is Base1 {

//计算一个数分别被2除和被3除的值,但是传入的参数必须是2和3的倍数
function getExactDividedBy2And3(uint _dividend) public exactDividedBy2And3(_dividend) pure returns(uint, uint) {
return getExactDividedBy2And3WithoutModifier(_dividend);
}

//计算一个数分别被2除和被3除的值
function getExactDividedBy2And3WithoutModifier(uint _dividend) public pure returns(uint, uint){
uint div2 = _dividend / 2;
uint div3 = _dividend / 3;
return (div2, div3);
}
}

Identifier合约可以直接在代码中使用父合约中的exactDividedBy2And3修饰器,也可以利用override关键字重写修饰器:

1
2
3
4
modifier exactDividedBy2And3(uint _a) override {
_;
require(_a % 2 == 0 && _a % 3 == 0);
}

构造函数的继承

子合约有两种方法继承父合约的构造函数。举个简单的例子,父合约A里面有一个状态变量a,并由构造函数的参数来确定:

1
2
3
4
5
6
7
8
// 构造函数的继承
abstract contract A {
uint public a;

constructor(uint _a) {
a = _a;
}
}
  • 在继承时声明父构造函数的参数,例如:contract B is A(1)

  • 在子合约的构造函数中声明构造函数的参数,例如:

1
2
3
contract C is A {
constructor(uint _c) A(_c * _c) {}
}

调用父合约的函数

子合约有两种方式调用父合约的函数,直接调用和利用super关键字。

  • 直接调用:子合约可以直接用父合约名.函数名()的方式来调用父合约函数,例如Yeye.pop()
1
2
3
function callParent() public{
Yeye.pop();
}
  • super关键字:子合约可以利用super.函数名()来调用最近的父合约函数。solidity继承关系按声明时从右到左的顺序是:contract Erzi is Yeye, Baba,那么Baba是最近的父合约,super.pop()将调用Baba.pop()而不是Yeye.pop()
1
2
3
4
function callParentSuper() public{
// 将调用最近的父合约函数,Baba.pop()
super.pop();
}

抽象

如果一个智能合约里至少有一个未实现的函数,即某个函数缺少主体{}中的内容,则必须将该合约标为abstract,不然编译会报错;另外,未实现的函数需要加virtual,以便子合约重写。拿我们之前的插入排序合约为例,如果我们还没想好具体怎么实现插入排序函数,那么可以把合约标为abstract,之后让别人补写上。

1
2
3
abstract contract InsertionSort{
function insertionSort(uint[] memory a) public pure virtual returns(uint[] memory);
}

接口

接口类似于抽象合约,但它不实现任何功能。接口的规则:

  1. 不能包含状态变量
  2. 不能包含构造函数
  3. 不能继承除接口外的其他合约
  4. 所有函数都必须是external且不能有函数体
  5. 继承接口的合约必须实现接口定义的所有功能

虽然接口不实现任何功能,但它非常重要。接口是智能合约的骨架,定义了合约的功能以及如何触发它们:如果智能合约实现了某种接口(比如ERC20ERC721),其他Dapps和智能合约就知道如何与它交互。因为接口提供了两个重要的信息:

  1. 合约里每个函数的bytes4选择器,以及基于它们的函数签名函数名(每个参数类型)
  2. 接口id(更多信息见EIP165

另外,接口与合约ABI(Application Binary Interface)等价,可以相互转换:编译接口可以得到合约的ABI,利用abi-to-sol工具也可以将ABI json文件转换为接口sol文件。

我们以ERC721接口合约IERC721为例,它定义了3个event和9个function,所有ERC721标准的NFT都实现了这些函数。我们可以看到,接口和常规合约的区别在于每个函数都以;代替函数体{ }结尾。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface IERC721 is IERC165 {
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

function balanceOf(address owner) external view returns (uint256 balance);

function ownerOf(uint256 tokenId) external view returns (address owner);

function safeTransferFrom(address from, address to, uint256 tokenId) external;

function transferFrom(address from, address to, uint256 tokenId) external;

function approve(address to, uint256 tokenId) external;

function getApproved(uint256 tokenId) external view returns (address operator);

function setApprovalForAll(address operator, bool _approved) external;

function isApprovedForAll(address owner, address operator) external view returns (bool);

function safeTransferFrom( address from, address to, uint256 tokenId, bytes calldata data) external;
}

IERC721事件

IERC721包含3个事件,其中TransferApproval事件在ERC20中也有。

  • Transfer事件:在转账时被释放,记录代币的发出地址from,接收地址totokenid
  • Approval事件:在授权时释放,记录授权地址owner,被授权地址approvedtokenid
  • ApprovalForAll事件:在批量授权时释放,记录批量授权的发出地址owner,被授权地址operator和授权与否的approved

IERC721函数

  • balanceOf:返回某地址的NFT持有量balance
  • ownerOf:返回某tokenId的主人owner
  • transferFrom:普通转账,参数为转出地址from,接收地址totokenId
  • safeTransferFrom:安全转账(如果接收方是合约地址,会要求实现ERC721Receiver接口)。参数为转出地址from,接收地址totokenId
  • approve:授权另一个地址使用你的NFT。参数为被授权地址approvetokenId
  • getApproved:查询tokenId被批准给了哪个地址。
  • setApprovalForAll:将自己持有的该系列NFT批量授权给某个地址operator
  • isApprovedForAll:查询某地址的NFT是否批量授权给了另一个operator地址。
  • safeTransferFrom:安全转账的重载函数,参数里面包含了data

什么时候使用接口?

如果我们知道一个合约实现了IERC721接口,我们不需要知道它具体代码实现,就可以与它交互。

无聊猿BAYC属于ERC721代币,实现了IERC721接口的功能。我们不需要知道它的源代码,只需知道它的合约地址,用IERC721接口就可以与它交互,比如用balanceOf()来查询某个地址的BAYC余额,用safeTransferFrom()来转账BAYC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
contract interactBAYC {
// 利用BAYC地址创建接口合约变量(ETH主网)
IERC721 BAYC = IERC721(0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D);

// 通过接口调用BAYC的balanceOf()查询持仓量
function balanceOfBAYC(address owner) external view returns (uint256 balance){
return BAYC.balanceOf(owner);
}

// 通过接口调用BAYC的safeTransferFrom()安全转账
function safeTransferFromBAYC(address from, address to, uint256 tokenId) external{
BAYC.safeTransferFrom(from, to, tokenId);
}
}

异常

介绍solidity三种抛出异常的方法:errorrequireassert,并比较三种方法的gas消耗。

写智能合约经常会出bugsolidity中的异常命令帮助我们debug

Error

errorsolidity 0.8版本新加的内容,方便且高效(省gas)地向用户解释操作失败的原因。人们可以在contract之外定义异常。下面,我们定义一个TransferNotOwner异常,当用户不是代币owner的时候尝试转账,会抛出错误:

1
error TransferNotOwner(); // 自定义error

在执行当中,error必须搭配revert(回退)命令使用。

1
2
3
4
5
6
function transferOwner1(uint256 tokenId, address newOwner) public {
if(_owners[tokenId] != msg.sender){
revert TransferNotOwner();
}
_owners[tokenId] = newOwner;
}

我们定义了一个transferOwner1()函数,它会检查代币的owner是不是发起人,如果不是,就会抛出TransferNotOwner异常;如果是的话,就会转账。

Require

require命令是solidity 0.8版本之前抛出异常的常用方法,目前很多主流合约仍然还在使用它。它很好用,唯一的缺点就是gas随着描述异常的字符串长度增加,比error命令要高。使用方法:require(检查条件,"异常的描述"),当检查条件不成立的时候,就会抛出异常。

我们用require命令重写一下上面的transferOwner函数:

1
2
3
4
function transferOwner2(uint256 tokenId, address newOwner) public {
require(_owners[tokenId] == msg.sender, "Transfer Not Owner");
_owners[tokenId] = newOwner;
}

Assert

assert命令一般用于程序员写程序debug,因为它不能解释抛出异常的原因(比require少个字符串)。它的用法很简单,assert(检查条件),当检查条件不成立的时候,就会抛出异常。

我们用assert命令重写一下上面的transferOwner函数:

1
2
3
4
function transferOwner3(uint256 tokenId, address newOwner) public {
assert(_owners[tokenId] == msg.sender);
_owners[tokenId] = newOwner;
}

在remix上验证

  1. 输入任意uint256数字和非0地址,调用transferOwner1,也就是error方法,控制台抛出了异常并显示我们自定义的TransferNotOwner
  2. 输入任意uint256数字和非0地址,调用transferOwner2,也就是require方法,控制台抛出了异常并打印出require中的字符串。
  3. 输入任意uint256数字和非0地址,调用transferOwner3,也就是assert方法,控制台只抛出了异常。

三种方法的gas比较

比较一下三种抛出异常的gas消耗,通过remix控制台的Debug按钮,能查到每次函数调用的gas消耗分别如下:

  1. error方法gas消耗:24445
  2. require方法gas消耗:24743
  3. assert方法gas消耗:24446

我们可以看到,error方法gas最少,其次是assertrequire方法消耗gas最多!因此,error既可以告知用户抛出异常的原因,又能省gas,大家要多用!(注意,由于部署测试时间的不同,每个函数的gas消耗会有所不同,但是比较结果会是一致的。)