上一篇文章介绍了Solidity的基本数据类型、函数书写和remix的基本使用方法;这一章准备介绍一些Solidity的继承、重载、接口和异常处理等进阶知识
引入合约
solidity
支持利用import
关键字导入其他源代码中的合约,让开发更加模块化。
可以在一个.sol
文件里写多个合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
// 第一个合约
contract SimpleStorage {
uint256 favoriteNumber;
mapping(string => uint256) public nameToFavoriteNumber;
struct People {
uint256 favoriteNumber;
string name;
}
// uint256[] public anArray;
People[] public people;
function store(uint256 _favoriteNumber) public virtual {
favoriteNumber = _favoriteNumber;
}
function retrieve() public view returns (uint256){
return favoriteNumber;
}
function addPerson(string memory _name, uint256 _favoriteNumber) public {
people.push(People(_favoriteNumber, _name));
nameToFavoriteNumber[_name] = _favoriteNumber;
}
}
// 新的合约,在这个里面创建另一个合约
contract StorageFactory {
// 创建一个合约名
SimpleStorage public simpleStorage;
function createSimpleStorageContract() public {
// new关键字会让 solidity 知道 我们需要部署一个新的SimpleStorage合约
simpleStorage = new SimpleStorage();
}
}
在部署时 就会显示 SimpleStorage, StorageFactory这两个合约
或者是创建一个合约文件,然后把上面的SimpleStorage
合约引入
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "./SimpleStorage.sol";
// 新的合约,在这个里面创建另一个合约
contract StorageFactory {
// 创建一个合约名
SimpleStorage public simpleStorage;
function createSimpleStorageContract() public {
// new关键字会让 solidity 知道 我们需要部署一个新的SimpleStorage合约
simpleStorage = new SimpleStorage();
}
}
继承 (inheritance)
继承是一种面向对象编程的概念,允许一个合约(子合约)从另一个合约(父合约)中继承功能和属性。子合约可以继承父合约的状态变量、函数和修饰器等。
继承的合约可以访问所有非 private
的成员。
首先有一个基础的合约SimpleStorage:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.8;
contract SimpleStorage{
uint256 favoriteNumber;
// mapping 创建一个映射,起一个名字并把它公开
mapping(string=>uint256) public nameToFavoriteNumber;
// 定义一个结构体,名为People
struct People{
uint256 favoriteNumber;
string name;
}
// 定义一个数组
People[] public peopleList;
function store(uint256 _favoriteNumber) public {
favoriteNumber = _favoriteNumber;
}
}
再创建一个文件ExtraStorage.sol引入SimpleStorage
is 关键字就是用于继承
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.8;
import "./SimpleStorage.sol";
contract ExtraStorage is SimpleStorage {
}
部署之后就会发现 ExtraStorage 有和 SimpleStorage合约一样的方法和变量
重写
在SimpleStorage有一个函数 store, 是赋值全局变量 favoriteNumber 的
现在我有一个需求:想在ExtraStorage合约里在赋值的时候 多 +5
这个时候就需要用重写了,重写的时候需要遵守以下两点:
virtual
: 父合约中的函数,如果希望子合约重写,需要加上virtual
关键字。override
:子合约重写了父合约中的函数,需要加上override
关键字。
- 在父合约的原函数添后面添加
virtual
function store(uint256 _favoriteNumber) public virtual {
favoriteNumber = _favoriteNumber;
}
- 在继承的合约里 ExtraStorage的 函数里用
override
修饰
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.8;
import "./SimpleStorage.sol";
contract ExtraStorage is SimpleStorage {
function store(uint256 _favoriteNumber) public override{
favoriteNumber = _favoriteNumber+5;
}
}
多重继承
solidity
的合约可以继承多个合约,书写的时候需要遵守一定的规则
-
继承时要按辈分最高到最低的顺序排。比如我们写一个
My
合约,继承Yeye
合约和Baba
合约,那么就要写成contract My is Yeye, Baba
,而不能写成contract My is Baba, Yeye
,不然就会报错。 -
如果某一个函数在多个继承的合约里都存在,比如
Yeye
合约和Baba
合约都有setName()
函数,在子合约里必须重写,不然会报错。 -
重写在多个父合约中都重名的函数时,
override
关键字后面要加上所有父合约名字,例如override(Yeye, Baba)
。
contract Erzi is Yeye, Baba{
// 继承两个function: hip()和pop(),输出值为Erzi。
function setName() public virtual override(Yeye, Baba){
// ...
}
构造函数的继承
// 构造函数的继承
abstract contract A {
uint public a;
constructor(uint _a) {
a = _a;
}
}
子合约有两种方法继承父合约的构造函数:
方法一: 在继承时声明父构造函数的参数
contract B is A(1){
}
方法二: 在子合约的构造函数中声明构造函数的参数
contract C is A{
constructor(uint _c) A(_c * _c) {}
}
调用父合约的函数
子合约有两种方式调用父合约的函数,直接调用和利用super
关键字
- 直接调用:子合约可以直接用
父合约名.函数名()
的方式来调用父合约函数,例如Yeye.setName()
。
function callParent() public{
Yeye.setName();
}
super
关键字:子合约可以利用super.函数名()
来调用最近的父合约函数。
solidity
继承关系按声明是从右到左的顺序:contract My is Yeye, Baba
,那么Baba
是最近的父合约,super.setName()
将调用Baba.setName()
而不是Yeye.setName()
:
function callParentSuper() public{
// 将调用最近的父合约函数,Baba.setName()
super.setName();
}
重载
Solidity
函数重载是指 Solidity 中可以有多个具有相同名称但不同参数的函数。这些函数的名称相同,但是它们的参数数量、类型或顺序不同。 solidity不允许修饰器(modifier)重载。
下面这个例子有两个叫f()的函数,一个参数为uint8,另一个为uint256:
function f(uint8 _in) public pure returns (uint8 out) {
out = _in;
}
function f(uint256 _in) public pure returns (uint256 out) {
out = _in;
}
抽象合约
如果一个智能合约里有一个未实现的函数,即某个函数缺少主体{}
中的内容,则必须将该合约标为abstract
,不然编译会报错;另外,未实现的函数需要加virtual
,以便子合约重写。
abstract contract InsertionSort{
function insertionSort(uint[] memory a) public pure virtual returns(uint[] memory);
}
接口(interface)
可以把接口比喻为一个合约的一种规范,它指定了合约应该提供哪些功能和行为,但并不涉及具体实现的细节。接口定义了一组函数头,包括函数的名称、参数类型和返回类型,但没有函数体。
interface IERC20 {
// ...
function myFunction(uint256 x) external view returns (uint256);
}
接口有哪些特性?
- 接口不能实现任何函数;
- 接口无法继承其它合约,但可以继承其它接口;
- 接口中的所有函数声明必须是external的;
- 接口不能定义构造函数;
- 接口不能定义状态变量;
库(library)
库与合约类似,但主要用于重用代码。库包含其他合约可以调用的函数。
把可以反复利用的代码独立出来,就成为一个库。
使用library关键字来定义库。库的定义类似于合约的定义,但没有状态变量。
定义库合约和函数
// 定义一个名为MathLibrary的库。
library MathLibrary {
function square(uint256 x) external pure returns (uint256) {
return x * x;
}
}
库的使用也有一定的限制:
-
库不能定义状态变量;
-
库不能发送接收以太币;
-
库不可以被销毁,因为它是无状态的。
-
库不能继承和被继承;
使用
使用指令using A for B
;可用于将库 A的所有函数附加到到任何类型B。添加完指令后,库 A中的函数会自动添加为B类型变量的成员,可以直接使用B.functionName()
调用。
使用库名+函数名的方式即可调用库合约中的函数。
pragma solidity ^0.8.0;
// 定义一个库合约和方法
library MathLibrary {
function square(uint256 x) external pure returns (uint256) {
return x * x;
}
}
contract ExampleContract {
//
using MathLibrary for uint256;
function calculateSquare(uint256 y) external pure returns (uint256) {
// 调用库合约的函数,y变量将默认作为第一个参数传入square函数。
return y.square();
}
}
事件 Event
事件(Event)是一种用于在智能合约中发布通知和记录信息的机制。它可以在合约执行期间发出消息,允许外部应用程序监听并对这些消息做出响应。
使用event关键字来声明一个事件,其后是事件名
//在这里我们定义了一个名为EventName的事件,其有parameter1和parameter2两个参数。
event EventName(
uint256 parameter1,
uint256 parameter2
);
触发事件 emit
要广播一个事件,你需要使用 emit 关键字。emit 用于初始化事件,并根据事件的定义设置所有需要的信息,然后广播这个事件。这样,所有订阅了该事件的人或系统就会收到通知。
提交事件使用emit关键字,其后跟事件名和参数即可。
pragma solidity ^0.8.0;
contract EmitExample {
// 定义事件
event MessageSent(address sender, string message);
// 发送消息函数
function sendMessage(string memory message) public {
// 触发事件
emit MessageSent(msg.sender, message);
}
}
索引 indexed
在Solidity中,事件的参数默认是不可搜索的,也就是说,你不能直接根据事件参数的值来过滤和搜索事件。然而,当你将某个参数标记为indexed时,Solidity会为该参数创建一个额外的索引,使得你可以根据该参数的值进行过滤和搜索。
pragma solidity ^0.8.0;
contract EventExample {
// 定义事件,其中sender可被搜索
event MessageSent(address indexed sender, string message);
// 发送消息函数
function sendMessage(string memory message) public {
// 触发事件
emit MessageSent(msg.sender, message);
}
}
异常处理机制
solidity
有多种抛出异常的方法:require
,error
和assert
等
require
使用方法:require(检查条件,"异常的描述")
,当检查条件不成立的时候,就会抛出异常。
function transferOwner2(uint256 tokenId, address newOwner) public {
require(_owners[tokenId] == msg.sender, "Transfer Not Owner");
_owners[tokenId] = newOwner;
}
当检查条件不成立的时候,就会抛出异常(执行()内的文字) 条件成立则继续往下执行
gas
费用会随着描述异常的字符串长度增加
revert
revert
语句的作用是立即停止当前函数的执行,并撤销所有对状态的更改。
revert()
函数在没有任何参数的情况下使用,用于终止函数的执行并回滚所有状态变化。它会自动返回一个默认的错误消息,指示函数执行失败。
也可以在 revert 关键字后附带一个字符串参数,以提供自定义的错误消息。这样可以在函数终止时提供更具体和详细的错误信息,方便开发者和用户理解发生的错误。
revert();
revert("Custom error message");
自定义 error 错误类型
error: 用于表示合约执行过程中的异常情况。它可以让开发者定义特定的错误状态,以便在合约的执行过程中进行特定的异常处理。
在Solidity中,定义错误类型使用关键字error,随后是参数。
//使用error关键字定义了一个名为MyCustomError的自定义错误类型
//并指定错误消息类型为string 和 uint。
error MyCustomError(string message, uint number);
function process(uint256 value) public pure {
//检查value是否超过了100。如果超过了限制,我们使用revert语句抛出自定义错误
//并传递错误消息"Value exceeds limit" 和value。
if (value >100) revert MyCustomError("Value exceeds limit",value);
}
assert
assert语句应用于检查一些被认为永远不应该发生的情况
assert的作用和revert没有区别,但在gas的消耗上有较大的差异:
-
assert:使用assert时,它会消耗掉调用者所发送的剩余未使用的gas。
-
revert和require:与assert不同,当使用revert或require时,Solidity会将未使用的gas退还给调用者。
try-catch
在处理错误时执行其他逻辑,而不仅仅是终止函数执行。这就需要使用try-catch
语句了。
使用try-catch语句来处理可能存在的错误。并且可以使用catch (error err)语句来捕获特定的错误类型:
try recipient.send(amount) {
// 正常执行的处理逻辑
} catch Error(string memory err) {
// 捕获特定错误类型为Error的处理逻辑
// 可以根据错误信息err进行相应的处理
} catch (bytes memory) {
// 捕获其他错误类型的处理逻辑
// 处理除了已声明的特定类型之外的所有错误
}
函数修饰符(modifier )
函数修饰符在修饰的函数执行之前被调用,允许在函数执行之前进行额外的检查或操作。modifier
的主要使用场景是运行函数前的检查,例如地址,变量,余额等。
在定义函数修饰符时,通过modifier
关键字来定义,其定义方式和函数一样,唯一的区别在于modifier关键字取代了function关键字。_;
表示继续执行被修饰的函数。
定义一个叫做onlyOwner的modifier:
// 定义modifier
modifier onlyOwner {
require(msg.sender == owner); // 检查调用者是否为owner地址
_; // 如果是的话,继续运行函数主体;否则报错并revert交易
}
带有onlyOwner
修饰符的函数只能被owner
地址调用:
function changeOwner(address _newOwner) external onlyOwner{
owner = _newOwner; // 只有owner地址运行这个函数,并改变owner
}
原文链接:https://juejin.cn/post/7339887346433146916 作者:董员外