引用類型可以通過多個不同的名稱修改它的值,而值類型的變數,每次都是獨立的, 因此必須比值類型更謹慎地處理引用類型。 目前,引用類型包括結構,陣列和映射,如果使用引用類型,則必須明確指明數據存儲哪種類型的位置(空間)里:
所有的引用類型,如陣列和結構體類型,都有一個額外註解 ,來說明數據存儲位置。 有三種位置: 記憶體memory 、 存儲storage 以及 調用數據calldata 。調用數據calldata 是不可修改的、非持久的函數參數存儲區域,效果大多類似記憶體memory 。
調用數據calldata 是外部函數的參數所必需指定的位置,但也可以用於其他變數。
數據位置不僅僅表示數據如何保存,它同樣影響著賦值行為:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
//import part...
contract Azuki is Ownable, ERC721A, ReentrancyGuard {
uint256 public immutable maxPerAddressDuringMint;
//...
constructor(
uint256 maxBatchSize_,
uint256 collectionSize_,
uint256 amountForAuctionAndDev_,
uint256 amountForDevs_
) ERC721A("Azuki", "AZUKI", maxBatchSize_, collectionSize_) {
maxPerAddressDuringMint = maxBatchSize_;
// ...
}
//...
function auctionMint(uint256 quantity) external payable callerIsUser {
//...
require(
numberMinted(msg.sender) + quantity <= maxPerAddressDuringMint,
"can not mint this many"
);
//...
}
//...
}
首先我們可以看到第一行先宣告了 maxPerAddressDuringMint
的資料位置是storage(並且資料類型為...),接著我們希望這個資料能夠在部署合約時,將 maxBatchSize_
參數存入這個storage插槽,以讓我們在合約中使用這個參數的值
以函數 auctionMint
為例,在Mint之前我們希望可以確認用戶Mint後所持有的NFT不會超過我們設想的值(也就是maxBatchSize_
)
而我們也可以看到 Azuki.sol
中有些地方使用的是memory而非storage,他們之間的差別除了上述storage是持久的、memory是臨時保存值(也就表示函數調用之間會被擦除),還有就是使用storage會比較昂貴
陣列可以在聲明時指定長度,也可以動態調整大小(長度)
元素類型為T
,固定長度為的陣列可以聲明為k
,而動態陣列聲明為 T[]
。 舉個例子,一個長度為5,元素類型為uint
的動態數位的陣列(二維數位),應聲明為 uint[][5]
陣列下標是從 0 開始的,且存取陣列時的下標順序與聲明時相反,例如:如果有一個變數為 uint[][5] memory x
, 要存取第三個動態陣列的第二個元素,使用 x[2][1]
,要訪問第三個動態數位使用 x[2]
public
的陣列,能使用Solidity 建立 getter函數 ,而數字索引就是 getter函數的參數。.push()
方法在末尾追加一個新元素,其中 .push()
追加一個零初始化的元素並返回對它的引用bytes
和strings
也是陣列bytes
和string
類型的變數是特殊的陣列。bytes
類似於 byte[]
,但它在calldata 和memory 中會被"緊打包"(將元素連續地存在一起,不會按每 32 位元組一單元的方式來存放)。string
與相同bytes
,但不允許用長度或索引來訪問。
Solidity沒有字串操作函數,但是可以使用第三方字串庫(許多人使用 Opensea 的 String.sol
),我們可以比較兩個字符串通過計算他們的 keccak256-hash (keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2))
),可使用 abi.encodePacked(s1, s2)
或者使用 string.concat(s1, s2)
來拼接字符串。
我們更多時候應該使用 bytes
而不是 byte[]
,因為Gas費用更低, 會在元素之間添加31個填充位元組
如果使用一個長度限制的位元組陣列,應該使用一個bytes1
到bytes32
的具體類型,因為它們便宜得多。
可使用 關鍵字在 new
記憶體memory 中基於運行時創建動態長度陣列。 與storage 陣列相反的是,你不能通過修改成員變數 改變 .push
記憶體memory 陣列的大小。
必須提前計算所需的大小或者創建一個新的記憶體陣列並複製每個元素
pragma solidity >=0.4.16 <0.9.0;
contract TX {
function f(uint len) public pure {
uint[] memory a = new uint[](7);
bytes memory b = new bytes(len);
assert(a.length == 7);
assert(b.length == len);
a[6] = 8;
}
}
陣列字面常量是在方括弧中( [...]
) 包含一個或多個逗號分隔的運算式。 例如 [1, a, f(3)]
必須有一個所有元素都可以隱式轉換到普通的類型,這個類型就是陣列的基本類型
在下面的例子中,[1, 2, 3]
的類型是 uint8[3] memory
。 因為每個常量的類型都是uint8
,如果你希望結果是 uint [3] memory
類型,你需要將第一個元素轉換為 uint
目前需要注意的是,定長的 記憶體memory 陣列並不能賦值給變長的memory 陣列,以下面為例,這會引發錯誤因為 uint[3] memory
不能轉換成 uint[] memory
pragma solidity >=0.4.0 <0.9.0;
contract LBC {
function f() public {
// here
uint[] x = [uint(1), 3, 4];
}
}
length
變數表示當前陣列的長度。 一經創建,記憶體memory 陣列的大小就是固定的(但卻是動態的,也就是說,它可以根據運行時的參數創建)。bytes
類型( string
類型不可以)都有一個 push()
的成員函數,它用來添加新的零初始化元素到陣列末尾,並傳回元素引用. 因此可以這樣:x.push().t = 2
或 x.push() = b
bytes
類型( string
類型不可以)都有一個 push(x)
的成員函數,用來在陣列末尾添加一個給定的元素,這個函數沒有返回值bytes
類型( string
類型不可以) 都有一個pop()
的成員函數, 它用來從陣列末尾刪除元素。 同樣的會在移除的元素上隱含呼叫 delete陣列切片是陣列連續部分的檢視,用法如:x[start:end]
,start
和 end
是 uint256 類型(或結果為 uint256 的運算式),第一個元素是 x[start]
, 最後一個元素是 x[end - 1]
如果 start
比 end
大或者 end
比陣組長度還大,將會拋出異常
start
和 end
都可以是可選的: 預設是 0, 而end
預設是陣列長度
陣列切片沒有任何成員。 它們可以隱式轉換為其「背後」類型的陣列,並支援索引訪問。
索引訪問也是相對於切片的開始位置。 陣列切片沒有類型名稱,這意味著沒有變數可以將陣列切片作為類型,它們僅存在於中間表達式中。
簡單用法如下:
bytes exampleBytes = '0xabcd'
exampleBytes[2:5]; # 'abc'
exampleBytes[:5]; # '0xabc'
exampleBytes[2:]; # 'abcd'
exampleBytes[:]; # '0xabcd'
雖然 Azuki.sol
沒有使用到陣列切片,但陣列切片在 ABI解碼數據的時候非常有用,如:
pragma solidity >=0.6.99 <0.9.0;
contract Proxy {
address client;
constructor(address _client) {
client = _client;
}
function forward(bytes calldata _payload) external {
// Forward call to "setOwner(address)" that is implemented by client
// after doing basic validation on the address argument.
bytes4 sig =
_payload[0] |
(bytes4(_payload[1]) >> 8) |
(bytes4(_payload[2]) >> 16) |
(bytes4(_payload[3]) >> 24);
if (sig == bytes4(keccak256("setOwner(address)"))) {
address owner = abi.decode(_payload[4:], (address));
require(owner != address(0), "Address of owner cannot be zero.");
}
(bool status,) = client.delegatecall(_payload);
require(status, "Forwarded call failed.");
}
}
Solidity 支援透過構造結構體的形式定義新的類型,以下是 Azuki.sol
中使用結構體的範例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// import ...
contract Azuki is Ownable, ERC721A, ReentrancyGuard {
//在合約內部定義結構體,使它僅在此合約和衍生合約中可見
struct SaleConfig {
uint32 auctionSaleStartTime;
uint32 publicSaleStartTime;
uint64 mintlistPrice;
uint64 publicPrice;
uint32 publicSaleKey;
}
SaleConfig public saleConfig;
//...
function auctionMint(uint256 quantity) external payable callerIsUser {
uint256 _saleStartTime = uint256(saleConfig.auctionSaleStartTime);
//...
}
}
在 Azuki.sol
中並沒有太複雜的使用結構體,這邊只是想把拍賣相關的資訊都存進 SaleConfig
中,以方便各個函數調用,如果想更深入了解結構體的使用方式可以參考 Solidity 官方文檔中的眾籌合約:Remix - Ethereum IDE