这是 Solidity 教程系列文章第 12 篇介绍库的使用: 库与合约的不同, 使用库的正姿势.
Solidity 系列完整的文章列表请查看分类 - Solidity.
写在前面
Solidity 是以太坊智能合约编程语言, 阅读本文前, 你应该对以太坊, 智能合约有所了解,
如果你还不了解, 建议你先看以太坊是什么
欢迎订阅 区块链技术专栏 https://xiaozhuanlan.com/blockchaincore 阅读更全面的分析文章.
库
库与合约类似, 它也部署在一个指定的地址上 (仅被部署一次, 当代码在不同的合约可反复使用), 然后通过 EVM 的特性 DELEGATECALL (Homestead 之前是用 CALLCODE) 来复用代码. 库函数在被调用时, 库代码是在发起合约 (下文称 主调合约 : 主动发起 DELEGATECALL 调用的合约) 的上下文中执行的, 使用 this 将会指向到主调合约, 而且库代码可以访问主调合约的存储(storage).
因为库合约是一个独立的代码, 它仅可以访问主调合约明确提供的状态变量, 否则, 没办法法去知道这些状态变量.
对比普通合约来说, 库存在以下的限制(这些限制将来也可能在将来的版本被解除):
无状态变量(state variables).
不能继承或被继承
不能接收以太币
不能销毁一个库
不会修改状态变量 (例如被声明 view 或 pure ) 库函数只能通过直接调用(如不用 DELEGATECALL ), 是因为其被认为是状态无关的.
库有许多使用场景. 两个主要的场景如下:
如果有许多合约, 它们有一些共同代码, 则可以把共同代码部署成一个库. 这将节省 gas, 因为 gas 也依赖于合约的规模. 因此, 可以把库想象成使用其合约的父合约. 使用父合约 (而非库) 切分共同代码不会节省 gas, 因为在 Solidity 中, 继承通过复制代码工作.
库可用于给数据类型添加成员函数.(参见下一节 Using for)
由于库被当作隐式的父合约 (不过它们不会显式的出现在继承关系中, 但调用库函数和调用父合约的方式是非常类似的, 如库 L 有函数 f(), 使用 L.f() 即可访问). 库里面的内部 (internal) 函数被复制给使用它的合约;
同样按调用内部函数的调用方式, 这意味着所有内部类型可以传进去, memory 类型则通过引用传递, 而不是拷贝的方式. 同样库里面的结构体 structs 和枚举 enums 也会被复制给使用它的合约.
因此, 如果一个库里只包含内部函数或结构体或枚举, 则不需要部署库, 因为库里面的所有内容都被复制给使用它的合约.
下面的例子展示了如何使用库.
- pragma solidity ^0.4.16;
- library Set {
- // 定义了一个结构体, 保存主调函数的数据(本身并未实际存储的数据).
- struct Data { mapping(uint => bool) flags; }
- // self 是一个存储类型的引用(传入的会是一个引用, 而不是拷贝的值), 这是库函数的特点.
- // 参数名定为 self 也是一个惯例, 就像调用一个对象的方法一样.
- function insert(Data storage self, uint value)
- public
- returns (bool)
- {
- if (self.flags[value])
- return false; // 已存在
- self.flags[value] = true;
- return true;
- }
- function remove(Data storage self, uint value)
- public
- returns (bool)
- {
- if (!self.flags[value])
- return false;
- self.flags[value] = false;
- return true;
- }
- function contains(Data storage self, uint value)
- public
- view
- returns (bool)
- {
- return self.flags[value];
- }
- }
- contract C {
- Set.Data knownValues;
- function register(uint value) public {
- // 库函数不需要实例化就可以调用, 因为实例就是当前的合约
- require(Set.insert(knownValues, value));
- }
- // 在这个合约中, 如果需要的话可以直接访问 knownValues.flags,
- }
当然, 我们也可以不按上面的方式来使用库函数, 可以不定义结构体, 可以不使用 storage 类型引用的参数, 还可以在任何位置有多个 storage 的引用类型的参数.
调用 Set.contains , Set.remove , Set.insert 都会编译为以 DELEGATECALL 的方式调用外部合约和库. 如果使用库, 需要注意的是一个真实的外部函数调用发生了. 尽管 msg.sender,msg.value,this 还会保持它们在主调合约中的值(在 Homestead 之前, 由于实际使用的是 CALLCODE,msg.sender,msg.value 会变化).
下面的例子演示了在库中如何使用 memory 类型和内部函数 (inernal function) 来实现一个自定义类型, 而不会用到外部函数调用(external function).
- pragma solidity ^0.4.16;
- library BigInt {
- struct bigint {
- uint[] limbs;
- }
- function fromUint(uint x) internal pure returns (bigint r) {
- r.limbs = new uint[](1);
- r.limbs[0] = x;
- }
- function add(bigint _a, bigint _b) internal pure returns (bigint r) {
- r.limbs = new uint[](max(_a.limbs.length, _b.limbs.length));
- uint carry = 0;
- for (uint i = 0; i <r.limbs.length; ++i) {
- uint a = limb(_a, i);
- uint b = limb(_b, i);
- r.limbs[i] = a + b + carry;
- if (a + b < a || (a + b == uint(-1) && carry> 0))
- carry = 1;
- else
- carry = 0;
- }
- if (carry> 0) {
- // too bad, we have to add a limb
- uint[] memory newLimbs = new uint[](r.limbs.length + 1);
- for (i = 0; i <r.limbs.length; ++i)
- newLimbs[i] = r.limbs[i];
- newLimbs[i] = carry;
- r.limbs = newLimbs;
- }
- }
- function limb(bigint _a, uint _limb) internal pure returns (uint) {
- return _limb < _a.limbs.length ? _a.limbs[_limb] : 0;
- }
- function max(uint a, uint b) private pure returns (uint) {
- return a> b ? a : b;
- }
- }
- contract C {
- using BigInt for BigInt.bigint;
- function f() public pure {
- var x = BigInt.fromUint(7);
- var y = BigInt.fromUint(uint(-1));
- var z = x.add(y);
- }
- }
合约的源码中不能添加库地址, 它是在编译时向编译器以参数形式提供的 (这些地址须由链接器(linker) 填进最终的字节码中, 使用命令行编译器来进行联接 TODO). 如果地址没有以参数的方式正确给到编译器, 编译后的字节码将会仍包含一个这样格式的占们符 Set__ (其中 Set 是库的名称). 可以通过手动将所有的 40 个符号替换为库的十六进制地址.
Using for 指令
指令 using A for B; 用来把库函数 (从库 A) 关联到类型 B. 这些函数将会把调用函数的实例作为第一个参数. 语法类似, python 中的 self 变量一样. 例如: A 库有函数 add(B b1, B b2) , 则使用 Using A for B 指令后, 如果有 B b1 就可以使用 b1.add(b2) .
using A for * 表示库 A 中的函数可以关联到任意的类型上.
在这两种情形中, 所有函数, 即使第一个参数的类型与调用函数的对象类型不匹配的, 也会被关联上. 类型检查是在函数被调用时执行, 以及函数重载是也会执行检查.
using A for B; 指令仅在当前的作用域有效, 且暂时仅仅支持当前的合约这个作用域, 后续也非常有可能解除这个限制, 允许作用到全局范围. 如果能作用到全局范围, 通过引入一些模块(module), 数据类型将能通过库函数扩展功能, 而不需要每个地方都得写一遍类似的代码了.
下面我们使用 Using for 指令方式重写上一节 Set 的例子:
- pragma solidity ^0.4.16;
- // 库合约代码和上一节一样
- library Set {
- struct Data { mapping(uint => bool) flags; }
- function insert(Data storage self, uint value)
- public
- returns (bool)
- {
- if (self.flags[value])
- return false; // already there
- self.flags[value] = true;
- return true;
- }
- function remove(Data storage self, uint value)
- public
- returns (bool)
- {
- if (!self.flags[value])
- return false; // not there
- self.flags[value] = false;
- return true;
- }
- function contains(Data storage self, uint value)
- public
- view
- returns (bool)
- {
- return self.flags[value];
- }
- }
- contract C {
- using Set for Set.Data; // 这是一个关键的变化
- Set.Data knownValues;
- function register(uint value) public {
- // 现在 Set.Data 都对应的成员方法
- // 效果和 Set.insert(knownValues, value)相同
- require(knownValues.insert(value));
- }
- }
同样可以使用 Using for 的方式来对基本类型 (elementary types) 进行扩展:
- 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;
- }
- }
需要注意的是所有库调用都实际上是 EVM 函数调用. 这意味着, 如果传的是 memory 类型的, 或者是值类型, 那么进行一次拷贝, 即使是 self 变量, 解决方法是使用存储 (storage) 类型的引用来避免拷贝内容.
深入浅出区块链 - 系统学习区块链, 打造最好的区块链技术博客.
来源: http://www.tuicool.com/articles/mIBrayV