區塊鏈的基礎入門(區塊鏈教程(三):Solidity編程基礎)
注:本教程為技術教程,不談論且不涉及炒作任何數字貨幣
Solidity 入門教學
1、 簡介
1.1 Solidity是什么
Solidity 是一門面向合約的、為實現智能合約而創建的高級編程語言。這門語言受到了 C++,Python 和 Javascript 語言的影響,設計的目的是能在以太坊虛擬機(EVM)上運行。
Solidity 是靜態類型語言,支持繼承、庫和復雜的用戶定義類型等特性。
內含的類型除了常見編程語言中的標準類型,還包括 address等以太坊獨有的類型,Solidity 源碼文件通常以 .sol 作為擴展名
目前嘗試 Solidity 編程的推薦方式是使用 Remix。Remix是一個基于 Web 瀏覽器的 IDE,它可以讓你編寫 Solidity 智能合約,然后部署并運行該智能合約。
1.2 Solidity語言特性
Solidity的語法接近于JavaScript,是一種面向對象的語言。但作為一種真正意義上運行在網絡上的去中心合約,它又有很多的不同:
以太坊底層基于帳戶,而不是 UTXO,所以增加了一個特殊的address 的數據類型用于定位用戶和合約賬戶。
語言內嵌框架支持支付。提供了 payable 等關鍵字,可以在語言層面直接支持支付。
使用區塊鏈進行數據存儲。數據的每一個狀態都可以永久存儲,所以在使用時需要確定變量使用內存,還是區塊鏈存儲。
運行環境是在去中心化的網絡上,所以需要強調合約或函數執行的調用的方式。
不同的異常機制。一旦出現異常,所有的執行都將會被回撤,這主要是為了保證合約執行的原子性,以避免中間狀態出現的數據不一致。
1.3 Solidity源碼和智能合約
Solidity 源代碼要成為可以運行在以太坊上的智能合約需要經歷如下的
步驟:
用 Solidity 編寫的智能合約源代碼需要先使用編譯器編譯為字節碼(Bytecode),編譯過程中會同時產生智能合約的二進制接口規范(Application Binary Interface,簡稱為ABI);
通過交易(Transaction)的方式將字節碼部署到以太坊網絡,每次成功部署都會產生一個新的智能合約賬戶;
使用 Javascript 編寫的 DApp 通常通過 web3.js + ABI去調用智能合約中的函數來實現數據的讀取和修改。
1.4 合約結構
狀態變量(State Variables)作為合約狀態的一部分,值會永久保存在存儲空間內。
函數(Functions)合約中可執行的代碼塊。
函數修飾器(Function Modifiers)在函數聲明中,用來補充修飾函數的語義。
事件(Events)非常方便的 EVM 日志工具接口。
2、 Solidity編譯器安裝以及簡單使用
Remix 是一個開源的 IDE,是一個瀏覽器在線編輯器。作為 Solidity 智能合約開發環境,Solidity IDE Remix(在線瀏覽器編輯器)提供基本的編譯、部署至本地或測試網絡、執行合約等功能。
2.1 remix安裝以及使用
瀏覽器端配置
在瀏覽器端有倆個選擇,分別為英文版與中文版(有些許差別)
Remix中文版地址:http://remix.hubwiz.com
Remix英文版地址(推薦):https://remix.ethereum.org/
PS.可能需要科學上網
在這里插入圖片描述
下面都以英文版為例子介紹
1、瀏覽器輸入 https://remix.ethereum.org/
如果出現加載慢,加載不完全的情況,刷新幾次即可
2、左側可以看到我們所有的文件,下面是我們的remix控制臺
在這里插入圖片描述
上圖小圖標從左到右依次為:
創建新文件
創建新文件夾
Github代碼片段分享
表示打開一個本地文件
控制臺圖片如下:
在這里插入圖片描述
1 從左至右表示隱藏控制臺、清除控制臺輸出、pending的交易數量
2 表示監聽所有交易
3 表示搜索框
4 表示輸出區域
5 表示使用JavaScript與以太坊交互的區域,可以使用Web3對象
3、點擊文件樣式圖標輸入我們的文件名即可(以.sol為后綴)
在這里插入圖片描述
4、安裝必要的插件
點擊插件管理器,頁面中為這個圖標
在這里插入圖片描述
安裝compiler 搜索關鍵字compiler
在這里插入圖片描述
5、寫一個簡單的樣例
pragma solidity ^0.4.0;contract SimpleStorage { uint storedData; function set(uint x) public { storedData = x; } function get() public view returns (uint) { return storedData; }}
第一行就是告訴大家源代碼使用Solidity版本0.4.0寫的,并且使用0.4.0以上版本運行也沒問題(最高到0.5.0,但是不包含0.5.0)。這是為了確保合約不會在新的編譯器版本中突然行為異常。關鍵字 pragma 的含義是,一般來說,pragmas(編譯指令)是告知編譯器如何處理源代碼的指令的。
Solidity中合約的含義就是一組代碼(它的 函數 )和數據(它的 狀態 ),它們位于以太坊區塊鏈的一個特定地址上。 代碼行 uint storedData; 聲明一個類型為 uint (256位無符號整數)的狀態變量,叫做 storedData 。 你可以認為它是數據庫里的一個位置,可以通過調用管理數據庫代碼的函數進行查詢和變更。對于以太坊來說,上述的合約就是擁有合約(owning contract)。在這種情況下,函數 set 和 get 可以用來變更或取出變量的值。
要訪問一個狀態變量,并不需要像 this. 這樣的前綴,雖然這是其他語言常見的做法。
該合約能完成的事情并不多(由于以太坊構建的基礎架構的原因):它能允許任何人在合約中存儲一個單獨的數字,并且這個數字可以被世界上任何人訪問,且沒有可行的辦法阻止你發布這個數字。當然,任何人都可以再次調用 set ,傳入不同的值,覆蓋你的數字,但是這個數字仍會被存儲在區塊鏈的歷史記錄中。
在這里插入圖片描述
點擊compile test.sol,可以看到編譯按鈕,建議將Auto compile打鉤(自動編譯),之后會在編譯圖標上看到一個以綠色為背景的對勾。
編譯組件說明:
Compiler可以選擇Solidity的編譯器版本
Language可以選擇編程語言
EVM Version可以選擇EVM虛擬機版本
Auto compile可以設置自動編譯,修改完代碼后自動執行編譯操作
Enable optimization可以設置對編譯進行優化
Hide warnings可以設置隱藏警告信息。
Contract選擇需要編譯的合約
Publish on Swarm和Publish on Ipfs分別將合約上傳到Swarm和Ipfs這兩個分布式文件系統上去
Compilation Details很重要,可以查看編譯的信息,包括ABI、字節碼、函數Hash等
ABI和Bytecode分別復制ABI和字節碼。
再下面的部分空白用來顯示編譯的Warnings和Errors。
我們點擊Compilation Details就能看到編譯之后的一些信息,如下圖所示(部分)
在這里插入圖片描述
NAME:合約名
METADATA:一些編譯相關的信息,比如版本、所用的語言、設置等
BYTECODE:寫入區塊的字節碼
ABI:此智能合約對應的 ABI ,也就是我們合約里面定義的一些接口
WEB3DEPLOY:智能合約編譯之后的發布命令,這個就是比較重要的,之后的web3就是調用這段命令來部署合約的
METADATAHASH:數據的一個哈希值
SWARMLOCATION:Swarm網絡的一個地址
FUNCTIONHASHES:合約定義的方法的hash,其實我們執行合約的時候就是通過這個hash去找到對應的方法進行執行的
GASESTIMATES:關于礦工費的一個預算,在ETH上進行合約的部署,執行等都是需要礦工費的。一般合約代碼越多礦工費越高。
點擊下面的run圖標,可以看到部署,以及賬戶信息,環境等等
在這里插入圖片描述
點擊deploy之后天可以看到自己的合約已經部署完成,打開之后可以看見我們寫的函數set,get了,給set函數輸入一個值,點擊get會得到相應的值
在這里插入圖片描述
Environment 表示合約部署的環境。Javascript VM是虛擬了一個節點,而Injected Web3和Web3 Provider則真正連接一個節點。
Account代表不同的虛擬賬戶,每個虛擬賬戶每個有 100 ETH
Deploy表示合約部署按鈕
Deployed Contracts表示已經部署的合約
中文版界面與英文版界面有些許不一致,但都大同小異,想了解同學可以查看本博客(界面與中文版大致相同): Solidity語言編輯器REMIX指導大全
本地配置: win下ubuntu下
Docker
我們為編譯器提供了最新的docker構建。 stable 倉庫里的是已發布的版本,nightly 倉庫則是在開發分支中的帶有不穩定變更的版本。
docker run ethereum/solc:stable solc --version
目前,docker 鏡像只含有 solc 的可執行程序,因此你需要額外的工作去把源代碼和輸出目錄連接起來。
3、Solidity基礎操作
由于篇幅有限,以下只會講解一些較基礎、重要的概念(足夠后面使用),有些可能會一帶而過或者“忽略”,如果大家途中有沒太明白地方建議先百度、Google,或者查看此教程Solifity中文文檔、Solidity英文文檔
3.1 Solidity源文件布局
源文件可以被版本雜注pragma所注解,表明要求的編譯器版本
例如:
pragma solidity ^0.4.0;
這樣,源文件將既不允許低于 0.4.0 版本的編譯器編譯, 也不允許高于(包含) 0.5.0 版本的編譯器編譯(第二個條件因使用 ^ 被添加)。 這種做法的考慮是,編譯器在 0.5.0 版本之前不會有重大變更,所以可確保源代碼始終按預期被編譯。 上面例子中不固定編譯器的具體版本號,因此編譯器的補丁版也可以使用。
import(導入其它源文件)
Solidity 所支持的導入語句import,語法同 JavaScript(從ES6 起)非常類似
import "filename";
從“filename”中導入所有的全局符號到當前全局作用域中
import * as symbolName from "filename";
創建一個新的全局符號 symbolName,其成員均來自 “filename”中全局符號
import {symbol1 as alias, symbol2} from "filename";
創建新的全局符號 alias 和 symbol2,分別從 "filename" 引用 symbol1 和 symbol2
import "filename" as symbolName;
這條語句等同于 import * as symbolName from "filename";
注釋
可以使用單行注釋(//)和多行注釋(/.../)
// 這是一個單行注釋。/*這是一個多行注釋。*/
3.2 數據類型與運算符
3.2.1 Solidity值類型介紹
布爾(bool):
可能的取值為字符常量值 true 或 false
例子:
pragma solidity ^0.4.0;contract helloworld { bool boola=true; //聲明一個布爾類型的值,只用一個等號 function booltesta() public view returns(bool){ return boola; } function booltestb(int a,int b) public pure returns(bool){ return a==b; }}
在這里插入圖片描述
整型(int/uint)**:
int / uint :分別表示有符號和無符號的不同位數的整型變量。 支持關鍵字 uint8 到 uint256 (無符號,從 8 位到 256 位)以及 int8 到 int256,以 8 位為步長遞增。 uint 和 int 分別是 uint256 和 int256 的別名。
定長浮點型(fixed / ufixed):
fixed/ ufixed:表示各種大小的有符號和無符號的定長浮點型。 在關鍵字 ufixedMxN 和 fixedMxN 中,M 表示該類型占用的位數,N 表示可用的小數位數。 M必須能整除 8,即 8 到 256 位。 N則可以是從 0 到 80 之間的任意數。 ufixed 和 fixed 分別是 ufixed128x19 和 fixed128x19 的別名。
地址(address 重點,后面細講):
地址類型存儲一個 20 字節的值(以太坊地址的大小)。 地址類型也有成員變量,并作為所有合約的基礎。
地址類型成員變量:balance 和 transfer
可以使用 balance 屬性來查詢一個地址的余額, 也可以使用 transfer 函數向一個地址發送 以太幣 (以 wei 為單位):
address x = 0x123;address myAddress = this;if (x.balance < 10 && myAddress.balance >= 10) x.transfer(10);
注:如果 x 是一個合約地址,它的代碼(更具體來說是它的 fallback 函數,如果有的話)會跟 transfer 函數調用一起執行(這是 EVM 的一個特性,無法阻止)。 如果在執行過程中用光了 gas 或者因為任何原因執行失敗,以太幣 交易會被打回,當前的合約也會在終止的同時拋出異常。
定長字節數組:
關鍵字有 bytes1, bytes2, bytes3, ..., bytes32 .length 表示這個字節數組的長度(只讀).
注:可以將 byte[] 當作字節數組使用,但這種方式非常浪費存儲空間,準確來說,是在傳入調用時,每個元素會浪費 31 字節。 更好地做法是使用 bytes。
變長字節數組
bytes:變長字節數組。它并不是值類型。
string:變長 UTF-8 編碼字符串類型。并不是值類型。
地址字面常數(Address Literals)
比如像 0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF 這樣的通過了地址校驗和測試的十六進制字面常數屬于 address 類型。 長度在 39 到 41 個數字的,沒有通過校驗和測試而產生了一個警告的十六進制字面常數視為正常的有理數字面常數。
有理數和整數字面常數
整數字面常數由范圍在 0-9 的一串數字組成,表現成十進制。 例如,69 表示數字 69。 Solidity 中是沒有八進制的,因此前置 0 是無效的。
十進制小數字面常數帶有一個 .,至少在其一邊會有一個數字。 比如:1.,.1,和 1.3。
科學符號也是支持的,盡管指數必須是整數,但底數可以是小數。 比如:2e10, -2e10, 2e-10, 2.5e1。
數值字面常數表達式本身支持任意精度,除非它們被轉換成了非字面常數類型(也就是說,當它們出現在非字面常數表達式中時就會發生轉換)。 這意味著在數值常量表達式中, 計算不會溢出而除法也不會截斷。
例如, (2**800 + 1) - 2**800 的結果是字面常數 1 (屬于 uint8 類型),盡管計算的中間結果已經超過了 以太坊虛擬機 的機器字長度。 此外, .5 * 8 的結果是整型 4 (盡管有非整型參與了計算)
字符串字面常數
字符串字面常數是指由雙引號或單引號引起來的字符串("foo"或者 'bar')。 不像在 C 語言中那樣帶有結束符;"foo" 相當于 3 個字節而不是 4 個。 和整數字面常數一樣,字符串字面常數的類型也可以發生改變,但它們可以隱式地轉換成 bytes1,……,bytes32,如果合適的話,還可以轉換成 bytes 以及 string。
十六進制字面常數
十六進制字面常數以關鍵字 hex 打頭,后面緊跟著用單引號或雙引號引起來的字符串(例如,hex"001122FF")。 字符串的內容必須是一個十六進制的字符串,它們的值將使用二進制表示。
枚舉(enum):
一種用戶可以定義類型的方法,與C語言類似,默認從0開始遞增,一般用來模擬合約的狀態
pragma solidity ^0.4.16;contract test { enum ActionChoices { GoLeft, GoRight, GoStraight, SitStill }; ActionChoices choice; ActionChoices constant defaultChoice = ActionChoices.GoStraight; function setGoStraight() public { choice = ActionChoices.GoStraight; } // 由于枚舉類型不屬于 |ABI| 的一部分,因此對于所有來自 Solidity 外部的調用, // "getChoice" 的簽名會自動被改成 "getChoice() returns (uint8)"。 // 整數類型的大小已經足夠存儲所有枚舉類型的值,隨著值的個數增加, // 可以逐漸使用 `uint16` 或更大的整數類型。 function getChoice() public view returns (ActionChoices) { return choice; } function getDefaultChoice() public pure returns (uint) { return uint(defaultChoice); }}
函數(function):
函數類型是一種表示函數的類型。可以將一個函數賦值給另一個函數類型的變量,也可以將一個函數作為參數進行傳遞,還能在函數調用中返回函數類型變量。 函數類型有兩類:- 內部(internal) 函數和 外部(external) 函數:
內部函數只能在當前合約內被調用(更具體來說,在當前代碼塊內,包括內部庫函數和繼承的函數中),因為它們不能在當前合約上下文的外部被執行。 調用一個內部函數是通過跳轉到它的入口標簽來實現的,就像在當前合約的內部調用一個函數。
外部函數由一個地址和一個函數簽名組成,可以通過外部函數調用傳遞或者返回。
函數類型表示成如下的形式
在這里插入圖片描述
function (<parameter types>) {internal|external} [pure|constant|view|payable] [returns (<return types>)]
與參數類型相反,返回類型不能為空 —— 如果函數類型不需要返回,則需要刪除整個 returns (<return types>) 部分。
函數類型默認是內部函數,因此不需要聲明 internal 關鍵字。 與此相反的是,合約中的函數本身默認是 public的,只有當它被當做類型名稱時,默認才是內部函數。
有兩種方法可以訪問當前合約中的函數:一種是直接使用它的名字,f ,另一種是使用 this.f 。 前者適用于內部函數,后者適用于外部函數。
如果當函數類型的變量還沒有初始化時就調用它的話會引發一個異常。 如果在一個函數被 delete 之后調用它也會發生相同的情況。
如果外部函數類型在 Solidity 的上下文環境以外的地方使用,它們會被視為 function 類型。 該類型將函數地址緊跟其函數標識一起編碼為一個 bytes24 類型。
請注意,當前合約的 public 函數既可以被當作內部函數也可以被當作外部函數使用。 如果想將一個函數當作內部函數使用,就用 f 調用,如果想將其當作外部函數,使用 this.f 。
Solidity函數可見性
函數的可見性可以指定為 external,public ,internal 或者 private;對于狀態變量,不能設置為 external ,默認是 internal。
external :外部函數作為合約接口的一部分,意味著我們可以從其他合約和交易中調用。 一個外部函數 f不能從內部調用(即 f 不起作用,但 this.f() 可以)。 當收到大量數據的時候,外部函數有時候會更有效率。
public :public 函數是合約接口的一部分,可以在內部或通過消息調用。對于 public 狀態變量, 會自動生成一個 getter 函數。
internal :這些函數和狀態變量只能是內部訪問(即從當前合約內部或從它派生的合約訪問),不使用 this 調用。
private :private 函數和狀態變量僅在當前定義它們的合約中使用,并且不能被派生合約使用。
Solidity函數狀態可變性
pure:純函數,不允許修改或訪問狀態
view:不允許修改狀態
payable:允許從消息調用中接收以太幣Ether 。
constant:與view相同,一般只修飾狀態變量,不允許賦值(除初始化以外)
內部函數調用
當前合約中的函數可以直接(“從內部”)調用,也可以遞歸調用,就像下邊這個荒謬的例子一樣
pragma solidity ^0.4.16;contract C { function g(uint a) public pure returns (uint ret) { return f(); } function f() internal pure returns (uint ret) { return g(7) + f(); }}
這些函數調用在 EVM 中被解釋為簡單的跳轉。這樣做的效果就是當前內存不會被清除,也就是說,通過內部調用在函數之間傳遞內存引用是非常有效的。
外部函數調用
表達式 this.g(8); 和 c.g(2); (其中 c 是合約實例)也是有效的函數調用,但是這種情況下,函數將會通過一個消息調用來被“外部調用”,而不是直接的跳轉。 請注意,不可以在構造函數中通過 this 來調用函數,因為此時真實的合約實例還沒有被創建。
如果想要調用其他合約的函數,需要外部調用。對于一個外部調用,所有的函數參數都需要被復制到內存。
當調用其他合約的函數時,隨函數調用發送的 Wei 和 gas 的數量可以分別由特定選項 .value() 和 .gas() 指定:
pragma solidity ^0.4.0;contract InfoFeed { function info() public payable returns (uint ret) { return 42; }}contract Consumer { InfoFeed feed; function setFeed(address addr) public { feed = InfoFeed(addr); } function callFeed() public { feed.info.value(10).gas(800)(); }}
payable 修飾符要用于修飾 info,否則,.value() 選項將不可用。
注意,表達式 InfoFeed(addr) 進行了一個的顯式類型轉換,說明”我們知道給定地址的合約類型是 InfoFeed “并且這不會執行構造函數。 顯式類型轉換需要謹慎處理。絕對不要在一個你不清楚類型的合約上執行函數調用。
我們也可以直接使用 function setFeed(InfoFeed _feed) { feed = _feed; } 。 注意一個事實,feed.info.value(10).gas(800) 只(局部地)設置了與函數調用一起發送的 Wei 值和 gas 的數量,只有最后的圓括號執行了真正的調用。
如果被調函數所在合約不存在(也就是賬戶中不包含代碼)或者被調用合約本身拋出異常或者 gas 用完等,函數調用會拋出異常。
3.2.2 引用類型介紹
比起之前討論過的值類型,在處理復雜的類型(即占用的空間超過 256 位的類型)時,我們需要更加謹慎。 由于拷貝這些類型變量的開銷相當大,我們不得不考慮它的存儲位置,是將它們保存在 內存 (并不是永久存儲)中, 還是 存儲 (保存狀態變量的地方)中。
數據位置
所有的復雜類型,即 數組 和 結構 類型,都有一個額外屬性,“數據位置”,說明數據是保存在 內存 中還是 存儲 中。 根據上下文不同,大多數時候數據有默認的位置,但也可以通過在類型名后增加關鍵字 storage 或 memory 進行修改。 函數參數(包括返回的參數)的數據位置默認是 memory, 局部變量的數據位置默認是 storage,狀態變量的數據位置強制是 storage。
也存在第三種數據位置, calldata ,這是一塊只讀的,且不會永久存儲的位置,用來存儲函數參數。 外部函數的參數(非返回參數)的數據位置被強制指定為 calldata,效果跟 memory 差不多。
例子:
pragma solidity ^0.4.0;contract C { uint[] x; // x 的數據存儲位置是 storage // memoryArray 的數據存儲位置是 memory function f(uint[] memoryArray) public { x = memoryArray; // 將整個數組拷貝到 storage 中,可行 var y = x; // 分配一個指針(其中 y 的數據存儲位置是 storage),可行 y[7]; // 返回第 8 個元素,可行 y.length = 2; // 通過 y 修改 x,可行 delete x; // 清除數組,同時修改 y,可行 // 下面的就不可行了;需要在 storage 中創建新的未命名的臨時數組, / // 但 storage 是“靜態”分配的: // y = memoryArray; // 下面這一行也不可行,因為這會“重置”指針, // 但并沒有可以讓它指向的合適的存儲位置。 // delete y; g(x); // 調用 g 函數,同時移交對 x 的引用 h(x); // 調用 h 函數,同時在 memory 中創建一個獨立的臨時拷貝 } function g(uint[] storage storageArray) internal {} function h(uint[] memoryArray) public {}}
歸納:
強制指定的數據位置:
外部函數的參數(不包括返回參數): calldata
狀態變量: storage
默認數據位置:
函數參數(包括返回參數): memory
所有其它局部變量: storage
數組
數組可以在聲明時指定長度,也可以動態調整大小。 對于 存儲 的數組來說,元素類型可以是任意的(即元素也可以是數組類型,映射類型或者結構體)。 對于 內存 的數組來說,元素類型不能是映射類型,如果作為 public 函數的參數,它只能是 ABI 類型。
一個元素類型為 T,固定長度為 k 的數組可以聲明為 T[k],而動態數組聲明為 T[]。
舉個例子,一個長度為 5,元素類型為 uint 的動態數組的數組,應聲明為 uint[][5] (注意這里跟其它語言比,數組長度的聲明位置是反的)。 要訪問第三個動態數組的第二個元素,你應該使用 x[2][1](數組下標是從 0 開始的,且訪問數組時的下標順序與聲明時相反,也就是說,x[2] 是從右邊減少了一級)。。
bytes 和 string 類型的變量是特殊的數組。 bytes 類似于 byte[],但它在 calldata 中會被“緊打包”(譯者注:將元素連續地存在一起,不會按每 32 字節一單元的方式來存放)。 string 與 bytes 相同,但(暫時)不允許用長度或索引來訪問。
注: 如果想要訪問以字節表示的字符串 s,請使用 bytes(s).length / bytes(s)[7] = 'x';。 注意這時你訪問的是 UTF-8 形式的低級bytes 類型,而不是單個的字符。
成員
length:
數組有 length 成員變量表示當前數組的長度。 動態數組可以在 存儲 (而不是 內存 )中通過改變成員變量 .length 改變數組大小。 并不能通過訪問超出當前數組長度的方式實現自動擴展數組的長度。 一經創建,內存 數組的大小就是固定的(但卻是動態的,也就是說,它依賴于運行時的參數)。 push: 變長的 存儲 數組以及 bytes 類型(而不是 string 類型)都有一個叫做 push 的成員函數,它用來附加新的元素到數組末尾。 這個函數將返回新的數組長度。
例子:
pragma solidity ^0.4.16;contract ArrayContract { uint[2**20] m_aLotOfIntegers; // 注意下面的代碼并不是一對動態數組, // 而是一個數組元素為一對變量的動態數組(也就是數組元素為長度為 2 的定長數組的動態數組)。 bool[2][] m_pairsOfFlags; // newPairs 存儲在 memory 中 —— 函數參數默認的存儲位置 function setAllFlagPairs(bool[2][] newPairs) public { // 向一個 storage 的數組賦值會替代整個數組 m_pairsOfFlags = newPairs; } function setFlagPair(uint index, bool flagA, bool flagB) public { // 訪問一個不存在的數組下標會引發一個異常 m_pairsOfFlags[index][0] = flagA; m_pairsOfFlags[index][1] = flagB; } function changeFlagArraySize(uint newSize) public { // 如果 newSize 更小,那么超出的元素會被清除 m_pairsOfFlags.length = newSize; } function clear() public { // 這些代碼會將數組全部清空 delete m_pairsOfFlags; delete m_aLotOfIntegers; // 這里也是實現同樣的功能 m_pairsOfFlags.length = 0; } bytes m_byteData; function byteArrays(bytes data) public { // 字節的數組(語言意義中的 byte 的復數 ``bytes``)不一樣,因為它們不是填充式存儲的, // 但可以當作和 "uint8[]" 一樣對待 m_byteData = data; m_byteData.length += 7; m_byteData[3] = byte(8); delete m_byteData[2]; } function addFlag(bool[2] flag) public returns (uint) { return m_pairsOfFlags.push(flag); } function createMemoryArray(uint size) public pure returns (bytes) { // 使用 `new` 創建動態 memory 數組: uint[2][] memory arrayOfPairs = new uint[2][](size); // 創建一個動態字節數組: bytes memory b = new bytes(200); for (uint i = 0; i < b.length; i++) b[i] = byte(i); return b; }}
結構體
Solidity 支持通過構造結構體的形式定義新的類型,以下是一個結構體的示例:
struct Funder { address addr; uint amount;}struct Campaign { address beneficiary; uint fundingGoal; uint numFunders; uint amount; mapping (uint => Funder) funders;}
映射 映射類型在聲明時的形式為 mapping(_KeyType => _ValueType)。 其中 _KeyType 可以是除了映射、變長數組、合約、枚舉以及結構體以外的幾乎所有類型。 _ValueType 可以是包括映射類型在內的任何類型。
映射可以視作 哈希表 https://en.wikipedia.org/wiki/Hash_table,它們在實際的初始化過程中創建每個可能的 key, 并將其映射到字節形式全是零的值:一個類型的 默認值。然而下面是映射與哈希表不同的地方: 在映射中,實際上并不存儲 key,而是存儲它的 keccak256 哈希值,從而便于查詢實際的值。
正因為如此,映射是沒有長度的,也沒有 key 的集合或 value 的集合的概念。
只有狀態變量(或者在 internal 函數中的對于存儲變量的引用)可以使用映射類型。。
可以將映射聲明為 public,然后來讓 Solidity 創建一個 getter。_KeyType 將成為 getter 的必須參數,并且 getter 會返回 _ValueType。
_ValueType 也可以是一個映射。這時在使用 getter 時將將需要遞歸地傳入每個 _KeyType參數。
pragma solidity ^0.4.0;contract MappingExample { mapping(address => uint) public balances; function update(uint newBalance) public { balances[msg.sender] = newBalance; }}contract MappingUser { function f() public returns (uint) { MappingExample m = new MappingExample(); m.update(100); return m.balances(this); }}
3.2.3 涉及 LValues 的運算符
刪除
delete a 的結果是將 a 的類型在初始化時的值賦值給 a。即對于整型變量來說,相當于 a = 0, 但 delete 也適用于數組,對于動態數組來說,是將數組的長度設為 0,而對于靜態數組來說,是將數組中的所有元素重置。 如果對象是結構體,則將結構體中的所有屬性重置。
delete 對整個映射是無效的(因為映射的鍵可以是任意的,通常也是未知的)。 因此在你刪除一個結構體時,結果將重置所有的非映射屬性,這個過程是遞歸進行的,除非它們是映射。 然而,單個的鍵及其映射的值是可以被刪除的。
理解 delete a的效果就像是給 a 賦值很重要,換句話說,這相當于在 a中存儲了一個新的對象。
pragma solidity ^0.4.0;contract DeleteExample { uint data; uint[] dataArray; function f() public { uint x = data; delete x; // 將 x 設為 0,并不影響數據 delete data; // 將 data 設為 0,并不影響 x,因為它仍然有個副本 uint[] storage y = dataArray; delete dataArray; // 將 dataArray.length 設為 0,但由于 uint[] 是一個復雜的對象,y 也將受到影響, // 因為它是一個存儲位置是 storage 的對象的別名。 // 另一方面:"delete y" 是非法的,引用了 storage 對象的局部變量只能由已有的 storage 對象賦值。 }}
3.3 單位和全局變量
3.3.1 以太幣單位
以太幣 單位之間的換算就是在數字后邊加上 wei、 finney、 szabo 或 ether 來實現的,如果后面沒有單位,缺省為 Wei。例如 2 ether == 2000 finney 的邏輯判斷值為 true。
3.3.2 時間單位
秒是缺省時間單位,在時間單位之間,數字后面帶有 seconds、 minutes、 hours、 days、 weeks 和 years 的可以進行換算,基本換算關系與現實生活相符。
3.3.3 特殊變量和函數
在全局命名空間中已經存在了(預設了)一些特殊的變量和函數,他們主要用來提供關于區塊鏈的信息或一些通用的工具函數。
區塊和交易屬性
block.blockhash(uint blockNumber) returns (bytes32):指定區塊的區塊哈希——僅可用于最新的 256 個區塊且不包括當前區塊;而 blocks 從 0.4.22 版本開始已經不推薦使用,由 blockhash(uint blockNumber) 代替
block.coinbase (address): 挖出當前區塊的礦工地
block.difficulty (uint): 當前區塊難度
block.gaslimit (uint): 當前區塊 gas 限額
block.number (uint): 當前區塊號
block.timestamp (uint): 自 unix epoch 起始當前區塊以秒計的時間戳
gasleft() returns (uint256):剩余的 gas
msg.data (bytes): 完整的 calldata
msg.gas (uint): 剩余 gas - 自 0.4.21 版本開始已經不推薦使用,由 gesleft() 代替
msg.sender (address): 消息發送者(當前調用)
msg.sig (bytes4): calldata 的前 4 字節(也就是函數標識符)
msg.value (uint): 隨消息發送的 wei 的數量
now (uint): 目前區塊時間戳(block.timestamp)
tx.gasprice (uint): 交易的gas 價格
tx.origin (address): 交易發起者(完全的調用鏈)
ABI 編碼函數
abi.encode(...) returns (bytes): ABI - 對給定參數進行編碼
abi.encodePacked(...) returns (bytes):對給定參數執行 緊打包編碼
abi.encodeWithSelector(bytes4 selector, ...) returns (bytes): ABI - 對給定參數進行編碼,并以給定的函數選擇器作為起始的 4 字節數據一起返回
abi.encodeWithSignature(string signature, ...) returns (bytes):等價于 abi.encodeWithSelector(bytes4(keccak256(signature), ...)
錯誤處理
assert(bool condition): 如果條件不滿足,則使當前交易沒有效果 — 用于檢查內部錯誤。
require(bool condition): 如果條件不滿足則撤銷狀態更改 - 用于檢查由輸入或者外部組件引起的錯誤。
require(bool condition, string message): 如果條件不滿足則撤銷狀態更改 - 用于檢查由輸入或者外部組件引起的錯誤,可以同時提供一個錯誤消息。
revert(): 終止運行并撤銷狀態更改。
revert(string reason): 終止運行并撤銷狀態更改,可以同時提供一個解釋性的字符串。
地址相關
<address>.balance (uint256): 以 Wei 為單位的 地址類型 的余額。
<address>.transfer(uint256 amount): 向 地址類型 發送數量為 amount 的 Wei,失敗時拋出異常,發送 2300 gas 的礦工費,不可調節。
<address>.send(uint256 amount) returns (bool): 向 地址類型 發送數量為 amount 的 Wei,失敗時返回 false,發送 2300 gas 的礦工費用,不可調節。
<address>.call(...) returns (bool): 發出低級函數 CALL,失敗時返回 false,發送所有可用 gas,可調節。
<address>.callcode(...) returns (bool): 發出低級函數 CALLCODE,失敗時返回 false,發送所有可用 gas,可調節。
<address>.delegatecall(...) returns (bool): 發出低級函數 DELEGATECALL,失敗時返回 false,發送所有可用 gas,可調節。
3.4 表達式和控制結構(*)
3.4.1 控制結構
avaScript 中的大部分控制結構在 Solidity 中都是可用的,除了 switch 和 goto。 因此 Solidity 中有 if,else,while,do,for,break,continue,return,? :這些與在 C 或者 JavaScript 中表達相同語義的關鍵詞。
用于表示條件的括號 不可以 被省略,單語句體兩邊的花括號可以被省略。
注意,與 C 和 JavaScript 不同, Solidity 中非布爾類型數值不能轉換為布爾類型,因此 if (1) { ... } 的寫法在 Solidity 中 無效 。
當一個函數有多個輸出參數時, return (v0, v1, ...,vn) 寫法可以返回多個值。不過元素的個數必須與輸出參數的個數相同
3.4.2 通過 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); // 將作為合約 C 構造函數的一部分執行 function createD(uint arg) public { D newD = new D(arg); } function createAndEndowD(uint arg, uint amount) public payable { //隨合約的創建發送 ether D newD = (new D).value(amount)(arg); }}
如示例中所示,使用 .value() 選項創建 D 的實例時可以轉發 Ether,但是不可能限制 gas 的數量。如果創建失敗(可能因為棧溢出,或沒有足夠的余額或其他問題),會引發異常。
3.4.3 錯誤處理:Assert, Require, Revert and Exceptions
Solidity 使用狀態恢復異常來處理錯誤。這種異常將撤消對當前調用(及其所有子調用)中的狀態所做的所有更改,并且還向調用者標記錯誤。 便利函數 assert 和 require 可用于檢查條件并在條件不滿足時拋出異常。assert 函數只能用于測試內部錯誤,并檢查非變量。
require 函數用于確認條件有效性,例如輸入變量,或合約狀態變量是否滿足條件,或驗證外部合約調用返回的值。 如果使用得當,分析工具可以評估你的合約,并標示出那些會使 assert 失敗的條件和函數調用。 正常工作的代碼不會導致一個 assert語句的失敗;如果這發生了,那就說明出現了一個需要你修復的 bug。
還有另外兩種觸發異常的方法:revert 函數可以用來標記錯誤并恢復當前的調用。 revert 調用中包含有關錯誤的詳細信息是可能的,這個消息會被返回給調用者。已經不推薦的關鍵字 throw 也可以用來替代 revert() (但無法返回錯誤消息)。
在下例中,你可以看到如何輕松使用require檢查輸入條件以及如何使用assert檢查內部錯誤,注意,你可以給 require 提供一個消息字符串,而 assert 不行。
pragma solidity ^0.4.22;contract Sharer { function sendHalf(address addr) public payable returns (uint balance) { require(msg.value % 2 == 0, "Even value required."); uint balanceBeforeTransfer = this.balance; addr.transfer(msg.value / 2); //由于轉移函數在失敗時拋出異常并且不能在這里回調,因此我們應該沒有辦法仍然有一半的錢。 assert(this.balance == balanceBeforeTransfer - msg.value / 2); return this.balance; }}
3.5 合約
Solidity 合約類似于面向對象語言中的類。合約中有用于數據持久化的狀態變量,和可以修改狀態變量的函數。 調用另一個合約實例的函數時,會執行一個 EVM 函數調用,這個操作會切換執行時的上下文,這樣,前一個合約的狀態變量就不能訪問了。
3.5.1 創建合約
可以通過以太坊交易“從外部”或從 Solidity 合約內部創建合約。 創建合約時,會執行一次構造函數(與合約同名的函數)。構造函數是可選的。只允許有一個構造函數,這意味著不支持重載。
在內部,構造函數參數在合約代碼之后通過 ABI 編碼 傳遞,但是如果你使用 web3.js 則不必關心這個問題。
如果一個合約想要創建另一個合約,那么創建者必須知曉被創建合約的源代碼(和二進制代碼)。 這意味著不可能循環創建依賴項。
3.5.2 getter 函數
編譯器自動為所有 public 狀態變量創建 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. ),它被認為為一個函數。
3.5.3 View 函數
可以將函數聲明為 view 類型,這種情況下要保證不修改狀態。
下面的語句被認為是修改狀態:
修改狀態變量。
產生事件。
創建其它合約。
使用 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; }}
3.5.4 Pure 函數
函數可以聲明為 pure ,在這種情況下,承諾不讀取或修改狀態。
除了上面解釋的狀態修改語句列表之外,以下被認為是從狀態中讀取:
讀取狀態變量。
訪問 this.balance 或者 .balance。
訪問 block,tx, 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); }}
參考自:
黃皮書:https://github.com/yuange1024/ethereum_yellowpaper/blob/master/ethereum_yellow_paper_cn.pdf
白皮書:https://github.com/ethereum/wiki/wiki/White-Paper INlinKC https://ethfans.org/wikis/Home
以太坊solidity學習記錄: https://blog.csdn.net/weixin_45067603/article/details/105726491
尚硅谷區塊鏈全套Go語言→GoWeb→以太坊→項目實戰
注:本系列來自datawhale組隊學習教程,將近結束時跟據同學反饋重新整理而來
author:荒、越前浩波、Yurk、Don