Solidity 教程系列12 - 库的使用

Solidity 教程系列第12篇 - Solidity 视图函数、纯函数讲解。
Solidity 系列完整的文章列表请查看大纲

写在前面

Solidity 是以太坊智能合约编程语言,阅读本文前,你应该对以太坊、智能合约有所了解,
如果你还不了解,建议你先看以太坊是什么

库与合约类似,它也部署在一个指定的地址上(仅被部署一次,当代码在不同的合约可反复使用),然后通过EVM的特性DELEGATECALL (Homestead之前是用CALLCODE)来复用代码。库函数在被调用时,库代码是在发起合约(下文称主调合约:主动发起DELEGATECALL调用的合约)的上下文中执行的,使用this将会指向到主调合约,而且库代码可以访问主调合约的存储(storage)。

因为库合约是一个独立的代码,它仅可以访问主调合约明确提供的状态变量,否则,没办法法去知道这些状态变量。

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

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

不会修改状态变量(例如被声明viewpure)库函数只能通过直接调用(如不用DELEGATECALL),是因为其被认为是状态无关的。

库有许多使用场景。两个主要的场景如下:

  1. 如果有许多合约,它们有一些共同代码,则可以把共同代码部署成一个库。这将节省gas,因为gas也依赖于合约的规模。因此,可以把库想象成使用其合约的父合约。使用父合约(而非库)切分共同代码不会节省gas,因为在Solidity中,继承通过复制代码工作。

  2. 库可用于给数据类型添加成员函数。(参见下一节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.containsSet.removeSet.insert都会编译为以DELEGATECALL的方式调用外部合约和库。如果使用库,需要注意的是一个真实的外部函数调用发生了。尽管msg.sender,msg.value,this还会保持它们在主调合约中的值(在Homestead之前,由于实际使用的是CALLCODE,msg.sender,msg.value会变化)。

下面的例子演示了在库中如何使用memory类型和内部函数(inernal function)来实现一个自定义类型,而不会用到外部函数调用(external function)。

```js
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;
    }
}
top Created with Sketch.