solidity 104
约 7714 字大约 26 分钟
2022-05-14
error handling
revert
concept
Solidity 中的异常处理机制。在之前的学习中,已经了解了一种常用的异常处理语句require
。在本节中,将介绍另一种异常处理语法——revert
。
当在 Solidity 合约中发生异常情况时,revert
语句的作用是立即停止当前函数的执行,并撤销所有对状态的更改。
比喻
以日常生活中的购物退货为例:假设购买了一件上衣,但发现尺寸不合适或有瑕疵,会去退货。退货时,商家需要将已支付的款项还给你,而你则需要归还上衣。在这个过程中,退货操作就相当于revert
,使得账户状态回到购物前的状态,保证了双方利益不受损失。
真实用例
在 ERC20 合约中,_approve
函数使用 revert
语句来处理多个异常,并且抛出了两个不同的异常类型。
function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual {
if (owner == address(0)) {
revert ERC20InvalidApprover(address(0));
}
if (spender == address(0)) {
revert ERC20InvalidSpender(address(0));
}
_allowances[owner][spender] = value;
if (emitEvent) {
emit Approval(owner, spender, value);
}
}
documentation
revert()
函数在没有任何参数的情况下使用,用于终止函数的执行并回滚所有状态变化。它会自动返回一个默认的错误消息,指示函数执行失败。
也可以在 revert
关键字后附带一个字符串参数,以提供自定义的错误消息。这样可以在函数终止时提供更具体和详细的错误信息,方便开发者和用户理解发生的错误。
revert();
revert("Custom error message");
示例代码:
pragma solidity 0.6.0;
contract Bank {
mapping(address => uint256) balanceOf;
function deposit(uint256 amount) public payable {
//使用require对参数进行检查
require(msg.value == amount, "Deposit amount must be equal to the sent value");
uint256 oldBalance = balanceOf[msg.sender];
balanceOf[msg.sender] += amount;
}
function withdraw(uint256 amount) public {
//当需要使用if - else这种分支判断多个可能的错误情况时,可以使用revert。
if(amount <= 0){
revert("Invalid withdrawal amount");
}else if(balanceOf[msg.sender] < amount){
revert("Insufficient balance for withdrawal");
}
payable(msg.sender).transfer(amount);
uint256 oldBalance = balanceOf[msg.sender];
balanceOf[msg.sender] -= amount;
}
}
FAQ
revert和require有何异同?
从底层的角度来看,两种方法是相同的。两者都会抛出一个异常。例如,下面revert语句和require语句是等价的。
在 gas 消耗方面,两者一样都会将剩余的 gas 费返还给调用者。
if (num == 1) {
revert(‘Something bad happened’);
}
require(num == 1, ‘Something bad happened’);
在 solidity 中,require
通常用来对函数的参数进行条件判断,确保函数调用的参数符合预期。
而当我们需要处理像if-else这样复杂的判断异常情况时,就需要使用到revert来更灵活的处理异常了。
if(num == 1) {
revert("[error1] : num == 1");
} else if(num == 10) {
revert("[error2] : num == 10");
} else if(num == 100) {
revert("[error3] : num == 100");
} else{
//执行其他的逻辑
}
error
concept
学习一种自定义的错误类型:错误(error
),用于表示合约执行过程中的异常情况。它可以让开发者定义特定的错误状态,以便在合约的执行过程中进行特定的异常处理。
比喻
error
就像结构体:
在结构体中,可以将不同的属性组合在一起;
在 error
中,可以将不同的错误信息组合在一起。
真实用例
在 OpenZepplin 的 Ownerble 合约中,使用 error
来表示身份验证失败的错误类型。
error OwnableUnauthorizedAccount(address account);
function _checkOwner() internal view virtual {
if (owner() != _msgSender()) {
revert OwnableUnauthorizedAccount(_msgSender());
}
}
documentation
在 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);
}
代码示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Bank {
mapping(address => uint) private balances;
//自定义错误类型InsufficientFunds
error InsufficientFunds(uint requested, uint available);
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw(uint amount) public {
if (amount > balances[msg.sender]) {
//回滚交易,并且提交自定义错误类型InsufficientFunds
revert InsufficientFunds(amount, balances[msg.sender]);
}
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
function getBalance(address account) public view returns (uint) {
return balances[account];
}
}
FAQ
使用 error 的好处?
有时我们需要定义一个错误的类型,让很多个地方报错信息一致,这就是 error
的作用。
使用 error
可以同时提高代码的可读性以及可维护性。
error 可以在合约外定义吗?
可以。 error
可以在contract{…}
外定义。
assert
concept
在前面,学习了require
和revert
两种错误处理机制,而还要将一个新的错误处理语法——assert
。
assert
语句应用于检查一些被认为永远不应该发生的情况(例如除数为0),如果发生这种情况,则说明你的合约中存在错误,你应该修复。
比喻
assert
的作用和 revert
、require
没有区别,但在 gas 的消耗上有较大的差异:
1.assert
:使用 assert
时,它会消耗掉调用者所发送的剩余未使用的 gas 。你可以将其看作一个恶霸,它不仅阻止你的前进,还会夺走你身上携带的所有现金。
2.revert和
require
:与 assert
不同,revert
和 require
更像是施工队。它们只会阻止你前进,但不会抢走你的财物。 当使用 revert
或 require
时,Solidity 会将未使用的 gas 退还给调用者。
真实用例
在 UniswapV3 中,E2E_swap 合约使用了assert
语句来断言流动性的变化为零。
function check_liquidityNet_invariant() internal {
...
assert(liquidityNet == 0);
}
documentation
在 Solidity 中,使用assert
关键字来检查内部错误和不变式( invariant
),以确保代码的正确性。
//确认a 和 b在任何情况下都相等
assert(a == b);
代码示例:
pragma solidity 0.6.0;
contract Bank {
mapping(address => uint256) balanceOf;
function deposit(uint256 amount) public payable {
//使用require对参数进行检查
require(msg.value == amount, "Deposit amount must be equal to the sent value");
uint256 oldBalance = balanceOf[msg.sender];
balanceOf[msg.sender] += amount;
//使用assert对代码执行正确性进行检查(在这里是防止溢出)
assert(balanceOf[msg.sender] > oldBalance);
}
function withdraw(uint256 amount) public {
//当需要使用if - else这种分支判断多个可能的错误情况时,可以使用revert。
if(amount < 0) {
revert("Invalid withdrawal amount");
} else if(balanceOf[msg.sender] < amount) {
revert("Insufficient balance for withdrawal");
}
payable(msg.sender).transfer(amount);
uint256 oldBalance = balanceOf[msg.sender];
balanceOf[msg.sender] -= amount;
//使用assert对代码执行正确性进行检查(在这里是防止溢出)
//使用assert时,它会消耗掉调用者所发送的剩余未使用的gas。
assert(balanceOf[msg.sender] >= oldBalance);
}
}
FAQ
require,revert,和assert的使用场景分别是什么样的?
require() 用法: 1.验证用户输入,例如:
require(input < 20);
2.验证外部合约的调用,例如:require(external.send(amount));
3.在执行之前验证状态条件,例如:require(balance[msg.sender] >= amount)
revert() 用法: 1.处理与
require()
类似但逻辑更复杂的情况 2.当存在复杂的嵌套if/else逻辑流时,可以使用revert()
代替require()
3.请注意,复杂的逻辑可能是代码质量不佳的一个迹象,所以在开发中请尽量避免使用revert
。assert() 用法: 1.检查溢出/下溢,例如:
c = a + b; assert(c > b);
2.检查不变量,例如:assert(this.balance >= totalSupply);
3.在更改后验证状态 4.防止永远不可能发生的情况
try catch
concept
前面学习了require
,revert
和assert
。这些都是终止函数执行的错误处理机制。
然而,有时希望能够在处理错误时执行其他逻辑,而不仅仅是终止函数执行。这就需要使用try-catch
语句了。
真实用例
在 OpenZepplin 的 GovernorVotes 合约中有一个获取时间的函数 clock
,该函数将调用token.clock()
来获取时间,但是由于可能的错误,该调用会被回滚。 为了保证治理的正常执行,这里使用try-catch
语句来捕获可能出现的异常,并在出现异常的时候调用SafeCast.toUint48(block.number)
来将当前区块号作为时间返回。
function clock() public view virtual override returns (uint48) {
try token.clock() returns (uint48 timepoint) {
return timepoint;
} catch {
return SafeCast.toUint48(block.number);
}
}
documentation
在 Solidity 中,使用try-catch
语句来处理可能存在的错误。并且可以使用catch (error err)
语句来捕获特定的错误类型:
try recipient.send(amount) {
// 正常执行的处理逻辑
} catch Error(string memory err) {
// 捕获特定错误类型为Error的处理逻辑
// 可以根据错误信息err进行相应的处理
} catch (bytes memory) {
// 捕获其他错误类型的处理逻辑
// 处理除了已声明的特定类型之外的所有错误
}
FAQ
为什么使用 try - catch ?
通过 try-catch
,我们可以捕获和处理特定的错误,而不会中止整个函数的执行。这使得我们能够在处理错误时采取适当的措施,例如记录错误信息、回滚状态或执行其他逻辑来修复错误。
在需要处理可能失败的函数调用时,我们可以使用 try
语句。将函数调用放在 try
中,如果调用成功,则执行 try
中的代码;如果调用失败,则执行 catch
中的代码。
try 语句可以单独存在吗?
try语句可以单独存在,不需要写catch 语句。
library
define
concept
(library
)的学习,这是一种特殊的合约。
库与合约类似,但主要用于重用代码。库包含其他合约可以调用的函数。
我们把可以反复利用的代码独立出来,成为一个库。
比喻
例如,你可能经常需要进行数学计算,如加法、乘法和求平方根等。你可以创建一个数学库,其中包含这些数学计算的函数。如下代码:
library MathLibrary {
function square(uint256 x) external pure returns (uint256) {
return x * x;
}
...
}
真实用例
在 Uniswap V3 中就实现了一个用于数据转换的库 SafeCast。
library SafeCast {
/// @notice Cast a uint256 to a uint160, revert on overflow
/// @param y The uint256 to be downcasted
/// @return z The downcasted integer, now type uint160
function toUint160(uint256 y) internal pure returns (uint160 z) {
require((z = uint160(y)) == y);
}
/// @notice Cast a int256 to a int128, revert on overflow or underflow
/// @param y The int256 to be downcasted
/// @return z The downcasted integer, now type int128
function toInt128(int256 y) internal pure returns (int128 z) {
require((z = int128(y)) == y);
}
/// @notice Cast a uint256 to a int256, revert on overflow
/// @param y The uint256 to be casted
/// @return z The casted integer, now type int256
function toInt256(uint256 y) internal pure returns (int256 z) {
require(y < 2**255);
z = int256(y);
}
}
documentation
可以使用library
关键字来定义库。库的定义类似于合约的定义,但没有状态变量。
pragma solidity ^0.8.0;
//定义MathLibrary 库
library MathLibrary {
//库中可以定义函数
function square(uint256 x) external pure returns (uint256) {
return x * x;
}
}
FAQ
library 在使用上有什么限制?
Solidity 对库的使用有一定的限制。以下是 Solidity 库的主要特征。 1.库不能定义状态变量; 2.库不能发送接收以太币; 3.库不可以被销毁,因为它是无状态的。 4.库不能继承和被继承;
call
concept
在上一节中,学习了库(library
)合约的定义,那么接下来,将学习如何使用library
。
比喻
之前我们讲过调用函数就像按下黑盒子上的按钮,它将为你执行某些操作。库合约就像一个魔法盒子,里面充满了各种有用的功能。我们只需要使用库名+函数名的方式即可调用库合约中的函数。
例如在上节课中我们定义了一个 MathLibrary
库,里面包含了一些数学运算,现在我们可以在另一个合约调用 square
函数计算一个数的平方:
MathLibrary.square(y);
真实用例
在 OpenZepplin 的 GovernorVotes 合约中有一个获取时间的函数 clock,在 catch 同调用了SafeCast 的 toUint48 函数。
function clock() public view virtual override returns (uint48) {
try token.clock() returns (uint48 timepoint) {
return timepoint;
} catch {
return SafeCast.toUint48(block.number);
}
}
documentation
使用 LibraryName.functionName() 的方式调用库合约的函数。
pragma solidity ^0.8.0;
library MathLibrary {
function square(uint256 x) external pure returns (uint256) {
return x * x;
}
}
contract ExampleContract {
function calculateSquare(uint256 y) external pure returns (uint256) {
// 调用库合约的函数
uint256 result = MathLibrary.square(y);
return result;
}
}
FAQ
library 的 call 和 contract call 有区别吗?
有区别,比如在合约A
中,尝试调用Library B
和 Contract C
。B
实际算是A
的一部分,所以A
可以调用B
中internal
函数。
而C
不算,所以A
不能调用C
中的internal
函数。
usage
concept
在这一节中,将学习库的一种特殊用法——将库合约中的函数附加到任何类型中。
实际上就是给一个普通的类型增加了一些库合约中的函数的功能,让它变得更加强大和有趣。
指令using A for B;
可用于将库 A
的所有函数附加到到任何类型 B
。添加完指令后,库 A
中的函数会自动添加为 B
类型变量的成员,可以直接使用B.functionName()
调用。
比喻
就好像是在把一个魔法标签贴在类型 B
上,标签上写着“ A
”。一旦贴上了这个标签,类型 B
就会变得神奇起来,因为它会自动拥有库 A
中的所有函数。
真实用例
在 Uniswap v3 的 UniswapV3Pool 合约中也使用到了刚刚提到的库。
using SafeCast for uint256;
using SafeCast for int256;
documentation
使用using...for...
语句可以将库中的函数附加到某个类型中。
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();
}
}
FAQ
通过 variableName.functionName() 调用和通过直接调用库函数 functionName() 有什么区别?
如果库函数有参数的话,变量 a.functionName()
的方式会自动把变量作为函数的第一个参数传入。
import
concept
在学习了库合约之后,来学习一个 solidity 当中常用的语法,import
。
它用于在一个 Solidity 合约中导入其他合约或库。举个例子,假如你想使用名为 MathLibrary 的库中的函数,但它在另一个.sol文件中。编译器无法知道你要调用的函数是什么。
这时,你可以使用import将库合约导入合约。使编译器知道你要调用的库合约长什么样。
documentation
使用import
关键字加文件路径(相对路径和绝对路径均可)即可将某个合约文件导入。
//将同级目录下的SafeMath.sol合约文件引入
//这样写在SafeMath.sol中的函数就可以直接被使用了
//同样,该合约中的其他类型,我们可以直接获取了。
import "./SafeMath.sol"
FAQ
import 的效果
其实 import 就相当于将另一个.sol文件的代码拷贝到该合约文件中。
import 的好处
import 允许我们把不同的 contract 放在不同的文件中,这样方便管理,毕竟你也不想要一个6000行的文件对吧?
inheritance
inheritance
concept
学习一下solidity中继承的概念。
比喻
继承可以理解为一种家族关系,就像父母将自己的特征传给孩子一样,一个合约(父合约)可以将自己的属性和函数传递给另一个合约(子合约)。继承的合约可以访问所有非 private
的成员。
真实用例
在上一节中提到的UniswapV3Pool合约同时也继承了一个接口IUniswapV3Pool
和一个合约NoDelegateCall
。
contract UniswapV3Pool is IUniswapV3Pool, NoDelegateCall {
documentation
使用is
关键字可以继承任意一个合约或接口。
例如这里我们定义了一个 ChildContract
合约并继承了 ParentContract
合约。
contract ChildContract is ParentContract { }
示例代码:
pragma solidity ^0.8.0;
contract ParentContract {
// 父合约中的状态变量和函数
uint public parentVariable = 1;
function parentFunction() public pure returns (string memory) {
return "Hello from parent contract!";
}
}
contract ChildContract is ParentContract {
// 子合约中的状态变量和函数
uint public childVariable;
function childFunctionCallParentFunction() public pure returns (string memory) {
// 调用父合约中的函数
return parentFunction();
}
function childFunctionUsingParentVariable() public {
// 使用父合约中的状态变量
childVariable = parentVariable;
}
}
FAQ
继承有什么好处?
最大的好处是子合约获得了父合约的全部功能。 Animal
实现了eat()
方法,Dog
和 Cat
作为它的子合约,不需要做任何事情,就自动拥有了eat()
。
constructor
concept
学习如何在继承合约时正确初始化被继承合约的构造函数。
设想一个情景,合约 A
继承了合约 B
,这意味着合约 B
的代码被复制到了合约 A
中。这样一来,合约A
中可能会存在两个构造函数的情况。
为了解决这个问题,Solidity 引入了一个机制——在继承时,继承合约需要在自己的构造函数中初始化被继承合约的构造函数。
比喻
就像现实中我们要先有父亲,有了父亲才能创造儿子。
真实用例
在 OpenZepplin 官方使用文档中给出以下代码,其中ERC20("MyToken", "MTK")
则是继承了 ERC20 中的构造函数,因此在这里需要对其进行传参初始化。
contract MyToken is ERC20, ERC20Burnable, Ownable {
constructor() ERC20("MyToken", "MTK") {}
...
}
documentation
只需要在构造函数参数
字段结束后使用被继承合约的ContractName(ParameterList)
就可以正确初始化被继承合约的构造函数。
pragma solidity ^0.8.0;
// 合约B
contract B {
uint public bValue;
constructor(uint _value) {
bValue = _value;
}
}
// 合约A 继承合约B
contract A is B {
uint public aValue;
// _valueA用于初始化aValue,
// _valueB用于调用合约B的构造函数初始化bValue
constructor(uint _valueA, uint _valueB) B(_valueB) {
aValue = _valueA;
}
}
override
concept
学习继承中的函数的覆盖-override
。
函数覆盖是指在子合约中重新实现从父合约继承的函数。这意味着子合约可以在自己的代码中提供新的函数实现,以替换父合约中原有的函数实现。
比喻
假设你的父亲是一位著名的厨师,他有一套独特的烹饪方法(函数)。
当你长大后,也成为了一位厨师,你从父亲那里继承了他的烹饪方法。但是,你发现你可以改进这些烹饪方法,使得菜肴更美味。所以,你在自己的厨房(子合约)中,使用新的烹饪方法(函数实现)替换了从父亲那里继承的烹饪方法。这就是override
的概念。
function cooking(uint time) public virtual {
// 祖传配方
}
..............override............
function cooking(uint time) public override {
// 新的烹饪方法
}
documentation
在函数的定义中使用override
关键字,即可覆盖父合约中的函数。
//这里定义了一个foo函数,并使用override关键字覆盖了父合约中的foo函数。
function foo() public override {
}
FAQ
如果我想要覆盖某个函数,必须使其和之前的函数名相同吗?
覆盖函数必须使用与被覆盖函数相同的函数名称、参数列表和返回类型,否则该合约会编译失败。
virtual
concept
继续学习继承中的另一个知识——virtual
关键字。
在父合约中,可以使用virtual
关键字来标记函数为可重写的,然后在子合约中使用override
关键字对其进行覆盖。
如果一个函数没有被virtual
标记,则不能被重写。
比喻
还是以上一节的烹饪为例,需要将烹饪方法用virtual来标记,这样才可以对其改进。
function cooking(uint time) public virtual {
// 祖传配方
}
documentation
在函数的定义中使用virtual
关键字,定义该函数可以被子合约覆盖。
pragma solidity ^0.8.0;
//定义了一个基础的Shape合约
contract Shape {
uint public sides;
constructor() {
sides = 0;
}
//定义为virtual,可以被继承的计算面积的函数
//子合约可以根据需要
function getArea() public virtual returns (uint) {
return 0;
}
}
//正方形
contract Square is Shape {
uint private sideLength;
constructor(uint _sideLength) {
sideLength = _sideLength;
sides = 4;
}
//正方形的面积计算公式是边*边
function getArea() public virtual override returns (uint) {
return sideLength * sideLength;
}
}
//三角形
contract Triangle is Shape {
uint private base;
uint private height;
constructor(uint _base, uint _height) {
base = _base;
height = _height;
sides = 3;
}
//三角形的计算公式是底*高/2
function getArea() public virtual override returns (uint) {
return (base * height) / 2;
}
}
super
concept
继续学习继承中super
的用法,这是用于在子合约中用于调用父合约的函数和变量。
比喻
前面讲到通过override
覆盖了祖传烹饪方式,使用了新的烹饪方法,每个人的口味不一样,有的的喜欢祖传的烹饪方式。一天,你的妈妈希望你按传统的方式做菜。
可以使用super
关键字来调用父合约(母亲菜谱)中的祖传烹饪方式。
super.cooking(time);
documentation
在函数内使用super.functionName
即可调用父合约中的函数。
//这里我们调用了父合约中的init函数。
super.init();
FAQ
为什么需要调用父合约的函数和变量?
通过使用super
关键字,我们可以在子合约中调用父合约的函数,确保原有逻辑得到执行,从而保持代码的一致性和正确性。
通过调用父合约的函数,子合约可以重用父合约中的代码,避免重复编写相同的功能,提高代码的可维护性和可复用性。
multiple inheritance
concept
接下来将学习继承的最后一个内容——多重继承。
多重继承是指一个合约可以从多个父合约继承功能和属性。当一个合约通过多重继承从多个父合约继承功能和属性时,它可以像拼图一样将这些不同的功能和属性组合在一起,形成一个更为复杂和功能丰富的合约。
documentation
在多重继承中使用is
关键字,将要继承的合约写在后面,用,
分隔即可。
//值得一提的是在多重继承时,super究竟指向哪一个父合约呢?
//事实是写在 最后面 的合约会被super调用。
contract Child is Parent1, Parent2 {
function foo() public {
super.foo(); // 这会调用Paren2的foo函数
}
}
FAQ
为什么需要多重继承
为了方便管理合约,当项目复杂度较高时,可以使用多继承的方式使代码结构变得更加清晰,也使代码模块化,方便维护。
interface
define
concept
可以将接口(interface
)比喻为一个合约的一种规范,它指定了合约应该提供哪些功能和行为,但并不涉及具体实现的细节。接口定义了一组函数头,包括函数的名称、参数类型和返回类型,但没有函数体。
比喻
接口就像是家庭中的电源插座。无论是什么品牌或型号的电器,只要符合电源插座的标准,就可以插入插座并获得电力供应。电源插座定义了电器应该遵循的接口规范,从而实现了不同电器与插座的互操作性。同样地,接口在 Solidity 中起到类似的作用,通过定义标准化的函数签名和行为,确保合约之间的兼容性和互操作性。
真实用例
在 OpenZepplin 的 ERC20 的接口 IERC20 实现如下:
interface IERC20 {
...
function transferFrom(address from, address to, uint256 amount) external returns (bool);
}
documentation
使用interface
关键字定义一个接口。
interface MyInterface {
function myFunction(uint256 x) external returns (uint256);
}
在这里我们定义了一个名为 MyInterface
的接口,其中有一个接口函数 myFunction
。
FAQ
接口有哪些特性?
●接口不能实现任何函数; ●接口无法继承其它合约,但可以继承其它接口; ●接口中的所有函数声明必须是external的; ●接口不能定义构造函数; ●接口不能定义状态变量;
usage
concept
一旦我们定义好了接口,就可以使用interfaceName(address).functionName()
的方式与其他合约进行交互。
MyInterface(contractAddress).myFunction();
真实用例
在 ERC20Wrapper 的实现中,_underlying
被定义为接口变量,随后可以使用interface.functionName()
的方式调用 balanceOf
函数。
abstract contract ERC20Wrapper is ERC20 {
IERC20 private immutable _underlying;
function _recover(address account) internal virtual returns (uint256) {
uint256 value = _underlying.balanceOf(address(this)) - totalSupply();
_mint(account, value);
return value;
}
}
documentation
要使用接口来交互,我们需要三个信息
1.接口的名称/定义
2.实现接口的合约地址
3.需要调用的函数
可以使用interfaceName(address).functionName()
的方式调用其他合约的函数。
//先定义一个接口变量otherContract
OtherContractInterface otherContract = OtherContractInterface(otherContractAddress);
//随后使用interface.getValue()调用otherContract的getValue函数
otherContract.getValue();
inheritance
concept
学习接口中的最后一个知识点——接口的继承。
合约的继承是指子合约继承了父合约的状态变量和函数,子合约可以直接使用父合约的资源和功能。
而接口的继承完全不同——它不提供任何功能和变量,而是定义了一组等待在子合约中实现的函数。
比喻
这种继承关系类似于学校制定的学习计划,学生必须按照学习计划的规定来完成学业。学校并不关心学生是如何学习的,只关心学生是否按照学习计划的要求来完成学业。
interface Plan {
function learningEnglish() external;
function learningHistory() external;
}
contract Students is Plan {
function learningEnglish() external override {
// 英语计划
...
}
function learningHistory() external override {
// 历史计划
...
}
}
documentation
可以使用is
关键字继承一个接口。
//在这里我们定义了一个合约ContractA并继承了InterfaceA接口,
//这意味着我们必须实现InterfaceA中规定的所有函数。
contract ContractA is InterfaceA { }
示例代码:
pragma solidity ^0.8.0;
// 定义接口:可转账接口
interface Transferable {
function transfer(address recipient, uint256 amount) external returns (bool);
function getBalance() external view returns (uint256);
}
// 合约:银行账户
//继承了Transferable接口,这意味着我们合约中必须包含transfer和getBalance函数。
contract BankAccount is Transferable {
mapping(address => uint256) private balances;
constructor(uint256 amount){
balances[msg.sender] = amount;
}
//实现transfer函数
function transfer(address recipient, uint256 amount) external override returns (bool) {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[recipient] += amount;
return true;
}
//实现getBalance函数
function getBalance() external view override returns (uint256) {
return balances[msg.sender];
}
}
FAQ
为什么需要继承接口?
在前面已经学习过,即使合约 A
没有继承接口 B
,但是合约 A
如果实现了接口 B
中的某一个函数,那合约 C
依然可以使用接口 B
来和合约 A
交互。那为什么需要继承接口呢?
因为这种情况下,合约 A
可能没有实现接口 B
中的所有函数,这可能会导致合约 C
在调用合约 A
时出现问题。
在合约 A
中显式地继承接口 B
可以确保 Solidity 编译器在编译时检查合约 A
是否实现了接口 B
的所有函数。这可以避免因遗漏实现某个函数而导致的潜在问题。
abstract & hash
abstract contract
concept
到目前为止,已经学习了library
,interface
这两种特殊合约的定义,这一章中将学习最后一种特殊的合约定义方式——abstract
,抽象合约。
抽象合约是一种不能被实例化的合约,只能被继承并作为其他合约的基类。它定义了一些函数和状态变量,并且实现了一些通用的功能。
documentation
可以使用abstract
关键字定义一个抽象合约。
pragma solidity ^0.8.0;
// 抽象合约
abstract contract Animal {
//抽象合约可以有变量的定义
string public name;
bool public hasEaten;
event EatEvent(string name);
//也可以有构造函数
constructor(string memory _name) {
name = _name;
}
function speak() public virtual returns (string memory);
function eat() public virtual {
// 抽象合约可以包含实现
// 具体的逻辑可以在子合约中重写
hasEaten = true;
}
}
// 实现抽象合约的合约
contract Cat is Animal {
constructor(string memory _name) Animal(_name) {}
function speak() public override returns (string memory) {
return "Meow";
}
function eat() public override {
// 重写抽象合约中的 eat 函数
// 提供猫的具体进食逻辑
super.eat();
emit EatEvent(name);
// 进行其他猫的进食操作
}
}
FAQ
抽象合约和接口的区别?
抽象合约和接口的最大区别在于,抽象合约可以包含变量和实现,而接口只包含没有实现的函数。
抽象合约和普通合约的区别?
抽象合约和普通合约的唯一区别在于能否被部署。
abstractContract = MyAbstractContract(abstractAddress); // error,抽象合约不能被部署
regularContract = MyRegularContract(regularAddress); // success
hash
concept
在本节中,将学习 solidity 当中的哈希计算。
哈希计算是一种将任意长度的数据转换为固定长度哈希值的过程。而哈希的特点是——不同的字符串哈希出来的结果几乎不可能相同。这在生成数据唯一标识、加密签名等领域有重大意义。
"hello world!!!" -> ds1b
"hello world!" -> tgf4
documentation
keccak256
是一个全局函数,可以在函数中直接使用该函数进行哈希计算。 ●输入:只接受bytes
类型的输入。 ●输出:bytes32
长度的字节。
//这里我们将字符串”HackQuest"转换成字节数组后,进行哈希的结果赋值给了res变量。
bytes32 res = keccak256(bytes("HackQuest"));
FAQ
有哪些哈希算法?
Keccak256
和 SHA3
是用于哈希计算的两个算法。
然而由于在以太坊的开发过程中,SHA3 还处于标准化阶段,以太坊开发团队选择了使用 Keccak256 来代替它。所以 EVM 和 solidity 中的 Keccak256 和 SHA3 都是使用 Keccak256 算法计算的哈希。为了避免概念混淆,在合约代码中直接使用 Keccak256 是最清晰和推荐的做法。