第四章:以太坊智慧合約 - 合約
以太坊智慧合約 - 合約
合約概述
Solidity 中合約和麵向物件語言中的類差不多。合約包含狀態變數(狀態變數的資料儲存在鏈上的區塊中)及函式。 呼叫另一個合約例項的函式時,會執行一個 EVM 函式呼叫 , 這個操作會切換執行時的上下文 ,這時上一個合約的狀態變數就無法訪問了 。
建立合約
合約可以通過發起交易(通過 Web3 發起)或是 Solidity 建立。 在以太坊上動態地建立合約可以使用 JavaScript API Web3.js 的 web3.eth.Contract 來建立合約。
使用 new 建立合約
合約內可以通過 new 關鍵字來建立一個新合約。
pragma solidity ^0.4.0; contract D { uint x; function D(uint a) public payable { x = a; } } contract C { D d = new D(4); function createD(uint arg) public { D newD = new D(arg); } function createAndEndowD(uint arg,uint amount) public payable { D newD = (new D).value(amount)(arg); } }
可以在建立的合約中傳送 Ether,但不能限制 gas。 如果建立發生 out-of-stack 或無足夠的餘額, 則會丟擲一個異常。
pragma solidity ^0.4.24; contract OwnedToken { TokenCreator creator; address owner; bytes32 name; constructor(bytes32 _name) public { owner = msg.sender; creator = TokenCreator(msg.sender); name = _name; } function changeName(bytes32 newName) public { if (msg.sender == address(creator)) name = newName; } function getCreator() returns (address) { return creator; } } contract TokenCreator { function createToken(bytes32 name) public returns (OwnedToken tokenAddress) { return new OwnedToken(name); } function changeName(OwnedToken tokenAddress, bytes32 name) public{ tokenAddress.changeName(name); } function isTokenTransferOK(address currentOwner, address newOwner) public view returns (bool ok) { address tokenAddress = msg.sender; return (keccak256(newOwner) & 0xff) == (bytes20(tokenAddress) & 0xff); } }
Solidity 有兩種函式呼叫方式,一種 是內部呼叫,不會建立 EVM 呼叫(也叫作訊息呼叫),另一種是外部呼叫,會建立 EVM 呼叫。
public - (任意訪問,作為合約介面)可以通過內部呼叫或通過訊息呼叫。對公共狀態變數而言,會有的自動訪問限制符的函式生成。
private - (僅當前合約內)私有函式和狀態變數僅僅在定義該合約中可見, 在派生的合約中不可見。
internal - (僅當前合約及所繼承的合約)這些函式和狀態變數只能內部訪問(即在當前合約或由它派生的合約),而不使用(關鍵字)this 。
external - (僅外部訪問,也是合約介面)它們可以從其他合約呼叫, 也可以通過事務呼叫。外部函式f不能被內部呼叫(在內部也只能用外部訪問方式訪問,即 f()不執行,但this.f()執行)。
在下面的例子中, D 可以呼叫 c.getData()來訪問 data 的值,但不能呼叫 f。 合約 E 繼承自 C,所以它可以訪問 compute 函式。
pragma solidity ^0.4.24;
contract C {
uint private data;
function f(uint a) private returns (uint) {
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); // private不可訪問
c.setData(3);
uint local = c.getData();
// local = c.compute(3,5); // 內部函式呼叫
}
}
// E繼承C
contract E is C{
function g() {
C c = new C();
uint val = compute(3,5);
}
}
訪問函式(Getter Function)
編譯器會自動為所有的 public 的狀態變數生成訪問函式。
pragma solidity ^0.4.0;
contract C {
uint public data = 42;
}
contract Caller {
C c = new C();
function f() public {
uint local = c.data();
}
}
訪問函式有外部可見性。 如果是內部訪問的,則可以直接訪問變數(不用this)。但如果使用外部方式來訪問 (通過 this. ),則必須通過函式的方式來呼叫 。
pragma solidity ^0.4.24;
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) public returns (uint a,bytes3 b) {
a = data[arg1][arg2].a;
b = data[arg1][arg2].b;
}
函式修改器(Function Modifier)
熟悉Python 的讀者會發現函式修改器的作用和 Python 的裝飾器很相似。
pragma solidity ^0.4.24;
contract owned {
function owned() public{
owner = msg.sender;
}
address owner;
// 修改器
// 定義了一個函式修改器,可被繼承
// 修改時, 函式體被插入到 ”_; "處
// 不符合條件時,將丟擲異常
modifier onlyOwner {
require(msg.sender == owner);
_;
}
}
contract mortal is owned {
function close() public onlyOwner {
selfdestruct(owner); // 用於銷燬合約。所以只需要暴露出自毀介面即可:
}
}
contract priced {
modifier costs (uint price) {
if (msg.value >= price) {
_;
}
}
}
contract Register is priced, owned {
mapping (address => bool) registerAddresses;
uint price;
function Register (uint initialPrice) public {
price = initialPrice;
}
function register() public payable costs(price) {
registerAddresses[msg.sender] = true;
}
function changePrice(uint _price) public onlyOwner {
price = _price;
}
function getPrice() public returns (uint){
return price;
}
function getMapping(address addr) returns (bool){
return registerAddresses[addr];
}
}
上面的 onlyOwner 就是一個函式修改器。當用這個修改器修飾一個函式時, 函式必須滿足 onlyOwner 的條件才能執行。 這裡的條件是: 函式必須是合約創 建的才能呼叫,否則丟擲異常。
多個函式修改器
如果同一個函式有多個修改器,並且他們之間以空格隔開,那麼修改器會 依次檢查執行。
在修改器中或函式內的顯式的 return語句, 僅僅跳出當前的修改器或函式。 返回的變數會被賦值,但執行流會在前一個修改器後面定義的"_"後繼續執行,
pragma solidity ^0.4.24;
contract Mutex {
bool locked;
modifier noReentrancy() {
require(!locked);
locked = true;
_;
locked = false;
}
// 防止遞迴呼叫
//return 7 之後, locked = false 依然會執行
function f() public noReentrancy returns (uint) {
require(msg.sender.call());
return 7;
}
}
理解修改器的執行次序
pragma solidity ^0.4.24;
contract modifysample {
uint a = 10;
modifier mf1 (uint b) {
uint c = b;
_;
c = a;
a = 11;
}
modifier mf2() {
uint c = a;
_;
}
modifier mf3() {
a = 12;
return ;
_;
a = 13;
}
function test1() mf1(a) mf2 mf3 public {
a = 1;
}
function test2() public constant returns (uint) {
return a;
}
}
結果是11。
順序表為:
狀態常量
狀態常量可以被定義為 constant。不過表示式有一些限制。
- 不允許訪問 storage。
- 不允許訪問 區塊鏈資料,如 now、 this.balance、 block.number。
- 不允許訪問合約執行中的資料,如 msg.value 或 gasleft()。
- 不允許向外部合約發起呼叫 。
但內建的函式 keccak256、keccak256、ripemd160、ecrecover、addmod、mulmod 可以允許呼叫,即使它們呼叫的是外部合約。 編譯器並不會為常量在 storage 上預留空間, 每個使用的常量都會被對應的 常量表達式所替換。
檢視函式(View Function)
函式可以宣告為 view,表示它不能修改狀態。下面幾種情況被認為是修改了狀態。
- 寫狀態變數。
- 觸發事件。
- 建立其他的合約。
- call 呼叫附加了以太幣 。
- 呼叫了任何沒有 view 或 pure 修飾的函式。
- 使用了低級別的呼叫( low-level call)。
- 使用 了包含特定操作符的內聯彙編。
pragma solidity ^0.4.16;
contract C {
function f(uint a, uint b) public view returns (uint) {
return a * (b + 42) + now;
}
}
有幾個地方需要注意一下:
- 宣告為 view 和宣告為 constant 是等價的, constant 是 view 的別名 。 constant 計劃在 Solidity 0.5.0 版本之後被棄用 。
- 訪問函式都被標記為 view。
純函式(Pure Function)
函式可以宣告為 view, 表示它既不能讀取狀態,也不能修改狀態。
- 讀狀態變數。
- 訪問了 this.balance 或.balance。
- 訪問了 block, tx, msg 的成員 Cmsg.sig 和 msg.data 除外)。
- 呼叫了任何沒有 pure 修飾的函式。
- 使用了包含特定操作符的內聯彙編。
pragma solidity ^0.4.16;
contract C {
function f(uint a, uint b) public view returns (uint) {
return a * (b + 42);
}
}
回退函式(Fallback Function)
一個合約可以有一個(最多也只能有一個)沒有名字的函式。 這個函式無引數,也無返回值。
如果呼叫合約時,沒有匹配上任何一個函式(或者沒有提供資料),則會呼叫回退函式。
另外,儘管回退函式沒有引數,其依然可以使用 msg.data 讀取呼叫時提供 的 payload。
函式過載(Function Overloading)
函式過載是指一個合約可以有多個引數不同的同名函式
pragma solidity ^0.4.16;
contract A{
function f(uint _in) public pure returns (uint out) {
out = 1;
}
function f(uint _in,bytes32 _key) public pure returns (uint out) {
out = 2;
}
}
有一種情況需要注意,如果僅僅是 Solidity型別不一樣,但在外部介面(ABI) 上表現一樣的話,那麼是無法函式過載的。 例如,下面的例子就是無法編譯的。
pragma solidity ^0.4.24;
contract B{}
contract A{
function f(B _in) public pure returns (B out) {
out = _in;
}
function f(address _in) public pure returns (address out) {
out = _in;
}
}
過載的引數匹配問題
過載函式在呼叫時, 會根據呼叫提供的引數的型別去匹配過載函式, 這期間可能會發生隱式的型別轉換。 如果可以匹配上多個過載函式,則會呼叫失敗。 來看一個例子:
pragma solidity ^0.4.24;
contract A{
function f(uint8 _in) public pure returns (uint8 out) {
out = _in;
}
function f(uint256 _in) public pure returns (uint256 out) {
out = _in;
}
}
f(50)會失敗, 因為 50 可以隱式轉換為 uint8 和 uint256。 而f(256)則會呼叫 f(uint256), 因為 256 不能轉換為 uint8。 注意: 返回值不參與匹配。
事件
事件主題(Topic)
主題是用來把事件索引化( Index)的數值。沒有主題,就不能搜尋事件 (主題的作用是用來檢索事件的)。一個事件最多可以有四個主題。
第一個主題是事件簽名 ,剩下三個主題是索引化的引數數值(即最多有三個引數接收屬性可以被設定為索引)。設定為主題後,可以允許通過這個引數來 查詢日誌,甚至可以按特定的值過濾。如果引數是字串、位元組或者陣列,那 麼主題是它的 keccak-256 雜湊。
注意: 1. 事件簽名 hash 是其中一個主題,匿名事件除外,這意味著對於匿 名事件無法通過名字來過濾。 2. 被索引的引數將不會儲存它們自己,可以搜尋它們的值,但不能檢索值本身。所有未被索引的引數將被作為日誌的一部分被儲存起來。假設下面這個例子
event PersonCreated(uint indexed age,uint indexed height)
// 通過引數觸發
emit PersonCreated(26,176);
這裡會生成 3 個主題:
- 0x6bel5e8568869blel00750dd5079151b32637268ec08d199b318b793181b8a7d 它是事件的簽名,計算方法是 Keccak-256(“PersonCreated(uint256, uint256)”)。
- 0x36383cc9cfbfl dc87c78c2529ae2fcd4e3fc4e575el 54b357ae3a8b2739113cf, 他是年齡 26 的Keccak-256 值。
- 0x048dd4d5794e69cea63353d940276ad6 lf89c65942226a2bb5bd352536892f82, 它是身高 176 的 Keccak-256 值。
在節點上就會建立這樣的可搜尋的索引,之後可以在 Web3 中對索引進行 過濾搜尋,比如可以過濾出所有 26 歲的人,只需要使用 以下程式碼:
var createdEvent = myContract.PersonCreated({age: 26});
createdEvent.watch(function(err,result) {
if(err) {
console.log(err)
return;
}
console.log("Found",result);
})
底層的日誌介面(Low-level Interface to Log)
通過函式 log0、 log1 、 log2、 log3、 log4 直接訪問底層的日誌。 logi 表示帶 i + 1 個 bytes32 型別的引數, i 表示的就是可帶引數的數目, 從 0 開始計數。
其中第一個引數會被用來作為日誌的資料部分,其他的引數作為主題。
繼承
Solidity繼承使用的是關鍵字 is。Solidity 是通過複製包括多型的程式碼來支援多重繼承的。如果沒有明確指定呼叫哪一個合約的函式,那麼最終被派生的方法通常會被呼叫 。如果一個合約從多個其他合約那裡繼承,那麼在區塊鏈上僅會建立一個合約,並且在父合約裡的程式碼會被複制用來建立被繼承合約。
pragma solidity ^0.4.16;
contract owned {
function owned() {
owner = msg.sender;
}
address owner;
}
contract mortal is owned {
function kill() {
if (msg.sender == owner) selfdestruct(owner);
}
}
contract Config {
function loopup(uint id) public returns (address adr);
}
contract NameReg {
function register(bytes32 name) public;
function unregister() public;
}
contract named is owned,mortal {
function named(bytes32 name) {
Config config = Config(0xD5dfD8D94886E70b06E474c3fB14Fd43E2E23970);
NameReg(config.loopup(1)).register(name);
}
function kill() public {
if (msg.sender == owner) {
Config config = Config(0xD5dfD8D94886E70b06E474c3fB14Fd43E2E23970);
NameReg(config.loopup(1)).unregister();
mortal.kill();
}
}
}
contract PriceFeed is owned,mortal,named("GoldFeed") {
function updateInfo(uint newInfo) public {
if (msg.sender == owner)
info = newInfo;
}
function get() public view returns (uint r) {
return info;
}
uint info;
}
**注意, 上面例子的 kill() 方法中,我們呼叫了 motal.kill() , 呼叫了父合約的 銷燬函式(destruction)。但這可能會引發一些問題,看下面的例子:
**
pragma solidity ^0.4.0;
contract owned {
function owned() public {
owner = msg.sender;
}
address owner;
}
contract mortal is owned {
function kill() public {
if (msg.sender == owner) selfdestruct(owner);
}
}
contract Base1 is mortal {
function kill() public {
mortal.kill();
}
}
contract Base2 is mortal {
function kill() public {
mortal.kill();
}
}
contract Final is Base1, Base2 {}
對 Final.kill() 的呼叫只會呼叫 Base2.kill() (最後過載的函式),而派生重寫會跳過 Base1.kill, 因為它根本就不知道有 Basel 。一個變通的方法就是使用 super, 看下面的例子:
pragma solidity ^0.4.0;
contract owned {
function owned() public {
owner = msg.sender;
}
address owner;
}
contract mortal is owned {
function kill() public {
if (msg.sender == owner) selfdestruct(owner);
}
}
contract Base1 is mortal {
function kill() public {
super.kill();
}
}
contract Base2 is mortal {
function kill() public {
super.kill();
}
}
contract Final is Base1, Base2 {}
如果 Base2 呼叫了函式 super,那麼它不僅會呼叫基類的合約函式,還會調 用繼承關係圖譜上的下一個基類合約,所以會呼叫 Basel.kill() 。需要注意的是 最終的繼承圖譜將會是 Final、 Base2、 Basel 、 mortal、 owned。
建構函式(Constructor)
建構函式也叫“構造器氣通常用來完成合約的初始化變數賦值。它是一個 可選函式,建構函式使用關鍵字 constructor 表示。 它僅在建立合約時執行二次。 如果沒有實現建構函式,那麼合約會新增一個預設的建構函式。
建構函式要麼是 public, 要麼是 internal。 如果是 internal,則作為抽象合約。
pragma solidity ^0.4.22;
contract A {
uint public a;
constructor (uint _a) internal {
a = _a;
}
}
contract B is A(1) {
constructor() public{}
}
在 Solidity 0.4.22 版本之前,構造是使用與合約同名的函式來實現的,語法如下所示(不過現在這個寫法已經棄用了)。
pragma solidity ^0.4.22;
contract A {
uint public a;
constructor (uint _a) internal {
a = _a;
}
}
contract B is A(1) {
constructor B() public{}
}
父合約建構函式的引數
派生的合約需要呼叫所有父合約的建構函式,並且需要提供所有父合約需 要的引數
pragma solidity ^0.4.22;
contract Base {
uint x;
constructor(uint _x) public {
x = _x;
}
function getValue() returns (uint){
return x;
}
}
// 第1種方式
contract Derived1 is Base(7) {
uint y;
constructor (uint _y) public {
y = _y;
}
function getValue() returns (uint){
return y;
}
}
//第2種方式
contract Derived2 is Base {
uint y;
constructor (uint _y) Base(_y * _y) public {
y =_y;
}
function getValue() returns (uint){
return y;
}
}
一種稱為“繼承列表”式,即直接在繼承列表中使用 is Base(7), 示例程式碼 Derived I 合約就是使用這個方式。
另一種稱為“修改器風格”式,示例程式碼 Derived2 合約就是使用這個方式。 Base(_y * _y)就像是一個函式修改器,它會作為修飾派生建構函式的一部分得到執行。
第一種方式對於構造器是常量的情況比較方便,可以直接說明合約的行為。 第二種方式適用於構造的引數值是由派生合約指定的情況。
多繼承與線性化
支援多繼承的程式語言需要解決幾個問題,其中之一就是菱形繼承問題又 稱“鑽石問題”。 Solidity 的解決方案可以參考 Python, 使用 C3 線性化方式來強制將基類合約轉換為一個有向無環圖(DAG)的特定順序。基類合約在 is 後 的順序變得非常重要。
pragma solidity ^0.4.0;
contract X {}
contract A is X {}
contract C is A, X {}
原因是 C 會請求 X 來重寫 A (因為繼承定義的順序是 A、 X),但 A 自身又是重寫 X 的,所以這是一個不可解決的矛盾。
一個簡單的指定基類合約的繼承順序,原則上是從最接近基類到最接近派生類的。
同名問題
如果在繼承時出現一個合約同時存在多個相同名字的修改器或函式,那麼會產生錯誤。 如果事件與修改器重名或函式與事件重名 ,則都會產生錯誤。
例外的是,狀態變數的 getter 可以覆蓋一個 public 函式。
抽象合約(Abstract Contract)
和java抽象一致。
如果一個合約從一個抽象合約裡繼承,但卻沒實現所有函式,那麼它也是 一個抽象合約。
下面是一個函式型別的例子,宣告一個變數。變數的型別是函式:
function (address) external returns (address) foo;
抽象合約分離了定義和實現,但也提供了擴充套件的能力。
介面(Interface)
介面被限制為合約 ABI 定義可以表示的內容, ABI 和介面定義之間應該可以進行轉換而不會有任何資訊丟失。 合約可以繼承介面,就像合約可以繼承其他的合約一樣。
庫
庫與合約類似,它也部署在一個指定的地址上(僅被部署一次,當然程式碼 可以在不同的合約反覆使用),然後通過 EVM 的特性 DELEGATECALL (Homestead 之前是用 CALLCODE)來複用程式碼。 庫函式在被呼叫時,庫程式碼 是在發起合約(下文稱主調合約: 主動發起DELEGATECALL 呼叫的合約) 的 上下文中執行的,使用 this 將會指向主調合約,而且庫程式碼可以訪問主調合約 的儲存(storage)。
對比普通合約來說, 庫有以下的限制。
- 無狀態變數。
- 不能繼承或被繼承。
- 不能接收以太幣。
- 不能銷燬一個庫。
不會修改狀態變數(例如被宣告 view 或 pure), 庫函式只能通過直接呼叫 (不用 DELEGATECALL )。這是因為庫被認為是與狀態無關的。
庫有許多使用場景。 其中兩個主要的場景如下:
- 如果有許多合約, 它們有一些共同程式碼,則可以把共同程式碼部署成一個 庫。這將節省 gas, 因為 gas 也依賴於合約的規模。因此, 可以把庫想象成使用 其合約的父合約。
- 庫可用於給資料型別新增成員函式。(Using for用法)
由於庫被當成隱式的父合約(它們不會顯式地出現在繼承關係中 ,但呼叫 庫函式和呼叫父合約的方式是非常類似的,如庫 L 有函式 f(),則使用 L.f() 即可訪問 ), 庫裡面的內部函式被複制給使用它的合約。
同樣按呼叫內部函式的呼叫方式, 這意味著所有內部型別可以傳進去, memory 型別則通過引用傳遞,而不是拷貝的方式。同樣庫裡面的結構體和列舉 也會被複制給使用它的合約。 因此,如果一個庫裡只包含內部函式或結構體或 列舉,則不需要部署庫,因為庫裡面的所有內容都被複制給使用它的合約了 。
pragma solidity ^0.4.16;
library Set {
struct Data {
mapping (uint => bool)flags;
}
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 conntains(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.contains、 Set.remove、 Set.insert 會編譯為 以 DELEGATECALL 的 方式呼叫外部合約和庫。 使用庫時,需要注意的是一個真實的外部函式呼叫發生了 。儘管 msg.sender、 msg.value、 this 還會保持它們在主調合約中的值。
下面的例子演示了在庫中如何使用 memo叩 型別和內部函式來實現一個自 定義型別,而不會用到外部函式呼叫 。
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 limb(bigint _a,uint _limb) internal pure returns (uint) {
return _limb < _a.limbs.length ? _a.limbs[_limb] : 0;
}
function max(uint a,uint b) private pure returns (uint) {
return a > b ? a : b;
}
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) {
uint[] memory newLimbs = new uint[](r.limbs.length + 1);
for (i = 0; i< r.limbs.length; ++i)
newLimbs[i] = r.limbs[i];
newLimbs[i] = r.limbs[i];
r.limbs = newLimbs;
}
}
}
contract C {
using BigInt for BigInt.bigint;
function f() public pure {
var x = BigInt.fromUint(7);
var y = BigInt.fromUint(uint(-1));
var z = x.add(y);
}
}
在合約的原始碼中不能新增庫地址,它是在編譯時向編譯器以引數形式提供的。
Using for 指令
指令 using A for B;用來把庫函式(從庫 A)關聯到型別 B。。這些函式將會 把呼叫函式的例項作為第一個引數。 語法與 Python 中的 self 變數一樣。 例如, 庫 A 有函式 add(B b1, B b2),使用 Using A for B 指令後,如果有 B b1 ,就可以使用 b1.add(b2)。
using A for *表示庫 A 中的函式可以關聯到任意的型別上。
using A for B,指令僅在當前的作用域有效, 即目前僅僅支援當前合約的作用域,後續也非常有可能解除這個限制,允許作用到全域性範圍。如果能作用到全域性範圍, 通過引入一些模組(module),資料型別將能通過庫函式擴充套件功能, 而不需要每個地方都寫一遍類似的程式碼了。
以之前的例子來說:
pragma solidity ^0.4.16;
library Set {
struct Data {
mapping (uint => bool)flags;
}
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 conntains(Data storage self,uint value) public view returns (bool){
return self.flags[value];
}
}
contract C {
using Set for Set.Data; //這是一個關鍵的變化
Set.Data knownValues;
function register(uint value) public {
// 庫函式不需要例項化就可以呼叫,因為例項就是當前的合約
// Set.insert(knownValues,value);
require(knownValues.insert(value));
// require(Set.insert(knownValues,value));
// 在這個合約中,如果需要的話,可以直接訪問 knownValues.flags
}
}
同樣可以使用 Using for 的方式來對基本型別(elementa可 type)進行擴充套件
pragma solidity ^0.4.16;
library Search {
function indexOf(uint[] storage slef, uint value) public view 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) public {
data.push(value);
}
function replace(uint _old, uint _new) public {
// 進行庫呼叫
uint index = data.indexOf(_old);
if (index == uint(-1))
data.push(_new);
else
data[index] = _new;
}
}
需要注意的是所有庫呼叫實際上是 EVM 函式呼叫 。 這意味著如果傳的是 memory 型別或者值型別,那麼進行拷貝時即使是 self變數,解決方法也是使用 儲存(storage)型別的引用來避免拷貝內容的。
借鑑https://blog.csdn.net/lj900911/article/details/83037536