跳到主要内容

理解库

在软件开发中,有两个方式来实现代码的重用,一个是继承,一个是组合。库(Library)就是通过组合的方式来实现代码的复用。

下面的图示了继承和组合的区别:

solidity - 继承与库(组合)的区别

继承表示“是” (is) , 如猫/狗(派生类/合约)是 动物(父类/合约)。

组合表示“有” (has), 如猫/狗有四条腿。

库(Library)是一组预先编写好功能模块的集合,使用库可提高开发效率,并且一些知名库经过多次审计及时间考验,使用他们他们也可以提高代码质量。

我们常说要避免重复造轮子,轮子很多时候指的就是各种库。

OpenZepplin 代码库中,大量使用了继承与库,前面介绍的ERC20 使用的是继承,而utils 工具中,有很多的使用库,例如:Address 用来帮助我们进行各种底层调用。

使用库

库使用关键字library来定义,例如,下面的代码定义了一个Math库。

pragma solidity ^0.8.19;

library Math {
function max(uint256 a, uint256 b) internal pure returns (uint256) {
return a > b ? a : b;
}

function min(uint256 a, uint256 b) internal pure returns (uint256) {
return a < b ? a : b;
}
}

Math 库封装了两个常用方法,max() 用来获取最大值,min() 用来获取最小值,这是库最典型的用法,将常用的功能封装起来,以便在多个不同的合约中复用。

这个是Math库,其实是 OpenZepplin Math 的简化版本。

在合约中引入库之后,可以直接调用库内的函数,参考下面的TestMax合约:

import "./Math.sol";

contract TestMax {
function max(uint x, uint y) public pure returns (uint) {
return Math.max(x, y);
}
}

在使用库时,要牢记:库是函数的封装, 库是无状态的,库内不能声明变量,也不能给库发送Ether。

库有两种使用方式:一种是库代码嵌入引用的合约里部署(可以称为“内嵌库”),一种是作为库合约单独部署(可以称为“链接库”)。

内嵌库

如果合约引用的库函数都是内部函数,那么编译器在编译合约的时候,会把库函数的代码嵌入合约里,就像合约自己实现了这些函数,这时的库并不会单独部署,上面的Math库就属于这个情况, 它的代码会在 TestMax 合约编译时,加入到 TestMax 合约里。

绝大部分的库都是内嵌的方式在使用。

注意:内嵌库在合约的字节码层,是没有复用的,内嵌库的字节码会存在于每一个引入该库的合约字节码中。

链接库

如果库代码内有公共或外部函数,库就可以被单独部署,它在以太坊链上有自己的地址,引用合约在部署合约的时候,需要通过库地址把库“链接”进合约里,合约是通过委托调用的方式来调用库函数的。

下图是一个内嵌库和链接库在部署后的对比图:

Solidity 内联库与链接库

在委托调用的方式下库合约函数是在发起的合约(下文称“主调合约”,即发起调用的合约)的上下文中执行的,因此库合约函数中使用的变量(如果有的话)都来自主调合约的变量(库代码不能声明自己的状态变量),库合约函数使用的this也是主调合约的地址。

把前面的Math库的add函数修改为外部函数,就可以通过链接库的方式来使用,示例代码如下:

pragma solidity ^0.8.19;

library Math {
function max(uint256 a, uint256 b) external pure returns (uint256) {
return a > b ? a : b;
}

}

TestMax代码不用作任何的更改,不过因为Math库是独立部署的, TestMax合约要调用Math库就必须先知道后者的地址,这相当于TestMax合约会依赖于Math库,因此部署TestMax合约会有一点不同,需要让 TestMax合约与Math库建立链接, Solidity 开发框架会帮助我们进行链接,以Hardhat 为例,部署脚本这样写就好:

  const ExLib = await hre.ethers.getContractFactory("Math");
const lib = await ExLib.deploy();
await lib.deployed();

await hre.ethers.getContractFactory("TestMax", {
libraries: {
Library: lib.address,
},
});

Using for

上面,我们通过Math.max(x, y)语法来调用库函数,还有一个语法糖是使用using LibA for B,它表示把所有LibA的库函数关联到类型B。这样就可以在B类型直接调用库的函数,代码示例如下:

contract testLib {
using Math for uint;

function callMax(uint x, uint y) public pure returns (uint) {
return x.max(y);
}

}

使用using...for...看上去就像扩展了类型的能力。比如,我们可以给数组添加一个indexOf函数,查看一个元素在数组中的位置,示例代码如下:

pragma solidity >=0.4.16;


library Search {
function indexOf(uint[] storage self, uint value)
public
view
returns (uint)
{
for (uint i = 0; i < self.length; i++)
if (self[i] == value) return i;
return uint(-1);
}
}


contract C {
using Search for uint[];
uint[] data;


function append(uint value) public {
data.push(value);
}


function replace(uint _old, uint _new) public {
// 执行库函数调用
uint index = data.indexOf(_old);
if (index == uint(-1))
data.push(_new);
else
data[index] = _new;
}
}

这段代码中indexOf的第一个参数存储变量self,实际上对应着合约 C 的data变量。

路上使用using LibA for B语法糖,大部分时候,可以让我们的代码更简洁。

例如:有一个库函数:isContract(address addr) , 可以使用 addr.isContract() 来调用库函数,代码就更简洁了。

若使用 using LibA for * 可以把 LibA 中的函数关联到任意的类型上。

小结

本文,我们学习了库的的概念, 总结要点:

  • 库是函数的封装, 主要用于代码复用
  • 库是没有状态的,也不能给库发送 Ether
  • 如果库函数都是 internal 的,库代码会嵌入到合约。
  • 如果库函数有external或 public ,库需要单独部署,并在部署合约时进行链接,EVM 中使用委托调用库方法。
  • 给类型扩展功能:Using lib for type; 如: using Math for uint;

DeCert.me 码一个未来,DeCert 让每一位开发者轻松构建自己的可信履历。

DeCert.me 由登链社区 @UpchainDAO 孵化,欢迎 Discord 频道 一起交流。

本教程来自贡献者 @Tiny熊