01.Hello World
开发工具
// 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 = uint256
uint public _uint = 123;
// 有符号整数
// int = int256
int public _int = -123;
// 运算符:
// + - * / **(幂) %(取余)
// -----------------------------------------------------
// *******特有的类型*******
// 普通地址
// 存储20字节的值(以太坊地址就是20字节)
address public addr_1 = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
// payable address
// 多了transfer和send两个成员方法,用于接收转账
address payable public addr_2 = payable(addr_1);
// -----------------------------------------------------
// 定长字节数组(数组长度在声明后不能改变)
// bytes1,bytes8,bytes32
bytes32 public b32 = "ABCDEFG"; // 0x4142434445464700000000000000000000000000000000000000000000000000
bytes1 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,返回时必须强转为uint256
return(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]也为0
arry_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;
// 当前区块的number
uint blockNum = block.number;
// 消息里的data
bytes memory data = msg.data;
return (sender, blockNum, data);
}
}
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;
// 赋值方法1
function initStudent() public {
student.id = 1;
student.score = 200;
}
// 赋值方法2
function 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 value
student_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 {
// false
bool public bool_var;
// 0
uint public uint_var;
// ""
string public string_var;
// 全0
address 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 = 全0
Test public test;
// 0 => 0x00...00
mapping (uint => string) public idToString;
}
08_03.delete操作符
delete
操作符会将变量的值变为初始值
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract Functions {
// 10
uint public uint_var = 10;
// 10 --> 0
function 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 else
function 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;
// 初始化address
constructor(address initAddress){
owner = initAddress;
}
}
构造函数在不同的语言版本中写法不一致,旧的版本(0.4.22之前)不用
constructor
关键字,而是使用与合约同名的函数作为构造函数;现在已不使用;
// SPDX-License-Identifier: MIT pragma solidity ^0.8.26; contract Functions { address owner; // 初始化address function Functions(address initAddress) public { owner = initAddress; } }
11_02.修饰器
modifier
是solidity
中特有的语法;声明函数拥有的特性,减少代码冗余;
主要的一些场景:运行函数前的检查(地址、变量、余额等)、权限控制等等;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract Functions {
address public owner;
// 初始化address
constructor(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 / 10
function 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
\ /
people
13_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 {
// 在转账时被释放,记录代币的发出地址、接收地址、tokenid
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
// 在授权时被释放,记录发出地址、被授权地址、tokenid
event 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;
// 授权另一个地址使用你的NFT
function 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
标签:function,函数,Solidity,笔记,学习,uint,contract,address,public From: https://www.cnblogs.com/WZM1230/p/18549744