原文鏈接
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引用變量。