原文链接
date:20170710
Solidity中合约的概念和其他面向对象语言中的类差不多。他们都有状态变量来存储永久数据,有函数来改变这些数据。调用不同的合约(实例)的函数其实是执行EVM的函数调用,并且会改变上下文,原上下文中的变量就变得不可访问了。
创建合约
合约可以从“外部”创建或者从Solidity合约中创建。当合约创建的时候,合约的构造函数(跟合约的名称相同)就会执行一次。
构造函数是可选的。但是只允许有一个构造函数,这意味着不支持重载。
在web3.js
中,例如javascript API,如下所示:
// 需要指定源码,包含合约名称(?Need to specify some source including contract name for the data param below)
var source = "contract CONTRACT_NAME { function CONTRACT_NAME(uint a, uint b) {} }";
// 编译器生成的json格式的abi 数组
var abiArray = [
{
"inputs":[
{"name":"x","type":"uint256"},
{"name":"y","type":"uint256"}
],
"type":"constructor"
},
{
"constant":true,
"inputs":[],
"name":"x",
"outputs":[{"name":"","type":"bytes32"}],
"type":"function"
}
];
var MyContract_ = web3.eth.contract(source);
MyContract = web3.eth.contract(MyContract_.CONTRACT_NAME.info.abiDefinition);
// 发布新合约
var contractInstance = MyContract.new(
10,
11,
{from: myAccount, gas: 1000000}
);
在内部,构造函数的参数在构造函数代码创建之后才被传递进去,但是如果你是使用web3.js
,你就无须关心这个问题。
如果一个合约想要创建另一个合约,那么必须要给创建者提供创建合约的源码(和二进制数据)。这意味这循环依赖创建是不可行的。
pragma solidity ^0.4.0;
contract OwnedToken {
// TokenCreator 是一个合约类型数据,在下面代码中定义
// 当他没有被用来创建一个合约的时候,这样引用是没有问题的。
TokenCreator creator;
address owner;
bytes32 name;
// 这是一个构造函数,在其中注册了创建者,并给name变量赋值。
function OwnedToken(bytes32 _name) {
// 状态变量可以通过变量名来访问,而不是通过this.owner.
// 这个准则也同样适用于函数,尤其是在构造函数中。
// 你只能通过“内部调用”的方式来调用他们。
// 因为合约本身都没有创建。
owner = msg.sender;
// 我们通过显式类型转换将`address`转换为`TokenCreator`类型。
// 并且假设调用合约的类型是TokenCreator。其实并没有方法来检验的。
creator = TokenCreator(msg.sender);
name = _name;
}
function changeName(bytes32 newName) {
// 只有创建者才能改变名称。
// 以下的比较是可行的,因为合约隐式的转换为地址类型.
if (msg.sender == address(creator))
name = newName;
}
function transfer(address newOwner) {
// 只有当前拥有者才能交易token
if (msg.sender != owner) return;
// 交易之后,我们同样要知道是否交易成功。
// 注意,这里调用了其他函数,函数在下面定义。
// 如果调用失败,例如gas不足,执行会马上在此停止。
if (creator.isTokenTransferOK(owner, newOwner))
owner = newOwner;
}
}
contract TokenCreator {
function createToken(bytes32 name)
returns (OwnedToken tokenAddress)
{
// 创建新的Token合约,并返回他的地址。
// 在Javascript侧,返回的类型只是简单的`address`
// 因为在ABI中最接近的可用的类型。
return new OwnedToken(name);
}
function changeName(OwnedToken tokenAddress, bytes32 name) {
// 同样, 外部类型"tokenAddress"只是简单的"address"类型.
tokenAddress.changeName(name);
}
function isTokenTransferOK(
address currentOwner,
address newOwner
) returns (bool ok) {
// 监测任意条件
address tokenAddress = msg.sender;
return (keccak256(newOwner) & 0xff) == (bytes20(tokenAddress) & 0xff);
}
}
可见性和getter
我们知道,函数调用有两种方式(内部调用并不会产生真实的EVM调用(也被成为消息调用)但是外部函数会),但是函数和状态变量的可见性有四种。
函数可以被指定为external
,public
,internal
或者private
,默认是public
的。对于状态变量,external
是非法的,默认是internal
。
external
:
外部函数是合约接口的一部分,意味着可以通过交易从其他合约中调用。外部函数f
不能在内部被调用(例如,f()
是不可行的,但是this.f()
是可以的)。有时候外部函数的执行效率会更高,当他们收到很大的数组数据的时候。
public
:
公有函数是合约接口的一部分,可以通过内部或者消息来调用。对于公有的状态变量,会自动创建getter函数。
internal
:
这些函数和变量只能在内部调用(例如,从当前合约或者从该合约派生的合约),不需要this
前缀。
private
:
私有函数和状态变量只能在他们定义的合约中可见,在派生的合约中不可见。
注意:合约里的所有都对外部观察者可见。private
只是为了阻止其他合约访问和改变信息,但是可以区块链中可见。
可见性指示在类型和变量名之间,对于函数,可见性的位置在参数列表和返回值之间。
pragma solidity ^0.4.0;
contract C {
function f(uint a) private returns (uint b) { return a + 1; }
function setData(uint a) internal { data = a; }
uint public data;
}
在以下的例子中,合约D
可以调用c.getData()
来获取data
中的数据。但是不能调用f
函数。合约E
是从合约C
派生而来,因此,可以调用compute
。
// 注释不会被编译
pragma solidity ^0.4.0;
contract C {
uint private data;
function f(uint a) private returns(uint b) { return a + 1; }
function setData(uint a) { data = a; }
function getData() public returns(uint) { return data; }
function compute(uint a, uint b) internal returns (uint) { return a+b; }
}
contract D {
function readData() {
C c = new C();
uint local = c.f(7); // 错误,f不可见
c.setData(3);
local = c.getData();
local = c.compute(3, 5); // 错误"compute" 不可见
}
}
contract E is C {
function g() {
C c = new C();
uint val = compute(3, 5); // acces to internal member (from derivated to parent contract)
}
}
getter函数
编译器会给所有的公有变量生成getter函数。以下的合约,编译器会生成一个data
函数,没有其他参数并返回uint
类型的data
变量的值。变量在定义的时候会进行初始化。
pragma solidity ^0.4.0;
contract C {
uint public data = 42;
}
contract Caller {
C c = new C();
function f() {
uint local = c.data();
}
}
getter函数在外部可见。如果通过内部调用的方式访问(没有this
),就会当作是状态变量。如果通过外部的方式调用(有this
),就会当作是函数。
pragma solidity ^0.4.0;
contract C {
uint public data;
function x() {
data = 3; //内部访问
uint val = this.data(); // 外部访问
}
}
以下的例子会更加复杂:
pragma solidity ^0.4.0;
contract Complex {
struct Data {
uint a;
bytes3 b;
mapping (uint => uint) map;
}
mapping (uint => mapping(bool => Data[])) public data;
}
将会生成如下函数:
function data(uint arg1, bool arg2, uint arg3) returns (uint a, bytes3 b) {
a = data[arg1][arg2][arg3].a;
b = data[arg1][arg2][arg3].b;
}
注意,结构体中的mapping被忽略了,因为没有好的方法来为mapping提供key。
函数修改器(function modifiers)
修改器可以用来轻易的改变函数的执行效果。例如,他们可以在执行函数之前自动的检查先决条件。修改器可以继承,并且可以被派生合约复写。
pragma solidity ^0.4.11;
contract owned {
function owned() { owner = msg.sender; }
address owner;
// This contract only defines a modifier but does not use
// it - it will be used in derived contracts.
// The function body is inserted where the special symbol
// "_;" in the definition of a modifier appears.
// This means that if the owner calls this function, the
// function is executed and otherwise, an exception is
// thrown.
modifier onlyOwner {
require(msg.sender == owner);
_;
}
}
contract mortal is owned {
// This contract inherits the "onlyOwner"-modifier from
// "owned" and applies it to the "close"-function, which
// causes that calls to "close" only have an effect if
// they are made by the stored owner.
function close() onlyOwner {
selfdestruct(owner);
}
}
contract priced {
// Modifiers can receive arguments:
modifier costs(uint price) {
if (msg.value >= price) {
_;
}
}
}
contract Register is priced, owned {
mapping (address => bool) registeredAddresses;
uint price;
function Register(uint initialPrice) { price = initialPrice; }
// It is important to also provide the
// "payable" keyword here, otherwise the function will
// automatically reject all Ether sent to it.
function register() payable costs(price) {
registeredAddresses[msg.sender] = true;
}
function changePrice(uint _price) onlyOwner {
price = _price;
}
}
contract Mutex {
bool locked;
modifier noReentrancy() {
require(!locked);
locked = true;
_;
locked = false;
}
/// This function is protected by a mutex, which means that
/// reentrant calls from within msg.sender.call cannot call f again.
/// The `return 7` statement assigns 7 to the return value but still
/// executes the statement `locked = false` in the modifier.
function f() noReentrancy returns (uint) {
require(msg.sender.call());
return 7;
}
}
函数可以有多个修改器,通过空格隔开。修改器的执行顺序为排列顺序。
警告:在Solidity早期版本中,在函数中的return
表达式有修改器的会有不同的执行效果。
Explicit returns from a modifier or function body only leave the current modifier or function body。返回变量被赋值并且控制流会在先前修改器的“ _ ”符号之后继续。
修改器允许任意表达式,所有对函数可见的符号对于修改器都是可见的。修改器中引入的符号在函数中并不可见(因为他们可能会被复写)。
静态状态变量
状态变量可以被声明为constant
。在这个情况下,变量必须在编译的时候被表达式赋于静态的值。所有访问storage,区块链数据(如now
,this.balance
或者block.number
)或者执行数据(msg.gas
)或者调用外部合约都是不允许的。在内存分配上可能会有单边效应的表达式是允许的,但是那些对其他内存对象有单边效应的是不允许的。(?Expressions that might have a side-effect on memory allocation are allowed, but those that might have a side-effect on other memory objects are not. )内建函数keccak256
,sha256
,ripemd160
,ecrecover
,addmod
和mulmod
是允许的(尽管他们调用了外部合约)。
允许内存分配单边效应的原因是它可以构建更加复杂的对象,例如查找表。这个功能还没有完全可用。
编译器不会对这些变量保留内存片,并且每个静态变量都会被替换为各自的静态表达式(优化器会计算表达式的值)。
现在还没有实现所有的静态类型。支持的类型是值类型和字符串。
pragma solidity ^0.4.0;
contract C {
uint constant x = 32**22 + 8;
string constant text = "abc";
bytes32 constant myHash = keccak256("abc");
}
静态函数
函数可以被声明为静态的,这种情况下,它们保证不回修改状态。(?Functions can be declared constant in which case they promise not to modify the state.)
pragma solidity ^0.4.0;
contract C {
function f(uint a, uint b) constant returns (uint) {
return a * (b + 42);
}
}
注意:getter函数被标记为constant。
警告:编译器不回强制要求静态函数返回值不会修改。
回调函数
合约可以有一个没命名的函数。这个函数没有参数也不能返回任何值。这个函数在合约被调用但是合约内没有该函数(或者不能提供该数据)的时候执行。
另外,无论何时,合约收到了以太币(没有数据),这个函数会被执行。在这个上下文中,这个函数调用只会消耗掉少量的gas(精确来说,是2300gas),所以将回调函数的开销降到最低是很重要的。
另外,下面的操作会消耗掉更多的gas。
- 写数据到storage
- 创建合约
- 调用外部函数,该外部函数可能消耗更多的gas
- 发送以太币
在发布合约之前,请保证你已经对你的回调函数进行了充分的测试,来保证执行开销小于2300gas。
警告: 合约收到了以太币(没有调用函数,例如使用send
或者transfer
)但是没有定义回调函数会抛出异常,并返回以太币(Solidity v0.4.0之前会不同)。所以如果你想要让合约接收以太币,你必须要实现一个回调函数。
pragma solidity ^0.4.0;
contract Test {
// 任何消息发给该合约都会执行该函数。(合约内没有其他函数)。
// 给这个合约发送以太币会产生异常。
// 因为这个函数没有实现payable修改器。
function() { x = 1; }
uint x;
}
// 这个合约会保留所有发送到该合约的以太币,且无法取回
contract Sink {
function() payable { }
}
contract Caller {
function callTest(Test test) {
test.call(0xabcdef01); // 哈希不存在
// 导致 test.x 赋值为1.
// 下面的代码不会被编译。即使有人发送以太币到这个合约,
// 交易也会失败并退回以太币
//test.send(2 ether);
}
}
事件
事件机制可以很方便的使用EVM的日志服务,它可以反过来用于调用去中心化的用户接口的javascript回调函数。
事件可以继承。当它们被调用的时候,它们会导致参数保存在交易日志里--区块链中一个特殊的数据结构。这些日志和合约地址相互关联。并且会被纳入到区块链中,将数据一致保持(在Frotier和homestead阶段会一致保存,但是在Serenity阶段可能会修改)。日志和事件数据在合约(即使是父合约)中是不可访问的。
日志的SPV(简单支付验证)证明是可行的,所以如果外部的实体提供了一个合约来提供证明,它可以用来证明日志确确实实存在于区块链中。但是需要注意,区块头必须要提供,因为我们只能访问最近的256块区块的hash。
如果参数数量大于3个,就会有一个indexed
属性,该属性可以使得参数可以被索引到:这可以在用户接口中实现过滤特定的值。
如果数组(包含string
和bytes
)可以被用来索引参数,它的Keccak-256hash值会保存下来作为主题。(?If arrays (including string and bytes) are used as indexed arguments, the Keccak-256 hash of it is stored as topic instead.)
事件签名的hash作为主题(topic)之一,除非该事件被声明为匿名anonymous
。这意味着无法通过名字来过滤匿名事件。
所有非索引的参数将会记录在日志的数据部分。
注意:索引参数不会被自动保存。你只可以搜索值,但是不可以获取值。(?
Indexed arguments will not be stored themselves. You can only search for the values, but it is impossible to retrieve the values themselves.)
pragma solidity ^0.4.0;
contract ClientReceipt {
event Deposit(
address indexed _from,
bytes32 indexed _id,
uint _value
);
function deposit(bytes32 _id) payable {
// 该函数的任何调用(即使是深度嵌套)都可以通过过滤关键字 `Deposit`,
// 在Javascript的API端检索到。
Deposit(msg.sender, _id, msg.value);
}
}
Javascript API的使用如下所示:
var abi = /* abi 是编译器生成的 */;
var ClientReceipt = web3.eth.contract(abi);
var clientReceipt = ClientReceipt.at(0x123 /* 地址 */);
var event = clientReceipt.Deposit();
// 等待改变
event.watch(function(error, result){
// result会包含很多信息,包括调用Deposit函数的参数。
if (!error)
console.log(result);
});
// 或者传递一个回调函数,马上开始监听
var event = clientReceipt.Deposit(function(error, result) {
if (!error)
console.log(result);
});
日志低级接口
也可以通过函数调用来访问日志机制的低级接口,函数log0
,log1
,log2
,log3
和log4
。logi
记录了i+1
个bytes32
类型的参数,第一个参数是用来记录日志的数据部分。其他则用来记录主题(topics)。上面例子的事件记录可以用log来实现同样的功能:
log3(
msg.value,
0x50cb9fe53daa9737b786ab3646f04d0150dc50ef4e75f59509d83667ad5adb20,
msg.sender,
_id
);
一大长串的十六进制数与keccak256("Deposit(address,hash256,uint256)")
相同。是事件的签名。
用来理解事件的其他资源
继承
Solidity支持多继承,通过拷贝代码,包括多态。
所有的函数调用是虚拟的,这意味着多数的派生函数会被调用,除非合约名称被指定。
当合约继承自多个合约,只会在区块链中创建一个合约。所有依赖的合约的代码会被拷贝进创建的合约。
继承系统和python的继承系统相似,尤其是多重继承。
详情参看以下例子:
pragma solidity ^0.4.0;
contract owned {
function owned() { owner = msg.sender; }
address owner;
}
// 使用 "is" 关键字来继承自其他合约。
// 派生合约可以访问其他所有非私有的成员变量,包括内部函数和状态变量
// 尽管它们不能通过`this`,从外部访问。
contract mortal is owned {
function kill() {
if (msg.sender == owner) selfdestruct(owner);
}
}
// 这些抽象合约只给编译器提供了接口。注意,函数没有函数体。
// 如果合约没有实现所有的函数,只能当作接口使用。
contract Config {
function lookup(uint id) returns (address adr);
}
contract NameReg {
function register(bytes32 name);
function unregister();
}
// 多重继承是可行的。注意”owned“也是“mortal”的基类,
// 但是只有一个“owner”实例(类似C++的虚拟继承)
contract named is owned, mortal {
function named(bytes32 name) {
Config config = Config(0xd5f9d8d94886e70b06e474c3fb14fd43e2f23970);
NameReg(config.lookup(1)).register(name);
}
// 函数可以被另一个具有相同名字和参数的函数复写。
// 如果复写的函数有不同的返回值,就会产生错误。
// 所有本地的或者基于消息的函数调用会把这些复写写入账户中(?Both local and message-based function calls take these overrides into account.)
function kill() {
if (msg.sender == owner) {
Config config = Config(0xd5f9d8d94886e70b06e474c3fb14fd43e2f23970);
NameReg(config.lookup(1)).unregister();
// 这可能调用一个特定的复写函数
mortal.kill();
}
}
}
// 如果构造函数有一个参数,这需要在开头提供 (or modifier-invocation-style at
// the constructor of the derived contract (see below)).
contract PriceFeed is owned, mortal, named("GoldFeed") {
function updateInfo(uint newInfo) {
if (msg.sender == owner) info = newInfo;
}
function get() constant returns(uint r) { return info; }
uint info;
}
注意上面的代码,我们调用mortal.kill()
来前向破坏请求。这种做法是有问题的,参看下面的例子。(?Note that above, we call mortal.kill() to “forward” the destruction request. The way this is done is problematic, as seen in the following example:)
pragma solidity ^0.4.0;
contract owned {
function owned() { owner = msg.sender; }
address owner;
}
contract mortal is owned {
function kill() {
if (msg.sender == owner) selfdestruct(owner);
}
}
contract Base1 is mortal {
function kill() { /* do cleanup 1 */ mortal.kill(); }
}
contract Base2 is mortal {
function kill() { /* do cleanup 2 */ mortal.kill(); }
}
contract Final is Base1, Base2 {
}
调用final.kill()
会调用Base2.kill
,因为它是最近一次复写。但是函数还是会跳过Base1.kill
,根本上来说,因为它不知道Base1
的存在。解决方法是通过super
关键字:
pragma solidity ^0.4.0;
contract owned {
function owned() { owner = msg.sender; }
address owner;
}
contract mortal is owned {
function kill() {
if (msg.sender == owner) selfdestruct(owner);
}
}
contract Base1 is mortal {
function kill() { /* do cleanup 1 */ super.kill(); }
}
contract Base2 is mortal {
function kill() { /* do cleanup 2 */ super.kill(); }
}
contract Final is Base2, Base1 {
}
如果Base1
调用super
函数,他不是简单的调用一个父类的函数。而是,会调用最近一次继承的函数,所以会调用Base2.kill()
(注意最后继承序列是从派生合约开始:Final,Base1,Base2,mortal,owned)。当在执行上下文中无法追踪父函数的时候,使用的函数是真正执行的函数。(?The actual function that is called when using super is not known in the context of the class where it is used, although its type is known. )这类似于普通的虚拟函数查找过程。
父合约的参数
派生合约需要提供父合约构造函数所需的所有参数。可以通过如下两种方式实现:
pragma solidity ^0.4.0;
contract Base {
uint x;
function Base(uint _x) { x = _x; }
}
contract Derived is Base(7) {
function Derived(uint _y) Base(_y * _y) {
}
}
一种方法是直接通过继承列表(is Base(7)
)。另一个方法是通过修改器来调用父合约代码(Base(_y * _y)
)。如果参数是一个常数,第一种方法更加方便,并且定义合约的行为或者表述。(?The first way to do it is more convenient if the constructor argument is a constant and defines the behaviour of the contract or describes it. )第二种方法用于构造函数的参数是基于派生合约的时候。如果,在当前的例子中,两种方法都用了,修改器方式的会优先。
多重继承和线性化(Multiple Inheritance and Linearization)
支持多重继承的语言必须解决几个问题。第一个问题是钻石问题。Solidity走python的路,并使用C3线性化来强制指定父类DAG的顺序。这达成了期望的单调性,但是会损失一些继承图表。尤其是父类中is
指令之后的顺序是非常重要的。在如下的代码中,Solidity将生成线性化不可实现
的错误。
// 这不会被编译
pragma solidity ^0.4.0;
contract X {}
contract A is X {}
contract C is A, X {}
原因是合约C
期望合约X
来复写合约A
(通过指定继承顺序A,X
),但是A
本身要求复写X
,这个矛盾就是不可以解决的。
需要记忆的一个简单的规则是把将父类顺序按照最基础的到最派生的顺序排列出来。(?A simple rule to remember is to specify the base classes in the order from “most base-like” to “most derived”.)
继承相同名称但是不同类型的成员变量
当继承导致合约里的函数和修改器重名,会被认为是错误。这个错误也有可能是事件和修改器重名,以及函数和事件重名。因为状态变量的getter可能复写公有函数。
抽象合约
合约函数可能会留下接口,如下例所示(注意函数声明以;
结尾):
pragma solidity ^0.4.0;
contract Feline {
function utterance() returns (bytes32);
}
这类合约不能编译(即使它们除了包含未实现的函数,也包含了已经实现的函数),但是它们可以用在父类中:
pragma solidity ^0.4.0;
contract Feline {
function utterance() returns (bytes32);
}
contract Cat is Feline {
function utterance() returns (bytes32) { return "miaow"; }
}
如果一个合约继承自抽象合约,并且没有实现接口,这个合约也是抽象的。
接口
接口和抽象合约类似,但是它们不能有任何以实现的函数。以下是一些限制条件:
- 不能继承自其他合约或者接口
- 不能定义构造函数
- 不能定义变量
- 不能定义结构体
- 不能定义枚举类型
将来,有些限制条件会被除去。
接口被限制为合约ABI所能呈现的功能,ABI和接口之间的转换可能没有任何信息损耗。
接口用自己的关键字定义:
pragma solidity ^0.4.11;
interface Token {
function transfer(address recipient, uint amount);
}
因为合约可以继承其他合约,所以合约可以继承自接口。
库
库和合约很相似,但是库的目的是将它们发布到特定地址,并只发布一次,库就可以通过EVM的DELEGATECALL
(版本Homestead之前是CALLCODE
)功能来重用。这意味着,如果库函数被调用,那么库函数的上下文是当前调用的合约。例如,this
指向当前调用的合约,尤其是当前合约的storage可以被访问。作为与合约源码分离的库函数,它只能被访问调用合约的允许访问的状态变量(另外,没有办法重命名)。
库可以被看作是调用该库的合约的隐式的父类。它们在继承链上不可见,但是调用库函数就像是调用父类的函数(L.f()
中,如果L
是库的名称)。另外,internal
函数对任何的合约都可见,就像库是该合约的父类一样。当然,调用内部函数使用内部调用的约定,这意味着可以被传递的所有内部类型和内存类型传递的是引用,而不是拷贝的值。在EVM中为了实现这一点,内部库函数的代码和所有库函数所需的函数都会被引入到当前合约中,JUMP
就可以用DELEGATECALL
代替了。
如下的例子说明了如何使用库(但是请确保查看下一小节应用的更多高级例子来实现集合):
pragma solidity ^0.4.11;
library Set {
// 我们定义了一个新的数据结构用来保存合约数据。
struct Data { mapping(uint => bool) flags; }
// 注意,第一个参数是"storage 引用",因此传递进来的只是引用地址,而不是引用的值
// 这是库函数的一个特点。
// 如果函数能够被看作是那个对象的方法,那么我们习惯把它来命名为`self`。
function insert(Data storage self, uint value)
returns (bool)
{
if (self.flags[value])
return false; // 已经有了
self.flags[value] = true;
return true;
}
function remove(Data storage self, uint value)
returns (bool)
{
if (!self.flags[value])
return false; // 不存在
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) {
// 库函数可以被调用而无须指定库实例。因为这个实例是当前合约。
require(Set.insert(knownValues, value));
}
// 在这个合约中,我们可以直接访问knownValues.flags。
}
当然,你不需要用这种方式来使用库--它们也可以不定义结构体数据类型。函数也可以没有任何storage引用参数,并且也可以有多个引用参数,并且可以在任何位置。
Set.contains
,Set.insert
和Set.remove
调用(DELEGATECALL
)都被编译为外部合约/库调用。如果你使用库,需要小心的是可能会调用一个外部函数。msg.sender
,msg.value
和this
可以保留它们的值(Homestead之前的版本,由于使用CALLCODE,msg.sender
和msg.value
有所不同)。
下面的例子说明了如何在库函数中使用内存类型和内部函数,而无须使用外部函数,来实现自定义类型。
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) {
// 太烂了,我们不得不添加一个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);
}
}
由于编译器不知道库会发布到哪个地址,所以这个地址会被连接器填充(参看使用编译器命令行章节学习如何使用编译器命令行来连接代码)。如果在编译阶段,地址没有给定,编译器会生成__set______
(其中set
是库的名称)占位符。地址可以手动的将这40个符号用库的地址的十六进制编码替代。
和合约相比,库的一些限制条件:
- 没有状态变量
- 不能继承或者被继承
- 不能接收以太币
(这些限制可能在以后的版本中移除)
应用
指令using A for B
可以用来把库函数(来自库A
)附着到任意类型(B
)。这些函数会收到一个调用者的对象实例,作为它们的第一个参数(像Python中的self
变量)。(? These functions will receive the object they are called on as their first parameter (like the self variable in Python).)
using A for *;
的作用是库A
的所有函数被附着到任意类型。
在两种情况中,所有函数,即使那些第一个参数不是该对象的类型,依旧会被附着。当函数调用的时候,类型会被检验,并执行函数重载解析。
指令using A for B;
只在当前作用域有效。目前为止,是限制在当前合约。但是之后会提升到全局作用域。这样一来,包含一个模块,库函数中的数据类型,就不需要添加额外的代码就可以直接使用了。
我们用库的形式来重写集合例子:
pragma solidity ^0.4.11;
// 这些代码和之前的相同,只是去除了注释
library Set {
struct Data { mapping(uint => bool) flags; }
function insert(Data storage self, uint value)
returns (bool)
{
if (self.flags[value])
return false; // 已经存在了
self.flags[value] = true;
return true;
}
function remove(Data storage self, uint value)
returns (bool)
{
if (!self.flags[value])
return false; // 尚未存在
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; // 这是最重要的变化
Set.Data knownValues;
function register(uint value) {
// 这里,所有的Set.Data类型的变量都有了对应的成员函数
// 下面的代码与Set.insert(knownValues, value)相同
require(knownValues.insert(value));
}
}
也可以通过这种方法来拓展基础类型:
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) {
// 调用库函数
uint index = data.indexOf(_old);
if (index == uint(-1))
data.push(_new);
else
data[index] = _new;
}
}
注意,所有的库调用是真实的EVM函数调用。这意味着,如果你传递了内存或者值类型,会传递一个拷贝,即使是self
变量。没有发生拷贝的一种情况是使用storage引用变量。