Solidity学习笔记-1

news/2024/11/16 20:20:07/文章来源:https://www.cnblogs.com/WZM1230/p/18549744

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!";
}

编译

image-20241104192640489

部署运行

Remix会提供虚拟机来模拟测试链,并提供了多个测试账户(有100ETH)来运行智能合约,部署后就能运行看到效果:

image-20241104193515370

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可以修饰状态变量,没有明确标明的会默认internalpublic修饰的会自动生成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发送的交易)

image-20241104230947368

04.函数返回

Solidity中与函数返回相关的有returnreturns两个关键字;

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.引用类型

引用类型有两个:arraystruct

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

  1. 存储位置必须是storage

  2. 不能用于public修饰函数的参数或返回值中(因为mapping记录的是一种关系key-value);

  3. 若声明为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 {}
}
image-20241110214637840

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;
}
image-20241110215847829

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;}
}
image-20241110220152988

09.常量

constantimmutable两个关键字来修饰常量;

  1. 状态变量用这两个关键字声明后,不能更改数值(提升安全性,节省gas);

  2. 只有数值变量可以声明constantimmutablestringbytes可以声明为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;}
}

将其编译并部署:

image-20241112235849185

发现有报错;

原因就是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;}
}
image-20241113000510152

11.构造函数和修饰器

11_01.构造函数

constructor是一个特殊的函数;可以用它来初始化一些参数(之前immutable声明的变量可以在这边初始化)

  1. 每个合约可以定义一个
  2. 部署合约时,自动执行
// 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.修饰器

modifiersolidity中特有的语法;声明函数拥有的特性,减少代码冗余;

主要的一些场景:运行函数前的检查(地址、变量、余额等)、权限控制等等;

// 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

image-20241113003505128

当前账户(owner)调用real owner_changeOwner函数修改为新的owner:0x0A0...,并且能执行成功:

image-20241113004417446

切换钱包(让当前的账户不为0x0A0...这个),调用发现失败:

image-20241113004711348

上面这个实例,实现了一个简单的权限控制。

12.事件

Event是EVM上日志的抽象;它有两个特点:

  1. 响应:应用程序可以通过RPC接口订阅和监听这些事件,并在前端做出响应;
  2. 经济:事件是EVM上比较经济的存储方式;(每个大概2000gas,而变量需要20000gas)

12_01.事件的声明

// 以event开头,接着是事件名称,括号里写好事件需要记录的变量类型及名字
// 以ERC20代币合约的Transfer事件
event Transfer(address indexed from, address indexed to, uint256 value);

可以看到上面这个Event记录了3个变量:fromto value

其中indexed关键字的意思是,将这些变量保存在EVM日志的topics中,方便后续检索;

12_02.事件的释放

在事件的前面加emit关键字即可,可以在函数中释放事件;

emit Transfer(from, to, value);

12_03.EVM日志

EVM用日志Log来存储事件,每条日志记录都包含主题topics和数据data

image-20241114230454640

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

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合约:

image-20241115005452716

部署BaBa合约(可以看到有四个函数,因为自己实现了一个父合约没有的函数):

image-20241115005544540

执行结果:

image-20241115005755904 image-20241115010056513

13_02.多重继承

一个合约可以继承多个合约(一对多);

  1. 继承时辈分要按从高到低的顺序排,比如contract 儿子 is 爷爷, 父亲
  2. 若某一个函数在它继承的多个合约中都存在,必须进行重写,不然会报错(比如上面这个 爷爷父亲合约中都有个不一样的say函数, 儿子 调用say函数时若不重写,根本不知道到底调哪一个)
  3. 重写在多个父合约中都重名的函数时,需要在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函数:

image-20241116154152550

13_03.修饰器的继承

修饰器Modifier同样可以继承,与函数继承类似,在相依地方加上virtualoverride即可;

// 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,应该是不通过检查):

image-20241116162314320

未改写修饰器运行的函数(输入14,应该是通过检查,并返回2):

image-20241116162448333

改写修饰器后运行的函数(输入14,应该是不通过检查):

image-20241116162611710

改写修饰器后运行的函数(输入10,应该是通过检查,并返回1):

image-20241116162710747

13_04.构造函数的继承

子合约有两种方式继承父合约构造函数;

  1. 继承时直接声明构造函数的参数,例如contract B is A(10)
  2. 在子合约的构造函数中声明构造函数的参数
// 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){// 子合约的构造函数内容}
}
image-20241116164559877

13_05.调用父合约的函数

有两种方法调用父合约里面的函数:

  1. 直接调用,父合约名.函数名
  2. 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();}
}

直接调用:

image-20241116170709891

super调用:

image-20241116170813146

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

虽然EveAdam都是God的子合约,但整个过程God只会被调用了一次;原因是Solidity借鉴了Python的方式,强制一个由基类构成的DAG(有向无环图)使其保证一个特定的顺序;

输出结果:

image-20241116174715612

14.抽象合约和接口

14_01.抽象合约

若一个合约中至少有一个未实现的函数,即function XXX() public {},则必须将合约标记为abstract,不然编译会报错;

同时,未实现的函数还得加上virtual,以便子合约重写;

14_02.接口

接口类似抽象合约,但它里面不实现任何具体功能;

接口的规则:

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

如果合约实现了某种接口(比如ERC20ERC721),其他Dapps和合约就知道如何与它交互;

因为接口提供了两个重要的消息:

  1. 合约里每个函数的bytes4选择器,以及函数签名函数名(每个参数类型)(比如前面的这个:keccak256("Transfer(address,address,uint256)")
  2. 接口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;}
}
image-20241116191530014

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;}
}
image-20241116192007641

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;}
}
image-20241116192454470

15_04.三种方法的gas比较(0.8.26版本)

执行函数后,可以在Debug中看到transaction costexecution cost

  1. error不带参数:24434 gas + 2874 gas
  2. error带参数:24677 gas + 3117 gas
  3. require:24749 gas + 3189 gas
  4. assert:24448 gas + 2888 gas

可以看到最小的消耗是error,其次是assert,最多的是require

参考:https://github.com/AmazingAng/WTF-Solidity

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/834772.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

gofiber: 使用模板

一,安装模板库 $ go get github.com/gofiber/template/html/v2 go: downloading github.com/gofiber/template/html/v2 v2.1.2 go: downloading github.com/gofiber/utils v1.1.0 go: added github.com/gofiber/template/html/v2 v2.1.2 go: added github.com/gofiber/utils v…

书生共学大模型实战营第4期 L1G6000 OpenCompass评测作业

基础任务:使用OpenCompass评测浦语API记录复现过程并截图 按照教程流程进行,这里我们采用的测试样本数为2,所以不同于教程,Acc可能为0,25%,50%,75%,100%,介绍两个遇到的bug:metadata模块导入错误:ModuleNotFoundError: No module named importlib.metadata解决方法:…

ShardingSphere如何轻松驾驭Seata柔性分布式事务?

0 前文 上一文解析了 ShardingSphere 强一致性事务支持 XAShardingTransactionManager ,本文继续:讲解该类 介绍支持柔性事务的 SeataATShardingTransactionManagersharding-transaction-xa-core中关于 XAShardingTransactionManager,本文研究 XATransactionManager 和 Shar…

20222303 2024-2025-1 《网络与系统攻防技术》实验六实验报告

1.实验内容 1.1 本周学习内容回顾 使用了Metasploit框架,其是一个功能强大的渗透测试框架。在使用的过程当中,Metasploit 提供了种类繁多的攻击模块,涵盖了远程代码执行、服务拒绝、提权等多种攻击方式,支持对多种操作系统和应用程序进行测试。除了漏洞利用,它还具备强大的…

20222325 2024-2025-1 《网络与系统攻防技术》实验五实验报告

1.实验内容 1.从www.besti.edu.cn、baidu.com、sina.com.cn中选择一个DNS域名进行查询,获取如下信息: DNS注册人及联系方式;该域名对应IP地址;IP地址注册人及联系方式;IP地址所在国家、城市和具体地理位置。 PS:使用whois、dig、nslookup、traceroute、以及各类在线和离线…

识海社区打卡-6

这场掉回灰名了,本来必然上大分,原因无他-查寝 先来查波战绩:c题没过纯属是因为被查寝查了室友也是个不知变通的让查寝记我头上了,byd害我赶回去,本来这题必出 看看我最后一发提交错哪了 void solve() {int n;cin >> n;if (n % 2){if (n < 27){cout << -1 …

第七次高级程序语言设计作业

班级:https://edu.cnblogs.com/campus/fzu/2024C 作业要求:https://edu.cnblogs.com/campus/fzu/2024C/homework/13304 学号:102400115 姓名:洪育豪 7.1问题:无7.2问题:无7.3问题:无7.4问题:无 含义说明 int a; - 定义一个普通整型变量。 int a; - 定义一个指向整型变量…

工具“dotnet-cnblog”安装失败。此故障可能由以下原因导致

工具“dotnet-cnblog”安装失败。此故障可能由以下原因导致解决方法 我这边用的wifi 有问题,换到手机热点就好了。本文来自博客园,作者:lanwf,转载请注明原文链接:https://www.cnblogs.com/lccsdncnblogs/p/18549701

idea免费激活到2099年

idea免费激活到2099年 目录idea免费激活到2099年前言一去官网上下载idea二 下载idea激活工具并使用第一步点击scripts(uninstall-all-users.vbs)清除信息第二步点击(install.vbs)安装配置信息第三步打开(idea.vmoptions)拷贝配置信息到 idea配置中三激活idea1复制激活码 …

学期:2024-2025-1 学号:20241303 《 计算机基础与程序设计》第八周学习总结

作业信息这个作业属于哪个课程 <班级的链接>(如2024-2025-1-计算机基础与程序设计)这个作业要求在哪里 <作业要求的链接>(如2024-2025-1计算机基础与程序设计第八周作业)这个作业的目标 <写上具体方面> 计算机科学概论(第七版)第9章 并完成云班课测试,《…

DM multipath总结---基于LINUX 7

DM multipath总结---基于LINUX 7 DM multipath总结DM Multipath提供的功能: 冗余:DM Multipath 能够在主动/被动配置下提供故障转移。在主动/被动配置下,只有一半的路径在每次进行 I/O 时会被使用。若一条 I/O 路径的任一元素(电缆、交换器或者控制器)出现故障,DM Multi…

如果让你来设计网络

如果让你来设计网络 你是一台电脑,你的名字叫 A 很久很久之前,你不与任何其他电脑相连接,孤苦伶仃。 ​​ 直到有一天,你希望与另一台电脑 B 建立通信,于是你们各开了一个网口,用一根网线连接了起来。 ​​ 用一根网线连接起来怎么就能"通信"了呢?我可以给你讲…