在前文我們講述了值類型,也就說再修改值類型的時候,每次都有一個獨立的副本,如:string 類型的狀態(tài)變量,其值是無法修改,而是拷貝出一份該狀態(tài)的變量,將新值存起來。對于處理稍微復雜地值類型時,拷貝將變得愈發(fā)大了,也正是介于此,才考慮到將數(shù)據(jù)存放在內(nèi)存(memory)
或是存放在存儲(storage)
。
在 Solidity 中,數(shù)組(array)和 結(jié)構(gòu)體(struct)屬于引用類型
。
更改數(shù)據(jù)位置或類型轉(zhuǎn)換將始終產(chǎn)生自動進行一份拷貝,而在同一數(shù)據(jù)位置內(nèi)(對于 存儲(storage) 來說)的復制僅在某些情況下進行拷貝。
數(shù)據(jù)位置和賦值行為
所有的引用類型,如數(shù)組(array0
和結(jié)構(gòu)體(struct)
類型,都有別同于其他類型,那便是引用類型
有額外地屬性——數(shù)據(jù)位置。
數(shù)據(jù)位置,顧名思義就是數(shù)據(jù)存放的位置在哪里?是在內(nèi)存(memory)
中還是在存儲(storage)
中。
當然咯,大多時候數(shù)據(jù)都有默認的存放位置。也可顯式地修改其數(shù)據(jù)存放的位置,只需在類型
后添加memory
或storage
。
而函數(shù)參數(shù)/形參(包括函數(shù)的返回參數(shù))
數(shù)據(jù)位置默認在memory
,局部變量
數(shù)據(jù)位置則默認在storage
,但狀態(tài)變量
數(shù)據(jù)位置被強制在storage
。
還有一個調(diào)用數(shù)據(jù)(calldata)
存儲方式,用于存放外部函數(shù)(external)
的參數(shù)/形參,其效果跟memory
差不離。
指定數(shù)據(jù)存放的位置是非常的重要,因為它們將會影響其賦值行為。
- 在 存儲storage 和 內(nèi)存memory 之間兩兩賦值(或者從 調(diào)用數(shù)據(jù)calldata 賦值 ),都會創(chuàng)建一份獨立的拷貝。
- 從 內(nèi)存memory 到 內(nèi)存memory 的賦值只創(chuàng)建引用, 這意味著更改內(nèi)存變量,其他引用相同數(shù)據(jù)的所有其他內(nèi)存變量的值也會跟著改變。
- 從 存儲storage 到本地存儲變量的賦值也只分配一個引用。
- 其他的向 存儲storage 的賦值,總是進行拷貝。 這種情況的示例如對狀態(tài)變量或 存儲storage 的結(jié)構(gòu)體類型的局部變量成員的賦值,即使局部變量本身是一個引用,也會進行一份拷貝(譯者注:查看下面
ArrayContract
合約 更容易理解)。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.4.0;
contract StorageExam {
uint[] x; // x 的數(shù)據(jù)存儲位置是 storage
// memoryArray 的數(shù)據(jù)存儲位置是 memory
function f(uint[] memory memoryArray) public {
x = memoryArray; // 將整個數(shù)組拷貝到 storage 中
uint[] storage y = x; // 分配一個指針(其中 y 的數(shù)據(jù)存儲位置是 storage)
y[7]; // 返回第 8 個元素
y.pop(); // 通過 y 修改 x
delete x; // 刪除數(shù)組 x,同樣也會刪除數(shù)組 y
// 下面的方法不起作用;它需要在存儲中創(chuàng)建一個新的臨時未命名數(shù)組
// 未命名的數(shù)組,但存儲是 "靜態(tài) "分配的。
// y = memoryArray;
// 同樣,"刪除y "也是無效的,因為對本地變量的賦值只能從現(xiàn)有的存儲對象中進行。
// 它將 "重置 "指針,但是沒有任何合理的位置可以讓它指向
// delete y;
g(x); // 調(diào)用 g 函數(shù),同時移交對 x 的引用
h(x); // 調(diào)用 h 函數(shù),同時在 memory 中創(chuàng)建一個獨立的臨時副本
}
function g(uint[] storage ) internal pure {}
function h(uint[] memory) public pure {}
}
歸納為:
強制指定的數(shù)據(jù)位置:
- 外部函數(shù)的參數(shù)(不包括返回參數(shù)): calldata
- 狀態(tài)變量: storage
默認數(shù)據(jù)位置:
- 函數(shù)參數(shù)(包括返回參數(shù)): memory
- 所有其它局部變量: storage
數(shù)組(array)
數(shù)組是用來存放一組數(shù)據(jù)(整數(shù)、字符串、地址等),它是一種常見的數(shù)據(jù)類型,而在 Solidity 中,數(shù)組可分為編譯時
的固定大小,動態(tài)大小
的兩種數(shù)組。
固定大小數(shù)組聲明格式:
T[k] // T 為元素類型 k則是數(shù)組的大小
uint[7] arr;
address[50] address1;
動態(tài)大小數(shù)組聲明格式:
T[] //T為元素類型 由于是動態(tài)分配的 所以只需[]
unit[] arr2;
bytes1[] arr3;
address[] arr4;
bytes arr5; //注意 bytes本身就是數(shù)組
一個長度為 5,元素類型為 uint
的動態(tài)數(shù)組的數(shù)組(二維數(shù)組),應聲明為 uint[][5]
(注意這里跟其它語言比,數(shù)組長度的聲明位置是反的)。作為對比,如在Java中,聲明一個包含5個元素、每個元素都是數(shù)組的方式為 int[5][]
在Solidity中, X[3]
總是一個包含三個 X
類型元素的數(shù)組,即使 X
本身就是一個數(shù)組,這和其他語言也有所不同,比如 C 語言。
數(shù)組下標是從 0 開始的,且訪問數(shù)組時的下標順序與聲明時相反。
如果有一個變量為 uint[][5] memory x
, 要訪問第三個動態(tài)數(shù)組的第7個元素,使用 x[2][6]
,要訪問第三個動態(tài)數(shù)組使用 x[2]
。 同樣,如果有一個 T
類型的數(shù)組 T[5] a
, T 也可以是一個數(shù)組,那么 a[2]
總會是 T
類型。
數(shù)組元素可以是任何類型,包括映射或結(jié)構(gòu)體。對類型的限制是映射只能存儲在 存儲storage 中,并且公開訪問函數(shù)的參數(shù)需要是 ABI 類型。
狀態(tài)變量標記 public
的數(shù)組,Solidity創(chuàng)建一個 getter函數(shù) 。 小標數(shù)字索引就是 getter函數(shù) 的參數(shù)。
訪問超出數(shù)組長度的元素會導致異常(assert 類型異常 )。 可以使用 .push()
方法在末尾追加一個新元素,其中 .push()
追加一個零初始化的元素并返回對它的引用。
bytes
和 string
類型的變量是特殊的數(shù)組。 bytes
類似于 bytes1[]
,但它在 調(diào)用數(shù)據(jù)calldata 和 內(nèi)存memory 中會被“緊打包”(譯者注:將元素連續(xù)地存在一起,不會按每 32 字節(jié)一單元的方式來存放)。 string
與 bytes
相同,但不允許用長度或索引來訪問。
Solidity沒有字符串操作函數(shù),但是可以使用第三方字符串庫,我們可以比較兩個字符串通過計算他們的 keccak256-hash ,可使用 keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2))
和使用 string.concat(s1, s2)
來拼接字符串。
我們更多時候應該使用 bytes
而不是 bytes1[]
,因為Gas 費用更低, 在 內(nèi)存memory 中使用 bytes1[]
時,會在元素之間添加31個填充字節(jié)。 而在 存儲storage 中,由于緊密包裝,這沒有填充字節(jié)。 作為一個基本規(guī)則,對任意長度的原始字節(jié)數(shù)據(jù)使用 bytes
,對任意長度字符串(UTF-8)數(shù)據(jù)使用 string
。
可以將數(shù)組標識為 public
,從而讓 Solidity 創(chuàng)建一個 getter。 之后必須使用數(shù)字下標作為參數(shù)來訪問 getter。
創(chuàng)建數(shù)組規(guī)則
- 使用
new
在內(nèi)存(memory)中創(chuàng)建動態(tài)數(shù)組,需聲明長度,且在聲明后不能修改數(shù)組的大小
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.4.16;
contract Arr {
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)存(memory) 數(shù)組類型,它的基礎(chǔ)類型是由其中元素的類型決定。 例如,
[1, 2, 3]
的類型是uint8[3] memory
,因為其中的每個字面常數(shù)的類型都是uint8
。 正因為如此,有必要將上面這個例子中的第一個元素轉(zhuǎn)換成uint
類型。 目前需要注意的是,定長的 內(nèi)存memory 數(shù)組并不能賦值給變長的 內(nèi)存memory 數(shù)組
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.4.16;
contract C {
function f() public pure {
g([uint(1), 2, 3]);
}
function g(uint[3] _data) public pure {
// ...
}
}
// SPDX-License-Identifier: GPL-3.0
// 這段代碼并不能編譯。
pragma solidity ^0.4.0;
contract C {
function f() public {
// 這一行引發(fā)了一個類型錯誤,因為 unint[3] memory
// 不能轉(zhuǎn)換成 uint[] memory。
uint[] x = [uint(1), 3, 4];
}
}
數(shù)組成員
-
length
: 數(shù)組有一個包含元素數(shù)量的length
成員,memory
數(shù)組的長度在創(chuàng)建后是固定的。 -
push()
:動態(tài)數(shù)組
和bytes
擁有push()
成員,可以在數(shù)組最后添加一個0
元素。 -
push(x)
:動態(tài)數(shù)組
和bytes
擁有push(x)
成員,可以在數(shù)組最后添加一個x
元素。 -
pop()
:動態(tài)數(shù)組
和bytes
擁有pop()
成員,可以移除數(shù)組最后一個元素。
// SPDX-License-Identifier: GPL-3.0
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)體(struct)
Solidity 中的結(jié)構(gòu)體與 c 語言、golang 很相似,通過構(gòu)造結(jié)構(gòu)體來定義一種新類型。
// SPDX-License-Identifier: GPL-3.0
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; //這是 映射 后續(xù)會講到
}
uint numCampaigns;
mapping (uint => Campaign) campaigns; //這是 映射 后續(xù)會講到
function newCampaign(address beneficiary, uint goal) public returns (uint campaignID) {
campaignID = numCampaigns++; // campaignID 作為一個變量返回
// 創(chuàng)建新的結(jié)構(gòu)體示例,存儲在 storage 中。我們先不關(guān)注映射類型。
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)體的基礎(chǔ)概念。 結(jié)構(gòu)體類型可以作為元素用在映射和數(shù)組中,其自身也可以包含映射和數(shù)組作為成員變量。
盡管結(jié)構(gòu)體本身可以作為映射的值類型成員,但它并不能包含自身。 這個限制是有必要的,因為結(jié)構(gòu)體的大小必須是有限的。
注意在函數(shù)中使用結(jié)構(gòu)體時,一個結(jié)構(gòu)體是如何賦值給一個局部變量(默認存儲位置是 存儲(storage) )的。 在這個過程中并沒有拷貝這個結(jié)構(gòu)體,而是保存一個引用,所以對局部變量成員的賦值實際上會被寫入狀態(tài)。
當然,你也可以直接訪問結(jié)構(gòu)體的成員而不用將其賦值給一個局部變量,就像這樣, campaigns[campaignID].amount = 0
。文章來源:http://www.zghlxwxcb.cn/news/detail-821589.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-821589.html
到了這里,關(guān)于玩以太坊鏈上項目的必備技能(類型-引用類型-Solidity之旅三)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!