solidity語法完整版


solidity雖然跟js很像,但實際還是有很多不一樣的地方,如果不專門學習solidity語法,只能朦朦朧朧,似懂非懂。

官方的資料:https://solidity-cn.readthedocs.io/zh/develop/index.html 但看完沒多大用

其他的鏈接:

https://www.qikegu.com/docs/4922 簡單易懂

https://www.tryblockchain.org/ 專業的文檔2,值得看

http://www.mamicode.com/info-detail-2286137.html 用於溫習回顧,走馬觀花列一遍語法

https://www.iqiyi.com/v_19rqt6letg.html 千峰視頻

 

 1,狀態變量storage和局部變量memory

兩者區別很容易理解,memory可以理解為臨時變量,不會記錄在鏈上,而storage是永久存儲的。

一個版本的解讀:

Storage
該存儲位置存儲永久數據,這意味着該數據可以被合約中的所有函數訪問。可以把它視為計算機的硬盤數據,所有數據都永久存儲。

保存在存儲區(Storage)中的變量,以智能合約的狀態存儲,並且在函數調用之間保持持久性。與其他數據位置相比,存儲區數據位置的成本較高。

Memory
內存位置是臨時數據,比存儲位置便宜。它只能在函數中訪問。

通常,內存數據用於保存臨時變量,以便在函數執行期間進行計算。一旦函數執行完畢,它的內容就會被丟棄。你可以把它想象成每個單獨函數的內存(RAM)。

Calldata
Calldata是不可修改的非持久性數據位置,所有傳遞給函數的值,都存儲在這里。此外,Calldata是外部函數的參數(而不是返回參數)的默認位置。

Stack
堆棧是由EVM (Ethereum虛擬機)維護的非持久性數據。EVM使用堆棧數據位置在執行期間加載變量。堆棧位置最多有1024個級別的限制。

可以看到,要永久性存儲,可以保存在存儲區(Storage)。

 

  • 變量定義時默認為storage,而作為函數參數時,默認為memory
contract HelloWorld{ //等價於 string storage public a; string public a; //參數等價於string memory _a function changeNum(string _a){ } } 
  • 當函數參數為memory類型時,相當於值傳遞,storage才是指針傳遞
contract HelloWorld2{ string public a; function HelloWorld2(){ a = "abc"; } function f(){ changeNum(a); } function changeNum(string _a){ bytes(_a)[0] = "d"; //由於_a默認為memory,所以_a只是值傳遞,所以此時修改a的值是不成功的,輸出還是abc //需要把函數參數修改為string storage _a,才能輸出dbc } } 
  • 將變量賦值給一個新變量時,新變量的類型由賦值給它的類型決定。
function changeNum(string _a){ //_a默認為memory類型,所以b也為memory string b = _a; bytes(_a)[0] = "d"; }
規則1:狀態變量,狀態變量總是存儲在存儲區中,不能顯式地標記狀態變量的位置。
規則2:函數參數與返回值,都存儲在內存中。
規則3:值類型的局部變量存儲在內存中,但引用類型的局部變量需要顯式指定數據位置(memory,還是storage)
規則4:外部函數的參數(不包含返回參數)存儲在Calldata中。
 
賦值的數據位置規則
規則1:將一個狀態(存儲)變量賦值給另一個狀態(存儲)變量,將創建一個新的副本。引用類型變量也是。
規則2:從內存變量復制到存儲變量,總是會創建一個新的副本。
規則3:從存儲變量復制到內存變量,將創建一個副本。
規則4:對於引用類型的局部變量,從一個內存變量復制到另一個內存變量不會創建副本。對於值類型的局部變量仍然創建一個新副本。
 
 


數組:

對於存儲(storage)數組,元素類型可以是任意的(可以是其他數組、映射或結構)。對於內存(memory)數組,元素類型不能是映射類型,如果它是一個公共函數的參數,那么元素類型必須是ABI類型。

類型為bytes和字符串的變量是特殊數組。bytes類似於byte[],但它在calldata中被緊密地打包。字符串等價於bytes,但(目前)不允許長度或索引訪問。

因此,相比於byte[],bytes應該優先使用,因為更便宜。

創建內存數組
可以使用new關鍵字在內存中創建動態數組。與存儲數組相反,不能通過設置.length成員來調整內存動態數組的長度。

length
數組有一個length成員來表示元素數量。動態數組可以通過更改.length成員,在存儲器(而不是內存)中調整大小。創建后,內存數組的大小是固定的(但是是動態的,長度可以是函數參數)。

 

類型轉換:

Solidity允許類型之間進行隱式轉換和顯式轉換。

隱式轉換時必須符合一定條件,不能導致信息丟失。例如,uint8可以轉換為uint16,但是int8不可以轉換為uint256,因為int8可以包含uint256中不允許的負值。

規則:https://www.qikegu.com/docs/4948

 

 

函數

(1)函數修飾符:

比如onlyOwner

 

// 定義修飾符 onlyOwner 不帶參數
modifier onlyOwner(){ //如果調用合約的人不是合約創建者則throw if(msg.sender != sender) throw; _; //占位符 } //這樣a函數就只能被合約的創建者調用了 function a() onlyOwner{ ... }

修飾符定義中出現特殊符號_的地方,用於插入函數體。如果在調用此函數時,滿足了修飾符的條件,則執行該函數,否則將拋出異常。 

// 定義修飾符 costs 帶參數 modifier costs(uint price) { if (msg.value >= price) { _; } }
contract Register is Owner { mapping (address => bool) registeredAddresses; uint price; constructor(uint initialPrice) public { price = initialPrice; } // 使用修飾符 costs function register() public payable costs(price) { registeredAddresses[msg.sender] = true; } // 使用修飾符 onlyOwner function changePrice(uint _price) public onlyOwner { price = _price; } }

 

(2)pure、view、constant三種函數定義

當函數有返回值時,可以添加這三種定義,用這三種方式定義的函數都只執行讀操作,不會進行編譯執行。即用了這三種方式定義的函數,不會執行函數里的邏輯,只會執行一個返回的讀操作。所以執行這些函數不需要消耗gas費用。

pure區別是用於返回非變量,如returns 10;
而view和constant用於返回全局變量,兩者的區別為新舊版本

uint public a = 1; //由於被constant聲明的函數執行讀操作,所以a無法被修改 //執行為f(),a依然為1 function f() constant { a = 3; }

 

View(視圖)函數不會修改狀態。如果函數中存在以下語句,則被視為修改狀態,編譯器將拋出警告。

  • 修改狀態變量。
  • 觸發事件。
  • 創建合約。
  • 使用selfdestruct
  • 發送以太。
  • 調用任何不是視圖函數或純函數的函數
  • 使用底層調用
  • 使用包含某些操作碼的內聯程序集。

Getter方法是默認的視圖函數。聲明視圖函數,可以在函數聲明里,添加view關鍵字。

 

 

Pure(純)函數不讀取或修改狀態。如果函數中存在以下語句,則被視為讀取狀態,編譯器將拋出警告。

  • 讀取狀態變量。
  • 訪問 address(this).balance 或 <address>.balance
  • 訪問任何區塊、交易、msg等特殊變量(msg.sig 與 msg.data 允許讀取)。
  • 調用任何不是純函數的函數。
  • 使用包含特定操作碼的內聯程序集。

如果發生錯誤,純函數可以使用revert()require()函數來還原潛在的狀態更改。

聲明純函數,可以在函數聲明里,添加pure關鍵字。

pragma solidity ^0.5.0; contract Test { function getResult() public pure returns(uint product, uint sum){ uint a = 1; uint b = 2; product = a * b; sum = a + b; } }

fallback(回退) 函數是合約中的特殊函數。它有以下特點

  • 當合約中不存在的函數被調用時,將調用fallback函數。
  • 被標記為外部函數。
  • 它沒有名字。
  • 它沒有參數。
  • 它不能返回任何東西。
  • 每個合約定義一個fallback函數。
  • 如果沒有被標記為payable,則當合約收到無數據的以太幣轉賬時,將拋出異常。
// 沒有名字,沒有參數,不返回,標記為external,可以標記為payable function() external { // statements }
pragma solidity ^0.5.0; contract Test { uint public x ; function() external { x = 1; } } contract Sink { function() external payable { } } contract Caller { function callTest(Test test) public returns (bool) { (bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()")); require(success); // test.x 是 1 address payable testPayable = address(uint160(address(test))); // 發送以太測試合同, // 轉賬將失敗,也就是說,這里返回false。 return (testPayable.send(2 ether)); } function callSink(Sink sink) public returns (bool) { address payable sinkPayable = address(sink); return (sinkPayable.send(2 ether)); } }

 

智能合約 Contract

Solidity中,合約類似於c++中的類。合約包含以下部分:

  • 構造函數 – 使用constructor關鍵字聲明的特殊函數,每個合約執行一次,在創建合約時調用。
  • 狀態變量 – 用於存儲合約狀態的變量
  • 函數 – 智能合約中的函數,可以修改狀態變量來改變合約的狀態。

(1)可見性:有public、private、internal和external四種訪問權限

  • 1.函數默認聲明為public,即可以以internal方式調用,也可以通過external方式調用。可以理解為能夠被內部合約訪問和外部合約訪問。
  • 2.Internal聲明的只允許通過internal方式調用,不能被外部合約。而external能夠被外部合約訪問。
  • 3.private和internal類似,都不能被外部合約訪問,唯一的不同是private函數不能被子類調用,而internal可以。
 (2)構造函數

構造函數是使用construct關鍵字聲明的特殊函數,用於初始化合約的狀態變量。合約中構造函數是可選的,可以省略。

構造函數有以下重要特性:

  • 一個合約只能有一個構造函數。
  • 構造函數在創建合約時執行一次,用於初始化合約狀態。
  • 在執行構造函數之后,合約最終代碼被部署到區塊鏈。合約最終代碼包括公共函數和可通過公共函數訪問的代碼。構造函數代碼或僅由構造函數使用的任何內部方法不包括在最終代碼中。
  • 構造函數可以是公共的,也可以是內部的。
  • 內部構造函數將合約標記為抽象合約。
  • 如果沒有定義構造函數,則使用默認構造函數。
  • 如果基合約具有帶參數的構造函數,則每個派生/繼承的合約也都必須包含參數。
  • 可以使用下面的方法直接初始化基構造函數
pragma solidity ^0.5.0; contract Base { uint data; constructor(uint _data) public { data = _data; } } contract Derived is Base (5) { constructor() public {} }
  • 可以使用以下方法間接初始化基構造函數
pragma solidity ^0.5.0; contract Base { uint data; constructor(uint _data) public { data = _data; } } contract Derived is Base { constructor(uint _info) Base(_info * _info) public {} }
  • 不允許直接或間接地初始化基合約構造函數。
  • 如果派生合約沒有將參數傳遞給基合約構造函數,則派生合約將成為抽象合約。

 

 (3)合約繼承

就像Java、C++中,類的繼承一樣,Solidity中,合約繼承是擴展合約功能的一種方式。Solidity支持單繼承和多繼承。Solidity中,合約繼承的重要特點:

  • 派生合約可以訪問父合約的所有非私有成員,包括內部方法和狀態變量。但是不允許使用this
  • 如果函數簽名保持不變,則允許函數重寫。如果輸出參數不同,編譯將失敗。
  • 可以使用super關鍵字或父合同名稱調用父合同的函數。
  • 在多重繼承的情況下,使用super的父合約函數調用,優先選擇被最多繼承的合約。

 (4)抽象合約

類似java中的抽象類,抽象合約至少包含一個沒有實現的函數(抽象函數)。通常,抽象合約作為父合約,被用來繼承,在繼承合約中實現抽象函數,抽象合約也可以包含有實現的函數。

如果派生合約沒有實現抽象函數,則該派生合約也將被標記為抽象合約。

pragma solidity ^0.5.0; contract Calculator { function getResult() public view returns(uint); } contract Test is Calculator { function getResult() public view returns(uint) { uint a = 1; uint b = 2; uint result = a + b; return result; } }

 (5)接口

接口類似於抽象合約,使用interface關鍵字創建,接口只能包含抽象函數,不能包含函數實現。以下是接口的關鍵特性:

  • 接口的函數只能是外部類型。
  • 接口不能有構造函數。
  • 接口不能有狀態變量。
  • 接口可以包含enum、struct定義,可以使用interface_name.訪問它們。

 (6)庫

庫類似於合約,但主要作用是代碼重用。庫中包含了可以被合約調用的函數。

Solidity中,對庫的使用有一定的限制。以下是庫的主要特征。

  • 如果庫函數不修改狀態,則可以直接調用它們。這意味着純函數視圖函數只能從庫外部調用。
  • 庫不能被銷毀,因為它被認為是無狀態的。
  • 庫不能有狀態變量。
  • 庫不能繼承任何其他元素。
  • 庫不能被繼承。
pragma solidity ^0.5.0; 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 Test { uint[] data; constructor() public { data.push(1); data.push(2); data.push(3); data.push(4); data.push(5); } function isValuePresent() external view returns(uint){ uint value = 4; // 使用庫函數搜索數組中是否存在值 uint index = Search.indexOf(data, value); return index; } }

 (7)事件

事件是智能合約發出的信號。智能合約的前端UI,例如,DApps、web.js,或者任何與Ethereum JSON-RPC API連接的東西,都可以偵聽這些事件。事件可以被索引,以便以后可以搜索事件記錄。

事件在區塊鏈中的存儲

區塊鏈是一個區塊鏈表,這些塊的內容基本上是交易記錄。每個交易都有一個附加的交易日志,事件結果存放在交易日志里。合約發出的事件,可以使用合約地址訪問。

// 聲明一個事件 event Deposit(address indexed _from, bytes32 indexed _id, uint _value); // 觸發事件 emit Deposit(msg.sender, _id, msg.value);

示例:

pragma solidity ^0.5.0; contract Counter { uint256 public count = 0; event Increment(address who); // 聲明事件 function increment() public { emit Increment(msg.sender); // 觸發事件 count += 1; } }

上面的代碼中,

  • event Increment(address who) 聲明一個合約級事件,該事件接受一個address類型的參數,該參數是執行increment操作的賬戶地址。
  • emit Increment(msg.sender) 觸發事件,事件會記入區塊鏈中。

按照慣例,事件名稱以大寫字母開頭,以區別於函數。

 

用JavaScript監聽事件

下面的JavaScript代碼偵聽Increment事件,並更新UI。

counter = web3.eth.contract(abi).at(address); counter.Increment(function (err, result) { if (err) { return error(err); } log("Count was incremented by address: " + result.args.who); getCount(); }); getCount();
 
  • contract.Increment(...) 開始偵聽遞增事件,並使用回調函數對其進行參數化。
  • getCount() 是一個獲取最新計數並更新UI的函數。

索引(indexed)參數

一個事件最多有3個參數可以標記為索引。可以使用索引參數有效地過濾事件。下面的代碼增強了前面的示例,來跟蹤多個計數器,每個計數器由一個數字ID標識:

pragma solidity ^0.4.21; contract Multicounter { mapping (uint256 => uint256) public counts; event Increment(uint256 indexed which, address who); function increment(uint256 which) public { emit Increment(which, msg.sender); counts[which] += 1; } }
  • counts替換countcounts是一個map。
  • event Increment(uint256 indexed which, address who) 添加一個索引參數,該參數表示哪個計數器。
  • emit Increment(which, msg.sender) 用2個參數記錄事件。

在Javascript中,可以使用索引訪問計數器:

... counter.Increment({ which: counterId }, function (err, result) { if (err) { return error(err); } log("Counter " + result.args.which + " was incremented by address: " + result.args.who); getCount(); }); ...

事件的局限

事件構建在Ethereum中,底層的日志接口之上。雖然您通常不會直接處理日志消息,但是了解它們的限制非常重要。

日志結構最多有4個“主題”和一個“數據”字段。第一個主題用於存儲事件簽名的哈希值,這樣就只剩下三個主題用於索引參數。主題需要32字節長,因此,如果使用數組作為索引參數(包括類型string和bytes),那么首先將哈希值轉換為32字節。非索引參數存儲在數據字段中,沒有大小限制。

日志,包括記錄在日志中的事件,不能從Ethereum虛擬機(EVM)中訪問。這意味着合約不能讀取自己的或其他合約的日志及事件。

總結

  • Solidity 提供了一種記錄交易期間事件的方法。
  • 智能合約前端(DApp)可以監聽這些事件。
  • 索引(indexed)參數為過濾事件提供了一種高效的方法。
  • 事件受其構建基礎日志機制的限制。

 (8)錯誤處理

Solidity 提供了很多錯誤檢查和錯誤處理的方法。通常,檢查是為了防止未經授權的代碼訪問,當發生錯誤時,狀態會恢復到初始狀態。

下面是錯誤處理中,使用的一些重要方法:

  • assert(bool condition) − 如果不滿足條件,此方法調用將導致一個無效的操作碼,對狀態所做的任何更改將被還原。這個方法是用來處理內部錯誤的。

     

  • require(bool condition) − 如果不滿足條件,此方法調用將恢復到原始狀態。此方法用於檢查輸入或外部組件的錯誤。

  • require(bool condition, string memory message) − 如果不滿足條件,此方法調用將恢復到原始狀態。此方法用於檢查輸入或外部組件的錯誤。它提供了一個提供自定義消息的選項。

  • revert() − 此方法將中止執行並將所做的更改還原為執行前狀態。

  • revert(string memory reason) − 此方法將中止執行並將所做的更改還原為執行前狀態。它提供了一個提供自定義消息的選項。

 

異常處理:

Solidity使用狀態恢復來處理異常,就是說當拋出異常時將恢復到調用(包括自調用)前的狀態。
拋出異常的方式有assert,require,revert,throw。

  • assert函數,用於條件檢查,只能測試內部錯誤和檢查常量。
function add(uint256 a, uint256 b) internal constant returns (uint256) { uint256 c = a + b; assert(c >= a); return c; } 
  • require函數,也是用於條件檢查,用於測試調用的輸入或者合約狀態變量。
function sendHalf(address addr) payable returns (uint balance) { require(msg.value % 2 == 0); // 只允許偶數 ..... }

 

solidity常用模式

1,提款模式

當在智能合約中,直接向一個地址轉賬時,如該地址是一個合約地址,合約中可以編寫代碼,拒絕接受付款,導致交易失敗。為避免這種情況,通常會使用提款模式。

提款模式是讓收款方主動來提取款項,而不是直接轉賬給收款方。

提款模式,讓收款方(前首富)主動來提取款項,交易不會失敗,游戲可以繼續。

2,限制訪問

 

 

 

 

 

編程風格:

結構體名稱
駝峰式命名,例如: SmartCoin
事件名稱
駝峰式命名,例如:AfterTransfer
函數名
駝峰式命名,首字母小寫,比如:initiateSupply
局部變量和狀態變量
駝峰式命名,首字母小寫,比如creatorAddress、supply
常量
大寫字母單詞用下划線分隔,例如:MAX_BLOCKS
修飾符的名字
駝峰式命名,首字母小寫,例如:onlyAfter
枚舉的名字
駝峰式命名,例如:TokenGroup

https://www.qikegu.com/docs/4955


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM