學習內容:智能合約的結構內容與solidity的數據類型的學習
Solidity 文件結構
合約版本聲明
pragma solidity ^0.4.0;
引入其他源文件
Solidity 支持 import 語句。
全局引入,引入形式如下:
import ”filename";
自定義命名空間號引入,引入形式如下:
import * as symbol Name from "filename";
創建一個全局的命名空間 symbolName,成員來自 filename 的全局符號。 有一種非 ES6 兼容的簡寫語法與其等價:
import ”filename" as symbolName;
分別定義引入,引入形式如下:
import {symboll as alias, symbol2} from "filename";
將創建一個新的全局變量別名 alias 和 symbol2,它們將分別從 filename 引 入 symboll 和 symbol2。
合約結構
Solidity 合約的定義和面向對象語言中的類很相似,每個合約都可以包含狀態變量、函數、函數修改器、事件、 結構類型和枚舉類型。另外,合約也支持繼承。
狀態變量(State Variable)
狀態變量和其他語言中類的成員變量很相似,狀態變量會被永久存儲在合 約的存儲空間里。
函數(Function)、函數修改器(Function Modifier)
事件(Event)
事件是以太坊虛擬機 (EVM)日志基礎設施提供的一個便利接口,用於獲 取當前發生的事件。
結構類型(Struct Type)、枚舉類型(Enum Type)
枚舉類型是用戶創建的包含幾個特定值的集合的自定義類型。
大概的智能合約結構圖如下:
Solidity數據類型
Solidity 是一種靜態類型語言, 這一章我們將深入介紹 Solidity 的數據類型。
靜態類型意味着在編譯時需要為每個變量(本地或狀態變量)都指定類型(或至少可以推導出類型)。Solidity 的類型非常在意所占空間的大小
引用類型: 數組(Array)、結構體(Struct)和映射(Mapping)。
布爾類型(Boolean)(省略)
整形(Integer)
當使用移位時,不能進行負移位,即運算符右邊的數不可以為負數,否則會拋出運行 時異常,如 3 >>-1 為非法的。
整型溢出問題
在使用整型時, 要特別注意整型的大小及所能容納的最大值和最小值,很 多合約就是因為溢出問題導致了漏洞。
避免溢出的一個方法是在運算之后對結果值進行一次檢查, 比如對上面的 k 做一個檢查,如使用 assert(k >= i)。
定長浮點型(Fixed Point Number)
定長浮點型的聲明方式如下:
fixed/ufixed 表示有符號和無符號的固定位浮點數。關鍵宇為 ufixedMxN 和 ufixedMxN
注意:它和大多數語言的 float 和 double 不一樣,這里的 M 表示整個數占 用的固定位數,包含整數部分和小數部分。
定長字節數組(Fixed-size Byte Array)
移位運算和整型類似,移位運算結果的正負取決於運算符左邊的數,且不 能進行負移位。 例如可以-5<<1 ,不可以 5<<-1 。定長字節數組有成員變量:.length,表示這個字節數組的長度(不能修改)。
有理數和整型常量(Rational and Integer Literal)
整型常量是由一系列 0~9 的數字組成的,以十進制數表示。 比如: 八進制 數是不存在的 ,前置 0 在 Solidity 中是無效的。
十進制小數常量 (Decimal Fraction Literal )帶了一個“.,在“.”的兩邊 少有一個數字,有效的表示如 1.、.1和 1.3 等。 只要操作數是整型,整型支持的運算符就適用於整型常量表達式。 如果兩 個操作數都是小數,則不允許進行位運算,指數也不能是小數。
注意:“Solidity 的每一個數字常量都有對應的數字常量類型, 整型常量和有理數常量屬於數字常量類型。 所有的數字常量表達式的結果都是數字常量。 在數字常量表達式中一旦含有非常量表達式,它就會被轉換為非常量類型。
不同類型之間沒法進行運算,因此下面的代碼會編譯出錯。
上述代碼編譯不能通過,因為 b 會被編譯器認為是小數。
字符串常量(String Literal)
字符串常量支持轉義字符, 比如\n、\xNN、 \uNNNN。其中\xNN 表示十六 進制值,最終會轉換為合適的字節數組; 而\uNNNN 表示 Unicode 編碼值, 最 終會轉換為 UTF8 的序列。 注意: 字符串常量不支持任何運算符,比如在其他語言中可以通過"+"來 拼接兩個字符串常量,但是在 Solidity 中是不可以的。
十六進制常量(Hexadecimal Literal)
十六進制常量以關鍵宇 hex 開頭,后面緊跟用單引號或雙引號包裹的字符 串 ,內容是十六進制字符串。
枚舉(Enum)
在 Solidity 中枚舉可以用來自定義類型。 它可以與整數進行顯式轉換,但不能進行隱式轉換。 顯式轉換會在運行時檢查數值范圍 ,如果不匹配將會引發異常。 枚舉類型應至少有一個成員 。 下面是一個枚舉的例子。
函數類型(Function Type)
Solidity 中的函數也可以是一種類型且屬於值類型。
函數類型有兩類:內部( internal) 函數和外部(external)函數。
內部函數只能在當前合約內被調用(在當前代碼塊內,包括內部庫函數和繼承的函數),它不會創建一個 EVM 消息調用,訪問方式是直接使用函數名f()。
外部函數通過 EVM 消息調用,它由地址和函數方法簽名兩部分組成,訪 問方式是 this.f()。 外部函數可作為外部函數調用的參數或返回值。
函數類型默認是 internal, 因此 internal 可以省去。但與此相反,合約中函數本身默認是 public 的,僅僅是當作類型名使用時默認才是 internal。如果應該 使用 internal,卻使用了 external,則很容易引發安全問題,同時也增加了 gas 的消耗。 如果應該 使用 internal,卻使用了 external,則很容易引發安全問題,同時也增加了 gas 的消耗。
注意一下,聲明一個函數和聲明一個函數類型的變量是不一樣的, 后面會繼續介紹。
有兩種方式訪問函數, 一種是直接用函數名 f,另一種是用 this.f。 前者用於內部函數, 后者用於外部函數。 合約中的 public 的函數, 可以使用 internal 和 external 兩種方式來調用 。 internal 訪問形式為f, external 訪問形式為
this.f。
selector 成員屬性
公有或外部(public /external)函數類型有一個特殊的成員屬性 selector,它 對應一個 ABI 函數選擇器,后續會繼續講解。(這里只要知道它是一個函數簽名即可。)
下面的代碼顯示內部( internal )函數類型的使用:
pragma solidity ^0.4.16;
library ArrayUtils {
// 內部函數可以在內部庫函數中使用,
// 因為它們會成為同一代碼上下文的一部分
function map(uint[] memory self, function (uint) returns (uint) f)internal returns (uint[] memory r){
// r=[0,1,4,9]
r = new uint[](self.length);
for (uint i = 0; i < self.length; i++) {
r[i] = f(self[i]);
}
}
// self 因為函數調用是r.函數,所以self可以理解為第一個參數調用
function reduce(uint[] memory self,function (uint, uint) returns (uint) f)internal returns (uint r){
r = self[0];
for (uint i = 1; i < self.length; i++) {
r = f(r, self[i]);
}
}
function range(uint length) internal returns (uint[] memory r) {
r = new uint[](length);
for (uint i = 0; i < r.length; i++) {
r[i] = i;
}
}
}
contract Pyramid {
using ArrayUtils for *;
function pyramid(uint l) public view returns (uint) {
return ArrayUtils.range(l).map(square).reduce(sum);
}
function square(uint x) internal returns (uint) {
return x * x;
}
function sum(uint x, uint y) internal returns (uint) {
return x + y;
}
}
下面的代碼顯示外部(external)函數類型的使用:
pragma solidity ^0.4.11;
contract Oracle {
struct Request {
bytes data;
function(bytes memory) external callback;
}
Request[] requests;
event NewRequest(uint);
function query(bytes data,function(bytes memory) external callback) public {
requests.push(Request(data,callback));
NewRequest(requests.length - 1);
}
function reply(uint requestID,bytes response) public {
// 這里檢查回復是否來自可信來源
requests[requestID].callback(response);
}
}
contract OracleUser {
Oracle constant oracle = Oracle(0x425148e9e030d8ae44da54458cd577218e1bea39); // 已知合同
function buySomething(){
oracle.query("USD",this.oracleResponse);
}
function oracleResponse(bytes response) public {
require(msg.sender == address(oracle));
// 使用的數據
}
}
public 還是 external? public 和 external 看上去有很多相似的地方,那么到底該用哪一個呢? 我們以下面一段代碼為例,來看看 public 與 external 的不同:
我們看下面這個例子:
pragma solidity ^0.4.18;
contract Test {
uint[10] x = [1,2,3,4,5,6,7,8,9,10];
function test(uint[10] a) public returns (uint) {
return a[8] * 2;
}
function test2(uint[10] a) external returns (uint) {
return a[8] * 2;
}
function calltest() {
test(x);
}
function calltest2() {
this.test2(x);
}
}
可以看到調用 public 函數花銷更大。當使用 public 函數時, Solidity 會立即復制數組參數到內存, 而 external 函 數則是從 calldata 讀取的, 分配內存的開銷比直接從 calldata 讀取的開銷要大得多。那為什么 public 函數要復制數組參數到內存呢? 因為 public 函數可能會被內部調用 ,而內部調用數組的參數是當成指向一塊內存的指針。 對於 external 函數不允許內部調用 , 它直接從 callda閱 讀取數據, 省去了復制的過程。
同樣, 我們接着對比 calltest()及 calltest2(),會有一個花費很大開銷的 CALL 調用,並且它傳參的方式也 比內部傳遞的開銷更大。 【這是因為通過 this.f()模式調用 】
地址類型(Address)
地址類型是一個值類型。 地址占用 20 字節, 即 以太坊地址的長度是 20 個字節。
目前,地址是所有合約的基礎 (基類),即合約也可以是一個類型並且繼承自地址類型。 不過官方文檔說,從 Solidity 0.5.0 版本開始,合約將不再繼承自地址類型,但會保留顯式轉換為地址。
地址類型的成員
balance 屬性及transfer()函數
balance 用來查詢賬戶余額, transfer()用來發送以太幣。addr.balance 用來查詢賬戶 addr 的余額, addr.transfer() 用來向地址 addr 發送以太幣(注意,很多人誤以為 addr 是發送方,但實際上 addr 是接收方)。
注解: 如果 x 是合約地址,那么合約的回退函數(fallback)會隨 transfer 調用一起執行(這個是 EVM 特性)。如果因 gas 耗光或其他原因失敗,則轉移交易會被還原, 並且合約會出現異常並停止。
sender()函數
send 與 transfer 對應,但在底層上。如果執行失敗, transfer 不會因異常停止,而 send會返回 false。實際上 addr.transfer(y)與 require(addr. send(y))是等價的。
如果交易失敗,則會退回以太幣 。
警告: send()執行有一些風險。如果調用棧的深度超過 1024或gas耗光,交易都會失敗。
call()、 delegatecall()、 callcode()函數這里不贅述。因為以后可能會被移除,且安全性低,容易鎖攻擊,想了解請自行百度。
如何區分合約地址及外部賬號地址
EVM 提供了一個操作碼 EXTCODESIZE,用來獲取地址相關聯的代碼大小(長度),如果是外部賬號地址, 則沒有代碼返回 。
如果是在合約外部判斷,則可以使用 web3.eth.getCode() , 或者是對應的 JSON-RPC 方法 eth_getcodea
getCode()用來獲取參數地址所對應合約的代碼,如果參數是一個外部賬號 地址,則返回”0x”;如果參數是合約,則返回對應的字節碼, 如下所示:
這樣我們就可以通過 getCode()的內容判斷是哪一種地址了。
數據位置(Data Location)
所有的復雜類型如數組和結構體都有一個額外屬性:數據的存 儲位置,即 memory或 storage。
局部變量:局部作用域(越過作用域即不可被訪問,等待被回收)的變量,如函數內的變量。 狀態變量:合約內聲明的公有變量。
還有一個存儲位置是 calldata,用來存儲函數參數,是只讀的、不會永久存儲的數據位置。 外部函數的參數(不包括返回參數)被強制指定為 calldatao 效 果與 memory 差不多。 將一個 storage 的狀態變量,賦值給一個 storage 的局部變量,則是通過引用傳遞完成的。所以對於局部變量的修改,要同時修改關聯的狀態變量。 另一方面,將 一個 memory 的引用類型賦值給另一個 memory 的引用,是不會創建拷貝的, 即 memory 之間是通過引用傳遞完成的。
- 不能將 memory賦值給局部變量。
- 對於值類型,總是會進行拷貝的。
強制指定的數據位置(Forced data location)
- 外部函數的參數(不包括返回參數)強制指定的數據位置為 calldata。
- 狀態變量強制指定的數據位置為 storage
默認的數據位置(Default data location)
- 函數參數及返回參數默認的數據位置為 memory。
- 復雜類型的局部變量默認的數據位置為 storage。
memory 只能用於函數內部, memory 聲明用來告知 EVM 在運行時創建一 塊(固定大小)內存區域給變量使用 。
storage 在區塊鏈中時用key/value的形式存儲的,而memory則表示為字節數組。
關於棧(stack)
EVM 是一個基於棧的語言,而棧實際上是內存( memory) 中的一個數據 結構。每個棧元素為 256 位,拔的最大長度為 1024。
不同存儲的 gas 消耗
- storage 會永久保存合約狀態變量, 開銷最大。
- memory 僅保存臨時變量,函數調用之后釋放,開銷很小。
- stack 保存很小的局部變量,幾乎免費使用 ,但有數量限制。
數組(Array)
數組可以在聲明時指定長度,也可以動態變長。一個元素類型為 T, 固定 長度為 k 的數組,可以聲明為 T[k];而一個動態變長的數組,可以聲明為 T[]。
或者用 new 關鍵字進行聲明,形式如下:
注意,在其他語言中,多維數組的長度聲明是反的。比如用java聲明一個包含5 個元素、 每個元素都是數組的方式為 int[5][]。
對存儲在 storage 的數組來說, 元素類型可以是任意的,類型可以是數組、 映射類型、 結構體等。但對於存儲在 memory 的數組來說,如果它是 public 函數的參數,則不能是映射類型的數組,只能是支持 ABI 的數組類型
創建內存數組
可以使用 new 關鍵宇創建一個存儲在 memory 上的數組。與存儲在 storage 上的數組不同的是, 該數組不能通過成員.length 的值來修改數組的大小屬性。 memory不是new的數組也是不能修改數組的大小屬性。
pragma solidity ^0.4.16;
contract C {
function f(uint len) public pure {
uint []memory a;
bytes memory b = new bytes(len);
a[6] = 8;
}
}
數組常量及內聯數組
數組常量,是一個數組表達式(還沒有賦值到變量)。
還需注意的一點是,定長數組不能與變長數組相互賦值,我們來看下面的 錯誤代碼示例 :
pragma solidity ^0.4.4;
contract C {
function f() public {
uint8[] x = [uint8(1),uint8(3),uint8(4)];
}
}
不過, Solidity 己經計划在未來移除這樣的限制。 當前是因為 ABI 傳遞數組, 所以還有些問題。
數組成員
length 屬性
數組有一個length 的成員屬性,表示當前的數組長度。 對於存儲在 storage 的變長數組,可以通過給.length 賦值調整數組長度。而存儲在 memory 的變長數組不支持修改.length 調整數組長度。
push 方法
存儲在 storage 的變長數組和 bytes 都有一個 push 成員方法(string 沒有), 用於附加新元素到數據末端,返回值為新的長度。
注意, string 沒有 push 方法,存儲在 memory 的數組也不支持 pusha
pragma solidity ^0.4.16;
contract ArrayContract {
uint[2**20] m_aLotOfIntegers;
bool[2][] m_pairOfFlags;
function selFlagPair(uint index,bool flagA,bool flagB) public {
// 訪問不存在的 index 會拋出異常
m_pairOfFlags[index][0] = flagA;
m_pairOfFlags[index][1] = flagB;
}
// 改變storage的數組大小
function changeFlagArraySize(uint newSize) public {
m_pairOfFlags.length = newSize;
}
function clear() public {
delete m_pairOfFlags;
delete m_aLotOfIntegers;
// 同樣是銷毀效果 銷毀只是把值清除
// m_pairOfFlags.length = 0;
}
bytes m_byteData;
function byteArrays(bytes data) public returns (byte){
m_byteData = data;
// 改數組長度
m_byteData.length += 7;
m_byteData[3] = byte(8);
delete m_byteData[2];
return m_byteData[2];
}
function addFlag(bool[2] flag) public returns (uint) {
return m_pairOfFlags.push(flag); // storage
}
function createMemoryArray(uint size) public pure returns (bytes) {
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;
}
}
字符串 string 及字節數組 bytes
bytes 是動態分配大小字節數組, bytes 類似於 byte[],但在外部函數作為參數調用中, bytes 會進行壓縮打包。 string 類似於 bytes, 但 目前不提供長度和按 序號的訪問方式。 所以應該盡量使用 bytes 而不是 byte[]。
可以將字符串 s 通過 bytes(s)轉為一個bytes, 通過bytes(s).length 獲取長度, bytes(s)[n]獲取對應的 UTF-8 編碼。 通過下標訪問獲取到的不是對應字符,而是 UTF-8 編碼,比如中文的編碼是變長的多字節,因此通過下標訪問中文字符串 得到的只是其中的一個編碼。
string 擴展
Solidity語言本身提供的 string 功能比較弱, 因此有人實現了 string 的實用 工具庫 stringutils, GitHub 地址為 https://github .com/ Arachnid/solidity-stringutils, 並且在這個庫中引入了一個 slice 的概念,下面列舉了幾個使用示例。 【具體去官方查看】
定長字節數組還是字符串
有時定長字節數組會用來代替字符串使用,這是為什么呢?我們先來對比 以下不同函數的 gas 消耗:
pragma solidity ^0.4.16;
contract compGas {
string constant ss = "Tiny Xiong";
bytes32 constant bt32 = "Tiny Xiong";
function getBytes32() payable public returns (bytes32) {
return bt32;
}
function getString() payable public returns (string) {
return ss;
}
}
我在本地測試時, getByte32()消耗了 21490 gas, getString()消耗了 21853 gas。 相比變長的 string, 定長字節數組 gas 消耗更少。 因此,如果宇符串的長度是固 定的(或長度可以確定),盡量使用 bytes32 (官方文檔里介紹的是盡量使用定 長的如 bytesl 到 bytes32 中的一個,因為更省空間,我的個人經驗是使用 bytes32 消耗的 gas 最少)。
結構體(Struct)
聲明與初始化
1.僅聲明變量而不初始化,此時會使用默認值創建結構體變量,例如:CustomType ctl;
2. 按成員順序(結構體聲明時的順序) 初始化,例如:
CustomType ctl = CustomType(true, 2); //只能作為狀態變量這樣使用
CustomType memory ct2 = CustomType(true, 2); //在函數內聲明
這種方式需要特別注意參數的類型及數量的匹配。另外,如果結構體中有 mapping,則需要跳過對 mapping 的初始化。 例如對上面 CustomType3 的初始化方法為:
CustomType3 memory ct = CustomType3(”tiny", 2);
3.3 . 命名方式初始化。
使用命名方式可以不按定義的順序初始化,初始化方法如下:
//使用命名變量初始化
CustomType memory ct = CustomType({ myBool: true, myint: 2});
參數的個數需要保持和定義時一致, 如果有 mapping 類型, 則同樣需要忽略。
下面是結構體的例子。
pragma solidity ^0.4.16;
contract CrowdFunding {
// 出資人
struct Funder {
address addr;
uint amount;
}
//
struct Campaign {
address beneficiary;// 受益人
uint fundingGoal; // 出資目標
uint numFunders; // 出資人數
uint amount; // 出資總金額
mapping (uint => Funder) funders;
}
uint numCampaigns;// 訂單數
mapping (uint => Campaign) campaigns;
// 新建出資訂單
function newCampaign(address beneficiary,uint goal) public returns (uint campaignID){
campaignID = numCampaigns++;
// 創建一個結構體實例,存儲在storage上,放入mapping里
campaigns[campaignID] = Campaign(beneficiary,goal,0,0);
}
// 貢獻
function contribute(uint campaignID) public payable {
Campaign storage c = campaigns[campaignID];
c.funders[c.numFunders++] = Funder({addr:msg.sender,amount:msg.value});
c.amount += msg.value;
}
//
function checkGoalReached(uint campaignID) public returns (bool reached) {
Campaign storage c = campaigns[campaignID];
if (c.amount < c.fundingGoal)
return false;
uint amount = c.amount;
c.amount = 0;
c.beneficiary.transfer(amount);
return true;
}
}
注意在函數中,將一個 struct 賦值給一個局部變量(默認是 storage 類型),實際是拷貝的引用,所以修改局部變量值的同時會影響到原變量。
結構體也可以直接通過訪問成員來修改成員的值
結構體限制
結構體目前僅支持在合約內部使用或繼承合約內使用,如果要在參數和返 回值中使用結構體,函數必須聲明 internal
//合法 function interFunc(CustomType ct) internal {
}
//非法
手unction exterFunc(CustomType ct) public {
}
映射(Mapping)
限制
映射類型僅能用來作為狀態變量,或者在內部函數中作為 storage 類型的引用。
映射並未提供選代輸出的方法,即不能通過遍歷訪問所有元素,也無法獲得所有鍵或值的列表。 如果需要使用,我們可以自行實現一個這樣的數據結構。 參考以下鏈接, http://github.com/ethereum/dapp-bin/bIob/master/library/Iiterab Ie_mapping.sol。
類型轉換
隱式轉換與顯式轉換[沒啥好說]
var 類型推導
函數的參數包括返回參數,不可以使用 var 這種不指定類型的方式。
有一個地方需要注意,由於類型推導是根據第一個變量進行的賦值,所以 代碼 for (var i = 0; i < 2000; i++) {}將是無限循環的,因為一個 uint8 的 i 將小於2000。
delete 操作符
在 Solidity 中, delete 操作符的功能盡管也可以釋放空間,但 delete 操作符更像是將某個變量重置為初始值,例如 delete a 對於整數 a, 效果等同於 a = 0。
如果 delete 作用到數組上,則是把數組中的每個元素設置為初始值。變長數組則是將長度設置為 0。 對於結構體也是一樣的,是將所有的成員均重置為初始值。
delete 對於映射類型幾乎無影響,如果你刪除一個結構體,它會遞歸刪除所有非 mapping 的成員。 當然,你可以單獨刪除映射里的某個鍵, 以及這個鍵映射的某個值。
mapping(address= >uint) balances;
function deleteMap() public {
delete balances;
delete balances[msg.sende];
}
如果刪除一個結構體,那么它會遞歸刪除所有非 mapping 的成員。
pragma solidity ^0.4.0;
contract DeleteExample {
uint data;
uint[] dataArray;
function f() public {
// 值傳遞
uint x = data;
delete x; // 刪除x不會影響data
delete data; // 刪除data同樣不會影響,因為是值傳遞,保存的是值拷貝
// 引用賦值
uint[] storage y = dataArray;
//刪除dataArray會影響y,y將被賦值為初值
detele dataArray;
// 報錯,因為刪除的是一個賦值操作,所以不能向引用類型的storage直接賦值
// delete y;
}
通過上面的代碼,我們可以看出對於值類型是值傳遞的,刪除 x 不會影響 到 data。 同樣,刪除 data 也不會影響到 x。 因為它們都存了一份原值的拷貝 。
由於 delete 的行為更像是賦值操作,所以不能在上述代碼中執行 delete y。 我們不能對一個 storage 的引用賦值。
另外,關於 delete 的 gas 消耗有一個看起來矛盾的地方,即在清理空間的 時候是可以獲得 gas 的返還的。 但因為 delete 操作本身消耗 gas,所以在實際使用時最好進行 gas 消耗的對比。