Loading...

智能合约语言 Solidity 教程 – 库的使用

智能合约语言 Solidity 教程 – 库的使用

电报联系方式

库与合约相似,它也被部署到特定地址上,通常只需要部署一次。然后,通过EVM的DELEGATECALL特性(在Homestead之前使用CALLCODE)来在不同合约中重用库的代码。当库函数被调用时,库的代码在主调合约(即主动执行DELEGATECALL的合约)的上下文中执行。使用”this”关键字会将库代码指向主调合约,同时也允许库代码访问主调合约的存储(storage)。

智能合约语言 Solidity 教程 - 库的使用

由于库合约是独立的代码单元,它只能访问主调合约显式提供的状态变量,无法直接访问其他未提供的状态变量。

对比普通合约来说,库存在以下的限制(这些限制将来也可能在将来的版本被解除):

  1. 无状态变量(state variables)。
  2. 不能继承或被继承
  3. 不能接收以太币
  4. 不能销毁一个库

库函数通常不修改状态变量(例如声明为view或pure),因此它们被认为是状态无关的。此外,库函数可以通过直接调用而不是DELEGATECALL来访问状态变量。

库在许多使用场景中非常有用,主要包括以下两个:

  1. 共享代码:如果有多个合约中存在相同的代码段,你可以将这段代码部署为一个库。这有助于减少gas成本,因为合约的大小会影响gas的消耗。可以将库视为其合约的“父合约”。使用继承(而不是库)将相同的代码复制到多个合约中无法减少gas成本,因为在Solidity中,继承是通过复制代码的方式工作的。
  2. 扩展数据类型:库还可用于为数据类型添加成员函数。这使得你可以在不修改原始数据类型的情况下,为它们添加额外的功能。

由于库在调用方式上与隐式的父合约非常相似(尽管它们不会在继承关系中明确列出,但使用库函数与调用继承的父合约函数方式几乎一样,例如,如果库L具有函数f(),可以通过L.f()来访问),库中的内部(internal)函数会被复制到使用它的合约中。同样,根据内部函数的调用方式,它们可以接受所有内部类型的参数,并对内存类型参数进行引用传递,而不是拷贝传递。此外,库中的结构体(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,这些值可能会更改)。

下面的例子演示了在库中如何使用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);
}
}

合约的源代码无法直接包含库的地址信息。库的地址是在编译时通过参数提供给编译器的。这些地址需要由链接器在最终的字节码中进行填充。要执行这个操作,可以使用命令行编译器进行链接,需要完成TODO部分的工作。

如果没有正确将地址以参数形式提供给编译器,编译后的字节码将仍然包含占位符”Set“(其中”Set”是库的名称)。你可以手动将所有这些占位符替换为库的实际十六进制地址。

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)类型的引用来传递参数。

© 版权声明

相关文章

暂无评论

暂无评论...