以太坊智能合約語言Solitidy是一種面向對象的語言,本文清楚合約定義,以及派生的抽象合約,接口,庫的定義。html
Solidity 合約相似於面嚮對象語言中的類。合約中有用於數據持久化的狀態變量,和能夠修改狀態變量的函數。 調用另外一個合約實例的函數時,會執行一個 EVM 函數調用,這個操做會切換執行時的上下文,這樣,前一個合約的狀態變量就不能訪問了。web
1.1 建立合約編程
能夠經過以太坊交易「從外部」或從 Solidity 合約內部建立合約。app
一些集成開發環境,例如 Remix, 經過使用一些用戶界面元素使建立過程更加流暢。 在以太坊上編程建立合約最好使用 JavaScript API web3.js。 如今,咱們已經有了一個叫作 web3.eth.Contract 的方法可以更容易的建立合約。ide
建立合約時,會執行一次構造函數(與合約同名的函數)。構造函數是可選的。只容許有一個構造函數,這意味着不支持重載。函數
在內部,構造函數參數在合約代碼以後經過 ABI 編碼 傳遞,可是若是你使用 web3.js 則沒必要關心這個問題。學習
若是一個合約想要建立另外一個合約,那麼建立者必須知曉被建立合約的源代碼(和二進制代碼)。 這意味着不可能循環建立依賴項。區塊鏈
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 { // 只有當前全部者才能發送 token。 if (msg.sender != owner) return; // 咱們也想詢問 creator 是否能夠發送。 // 請注意,這裏調用了一個下面定義的合約中的函數。 // 若是調用失敗(好比,因爲 gas 不足),會當即中止執行。 if (creator.isTokenTransferOK(owner, newOwner)) owner = newOwner; } } contract TokenCreator { function createToken(bytes32 name) public returns (OwnedToken tokenAddress) { // 建立一個新的 Token 合約而且返回它的地址。 // 從 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); } }
合約函數能夠缺乏實現,以下例所示(請注意函數聲明頭由 ; 結尾):ui
pragma solidity ^0.4.0; contract Feline { function utterance() public returns (bytes32); }
這些合約沒法成功編譯(即便它們除了未實現的函數還包含其餘已經實現了的函數),但他們能夠用做基類合約:this
pragma solidity ^0.4.0; contract Feline { function utterance() public returns (bytes32); } contract Cat is Feline { function utterance() public returns (bytes32) { return "miaow"; } }
若是合約繼承自抽象合約,而且沒有經過重寫來實現全部未實現的函數,那麼它自己就是抽象的。
接口相似於抽象合約,可是它們不能實現任何函數。還有進一步的限制:
沒法繼承其餘合約或接口。
沒法定義構造函數。
沒法定義變量。
沒法定義結構體
沒法定義枚舉。 未來可能會解除這裏的某些限制。
接口基本上僅限於合約 ABI 能夠表示的內容,而且 ABI 和接口之間的轉換應該不會丟失任何信息。
接口由它們本身的關鍵字表示:
pragma solidity ^0.4.11; interface Token { function transfer(address recipient, uint amount) public; }
庫與合約相似,它們只須要在特定的地址部署一次,而且它們的代碼能夠經過 EVM 的 DELEGATECALL (Homestead 以前使用 CALLCODE 關鍵字)特性進行重用。 這意味着若是庫函數被調用,它的代碼在調用合約的上下文中執行,即 this 指向調用合約,特別是能夠訪問調用合約的存儲。 由於每一個庫都是一段獨立的代碼,因此它僅能訪問調用合約明確提供的狀態變量(不然它就沒法經過名字訪問這些變量)。 由於咱們假定庫是無狀態的,因此若是它們不修改狀態(也就是說,若是它們是 view 或者 pure 函數), 庫函數僅能夠經過直接調用來使用(即不使用 DELEGATECALL 關鍵字), 特別是,除非能規避 Solidity 的類型系統,不然是不可能銷燬任何庫的。
庫能夠看做是使用他們的合約的隱式的基類合約。雖然它們在繼承關係中不會顯式可見,但調用庫函數與調用顯式的基類合約十分相似 (若是 L 是庫的話,可使用 L.f() 調用庫函數)。此外,就像庫是基類合約同樣,對全部使用庫的合約,庫的 internal 函數都是可見的。 固然,須要使用內部調用約定來調用內部函數,這意味着全部內部類型,內存類型都是經過引用而不是複製來傳遞。 爲了在 EVM 中實現這些,內部庫函數的代碼和從其中調用的全部函數都在編譯階段被拉取到調用合約中,而後使用一個 JUMP 調用來代替 DELEGATECALL。
下面的示例說明如何使用庫(但也請務必看看 using for-https://solidity-cn.readthedocs.io/zh/develop/contracts.html?highlight=view#using-for 有一個實現 set 更好的例子)。
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)。
如下示例展現瞭如何在庫中使用內存類型和內部函數來實現自定義類型,而無需支付外部函數調用的開銷:
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); } }
因爲編譯器沒法知道庫的部署位置,咱們須要經過連接器將這些地址填入最終的字節碼中 (請參閱 使用命令行編譯器-https://solidity-cn.readthedocs.io/zh/develop/using-the-compiler.html#commandline-compiler 以瞭解如何使用命令行編譯器來連接字節碼)。 若是這些地址沒有做爲參數傳遞給編譯器,編譯後的十六進制代碼將包含 Set____ 形式的佔位符(其中 Set 是庫的名稱)。 能夠手動填寫地址來將那 40 個字符替換爲庫合約地址的十六進制編碼。
與合約相比,庫的限制:
沒有狀態變量
不可以繼承或被繼承
不能接收以太幣
(未來有可能會解除這些限制)
4.1 庫的調用保護
若是庫的代碼是經過 CALL 來執行,而不是 DELEGATECALL 或者 CALLCODE 那麼執行的結果會被回退, 除非是對 view 或者 pure 函數的調用。
EVM 沒有爲合約提供檢測是否使用 CALL 的直接方式,可是合約可使用 ADDRESS 操做碼找出正在運行的「位置」。 生成的代碼經過比較這個地址和構造時的地址來肯定調用模式。
更具體地說,庫的運行時代碼老是從一個 push 指令開始,它在編譯時是 20 字節的零。當部署代碼運行時,這個常數 被內存中的當前地址替換,修改後的代碼存儲在合約中。在運行時,這致使部署時地址是第一個被 push 到堆棧上的常數, 對於任何 non-view 和 non-pure 函數,調度器代碼都將對比當前地址與這個常數是否一致。
4.2 Using For
指令 using A for B; 可用於附加庫函數(從庫 A)到任何類型(B)。 這些函數將接收到調用它們的對象做爲它們的第一個參數(像 Python 的 self 變量)。
using A for *; 的效果是,庫 A 中的函數被附加在任意的類型上。
在這兩種狀況下,全部函數都會被附加一個參數,即便它們的第一個參數類型與對象的類型不匹配。 函數調用和重載解析時纔會作類型檢查。
using A for B; 指令僅在當前做用域有效,目前僅限於在當前合約中,後續可能提高到全局範圍。 經過引入一個模塊,不須要再添加代碼就可使用包括庫函數在內的數據類型。
讓咱們用這種方式將 庫 中的 set 例子重寫:
// 這是和以前同樣的代碼,只是沒有註釋。 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 { // Here, all variables of type Set.Data have // corresponding member functions. // The following function call is identical to // `Set.insert(knownValues, value)` // 這裏, Set.Data 類型的全部變量都有與之相對應的成員函數。 // 下面的函數調用和 `Set.insert(knownValues, value)` 的效果徹底相同。 require(knownValues.insert(value)); } } 也能夠像這樣擴展基本類型: 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區塊鏈社區技術佈道者輝哥
原文發佈於簡書
區塊鏈馬拉松|Blockathon(2018)上海站開放報名
如下是咱們的社區介紹,歡迎各類合做、交流、學習:)