Solidity 教學: 資料結構 Data Structures

本篇預計閱讀時間 15 分鐘, 實作時間 30 分鐘。

在開始之前先來思考底下的練習題,閱讀後即可練習完成。

練習題

  • 練習題 1

    • 如何利用迴圈找出陣列 [34, 231, 4534, 231, 23, 45, 80, 67798, 67783] 的最小值與最大值。

  • 練習題 2

    • 如何利用迴圈找出 [34, 231, 4534, 231, 23, 45, 80, 67798, 67783] 有幾個奇數。

  • 練習題 3

    • 如何利用迴圈找出 [2, 256, 4534, 1024, 23, 45, 0, 2048, 67783, 8, 64] 有幾個為 2 的正整數次方。

  • 練習題 4

    • 如何利用 mapping 建立一個名為 whitelist 的容器,並儲存三筆 address 映射 bool 資料。

  • 練習題 5

    • 小明想要架設一個交易多餘電力的合約,在合約中宣告一個 struct 表示當前加入此電力系統的會員們,並在內宣告一個 mapping 需要由 address 映射 uint,代表他們當前擁有可販賣的多餘電力。

  • 練習題 6

    • 承上題,宣告一個 enum 分別代表三種會員電力系統的狀態:malfunction, insufficient, surplus。

  • 練習題 7

    • 承上題,在練習題 5 的 struct 中再宣告一個 mapping 由 address 映射 uint,用於儲存每位會員的電力系統狀態。

Array

一個程式語言中最經典的資料結構莫過於 array 了,陣列的性質如下:

  1. 擁有兩個成員:.length, .push(element)

  2. 可為固定或動態的長度,取決於我們是否宣告陣列的長度。

  3. T[][5] 是五個動態陣列長度的組合(reverse notation)


 

pragma solidity ^0.8.11;
contract MyArray {

    uint256[] arr_1;

    uint256[] arr_2 = [3,2,4];

    uint256[5] arr_3;

    uint[][] array2D = [ [1,2,3], [4,5,6] ];

    function getValueOfIndex(uint256 _index) public view returns (uint256) {

return arr_2[_index];

}

// 在一個陣列的最後加入一個元素 

    function addToArray(uint256 _value) public  {arr_2.push(_value);

}

// 調用一個陣列的長度

    function valueCount() public view returns(uint) {return arr_3.length;}}

動態記憶體配置


uint[] memory arr = new uint[](3);

如果我們訪問了超過記憶體的值,Solidity 不會向其他語言一樣丟 Runtime Error 回來,而是會返回該型態的預設值。那我們所謂的非法訪問其實是訪問一個負的 index。

Mapping

mapping 在 Solidity 的運作就像是 python 的 dict 或常見的資料結構 hash-map。

圖 6-1 雜湊表

在圖 6-1 中的例子,當我們 call "k1" 的時候,這個 mapping 就會回傳 key "k1" 指向的值,也就是 "AAA,BBB,CCC"。此外,我們還可以儲存新的資料進去 mapping

mapping(uint => string) public names;

在以上這段語法中,每一個 key 在這個名為 namesmapping 中,是以 uint 形態存在。而相對應的每個 value 型態為 string

宣告語法為:mapping(_keyType => _valueType) name;

  • _keyType 可以是任何的元素型態,也就是說它可以為任何的既有數值型態包含 bytes string

  • _valueType 可以是任何的型態,包含複雜的資料結構,甚至是 mapping

Mapping具有以下的特色:

  • 所有元素都會被初始化

  • 不存在長度,如果需要紀錄長度得額外宣告一個變數

  • mapping 的 Public state variables 會成為Getter Function

  • 如果需要可遍歷的 mapping 可以藉由導入一些 libraries 達到。


 

pragma solidity ^0.8.11;

contract MyMappings {mapping(uint256 => address) nfts;
uint256 counter = 0;



    function getOwnerOfNFT(uint256 _id) public view returns (address) {

        return nfts[_id];

}

    function mintNFT() public {

        nfts[counter] = msg.sender;counter++;

    }

}

如同二維陣列,我們也可以宣告 Nested mapping 來做為類似的用途:

mapping(address => mapping(address => bool)) a; 

既然用途很像,我們什麼時候用 array,什麼時候用 mapping 呢?

我會用使用情況來區分我們要使用哪種資料結構:

  • 當常常需要遍歷(iterate)這個資料結構時,建議使用 array

  • 如果我們需要常常查看某些特定的鍵值(key)時,建議使用 mapping

需要注意的是,我們沒辦法使用動態記憶體配置來宣告 mapping。同時 mapping 也不具有可以直接呼叫長度的成員函式。

Structs

宣告 struct 在這裡的用途和 C 的語言一樣,如果是沒有學過 C/C++ 的話,可以把 struct 理解成一種比較低階的 C++, python 的 class

定義一個 struct 之後會產生一種新的自定義、抽象、複雜的的資料型態。

我們就可以藉由這個資料型態宣告我們自己的結構變數。

struct User {
    address id;
    string name;
}
//Method 1 依照順序宣告 (argument order matters)

User("0xAio90....", "Mike");

//Method 2 註明則不必按照順序宣告 (argument order does not 
matter)

User({name: "Mike", id: "0xAio90...."});

就像是我們定義一個新的形態是車子,並且定義一台車子裡面要有的資料有:持有者(address)、品牌(string)、公升數(uint)。

此後就可以宣告一個資料型態為車子的變數 myCar,並定義這個變數裡面的每個型態。

struct Car {
    address owner;
    string brand;
    uint cc;}

myCar = Car({string: "Toyota", address: "0xAio90....", cc: 2000});

Structs 具有以下的性質:

  • 在 Solidity 中不可以使用遞迴性的方式宣告變數,也就是說 struct 的成員的型別不可是該 struct

  • 以 structs 取代 objects 可以達到 gas consumption

  • 可以組合 struct mapping 或甚至其他容器來建置些複雜的資料結構,像是以下例子是使用巢狀的資料結構(Nested Data Structures)來達成目的。

struct User {
    uint id;
    string name;
}

uint[] userIds;

mapping(uint => User) users;​

 

Enum

枚舉(enum)和 struct 非常類似,只是 enum 中的每個參數會自動從 0 開始依序初始化。

Enum 具有以下的性質:

  • 內部已有整數型態的元素存在

  • 可以存進 uint8 的型態,如果數值數量介於 0~255,若大於 256 個值則可宣告為 uint16

enum ActionChoices { GoLeft, GoRight, GoStraight, SitsStill }ActionChoices choice;
ActionChoices constant defaultChoice = ActionChoices.Gostraight;

在 Solidity 中 enum 更常被用來進行模型選擇(model choice),或者追蹤狀態(track of state)。

pragma solidity ^0.8.11;

contract MyEnums {

    enum Rarity {
        original, // 0
        rare, // 1
        super_rare // 2
}
    Rarity public rarity;

    constructor() {
        rarity = Rarity.rare;
}
    // 我們可以直接將一個型態為該 enum 的變數賦值為某特定屬性
    function makeSuperRare() public {
        rarity = Rarity.super_rare;
}

    // 我們可以藉由 enum 初始化的 INDEX 來進行賦值
    function set(Rarity _rarity) public {
       rarity = _rarity;}

    // 我們可以藉由 delete 來重置這個 enum 成他的初始值 0function reset() public {delete rarity;
    }
}

structenum 的差別:

  • Struct 用於表示一個複雜變數

  • Enum 用於表示一個單一變數的不同狀態或值

練習題解答

  • 練習題解答 1

    • 利用迴圈找出陣列 [34, 231, 4534, 231, 23, 45, 80, 67798, 67783] 的最小值與最大值。

提供 2 個方法來取最大值與最小值,分別是根據型別的定義和 type().min 或 .max 這兩者。

pragma solidity ^0.8.11; 

contract Practice{

    uint[] array = [34, 231, 4534, 231, 23, 45, 80, 67798, 67783];

    function getMin() public view returns(uint){
        uint min = 2**256 - 1;
        for(uint i = 0; i < array.length; i++){
          if(array[i] < min){
              min = array[i];
          } 
    }
return min; 
}

function getMax() public view returns(uint){ 
    uint max = type(uint256).min;
    // for(uint i = 0; i < array.length; i++){

    //   if(array[i] > max){
    //          max = array[i];
            
    //   }

        //}

  • 練習題解答 2

    • 利用迴圈找出 [34, 231, 4534, 231, 23, 45, 80, 67798, 67783] 有幾個奇數。

pragma solidity ^0.8.11; 

contract Practice{

    uint[] array = [34, 231, 4534, 231, 23, 45, 80, 67798, 67783];

    function getOdd() public view returns(uint){ 
    uint cnt = 0;

    for(uint i = 0; i < array.length; i++){ 
        if(array[i] % 2 != 0){
           cnt++; 
        }

    }

    return cnt; 

    }
}
  • 練習題解答 3

    • 利用迴圈找出 [2, 256, 4534, 1024, 23, 45, 0, 2048, 67783, 8, 64] 有幾個為 2 的正整數次方。

pragma solidity ^0.8.11; 

contract Practice{

uint[] array = [2, 256, 4534, 1024, 23, 45, 0, 2048, 67783, 8, 64];

function getEven() public view returns(uint){ 
    uint cnt = 0;
    for(uint i = 0; i < array.length; i++){ 
        if(array[i] % 2 == 0){
            cnt++; 
         }
    }
    return cnt; 
    }
}

這個例子跟取得偶數的函式是一樣的。

  • 練習題解答 4

    • 利用 mapping 建立一個名為 whitelist 的容器,並儲存三筆 address 映射bool 資料。

pragma solidity ^0.8.11; 

contract Practice{

mapping(address => bool) public whitelist; 

}
  • 練習題解答 5

    • 小明想要架設一個交易多餘電力的合約,在合約中宣告一個 struct 表示當前加入此電力系統的會員們,並在內宣告一個 mapping 需要由 address映射 uint,代表他們當前擁有可販賣的多餘電力。

  • 練習題解答 6 與 7

    • 宣告一個 enum 分別代表三種會員電力系統的狀態:malfunction, insufficient, surplus。

    • 在我們 Practice 5 的 struct 中再宣告一個 mapping 由 address 映射 uint,用於儲存每位會員的電力系統狀態。

pragma solidity ^0.8.11; 

contract Practice{

    enum status {malfunction, insufficient, surplus}

    struct System {
    mapping(address => bool) whitelist; 
    mapping(address => uint) electricity; 
    mapping(address => status) UserStatus;
    }
}