區塊鏈100講:一文了解Solidity合約

image

Solidity 合約相似於面嚮對象語言中的類。合約中有用於數據持久化的狀態變量,和能夠修改狀態變量的函數。 調用另外一個合約實例的函數時,會執行一個 EVM 函數調用,這個操做會切換執行時的上下文,這樣,前一個合約的狀態變量就不能訪問了。html

.. index:: ! contract;creation, constructor
複製代碼

1

建立合約

能夠經過以太坊交易 "從外部" 或從 Solidity 合約內部建立合約。 集成開發環境,像 Remix(https://remix.ethereum.org/), 使用用戶界面元素流暢的建立合約。 在以太坊上編程建立合約最好使用 JavaScript API web3.js(https://github.com/ethereum/web3.js)。 從今天開始,有一個名爲 web3.eth.Contract (https://web3js.readthedocs.io/en/1.0/web3-eth-contract.html#new-contract)的方法可以更容易的建立合約。git

建立合約時,會執行一次構造函數(與合約同名的函數)。 構造函數是可選的。 只容許有一個構造函數,這意味着不支持重載。github

.. index:: constructor;arguments
複製代碼

在內部,構造函數參數在合約代碼以後經過 :ref:ABI 編碼 <ABI>,可是若是你使用 web3.js 則沒必要關心這個問題。web

若是一個合約想要建立另外一個合約,那麼建立者必須知曉被建立合約的源代碼(和二進制)。 這意味着不可能循環建立依賴項。編程

pragma solidity ^0.4.16;

contract OwnedToken {
   // TokenCreator 是以下定義的合約類型.
   // 不建立新合約的話,也能夠引用它。
   TokenCreator creator;
   address owner;
   bytes32 name;

   // 這是註冊 creator 和分配名稱的構造函數。
   function OwnedToken(bytes32 _name) public {
       // 狀態變量經過其名稱訪問,而不是經過例如 this.owner.
       // 這也適用於函數,特別是在構造函數中,你只能那樣調用他們("內部調用"),
       // 由於合約自己還不存在。
       owner = msg.sender;
       // 從 `address` 到 `TokenCreator` ,咱們作顯式的類型轉換
       // 而且假定調用合約的類型是 TokenCreator,沒有真正的檢查方法。
       creator = TokenCreator(msg.sender);
       name = _name;
   }

   function changeName(bytes32 newName) public {
       // 只有 creator 可以更更名稱 -- 由於合約是隱式轉換爲地址的,
       // 因此這裏的比較是可能的。
       if (msg.sender == address(creator))
           name = newName;
   }

   function transfer(address newOwner) public {
       // 只有當前全部者才能傳送權證
       if (msg.sender != owner) return;
       // 咱們還想確認 creator 是否權證轉移是正常操做。
       // 請注意,這裏調用了一個下面定義的合約中的函數。
       // 若是調用失敗(好比,因爲 gas 不足),會當即中止執行。
       if (creator.isTokenTransferOK(owner, newOwner))
           owner = newOwner;
   }
}

contract TokenCreator {
   function createToken(bytes32 name)
      public
      returns (OwnedToken tokenAddress)
   {
       // 建立一個新的權證合約而且返回它的地址。
       // 從 JavaScript 方面來講,返回類型是簡單的 `address` 類型,這是由於
       // 這是在 ABI 中最接近的類型。
       return new OwnedToken(name);
   }

   function changeName(OwnedToken tokenAddress, bytes32 name)  public {
       // 一樣,`tokenAddress` 的外部類型也是 `address` 。
       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);
   }
}

.. index:: ! visibility, external, public, private, internal
複製代碼

2

可見性和getter函數

因爲 Solidity 有兩種函數調用(內部調用不會產生實際的 EVM 調用(也稱爲 一個「消息調用」)和外部調用),有四種函數可見性類型和狀態變量。數組

函數能夠指定爲* external public ,internal 或者 private , 默認狀況下函數類型爲 public 。對於狀態變量,不能設置爲 external* ,默認 是 internal 。bash

*external *:微信

外部函數做爲合約接口的一部分,意味着咱們能夠從其餘合約和交易中調用。一個外部函數* f* 不能從內部調用(好比 f不起做用,但* this.f()* 能夠)。數據結構

當收到大量數據的時候,外部函數有時候會更有效率。app

public :

公共函數是合約接口的一部分,能夠在內部或經過消息調用。對於公共狀態變量, 會自動生成一個 getter 函數(見下面)。

*internal *:

這些函數和狀態變量只能是內部訪問(即從當前合約內部或從它派生的合約訪問),不使用* this *調用。

*private *:

私有函數和狀態變量僅在當前定義它們的合約中使用,而且不能被派生合約使用。

Note

合約中的全部內容對外部觀察者都是可見的。設置一些 private 類型只能阻止其餘合約訪問和修改這些信息, 可是對於區塊鏈外的整個世界它仍然是可見的。

可見性的標識符的定義位置,對於狀態變量來講是在類型後面,對於函數是在參數列表和返回關鍵字中間。

pragma solidity ^0.4.16;

contract C {
   function f(uint a) private pure returns (uint b) { return a + 1; }
   function setData(uint a) internal { data = a; }
   uint public data;
}
複製代碼

在下面的例子中,能夠調用 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) public { 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() public {
       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() public {
       C c = new C();
       uint val = compute(3, 5); // 訪問內部成員(從繼承合約訪問父合約成員)
   }
}

.. index:: ! getter;function, ! function;getter
複製代碼

Getter 函數

編譯器自動爲全部 公有 狀態變量建立 getter 函數。 對於下面給出的合約,編譯器會生成一個名爲* data* 的函數,該函數不會接收任何參數並返回一個 *uint *,即狀態變量 data 的值。 能夠在聲明時完成狀態 變量的初始化。

pragma solidity ^0.4.0;

contract C {
   uint public data = 42;
}

contract Caller {
   C c = new C();
   function f() public {
       uint local = c.data();
   }
}
複製代碼

getter 函數具備外部可見性。 若是在內部訪問 getter(即沒有 this. ),它被認爲一個狀態變量。 若是 它是外部訪問的(即用* this.* ),它被認爲爲一個函數。

pragma solidity ^0.4.0;

contract C {
   uint public data;
   function x() public {
       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) public returns (uint a, bytes3 b) {
   a = data[arg1][arg2][arg3].a;
   b = data[arg1][arg2][arg3].b;
}
複製代碼

請注意,由於沒有好的方法來提供映射的鍵,因此結構中的映射被省略。

.. index:: ! function;modifier
複製代碼

3

函數修改器

使用修改器能夠輕鬆改變函數的行爲。 例如,它們能夠在執行該功能以前自動檢查條件。 修改器是合約的可繼承屬性, 並可能被派生合約覆蓋。

pragma solidity ^0.4.11;

contract owned {
   function owned() public { owner = msg.sender; }
   address owner;

   // 這個合約只定義一個修改器,但並未使用: 它將會在派生合約中用到。
   // 將函數體插入到特殊符號 `_;` 出現的位置。
   // 這意味着若是是 owner 調用這個函數,則函數會被執行,不然則會拋出異常。

   modifier onlyOwner {
       require(msg.sender == owner);
       _;
   }
}

contract mortal is owned {
   // 這個合約從 `owned` 繼承了 `onlyOwner` 修飾符,並並將其應用於 `close` 函數,
   // 只有存儲的擁有者調用 `close` 函數,纔會生效。

   function close() public onlyOwner {
       selfdestruct(owner);
   }
}

contract priced {
   // 修改器能夠接收參數:
   modifier costs(uint price) {
       if (msg.value >= price) {
           _;
       }
   }
}

contract Register is priced, owned {
   mapping (address => bool) registeredAddresses;
   uint price;

   function Register(uint initialPrice) public { price = initialPrice; }

   // 重要的是這裏也應該提供 `payable` 關鍵字,不然函數會自動拒絕全部發送給它的以太幣。
   function register() public payable costs(price) {
       registeredAddresses[msg.sender] = true;
   }

   function changePrice(uint _price) public onlyOwner {
       price = _price;
   }
}

contract Mutex {
   bool locked;
   modifier noReentrancy() {
       require(!locked);
       locked = true;
       _;
       locked = false;
   }

   // 這個函數受互斥量保護,這意味着 `msg.sender.call` 中的可重入調用不能再次調用  `f` 。
   // `return 7` 語句指定返回值爲7,但仍然是在修改器中執行語句 `locked = false` 。

   function f() public noReentrancy returns (uint) {
       require(msg.sender.call());
       return 7;
   }
}
複製代碼

若是同一個函數有多個修改器,它們之間以空格隔開,修飾器會依次檢查執行。

Warning

在早期的Solidity版本中,有修改器的函數, return 語句的行爲表現不一樣。

從修改器或函數體的顯式的 return 語句僅僅跳出當前的修改器和函數體。 返回變量會被賦值,但整個執行 邏輯會在前一個修改器後面定義的 "_" 後繼續執行。

修改器的參數能夠是任意表達式,在上下文中,全部的函數中引入的符號,在修改器中都可見。在修改器中 引入的符號在函數中不可見(可能被重載改變)。

.. index:: ! constant
複製代碼

4

常  量

狀態變量能夠被聲明爲 constant 。這種狀況下,必須在編譯階段將他們指定爲常量。不容許任何訪問 storage,區 塊鏈數據(例如 now, this.balance 或者 block.number)或執行數據( msg.gas ) 或調用外部合約。容許表達式可能會對內存分配產生反作用,但不容許可能會對其餘內存對象產生 反作用。 容許內置的函數,好比 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");
}

.. index:: ! functions
複製代碼

5

函  數

.. index:: ! view function, function;view
複製代碼

View 函數

能夠將函數聲明爲 view 類型,這種狀況下要保證不修改狀態變量。

下面的語句被認爲是修改狀態:

  • 寫狀態變量.

  • :ref:發送事件 <events>

  • :ref:建立其它合約 <creating-contracts>

  • 使用 selfdestruct。

  • 經過調用發送以太幣。

  • 調用任何沒有標記爲 view 或者 pure 的函數.

  • 使用低級調用。

  • 使用包含特定操做碼的內聯彙編。

pragma solidity ^0.4.16;

contract C {
   function f(uint a, uint b) public view returns (uint) {
       return a * (b + 42) + now;
   }
}
複製代碼

Note

constant 是* view* 的別名。

Note

Getter 方法被標記爲 view

Warning

編譯器沒有強制 view 方法不能修改狀態變量。

.. index:: ! pure function, function;pure
複製代碼

pure 函數

函數能夠聲明爲 pure ,在這種狀況下,承諾不讀取或修改狀態。

除了上面解釋的狀態修改語句列表以外,如下被認爲是從狀態中讀取:

  • 讀取狀態變量。

  • 訪問 this.balance 或者 

    .balance

  • 訪問 blocktx, msg 中任意成員 (除 msg.sig 和 msg.data 以外)。

  • 調用任何未標記爲 pure 的函數。

  • 使用包含某些操做碼的內聯彙編。

pragma solidity ^0.4.16;

contract C {
   function f(uint a, uint b) public pure returns (uint) {
       return a * (b + 42);
   }
}
複製代碼

Warning

編譯器沒有強制* pure *方法不能讀取狀態。

.. index:: ! fallback function, function;fallback

Fallback 函數

合約能夠有一個未命名的函數。這個函數不能有參數也不能有返回值。 若是合約中沒有與給定的函數標識符匹配的函數,將調用未命名函數(或者若是根本沒有提供數據)。

此外,當合約收到以太幣(沒有任何數據),這個函數就會執行。 此外,爲了接收以太幣,fallback 函數 必須標記爲 payable。 若是不存在這樣的函數,則合約不能經過常規交易接收以太幣。

在這種上下文中,函數調用一般只消耗不多的 gas(準確地說,2300 個 gas ),因此重要的是使 fallback 函數儘量便宜。 請注意,調用 fallback 函數的交易(而不是內部呼叫)所需的 gas 要高得多,由於每次交易都會額外收取 21000 gas 或更多的費用, 用於簽名檢查等事情。

特別的,如下操做會消耗比 fallback 函數更多的 gas:

  • 寫入存儲

  • 建立合約

  • 調用消耗大量 gas 的外部函數

  • 發送以太幣

請確保您在部署合約以前完全測試您的 fallback 函數,以確保執行成本低於 2300 個 gas 。

Note

即便 fallback 函數不能有參數,仍然可使用 msg.data 來獲取隨調用提供的任何有效負載。

Warning

一個沒有定義 fallback 函數的合約,直接接收以太幣(沒有函數調用,即便用 send 或 transfer)會拋出一個異常, 返還以太幣(在Solidity v0.4.0以前行爲會有所不一樣)。 因此若是你想讓你的合約接收以太幣,必須實現 fallback 函數。

Warning

一個沒有可支付的 fallback 函數的合約,能夠做爲 coinbase transaction (又名 miner block reward )的接收者或者做爲 *selfdestruct *的目的地來接收以太幣。 對這種以太幣轉移合約不能做出反應,所以也不能拒絕它們。 這是 EVM 的設計選擇,並且 Solidity 沒法解決這個問題。 這也意味着 this.balance 能夠高於合約中實現的一些手工記賬的總和(即,在 fallback 函數中更新的計數器)。

pragma solidity ^0.4.0;

contract Test {
   // 發送到這個合約的全部消息都會調用此函數(由於該合約沒有其它函數)。
   // 向這個合約發送以太幣會致使異常,由於 fallback 函數沒有  `payable` 修飾符
   function() public { x = 1; }
   uint x;
}

// 這個合約會保留全部發送給它的以太幣,沒有辦法返還。
contract Sink {
   function() public payable { }
}

contract Caller {
   function callTest(Test test) public {
       test.call(0xabcdef01); // 不存在的哈希
       // 致使 test.x 變成 == 1。

       // 如下將不會編譯,但若是有人向該合約發送以太幣,交易將失敗並拒絕以太幣。
       // test.send(2 ether);
   }
}

.. index:: ! overload
複製代碼

函數重載

合約能夠具備多個不一樣參數的同名函數。這也適用於繼承函數。 如下示例展現了合約* A* 中的重載函數* f*

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 類型不一樣可是外部類型相同會致使錯誤。

// This will not compile
pragma solidity ^0.4.16;

contract A {
   function f(B _in) public pure returns (B out) {
       out = _in;
   }

   function f(address _in) public pure returns (address out) {
       out = _in;
   }
}

contract B {
}
複製代碼

以上兩個 f 函數重載都接受了 ABI 的地址類型,雖然它們在 Solidity 中被認爲是不一樣的。

重載解析和參數匹配

經過將當前範圍內的函數聲明與函數調用中提供的參數相匹配,能夠選擇重載函數。若是全部參數均可以隱式地轉換爲預期類型, 則選擇函數做爲重載候選項。若是沒有一個候選,解析失敗。

Note

返回參數不做爲重載解析的依據。

pragma solidity ^0.4.16;

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

.. index:: ! event
複製代碼

6

事  件

經過事件能夠方便地使用 EVM 日誌記錄工具,在一個dapp的接口中,它能夠反過來 "調用" Javascript 的監聽事件的回調。

事件在合約中可被繼承。 當他們被調用時,會觸發參數存儲到交易的日誌中 - 一種區塊鏈中的特殊數據結構。 這些日誌與地址相關聯,被併入區塊鏈中,只要區塊能夠訪問就一直存在(至少 Frontier,Homestead 是這樣,但 Serenity 也許不是這樣)。 日誌和事件在合約內不可直接被訪問(甚至是建立日誌的合約也不能訪問)。

日誌的 SPV 驗證是可能的,若是一個外部的實體提供了這樣驗證的合約,它能夠實際檢查日誌在區塊鏈中是否存在。 但須要留意的是,因爲合約中僅能訪問最近的 256 個區塊哈希,因此還須要提供區塊頭信息。

能夠最多有三個參數被設置爲 indexed,來設置是否被索引:在用戶界面上能夠按索引參數的特定值來過濾。

若是數組(包括 string 和 bytes)類型被標記爲索引項,則它存儲的 Keccak-256 哈希值做爲主題索引。

除非你用* anonymous* 說明符聲明事件,不然事件簽名的哈希值是主題之一。同時也意味着對於匿名事件沒法經過名字來過濾。

全部非索引參數都將存儲在日誌的數據部分中。

Note

索引參數不會自行存儲。 你只能按值進行搜索,但不可能檢索值自己。

pragma solidity ^0.4.0;

contract ClientReceipt {
   event Deposit(
       address indexed _from,
       bytes32 indexed _id,
       uint _value
   );

   function deposit(bytes32 _id) public payable {
       // 任何對這個函數的調用(甚至是深度嵌套)均可以過濾被調用的 `Deposit` 來被 JavaScript API 檢測到。

       Deposit(msg.sender, _id, msg.value);
   }
}

在 JavaScript API 的用法以下:

var abi = /* abi as generated by the compiler */;
var ClientReceipt = web3.eth.contract(abi);
var clientReceipt = ClientReceipt.at("0x1234...ab67" /* address */);

var event = clientReceipt.Deposit();

// 監視變化
event.watch(function(error, result){
   // 結果包括對 `Deposit` 的調用參數在內的各類信息。

   if (!error)
       console.log(result);
});

// 或者經過回調當即開始觀察
var event = clientReceipt.Deposit(function(error, result) {
   if (!error)
       console.log(result);
});

.. index:: ! log
複製代碼

日誌的底層接口

經過函數 log0log1,* log2*, *log3 和 log4 能夠訪問日誌機制的底層接口。 logi *表示總共有帶 *i + 1 *個 bytes32類型的參數。其中第一個參數會被用來作爲日誌的數據部分, 其它的會作爲主題。上面的事件調用能夠以相同的方式執行。

pragma solidity ^0.4.10;

contract C {
   function f() public payable {
       bytes32 _id = 0x420042;
       log3(
           bytes32(msg.value),
           bytes32(0x50cb9fe53daa9737b786ab3646f04d0150dc50ef4e75f59509d83667ad5adb20),
           bytes32(msg.sender),
           _id
       );
   }
}
複製代碼

長十六進制數等於* keccak256("Deposit(address,hash256,uint256)")*,即事件的簽名。

  • 瞭解事件的其餘資源

  • Javascript 文檔

  • 事件使用例程

  • 如何在 js 中訪問它們

.. index:: ! inheritance, ! base class, ! contract;base, ! deriving
複製代碼

7

繼  承

經過複製包括多態的代碼,Solidity 支持多重繼承。

全部的函數調用都是虛擬的,這意味着最遠的派生函數會被調用,除非明確給出合約名稱。

當一個合約從多個合約繼承時,在區塊鏈上只有一個合約被建立,全部基類合約的代碼被複制到建立的合約中。

總的繼承系統與 Python's, 很是 類似,特別是多重繼承方面。

下面的例子進行了詳細的說明。

pragma solidity ^0.4.16;

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) public returns (address adr);
}

contract NameReg {
   function register(bytes32 name) public;
   function unregister() public;
}

// 能夠多重繼承。 請注意,`owned` 也是 `mortal` 的基類,但只有一個 `owned` 實例(如C ++中的虛擬繼承)。
contract named is owned, mortal {
   function named(bytes32 name) {
       Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
       NameReg(config.lookup(1)).register(name);
   }

   // 函數能夠被另外一個具備相同名稱和相同數量/類型輸入的函數重載。若是重載函數有不一樣類型的輸出參數,會致使錯誤。
   // 本地和基於消息的函數調用都會考慮這些重載。
   function kill() public {
       if (msg.sender == owner) {
           Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
           NameReg(config.lookup(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;
}
複製代碼

請注意,咱們調用* mortal.kill() *來調用父合約的銷燬請求。這樣作法是有問題的,就像 在下面的例子中看到:

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 { /* do cleanup 1 */ mortal.kill(); }
}

contract Base2 is mortal {
   function kill() public { /* 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() 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 { /* do cleanup 1 */ super.kill(); }
}

contract Base2 is mortal {
   function kill() public { /* do cleanup 2 */ super.kill(); }
}

contract Final is Base1, Base2 {
}
複製代碼

若是 *Base2 *調用 *super *的函數,它不會簡單在其基類合約上調用該函數。 相反,它 在最終的繼承關係圖譜的下一個基類合約中調用這個函數,因此它會調用 *Base1.kill() *(注意 最終的繼承序列是 -- 從最遠派生合約開始:FinalBase2Base1mort**alownerd)。 在類中使用 super 調用的實際函數在當前類的上下文中是未知的,儘管它的類型是已知的。 這與普通的 虛擬方法查找相似。

.. index:: ! constructor
複製代碼

基類構造函數的參數

派生合約須要提供基類構造函數須要的全部參數。這能夠經過兩種方式來完成:

pragma solidity ^0.4.0;

contract Base {
   uint x;
   function Base(uint _x) public { x = _x; }
}

contract Derived is Base(7) {
   function Derived(uint _y) Base(_y * _y) public {
   }
}
複製代碼

一種方法直接在繼承列表中調用基類構造函數(is Base(7))。另外一種方法是像修改器使用方法同樣, 做爲派生合約構造函數定義頭的一部分,(Base(_y * _y))。 若是構造函數參數是常量而且定義或描述了合約的行爲,使用第一種方法比較方便。若是基類構造函數的參數依賴於派生合約,那麼 必須使用第二種方法。若是,像這個簡單的例子同樣,兩個地方都用到了,優先使用修改器風格的參數。

.. index:: ! inheritance;multiple, ! linearization, ! C3 linearization
複製代碼

多重繼承與線性化

編程語言實現多重繼承須要解決幾個問題。一個問題是 鑽石問題。 Solidity 借鑑了 Python 的方式而且使用 "C3 線性化" 強制將基類合約轉換一個有向無環圖(DAG) 的特定順序。這致使了咱們所但願的單調性,可是卻禁止了某些繼承圖。特別是,基類在 is 後面的順序很重要。在下面的代碼中,Solidity 將會報錯 "Linearization of inheritance graph impossible" 。

// 下面代碼編譯出錯

pragma solidity ^0.4.0;

contract X {}
contract A is X {}
contract C is A, X {}
複製代碼

緣由是* C* 請求 X 重寫 *A *(由於定義的順序是 AX),可是 A*自己要求重寫*X,沒法解決這種衝突。

一個指定基類合約的繼承順序的簡單原則是從 *"most base-like" *到 "most derived"

繼承有相同名字的不一樣類型成員

一種錯誤狀況是繼承致使一個合約同時存在相同名字的修改器和函數時。另外一種錯誤狀況是繼承致使的事件和修改器重名,函數和修改器重名。 有一種例外狀況,狀態變量的 getter 能夠覆蓋一個公有函數。

.. index:: ! contract;abstract, ! abstract contract
複製代碼

8

抽象合約

合約函數能夠缺乏實現,以下例所示(請注意函數聲明頭由 ; 結尾)

pragma solidity ^0.4.0;

contract Feline {
   function utterance() public returns (bytes32);
}
複製代碼

這些合約沒法成功編譯(即便它們除了未實現的函數還包含其餘已經實現了的函數),但他們能夠用做基類合約:

pragma solidity ^0.4.0;

contract Feline {
   function utterance() public returns (bytes32);
}

contract Cat is Feline {
   function utterance() public returns (bytes32) { return "miaow"; }
}
複製代碼

若是合約繼承自抽象合約,而且不經過重寫實現全部未實現的函數,那麼它自己就是抽象的。

.. index:: ! contract;interface, ! interface contract
複製代碼

9

接  口

接口相似於抽象合約,可是它們不能實現任何函數。還有進一步的限制:

  • 沒法繼承其餘合約或接口。

  • 沒法定義構造函數。

  • 沒法定義變量。

  • 沒法定義結構體

  • 沒法定義枚舉。

  • 未來可能會解除這些限制。

接口基本上僅限於合約 ABI 能夠表示的內容,而且 ABI 和接口之間的轉換應該不會丟失任何信息。

接口由它們本身的關鍵字表示:

pragma solidity ^0.4.11;

interface Token {
   function transfer(address recipient, uint amount) public;
}
複製代碼

就像繼承其餘合約同樣,合約能夠繼承接口。

.. index:: ! library, callcode, delegatecall
複製代碼

10

庫與合約相似,但其用途是在指定的地址僅部署一次,而且代碼被使用 EVM 的 DELEGATECALL (Homestead 以前使用 CALLCODE 關鍵字)特性。 這意味着若是庫函數被調用,它的代碼在調用合約的上下文中執行,即 this 指向調用合約,特別是能夠訪問調用合約的存儲。 由於一個合約是一個獨立的代碼塊,它僅能夠訪問調用合約明確提供的狀態變量(不然沒法命名它們)。 若是不修改狀態變量(即,若是是 view 或者 pure 函數), 庫函數只能被直接調用(即不使用* DELEGATECALL* 關鍵字),由於庫被假定爲無狀態的。特別是,除非繞過 Solidity 類型系統,不然庫不可能破壞。

使用庫的合約,能夠認爲庫是隱式的基類合約。雖然它們在繼承關係中不會顯式可見,但調用庫函數與調用顯式的基類合約十分相似(若是* L* 是庫的話,使用 L.f() 調用庫函數)。

此外,就像庫是基類同樣,對全部使用庫的合約,庫的 *internal *函數都是可見的。固然,須要使用內部調用約定 來調用內部函數,這意味着全部全部內部類型,內存類型都是經過引用而不是複製來傳遞。爲了在 EVM 中實現這些,內部庫函數的代碼和從其中調用的全部 函數都在編譯階段被拉取到調用合約中,而後使用一個 JUMP調用來代替 DELEGATECALL

.. index:: using for, set
複製代碼

下面的示例說明如何使用庫(可是請務必檢查 :ref:using for <using-for> 有一個實現 set 更好的例子)。

pragma solidity ^0.4.16;

library Set {
 // 咱們定義了一個新的結構體數據類型,用於在調用合約中保存數據。
 struct Data { mapping(uint => bool) flags; }

 // 注意第一個參數是 "storage reference" 類型,所以在調用中參數傳遞的只是它的存儲地址而不是內容。
 // 這是庫函數的一個特性。若是該函數能夠被視爲對象的方法,則習慣稱第一個參數爲 `self`。
 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 contains(Data storage self, uint value)
     public
     view
     returns (bool)
 {
     return self.flags[value];
 }
}

contract C {
   Set.Data knownValues;

   function register(uint value) public {
       // 不須要庫的特定實例就能夠調用庫函數,由於當前合約就是 "instance"。
       require(Set.insert(knownValues, value));
   }
   // 若是咱們願意,咱們也能夠在這個合約中直接訪問 knownValues.flags
}
複製代碼

固然,你沒必要按照這種方式去使用庫:它們也能夠在不定義結構數據類型的狀況下使用。函數也不須要任何存儲 引用參數,庫能夠出如今任何位置而且能夠有多個存儲引用參數。

調用 Set.contains, Set.insert 和 Set.remove 都被編譯爲外部調用( DELEGATECALL )。 若是使用庫,請注意實際執行的是外部函數調用。 msg.sender,* msg.value* 和* this 在調用中將保留它們的值,(在 Homestead 以前, 由於使用了 CALLCODE*,改變了 *msg.sender *和 msg.value)。

如下示例顯示如何使用庫的內存類型和內部函數來實現自定義類型,無需外部函數調用的開銷:

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 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) {
           // 太差了,咱們須要增長一個 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 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;
   }
}

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);
   }
}
複製代碼

因爲編譯器沒法知道庫的部署位置,連接器須要填入這些地址必的最終字節碼(請參閱 :ref:commandline-compiler 以瞭解如何使用鏈接器的命令行工具)。 若是這些地址沒有做爲參數傳遞給編譯器, 編譯後的十六進制代碼將包含* Set____ *形式的佔位符(其中 Set 是庫的名稱)。能夠手動填寫地址來 替換庫中十六進制編碼的全部40個符號。

與合約相比,庫的限制:

  • 沒有狀態變量

  • 不可以繼承或被繼承

  • 不能接受以太幣

  • (未來有可能會解除這些限制)

庫的調用保護

正如介紹中所述,除調用* view* 或者 pure 庫函數以外,經過 CALL 而不是 DELEGATECALL 或者 CALLCODE 的庫的代碼,將會恢復。

EVM 沒有爲合約提供檢測是否使用 *CALL *的直接方式,可是合約可使用 ADDRESS 操做碼找出正在運行 的「位置」。生成的代碼經過比較這個地址和構造時的地址來肯定調用模式。

更具體地說,庫的運行時代碼老是由 *push *指令啓用,它在編譯時是 20 字節的零。當部署代碼運行時,這個常數 被內存中的當前地址替換,修改後的代碼存儲在合約中。在運行時,這致使部署時地址是第一個被 push 到堆棧上的常數, 對於任何 non-view 和 non-pure 函數,調度器代碼都將對比當前地址與這個常數是否一致。

.. index:: ! using for, library
複製代碼

11

Using For

指令 using A for B; 可用於附加庫函數(從庫 A)到任何類型(B)。 調用對象將做爲這些函數的第一個參數(像 Python 的 *self *變量)。

*using A for **; 的效果是,庫 A 中的函數被附加在任意的類型上。

在這兩種狀況下,全部函數,即便那些與對象類型不匹配的第一參數類型的函數,也被附加上了。 函數調用和重載解析時纔會作類型檢查。

using A for B; 指令僅在當前做用域有效,僅限於在當前合約中,後續可能提高到全局範圍。 經過引入一個模塊,不須要再添加代碼就可使用包括庫函數在內的數據類型。

讓咱們用這種方式將 :ref:libraries 中的set例子重寫:

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 contains(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.Data 類型的全部變量都有對應的成員函數。
       // 如下函數調用與 `Set.insert(knownValues, value)` 效果相同。
       require(knownValues.insert(value));
   }
}
複製代碼

也能夠像這樣擴展基本類型:

pragma solidity ^0.4.16;

library Search {
   function indexOf(uint[] storage self, 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 函數調用。這意味着若是傳遞內存或值類型,將執行一個拷貝副本,即便是 self 變量。 使用存儲引用變量是惟一不會發生拷貝的狀況。

本文做者:HiBlock區塊鏈技術佈道羣-毛明旺

加微信baobaotalk_com,加入技術佈道羣

活動推薦

image

掃描下圖中二維碼或點擊「閱讀原文」便可報名參加

相關文章
相關標籤/搜索