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!";
}
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!” 了
2、数值类型
Solidity变量类型:
数值类型(Value Type):包括布尔型,整数型等等,这类变量赋值时候直接传递数值。
引用类型(Reference Type):包括数组和结构体,这类变量占空间大,赋值时候直接传递地址(类似指针)。
映射类型(Mapping Type): Solidity里的哈希表。
函数类型(Function Type):Solidity文档里把函数归到数值类型,但我觉得他跟其他类型差别很大,所以单独分一类。
我们只介绍一些常用的类型,不常用的不讲。这篇介绍数值类型,第3讲介绍函数类型,第4讲介绍引用和映射。
数值类型
1. 布尔型
布尔型是二值变量,取值为true或false
// 布尔值
bool public _bool = true;
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; //不相等
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位正整数
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; // 比大小
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
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];
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;
enum ActionSet { Buy, Hold, Sell }
// 创建enum变量 action
ActionSet action = ActionSet.Buy;
它可以显式的和uint相互转换,并会检查转换的正整数是否在枚举的长度内,不然会报错
// enum可以和uint显式的转换
function enumToUint() external view returns(uint){
return uint(action);
}
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;
}
3.、函数类型
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 ()]:函数返回的变量类型和名称。
到底什么是Pure
和View?
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;
pragma solidity ^0.8.4;
contract FunctionTypes{
uint256 public number = 5;
定义一个add()函数,每次调用,每次给number + 1
// 默认
function add() external{
number = 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;
}
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;
}
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();
}
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;
}
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,则说明加法运算溢出,抛出异常
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进行操作了!
4、函数输出
介绍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]);
}
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];
}
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]);
}
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 _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数组,不能被修改
// _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;
}
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;
}
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;
uint public x = 1;
uint public y;
string public z;
我们可以在函数里更改状态变量的值:
function foo() external{
// 可以在函数里更改状态变量的值
x = 5;
y = 2;
z = "0xAA";
}
// 可以在函数里更改状态变量的值
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);
}
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);
}
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
): 当前链 idblock.coinbase
(address
): 挖出当前区块的矿工地址block.difficulty
(uint
): 当前区块难度block.gaslimit
(uint
): 当前区块 gas 限额block.number
(uint
): 当前区块号block.timestamp
(uint
): 自 unix epoch 起始当前区块以秒计的时间戳gasleft() returns (uint256)
:剩余的 gasmsg.data
(bytes
): 完整的 calldatamsg.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;
uint[8] array1;
bytes1[5] array2;
address[100] array3;
可变长度数组(动态数组):在声明时不指定数组的长度。用T[]的格式声明,其中T是元素的类型,例如(bytes比较特殊,是数组,但是不用加[]):
// 可变长度 Array
uint[] array4;
bytes1[] array5;
address[] array6;
bytes array7;
uint[] array4;
bytes1[] array5;
address[] array6;
bytes array7;
创建数组的规则
创建数组有一些规则:
1).对于memory修饰的动态数组,可以用new操作符来创建,但是必须声明长度,并且声明后长度不能改变。例子:
// memory动态数组
uint[] memory array8 = new uint[](5);
bytes memory array9 = new bytes(9);
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]);
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 {
// ...
}
}
}
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;
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;
}
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;
}
// 方法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;
}
function initStudent2() external{
student.id = 1;
student.score = 80;
}
错题:
数组和结构体分别属于什么类型
引用类型和引用类型
7. 映射类型
映射Mapping
在映射中,人们可以通过键(Key)来查询对应的值(Value),比如:通过一个人的id来查询他的钱包地址。
声明映射的格式为mapping(_KeyType => _ValueType),其中_KeyType和_ValueType分别是Key和Value的变量类型。例子:
mapping(uint => address) public idToAddress; // id映射到地址
mapping(address => address) public swapPair; // 币对的映射,地址到地址
mapping(address => address) public swapPair; // 币对的映射,地址到地址
映射的规则
规则1:映射的_KeyType只能选择solidity默认的类型,比如uint,address等,不能用自定义的结构体。而_ValueType可以使用自定义的类型。下面这个例子会报错,因为_KeyType使用了我们自定义的结构体:
// 我们定义一个结构体 Struct
struct Student{
uint256 id;
uint256 score;
}
mapping(Student => uint) public testVar;
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;
}
idToAddress[_Key] = _Value;
}
映射的原理
原理1: 映射不储存任何键(Key)的资讯,也没有length的资讯。
原理2: 映射使用keccak256(key)当成offset存取value。
原理3: 因为Ethereum会定义所有未使用的空间为0,所以未赋值(Value)的键(Key)初始值都是0。
8. 变量初始值
变量初始值
在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
}
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;
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;
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);
}
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的同时提升合约的安全性
错题:
if-else
function ifElseTest(uint256 _number) public pure returns(bool){
if(_number == 0){
return(true);
}else{
return(false);
}
}
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);
}
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);
}
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);
}
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;
}
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
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);
}
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);
}
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);
}
}
11. 构造函数和修饰器
我们将用合约权限控制(Ownable)的例子介绍solidity语言中构造函数(constructor)和独有的修饰器(modifier)
构造函数
构造函数(constructor)是一种特殊的函数,每个合约可以定义一个,并在部署合约的时候自动运行一次。它可以用来初始化合约的一些参数,例如初始化合约的owner地址
address owner; // 定义owner变量
// 构造函数
constructor() {
owner = msg.sender; // 在部署合约的时候,将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 {
}
}
contract Parents {
// 与合约名Parents同名的函数就是构造函数
function Parents () public {
}
}
修饰器
修饰器(modifier)是solidity特有的语法,类似于面向对象编程中的decorator,声明函数拥有的特性,并减少代码冗余。它就像钢铁侠的智能盔甲,穿上修饰器的函数会带有某些特定的行为。modifier的主要使用场景是运行函数前的检查,例如地址,变量,余额等
我们来定义一个叫做onlyOwner的modifier:
// 定义modifier
modifier onlyOwner {
require(msg.sender == owner); // 检查调用者是否为owner地址
_; // 如果是的话,继续运行函数主体;否则报错并revert交易
}
modifier onlyOwner {
require(msg.sender == owner); // 检查调用者是否为owner地址
_; // 如果是的话,继续运行函数主体;否则报错并revert交易
}
代有onlyOwner修饰符的函数只能被owner地址调用,比如下面这个例子:
function changeOwner(address _newOwner) external onlyOwner{
owner = _newOwner; // 只有owner地址运行这个函数,并改变owner
}
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);
}
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");
}
}
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");
}
}
// 继承两个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");
}
// 继承两个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);
}
}
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);
}
_;
require(_a % 2 == 0 && _a % 3 == 0);
}
构造函数的继承
子合约有两种方法继承父合约的构造函数。举个简单的例子,父合约A里面有一个状态变量a,并由构造函数的参数来确定:
// 构造函数的继承
abstract contract A {
uint public a;
constructor(uint _a) {
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) {}
}
constructor(uint _c) A(_c * _c) {}
}
调用父合约的函数
子合约有两种方式调用父合约的函数,直接调用和利用super关键字。
直接调用:子合约可以直接用父合约名.函数名()的方式来调用父合约函数,例如Yeye.pop()。
function callParent() public{
Yeye.pop();
}
Yeye.pop();
}
super关键字:子合约可以利用super.函数名()来调用最近的父合约函数。solidity继承关系按声明时从右到左的顺序是:contract Erzi is Yeye, Baba,那么Baba是最近的父合约,super.pop()将调用Baba.pop()而不是Yeye.pop():
function callParentSuper() public{
// 将调用最近的父合约函数,Baba.pop()
super.pop();
}
// 将调用最近的父合约函数,Baba.pop()
super.pop();
}
14. 抽象合约和接口
我们用ERC721的接口合约为例介绍solidity中的抽象合约(abstract)和接口(interface),帮助大家更好的理解ERC721标准
抽象合约
如果一个智能合约里至少有一个未实现的函数,即某个函数缺少主体{}中的内容,则必须将该合约标为abstract,不然编译会报错;另外,未实现的函数需要加virtual,以便子合约重写。拿我们之前的插入排序合约为例,如果我们还没想好具体怎么实现插入排序函数,那么可以把合约标为abstract,之后让别人补写上
abstract contract InsertionSort{
function insertionSort(uint[] memory a) public pure virtual returns(uint[] memory);
}
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;
}
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);
}
}
// 利用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;
}
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;
}
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;
}
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可以带有参数
正确