💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
# 库(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`填进最终的字节码中(使用[命令行编译器](http://solidity.readthedocs.io/en/develop/miscellaneous.html#commandline-compiler)来进行联接)。如果地址没有以参数的方式正确给到编译器,编译后的字节码将会仍包含一个这样格式的占们符`_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)`类型的变量,这样就不会拷贝内容。