库
库是用于实现代码复用的,使用 library {}
进行声明。库通常用于保存内部与外部函数调用。
库很像合约,它也可以被部署到某个地址。当它被部署到某个地址时,库中的外部函数也可以被另一个合约调用。但目前库存在以下限制:
- 不能拥有状态变量
- 不能继承或被继承
- 不能接受以太币
- 不能被销毁
库中的内部函数与外部函数调用的实现,存在很大的差异,因此我们分开讲。
调用库中的内部函数
假设我们有一个数学库 Math
,其中有一个内部函数 max
。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;
library Math {
// 库中的内部函数
function max(uint a, uint b) internal pure returns (uint) {
return a > b ? a : b;
}
}
contract LibraryInternal {
function test() public pure {
Math.max(0, 1);
}
}
当我们在合约内调用 Math.max
时,由于 max
是内部函数,因此最终 Math.max
函数的代码在编译时,会一起被添加到合约代码中去,调用 max
函数时,跟调用合约内的其他内部函数是一样的,直接通过 jump
跳转到该代码中过去。
因此,当合约被部署到以太坊区块链上时,只有合约被部署,而 Math
库是不会被部署上去的。
由于 max
函数实际上相当于直接添加到合约代码中去。因此,max
函数内,this
仍指向合约实例:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;
library Math {
// 库中的内部函数
function max() internal view returns (address) {
// 返回 this 的地址,在本例中,由于 max 是内部函数
// 因此 this 即是合约实例,即 this 地址等于合约的部署地址
return address(this);
}
}
contract LibraryInternal {
function getMaxThisAddress() public view returns (bool) {
return address(this) == Math.max();
}
}
调用 getMaxThisAddress
函数将返回 true
。表明库的内部函数仍然是在 LibraryInternal
合约实例的上下文中执行的,this
指向 LibraryInternal
合约实例。
由于库的内部函数仍然是在原合约的实例上下文中执行的,因此库中的内部函数也可以直接访问和修改原合约的状态变量:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;
library Math {
// 库中的内部函数
function max(int256[] storage nums) internal view returns (int256) {
int256 result = type(int256).min;
for (uint256 i = 0; i < nums.length; i++)
if (result < nums[i]) result = nums[i];
return result;
}
}
contract LibraryInternal {
int256[] nums;
function getMaxNum() public returns (int256) {
nums.push(1);
nums.push(2);
nums.push(1);
// 调用 Math 库 math 内部函数,传入状态变量引用
return Math.max(nums);
}
}
调用库中的外部函数
接着我们来看调用库中的外部函数时,会发生什么,我们还是用上面的例子,但是这次将 max
标志为 external
:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;
library Math {
// 库中的外部函数
function max(uint a, uint b) external pure returns (uint) {
return a > b ? a : b;
}
}
contract LibraryExternal {
function test() public pure {
Math.max(0, 1);
}
}
对于 internal
版本,部署成本为 95762 gas
,调用 test
函数成本为 206 gas
。
而对于 external
版本,部署成本为 183739 gas
,调用 test
函数成本为 4378 gas
。
可以看到 external
版本,部署成本是 internal
版本的两倍。这是因为当我们调用库中的外部函数时,我们不但部署了当前合约,还部署了 Math
库。
而 external
版本中,调用 test
函数的成本是 internal
版本的 20 多倍。这是因为当我们调用库的外部函数时,最终实际上是通过 EVM
消息调用的方式(类似于合约外部调用):根据库的部署地址,以及 max
的函数选择子,找到库的部署代码中的 max
函数代码并执行,整个成本要比直接 jump
跳转高很多。
库的部署方式
下面简单讲一下,调用库的外部函数时的编译过程和部署过程。
在编译时,由于我们还不知道库 Math
最终会部署到哪一个以太坊地址,Solidity
会根据以下方式得到一个地址占位符:
// 占位符计算:
"__$" + bytes17(keccak256(bytes("库文件路径:库名称"))) + "__$"
例如在上例上,我们查看编译之后的字节码可以看到:
...5b73__$f97769693a1ff24517255cd3d5a093436a$__636d5...
其中 __$f97769693a1ff24517255cd3d5a093436a$__
即是库 Math
的地址占位符。中间的占位码是使用 XLCYun/unileg_solidity_code/LibraryExternal.sol:Math
通过上面的占位符计算方法计算得到的。
当我们编译后的合约代码提交到以太坊网络进行部署时,EVM
会首先部署 Math
库到某一个地址 A
,然后使用 A
,替换掉字节码中的地址占用符 __$f97769693a1ff24517255cd3d5a093436a$__
。
得到新的合约代码之后,再部署新的合约代码。整个过程就完成了。
库的外部函数的 this
既然库也是会被单独部署,那么当我们调用库的外部函数时,this
是指向库(地址),还是指向当前合约实例(地址)呢?
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;
library Math {
// 库中的外部函数,返回库外部函数中的 this
function libAddress() external view returns (address) {
return address(this);
}
}
contract LibraryExternal {
function sameThis() public view returns (bool) {
return address(this) == Math.libAddress();
}
}
部署以上合约,并运行 sameThis
,可以看到结果为 true
。因此,即使是调用库的外部函数,其上下文仍保留为当前合约实例。
因此,库的外部函数也可以接收一个状态变量的引用,从而实现对状态变量的读取和修改。
库的内外部函数总结
库的内外部函数对比,可以总结如下:
- 调用库的内部函数,将直接把内部函数代码插入到当前合约中,使用 jump 直接调用,gas 耗费少
- 调用库的外部函数,将一同部署库到以太坊,调用时类似调用合约外部调用,gas 耗费大
- 无论是内部函数,还是外部函数,在库函数内部,执行的上下文还是原合约实例,
this
仍然指向原合约实例。因此,只要拿到状态变量的引用,库函数也可以访问状态变量。
集合库实现
库的使用,除以上面的示例,还有一种更流行的使用方式,那就是结合 using for
实现复杂的数据结构。
我们要使用使用库来实现一个 集合
类型。该类型支持:
- 插入操作
- 移除操作
- 判断元素是否在集合中
- 并集操作
- 差集操作
我们创建一个文件 Set.sol
用于保存该库的代码:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;
/// @dev 数组实现的集合
struct Set {
int[] elements;
}
/// @dev 数组实现的集合操作
library SetOp {
/// @notice 判断集合内是否存在存在某元素
/// @param self 集合结构体
/// @param v 要查找的元素
/// @return bool true 表示存在,false 表示不存在
function has(Set storage self, int v) internal view returns (bool) {
for (uint i = 0; i < self.elements.length; i++)
if (self.elements[i] == v) return true;
return false;
}
/// @notice 插入元素到集合中
/// @param self 集合结构体
/// @param v 要插入的元素
/// @return bool true 表示插入成功,false 表示元素已存在
function insert(Set storage self, int v) internal returns (bool) {
if (has(self, v)) return false;
self.elements.push(v);
return true;
}
/// @notice 从集合中移除元素
/// @param self 集合结构体
/// @param v 要移除的元素
/// @return bool true 表示移除成功,false 表示元素不存在
function remove(Set storage self, int v) internal returns (bool) {
for (uint i = 0; i < self.elements.length; i++)
if (self.elements[i] == v) {
for (i++; i < self.elements.length; i++)
self.elements[i - 1] = self.elements[i];
self.elements.pop();
return true;
}
return false;
}
/// @notice 并集操作
/// @param self 集合结构体,并集结果仍存回此集合
/// @param unionSet 将此集合元素并入 self 集合
function union(Set storage self, Set storage unionSet) internal {
for (uint i = 0; i < unionSet.elements.length; i++)
insert(self, unionSet.elements[i]);
}
/// @notice 交集操作
/// @param self 集合结构体,交集结果仍存回此集合
/// @param diffSet 将此集合元素与 self 取交集,不修改此集合
function diff(Set storage self, Set storage diffSet) internal {
for (uint i = 0; i < diffSet.elements.length; i++)
remove(self, diffSet.elements[i]);
}
}
在我们的合约文件内,我们导入该库并尝试使用它:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;
// 导入 SetOp 库
import { SetOp, Set } from "./Set.sol";
contract TestSet {
Set set1;
Set set2;
function test() public returns (Set memory, Set memory) {
SetOp.insert(set1, 0);
SetOp.insert(set1, 2);
SetOp.insert(set1, 4);
SetOp.insert(set1, 6);
// set1: 0, 2, 4, 6
SetOp.insert(set2, 1);
SetOp.insert(set2, 3);
SetOp.insert(set2, 5);
SetOp.insert(set2, 7);
// set2: 1, 3, 5, 7
SetOp.remove(set1, 2);
SetOp.remove(set2, 5);
// set1: 0, 4, 6
// set2: 1, 3, 7
SetOp.union(set1, set2);
// set1: 0, 4, 6, 1, 3, 7
// set2: 1, 3, 7
SetOp.diff(set2, set1);
// set1: 0, 4, 6, 1, 3, 7
// set2:
return (set1, set2);
}
}