库(Libraries)
库与合约类似,但它的目的是在一个指定的地址,且仅部署一次,然后通过EVM的特性DELEGATECALL
(Homestead之前是用CALLCODE
)来复用代码。这意味着库函数调用时,它的代码是在调用合约的上下文中执行。使用this
将会指向到调用合约,而且可以访问调用合约的存储(storage)
。因为一个合约是一个独立的代码块,它仅可以访问调用合约明确提供的状态变量(state variables)
,否则除此之外,没有任何方法去知道这些状态变量。
使用库合约的合约,可以将库合约视为隐式的父合约(base contracts)
,当然它们不会显式的出现在继承关系中。但调用库函数的方式非常类似,如库L
有函数f()
,使用L.f()
即可访问。此外,internal
的库函数对所有合约可见,如果把库想像成一个父合约就能说得通了。当然调用内部函数使用的是internal
的调用惯例,这意味着所有internal
类型可以传进去,memory
类型则通过引用传递,而不是拷贝的方式。为了在EVM中实现这一点,internal
的库函数的代码和从其中调用的所有函数将被拉取(pull into)
到调用合约中,然后执行一个普通的JUMP
来代替DELEGATECALL
。
下面的例子展示了如何使用库(后续在using for
章节有一个更适合的实现Set
的例子)。
pragma solidity ^0.4.0;
library Set {
// We define a new struct datatype that will be used to
// hold its data in the calling contract.
struct Data { mapping(uint => bool) flags; }
// Note that the first parameter is of type "storage
// reference" and thus only its storage address and not
// its contents is passed as part of the call. This is a
// special feature of library functions. It is idiomatic
// to call the first parameter 'self', if the function can
// be seen as a method of that object.
function insert(Data storage self, uint value)
returns (bool)
{
if (self.flags[value])
return false; // already there
self.flags[value] = true;
return true;
}
function remove(Data storage self, uint value)
returns (bool)
{
if (!self.flags[value])
return false; // not there
self.flags[value] = false;
return true;
}
function contains(Data storage self, uint value)
returns (bool)
{
return self.flags[value];
}
}
contract C {
Set.Data knownValues;
function register(uint value) {
// The library functions can be called without a
// specific instance of the library, since the
// "instance" will be the current contract.
if (!Set.insert(knownValues, value))
throw;
}
// In this contract, we can also directly access knownValues.flags, if we want.
}
上面的例子中:
-
Library
定义了一个数据结构体,用来在调用的合约中使用(库本身并未实际存储的数据)。如果函数需要操作数据,这个数据一般是通过库函数的第一个参数传入,按惯例会把参数名定为self
。 - 另外一个需要留意的是上例中
self
的类型是storage
,那么意味着传入的会是一个引用,而不是拷贝的值,那么修改它的值,会同步影响到其它地方,俗称引用传递,非值传递。 - 库函数的使用不需要实例化,
c.register
中可以看出是直接使用Set.insert
。但实际上当前的这个合约本身就是它的一个实例。 - 这个例子中,
c
可以直接访问,knownValues
。虽然这个值主要是被库函数使用的。
当然,你完全可以不按上面的方式来使用库函数,可以不需要定义结构体,不需要使用storage
类型的参数,还可以在任何位置有多个storage
的引用类型的参数。
调用Set.contains
,Set.remove
,Set.insert
都会编译为以DELEGATECALL
的方式调用external
的合约和库。如果使用库,需要注意的是一个实实在在的外部函数调用发生了。尽管msg.sender
,msg.value
,this
还会保持它们在此调用中的值(在Homestead
之前,由于实际使用的是CALLCODE
,msg.sender
,msg.value
会变化)。
下面的例子演示了如何使用memory
类型和内部函数(inernal function)
,来实现一个自定义类型,但不会用到外部函数调用(external function)
。
pragma solidity ^0.4.0;
library BigInt {
struct bigint {
uint[] limbs;
}
function fromUint(uint x) internal returns (bigint r) {
r.limbs = new uint[](1);
r.limbs[0] = x;
}
function add(bigint _a, bigint _b) internal 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 returns (uint) {
return _limb < _a.limbs.length ? _a.limbs[_limb] : 0;
}
function max(uint a, uint b) private returns (uint) {
return a > b ? a : b;
}
}
contract C {
using BigInt for BigInt.bigint;
function f() {
var x = BigInt.fromUint(7);
var y = BigInt.fromUint(uint(-1));
var z = x.add(y);
}
}
因为编译器并不知道库最终部署的地址。这些地址须由linker
填进最终的字节码中(使用命令行编译器来进行联接)。如果地址没有以参数的方式正确给到编译器,编译后的字节码将会仍包含一个这样格式的占们符_Set___
(其中Set
是库的名称)。可以通过手动将所有的40个符号替换为库的十六进制地址。
对比普通合约来说,库的限制:
- 无
状态变量(state variables)
。 - 不能继承或被继承
- 不能接收
ether
。
这些限制将来也可能被解除!
附着库(Using for)
指令using A for B;
用来附着库里定义的函数(从库A
)到任意类型B
。这些函数将会默认接收调用函数对象的实例作为第一个参数。语法类似,python
中的self
变量一样。
using A for *
的效果是,库A
中的函数被附着在做任意的类型上。
在这两种情形中,所有函数,即使那些第一个参数的类型与调用函数的对象类型不匹配的,也被附着上了。类型检查是在函数被真正调用时,函数重载检查也会执行。
using A for B;
指令仅在当前的作用域有效,且暂时仅仅支持当前的合约这个作用域,后续也非常有可能解除这个限制,允许作用到全局范围。如果能作用到全局范围,通过引入一些模块(module),数据类型将能通过库函数扩展功能,而不需要每个地方都得写一遍类似的代码了。
下面我们来换个方式重写set
的例子。
pragma solidity ^0.4.0;
// This is the same code as before, just without comments
library Set {
struct Data { mapping(uint => bool) flags; }
function insert(Data storage self, uint value)
returns (bool)
{
if (self.flags[value])
return false; // already there
self.flags[value] = true;
return true;
}
function remove(Data storage self, uint value)
returns (bool)
{
if (!self.flags[value])
return false; // not there
self.flags[value] = false;
return true;
}
function contains(Data storage self, uint value)
returns (bool)
{
return self.flags[value];
}
}
contract C {
using Set for Set.Data; // this is the crucial change
Set.Data knownValues;
function register(uint value) {
// Here, all variables of type Set.Data have
// corresponding member functions.
// The following function call is identical to
// Set.insert(knownValues, value)
if (!knownValues.insert(value))
throw;
}
}
我们也可以通过这种方式来扩展基本类型(elementary types)
。
pragma solidity ^0.4.0;
library Search {
function indexOf(uint[] storage self, uint value) 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) {
data.push(value);
}
function replace(uint _old, uint _new) {
// This performs the library function call
uint index = data.indexOf(_old);
if (index == uint(-1))
data.push(_new);
else
data[index] = _new;
}
}
需要注意的是所有库调用都实际上是EVM函数调用。这意味着,如果你传的是memory
类型的,或者是值类型(vaue types)
,那么仅会传一份拷贝,即使是self
变量。变通之法就是使用存储(storage)
类型的变量,这样就不会拷贝内容。
处于某些特定的环境下,可以看到评论框,欢迎留言交流^_^。