Solidity文檔(中文版)連載五:類型

序言
本文是 Solidity 文檔(以太坊官方 Solidity 開發(fā)手冊)中文版連載的第五部分。這個連載的前四部分是 智能合約概述、安裝 Solidity 編譯器、結(jié)合實例學習 Solidity源文件結(jié)構(gòu)
這份文檔的英文原文可以在以太坊官網(wǎng)的最下方 Solidity 鏈接中找到。官方的英文版本文檔中有中譯版鏈接,即是本連載內(nèi)容的出處。這個連載將按照英文文檔的先后順序進行。
Solidity 是以太坊官方的智能合約開發(fā)高級語言。這份中文譯本是由 Hiblock 社區(qū)組織貢獻的,官方 Github:https://github.com/etherchina/solidity-doc-cn
我本人于 3 月初加入本項目,目前作為管理員、貢獻者和校訂人利用業(yè)余時間參與日常工作;截至到 5 月底,翻譯工作已接近完成。有興趣的朋友請直接在以太坊官網(wǎng)的鏈接中查看最新中文版本狀態(tài),或者關注上述中文譯本的 Github repository。
出于單獨閱讀的需要,我在連載中會刪除原文里的 rst 控制標簽、外部鏈接以及文內(nèi)鏈接。
按照原文的順序,第五部分應該是合約結(jié)構(gòu),但原文中的合約結(jié)構(gòu)這章非常簡單,僅僅對狀態(tài)變量(state variable)、函數(shù)(function)、函數(shù)修飾器(function modifier)、事件(event)、結(jié)構(gòu)類型(struct type)以及枚舉類型(enum type)做了一句話介紹,故這次連載沒有單獨將其列為一章。在后續(xù)連載中大家也能看到它們各自的詳細介紹。
本文是對 Solidity 中的基本類型的完整介紹,內(nèi)容很多、代碼示例也很多,需要花費一定的時間來閱讀學習。

類型

Solidity 是一種靜態(tài)類型語言,這意味著每個變量(狀態(tài)變量和局部變量)都需要在編譯時指定變量的類型(或至少可以推導出變量類型——參考下文的 類型判斷)。
Solidity 提供了幾種基本類型,可以用來組合出復雜類型。

除此之外,類型之間可以在包含運算符號的表達式中進行交互。

值類型

以下類型也稱為值類型,因為這些類型的變量將始終按值來傳遞。
也就是說,當這些變量被用作函數(shù)參數(shù)或者用在賦值語句中時,總會進行值拷貝。

布爾類型

bool :可能的取值為字面常數(shù)值 truefalse 。

運算符:

  • ! (邏輯非)
  • && (邏輯與, "and" )
  • || (邏輯或, "or" )
  • == (等于)
  • != (不等于)

運算符 ||&& 都遵循同樣的短路( short-circuiting )規(guī)則。就是說在表達式 f(x) || g(y) 中,
如果 f(x) 的值為 true ,那么 g(y) 就不會被執(zhí)行,即使會出現(xiàn)一些副作用。

整型

int / uint :分別表示有符號和無符號的不同位數(shù)的整型變量。
支持關鍵字 uint8uint256 (無符號,從 8 位到 256 位)以及 int8int256,以 8 位為步長遞增。
uintint 分別是 uint256int256 的別名。

運算符:

  • 比較運算符: <= , < , == , != , >= , > (返回布爾值)
  • 位運算符: & , |^ (異或), ~ (位取反)
  • 算數(shù)運算符: +- , 一元運算 - , 一元運算 + , */ , % (取余) , ** (冪), << (左移位) , >> (右移位)

除法總是會截斷的(僅被編譯為 EVM 中的 DIV 操作碼),
但如果操作數(shù)都是 字面常數(shù)(literals)(或者字面常數(shù)表達式),則不會截斷。

除以零或者模零運算都會引發(fā)運行時異常。

移位運算的結(jié)果取決于運算符左邊的類型。
表達式 x << yx * 2**y 是等價的,
x >> yx / 2**y 是等價的。這意味對一個負數(shù)進行移位會導致其符號消失。
按負數(shù)位移動會引發(fā)運行時異常。

警告:
由有符號整數(shù)類型負值右移所產(chǎn)生的結(jié)果跟其它語言中所產(chǎn)生的結(jié)果是不同的。
在 Solidity 中,右移和除是等價的,因此對一個負數(shù)進行右移操作會導致向 0 的取整(截斷)。
而在其它語言中, 對負數(shù)進行右移類似于(向負無窮)取整。

定長浮點型

警告:
Solidity 還沒有完全支持定長浮點型。可以聲明定長浮點型的變量,但不能給它們賦值或把它們賦值給其他變量。

fixed / ufixed:表示各種大小的有符號和無符號的定長浮點型。
在關鍵字 ufixedMxNfixedMxN 中,M 表示該類型占用的位數(shù),N 表示可用的小數(shù)位數(shù)。
M 必須能整除 8,即 8 到 256 位。
N 則可以是從 0 到 80 之間的任意數(shù)。
ufixedfixed 分別是 ufixed128x19fixed128x19 的別名。

運算符:

  • 比較運算符:<=, <, ==!=, >=, > (返回值是布爾型)
  • 算術運算符:+, -, 一元運算 -, 一元運算 +*, /% (取余數(shù))

注意:
浮點型(在許多語言中的 floatdouble 類型,更準確地說是 IEEE 754 類型)和定長浮點型之間最大的不同點是,
在前者中整數(shù)部分和小數(shù)部分(小數(shù)點后的部分)需要的位數(shù)是靈活可變的,而后者中這兩部分的長度受到嚴格的規(guī)定。
一般來說,在浮點型中,幾乎整個空間都用來表示數(shù)字,但只有少數(shù)的位來表示小數(shù)點的位置。

地址類型

address:地址類型存儲一個 20 字節(jié)的值(以太坊地址的大?。?。
地址類型也有成員變量,并作為所有合約的基礎。

運算符:

  • <=, <==, !=, >=>

注意:
從 0.5.0 版本開始,合約不會從地址類型派生,但仍然可以顯式地轉(zhuǎn)換成地址類型。

地址類型成員變量

  • balancetransfer

可以使用 balance 屬性來查詢一個地址的余額,
也可以使用 transfer 函數(shù)向一個地址發(fā)送以太幣(以 wei 為單位):

address x = 0x123;
address myAddress = this;
if (x.balance < 10 && myAddress.balance >= 10) x.transfer(10);

注意:
如果 x 是一個合約地址,它的代碼(更具體來說是它的 fallback 函數(shù),如果有的話)會跟 transfer 函數(shù)調(diào)用一起執(zhí)行(這是 EVM 的一個特性,無法阻止)。
如果在執(zhí)行過程中用光了 gas 或者因為任何原因執(zhí)行失敗,|ether| 交易會被打回,當前的合約也會在終止的同時拋出異常。

  • send

sendtransfer 的低級版本。如果執(zhí)行失敗,當前的合約不會因為異常而終止,但 send 會返回 false。

警告:
在使用 send 的時候會有些風險:如果調(diào)用棧深度是 1024 會導致發(fā)送失?。ㄟ@總是可以被調(diào)用者強制),如果接收者用光了 gas 也會導致發(fā)送失敗。
所以為了保證以太幣發(fā)送的安全,一定要檢查 send 的返回值,使用 transfer 或者更好的辦法:
使用一種接收者可以取回資金的模式。

  • call, callcodedelegatecall

此外,為了與不符合 ABI 接口的合約交互,于是就有了可以接受任意類型任意數(shù)量參數(shù)的 call 函數(shù)。
這些參數(shù)會被打包到以 32 字節(jié)為單位的連續(xù)區(qū)域中存放。
其中一個例外是當?shù)谝粋€參數(shù)被編碼成正好 4 個字節(jié)的情況。
在這種情況下,這個參數(shù)后邊不會填充后續(xù)參數(shù)編碼,以允許使用函數(shù)簽名。

address nameReg = 0x72ba7d8e73fe8eb666ea66babc8116a41bfb10e2;
nameReg.call("register", "MyName");
nameReg.call(bytes4(keccak256("fun(uint256)")), a);

call 返回的布爾值表明了被調(diào)用的函數(shù)已經(jīng)執(zhí)行完畢(true)或者引發(fā)了一個 EVM 異常(false)。
無法訪問返回的真實數(shù)據(jù)(為此我們需要事先知道編碼和大?。?。

可以使用 .gas() 修飾器調(diào)整提供的 gas 數(shù)量

namReg.call.gas(1000000)("register", "MyName");

類似地,也能控制提供的以太幣的值

nameReg.call.value(1 ether)("register", "MyName");

最后一點,這些修飾器可以聯(lián)合使用。每個修改器出現(xiàn)的順序不重要

nameReg.call.gas(1000000).value(1 ether)("register", "MyName");

注意:
目前還不能在重載函數(shù)中使用 gas 或者 value 修飾器。
一種解決方案是給 gas 和值引入一個特例,并重新檢查它們是否在重載的地方出現(xiàn)。

類似地,也可以使用 delegatecall
區(qū)別在于只使用給定地址的代碼,其它屬性(存儲,余額,……)都取自當前合約。
delegatecall 的目的是使用存儲在另外一個合約中的庫代碼。
用戶必須確保兩個合約中的存儲結(jié)構(gòu)都適用于 delegatecall。
在 homestead 版本之前,只有一個功能類似但作用有限的 callcode 的函數(shù)可用,但它不能獲取委托方的 msg.sendermsg.value。

這三個函數(shù) calldelegatecallcallcode 都是非常低級的函數(shù),應該只把它們當作 最后一招 來使用,因為它們破壞了 Solidity 的類型安全性。

注意:
所有合約都繼承了地址(address)的成員變量,因此可以使用 this.balance 查詢當前合約的余額。

注意:
不鼓勵使用 callcode,在未來也會將其移除。

警告:
這三個函數(shù)都屬于低級函數(shù),需要謹慎使用。
具體來說,任何未知的合約都可能是惡意的。
你在調(diào)用一個合約的同時就將控制權(quán)交給了它,它可以反過來調(diào)用你的合約,
因此,當調(diào)用返回時要為你的狀態(tài)變量的改變做好準備。

定長字節(jié)數(shù)組

關鍵字有:bytes1bytes2, bytes3, ..., bytes32。bytebytes1 的別名。

運算符:

  • 比較運算符:<=, <, ==!=, >=> (返回布爾型)
  • 位運算符: &, |^ (按位異或), ~ (按位取反), << (左移位), >> (右移位)
  • 索引訪問:如果 xbytesI 類型,那么 x[k] (其中 0 <= k < I)返回第 k 個字節(jié)(只讀)。

該類型可以和作為右操作數(shù)的任何整數(shù)類型進行移位運算(但返回結(jié)果的類型和左操作數(shù)類型相同),右操作數(shù)表示需要移動的位數(shù)。
進行負數(shù)位移運算會引發(fā)運行時異常。

成員變量:

  • .length 表示這個字節(jié)數(shù)組的長度(只讀).

注意:
可以將 byte[] 當作字節(jié)數(shù)組使用,但這種方式非常浪費存儲空間,準確來說,是在傳入調(diào)用時,每個元素會浪費 31 字節(jié)。
更好地做法是使用 bytes。

變長字節(jié)數(shù)組

bytes:
變長字節(jié)數(shù)組,參見 數(shù)組。它并不是值類型。
string:
變長 UTF-8 編碼字符串類型,參見 數(shù)組。并不是值類型。

地址字面常數(shù)(Address Literals)

比如像 0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF 這樣的通過了地址校驗和測試的十六進制字面常數(shù)屬于 address 類型。
長度在 39 到 41 個數(shù)字的,沒有通過校驗和測試而產(chǎn)生了一個警告的十六進制字面常數(shù)視為正常的有理數(shù)字面常數(shù)。

注意:
混合大小寫的地址校驗和格式定義在 EIP-55 中。

有理數(shù)和整數(shù)字面常數(shù)

整數(shù)字面常數(shù)由范圍在 0-9 的一串數(shù)字組成,表現(xiàn)成十進制。
例如,69 表示數(shù)字 69。
Solidity 中是沒有八進制的,因此前置 0 是無效的。

十進制小數(shù)字面常數(shù)帶有一個 .,至少在其一邊會有一個數(shù)字。
比如:1.,.1,和 1.3。

科學符號也是支持的,盡管指數(shù)必須是整數(shù),但底數(shù)可以是小數(shù)。
比如:2e10, -2e102e-10, 2.5e1。

數(shù)值字面常數(shù)表達式本身支持任意精度,除非它們被轉(zhuǎn)換成了非字面常數(shù)類型(也就是說,當它們出現(xiàn)在非字面常數(shù)表達式中時就會發(fā)生轉(zhuǎn)換)。
這意味著在數(shù)值常量表達式中, 計算不會溢出而除法也不會截斷。

例如, (2**800 + 1) - 2**800 的結(jié)果是字面常數(shù) 1 (屬于 uint8 類型),盡管計算的中間結(jié)果已經(jīng)超過了 EVM 的機器字長度。
此外, .5 * 8 的結(jié)果是整型 4 (盡管有非整型參與了計算)。

只要操作數(shù)是整型,任意整型支持的運算符都可以被運用在數(shù)值字面常數(shù)表達式中。
如果兩個中的任一個數(shù)是小數(shù),則不允許進行位運算。如果指數(shù)是小數(shù)的話,也不支持冪運算(因為這樣可能會得到一個無理數(shù))。

注意:
Solidity 對每個有理數(shù)都有對應的數(shù)值字面常數(shù)類型。
整數(shù)字面常數(shù)和有理數(shù)字面常數(shù)都屬于數(shù)值字面常數(shù)類型。
除此之外,所有的數(shù)值字面常數(shù)表達式(即只包含數(shù)值字面常數(shù)和運算符的表達式)都屬于數(shù)值字面常數(shù)類型。
因此數(shù)值字面常數(shù)表達式 1 + 22 + 1 的結(jié)果跟有理數(shù)三的數(shù)值字面常數(shù)類型相同。

警告:
在早期版本中,整數(shù)字面常數(shù)的除法也會截斷,但在現(xiàn)在的版本中,會將結(jié)果轉(zhuǎn)換成一個有理數(shù)。即 5 / 2 并不等于 2,而是等于 2.5。

注意:
數(shù)值字面常數(shù)表達式只要在非字面常數(shù)表達式中使用就會轉(zhuǎn)換成非字面常數(shù)類型。
在下面的例子中,盡管我們知道 b 的值是一個整數(shù),但 2.5 + a 這部分表達式并不進行類型檢查,因此編譯不能通過。

uint128 a = 1;
uint128 b = 2.5 + a + 0.5;

字符串字面常數(shù)

字符串字面常數(shù)是指由雙引號或單引號引起來的字符串("foo" 或者 'bar')。
不像在 C 語言中那樣帶有結(jié)束符;"foo" 相當于 3 個字節(jié)而不是 4 個。
和整數(shù)字面常數(shù)一樣,字符串字面常數(shù)的類型也可以發(fā)生改變,但它們可以隱式地轉(zhuǎn)換成 bytes1,……,bytes32,如果合適的話,還可以轉(zhuǎn)換成 bytes 以及 string

字符串字面常數(shù)支持轉(zhuǎn)義字符,例如 \n,\xNN\uNNNN。\xNN 表示一個 16 進制值,最終轉(zhuǎn)換成合適的字節(jié),
\uNNNN 表示 Unicode 編碼值,最終會轉(zhuǎn)換為 UTF-8 的序列。

十六進制字面常數(shù)

十六進制字面常數(shù)以關鍵字 hex 打頭,后面緊跟著用單引號或雙引號引起來的字符串(例如,hex"001122FF")。
字符串的內(nèi)容必須是一個十六進制的字符串,它們的值將使用二進制表示。

十六進制字面常數(shù)跟字符串字面常數(shù)很類似,具有相同的轉(zhuǎn)換規(guī)則。

枚舉類型

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 外部的調(diào)用,
    // "getChoice" 的簽名會自動被改成 "getChoice() returns (uint8)"。
    // 整數(shù)類型的大小已經(jīng)足夠存儲所有枚舉類型的值,隨著值的個數(shù)增加,
    // 可以逐漸使用 `uint16` 或更大的整數(shù)類型。
    function getChoice() public view returns (ActionChoices) {
        return choice;
    }

    function getDefaultChoice() public pure returns (uint) {
        return uint(defaultChoice);
    }
}

函數(shù)類型

函數(shù)類型是一種表示函數(shù)的類型??梢詫⒁粋€函數(shù)賦值給另一個函數(shù)類型的變量,也可以將一個函數(shù)作為參數(shù)進行傳遞,還能在函數(shù)調(diào)用中返回函數(shù)類型變量。
函數(shù)類型有兩類:- 內(nèi)部(internal) 函數(shù)和 外部(external) 函數(shù):

內(nèi)部函數(shù)只能在當前合約內(nèi)被調(diào)用(更具體來說,在當前代碼塊內(nèi),包括內(nèi)部庫函數(shù)和繼承的函數(shù)中),因為它們不能在當前合約上下文的外部被執(zhí)行。
調(diào)用一個內(nèi)部函數(shù)是通過跳轉(zhuǎn)到它的入口標簽來實現(xiàn)的,就像在當前合約的內(nèi)部調(diào)用一個函數(shù)。

外部函數(shù)由一個地址和一個函數(shù)簽名組成,可以通過外部函數(shù)調(diào)用傳遞或者返回。

函數(shù)類型表示成如下的形式

function (<parameter types>) {internal|external} [pure|constant|view|payable] [returns (<return types>)]

與參數(shù)類型相反,返回類型不能為空 —— 如果函數(shù)類型不需要返回,則需要刪除整個 returns (<return types>) 部分。

函數(shù)類型默認是內(nèi)部函數(shù),因此不需要聲明 internal 關鍵字。
與此相反的是,合約中的函數(shù)本身默認是 public 的,只有當它被當做類型名稱時,默認才是內(nèi)部函數(shù)。

有兩種方法可以訪問當前合約中的函數(shù):一種是直接使用它的名字,f ,另一種是使用 this.f
前者適用于內(nèi)部函數(shù),后者適用于外部函數(shù)。

如果當函數(shù)類型的變量還沒有初始化時就調(diào)用它的話會引發(fā)一個異常。
如果在一個函數(shù)被 delete 之后調(diào)用它也會發(fā)生相同的情況。

如果外部函數(shù)類型在 Solidity 的上下文環(huán)境以外的地方使用,它們會被視為 function 類型。
該類型將函數(shù)地址緊跟其函數(shù)標識一起編碼為一個 bytes24 類型。。

請注意,當前合約的 public 函數(shù)既可以被當作內(nèi)部函數(shù)也可以被當作外部函數(shù)使用。
如果想將一個函數(shù)當作內(nèi)部函數(shù)使用,就用 f 調(diào)用,如果想將其當作外部函數(shù),使用 this.f 。

除此之外,public(或 external)函數(shù)也有一個特殊的成員變量稱作 selector,可以返回 ABI 函數(shù)選擇器

pragma solidity ^0.4.16;

contract Selector {
    function f() public view returns (bytes4) {
    return this.f.selector;
    }
}

如果使用內(nèi)部函數(shù)類型的例子

pragma solidity ^0.4.16;

library ArrayUtils {
    // 內(nèi)部函數(shù)可以在內(nèi)部庫函數(shù)中使用,
    // 因為它們會成為同一代碼上下文的一部分
    function map(uint[] memory self, function (uint) pure returns (uint) f)
        internal
        pure
        returns (uint[] memory r)
    {
        r = new uint[](self.length);
        for (uint i = 0; i < self.length; i++) {
            r[i] = f(self[i]);
        }
    }
    function reduce(
        uint[] memory self,
        function (uint, uint) pure returns (uint) f
        )
        internal
        pure
        returns (uint r)
    {
        r = self[0];
        for (uint i = 1; i < self.length; i++) {
            r = f(r, self[i]);
        }
    }
    function range(uint length) internal pure 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 pure returns (uint) {
        return ArrayUtils.range(l).map(square).reduce(sum);
    }
    function square(uint x) internal pure returns (uint) {
        return x * x;
    }
    function sum(uint x, uint y) internal pure returns (uint) {
        return x + y;
    }
}

另外一個使用外部函數(shù)類型的例子

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 {
        // 這里要驗證 reply 來自可信的源
        requests[requestID].callback(response);
    }
}

contract OracleUser {
    Oracle constant oracle = Oracle(0x1234567); // 已知的合約
    function buySomething() {
        oracle.query("USD", this.oracleResponse);
    }
    function oracleResponse(bytes response) public {
        require(msg.sender == address(oracle));
        // 使用數(shù)據(jù)
    }
}

注意:
Lambda 表達式或者內(nèi)聯(lián)函數(shù)的引入在計劃內(nèi),但目前還沒支持。

引用類型

比起之前討論過的值類型,在處理復雜的類型(即占用的空間超過 256 位的類型)時,我們需要更加謹慎。
由于拷貝這些類型變量的開銷相當大,我們不得不考慮它的存儲位置,是將它們保存在 內(nèi)存 (并不是永久存儲)中,
還是 存儲 (保存狀態(tài)變量的地方)中。

數(shù)據(jù)位置

所有的復雜類型,即 數(shù)組結(jié)構(gòu) 類型,都有一個額外屬性,“數(shù)據(jù)位置”,說明數(shù)據(jù)是保存在內(nèi)存中還是存儲中。
根據(jù)上下文不同,大多數(shù)時候數(shù)據(jù)有默認的位置,但也可以通過在類型名后增加關鍵字 storagememory 進行修改。
函數(shù)參數(shù)(包括返回的參數(shù))的數(shù)據(jù)位置默認是 memory
局部變量的數(shù)據(jù)位置默認是 storage,狀態(tài)變量的數(shù)據(jù)位置強制是 storage (這是顯而易見的)。

也存在第三種數(shù)據(jù)位置, calldata ,這是一塊只讀的,且不會永久存儲的位置,用來存儲函數(shù)參數(shù)。
外部函數(shù)的參數(shù)(非返回參數(shù))的數(shù)據(jù)位置被強制指定為 calldata ,效果跟 memory 差不多。

數(shù)據(jù)位置的指定非常重要,因為它們影響著賦值行為:
在存儲和內(nèi)存之間兩兩賦值,或者存儲向狀態(tài)變量(甚至是從其它狀態(tài)變量)賦值都會創(chuàng)建一份獨立的拷貝。
然而狀態(tài)變量向局部變量賦值時僅僅傳遞一個引用,而且這個引用總是指向狀態(tài)變量,因此后者改變的同時前者也會發(fā)生改變。
另一方面,從一個內(nèi)存存儲的引用類型向另一個內(nèi)存存儲的引用類型賦值并不會創(chuàng)建拷貝。

pragma solidity ^0.4.0;

contract C {
    uint[] x; // x 的數(shù)據(jù)存儲位置是 storage

    // memoryArray 的數(shù)據(jù)存儲位置是 memory
    function f(uint[] memoryArray) public {
        x = memoryArray; // 將整個數(shù)組拷貝到 storage 中,可行
        var y = x;  // 分配一個指針(其中 y 的數(shù)據(jù)存儲位置是 storage),可行
        y[7]; // 返回第 8 個元素,可行
        y.length = 2; // 通過 y 修改 x,可行
        delete x; // 清除數(shù)組,同時修改 y,可行
        // 下面的就不可行了;需要在 storage 中創(chuàng)建新的未命名的臨時數(shù)組, /
        // 但 storage 是“靜態(tài)”分配的:
        // y = memoryArray;
        // 下面這一行也不可行,因為這會“重置”指針,
        // 但并沒有可以讓它指向的合適的存儲位置。
        // delete y;
        g(x); // 調(diào)用 g 函數(shù),同時移交對 x 的引用
        h(x); // 調(diào)用 h 函數(shù),同時在 memory 中創(chuàng)建一個獨立的臨時拷貝
    }

    function g(uint[] storage storageArray) internal {}
    function h(uint[] memoryArray) public {}
}

總結(jié)

強制指定的數(shù)據(jù)位置:

  • 外部函數(shù)的參數(shù)(不包括返回參數(shù)): calldata
  • 狀態(tài)變量: storage

默認數(shù)據(jù)位置:

  • 函數(shù)參數(shù)(包括返回參數(shù)): memory
  • 所有其它局部變量: storage

數(shù)組

數(shù)組可以在聲明時指定長度,也可以動態(tài)調(diào)整大小。
對于存儲的數(shù)組來說,元素類型可以是任意的(即元素也可以是數(shù)組類型,映射類型或者結(jié)構(gòu)體)。
對于內(nèi)存的數(shù)組來說,元素類型不能是映射類型,如果作為 public 函數(shù)的參數(shù),它只能是 ABI 類型。

一個元素類型為 T,固定長度為 k 的數(shù)組可以聲明為 T[k],而動態(tài)數(shù)組聲明為 T[]。
舉個例子,一個長度為 5,元素類型為 uint 的動態(tài)數(shù)組的數(shù)組,應聲明為 uint[][5] (注意這里跟其它語言比,數(shù)組長度的聲明位置是反的)。
要訪問第三個動態(tài)數(shù)組的第二個元素,你應該使用 x[2][1](數(shù)組下標是從 0 開始的,且訪問數(shù)組時的下標順序與聲明時相反,也就是說,x[2] 是從右邊減少了一級)。。

bytesstring 類型的變量是特殊的數(shù)組。
bytes 類似于 byte[],但它在 calldata 中會被“緊打包”(譯者注:將元素連續(xù)地存在一起,不會按每 32 字節(jié)一單元的方式來存放)。
stringbytes 相同,但(暫時)不允許用長度或索引來訪問。

注意:
如果想要訪問以字節(jié)表示的字符串 s,請使用 bytes(s).length / bytes(s)[7] = 'x';。
注意這時你訪問的是 UTF-8 形式的低級 bytes 類型,而不是單個的字符。

可以將數(shù)組標識為 public,從而讓 Solidity 創(chuàng)建一個 getter。
之后必須使用數(shù)字下標作為參數(shù)來訪問 getter。

創(chuàng)建內(nèi)存數(shù)組

可使用 new 關鍵字在內(nèi)存中創(chuàng)建變長數(shù)組。
與存儲數(shù)組相反的是,你 不能 通過修改成員變量 .length 改變內(nèi)存數(shù)組的大小。

pragma solidity ^0.4.16;

contract C {
    function f(uint len) public pure {
        uint[] memory a = new uint[](7);
        bytes memory b = new bytes(len);
        // 這里我們有 a.length == 7 以及 b.length == len
        a[6] = 8;
    }
}

數(shù)組字面常數(shù) / 內(nèi)聯(lián)數(shù)組

數(shù)組字面常數(shù)是寫作表達式形式的數(shù)組,并且不會立即賦值給變量。

pragma solidity ^0.4.16;

contract C {
    function f() public pure {
        g([uint(1), 2, 3]);
    }
    function g(uint[3] _data) public pure {
        // ...
    }
}

數(shù)組字面常數(shù)是一種定長的內(nèi)存數(shù)組類型,它的基礎類型由其中元素的普通類型決定。
例如,[1, 2, 3] 的類型是 uint8[3] memory,因為其中的每個字面常數(shù)的類型都是 uint8。
正因為如此,有必要將上面這個例子中的第一個元素轉(zhuǎn)換成 uint 類型。
目前需要注意的是,定長的內(nèi)存數(shù)組并不能賦值給變長的內(nèi)存數(shù)組,下面是個反例:

// 這段代碼并不能編譯。

pragma solidity ^0.4.0;

contract C {
    function f() public {
        // 這一行引發(fā)了一個類型錯誤,因為 unint[3] memory
        // 不能轉(zhuǎn)換成 uint[] memory。
        uint[] x = [uint(1), 3, 4];
    }
}

已經(jīng)計劃在未來移除這樣的限制,但目前數(shù)組在 ABI 中傳遞的問題造成了一些麻煩。

成員

length:
數(shù)組有 length 成員變量表示當前數(shù)組的長度。
動態(tài)數(shù)組可以在存儲(而不是內(nèi)存)中通過改變成員變量 .length 改變數(shù)組大小。
并不能通過訪問超出當前數(shù)組長度的方式實現(xiàn)自動擴展數(shù)組的長度。
一經(jīng)創(chuàng)建,|memory| 數(shù)組的大小就是固定的(但卻是動態(tài)的,也就是說,它依賴于運行時的參數(shù))。

push:
變長的存儲數(shù)組以及 bytes 類型(而不是 string 類型)都有一個叫做 push 的成員函數(shù),它用來附加新的元素到數(shù)組末尾。
這個函數(shù)將返回新的數(shù)組長度。

警告:
在外部函數(shù)中目前還不能使用多維數(shù)組。

警告:
由于 EVM 的限制,不能通過外部函數(shù)調(diào)用返回動態(tài)的內(nèi)容。
例如,如果通過 web3.js 調(diào)用 contract C { function f() returns (uint[]) { ... } } 中的 f 函數(shù),它會返回一些內(nèi)容,但通過 Solidity 不可以。
目前唯一的變通方法是使用大型的靜態(tài)數(shù)組。

pragma solidity ^0.4.16;

contract ArrayContract {
    uint[2**20] m_aLotOfIntegers;
    // 注意下面的代碼并不是一對動態(tài)數(shù)組,
    // 而是一個數(shù)組元素為一對變量的動態(tài)數(shù)組(也就是數(shù)組元素為長度為 2 的定長數(shù)組的動態(tài)數(shù)組)。
    bool[2][] m_pairsOfFlags;
    // newPairs 存儲在 memory 中 —— 函數(shù)參數(shù)默認的存儲位置

    function setAllFlagPairs(bool[2][] newPairs) public {
        // 向一個 storage 的數(shù)組賦值會替代整個數(shù)組
        m_pairsOfFlags = newPairs;
    }

    function setFlagPair(uint index, bool flagA, bool flagB) public {
        // 訪問一個不存在的數(shù)組下標會引發(fā)一個異常
        m_pairsOfFlags[index][0] = flagA;
        m_pairsOfFlags[index][1] = flagB;
    }

    function changeFlagArraySize(uint newSize) public {
        // 如果 newSize 更小,那么超出的元素會被清除
        m_pairsOfFlags.length = newSize;
    }

    function clear() public {
        // 這些代碼會將數(shù)組全部清空
        delete m_pairsOfFlags;
        delete m_aLotOfIntegers;
        // 這里也是實現(xiàn)同樣的功能
        m_pairsOfFlags.length = 0;
    }

    bytes m_byteData;

    function byteArrays(bytes data) public {
        // 字節(jié)的數(shù)組(語言意義中的 byte 的復數(shù) ``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` 創(chuàng)建動態(tài) memory 數(shù)組:
        uint[2][] memory arrayOfPairs = new uint[2][](size);
        // 創(chuàng)建一個動態(tài)字節(jié)數(shù)組:
        bytes memory b = new bytes(200);
        for (uint i = 0; i < b.length; i++)
            b[i] = byte(i);
        return b;
    }
}

結(jié)構(gòu)體

Solidity 支持通過構(gòu)造結(jié)構(gòu)體的形式定義新的類型,以下是一個結(jié)構(gòu)體使用的示例:

pragma solidity ^0.4.11;

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++; // campaignID 作為一個變量返回
        // 創(chuàng)建新的結(jié)構(gòu)體示例,存儲在 storage 中。我們先不關注映射類型。
        campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0);
    }

    function contribute(uint campaignID) public payable {
        Campaign storage c = campaigns[campaignID];
        // 以給定的值初始化,創(chuàng)建一個新的臨時 memory 結(jié)構(gòu)體,
        // 并將其拷貝到 storage 中。
        // 注意你也可以使用 Funder(msg.sender, msg.value) 來初始化。
        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;
    }
}

上面的合約只是一個簡化版的眾籌合約,但它已經(jīng)足以讓我們理解結(jié)構(gòu)體的基礎概念。
結(jié)構(gòu)體類型可以作為元素用在映射和數(shù)組中,其自身也可以包含映射和數(shù)組作為成員變量。

盡管結(jié)構(gòu)體本身可以作為映射的值類型成員,但它并不能包含自身。
這個限制是有必要的,因為結(jié)構(gòu)體的大小必須是有限的。

注意在函數(shù)中使用結(jié)構(gòu)體時,一個結(jié)構(gòu)體是如何賦值給一個局部變量(默認存儲位置是存儲)的。
在這個過程中并沒有拷貝這個結(jié)構(gòu)體,而是保存一個引用,所以對局部變量成員的賦值實際上會被寫入狀態(tài)。

當然,你也可以直接訪問結(jié)構(gòu)體的成員而不用將其賦值給一個局部變量,就像這樣,
campaigns[campaignID].amount = 0。

映射

映射類型在聲明時的形式為 mapping(_KeyType => _ValueType)。
其中 _KeyType 可以是除了映射、變長數(shù)組、合約、枚舉以及結(jié)構(gòu)體以外的幾乎所有類型。
_ValueType 可以是包括映射類型在內(nèi)的任何類型。

映射可以視作哈希表,它們在實際的初始化過程中創(chuàng)建每個可能的 key,
并將其映射到字節(jié)形式全是零的值:一個類型的 默認值。然而下面是映射與哈希表不同的地方:
在映射中,實際上并不存儲 key,而是存儲它的 keccak256 哈希值,從而便于查詢實際的值。

正因為如此,映射是沒有長度的,也沒有 key 的集合或 value 的集合的概念。

只有狀態(tài)變量(或者在 internal 函數(shù)中的對于存儲變量的引用)可以使用映射類型。。

可以將映射聲明為 public,然后來讓 Solidity 創(chuàng)建一個 getter。
_KeyType 將成為 getter 的必須參數(shù),并且 getter 會返回 _ValueType。

_ValueType 也可以是一個映射。這時在使用 getter 時將將需要遞歸地傳入每個 _KeyType 參數(shù)。

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);
    }
}

注意:
遞歸不支持迭代,但可以在此之上實現(xiàn)一個這樣的數(shù)據(jù)結(jié)構(gòu)。
例子可以參考可迭代的映射

涉及 LValues 的運算符

如果 a 是一個 LValue(即一個變量或者其它可以被復制的東西),以下運算符都可以使用簡寫:

a += e 等同于 a = a + e。 其它運算符 -=*=, /=, %=, |=, &= 以及 ^= 都是如此定義的。
a++a-- 分別等同于 a += 1a -= 1,但表達式本身的值等于 a 在計算之前的值。
與之相反,--a++a 雖然最終 a 的結(jié)果與之前的表達式相同,但表達式的返回值是計算之后的值。

刪除

delete a 的結(jié)果是將 a 的類型在初始化時的值賦值給 a。即對于整型變量來說,相當于 a = 0,
但 delete 也適用于數(shù)組,對于動態(tài)數(shù)組來說,是將數(shù)組的長度設為 0,而對于靜態(tài)數(shù)組來說,是將數(shù)組中的所有元素重置。
如果對象是結(jié)構(gòu)體,則將結(jié)構(gòu)體中的所有屬性重置。

delete 對整個映射是無效的(因為映射的鍵可以是任意的,通常也是未知的)。
因此在你刪除一個結(jié)構(gòu)體時,結(jié)果將重置所有的非映射屬性,這個過程是遞歸進行的,除非它們是映射。
然而,單個的鍵及其映射的值是可以被刪除的。

理解 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,并不影響數(shù)據(jù)
        delete data; // 將 data 設為 0,并不影響 x,因為它仍然有個副本
        uint[] storage y = dataArray;
        delete dataArray; 
        // 將 dataArray.length 設為 0,但由于 uint[] 是一個復雜的對象,y 也將受到影響,
        // 因為它是一個存儲位置是 storage 的對象的別名。
        // 另一方面:"delete y" 是非法的,引用了 storage 對象的局部變量只能由已有的 storage 對象賦值。
    }
}

基本類型之間的轉(zhuǎn)換

隱式轉(zhuǎn)換

如果一個運算符用在兩個不同類型的變量之間,那么編譯器將隱式地將其中一個類型轉(zhuǎn)換為另一個類型(不同類型之間的賦值也是一樣)。
一般來說,只要值類型之間的轉(zhuǎn)換在語義上行得通,而且轉(zhuǎn)換的過程中沒有信息丟失,那么隱式轉(zhuǎn)換基本都是可以實現(xiàn)的:
uint8 可以轉(zhuǎn)換成 uint16,int128 轉(zhuǎn)換成 int256,但 int8 不能轉(zhuǎn)換成 uint256
(因為 uint256 不能涵蓋某些值,例如,-1)。
更進一步來說,無符號整型可以轉(zhuǎn)換成跟它大小相等或更大的字節(jié)類型,但反之不能。
任何可以轉(zhuǎn)換成 uint160 的類型都可以轉(zhuǎn)換成 address 類型。

顯式轉(zhuǎn)換

如果某些情況下編譯器不支持隱式轉(zhuǎn)換,但是你很清楚你要做什么,這種情況可以考慮顯式轉(zhuǎn)換。
注意這可能會發(fā)生一些無法預料的后果,因此一定要進行測試,確保結(jié)果是你想要的!
下面的示例是將一個 int8 類型的負數(shù)轉(zhuǎn)換成 uint

int8 y = -3;
uint x = uint(y);

這段代碼的最后,x 的值將是 0xfffff..fd (64 個 16 進制字符),因為這是 -3 的 256 位補碼形式。

如果一個類型顯式轉(zhuǎn)換成更小的類型,相應的高位將被舍棄

uint32 a = 0x12345678;
uint16 b = uint16(a); // 此時 b 的值是 0x5678

類型推斷

為了方便起見,沒有必要每次都精確指定一個變量的類型,編譯器會根據(jù)分配該變量的第一個表達式的類型自動推斷該變量的類型

uint24 x = 0x123;
var y = x;

這里 y 的類型將是 uint24。不能對函數(shù)參數(shù)或者返回參數(shù)使用 var。

警告:
類型只能從第一次賦值中推斷出來,因此以下代碼中的循環(huán)是無限的,
原因是i 的類型是 uint8,而這個類型變量的最大值比 2000 小。
for (var i = 0; i < 2000; i++) { ... }

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容