貢獻方式及常見問題——Solidity中文文檔(13)
來自專欄 HiBlock區塊鏈社區
寫在前面:HiBlock區塊鏈社區成立了翻譯小組,翻譯區塊鏈相關的技術文檔及資料,本文為Solidity文檔翻譯的第十三部分《貢獻方式及常見問題》,特發布出來邀請solidity愛好者、開發者做公開的審校,您可以添加微信baobaotalk_com,驗證輸入「solidity」,然後將您的意見和建議發送給我們,也可以在文末「留言」區留言,有效的建議我們會採納及合併進下一版本,同時將送一份小禮物給您以示感謝。
貢獻方式
對於大家的幫助,我們一如既往地感激。
你可以試著 從源代碼編譯 開始,以熟悉 Solidity 的組件和編譯流程。這對精通 Solidity 上智能合約的編寫也有幫助。
我們特別需要以下方面的幫助:
- 改善文檔
- 回復 StackExchange(https://ethereum.stackexchange.com/) 和 Solidity Gitter(https://gitter.im/ethereum/solidity) 上的用戶提問
- 解決並回復 Soliditys GitHub issues(https://github.com/ethereum/solidity/issues) 上的問題,特別是被標記為 up-for-grabs(https://github.com/ethereum/solidity/issues?q=is%3Aopen+is%3Aissue+label%3Aup-for-grabs) 的問題,他們是針對外部貢獻者的入門問題。
1
怎樣報告問題
請用 GitHub issues tracker(https://github.com/ethereum/solidity/issues) 來報告問題。彙報問題時,請提供下列細節:
- 你所使用的 Solidity 版本
- 源碼(如果可以的話)
- 你在哪個平台上運行代碼
- 如何重現該問題
- 該問題的結果是什麼
- 預期行為是什麼樣的
將造成問題的源碼縮減到最少,總是很有幫助的,並且有時候甚至能澄清誤解。
2
Pull Request 的工作流
為了進行貢獻,請 fork 一個 develop 分支並在那裡進行修改。除了你 做了什麼 之外,你還需要在 commit 信息中說明,你 為什麼 做這些修改(除非只是個微小的改動)。
在進行了 fork 之後,如果你還需要從 develop 分支 pull 任何變更的話(例如,為了解決潛在的合併衝突),請避免使用 git merge ,而是 git rebase 你的分支。
此外,如果你在編寫一個新功能,請確保你編寫了合適的 Boost 測試案例,並將他們放在了 test/下。
但是,如果你在進行一個更大的變更,請先與 Solidity Development Gitter channel(https://gitter.im/ethereum/solidity-dev) 進行商量(與上文提到的那個功能不同,這個變更側重於編譯器和編程語言開發,而不是編程語言的使用)。
新的特性和 bug 修復會被添加到 Changelog.md 文件中:使用的時候請遵循上述方式。
最後,請確保你遵守了這個項目的 編碼風格(https://raw.githubusercontent.com/ethereum/solidity/develop/CODING_STYLE.md) 。還有,雖然我們採用了持續集成測試,但是在提交 pull request 之前,請測試你的代碼並確保它能在本地進行編譯。
感謝你的幫助!
3
運行編譯器測試
Solidity 有不同類型的測試,他們包含在應用 soltest 中。其中一些需要 cpp-ethereum 客戶端運行在測試模式下,另一些需要安裝 libz3。
soltest 會從保存在 ./test/libsolidity/syntaxTests 中的測試合約中獲取所期待的結果。為了使 soltest 可以找到這些測試,可以使用 --testpath 命令行參數來指定測試根目錄,例如 ./build/test/soltest -- --testpath ./test。
若要禁用 z3 測試,可使用 ./build/test/soltest -- --no-smt --testpath ./test ,若要執行不需要 cpp-ethereum 的測試子集,則用 ./build/test/soltest -- --no-ipc --testpath ./test。
對於其他測試,你都需要安裝 cpp-ethereum ,並在測試模式下運行它:eth --test -d /tmp/testeth。
之後再執行實際的測試文件:./build/test/soltest -- --ipcpath /tmp/testeth/geth.ipc --testpath ./test。
可以用過濾器來執行一組測試子集:soltest -t TestSuite/TestName -- --ipcpath /tmp/testeth/geth.ipc --testpath ./test,其中 TestName 可以是通配符 *。
另外, scripts/test.sh 里有一個測試腳本可執行所有測試,並自動運行 cpp-ethereum,如果它在 scripts 路徑中的話(但不會去下載它)。
Travis CI 甚至會執行一些額外的測試(包括 solc-js 和對第三方 Solidity 框架的測試),這些測試需要去編譯 Emscripten 目標代碼。
編寫和運行語法測試
就像前文提到的,語法測試存儲在單獨的合約里。這些文件必須包含註解,為相關的測試標註預想的結果。測試工具將編譯並基於給定的預想結果進行檢查。
例如:./test/libsolidity/syntaxTests/double_stateVariable_declaration.sol
contract test { uint256 variable; uint128 variable;}// ----// DeclarationError: Identifier already declared.
一個語法測試必須在合約代碼之後包含跟在分隔符 ---- 之後的測試代碼。上邊例子中額外的注釋則用來描述預想的編譯錯誤或警告。如果合約不會出現編譯錯誤或警告,這部分可以為空。
在上邊的例子里,狀態變數 variable 被聲明了兩次,這是不允許的。這會導致一個 DeclarationError 來告知標識符已經被聲明過了。
用來進行那些測試的工具叫做 isoltest,可以在 ./test/tools/ 下找到。它是一個交互工具,允許你使用你喜歡的文本編輯器編輯失敗的合約。讓我們把第二個 variable 的聲明去掉來使測試失敗:
contract test { uint256 variable;}// ----// DeclarationError: Identifier already declared.
再次運行 ./test/isoltest 就會得到一個失敗的測試:
syntaxTests/double_stateVariable_declaration.sol: FAIL Contract: contract test { uint256 variable; } Expected result: DeclarationError: Identifier already declared. Obtained result: Success
這裡,在獲得了結果之後列印了預想的結果,但也提供了編輯/更新/跳過當前合約或直接退出的辦法,isoltest 提供了下列測試失敗選項:
- edit:isoltest 會嘗試打開先前用 isoltest --editor /path/to/editor 所指定的編輯器。如果沒設定路徑,則會產生一個運行時錯誤。如果指定了編輯器,這將打開編輯器並允許你修改合約代碼。
- update:更新測試中的合約。這將會移除包含了不匹配異常的註解,或者增加缺失的預想結果。然後測試會重新開始。
- skip:跳過當前測試的執行。
- quit:退出 isoltest。
在上邊的情況自動更新合約會把它變為:
contract test { uint256 variable;}// ----
並重新運行測試。它將會通過:
Re-running test case...
syntaxTests/double_stateVariable_declaration.sol: OK
4
通過 AFL 運行 Fuzzer
Fuzzing 是一種測試技術,它可以通過運行多少不等的隨機輸入來找出異常的執行狀態(片段故障、異常等等)。現代的 fuzzer 已經可以很聰明地在輸入中進行直接的查詢。 我們有一個專門的程序叫做 solfuzzer,它可以將源代碼作為輸入,當發生一個內部編譯錯誤、片段故障或者類似的錯誤時失敗,但當代碼包含錯誤的時候則不會失敗。 通過這種方法,fuzzing 工具可以找到那些編譯級別的內部錯誤。
我們主要使用 AFL(http://lcamtuf.coredump.cx/afl/) 來進行 fuzzing 測試。你需要手工下載和構建 AFL。然後用 AFL 作為編譯器來構建 Solidity(或直接構建 solfuzzer):
cd build# if neededmake cleancmake .. -DCMAKE_C_COMPILER=path/to/afl-gcc -DCMAKE_CXX_COMPILER=path/to/afl-g++make solfuzzer
然後,你需要一個源文件例子。這將使 fuzzer 可以更容易地找到錯誤。你可以從語法測試目錄下拷貝一些文件或者從文檔中提取一些測試文件或其他測試:
mkdir /tmp/test_casescd /tmp/test_cases# extract from tests:path/to/solidity/scripts/isolate_tests.py path/to/solidity/test/libsolidity/SolidityEndToEndTest.cpp# extract from documentation:path/to/solidity/scripts/isolate_tests.py path/to/solidity/docs docs
AFL 的文檔指出,賬冊(初始的輸入文件)不應該太大。每個文件本身不應該超過 1 kB,並且每個功能最多只能有一個輸入文件;所以最好從少量的輸入文件開始。 此外還有一個叫做 afl-cmin 的工具,可以將輸入文件整理為可以具有近似行為的二進位代碼。
現在運行 fuzzer(-m 參數將使用的內存大小擴展為 60 MB):
afl-fuzz -m 60 -i /tmp/test_cases -o /tmp/fuzzer_reports -- /path/to/solfuzzer
fuzzer 會將導致失敗的源文件創建在 /tmp/fuzzer_reports 中。通常它會找到產生相似錯誤的類似的源文件。 你可以使用 scripts/uniqueErrors.sh 工具來過濾重複的錯誤。
5
Whiskers 模板系統
Whiskers 是一個類似於 Mustache(https://mustache.github.io/) 的模板系統。編譯器在各種各樣的地方使用 Whiskers 來增強可讀性,從而提高代碼的可維護性和可驗證性。
它的語法與 Mustache 有很大差別:模板標記 {{ 和 }} 被替換成了 < 和 > ,以便增強語法分析,避免與 內聯彙編 的衝突(符號 < 和 > 在內聯彙編中是無效的,而 { 和 } 則被用來限定塊)。另一個局限是,列表只會被解析一層,而不是遞歸解析。未來可能會改變這一個限制。
下面是一個粗略的說明:
任何出現 <name> 的地方都會被所提供的變數 name 的字元串值所替換,既不會進行任何轉義也不會迭代替換。可以通過 <#name>...</name> 來限定一個區域。該區域中的內容將進行多次拼接,每次拼接會使用相應變數集中的值替換區域中的 <inner> 項,模板系統中提供了多少組變數集,就會進行多少次拼接。頂層變數也可以在這樣的區域的內部使用。
譯者註:對於區域<#name>...</name>的釋義,譯者參考自:https://github.com/janl/mustache.js#sections
常見問題
這份清單最早是由 fivedogit 收集整理的。
1
基本問題
可以在特定的區塊上進行操作嗎?(比如發布一個合約或執行一筆交易)
鑒於交易數據的寫入是由礦工決定的而不是由提交者決定的,誰也無法保證交易一定會發生在下一個或未來某一個特定的區塊上。這個結論適用於函數調用/交易以及合約的創建。
如果你希望你的合約被定時調用,可以使用:alarm clock(https://www.ethereum-alarm-clock.com/)。
什麼是交易的「有效載荷(payload)」?
就是隨交易一起發送的位元組碼「數據」。
存在反編譯器嗎?
除了 Porosity 有點接近之外,Solidity 沒有嚴格意義上的反編譯器。由於諸如變數名、注釋、代碼格式等會在編譯過程中丟失,所以完全反編譯回源代碼是沒有可能的。
很多區塊鏈瀏覽器都能將位元組碼分解為一系列操作碼。
如果區塊鏈上的合約會被第三方使用,那麼最好將源代碼一起進行發布。
創建一個可以被中止並退款的合約
首先,需要提醒一下:中止合約聽起來是一個好主意,把垃圾打掃乾淨是個好習慣,但如上所述,合約是不會被真正清理乾淨的。甚至,被發送至已移除合約的以太幣,會從此丟失。
如果想讓合約不再可用,建議的做法是修改合約內部狀態來使其 失效 ,讓所有函數調用都變為無效返回。這樣就無法使用這份合約了,而且發送過去的以太幣也會被自動退回。
現在正式回答這個問題:在構造函數中,將 creator 賦值為 msg.sender ,並保存。然後調用 selfdestruct(creator); 來中止程序並進行退款。
例子(https://github.com/fivedogit/solidity-baby-steps/blob/master/contracts/05_greeter.sol)
需要注意的是,如果你已經在合約頂部做了引用 import "mortal" 並且聲明了 contract SomeContract is mortal { ... ,然後再在已存在此合約的編譯器中進行編譯(包含 Remix),那麼 kill() 就會自動執行。當一份合約被聲明為 mortal 時,你可以仿照我的例子,使用 contractname.kill.sendTransaction({from:eth.coinbase}) 來中止它。
調用 Solidity 方法可以返回一個數組或字元串(string)嗎?
可以。參考 array_receiver_and_returner.sol 。
但是,在 Solidity內部 調用一個函數並返回變長數據(例如 uint[] 這種變長數組)時,往往會出現問題。這是 以太坊虛擬機Ethereum Virtual Machine(EVM) 自身的限制,我們已經計劃在下一次協議升級時解決這個問題。
將變長數據作為外部交易或調用的一部分返回是沒問題的。
數組可以使用 in-line 的方式(指在聲明變數的同一個語句中)來初始化嗎?比如: string[] myarray = ["a", "b"];
可以。然而需要注意的是,這方法現在只能用於定長 內存memory 數組。你甚至可以在返回語句中用 in-line 的方式新建一個 內存memory 數組。聽起來很酷,對吧!
例子:
pragma solidity ^0.4.16;contract C { function f() public pure returns (uint8[5]) { string[4] memory adaArr = ["This", "is", "an", "array"]; return ([1, 2, 3, 4, 5]); }}
合約的函數可以返回結構(struct)嗎?
可以,但只適用於內部(internal)函數調用。
我從一個返回的枚舉類型(enum)中,使用 web3.js 只得到了整數值。我該如何獲取具名數值?
雖然 Solidity 支持枚舉類型,但 ABI(應用程序二進位介面)並不支持。當前階段你需要自己去做映射,將來我們可能會提供一些幫助。
可以使用 in-line 的方式來初始化狀態變數嗎?
可以,所有類型都可以(甚至包括結構)。然而需要注意的是,在數組使用這個方法的時候需要將其定義為靜態 內存memory 數組。
例子:
pragma solidity ^0.4.0;contract C { struct S { uint a; uint b; } S public x = S(1, 2); string name = "Ada"; string[4] adaArr = ["This", "is", "an", "array"];}contract D { C c = new C();}
結構(structs)如何使用?
參考 struct_and_for_loop_tester.sol 。
循環(for loops)如何使用?
和 JavaScript 非常相像。但有一點需要注意:
如果你使用 for (var i = 0; i < a.length; i ++) { a[i] = i; } ,那麼 i 的數據類型將會是 uint8,需要從 0 開始計數。也就是說,如果 a 有超過 255 個元素,那麼循環就無法中止,因為 i 最大只能變為 255。
最好使用 for (uint i = 0; i < a.length...
參考 struct_and_for_loop_tester.sol。
有沒有一些簡單的操作字元串的例子(substring,indexOf,charAt 等)?
這裡有一些字元串相關的功能性函數 stringUtils.sol ,並且會在將來作擴展。另外,Arachnid 有寫過 solidity-stringutils。
當前,如果你想修改一個字元串(甚至你只是想獲取其長度),首先都必須將其轉化為一個 bytes
pragma solidity ^0.4.0;contract C { string s; function append(byte c) public { bytes(s).push(c); } function set(uint i, byte c) public { bytes(s)[i] = c; }}
我能拼接兩個字元串嗎?
目前只能通過手工實現。
為什麼大家都選擇將合約實例化成一個變數(ContractB b;),然後去執行變數的函數(b.doSomething();),而不是直接調用這個 低級函數low-level function .call() ?
如果你調用實際的成員函數,編譯器會提示諸如參數類型不匹配的問題,如果函數不存在或者不可見,他也會自動幫你打包參數。
參考 ping.sol 和 pong.sol 。
沒被使用的 gas 會被自動退回嗎?是的,馬上會退回。也就是說,作為交易的一部分,在交易完成的同時完成退款。
當返回一個值的時候,比如說 uint 類型的值, 可以返回一個 undefined 或者類似 null 的值嗎?
這不可能,因為所有的數據類型已經覆蓋了全部的取值範圍。
替代方案是可以在錯誤時拋出(throw),這同樣能復原整個交易,當你遇到意外情況時不失為一個好的選擇。
如果你不想拋出,也可以返回一對(a pair)值
pragma solidity >0.4.23 <0.5.0;contract C { uint[] counters; function getCounter(uint index) public view returns (uint counter, bool error) { if (index >= counters.length) return (0, true); else return (counters[index], false); } function checkCounter(uint index) public view { (uint counter, bool error) = getCounter(index); if (error) { // ... } else { // ... } }}
注釋會被包含在已部署的合約里嗎,而且會增加部署的 gas 嗎?
不會,所有執行時非必須的內容都會在編譯的時候被移除。 其中就包括注釋、變數名和類型名。
如果在調用合約的函數時一起發送了以太幣,將會發生什麼?
就像在創建合約時發送以太幣一樣,會累加到合約的餘額總數上。 你只可以將以太幣一起發送至擁有 payable 修飾符的函數,不然會拋出異常。
合約對合約的交易可以獲得交易回執嗎?
不能,合約對合約的函數調用並不會創建前者自己的交易,你必須要去查看全部的交易。這也是為什麼很多區塊瀏覽器無法正確顯示合約對合約發送的以太幣。
關鍵字 memory 是什麼?是用來做什麼的?以太坊虛擬機Ethereum Virtual Machine(EVM) 擁有三類存儲區域。
第一類是 存儲storage,貯存了合約聲明中所有的變數。 虛擬機會為每份合約分別划出一片獨立的 存儲storage 區域,並在函數相互調用時持久存在,所以其使用開銷非常大。
第二類是 內存memory,用於暫存數據。其中存儲的內容會在函數被調用(包括外部函數)時擦除,所以其使用開銷相對較小。
第三類是棧,用於存放小型的局部變數。使用幾乎是免費的,但容量有限。
對絕大部分數據類型來說,由於每次被使用時都會被複制,所以你無法指定將其存儲在哪裡。
在數據類型中,對所謂存儲地點比較重視的是結構和數組。 如果你在函數調用中傳遞了這類變數,假設它們的數據可以被貯存在 存儲storage 或 內存memory 中,那麼它們將不會被複制。也就是說,當你在被調用函數中修改了它們的內容,這些修改對調用者也是可見的。
不同數據類型的變數會有各自默認的存儲地點:
- 狀態變數總是會貯存在 存儲storage中
- 函數參數默認存放在內存memory中
- 結構、數組或映射類型的局部變數,默認會放在存儲storage中
- 除結構、數組及映射類型之外的局部變數,會儲存在棧中
例子:
pragma solidity ^0.4.0;contract C { uint[] data1; uint[] data2; function appendOne() public { append(data1); } function appendTwo() public { append(data2); } function append(uint[] storage d) internal { d.push(1); }}
函數 append 能一起作用於 data1 和 data2,並且修改是永久保存的。如果你移除了 storage 關鍵字,函數的參數會默認存儲於 memory。這帶來的影響是,在 append(data1) 或 append(data2)被調用的時候,一份全新的狀態變數的拷貝會在 內存memory 中被創建,append 操作的會是這份拷貝(也不支持 .push ——但這又是另一個話題了)。針對這份全新的拷貝的修改,不會反過來影響 data1 或 data2。
一個常見誤區就是聲明了一個局部變數,就認為它會創建在 內存memory 中,其實它會被創建在 存儲storage 中:
/// 這份合約包含一處錯誤pragma solidity ^0.4.0;contract C { uint someVariable; uint[] data; function f() public { uint[] x; x.push(2); data = x; }}
局部變數 x 的數據類型是 uint[] storage,但由於 存儲storage 不是動態分配的,它需要在使用前通過狀態變數賦值。所以 x 本身不會被分配 存儲storage 的空間,取而代之的是,它只是作為 存儲storage 中已有變數的別名。
實際上會發生的是,編譯器將 x 解析為一個 存儲storage 指針,並默認將指針指向 存儲插槽storage slot 0 。這就造成 someVariable (貯存在 存儲插槽storage slot 0)會被 x.push(2) 更改。(在本例中,兩個合約變數 someVariable 和 data 會被預先分配到兩個 存儲插槽storage slot 中,即 存儲插槽storage slot 0 和 存儲插槽storage slot 1 。上面的程序會使局部變數 x 變成指向保存了變數 someVariable 的 存儲插槽storage slot 0 的指針。譯者注。)
正確的方法如下:
pragma solidity ^0.4.0;contract C { uint someVariable; uint[] data; function f() public { uint[] x = data; x.push(2); }}
2
高級問題
怎樣才能在合約中獲取一個隨機數?(實施一份自動回款的博彩合約)
做好隨機這件事情,往往是一個加密項目最關鍵的部分,大部分的失敗都來自於使用了低劣的隨機數發生器。
如果你不考慮安全性,可以做一個類似於 coin flipper 的東西,反之,最好調用一份可以提供隨機性的合約,比如 RANDAO 。
從另一份合約中的 non-constant 函數獲取返回值
關鍵點是調用者(合約)需要了解將被調用的函數。
參考 ping.sol 和 pong.sol 。
讓合約在首次被挖出時就開始做些事情
使用構造函數。在構造函數中寫的任何內容都會在首次被挖出時執行。
參考 replicator.sol 。
怎樣才能創建二維數組?
參考 2D_array.sol 。
需要注意的是,用 uint8 類型的數據填滿一個 10x10 的方陣,再加上合約創建,總共需要花費超過 800,000 的 gas。如果是 17x17 需要 2,000,000 的 gas。然而交易的 gas 上限是 314 萬。。。好吧,其實你也玩不了太大的花樣。
注意,「創建」數組純粹是免費的,成本在於填充數組。
還需注意,優化 存儲storage 訪問可以大大降低 gas 的花費,因為一個 存儲插槽storage slot 可以存放下 32 個 uint8類型的值。但這類優化目前也存在一些問題:在跨循環的時候不起作用;以及在邊界檢查時候會出問題。當然,在未來這種情況會得到改觀。
當我們複製一個結構(struct)時, 結構 (struct)中定義的映射會被怎麼處理?
這是一個非常有意思的問題。假設我們有一份合約,裡面的欄位設置如下:
struct User { mapping(string => string) comments;}function somefunction public { User user1; user1.comments["Hello"] = "World"; User user2 = user1;}
在這種情況下,由於缺失「被映射的鍵列表」,被複制至 userList 的結構中的映射會被忽視。因此,系統無法找出什麼值可以被複制過去。
我應該如何初始化一份只包含指定數量 wei 的合約?
目前實現方式不是太優雅,當然暫時也沒有更好的方法。 就拿 合約A 調用一個 合約B 的新實例來說,new B 周圍必須要加括弧,不然 B.value 會被認作是 B 的一個成員函數,叫做 value。 你必須確保兩份合約都知道對方的存在,並且 合約B 擁有 payable 構造函數。
就是這個例子:
pragma solidity ^0.4.0;contract B { function B() public payable {}}contract A { address child; function test() public { child = (new B).value(10)(); //construct a new B with 10 wei }}
合約的函數可以接收二維數組嗎?
二維數組還無法使用於外部調用和動態數組——你只能使用一維的動態數組。
bytes32 和 string 有什麼關係嗎?為什麼 bytes32 somevar = "stringliteral"; 可以生效,還有保存下來的那個 32 位元組的 16 進位數值有什麼含義嗎?
數據類型 bytes32 可以存放 32 個(原始)位元組。在給變數分配值的過程中 bytes32 samevar = "stringliteral";, 字元串已經被逐字翻譯成了原始位元組。如果你去檢查 somevar ,會發現一個 32 位元組的 16 進位數值,這就是用 16 進位表示的 字元串的文字 。
數據類型 bytes 與此類似,只是它的長度可以改變。
最終來看,假設 bytes 儲存的是字元串的 UTF-8 編碼,那麼它和 string 基本是等同的。由於 string 存儲storage 的是 UTF-8 編碼格式的數據,所以計算字元串中字元數量的成本是很高的(某些字元的編碼甚至大於一個位元組)。因此,系統還不支持 string s; s.length ,甚至不能通過索引訪問 s[2] 。但如果你想訪問字元串的下級位元組編碼,可以使用 bytes(s).length 和 bytes(s)[2],它們分別會返回字元串在 UTF-8 編碼下的位元組數量(不是字元數量)以及字元串 UTF-8 編碼的第二個位元組(不是字元)。
一份合約可以傳遞一個數組(固定長度)或者一個字元串或者一個 bytes (不定長度)給另一份合約嗎?
當然可以。但如果不小心跨越了 內存memory / 存儲storage 的邊界,一份獨立的拷貝就會被創建出來:
pragma solidity ^0.4.16;contract C { uint[20] x; function f() public { g(x); h(x); } function g(uint[20] y) internal pure { y[2] = 3; } function h(uint[20] storage y) internal { y[3] = 4; }}
由於會在 內存memory 中對 存儲storage 的值創建一份獨立的拷貝(默認存儲在 內存memory 中),所以對 g(x) 的調用其實並不會對 x 產生影響。另一方面,由於傳遞的只是引用而不是一個拷貝, h(x) 得以成功地修改了 x。
有些時候,當我想用類似這樣的表達式: arrayname.length = 7; 來修改數組長度,卻會得到一個編譯錯誤 Value must be an lvalue。這是為什麼?
你可以使用 arrayname.length = <some new length>; 來調整 存儲storage 中的動態數組(也就是在合約級別聲明的數組)的長度。如果你得到一個 lvalue 錯誤,那麼你有可能做錯了以下兩件事中的一件或全部。
- 你在嘗試修改長度的數組可能是保存在 內存memory中的,或者
- 你可能在嘗試修改一個非動態數組的長度。
// 這將無法編譯通過pragma solidity ^0.4.18;contract C { int8[] dynamicStorageArray; int8[5] fixedStorageArray; function f() { int8[] memory memArr; // 第一種情況 memArr.length++; // 非法 int8[5] storage storageArr = fixedStorageArray; // 第二種情況 storageArr.length++; // 非法 int8[] storage storageArr2 = dynamicStorageArray; storageArr2.length++; // 非法 }}
重要提醒: 在 Solidity 中,數組維數的聲明方向是和在 C 或 Java 中的聲明方向相反的,但訪問方式相同。
舉個例子,int8[][5] somearray; 是5個 int8 格式的動態數組。
這麼做的原因是,T[5] 總是能被識別為5個 T 的數組,哪怕 T 本身就是一個數組(而在 C 或 Java 是不一樣的)。
Solidity 的函數可以返回一個字元串數組嗎(string[])?
暫時還不可以,因為這要求兩個維度都是動態數組(string 本身就是一種動態數組)。
如果你發起了一次獲取數組的調用,有可能獲得整個數組嗎?還是說另外需要寫一個輔助函數來實現?
一個數組類型的公共狀態變數會有一個自動的獲取函數 getter function , 這個函數只會返回單個元素。如果你想獲取完整的數組,那麼只能再手工寫一個函數來實現。
如果某個賬戶只存儲了值但沒有任何代碼,將會發生什麼?例子: http://test.ether.camp/account/5f740b3a43fbb99724ce93a879805f4dc89178b5
構造函數做的最後一件事情是返回合約的代碼。這件事消耗的 gas 取決於代碼的長度,其中有種可能的情況是提供的 gas 不夠。這是唯一的一種情況下,出現了 「out of gas」 異常卻不會去復原改變了的狀態,這個改變在這裡就是對狀態變數的初始化。
https://github.com/ethereum/wiki/wiki/Subtleties
當 CREATE 操作的某個階段被成功執行,如果這個操作返回 x,那麼 5 * len(x) 的 gas 在合約被創建前會從剩餘 gas 中被扣除。如果剩餘的 gas 少於 5 * len(x),那麼就不進行 gas 扣除,而是把創建的合約代碼改變成空字元串,但這時候並不認為是發生了異常——不會發生復原。
在定製 通證token 的合約中,下面這些奇怪的校驗是做什麼的?
require((balanceOf[_to] + _value) >= balanceOf[_to]);
在Solidity(以及大多數其他機器相關的編程語言)中的整型都會被限定在一定範圍內。 比如 uint256 ,就是從 0 到 2**256 - 1 。如果針對這些數字進行操作的結果不在這個範圍內,那麼就會被截斷。這些截斷會帶來 嚴重的後果 ,所以像上面這樣的代碼需要考慮避免此類攻擊。
更多問題?
如果你有其他問題,或者你的問題在這裡找不到答案,請在此聯繫我們 gitter(https://gitter.im/ethereum/solidity) 或者提交一個 issue(https://github.com/ethereum/solidity/issues)。
延伸閱讀:智能合約-Solidity官方文檔(1)
安裝Solidity編譯器-Solidity官方文檔(2)
根據例子學習Solidity-Solidity官方文檔(3)
深入理解Solidity之源文件及合約結構——Solidity中文文檔(4)
安全考量——Solidity中文文檔(5)
合約的元數據——Solidity中文文檔(6)
應用二進位介面(ABI) 說明——Solidity中文文檔(7)
使用編譯器——Solidity中文文檔(8)
Yul語言及對象說明——Solidity中文文檔(9)
風格指南——Solidity中文文檔(10)
通用模式——Solidity中文文檔(11)
已知bug列表——Solidity中文文檔(12)
點擊「閱讀原文」即可查看完整中文文檔
本文內容來源於HiBlock區塊鏈社區翻譯小組,感謝全體譯者的辛苦工作。點擊「閱讀原文」即可查看完整中文文檔。
推薦閱讀: