Solidity入门

参考这里


Solidity是以太坊虚拟机(EVM)智能合约的语言

remix是以太坊官方推荐的智能合约开发IDE,最左边的菜单有三个按钮,分别对应文件(写代码的地方)编译(跑代码)部署(部署到链上)

Remix快捷键:
Shift+Alt+F: 格式化代码
Shift+F12: 跳转到引用
Ctrl+F12:跳转到定义
第一个Solidity程序
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
contract HelloWeb3{
string public _string = "Hello Web3!";
}

第1行是注释,会写一下这个代码所用的软件许可(license),这里用的是MIT license。如果不写许可,编译时会警告(warning),但程序可以运行

第2行声明源文件所用的solidity版本,因为不同版本语法有差别。这行代码意思是源文件将不允许小于 0.8.4 版本或大于等于 0.9.0 版本的编译器编译第二个条件由^提供)。Solidity 语句以分号(;)结尾

第3-4行是合约部分,第3行创建合约(contract),并声明合约的名字 HelloWeb3。第4行是合约的内容,我们声明了一个string(字符串)变量_string,并给他赋值 “Hello Web3!”

编译并部署代码:
在编辑代码的页面,按ctrl+S就可以编译代码,非常方便。编译好之后,点击左侧菜单的“部署”按钮,进入部署页面

默认情况下,remix会用JS虚拟机来模拟以太坊链,运行智能合约,类似在浏览器里跑一条测试链。并且remix会分配几个测试账户给你,每个里面有100 ETH(测试代币),可劲儿用。你点Deploy(黄色按钮),就可以部署咱们写好的合约了

部署成功后,你会在下面看到名为HelloWeb3的合约,点击_string,就能看到我们代码中写的 “Hello Web3!” 了



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

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

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

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

我们只介绍一些常用的类型,不常用的不讲。这篇介绍数值类型,第3讲介绍函数类型,第4讲介绍引用和映射。

数值类型


1. 布尔型

布尔型是二值变量,取值为true或false
// 布尔值
bool public _bool = true;

布尔值的运算符,包括: 
! (逻辑非) 
&& (逻辑与, "and" )
|| (逻辑或, "or" ) 
== (等于)
!= (不等于)

// 布尔运算
bool public _bool1 = !_bool; //取非
bool public _bool2 = _bool && _bool1; //与
bool public _bool3 = _bool || _bool1; //或
bool public _bool4 = _bool == _bool1; //相等
bool public _bool5 = _bool != _bool1; //不相等

上面的代码中:
_bool1 = false;
_bool2 = false
_bool3 = true
_bool4 = false
_bool5 = true
变量_bool的取值是true;
_bool1是_bool的非,为false;
_bool && _bool1为false;
_bool || _bool1为true;
_bool == _bool1为false;
_bool != _bool1为true。 
值得注意的是:&& 和 ||运算符遵循短路规则,这意味着,假如存在f(x) || g(y)的表达式,如果f(x)是true,g(y)不会被计算,即使它和f(x)的结果是相反的

2. 整型


整型是solidity中的整数,最常用的包括: int: 整数 ; uint: 正整数 ; uint256: 256位正整数
// 整型
int public _int = -1; // 整数,包括负数
uint public _uint = 1; // 正整数
uint256 public _number = 20220330; // 256位正整数

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


3. 地址类型


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

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


4. 定长字节数组


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

// 固定长度的字节数组
bytes32 public _byte32 = "MiniSolidity";
bytes1 public _byte = _byte32[0];

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


5. 枚举 enum


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

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

它可以显式的和uint相互转换,并会检查转换的正整数是否在枚举的长度内,不然会报错
// enum可以和uint显式的转换
function enumToUint() external view returns(uint){
return uint(action);
}

tips:
注意到定义变量语法和java有些不一样,如bool的定义,
java定义变量是: public boolean  _bool1 = true
solidity定义方法是:bool public _bool1 = true
于是尝试在remix中修改为: public bool _bool1 = true , 然后报错了。
from solidity: ParserError: Function, variable, struct or modifier declaration expected

修改为 bool public _bool1 = true 才可以

错题: 
address payable addr;
addr.transfer(1);
应该是: 合约向addr转账1wei
我选择的是: 调用者向addr转账1wei
valuetype.sol: 
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

contract ValueType {
    //布尔类型
    bool public _bool = true;
    bool public _b1 = !_bool; //非运算
    bool public _b2 = _bool && _b1; //与运算
    bool public _b3 = _bool || _b1; //或运算
    bool public _b4 = _bool == _b1; //是否相等
    bool public _b5 = _bool != _b1; //是否不相等

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

    // 整数运算
    uint256 public _number1 = _number + 1; // +,-,*,/
    uint256 public _number2 = 2**10; // 指数
    uint256 public _number3 = 19 % 5; // 取余数
    bool public _numberbool = _number2 > _number3; // 比大小

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


    //定长数组
    bytes32 public _byte32 = "MiniSolidity";
    bytes1 public _byte = _byte32[0];


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




solidity中函数的形式:
function <function name> (<parameter types>) {internal|external|public|private} [pure|view|payable] [returns (<return types>)]

说明如下: 
function:声明函数时的固定用法,想写函数就要以function关键字开头。

<function name>:函数名

(<parameter types>):圆括号里写函数的参数,也就是要输入到函数的变量类型和名字

{internal|external|public|private}:函数可见性说明符,一共4种。没标明函数类型的,默认internal。

public: 内部外部均可见。(也可用于修饰状态变量,public变量会自动生成 getter函数,用于查询数值).
private: 只能从本合约内部访问继承的合约也不能用(也可用于修饰状态变量)。
external: 只能从合约外部访问(但是可以用this.f()来调用,f是函数名)
internal: 只能从合约内部访问继承的合约可以用(也可用于修饰状态变量)。
[pure|view|payable]:决定函数权限/功能的关键字payable(可支付的)很好理解,带着它的函数,运行的时候可以给合约转入ETH。pure和view的介绍见下一节。

[returns ()]:函数返回的变量类型和名称



到底什么是PureView?


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

在以太坊中,以下语句被视为修改链上状态
写入状态变量。
释放事件。
创建其他合同。
使用selfdestruct.
通过调用发送以太币。
调用任何未标记view或pure的函数
使用低级调用(low-level calls)。
使用包含某些操作码的内联汇编。

我把合约中的状态变量(存储在链上)比作碧池公主,三种不同的角色代表不同的关键字


pure,中文意思是“纯”,在solidity里理解为“纯纯牛马”。包含pure关键字的函数,不能读取也不能写入存储在链上的状态变量。就像小怪一样,看不到也摸不到碧池公主。

view,“看”,在solidity里理解为“看客”。包含view关键字的函数,能读取但也不能写入状态变量。类似马里奥,能看到碧池,但终究是看客,不能入洞房。

不写pure也不写view,函数既可以读取也可以写入状态变量。类似马里奥里的boss,可以对碧池公主为所欲为


pure v.s. view

我们在合约里定义一个状态变量 number = 5
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
contract FunctionTypes{
uint256 public number = 5;

定义一个add()函数,每次调用,每次给number + 1
// 默认
function add() external{
number = number + 1;
}

如果add()包含了pure关键字,例如 function add() pure external,就会报错。因为pure(纯纯牛马)是不配读取合约里的状态变量的更不配改写。那pure函数能做些什么?举个例子,你可以给函数传递一个参数 _number,然后让他返回 _number+1
// pure: 纯纯牛马
function addPure(uint256 _number) external pure returns(uint256 new_number){
new_number = _number+1;
}


如果add()包含view,比如function add() view external,也会报错。因为view能读取,但不能够改写状态变量。可以稍微改写下方程,让他不改写number,而是返回一个新的变量
// view: 看客
function addView() external view returns(uint256 new_number) {
new_number = number + 1;
}


2. internal v.s. external


// internal: 内部
function minus() internal {
number = number - 1;
}

// 合约内的函数可以调用内部函数
function minusCall() external {
minus();
}

我们定义一个internal的minus()函数,每次调用使得number变量减1。由于是internal,只能由合约内部调用,而外部不能。因此,我们必须再定义一个external的minusCall()函数,来间接调用内部的minus()


3. payable

// payable: 递钱,能给合约支付eth的函数
function minusPayable() external payable returns(uint256 balance) {
minus();
balance = address(this).balance;
}

我们定义一个external payable的minusPayable()函数,间接的调用minus(),并且返回合约里的ETH余额(this关键字可以让我们引用合约地址)。 我们可以在调用minusPayable()时,往合约里转入1个ETH

错题: 
4.以下代码截取自 SafeMath Library,其定义了一个函数以替代“加法”,如果加法的结果溢出则会返回异常:

function add(uint256 a, uint256 b) internal _____ returns (uint256) {
    uint256 c = a + b;
    assert(c >= a);  // 本行代码意为:若c不大于等于a,则说明加法运算溢出,抛出异常
    return c;
}

那么,下划线处最适合填写的关键字是:    pure

思考: 我一直思考为啥错了,仔细理解了一下文章内容发现我钻了牛角尖了,pure和view确实不能修改状态变量,但题目中的a和b并不是状态变量,仅仅是两个传入参数
所以pure可用于对传入参数进行操作,然后返回一个新的变量
view可用于对状态变量进行读取,然后用一个新的变量返回对状态变量的某些操作
举例:状态变量number,加1后用new_num返回,new_num = number + 1,
切记不能这么写: number = number + 1 , new_num = number ,因为对number进行操作了!



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


返回值 return和returns


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

returns加在函数名后面,用于声明返回的变量类型及变量名
return用于函数主体中返回指定的变量

// 返回多个变量
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])确定了返回值。uint256[3] memory

命名式返回


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

// 命名式返回
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来返回变量
// 命名式返回,依然支持return
function returnNamed2() public pure returns(uint256 _number, bool _bool, uint256[3] memory _array){
return(1, true, [uint256(1),2,5]);
}


解构式赋值


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

读取所有返回值声明变量,并且将要赋值的变量用,隔开,按顺序排列
uint256 _number;
bool _bool;
uint256[3] memory _array;
(_number, _bool, _array) = returnNamed();

读取部分返回值声明要读取的返回值对应的变量,不读取的留空。下面这段代码中,我们只读取_bool,而不读取返回的_number和_array:
(, _bool2, ) = returnNamed();



我们介绍函数的返回值return和returns,包括:返回多种变量,命名式返回,以及利用解构式赋值读取全部和部分返回值



Solidity中的引用类型


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


数据位置


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

storage合约里的状态变量默认都是storage,存储在链上

memory函数里的参数和临时变量一般用memory,存储在内存中,不上链

calldata:和memory类似,存储在内存中,不上链。与memory的不同点在于calldata变量不能修改(immutable),一般用于函数的参数。例子:
function fCalldata(uint[] calldata _x) public pure returns(uint[] calldata){
//参数为calldata数组,不能被修改
// _x[0] = 0 //这样修改会报错
return(_x);
}

calldata arrays are read-only 


数据位置和赋值规则


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

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

function fStorage() public{
//声明一个storage的变量 xStorage,指向x。修改xStorage也会影响x
uint[] storage xStorage = x;
xStorage[0] = 100;
}

2)、storage赋值给memory,会创建独立的复本,修改其中一个不会影响另一个;反之亦然。例子:
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;
}

3)、memory赋值给memory,会创建引用,改变新变量会影响原变量

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

简单理解一下,storage类型变量相互赋值,会改变原始变量
memory类型变量相互赋值,会改变原始变量
其他类型变量赋值在storage类型变量,会产生独立副本,不会改变原始变量


变量的作用域


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

1. 状态变量


状态变量是数据存储在链上的变量所有合约内函数都可以访问 ,gas消耗高。状态变量在合约内、函数外声明
contract Variables {
uint public x = 1;
uint public y;
string public z;

我们可以在函数里更改状态变量的值:
function foo() external{
// 可以在函数里更改状态变量的值
x = 5;
y = 2;
z = "0xAA";
}


2. 局部变量


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

function bar() external pure returns(uint){
uint xx = 1;
uint yy = 3;
uint zz = xx + yy;
return(zz);
}


3. 全局变量


全局变量是全局范围工作的变量,都是solidity预留关键字。他们可以在函数内不声明直接使用
function global() external view returns(address, uint, bytes memory){
address sender = msg.sender;
uint blockNum = block.number;
bytes memory data = msg.data;
return(sender, blockNum, data);
}

在上面例子里,我们使用了3个常用的全局变量:msg.sender, block.number和msg.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值

  • blockhash(uint blockNumber) returns (bytes32):指定区块的区块哈希 —— 仅可用于最新的 256 个区块且不包括当前区块,否则返回 0 。
  • block.basefee (uint): 当前区块的基础费用,参考: (EIP-3198 和 EIP-1559)
  • block.chainid (uint): 当前链 id
  • block.coinbase ( address ): 挖出当前区块的矿工地址
  • block.difficulty ( uint ): 当前区块难度
  • block.gaslimit ( uint ): 当前区块 gas 限额
  • block.number ( uint ): 当前区块号
  • block.timestamp ( uint): 自 unix epoch 起始当前区块以秒计的时间戳
  • gasleft() returns (uint256) :剩余的 gas
  • msg.data ( bytes ): 完整的 calldata
  • msg.sender ( address ): 消息发送者(当前调用)
  • msg.sig ( bytes4 ): calldata 的前 4 字节(也就是函数标识符)
  • msg.value ( uint ): 随消息发送的 wei 的数量
  • tx.gasprice (uint): 交易的 gas 价格
  • tx.origin ( address ): 交易发起者(完全的调用链)


数组 array


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

固定长度数组:在声明时指定数组的长度用T[k]的格式声明,其中T是元素的类型,k是长度,例如:
// 固定长度 Array
uint[8] array1;
bytes1[5] array2;
address[100] array3;

可变长度数组(动态数组):在声明时不指定数组的长度。用T[]的格式声明,其中T是元素的类型,例如(bytes比较特殊,是数组,但是不用加[]):
// 可变长度 Array
uint[] array4;
bytes1[] array5;
address[] array6;
bytes array7;


创建数组的规则


创建数组有一些规则:

1).对于memory修饰的动态数组,可以用new操作符来创建,但是必须声明长度,并且声明后长度不能改变。例子:
// memory动态数组
uint[] memory array8 = new uint[](5);
bytes memory array9 = new bytes(9);

2).数组字面常数(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,就会报错了。

// 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]);
g([1, 2, 3]);//Error
}
function g(uint[3] memory) public pure {
// ...
}
}

我在Remix中得到的报错信息是: 
TypeError: Invalid type for argument in function call. Invalid implicit conversion from uint8[3] memory to uint256[3] memory requested

如果创建的是动态数组,你需要一个一个元素的赋值
uint[] memory x = new uint[](3); ///Error 我这儿报错了
x[0] = 1;
x[1] = 3;
x[2] = 4;

from solidity: ParserError: Expected identifier but got 'memory'

数组成员


length: 数组有一个包含元素数量的length成员,memory数组的长度在创建后是固定的。
push(): 动态数组和bytes拥有push()成员,可以在数组最后添加一个0元素。
push(x): 动态数组和bytes拥有push(x)成员,可以在数组最后添加一个x元素。
pop(): 动态数组和bytes拥有pop()成员,可以移除数组最后一个元素。


结构体 struct


Solidity支持通过构造结构体的形式定义新的类型。创建结构体的方法
// 结构体
struct Student{
uint256 id;
uint256 score;
}

Student student; // 初始一个student结构体

给结构体赋值的两种方法

// 给结构体赋值
// 方法1:在函数中创建一个storage的struct引用
function initStudent1() external{
Student storage _student = student; // assign a copy of student
_student.id = 11;
_student.score = 100;
}

// 方法2:直接引用状态变量的struct
function initStudent2() external{
student.id = 1;
student.score = 80;
}

错题:
数组和结构体分别属于什么类型
引用类型和引用类型



映射Mapping


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

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

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


映射的规则


规则1:映射的_KeyType只能选择solidity默认的类型,比如uint,address等,不能用自定义的结构体。而_ValueType可以使用自定义的类型。下面这个例子会报错,因为_KeyType使用了我们自定义的结构体:

// 我们定义一个结构体 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对应新增的键值对。例子:
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中,声明但没赋值的变量都有它的初始值或默认值。这一讲,我们将介绍常用变量的初始值

值类型初始值
boolean: false
string: ""
int: 0
uint: 0
enum: 枚举中的第一个元素
address: 0x0000000000000000000000000000000000000000 (或 address(0))
function
    internal: 空白方程
    external: 空白方程
可以用public变量的getter函数验证上面写的初始值是否正确

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

contract VarInit {
    bool public _bool; // false
    string public _string; // ""
    int256 public _int; // 0
    uint256 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函数验证上面写的初始值是否正确

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

contract VarInit {
    bool public _bool; // false
    string public _string; // ""
    int256 public _int; // 0
    uint256 public _uint; // 0
    address public _address; // 0x0000000000000000000000000000000000000000

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

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

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

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



delete操作符


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

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



我们介绍solidity中两个关键字,constant(常量)和immutable(不变量)
状态变量声明这个两个关键字之后,不能在合约后更改数值;并且还可以节省gas
另外,只有数值变量可以声明constant和immutable
string和bytes可以声明为constant但不能为immutable

constant和immutable


constant

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

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

immutable


immutable变量可以在声明时或构造函数中初始化,因此更加灵活
// 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
// 利用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);
}

部署好合约之后,通过remix上的getter函数,能获取到constant和immutable变量初始化好的值

constant变量初始化之后,尝试改变它的值,会编译不通过并抛出TypeError: Cannot assign to a constant variable.的错误。

immutable变量初始化之后,尝试改变它的值,会编译不通过并抛出TypeError: Immutable state variable already initialized.的错误。

我们介绍solidity中两个关键字,constant(常量)和immutable(不变量),让不应该变的变量保持不变。这样的做法能在节省gas的同时提升合约的安全性

错题:
下列哪一个变量不适合用 constant 或 immutable 来修饰?
合约中的 ETH 数量



if-else
function ifElseTest(uint256 _number) public pure returns(bool){
if(_number == 0){
return(true);
}else{
return(false);
}
}

    //if-else
    function ifElseTest(uint256 num) public pure returns (bool) {
        if (num == 0) {
            return true;
        } else {
            return false;
        }
    }

for循环

function forLoopTest() public pure returns(uint256){
uint sum = 0;
for(uint i = 0; i < 10; i++){
sum += i;
}
return(sum);
}

    //for loop
    function loopTest(uint256 n) public pure returns (uint256) {
        uint256 sum = 0;
        uint256 i = 0;
        for (i = 0; i <= n; ++i) {
            sum += i;
        }
        return sum;
    }

while循环
function whileTest() public pure returns(uint256){
uint sum = 0;
uint i = 0;
while(i < 10){
sum += i;
i++;
}
return(sum);
}

    //while loop
    function whileTest(uint256 n) public pure returns (uint256) {
        uint256 sum = 0;
        uint256 i = 0;
        while (i <= n) {
            sum += i;
            i++;
        }
        return sum;
    }

do-while循环
function doWhileTest() public pure returns(uint256){
uint sum = 0;
uint i = 0;
do{
sum += i;
i++;
}while(i < 10);
return(sum);
}

    //do-while loop
    function doWhileTest(uint256 n) public pure returns (uint256) {
        uint256 sum = 0;
        uint256 i = 0;
        do {
            sum += i;
            i++;
        } while (i <= n);
        return sum;
    }

三元运算符 三元运算符是solidity中唯一一个接受三个操作数的运算符,规则条件? 条件为真的表达式:条件为假的表达式。 此运算符经常用作 if 语句的快捷方式。
// 三元运算符 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(跳出当前循环)关键字可以使用。

用solidity实现插入排序



python代码

# 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

// 插入排序 错误版
function insertionSortWrong(uint[] memory a) public pure returns(uint[] memory) {

for (uint i = 1;i < a.length;i++){
uint temp = a[i];
uint j=i-1;
while( (j >= 0) && (temp < a[j])){
a[j+1] = a[j];
j--;
}
a[j+1] = temp;
}
return(a);
}

正确的solidity插入排序


花了几个小时,在Dapp-Learning社群一个朋友的帮助下,终于找到了bug所在。solidity中最常用的变量类型是uint,也就是正整数,取到负值的话,会报underflow错误。而在插入算法中,变量j有可能会取到-1,引起报错。这里,我们需要把j加1,让它无法取到负值。正确代码:

// 插入排序 正确版
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);
}

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

contract ControlStream {
    //if-else
    function ifElseTest(uint256 num) public pure returns (bool) {
        if (num == 0) {
            return true;
        } else {
            return false;
        }
    }

    //for loop
    function loopTest(uint256 n) public pure returns (uint256) {
        uint256 sum = 0;
        uint256 i = 0;
        for (i = 0; i <= n; ++i) {
            sum += i;
        }
        return sum;
    }

    //while loop
    function whileTest(uint256 n) public pure returns (uint256) {
        uint256 sum = 0;
        uint256 i = 0;
        while (i <= n) {
            sum += i;
            i++;
        }
        return sum;
    }

    //do-while loop
    function doWhileTest(uint256 n) public pure returns (uint256) {
        uint256 sum = 0;
        uint256 i = 0;
        do {
            sum += i;
            i++;
        } while (i <= n);
        return sum;
    }

    // 插入排序 正确版
    function insertionSort(uint256[] memory a)
        public
        pure
        returns (uint256[] memory)
    {
        // note that uint can not take negative value
        for (uint256 i = 1; i < a.length; i++) {
            uint256 temp = a[i];
            uint256 j = i;
            while ((j >= 1) && (temp < a[j - 1])) {
                a[j] = a[j - 1];
                j--;
            }
            a[j] = temp;
        }
        return (a);
    }
}





我们将用合约权限控制(Ownable)的例子介绍solidity语言中构造函数(constructor)和独有的修饰器(modifier)

构造函数


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

address owner; // 定义owner变量

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

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

构造函数的旧写法代码示例:
pragma solidity =0.4.21;
contract Parents {
// 与合约名Parents同名的函数就是构造函数
function Parents () public {
}
}

修饰器


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

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

代有onlyOwner修饰符的函数只能被owner地址调用,比如下面这个例子:
function changeOwner(address _newOwner) external onlyOwner{
owner = _newOwner; // 只有owner地址运行这个函数,并改变owner
}

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





12. 事件

我们用转账ERC20代币为例来介绍solidity中的事件(event)

事件


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

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

规则


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

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

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

同时from和to前面带着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事件,并记录相应的变量

// 定义_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);
}







我们介绍了如何使用和查询solidity中的事件。很多链上分析工具包括Nansen和Dune Analysis都是基于事件工作的




13. 继承
我们介绍solidity中的继承(inheritance),包括简单继承,多重继承,以及修饰器(modifier)和构造函数(constructor)的继承

继承


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

规则


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

override子合约重写了父合约中的函数,需要加上override关键字


简单继承


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

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”。

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)。 例子

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”,并且还分别从Yeye和Baba合约继承了yeye()和baba()两个函数

修饰器的继承


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

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关键字重写修饰器:

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


构造函数的继承


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

// 构造函数的继承
abstract contract A {
uint public a;

constructor(uint _a) {
a = _a;
}
}

在继承时声明父构造函数的参数,例如:contract B is A(1) 在子合约的构造函数中声明构造函数的参数,例如:

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

调用父合约的函数


子合约有两种方式调用父合约的函数,直接调用和利用super关键字
直接调用:子合约可以直接用父合约名.函数名()的方式来调用父合约函数,例如Yeye.pop()。



function callParent() public{
Yeye.pop();
}

super关键字子合约可以利用super.函数名()来调用最近的父合约函数。solidity继承关系按声明时从右到左的顺序是:contract Erzi is Yeye, Baba,那么Baba是最近的父合约,super.pop()将调用Baba.pop()而不是Yeye.pop():

function callParentSuper() public{
// 将调用最近的父合约函数,Baba.pop()
super.pop();
}





我们用ERC721的接口合约为例介绍solidity中的抽象合约(abstract)和接口(interface),帮助大家更好的理解ERC721标准

抽象合约


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

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

接口


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

不能包含状态变量
不能包含构造函数
不能继承除接口外的其他合约
所有函数都必须是external且不能有函数体
继承接口的合约必须实现接口定义的所有功能
虽然接口不实现任何功能,但它非常重要。接口是智能合约的骨架,定义了合约的功能以及如何触发它们:如果智能合约实现了某种接口(比如ERC20或ERC721),其他Dapps和智能合约就知道如何与它交互。因为接口提供了两个重要的信息:

合约里每个函数的bytes4选择器,以及基于它们的函数签名函数名(每个参数类型)。

接口id(更多信息见EIP165)

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

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

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个事件,其中Transfer和Approval事件在ERC20中也有。

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

IERC721函数


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

什么时候使用接口


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

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

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

错题: 
被标记为abstract的合约能否被部署?
答案:不能被部署

15. 异常

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

Error


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

在执行当中,error必须搭配revert(回退)命令使用
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函数:

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函数:

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


三种方法的gas比较


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

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

我们介绍solidity三种抛出异常的方法:error,require和assert,并比较了三种方法的gas消耗。结论:error既可以告知用户抛出异常的原因,又能省gas


错题:
require: 可以不带有”异常的描述“
error: 必须搭配revert使用
assert : 不允许带有”异常的描述“

error可以带有参数 
正确