Solidity是一種靜態類型語言,需要再編譯期間指定每個變量(靜態和局部)的類型。Solidity提供了幾種基本類型,可以通過基本類型組合成復雜類型。另外,在帶有操作符的表達式中,類型之間會相互影響。
數值類型
下面介紹數值類型,為什么叫數據類型,因為這些變量類型都需要傳入一個值,例如:在函數變量或是賦值,數值類型都會進行拷貝。
布爾型
bool: 值為 true 或是 false
操作:
! 邏輯非
&& 邏輯與
|| 邏輯或
== 相等
!= 不等
|| 和 && 具有簡化規則,比如 f(x) || g(x) ,如果 f(x)為true,將不會計算g(x)的值,即使g(x)有可能為false
整型
int/uint : 有符號整型和無符號整型 有多種長度。 unit8 到 uint258 相差8個(從8到258),int8到int256一樣。unit 和int的別名分別為:unit256 和 int256.
操作:
比較:<= , < , == , != , >= , >
位操作: &(與) , |(或) , ^ (異或), ~ (非)
算術操作:+ , - , * , /, % , **(乘方,求冪) ,<<(左移) , >>(右移)
除法操作經常會被截斷(在EVM中會被編譯成DIV操作碼),但是如果相除的兩個數都是 數字常量,結果不會被截斷。如果除數為0,將會拋出運行時異常。
移位操作的結果是左操作數,x << y 和 x * 2 ** y 相同,x >> y 和 x / 2**y 相同。這就意味着,如果對一個負數進行移位操作,會擴展到符號位。如果一個數被 移位了 負數次,會拋出運行時異常。
地址
address: 20 字節長度的值(以太坊的地址),地址類型有很多成員變量,是所有合約的基礎。
操作: <= , < , == , != , >= , >
地址類型的成員
- balance 和 transfer
可以通過地址的balance屬性來查看一個地址的余額,發送以太幣(單位為:wei)到一個地址可以使用 transfer方法
address x = 0x123; address myAddress = this; if (x.balance < 10 && myAddress.balance >= 10) x.transfer(10);
注意:如果x是一個合約地址,它的代碼(如果存在的話,更明確是為 fallback 函數)將會和 transfer 調用一起被執行(這是EVM的限制,是不可阻止的)。如果執行過程中gas不夠或是失敗,當前合約會終止拋出異常
-
send
send方法和transfer很相似,但是比transfer更低級。如果send失敗,當前的合約不會中斷,也不會拋出異常,會返回一個false。
注意:使用send有一些風險:如果調用棧深度超過1024或是gas不夠,所有的轉讓操作都會失敗,為了更安全的以太幣轉移,如果用send就必須每次都要檢查返回值,使用transfer方法會更好;
- call, callcode, delegatecall
而且,call方法可以和沒有依附於ABI上的合約進行交互,它提供了任意多個任意類型的參數。這些參數填充成32字節,並連接起來。有情況如果第一個參數被加密成4個字節,就會拋出異常,這種情況下,是不允許使用這個方法。
address nameReg = 0x72ba7d8e73fe8eb666ea66babc8116a41bfb10e2; nameReg.call("register", "MyName"); nameReg.call(bytes4(keccak256("fun(uint256)")), a);
call 返回一個boolean值,被調用函數終止返回true或是有EVM異常就返回false。不會返回訪問的具體數據(對此,我們需要預先知道編碼方式和大小)。
同樣的方式,delegatecall也可以使用,和call的唯一區別是 delegatecall會使用給定地址的代碼。所有其他方面(存儲,余額等)都是從當前合約中獲取。delegatecall的作用是使用其他合約里的library代碼。用戶需要確保兩個合約的存儲布局適用於 deletegatecall的使用。
call, delegatecall和call都是很低層次的方法,只有在實在沒辦法的時候才使用,這三個方法會破壞Solidity里的類型安全。
三個方法都可以使用 .gas(),而 .value() 不適合deletegatecall.
注意:所有的合約都繼承了地址相關成員方法。所以可以使用 this.balance 來查詢當前合約的余額
⚠️ 這些方法都是低層數的方法,使用的時候一定要小心。如果使用不當,任何未知的合約都可能別破壞。 你應該移交控制到可以通過回調到你自己合約的那個合約里,這樣通過返回值就可以更新你自己的state變量
固定大小的byte數組
bytes1, bytes2, bytes3 ... bytes32。 byte和byte1一樣。
操作:
比較: <= , <, == , != , >= , >
位操作:& , | , ^ , ~, << , >>
下標訪問:如果x是 byteI類型,那么 x[k] (0<= k < I),返回第k個byte (只讀)
移位操作可以作為右操作使用在任何整型上(返回的是一個左操作的類型),如果移位的位數是個負數,會拋出運行時異常。
成員: .length 得到byte數組的固定長度
動態大小的byte數組
bytes: 動態大小byte數組,不是一個值類型
string:動態大小UTF-8編碼string,不是一個值類型
作為一個經驗法則,對於任意長度的raw數據,使用bytes。對於任意長度的string(UTF-8)數據,使用string。如果可以限定bytes到一定長度,優先使用byte1到byte32,這32個byte類型開銷更小。
地址常量
十六進制常量並通過地址的checksum的驗證。比如 0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF 是一個address類型。十六進制常量是39位到41位數字長度。如果沒有通過checksum的檢查,會產生一個警告,但是還是會作為一個合理的數字常量。
有理數和整數常量
整數常量由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(雖然在表達式中使用了非整型)。
如果結果不是一個整型,會根據小數部分的bits大小來使用ufixed還是fixed類型。在var x = 1/4; 中,x的類型為 ufixed0x8,在 var x = 1/3 ,x的類型為ufixed0x256,因為 1/3 的結果在二進制中是不能用有限來表示的。只要操作數是整型,任何應用於整型的操作都可以用於數字常量表達式中。如果兩個操作數中公有一個是小數,位操作就不允許。同樣的,如果指數是小數,乘方操作也不允許。
字符串常量
字符串常量可以寫成用雙引號或是單引號("foo"
, 'bar'
)。Solidity里的字符串常量不像C語言中,后面有個0結尾。"foo"是三個bytes長度,而不是四個。在整型常量中,類型可以改變,他們可以轉換成 bytes1
, ..., bytes32 ,如果他們符合類型,可以從bytes或是string。字符串常量支持轉義字符,例如
\n
, \xNN,
\uNNNN 。
十六進制常量
十六進制是以 關鍵字 hex開頭的,后面的數字用雙引號或是單引號引起(hex"001122FF")。引號內的內容必須是十六進制的字符串。十六進制常量的行為和字符串常量一樣,並且有同樣的限制。
枚舉
枚舉是一種用戶定義類型。他們可以顯示轉換成所有的整型類型,但是不允許隱式轉換。在運行時會檢查顯示轉換的值范圍,如果轉換失敗,就會拋出異常。枚舉至少要有一個成員
pragma solidity ^0.4.0; contract test { enum ActionChoices { GoLeft, GoRight, GoStraight, SitStill } ActionChoices choice; ActionChoices constant defaultChoice = ActionChoices.GoStraight; function setGoStraight() { choice = ActionChoices.GoStraight; } // Since enum types are not part of the ABI, the signature of "getChoice" // will automatically be changed to "getChoice() returns (uint8)" // for all matters external to Solidity. The integer type used is just // large enough to hold all enum values, i.e. if you have more values, // `uint16` will be used and so on. function getChoice() returns (ActionChoices) { return choice; } function getDefaultChoice() returns (uint) { return uint(defaultChoice); } }
函數類型
函數類型是方法類型,類似於函數指針。方法可以賦值給函數類型的變量,函數類型的方法參數可用於傳遞方法或是返回方法。函數類型有兩種形式:內部方法和外部方法。內部方法只能用於當前合約中,因為他們不可在當前合約的上下文之外執行。調用一個內部就跳轉到這個方法的人口處,就想調用一個當前合約的內部方法一樣。外部方法由一個地址和一個函數簽名組成,可以通過一個外部方法傳值或是返回。
函數類型聲明如下:
function (<parameter types>) {internal|external} [constant] [payable] [returns (<return types>)]
和參數類型相比,返回類型不能為空。如果函數類型不需要返回任何值,整個returns (<return types>) 都會被省略。默認情況下,函數內類是內部的。所以 internal 會被省略。在當前的合約中,有兩種方式可以訪問一個方法:直接使用方法名 f, 或使用 this.f ,前面結果是一個內部方法,而后面一種是一種外部方法。
調用一個沒有初始化的方法類型變量會拋出異常,同樣的如果你如果調用一個已經調用過delete之后的方法類型變量也會拋出異常。如果一個外部方法類型使用在Solidity的上下文之外,他們被看成是一個 函數類型,它的方法標識符和它的地址會用一個bytes24類型 編碼在一起。注意當前合約的public方法可以即作為內部和外部方法。直接使用 f 就作為一個內部方法,如果你想使用外部方法形式,就使用 this.f
如下例子展示了如何使用內部函數類型
pragma solidity ^0.4.5; library ArrayUtils { // internal functions can be used in internal library functions because // they will be part of the same code context function map(uint[] memory self, function (uint) returns (uint) f) internal 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) returns (uint) f ) internal returns (uint) { 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) 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; } }
另外一個使用外部函數類型例子:
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) { requests.push(Request(data, callback)); NewRequest(requests.length - 1); } function reply(uint requestID, bytes response) { // Here goes the check that the reply comes from a trusted source requests[requestID].callback(response); } } contract OracleUser { Oracle constant oracle = Oracle(0x1234567); // known contract function buySomething() { oracle.query("USD", this.oracleResponse); } function oracleResponse(bytes response) { require(msg.sender == address(oracle)); // Use the data } }
引用類型
復雜類型,比如相比於我們常見的數值類型, 我們要更小心的處理那些不一定總是會符合256bits的類型。由於拷貝操作的代價很高昂,我們需要考慮是否要把他們存在內存中還是存在存儲空間中。
數據位置
每一個復雜類型,比如 array或是structs 都有一個額外的注解,數據位置,用於標識數據是在存在內存還是存儲空間中。根據上下文,總是有一個默認值, 但是可以通過重寫來指定是存儲空間還是內存。默認的方法形參包括返回參數都是存儲在內存中,局部變量的默認存儲是在存儲空間,靜態變量智能存儲在存儲空間中。
還有一種是第三數據位置,calldata, 是一個不可變,非持久區域,方法實參存儲在這個區域。外部方法的形參不是返回參數智能存儲在 calldata中,存儲在calldata里的數據行為和存在內存里一樣。
數據位置非常重要,它決定了如何分配。存儲空間,內存和一個靜態變量的分配總是創建一個獨立的拷貝空間。而一個局部變量只是分配一個引用,這個引用總是 指向一個靜態變量,即使靜態變量同時改變了。另一方面,從一個內存存儲的引用到一個存儲空間存儲的引用不會創建一個拷貝。
pragma solidity ^0.4.0; contract C { uint[] x; // the data location of x is storage // the data location of memoryArray is memory function f(uint[] memoryArray) { x = memoryArray; // works, copies the whole array to storage var y = x; // works, assigns a pointer, data location of y is storage y[7]; // fine, returns the 8th element y.length = 2; // fine, modifies x through y delete x; // fine, clears the array, also modifies y // The following does not work; it would need to create a new temporary / // unnamed array in storage, but storage is "statically" allocated: // y = memoryArray; // This does not work either, since it would "reset" the pointer, but there // is no sensible location it could point to. // delete y; g(x); // calls g, handing over a reference to x h(x); // calls h and creates an independent, temporary copy in memory } function g(uint[] storage storageArray) internal {} function h(uint[] memoryArray) {} }
概括
指定地址位置
- 外部方法的形參: calldata
- 靜態變量: 存儲空間
默認地址位置
- 內部方法的形參包括返回參數 : 內存
- 所有的其他局部變量:存儲空間
數組
數組在編譯期間固定大小或是聲明為一個動態數組, 對於存儲數組,數組里的元素類型是可以為任意的(可以是其他數組,mapping或是structs).對於內存數組,如果是一個public方法的實參,元素不能是mapping,而且必須是ABI類型。
如果數組的長度為x, 元素類型為T,則可以寫成 T[x],一個動態長度的數組,可以寫成 T[]。例如,一個數組為5的動態數組,類型為uint,可以寫成 uint[][5]。訪問第二個uint中的第三個元素,可以使用 T[2][1] (數組下標從0開始)。bytes和string是特殊的數組,bytes類似於byte[],但是它在calldata里是緊湊存放。string等於bytes,但是不允許根據長度或下標訪問。所以從廉價程度來說,相對於byte[],更優先使用bytes。
分配內存數組
在內存里創建一個可變長度數組可以使用new關鍵字,和存儲空間數組相反,它不能通過 .length 重新設置長度。
pragma solidity ^0.4.0; contract C { function f(uint len) { uint[] memory a = new uint[](7); bytes memory b = new bytes(len); // Here we have a.length == 7 and b.length == len a[6] = 8; } }
數組常量 或是內聯數組
常量數組可以寫成一種表達式,並且不可以賦值給一個變量。
pragma solidity ^0.4.0; contract C { function f() { g([uint(1), 2, 3]); } function g(uint[3] _data) { // ... } }
常量數組是一個固定長度的內存數組,它的基本類型是給定元素的類型。[1,2,3]的類型為 uint8[3] memory ,因為包含的每個元素類型都為 uint8。 所以有必要把前面例子的第一個元素類型改成 uint。需要注意:固定長度的內存數組是不可以賦值給 動態長度的內存數組。下面的例子是錯誤的。
pragma solidity ^0.4.0; contract C { function f() { // The next line creates a type error because uint[3] memory // cannot be converted to uint[] memory. uint[] x = [uint(1), 3, 4]; }
這個限制現有有計划會在后續版本里去掉。
成員
- length
數組有個length成員來表示數組元素的個數。動態數組可以在存儲空間(不是內存)里通過改變 length值來重新設置長度。
- push
動態存儲數組 和 bytes(不是string)可以使用push來增加一個元素到數組的尾部。這個方法將返回一個新的長度。
pragma solidity ^0.4.0; contract ArrayContract { uint[2**20] m_aLotOfIntegers; // Note that the following is not a pair of dynamic arrays but a // dynamic array of pairs (i.e. of fixed size arrays of length two). bool[2][] m_pairsOfFlags; // newPairs is stored in memory - the default for function arguments function setAllFlagPairs(bool[2][] newPairs) { // assignment to a storage array replaces the complete array m_pairsOfFlags = newPairs; } function setFlagPair(uint index, bool flagA, bool flagB) { // access to a non-existing index will throw an exception m_pairsOfFlags[index][0] = flagA; m_pairsOfFlags[index][1] = flagB; } function changeFlagArraySize(uint newSize) { // if the new size is smaller, removed array elements will be cleared m_pairsOfFlags.length = newSize; } function clear() { // these clear the arrays completely delete m_pairsOfFlags; delete m_aLotOfIntegers; // identical effect here m_pairsOfFlags.length = 0; } bytes m_byteData; function byteArrays(bytes data) { // byte arrays ("bytes") are different as they are stored without padding, // but can be treated identical to "uint8[]" m_byteData = data; m_byteData.length += 7; m_byteData[3] = 8; delete m_byteData[2]; } function addFlag(bool[2] flag) returns (uint) { return m_pairsOfFlags.push(flag); } function createMemoryArray(uint size) returns (bytes) { // Dynamic memory arrays are created using `new`: uint[2][] memory arrayOfPairs = new uint[2][](size); // Create a dynamic byte array: bytes memory b = new bytes(200); for (uint i = 0; i < b.length; i++) b[i] = byte(i); return b; } }
結構
Solidity提供了一種用戶自定義格式的新類型。例如:
pragma solidity ^0.4.11; contract CrowdFunding { // Defines a new type with two fields. 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) returns (uint campaignID) { campaignID = numCampaigns++; // campaignID is return variable // Creates new struct and saves in storage. We leave out the mapping type. campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0); } function contribute(uint campaignID) payable { Campaign c = campaigns[campaignID]; // Creates a new temporary memory struct, initialised with the given values // and copies it over to storage. // Note that you can also use Funder(msg.sender, msg.value) to initialise. c.funders[c.numFunders++] = Funder({addr: msg.sender, amount: msg.value}); c.amount += msg.value; } function checkGoalReached(uint campaignID) returns (bool reached) { Campaign c = campaigns[campaignID]; if (c.amount < c.fundingGoal) return false; uint amount = c.amount; c.amount = 0; c.beneficiary.transfer(amount); return true; } }
結構可以用在mapping和數組的內部,當然結構本身也可以包含mapping或是數組。雖然結構本身可以作為mapping的成員值,但是結構不可以包含它本身類型。這個約束是有必要的,是為了防止出現結構套結構,無限的循環套用。注意:結構是賦值給一個局部變量(默認為存儲空間數據位置),不是拷貝結構,而是存儲一個引用指向局部變量。當然,不用賦值給局部變量,也可以直接訪問結構體的成員 campaigns[campaignID].amount = 0
Mappings
Mappings類型定義為 mapping(_KeyType => _ValueType),這里 _KeyType可以是除了mapping外的其他任意類型。 _ValueType 可以是任意類型,包括mapping。Mapping可以看成是一個哈希表,初始化每個存在的key,對應的value的值會初始化為所有的字節都為0。mapping的key事實上不是存在mapping中,只有key的keccak256 哈希才用於查找值。所以,mapping是沒有長度和設置key和value的概念。mapping只允許靜態變量或是內部方法中的存儲空間引用類型。
pragma solidity ^0.4.0; contract MappingExample { mapping(address => uint) public balances; function update(uint newBalance) { balances[msg.sender] = newBalance; } } contract MappingUser { function f() returns (uint) { return MappingExample(<address>).balances(this); } }
LValues的操作
如果a是一個LValue(一個變量,或是可以被賦值的),提供了一下操作:
a+= e 等於 a = a + e;同樣 -=
, *=
, /=
, %=
, a |=
, &=
和 ^= , a++ , a--, ++a, --a。
- delete
delete a 為a賦值一個初始值。比如 對於 整型,a = 0; 這個也可以用於數組,把一個動態數組長度設置為0 或是靜態數組中的所有元素重設為初始值。對於結構體來說,也是重設結構體里的所有元素。delete對於mapping是無效的。所以delete 一個結構體,會遞歸重置所有元素,除了mapping。但是對於mapping的單獨的key或是key指定的元素值可以被 delete。
delete a是表示重設a的值。
pragma solidity ^0.4.0; contract DeleteExample { uint data; uint[] dataArray; function f() { uint x = data; delete x; // sets x to 0, does not affect data delete data; // sets data to 0, does not affect x which still holds a copy uint[] y = dataArray; delete dataArray; // this sets dataArray.length to zero, but as uint[] is a complex object, also // y is affected which is an alias to the storage object // On the other hand: "delete y" is not valid, as assignments to local variables // referencing storage objects can only be made from existing storage objects. } }
數據類型的轉換
隱式轉換
如果操作兩個不同類型的數據,編譯器會嘗試隱式轉換其中的一個操作數類型到另外一個操作類型。通常來說,如果語法是有意義並且不會出現信息丟失情況下,值之間的隱式轉換是可以的:uint8轉換到 uint16, uint 128到 uint 256。但是 uint8不可以轉換到 uint256,因為uint256不能包含uint8的所有數字,比如 -1。無符號整型可以轉換成同樣大小的 bytes,但是反過來不行。所有可以轉換成uint160的都可以轉換成 address。
顯式轉換
如果編譯器不允許隱式轉換,但是你自己知道自己的做法,可以進行顯式轉換。但是顯式轉換可能出現很多異常情況,需要充分測試。比如:
int8 y = -3; uint x = uint(y);
如果嘗試去強制轉換到一個小的數據類型,會出現高位數據丟失情況
uint32 a = 0x12345678; uint16 b = uint16(a); // b will be 0x5678 now
歡迎大家關注微信號:蝸牛講技術。掃下面的二維碼