01.Hello World
开发工具
Remix
// SPDX-License-Identifier: MIT
// 软件许可,不写编译会出现警告
// 版本,“0.8.21”-->不能小于这个版本,"^"-->不能大于0.9.0
pragma solidity ^0.8.21;
// 创建合约
contract HelloWorld {string public helloworld = "Hello World!";
}
编译
部署运行
Remix会提供虚拟机来模拟测试链,并提供了多个测试账户(有100ETH)来运行智能合约,部署后就能运行看到效果:
02.值类型
重要的就是address,bytes32这些,其余的和C差不多;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract ValueTypes {// 布尔值bool public _bool = true;bool public _false = false;// 运算符:与C差不多// ! && || == !=// -----------------------------------------------------// 无符号整数,无负数// uint = uint256uint public _uint = 123;// 有符号整数// int = int256int public _int = -123;// 运算符:// + - * / **(幂) %(取余)// -----------------------------------------------------// *******特有的类型*******// 普通地址// 存储20字节的值(以太坊地址就是20字节)address public addr_1 = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;// payable address// 多了transfer和send两个成员方法,用于接收转账address payable public addr_2 = payable(addr_1);// -----------------------------------------------------// 定长字节数组(数组长度在声明后不能改变)// bytes1,bytes8,bytes32bytes32 public b32 = "ABCDEFG"; // 0x4142434445464700000000000000000000000000000000000000000000000000bytes1 public b1 = b32[0]; // 0x41// -----------------------------------------------------// 枚举 enum,比较冷门enum Actions { Eat, Drink, Smell}// Eat-->0, Drink-->1, Smell-->2// 创建枚举对象(类似于创建对象)Actions act = Actions.Drink;
}
03.函数
solidity
中函数的形式:
function <function name>(<parameter types>) {internal|external|public|private} [pure|view|payable] [returns (<return types>)]
03_01.function
声明的固定用法
03_02.function name
函数名
03_03.parameter types
函数参数,输入的变量类型和名称
03_04.internal|external|public|private
函数可见性说明符(必须明确指定)
public
:内外均可访问
private
:只能内部访问,继承的合约也不能访问
external
:只能从外部访问(内部可以通过this.xxx()
访问)
internal
:只能从内部访问,继承的合约可以用
public | private | internal
可以修饰状态变量,没有明确标明的会默认internal
,public
修饰的会自动生成getter()
函数,用于查询数值;
03_05.pure|view|payable
决定函数权限/功能的关键字
payable
:可转入的,带着它,运行时可给合约转账引入pure和view的原因是以太坊交易需要支付gas,而状态变量存在链上的gas fee很高,若运行不改变状态变量,就不需要付gas
包含
pure
或者view
的函数执行时不需要支付gas;
pure
:纯粹的,不能读不能写
view
:可以看的,能读但不能写
03_06.returns (return types)
函数返回的值,类型
03_07.示例
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract Functions {uint public num = 100;// 在这添加pure或view就会报错,因为该函数对状态变量修改了function Add() external {num += 1;}function Add_pure(uint temp_pure) external pure returns(uint temp_pure_add){// 在其中读取num(状态变量)也会报错temp_pure_add = temp_pure + 1;}function Add_view() external view returns(uint temp_view_add){// 可以读取,但不能改写temp_view_add = num + 1;}// -----------------------------------------------------------------function Minus_internal() internal {num -= 10;}// 调用内部函数function Minus_internal_Call() external {Minus_internal();}// -----------------------------------------------------------------// 给合约转账,并返回余额function Pay() external payable returns (uint balance){balance = address(this).balance;}
}
最后一个Pay函数,调用时,可以在value处填写金额,运行后便看到账户上多了23ETH;
(不是部署的时候填写,是调用该函数前填写,部署合约是向0x0000...000发送的交易)
04.函数返回
Solidity中与函数返回相关的有return
和returns
两个关键字;
return
:在函数中
returns
:跟在函数定义后面
示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract Functions {// 命名式返回function Cacl_1(uint num1, uint num2) public pure returns(uint add_num, uint minus_num){add_num = num1 + num2;minus_num = num1 - num2;// 可以不加return(add_num, minus_num);}// 非命名式返回function Cacl_2() public pure returns(uint, bool, uint[3] memory){// 数组类型后面的memory// 数组类型返回值默认要加memory// [uint(1), 2, 3]这么写的原因:// [1, 2, 3]默认是uint8,返回时必须强转为uint256return(100, true, [uint(1), 2, 3]);}// 解构式赋值function getVal() public pure{uint a;bool b;uint[3] memory c;// 读取全部返回值(a, b, c) = Cacl_2();// 读取部分返回值// 不读取的留空bool b2;(, b2, ) = Cacl_2();}
}
05.变量数据存储和数据域
引用类型:数组、结构体;
三类数据存储位置(不同存储位置的变量消耗的gas不同):
storage
:存储在链上,消耗的gas多,合约中的状态变量
memory
:内存中,消耗的gas少,变长的数据类型必须用memory修饰,如string、bytes、array、struct
calldata
:内存中,消耗的gas少,与memory不同的是其不能修改,常用做函数的参数
当声明一个storage
类型当变量时(也就是状态变量),对它的赋值是引用操作(类似C中的指针),修改其中一个,另一个也修改:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract Functions {uint[] arry = [1, 1, 1];function Change_storage() public {uint[] storage arry_2 = arry;// 此时arry中的arry[1]也为0arry_2[1] = 0;}
}
05_01.状态变量
存储在链上,消耗的gas高,公开的,所有合约均可访问(像C中的全局变量);
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract Functions {// 状态变量声明处uint[] arry = [1, 1, 1];// 更多状态变量function fun1() public {}function func2() public {}// ...
}
05_02.局部变量
同C语言中的局部变量,gas消耗低;
05_03.全局变量
与C语言中的全局变量不同!
此处的全局变量都是solidity
已经预留的关键字,不需要声明直接使用:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract Functions {function global() public view returns(address, uint, bytes memory) {// 消息发送者(当前是谁调用的)address sender = msg.sender;// 当前区块的numberuint blockNum = block.number;// 消息里的databytes memory data = msg.data;return (sender, blockNum, data);}
}
完整的全局变量列表:https://learnblockchain.cn/docs/solidity/units-and-global-variables.html#special-variables-and-functions
05_04.全局变量-以太单位和时间单位
以太单位
Solidity
中不存在小数点,以0代替小数点,方便货币交易额度的计算:
wei
:1
gwei
:1e9 = 1000000000wei
ether
:1e18 = 1000000000000000000wei
function weiUnit() external pure returns(uint) {assert(1 wei == 1e0);assert(1 wei == 1);return 1 wei;
}function gweiUnit() external pure returns(uint) {assert(1 gwei == 1e9);assert(1 gwei == 1000000000);return 1 gwei;
}function etherUnit() external pure returns(uint) {assert(1 ether == 1e18);assert(1 ether == 1000000000000000000);return 1 ether;
}
时间单位
可以规定一个操作必须在一周内、一个月内完成,这样能够使得合约的执行更加精准,不会因为技术的误差而影响合约的结果;
时间单位在solidity
中是一个很重要的概念,有助于提高合约的可读性和维护性;
seconds
:1
minutes
:60 * 1
hours
:60 * 60 * 1
days
:24 * 60 * 60 * 1
weeks
:7 * 24 * 60 * 60 * 1
function secondsUnit() external pure returns(uint) {assert(1 seconds == 1);return 1 seconds;
}function minutesUnit() external pure returns(uint) {assert(1 minutes == 60);assert(1 minutes == 60 seconds);return 1 minutes;
}function hoursUnit() external pure returns(uint) {assert(1 hours == 3600);assert(1 hours == 60 minutes);return 1 hours;
}function daysUnit() external pure returns(uint) {assert(1 days == 86400);assert(1 days == 24 hours);return 1 days;
}function weeksUnit() external pure returns(uint) {assert(1 weeks == 604800);assert(1 weeks == 7 days);return 1 weeks;
}
06.引用类型
引用类型有两个:array
和struct
;
06_01.array数组
array有固定长度数组和可变长度数组;
// 固定长度
uint[10] array_1;
// 可变长度
uint[] array_2;// 特别的,bytes声明时不用加[]
bytes array_3;// 可变长
bytes1[5] array_4;// 固定长
不能用byte[]来声明单字节数组,需要用bytes或者bytes1[]来声明,bytes比bytes[]更省gas
// 对于memory修饰的动态数组,可以用new来创建
// 必须声明长度,且长度不可改变
uint[] memory a1 = new uint[](10);
bytes memory a2 = new bytes(20);
数组里面的类型是以第一个元素为准的,默认是uint8
;
[1, 2, 3] // 这里面全是uint8
[uint(1), 2, 3] // 这里面全是uint类型
数组成员:
length
:元素数量,memory创建的数组长度是固定的;
push()
:只有动态数组有,在数组最后添加元素,默认是0,push(10)就会添加10;
pop()
:只有动态数组有,移除最后一个元素;
06_02.struct结构体
结构体的一些操作,基本与其他语言类似:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract Functions {// 创建结构体// 里面可以是原始类型,也可以是引用类型(数组,结构体)struct Student{uint256 id;uint256 score; }// 初始一个student结构体,状态变量Student student;// 赋值方法1function initStudent() public {student.id = 1;student.score = 200;}// 赋值方法2function initStudent_1() public pure {Student memory student_1;student_1.id = 2;student_1.score = 100;}// 构造函数式function createStudent() public pure {// 直接Student memory student_2 = Student(3, 300);// key valuestudent_2 = Student({id:4, score:400});}
}
07.映射类型mapping
可以理解为哈希表,一个key
对应一个value
;
-
存储位置必须是
storage
; -
不能用于
public
修饰函数的参数或返回值中(因为mapping记录的是一种关系key-value); -
若声明为
public
,系统会自动创建一个getter
函数,可以通过key来查询value;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract Functions {// 声明映射的格式// id 到 地址mapping (uint => address) public idToAddress;struct Wallet{uint id;address wallet_addr;}// 下面这个会报错// key只能用内置的类型,value可以随意// mapping (Wallet => address) public walletToAddress;mapping (address => Wallet) public addressToWallet;// 添加key-value对// wallet必须指明memory,因为是引用类型function addMappingValue(address addr, Wallet memory wallet) public {addressToWallet[addr] = wallet;}
}
08.变量的初始值
08_01.值类型
bool
:false
string
:""
int
:0
uint
:0
enum
:枚举中的第一个元素
address
:0x000....000(全0)或者address(0)
function
:
internal
:空白函数
external
:空白函数
可以用public
声明的变量的getter
函数来查看:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract Functions {// falsebool public bool_var;// 0uint public uint_var;// ""string public string_var;// 全0address public address_var;enum Actions{Eat, Drink, Smell}// 默认第一个的索引Actions public act;// 为空function internal_fun() internal {}function external_fun() external {}
}
08_02.引用类型
array
:
固定长度
:所有成员均为其默认值的静态数组(比如uint
就为0)
动态数组
:[]
struct
:所有成员设为其默认值的结构体
mapping
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract Functions {// [0, 0, 0, 0, 0]uint[5] public static_array;// '[]'uint[] public dynamic_array;struct Test{uint id;string str;address addr;}// id = 0, str = "", addr = 全0Test public test;// 0 => 0x00...00mapping (uint => string) public idToString;
}
08_03.delete操作符
delete
操作符会将变量的值变为初始值
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract Functions {// 10uint public uint_var = 10;// 10 --> 0function delete_var() public {delete uint_var;}
}
09.常量
有constant
和immutable
两个关键字来修饰常量;
-
状态变量用这两个关键字声明后,不能更改数值(提升安全性,节省gas);
-
只有数值变量可以声明
constant
和immutable
;string
和bytes
可以声明为constant
,但不能为immutable
;
constant
:声明的时候必须初始化,之后也不能改变;
immutable
:可以在函数声明时或者构造函数中初始化;constant严格一点,immutable更加灵活一点
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract Functions {// constant// 不赋值就会报错uint constant CONSTANT_VAR = 10;// -------------------------------------------// immutable// 不初始化不会报错uint immutable IMMUTABLE_UINT;address immutable IMMUTABLE_AAARESS;uint immutable IMMUTABLE_TEST;// 在构造函数里面初始化// 可以用address(this),block.number等全局变量来赋值// 也可以用自定义的函数来对常量进行初始化constructor(){IMMUTABLE_AAARESS = address(this);IMMUTABLE_UINT = block.number;IMMUTABLE_TEST = test();}// 自定义函数function test() public pure returns(uint){uint res = 999;return res;}
}
10.控制流
solidity
中的一些控制流与其他语言基本类似:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract Functions {// if elsefunction Test1() public pure returns(bool) {uint num = 0;if(num == 1){return true;}else {return false;}}// for循环function Test2() public pure returns(uint){uint sum = 0;for(uint i = 0; i < 100; i++){sum += i;}return sum;}// while循环function Test3() public pure returns(uint){uint sum = 0;uint i = 100;while(i >= 0){sum += i;i -= 1;}return sum;}// do-while循环function Test4() public pure returns(uint){uint sum = 0;uint i = 0;do {i += 1;sum += 1;}while (i <= 100);return sum;}// 三元运算符function Test5(uint a, uint b) public pure returns(bool){return a >= b ? true : false;}
}
下面用solidity
写一个插入排序;
这是Python
实现的一个插入排序:
# Python实现的代码
def insertion_sort(arr):# 遍历从1到len(arr)的每个元素for i in range(1, len(arr)):key = arr[i]# 将选中的元素与已排序序列中的元素进行比较,并向后移动j = i - 1# 将比key大的元素向后移动一个位置while j >= 0 and key < arr[j]:arr[j + 1] = arr[j]j -= 1# 将key插入到正确的位置arr[j + 1] = key
下面将用solidity
将其改写:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract Functions {function insertion_sort(uint256[] memory arr) public pure returns (uint256[] memory){for (uint256 i = 1; i < arr.length; i++) {uint256 temp = arr[i];uint256 j = i - 1;while ((j >= 0) && (temp < arr[j])) {arr[j + 1] = arr[j];j--;}arr[j + 1] = temp;}return arr;}
}
将其编译并部署:
发现有报错;
原因就是uint
类型是正整数,取到负值的话,就会报错;
正确代码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract Functions {function insertion_sort(uint256[] memory arr) public pure returns (uint256[] memory){for (uint256 i = 1; i < arr.length; i++) {uint256 temp = arr[i];// 原// uint256 j = i - 1;// 将j + 1,使其取不到负数uint256 j = i;// 原// while ((j >= 0) && (temp < arr[j])) {while ((j >= 1) && (temp < arr[j - 1])) {// 原// arr[j + 1] = arr[j];arr[j] = arr[j - 1];j--;}// 原// arr[j + 1] = temp;arr[j] = temp;}return arr;}
}
11.构造函数和修饰器
11_01.构造函数
constructor
是一个特殊的函数;可以用它来初始化一些参数(之前immutable
声明的变量可以在这边初始化)
- 每个合约可以定义一个;
- 部署合约时,自动执行;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract Functions {address owner;// 初始化addressconstructor(address initAddress){owner = initAddress;}
}
构造函数在不同的语言版本中写法不一致,旧的版本(0.4.22之前)不用
constructor
关键字,而是使用与合约同名的函数作为构造函数;现在已不使用;
// SPDX-License-Identifier: MIT pragma solidity ^0.8.26;contract Functions {address owner;// 初始化addressfunction Functions(address initAddress) public {owner = initAddress;} }
11_02.修饰器
modifier
是solidity
中特有的语法;声明函数拥有的特性,减少代码冗余;
主要的一些场景:运行函数前的检查(地址、变量、余额等)、权限控制等等;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract Functions {address public owner;// 初始化addressconstructor(address initAddress){owner = initAddress;}modifier only_owner_can_do{// 检查调用者是否是owner地址require(msg.sender == owner);// 如果是的话,继续运行,不是则报错并revert交易_;}// 真正的owner来才能调用function realowner_changeOwner(address newAddress) external only_owner_can_do{owner = newAddress;}
}
通过构造器传入initAddress
:
当前账户(owner
)调用real owner_changeOwner
函数修改为新的owner
:0x0A0...,并且能执行成功:
切换钱包(让当前的账户不为0x0A0...这个),调用发现失败:
上面这个实例,实现了一个简单的权限控制。
12.事件
Event
是EVM上日志的抽象;它有两个特点:
- 响应:应用程序可以通过
RPC
接口订阅和监听这些事件,并在前端做出响应; - 经济:事件是EVM上比较经济的存储方式;(每个大概2000gas,而变量需要20000gas)
12_01.事件的声明
// 以event开头,接着是事件名称,括号里写好事件需要记录的变量类型及名字
// 以ERC20代币合约的Transfer事件
event Transfer(address indexed from, address indexed to, uint256 value);
可以看到上面这个Event
记录了3个变量:from
,to
,value
;
其中indexed
关键字的意思是,将这些变量保存在EVM
日志的topics
中,方便后续检索;
12_02.事件的释放
在事件的前面加emit
关键字即可,可以在函数中释放事件;
emit Transfer(from, to, value);
12_03.EVM日志
EVM
用日志Log
来存储事件,每条日志记录都包含主题topics
和数据data
;
topics
这个部分是个数组,用于描述事件,但长度不能超过4;
第一个元素是事件的签名:
比如上面这个事件:
keccak256("Transfer(address,address,uint256)")
// 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
除了事件的hash,此处还可以包含最多3个indexed
参数,都可以记录在这;
indexed
标记的参数可以理解为索引事件的index,方便后面索引;每个
indexed
参数的大小固定为256比特;如果参数太大了,比如一些字符串什么的,会自动计算哈希存储在主题中;
data
可以看到带有indexed
标记的被存储在topics
中,那不带indexed
的参数就会被存储在data
中;
这部分的变量不能被直接检索,但可以存储任意大小的数据(比如复杂的数据结构,很长的数组、字符串等等,这些即使存储在topics中,也是以hash的形式);
此外,data
部分的变量在存储上消耗的gas比topics
上的更少;
gas比较:状态变量 > 事件(topics > data)
12_04.代币转账演示
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract Functions {// 一个地址-余额的映射mapping (address => uint256) public address_money;// 以event开头,接着是事件名称,括号里写好事件需要记录的变量类型及名字// 以ERC20代币合约的Transfer事件event Transfer(address indexed from, address indexed to, uint256 value);// from向to转账function transfer_fun(address from, address to, uint256 amount) external {// 给转账的地址赠予一些代币address_money[from] = 100000000;// 先扣钱(不然会有漏洞,可以搜索DAO)address_money[from] -= amount;// 再赋值address_money[to] += amount;// 释放事件emit Transfer(from, to, amount);}
}
13.继承
inheritance
继承是面向对象语言的重要组成部分,可以显著减少重复代码;可以把合约看作是对象;
virtual
:父合约中的函数若希望子合约重写,需要加上这个关键字
override
:子合约重写了父合约中的函数,需要加上这个关键字若用
override
修饰了public
变量,会重写与变量同名的getter
函数:mapping(address=>uint256) public override balanceOf;
13_01.简单继承
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;// 均输出"YeYe"
contract YeYe {event Log(string str);function Ye1() public virtual {emit Log("YeYe");}function Ye2() public virtual {emit Log("YeYe");}function Ye3() public virtual {emit Log("YeYe");}
}// 定义BBB继承AAA
// 改写了两个函数,又自己写了个新函数
contract BaBa is YeYe{// 改写函数function Ye1() public virtual override {emit Log("BaBa");}// 改写函数function Ye2() public override {emit Log("BaBa");}// 新写函数function Ba1() public virtual {emit Log("BaBa");}
}
部署YeYe合约:
部署BaBa合约(可以看到有四个函数,因为自己实现了一个父合约没有的函数):
执行结果:
13_02.多重继承
一个合约可以继承多个合约(一对多);
- 继承时辈分要按从高到低的顺序排,比如
contract 儿子 is 爷爷, 父亲
; - 若某一个函数在它继承的多个合约中都存在,必须进行重写,不然会报错(比如上面这个
爷爷
和父亲
合约中都有个不一样的say
函数,儿子
调用say
函数时若不重写,根本不知道到底调哪一个) - 重写在多个父合约中都重名的函数时,需要在
override
关键词后面加上所有父合约的名字,比如上面这个:function say() public virtual override(爷爷, 父亲){}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;// 均输出"YeYe"
contract YeYe {event Log(string str);function Ye1() public virtual {emit Log("Ye1");}function Ye2() public virtual {emit Log("Ye2");}function Ye3() public virtual {emit Log("Ye3");}
}// 定义BBB继承AAA
// 改写了两个函数,又自己写了个新函数
contract BaBa is YeYe{// 改写函数function Ye1() public virtual override {emit Log("override Ye1");}// 改写函数function Ye2() public virtual override {emit Log("override Ye2");}// 新写函数function Ba1() public virtual {emit Log("new func Ba1");}
}contract Erzi is YeYe, BaBa{// 必须重写(因为YeYe和BaBa中的这个函数不一样)function Ye1() public virtual override (YeYe, BaBa){emit Log("Erzi override Ye1");}// 必须重写(因为YeYe和BaBa中的这个函数不一样)function Ye2() public virtual override (YeYe, BaBa){emit Log("Erzi override Ye2");}// 重写BaBa的Ba1函数function Ba1() public virtual override (BaBa){emit Log("Erzi override Ba1");}
}
可以看到Erzi
合约中有4个函数(2个必须重写,1个重写的BaBa
的,还有一个是YeYe
自带的),此处展示了重写了其父合约们共有的一个Ye1
函数:
13_03.修饰器的继承
修饰器Modifier
同样可以继承,与函数继承类似,在相依地方加上virtual
和override
即可;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract Modifier_Origin{// 起初的修饰器,判断数字是否是7的倍数// 加了virtual,可以对其进行改写modifier judgeNum(uint a) virtual {require(a % 7 == 0);_;}
}// 继承,但不重写修饰器
contract Unoverride is Modifier_Origin{// 首先修饰器判断输入的数字是否是7的倍数// 是的话直接返回a / 7// 不是则直接退出function judgeInputNumber_Unoverride(uint a) public pure judgeNum(a) returns(uint){return a / 7;}
}// 继承,同时重写修饰器
contract Override is Modifier_Origin{// 重写修饰器// 判断数字是否是10的倍数modifier judgeNum(uint a) virtual override {require(a % 10 == 0);_;}// 修饰器判断是否为10的倍数// 之后返回a / 10function judgeInputNumber_Override(uint a) public pure judgeNum(a) returns(uint){return a / 10;}
}
未改写修饰器运行的函数(输入10,应该是不通过检查):
未改写修饰器运行的函数(输入14,应该是通过检查,并返回2):
改写修饰器后运行的函数(输入14,应该是不通过检查):
改写修饰器后运行的函数(输入10,应该是通过检查,并返回1):
13_04.构造函数的继承
子合约有两种方式继承父合约构造函数;
- 继承时直接声明构造函数的参数,例如
contract B is A(10)
; - 在子合约的构造函数中声明构造函数的参数;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract YeYe{uint public num;// 构造函数给状态变量赋值constructor(uint a){num = a;}
}// 继承时直接声明构造函数的参数
contract BaBa is YeYe(10 * 10){}// 在子合约的构造函数中
contract Erzi is YeYe{constructor(uint a) YeYe(a * a){// 子合约的构造函数内容}
}
13_05.调用父合约的函数
有两种方法调用父合约里面的函数:
- 直接调用,
父合约名.函数名
; super
关键字,子合约可以利用这个关键字调用最近的父合约函数(继承时最右边的合约),例如contract Erzi is YeYe, BaBa
,调用的就是BaBa
中的函数;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;// 均输出"YeYe"
contract YeYe {event Log(string str);function Ye1() public virtual {emit Log("Ye1");}function Ye2() public virtual {emit Log("Ye2");}
}contract BaBa is YeYe{// 改写函数function Ye1() public virtual override {emit Log("override Ye1");}// 新写函数function Ba1() public virtual {emit Log("new func Ba1");}
}contract Erzi is YeYe, BaBa{// 必须重写的函数function Ye1() public virtual override (YeYe, BaBa){emit Log("Erzi override Ye1");}// 直接调用function callParent() public {// 期望输出"Ye1"YeYe.Ye1();}// 利用super调用最近的父合约函数function callParent_Super() public {// 期望输出"override Ye1"super.Ye1();}
}
直接调用:
super调用:
13_06.钻石继承
也称菱形继承,指一个派生类同时有两个或两个以上的基类;
在这种情况下,使用super
关键字时,需要注意的是其会调用继承链条上的每一个合约的相关函数,而不是只调用最近的父合约;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;/*
继承树:God/ \
Adam Eve\ /people13_05的继承树(对比一下为什么不是钻石继承)
YeYe| \| BaBa| /
Erzi(实质上只有一个基类)
*/contract God {event Log(string message);function foo() public virtual {emit Log("God.foo called");}function bar() public virtual {emit Log("God.bar called");}
}contract Adam is God {// 预期输出// "Adam.foo called"// "God.foo called"function foo() public virtual override {emit Log("Adam.foo called");super.foo();}// 预期输出// "Adam.bar called"// "God.bar called"function bar() public virtual override {emit Log("Adam.bar called");super.bar();}
}contract Eve is God {// 预期输出// "Eve.foo called"// "God.foo called"function foo() public virtual override {emit Log("Eve.foo called");super.foo();}// 预期输出// "Eve.bar called"// "God.bar called"function bar() public virtual override {emit Log("Eve.bar called");super.bar();}
}contract people is Adam, Eve {// 预期输出// "Eve.foo called"// "Adam.foo called"// "God.foo called"function foo() public override(Adam, Eve) {super.foo();}// 预期输出// "Eve.bar called"// "Adam.bar called"// "God.bar called"function bar() public override(Adam, Eve) {super.bar();}
}// 换继承顺序后,输出内容也会变(Adam, Eve)-->(Eve, Adam)
// contract people is Eve, Adam {
// // 预期输出
// // "Adam.foo called"
// // "Eve.foo called"
// // "God.foo called"
// function foo() public override(Adam, Eve) {
// super.foo();
// }
// // 预期输出
// // "Adam.bar called"
// // "Eve.bar called"
// // "God.bar called"
// function bar() public override(Adam, Eve) {
// super.bar();
// }
// }
有点像递归,contract people is Adam, Eve
从右向左,先Eve
,再Adam
,最后God
;
虽然Eve
和Adam
都是God
的子合约,但整个过程God
只会被调用了一次;原因是Solidity
借鉴了Python
的方式,强制一个由基类构成的DAG(有向无环图)使其保证一个特定的顺序;
输出结果:
14.抽象合约和接口
14_01.抽象合约
若一个合约中至少有一个未实现的函数,即function XXX() public {}
,则必须将合约标记为abstract
,不然编译会报错;
同时,未实现的函数还得加上virtual
,以便子合约重写;
14_02.接口
接口类似抽象合约,但它里面不实现任何具体功能;
接口的规则:
- 不能有状态变量
- 不能有构造函数
- 不能继承除接口外的其他合约
- 所有函数必须都是
external
,且不能有函数体- 继承接口的非抽象合约必须实现接口定义的所有功能
如果合约实现了某种接口(比如ERC20
或ERC721
),其他Dapps和合约就知道如何与它交互;
因为接口提供了两个重要的消息:
- 合约里每个函数的
bytes4
选择器,以及函数签名函数名(每个参数类型)
(比如前面的这个:keccak256("Transfer(address,address,uint256)")
) - 接口id
另外,接口与合约ABI
等价,可以相互转换;编译接口可以得到合约的ABI
,利用abi-to-sol工具,也可以将ABI json
文件变为sol
文件;
ERC721
接口合约:
// 有3个事件和9个函数
// 所有ERC721标准的NFT都实现了这些函数
// 函数体都用';'替代了
interface IERC721 is IERC165 {// 在转账时被释放,记录代币的发出地址、接收地址、tokenidevent Transfer(address indexed from, address indexed to, uint256 indexed tokenId);// 在授权时被释放,记录发出地址、被授权地址、tokenidevent Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);// 在批量授权时被释放,记录批量授权的发出地址、被授权地址、授权与否event ApprovalForAll(address indexed owner, address indexed operator, bool approved);// 返回某地址的NFT持有量function balanceOf(address owner) external view returns (uint256 balance);// 返回某tokenid的主人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;// 授权另一个地址使用你的NFTfunction approve(address to, uint256 tokenId) external;// 查询tokenid被批准给了哪个地址function getApproved(uint256 tokenId) external view returns (address operator);// 将自己持有的某系列NFT批量授权给某个地址function setApprovalForAll(address operator, bool _approved) external;// 查询某地址的NFT是否批量授权给了另一个地址function isApprovedForAll(address owner, address operator) external view returns (bool);// 安全转账的重载函数function safeTransferFrom( address from, address to, uint256 tokenId, bytes calldata data) external;
}
14_03.什么时候使用接口
如果我们知道一个合约实现了ERC721
接口,我们不需要知道它具体的代码实现,就可以与其相互交互;
若某个代币实现了ERC721
接口的功能,我们不需要知道它的源码,只需要知道它的合约地址,用IERC721
接口就可以与其交互:
contract interactCoin{// 通过代币的地址创建接口合约变量IERC721 COIN = IERC721(0x.....);// 查询代币的持有量function balanceOfCOIN(address owner) external view returns (uint256 balance){// 直接调用接口中的查询return COIN.balanceOf(owner);}// 安全转账function safeTransferFromCOIN(address from, address to, uint256 tokenId) external {// 直接调用接口中的安全转账COIN.safeTransferFrom(from, to, tokenId);}
}
15.异常
写代码时的异常处理很重要;Solidity
中有3种异常命令;
15_01.error
error
可以方便且高效(省gas)地向用户解释操作失败的原因;
还可以在抛出异常的同时携带参数,帮助开发者更好的调试;
可以在contract
之外定义异常;
error
必须搭配revert
来使用;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;// 定义异常
// error TransferError();
error TransferError(address sender);contract A{mapping (uint256 => address) private owners;// 给newOwner转账,但只能发送方来转function transfer_func_1(uint256 tokenId, address newOwner) public {if(owners[tokenId] != msg.sender){revert TransferError(msg.sender);}owners[tokenId] = newOwner;}
}
15_02.require
很好用的抛出异常的方法,缺点就是gas
会随着描述异常的字符串长度增加而增加;
使用方法:require(检查条件,异常描述)
,条件不成立则抛出异常;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract A{mapping (uint256 => address) private owners;// 给newOwner转账,但只能发送方来转function transfer_func_2(uint256 tokenId, address newOwner) public {require(owners[tokenId] == msg.sender, "Transfer not owner!");owners[tokenId] = newOwner;}
}
15_03.assert
assert
命令一般用于开发人员debug
,因为它不能解释抛出异常的原因(比require
少个字符串);
使用方法:assert(检查条件)
,条件不成立,则抛出异常;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;contract A{mapping (uint256 => address) private owners;// 给newOwner转账,但只能发送方来转function transfer_func_3(uint256 tokenId, address newOwner) public {assert(owners[tokenId] == msg.sender);owners[tokenId] = newOwner;}
}
15_04.三种方法的gas比较(0.8.26版本)
执行函数后,可以在Debug中看到transaction cost
和execution cost
;
- error不带参数:24434 gas + 2874 gas
- error带参数:24677 gas + 3117 gas
- require:24749 gas + 3189 gas
- assert:24448 gas + 2888 gas
可以看到最小的消耗是error
,其次是assert
,最多的是require
。
参考:https://github.com/AmazingAng/WTF-Solidity