《Solidity 教程》引用類型

引用類型可以通過多個不同的名稱修改它的值,而值類型的變數,每次都是獨立的, 因此必須比值類型更謹慎地處理引用類型。 目前,引用類型包括結構,陣列和映射,如果使用引用類型,則必須明確指明數據存儲哪種類型的位置(空間)里:

  • 記憶體memory : 在記憶體中的數據,因此數據僅在其生命週期內(函數調用期間)有效,且不能用於外部調用。
  • 存儲storage : 狀態變數保存的位置,只要合約存在就一直存儲
  • 調用數據calldata : 用來保存函數參數的特殊數據位置,是一個唯讀位置。

數據位置 Data location

所有的引用類型,如陣列和結構體類型,都有一個額外註解 ,來說明數據存儲位置。 有三種位置: 記憶體memory 、 存儲storage 以及 調用數據calldata 。調用數據calldata 是不可修改的、非持久的函數參數存儲區域,效果大多類似記憶體memory 。

調用數據calldata 是外部函數的參數所必需指定的位置,但也可以用於其他變數。

數據位置與賦值行為 Data location and assignment behaviour

數據位置不僅僅表示數據如何保存,它同樣影響著賦值行為:

  • 在storage和memory之間兩兩賦值(或者從調用數據賦值 ),都會創建一份獨立的拷貝
  • 從memory到memory的賦值只創建引用, 這意味著更改記憶體變數,其他引用相同數據的所有其他記憶體變數的值也會跟著改變。
  • 從存儲storage到本地存儲變數的賦值也只分配一個引用。
  • 其他的向 storage的賦值,總是進行拷貝。 這種情況的示例如對狀態變數或storage的結構體類型的局部變數成員的賦值,即使局部變數本身是一個引用,也會進行一份拷貝
// 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會比較昂貴

陣列 Arrays

陣列可以在聲明時指定長度,也可以動態調整大小(長度)

元素類型為T ,固定長度為的陣列可以聲明為k,而動態陣列聲明為 T[]。 舉個例子,一個長度為5,元素類型為uint的動態數位的陣列(二維數位),應聲明為 uint[][5]

陣列下標是從 0 開始的,且存取陣列時的下標順序與聲明時相反,例如:如果有一個變數為 uint[][5] memory x, 要存取第三個動態陣列的第二個元素,使用 x[2][1],要訪問第三個動態數位使用 x[2]

  • 陣列元素可以是任何類型,包括映射或結構體。 對類型的限制是映射只能存儲在storage 中,並且公開訪問函數的參數需要是ABI類型
  • 狀態變數標記public的陣列,能使用Solidity 建立 getter函數 ,而數字索引就是 getter函數的參數。
  • 訪問超出陣列長度的元素會導致異常(assert 類型異常 )。 可以使用 .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個填充位元組

如果使用一個長度限制的位元組陣列,應該使用一個bytes1bytes32的具體類型,因為它們便宜得多。

創建記憶體陣列

可使用 關鍵字在 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;
    }
}

陣列字面常量 Array Literals

陣列字面常量是在方括弧中( [...]) 包含一個或多個逗號分隔的運算式。 例如 [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];
    }
}

陣列成員 Array Members

  • **length:**陣列有成員length變數表示當前陣列的長度。 一經創建,記憶體memory 陣列的大小就是固定的(但卻是動態的,也就是說,它可以根據運行時的參數創建)。
  • **push():**動態的storage陣列以及bytes類型( string類型不可以)都有一個 push()的成員函數,它用來添加新的零初始化元素到陣列末尾,並傳回元素引用. 因此可以這樣:x.push().t = 2x.push() = b
  • **push(x):**動態的storage陣列以及bytes類型( string類型不可以)都有一個 push(x)的成員函數,用來在陣列末尾添加一個給定的元素,這個函數沒有返回值
  • **pop:**動態的 storage 陣列以及bytes類型( string類型不可以) 都有一個pop() 的成員函數, 它用來從陣列末尾刪除元素。 同樣的會在移除的元素上隱含呼叫 delete

陣列切片 Array Slices

陣列切片是陣列連續部分的檢視,用法如:x[start:end]startend是 uint256 類型(或結果為 uint256 的運算式),第一個元素是 x[start], 最後一個元素是 x[end - 1]

如果 startend大或者 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.");
    }
}

結構體 Structs

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

Subscribe to Samumu.eth
Receive the latest updates directly to your inbox.
Verification
This entry has been permanently stored onchain and signed by its creator.