合約
Solidity 合約類似于面向對象語言中的類。合約中有用于資料持久化的狀态變量,和可以修改狀态變量的函數。 調用另一個合約執行個體的函數時,會執行一個 EVM 函數調用,這個操作會切換執行時的上下文,這樣,前一個合約的狀态變量就不能通路了。
可以通過以太坊交易“從外部”或從 Solidity 合約内部建立合約。
一些內建開發環境,例如 Remix, 通過使用一些使用者界面元素使建立過程更加流暢。 在以太坊上程式設計建立合約最好使用 JavaScript API web3.js。 現在,我們已經有了一個叫做 web3.eth.Contract 的方法能夠更容易的建立合約。
建立合約時,會執行一次構造函數(與合約同名的函數)。構造函數是可選的。隻允許有一個構造函數,這意味着不支援重載。
在内部,構造函數參數在合約代碼之後通過 ABI 編碼 傳遞,但是如果你使用 <code>web3.js</code> 則不必關心這個問題。
如果一個合約想要建立另一個合約,那麼建立者必須知曉被建立合約的源代碼(和二進制代碼)。 這意味着不可能循環建立依賴項。
由于 Solidity 有兩種函數調用(内部調用不會産生實際的 EVM 調用或稱為“消息調用”,而外部調用則會産生一個 EVM 調用), 函數和狀态變量有四種可見性類型。 函數可以指定為 <code>external</code>,<code>public</code> ,<code>internal</code> 或者 <code>private</code>,預設情況下函數類型為 <code>public</code>。 對于狀态變量,不能設定為 <code>external</code> ,預設是 <code>internal</code> 。
<code>external</code> :
外部函數作為合約接口的一部分,意味着我們可以從其他合約和交易中調用。 一個外部函數 <code>f</code>不能從内部調用(即 <code>f</code> 不起作用,但 <code>this.f()</code> 可以)。 當收到大量資料的時候,外部函數有時候會更有效率。
<code>public</code> :
public 函數是合約接口的一部分,可以在内部或通過消息調用。對于公共狀态變量, 會自動生成一個 getter 函數(見下面)。
<code>internal</code> :
這些函數和狀态變量隻能是内部通路(即從目前合約内部或從它派生的合約通路),不使用 <code>this</code> 調用。
<code>private</code> :
private 函數和狀态變量僅在目前定義它們的合約中使用,并且不能被派生合約使用。
注解
合約中的所有内容對外部觀察者都是可見的。設定一些 <code>private</code> 類型隻能阻止其他合約通路和修改這些資訊, 但是對于區塊鍊外的整個世界它仍然是可見的。
可見性辨別符的定義位置,對于狀态變量來說是在類型後面,對于函數是在參數清單和傳回關鍵字中間。
在下面的例子中,<code>D</code> 可以調用 <code>c.getData()</code> 來擷取狀态存儲中 <code>data</code> 的值,但不能調用 <code>f</code> 。 合約 <code>E</code> 繼承自 <code>C</code> ,是以可以調用 <code>compute</code>。
編譯器自動為所有 public 狀态變量建立 getter 函數。對于下面給出的合約,編譯器會生成一個名為 <code>data</code> 的函數, 該函數不會接收任何參數并傳回一個 <code>uint</code> ,即狀态變量 <code>data</code> 的值。可以在聲明時完成狀态變量的初始化。
getter 函數具有外部可見性。如果在内部通路 getter(即沒有 <code>this.</code> ),它被認為一個狀态變量。 如果它是外部通路的(即用 <code>this.</code> ),它被認為為一個函數。
下一個例子稍微複雜一些:
這将會生成以下形式的函數
請注意,因為沒有好的方法來提供映射的鍵,是以結構中的映射被省略。
使用 修飾器modifier 可以輕松改變函數的行為。 例如,它們可以在執行函數之前自動檢查某個條件。 修飾器modifier 是合約的可繼承屬性, 并可能被派生合約覆寫。
如果同一個函數有多個 修飾器modifier,它們之間以空格隔開,修飾器modifier 會依次檢查執行。
警告
在早期的 Solidity 版本中,有 修飾器modifier 的函數,<code>return</code> 語句的行為表現不同。
修飾器modifier 或函數體中顯式的 return 語句僅僅跳出目前的 修飾器modifier 和函數體。 傳回變量會被指派,但整個執行邏輯會從前一個 修飾器modifier 中的定義的 “_” 之後繼續執行。
修飾器modifier 的參數可以是任意表達式,在此上下文中,所有在函數中可見的符号,在 修飾器modifier 中均可見。 在 修飾器modifier 中引入的符号在函數中不可見(可能被重載改變)。
狀态變量可以被聲明為 <code>constant</code>。在這種情況下,隻能使用那些在編譯時有确定值的表達式來給它們指派。 任何通過通路 storage,區塊鍊資料(例如 <code>now</code>, <code>this.balance</code> 或者 <code>block.number</code>)或執行資料( <code>msg.gas</code> ) 或對外部合約的調用來給它們指派都是不允許的。 在記憶體配置設定上有邊界效應(side-effect)的表達式是允許的,但對其他記憶體對象産生邊界效應的表達式則不行。 内建(built-in)函數 <code>keccak256</code>,<code>sha256</code>,<code>ripemd160</code>,<code>ecrecover</code>,<code>addmod</code> 和 <code>mulmod</code> 是允許的(即使他們确實會調用外部合約)。
允許帶有邊界效應的記憶體配置設定器的原因是這将允許建構複雜的對象,比如查找表(lookup-table)。 此功能尚未完全可用。
編譯器不會為這些變量預留存儲,它們的每次出現都會被替換為相應的常量表達式(這将可能被優化器計算為實際的某個值)。
不是所有類型的狀态變量都支援用 constant 來修飾,目前支援的僅有值類型和字元串。
As in JavaScript, functions may take parameters as input. Unlike in JavaScript and C, functions may also return an arbitrary number of values as output.
函數參數
Function parameters are declared the same way as variables, and the name of unused parameters can be omitted.
For example, if you want your contract to accept one kind of external call with two integers, you would use something like:
Function parameters can be used as any other local variable and they can also be assigned to.
An external function cannot accept a multi-dimensional array as an input parameter. This functionality is possible if you enable the new experimental <code>ABIEncoderV2</code> feature by adding <code>pragma experimental ABIEncoderV2;</code> to your source file.
An internal function can accept a multi-dimensional array without enabling the feature.
傳回值
Function return variables are declared with the same syntax after the <code>returns</code> keyword.
For example, suppose you want to return two results: the sum and the product of two integers passed as function parameters, then you use something like:
The names of return variables can be omitted. Return variables can be used as any other local variable and they are initialized with their default value and have that value unless explicitly set.
You can either explicitly assign to return variables and then leave the function using <code>return;</code>, or you can provide return values (either a single or multiple ones) directly with the <code>return</code> statement:
This form is equivalent to first assigning values to the return variables and then using <code>return;</code> to leave the function.
You cannot return some types from non-internal functions, notably multi-dimensional dynamic arrays and structs. If you enable the new experimental <code>ABIEncoderV2</code> feature by adding <code>pragma experimental ABIEncoderV2;</code> to your source file then more types are available, but <code>mapping</code>types are still limited to inside a single contract and you cannot transfer them.
傳回多個值
When a function has multiple return types, the statement <code>return (v0, v1, ..., vn)</code> can be used to return multiple values. The number of components must be the same as the number of return types.
可以将函數聲明為 <code>view</code> 類型,這種情況下要保證不修改狀态。
下面的語句被認為是修改狀态:
修改狀态變量。
産生事件。
建立其它合約。
使用 <code>selfdestruct</code>。
通過調用發送以太币。
調用任何沒有标記為 <code>view</code> 或者 <code>pure</code> 的函數。
使用低級調用。
使用包含特定操作碼的内聯彙編。
<code>constant</code> 是 <code>view</code> 的别名。
Getter 方法被标記為 <code>view</code>。
編譯器沒有強制 <code>view</code> 方法不能修改狀态。
函數可以聲明為 <code>pure</code> ,在這種情況下,承諾不讀取或修改狀态。
除了上面解釋的狀态修改語句清單之外,以下被認為是從狀态中讀取:
讀取狀态變量。
通路 <code>this.balance</code> 或者 <code><address>.balance</code>。
通路 <code>block</code>,<code>tx</code>, <code>msg</code> 中任意成員 (除 <code>msg.sig</code> 和 <code>msg.data</code> 之外)。
調用任何未标記為 <code>pure</code> 的函數。
使用包含某些操作碼的内聯彙編。
編譯器沒有強制 <code>pure</code> 方法不能讀取狀态。
合約可以有一個未命名的函數。這個函數不能有參數也不能有傳回值。 如果在一個到合約的調用中,沒有其他函數與給定的函數辨別符比對(或沒有提供調用資料),那麼這個函數(fallback 函數)會被執行。
除此之外,每當合約收到以太币(沒有任何資料),這個函數就會執行。此外,為了接收以太币,fallback 函數必須标記為 <code>payable</code>。 如果不存在這樣的函數,則合約不能通過正常交易接收以太币。
在這樣的上下文中,通常隻有很少的 gas 可以用來完成這個函數調用(準确地說,是 2300 gas),是以使 fallback 函數的調用盡量廉價很重要。 請注意,調用 fallback 函數的交易(而不是内部調用)所需的 gas 要高得多,因為每次交易都會額外收取 21000 gas 或更多的費用,用于簽名檢查等操作。
具體來說,以下操作會消耗比 fallback 函數更多的 gas:
寫入存儲
建立合約
調用消耗大量 gas 的外部函數
發送以太币
請確定您在部署合約之前徹底測試您的 fallback 函數,以確定執行成本低于 2300 個 gas。
即使 fallback 函數不能有參數,仍然可以使用 <code>msg.data</code> 來擷取随調用提供的任何有效資料。
一個沒有定義 fallback 函數的合約,直接接收以太币(沒有函數調用,即使用 <code>send</code> 或 <code>transfer</code>)會抛出一個異常, 并返還以太币(在 Solidity v0.4.0 之前行為會有所不同)。是以如果你想讓你的合約接收以太币,必須實作 fallback 函數。
一個沒有 payable fallback 函數的合約,可以作為 coinbase transaction (又名 miner block reward)的接收者或者作為 <code>selfdestruct</code> 的目标來接收以太币。
一個合約不能對這種以太币轉移做出反應,是以也不能拒絕它們。這是 EVM 在設計時就決定好的,而且 Solidity 無法繞過這個問題。
這也意味着 <code>this.balance</code> 可以高于合約中實作的一些手工記帳的總和(即在 fallback 函數中更新的累加器)。
合約可以具有多個不同參數的同名函數。這也适用于繼承函數。以下示例展示了合約 <code>A</code> 中的重載函數 <code>f</code>。
重載函數也存在于外部接口中。如果兩個外部可見函數僅差別于 Solidity 内的類型而不是它們的外部類型則會導緻錯誤。
以上兩個 <code>f</code> 函數重載都接受了 ABI 的位址類型,雖然它們在 Solidity 中被認為是不同的。
重載解析和參數比對
通過将目前範圍内的函數聲明與函數調用中提供的參數相比對,可以選擇重載函數。 如果所有參數都可以隐式地轉換為預期類型,則選擇函數作為重載候選項。如果一個候選都沒有,解析失敗。
傳回參數不作為重載解析的依據。
調用 <code>f(50)</code> 會導緻類型錯誤,因為 <code>50</code> 既可以被隐式轉換為 <code>uint8</code> 也可以被隐式轉換為 <code>uint256</code>。 另一方面,調用 <code>f(256)</code> 則會解析為 <code>f(uint256)</code> 重載,因為 <code>256</code> 不能隐式轉換為 <code>uint8</code>。
事件允許我們友善地使用 EVM 的日志基礎設施。 我們可以在 dapp 的使用者界面中監聽事件,EVM 的日志機制可以反過來“調用”用來監聽事件的 Javascript 回調函數。
事件在合約中可被繼承。當他們被調用時,會使參數被存儲到交易的日志中 —— 一種區塊鍊中的特殊資料結構。 這些日志與位址相關聯,被并入區塊鍊中,隻要區塊可以通路就一直存在(在 Frontier 和 Homestead 版本中會被永久儲存,在 Serenity 版本中可能會改動)。 日志和事件在合約内不可直接被通路(甚至是建立日志的合約也不能通路)。
對日志的 SPV(Simplified Payment Verification)證明是可能的,如果一個外部實體提供了一個帶有這種證明的合約,它可以檢查日志是否真實存在于區塊鍊中。 但需要留意的是,由于合約中僅能通路最近的 256 個區塊哈希,是以還需要提供區塊頭資訊。
最多三個參數可以接收 <code>indexed</code> 屬性,進而使它們可以被搜尋:在使用者界面上可以使用 indexed 參數的特定值來進行過濾。
如果數組(包括 <code>string</code> 和 <code>bytes</code>)類型被标記為索引項,則它們的 keccak-256 哈希值會被作為 topic 儲存。
除非你用 <code>anonymous</code> 說明符聲明事件,否則事件簽名的哈希值是 topic 之一。 同時也意味着對于匿名事件無法通過名字來過濾。
所有非索引參數都将存儲在日志的資料部分中。
索引參數本身不會被儲存。你隻能搜尋它們的值(來确定相應的日志資料是否存在),而不能擷取它們的值本身。
使用 JavaScript API 調用事件的用法如下:
通過函數 <code>log0</code>,<code>log1</code>, <code>log2</code>, <code>log3</code> 和 <code>log4</code> 可以通路日志機制的底層接口。 <code>logi</code> 接受 <code>i + 1</code>個 <code>bytes32</code> 類型的參數。其中第一個參數會被用來做為日志的資料部分, 其它的會做為 topic。上面的事件調用可以以相同的方式執行。
其中的長十六進制數的計算方法是 <code>keccak256("Deposit(address,hash256,uint256)")</code>,即事件的簽名。
Javascript 文檔
Web3.js 0.2x 中文文檔
事件使用例程
如何在 js 中通路它們
通過複制包括多态的代碼,Solidity 支援多重繼承。
所有的函數調用都是虛拟的,這意味着最遠的派生函數會被調用,除非明确給出合約名稱。
當一個合約從多個合約繼承時,在區塊鍊上隻有一個合約被建立,所有基類合約的代碼被複制到建立的合約中。
總的來說,Solidity 的繼承系統與 Python的繼承系統 ,非常 相似,特别是多重繼承方面。
下面的例子進行了詳細的說明。
注意,在上邊的代碼中,我們調用 <code>mortal.kill()</code> 來“轉發”銷毀請求。 這樣做法是有問題的,在下面的例子中可以看到:
調用 <code>Final.kill()</code> 時會調用最遠的派生重載函數 <code>Base2.kill</code>,但是會繞過 <code>Base1.kill</code>, 主要是因為它甚至都不知道 <code>Base1</code> 的存在。解決這個問題的方法是使用 <code>super</code>:
如果 <code>Base2</code> 調用 <code>super</code> 的函數,它不會簡單在其基類合約上調用該函數。 相反,它在最終的繼承關系圖譜的下一個基類合約中調用這個函數,是以它會調用 <code>Base1.kill()</code> (注意最終的繼承序列是——從最遠派生合約開始:Final, Base2, Base1, mortal, ownerd)。 在類中使用 super 調用的實際函數在目前類的上下文中是未知的,盡管它的類型是已知的。 這與普通的虛拟方法查找類似。
A 構造器函數 is an optional function declared with the <code>constructor</code> keyword which is executed upon contract creation, and where you can run contract initialisation code.
Before the constructor code is executed, state variables are initialised to their specified value if you initialise them inline, or zero if you do not.
After the constructor has run, the final code of the contract is deployed to the blockchain. The deployment of the code costs additional gas linear to the length of the code. This code includes all functions that are part of the public interface and all functions that are reachable from there through function calls. It does not include the constructor code or internal functions that are only called from the constructor.
Constructor functions can be either <code>public</code> or <code>internal</code>. If there is no constructor, the contract will assume the default constructor, which is equivalent to <code>constructor() public {}</code>. For example:
A constructor set as <code>internal</code> causes the contract to be marked as abstract.
Prior to version 0.4.22, constructors were defined as functions with the same name as the contract. This syntax was deprecated and is not allowed anymore in version 0.5.0.
派生合約需要提供基類構造函數需要的所有參數。這可以通過兩種方式來完成:
一種方法直接在繼承清單中調用基類構造函數(<code>is Base(7)</code>)。 另一種方法是像 修飾器modifier 使用方法一樣, 作為派生合約構造函數定義頭的一部分,(<code>Base(_y * _y)</code>)。 如果構造函數參數是常量并且定義或描述了合約的行為,使用第一種方法比較友善。 如果基類構造函數的參數依賴于派生合約,那麼必須使用第二種方法。 如果像這個簡單的例子一樣,兩個地方都用到了,優先使用 修飾器modifier 風格的參數。
程式設計語言實作多重繼承需要解決幾個問題。 一個問題是 鑽石問題。 Solidity 借鑒了 Python 的方式并且使用“ C3 線性化 ”強制一個由基類構成的 DAG(有向無環圖)保持一個特定的順序。 這最終反映為我們所希望的唯一化的結果,但也使某些繼承方式變為無效。尤其是,基類在 <code>is</code> 後面的順序很重要。 在下面的代碼中,Solidity 會給出“ Linearization of inheritance graph impossible ”這樣的錯誤。
代碼編譯出錯的原因是 <code>C</code> 要求 <code>X</code> 重寫 <code>A</code> (因為定義的順序是 <code>A, X</code> ), 但是 <code>A</code> 本身要求重寫 <code>X</code>,無法解決這種沖突。
可以通過一個簡單的規則來記憶: 以從“最接近的基類”(most base-like)到“最遠的繼承”(most derived)的順序來指定所有的基類。
當繼承導緻一個合約具有相同名字的函數和 修飾器modifier 時,這會被認為是一個錯誤。 當事件和 修飾器modifier同名,或者函數和事件同名時,同樣會被認為是一個錯誤。 有一種例外情況,狀态變量的 getter 可以覆寫一個 public 函數。
合約函數可以缺少實作,如下例所示(請注意函數聲明頭由 <code>;</code> 結尾):
這些合約無法成功編譯(即使它們除了未實作的函數還包含其他已經實作了的函數),但他們可以用作基類合約:
如果合約繼承自抽象合約,并且沒有通過重寫來實作所有未實作的函數,那麼它本身就是抽象的。
Note that a function without implementation is different from a Function Type even though their syntax looks very similar.
Example of function without implementation (a function declaration):
Example of a Function Type (a variable declaration, where the variable is of type <code>function</code>):
Abstract contracts decouple the definition of a contract from its implementation providing better extensibility and self-documentation and facilitating patterns like the Template method and removing code duplication. Abstract contracts are useful in the same way that defining methods in an interface is useful. It is a way for the designer of the abstract contract to say “any child of mine must implement this method”.
接口類似于抽象合約,但是它們不能實作任何函數。還有進一步的限制:
無法繼承其他合約或接口。
無法定義構造函數。
無法定義變量。
無法定義結構體
無法定義枚舉。
将來可能會解除這裡的某些限制。
接口基本上僅限于合約 ABI 可以表示的内容,并且 ABI 和接口之間的轉換應該不會丢失任何資訊。
接口由它們自己的關鍵字表示:
就像繼承其他合約一樣,合約可以繼承接口。
庫與合約類似,它們隻需要在特定的位址部署一次,并且它們的代碼可以通過 EVM 的 <code>DELEGATECALL</code> (Homestead 之前使用 <code>CALLCODE</code> 關鍵字)特性進行重用。 這意味着如果庫函數被調用,它的代碼在調用合約的上下文中執行,即 <code>this</code> 指向調用合約,特别是可以通路調用合約的存儲。 因為每個庫都是一段獨立的代碼,是以它僅能通路調用合約明确提供的狀态變量(否則它就無法通過名字通路這些變量)。 因為我們假定庫是無狀态的,是以如果它們不修改狀态(也就是說,如果它們是 <code>view</code> 或者 <code>pure</code> 函數), 庫函數僅可以通過直接調用來使用(即不使用 <code>DELEGATECALL</code>關鍵字), 特别是,除非能規避 Solidity 的類型系統,否則是不可能銷毀任何庫的。
庫可以看作是使用他們的合約的隐式的基類合約。雖然它們在繼承關系中不會顯式可見,但調用庫函數與調用顯式的基類合約十分類似 (如果 <code>L</code> 是庫的話,可以使用 <code>L.f()</code> 調用庫函數)。此外,就像庫是基類合約一樣,對所有使用庫的合約,庫的 <code>internal</code> 函數都是可見的。 當然,需要使用内部調用約定來調用内部函數,這意味着所有内部類型,記憶體類型都是通過引用而不是複制來傳遞。 為了在 EVM 中實作這些,内部庫函數的代碼和從其中調用的所有函數都在編譯階段被拉取到調用合約中,然後使用一個 <code>JUMP</code> 調用來代替 <code>DELEGATECALL</code>。
下面的示例說明如何使用庫(但也請務必看看 using for 有一個實作 set 更好的例子)。
當然,你不必按照這種方式去使用庫:它們也可以在不定義結構資料類型的情況下使用。 函數也不需要任何存儲引用參數,庫可以出現在任何位置并且可以有多個存儲引用參數。
調用 <code>Set.contains</code>,<code>Set.insert</code> 和 <code>Set.remove</code> 都被編譯為外部調用( <code>DELEGATECALL</code> )。 如果使用庫,請注意實際執行的是外部函數調用。 <code>msg.sender</code>, <code>msg.value</code> 和 <code>this</code> 在調用中将保留它們的值, (在 Homestead 之前,因為使用了 <code>CALLCODE</code>,改變了 <code>msg.sender</code> 和 <code>msg.value</code>)。
以下示例展示了如何在庫中使用記憶體類型和内部函數來實作自定義類型,而無需支付外部函數調用的開銷:
由于編譯器無法知道庫的部署位置,我們需要通過連結器将這些位址填入最終的位元組碼中 (請參閱 使用指令行編譯器 以了解如何使用指令行編譯器來連結位元組碼)。 如果這些位址沒有作為參數傳遞給編譯器,編譯後的十六進制代碼将包含 <code>__Set______</code> 形式的占位符(其中 <code>Set</code> 是庫的名稱)。 可以手動填寫位址來将那 40 個字元替換為庫合約位址的十六進制編碼。
與合約相比,庫的限制:
沒有狀态變量
不能夠繼承或被繼承
不能接收以太币
(将來有可能會解除這些限制)
如果庫的代碼是通過 <code>CALL</code> 來執行,而不是 <code>DELEGATECALL</code> 或者 <code>CALLCODE</code> 那麼執行的結果會被回退, 除非是對 <code>view</code> 或者 <code>pure</code> 函數的調用。
EVM 沒有為合約提供檢測是否使用 <code>CALL</code> 的直接方式,但是合約可以使用 <code>ADDRESS</code> 操作碼找出正在運作的“位置”。 生成的代碼通過比較這個位址和構造時的位址來确定調用模式。
更具體地說,庫的運作時代碼總是從一個 push 指令開始,它在編譯時是 20 位元組的零。當部署代碼運作時,這個常數 被記憶體中的目前位址替換,修改後的代碼存儲在合約中。在運作時,這導緻部署時位址是第一個被 push 到堆棧上的常數, 對于任何 non-view 和 non-pure 函數,排程器代碼都将對比目前位址與這個常數是否一緻。
指令 <code>using A for B;</code> 可用于附加庫函數(從庫 <code>A</code>)到任何類型(<code>B</code>)。 這些函數将接收到調用它們的對象作為它們的第一個參數(像 Python 的 <code>self</code> 變量)。
<code>using A for *;</code> 的效果是,庫 <code>A</code> 中的函數被附加在任意的類型上。
在這兩種情況下,所有函數都會被附加一個參數,即使它們的第一個參數類型與對象的類型不比對。 函數調用和重載解析時才會做類型檢查。
<code>using A for B;</code> 指令僅在目前作用域有效,目前僅限于在目前合約中,後續可能提升到全局範圍。 通過引入一個子產品,不需要再添加代碼就可以使用包括庫函數在内的資料類型。
讓我們用這種方式将 庫 中的 set 例子重寫:
也可以像這樣擴充基本類型:
注意,所有庫調用都是實際的 EVM 函數調用。這意味着如果傳遞記憶體或值類型,都将産生一個副本,即使是 <code>self</code> 變量。 使用存儲引用變量是唯一不會發生拷貝的情況。