《Solidity 教程》值類型

Solidity 是一種靜態類型語言,這意味著每個變數(狀態變數和局部變數)都需要在編譯時指定變數的類型

Solidity 提供了幾種基本類型,並且基本類型可以用來組合出複雜類型。除此之外,類型之間可以在包含運算符號的表達式中進行交互, undefinednull 值的概念在Solidity中不存在,但是新聲明的變數總是有一個 默認值,具體的預設值跟類型相關。 要處理任何意外的值,應該使用 錯誤處理來恢復整個交易,或者返回一個帶有第二個 bool 值的元組表示成功

類型可以說是所有合約都會使用到的基本概念,他非常簡單也經常被使用,各位讀者請確保自己牢記這些簡單的知識點

運算子

下列運算子是經常使用到的,每個的用法都必須搞清楚

  • !(邏輯非)

  • &&(邏輯與, "and" )

  • ||(邏輯或, "or" )

  • 比較運算子: <=<==!=>=>( 傳回布林值 )

  • 算數運算子: +-*/%( 取餘或叫模運算 ) ** (指數)

  • 位運算子: &|^( 異或)~ ( 位取反 )

  • 移位運算子: <<(左移位),>>(右移位)

而關於Solidity的運算溢出,可以參考此篇解析

  • 解析 SafeMath 合約

Booleans

bool : 可能的取值為字面常量值 truefalse

Integers

int / uint :分別表示有符號和無符號的不同位數的整型變數。 支援關鍵字 uint8uint256,以 8 位為遞增

uintint 分別表示 uint256int256

uint256 有最大值 2^256-1 以及最小值 0

int256 有最大值 2^255-1 以及最小值 -2^255

Function Types

函數類型是一種表示函數的類型。 可以將一個函數賦值給另一個函數類型的變數,也可以將一個函數作為參數進行傳遞,還能在函數調用中返回函數類型變數。 函數類型有兩類:

內部(internal) 函數類型外部(external) 函數類型

  • 內部函數只能在當前合約內被調用,更具體來說,在當前合約內,包括內部庫函數和繼承的函數中

  • 外部函數由一個位址和一個函數簽名組成,可以通過外部函數調用傳遞或者返回。

函數類型表示成如下的形式

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

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

函數類型預設是內部函數,因此不需要聲明 internal 關鍵字。

💡 請注意,這僅適用於函數類型,合約中定義的函數明確指定可見性,它們沒有預設值。

如果當函數類型的變數還沒有初始化時就調用它的話會引發一個 Panic 異常。 如果在一個函數被delete之後調用它也會發生相同的情況。

請注意,當前合約的 public 函數既可以被當作內部函數也可以被當作外部函數使用。 如果想將函式當作內部函數使用,就用f呼叫,如果想將其當作外部函數,使用 this.f

成員方法:

public(或 external)函數都有下面的成員:

View, Pure, Payable

  • View 函數是只讀函數***,不會修改區塊鏈的狀態。***換句話說,如果你想從區塊鏈中讀取數據,可以使用 View 函數

  • Pure 函數比 View 函數更具限制性,***並且不修改狀態,也不讀取區塊鏈的狀態。***換句話說,他是純粹幫助運算、執行其他函數的函數

  • Payable 函數的話請記住以下幾點

    • 在函數或狀態變數中使用payable來發送和接收乙太幣

    • 在狀態變數中包含payable,以便從合同中退出

    • 在構造函數中包含payable,以便在創建/部署合約時能夠存入合約

    • 在函數中包含payable,以允許將存款存入合同

內部函數使用例子

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Azuki is Ownable, ERC721A, ReentrancyGuard {
    //...
    string private _baseTokenURI;

  function _baseURI() internal view virtual override returns (string memory) {
    return _baseTokenURI;
  }
    //...
}

這個函數非常簡單就是將原本的函數 _baseURI() 從回傳空值覆寫成回傳 _baseTokenURI ,並且因為他被宣告為 internal ,所以理論上我們可以在合約找到這個函數被誰使用,在 ERC721A.sol 中:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract ERC721A is
  Context,
  ERC165,
  IERC721,
  IERC721Metadata,
  IERC721Enumerable
{
    //...
    function tokenURI(uint256 tokenId)
    public
    view
    virtual
    override
    returns (string memory)
  {
    require(
      _exists(tokenId),
      "ERC721Metadata: URI query for nonexistent token"
    );

    string memory baseURI = _baseURI();
    return
      bytes(baseURI).length > 0
        ? string(abi.encodePacked(baseURI, tokenId.toString()))
        : "";
  }

    function _baseURI() internal view virtual returns (string memory) {
    return "";
  }
    //...
}

首先看到函數 _baseURI() 他確實在宣告時帶有 internal & virtual 兩個性質,也印證了上面的說法,接著,看到函數 tokenURI() 他會先確認 tokenId 是否存在,如果存在的話就會回傳 tokenURI (注意到他帶有 view 表示該函數不會修改區塊鏈的狀態),裡面用到的 abi.encodePacked 會在後面內容中解釋

外部函數使用例子

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

contract Azuki is Ownable, ERC721A, ReentrancyGuard {
    //...
    function allowlistMint() external payable callerIsUser {
    uint256 price = uint256(saleConfig.mintlistPrice);
    require(price != 0, "allowlist sale has not begun yet");
    require(allowlist[msg.sender] > 0, "not eligible for allowlist mint");
    require(totalSupply() + 1 <= collectionSize, "reached max supply");
    allowlist[msg.sender]--;
    _safeMint(msg.sender, 1);
    refundIfOver(price);
  }
    //...
}

外部函數如上述定義所描述,通常不會是合約內部去使用它,而是提供外面的使用者跟合約互動的函數,以函數 allowlistMint() 為例,前面三個 require 先檢查了使用者使否有資格 mint,然後再去呼叫 _safeMint() 來讓使用者鑄造NFT,是很常見的白單公售NFT的函數(這個函數看他的功能就應該能知道他是 payable ,因為他需要添加新的鏈上狀態,紀錄這個使用者鑄造了一個NFT)

Contract Types

每一個contract定義都有他自己的類型

如果聲明一個合約類型的局部變數(MyContract c),則可以調用該合約的函數。

  • 注意需要賦相同合約類型的值給它

合約特性:

  • 可以實例化合約(即新創建一個合約物件),參考 『使用new創建合約』

  • 合約和address的數據表示是相同的, 參考 ABI

  • 合約不支援任何運算符

  • 合約類型的成員是合約的外部函數及 public 的 狀態變數

  • 對於合約 C可以使用type(C)取得合約的類型資訊,參考 類型資訊

Address

位址類型有兩種形式,他們大致相同:

  • address:保存一個20位元組的值(乙太坊位址的大小)

  • address payable :可支付位址,與 address相同,不過有成員函數 transfersend

地址類型成員變數

查看所有的成員,可參考 地址成員

balance : 查詢一個地址的餘額

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

contract Azuki is Ownable, ERC721A, ReentrancyGuard {
    //...
    function withdrawMoney() external onlyOwner nonReentrant {
    (bool success, ) = msg.sender.call{value: address(this).balance}("");
    require(success, "Transfer failed.");
  }
    //...
}

transfer : 向一個可支付位址(payable address)發送乙太幣Ether (以 wei 為單位)

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

contract Azuki is Ownable, ERC721A, ReentrancyGuard {
    //...
    function refundIfOver(uint256 price) private {
    require(msg.value >= price, "Need to send more ETH.");
    if (msg.value > price) {
      payable(msg.sender).transfer(msg.value - price);
    }
  }
    //...
}

💡 如果當前合約的餘額不夠多,則`transfer`函數會執行失敗,或者如果乙太轉移被接收帳戶拒絕, `transfer`函數同樣會失敗而進行回退

call : 是與其他合約交互的低級函數,當你只是通過調用函數發送乙太幣時要使用的推薦方法,但是,這不是調用現有函數的推薦方法

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

contract Azuki is Ownable, ERC721A, ReentrancyGuard {
    //...
    function withdrawMoney() external onlyOwner nonReentrant {
    (bool success, ) = msg.sender.call{value: address(this).balance}("");
    require(success, "Transfer failed.");
  }
    //...
}

上述的函數 withdrawMoneymsg.sender.call 就是要領出合約中的ETH,不過他繼承自 onlyOwnernonReentrant 所以想要呼叫這函數會有限制,之後再作詳盡的解釋

  • delegatecall : 使用起來並不容易,錯誤的用法或不正確的理解會導致毀滅性的結果,詳細使用方式可以參考這篇

💡 不管是讀取狀態還是寫入狀態,最好避免在合約代碼中硬編碼使用的 gas 值。 這可能會引入"陷阱",而且 gas 的消耗也是可能會改變的

Fixed-size byte array

種類有以下幾種:bytes1bytes2, ...bytes32

除了第一部分講到的通用運算子,他能進行索引訪問:

  • 索引訪問:如果xbytesI 類型,那麼 x[k](其中0 <= k < I)返回第k個字節(只讀)

成員變數:

  • .length

    表示這個位元組陣組的長度(只讀).

Dynamically-sized byte array

  • bytes: Dynamically-sized ****位元組陣列

  • string: Dynamically-sized UTF-8 編碼字串類型

  • 以上可參閱 陣列 他們不是值類型

Enums

枚舉是在Solidity中創建使用者定義類型的一種方法。 它們是顯示所有整型相互轉換,但不允許隱式轉換。 從整型顯式轉換枚舉,會在運行時檢查整數時候在枚舉範圍內,否則會導致異常( Panic異常 )。 枚舉需要至少一個成員,預設值是第一個成員,枚舉不能多於256個成員。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.8;

contract test {
    enum ActionChoices { GoLeft, GoRight, GoStraight, SitStill }
    ActionChoices choice;
    ActionChoices constant defaultChoice = ActionChoices.GoStraight;

    function setGoStraight() public {
        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.
    function getChoice() public view returns (ActionChoices) {
        return choice;
    }

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

    function getLargestValue() public pure returns (ActionChoices) {
                //get max value in enums
        return type(ActionChoices).max;
    }

    function getSmallestValue() public pure returns (ActionChoices) {
                //get max value in enums
        return type(ActionChoices).min;
    }
}