Solidity 教學: 記憶體配置 Memory Allocation

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

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

練習題

  • 練習題 1

    • 請觀察 0.8 和 0.7 版本分別如何處理 Overflow and Underflow?

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

contract UnderorOver {
    uint8 public myUint8;

    function decrement() public {
        myUint8 -= 1;
    }

    function increment() public {
        myUint8 += 1;
    }
}
  • 練習題 2

    • 如何使用一個智能合約來 Deploy 另一個智能合約?

 

a, b = b, a 可以在 python 成功把 a, b 兩個值交換,

在 Function 的章節提過同時賦值多個變數的方法,而這段程式碼在 Solidity 的運作會怎麼樣呢?

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

contract myContract {
    
    uint public a = 1;
    uint public b = 0;

    function swap() public{
        (a, b) = (b, a);
    }
}

new

我們可以透過 new 來宣告任何新的物件,前面我們也有嘗試過使用 new 關鍵字來動態記憶體配一個陣列,當然我們也可以以此創建一個合約。

Call by Reference vs. Call by Value

在 Solidity 中,函數的參數傳遞也和其他程式語言相同,有傳址(Call by Reference)和傳值(Call by Value)的區別:

  • 傳址(Call by Reference):代表我們真的把此變數地址傳入函數的參數中,在函式中進行運作時如果我們去改變這個參數的值,那離開函數後這個傳入的變數也會改變。

  • 傳值(Call by Value):在 Solidity 的編譯器中,會新建一個複製此參數的值的變數,然後傳入我們的函式中進行運算。也就是說,我們如果在這個函式中改變這個變數,在函數結束後,這個傳入的參數不會被影響。

變數型態的參數(bool, uint, bytes, addres)會是以傳值(Call by Value)傳入函數,而非變數型態像是我們之後會介紹到的資料結構(array, struct, mapping, string)們,則是傳址(Call by Reference)。

為了和狀態變數區辨,我們會習慣在函數的參數名稱前加上一個底線(_, underscore)。

Data Locations - Storage, Memory and Calldata

在 Solidity 裡面我們有三種宣告記憶體的方法:Storage, Memory 和 Calldata。

合約裡的 storage 會在合約建置時預先配置記憶體,如果是在函數裡面被宣告的記憶體不能被配置為 storage。相對的,memory 不能在合約建置時預先配置。

  • Storage 代表這個變數會永遠地儲存在區塊鏈中

  • Memory 變數是暫時存在的,在 external 函數在合約內被 call 並結束之後他們也會隨之被刪除

  • StorageMemory 的關係就像電腦的硬碟和 RAM

大多數的時候我們都不需要特別去敘述變數在記憶體的儲存類型,因為 Solidity 非常聰明會幫我們處理這些記憶體問題。

就像狀態變數(State variables)會預設為 Storage,永遠地儲存在區塊鏈上。而區域變數會預設以 memory 的形式儲存,在函數結束後就會消失。

calldata:一般只有 external 函數的參數(不包括返回參數)被強制指定為 calldata。這種變數是唯讀不可改變的,不會持久化到區塊鏈,效果類似 memory

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.11;
contract Test {
    function memoryTest(string memory _exampleString) public returns (string memory) {
    _exampleString = "example";  
    // 可以修改 memory // You can modify memory
    string memory newString = _exampleString; 
    // 可以在函式中使用 memory // You can use memory within a function's logic
    return newString;  
    // 可以回傳 memory // You can return memory
    }

    function calldataTest(string calldata _exampleString) external returns (string memory) {
    // 無法改變 _exampleString // cannot modify _exampleString
    // 也無法回傳 // but can return it
    return _exampleString;
    }
}

Storage Slot

在 Solidity 中的 Storage 記憶體配置模式是以 slot 為單位的,一個 slot 能容納的資料量為 32 bytes。我們可以把智能合約的記憶體想成是一個雜湊表(mapping),容量為 2^256 個 32-bytes 的 slots。

依照相對應的型別會有不同存入 slot 的規則。

 

練習題解答

  • 練習題解答 1

    • 請觀察 0.8 和 0.7 版本分別如何處理 Overflow and Underflow?

版本不同會有不同的整數溢位錯誤:

  1. Solidity 版本 < 0.8: 不會出現任何 overflow / underflow 錯誤
  2. Solidity 版本 >= 0.8: 會出現 overflow / underflow 錯誤
  3. 我們應該要使用 SafeMath 來避免運算時的 overflow 和 underflow

 

  • 練習題解答 2

    • 如何使用一個智能合約來 Deploy 另一個智能合約?

如果要佈署另一個合約的話從底層的角度你得讓 Deployer 知道 ABI 跟 bytecode,但在 Solidity 裡面用 new 跟賦值的方式就可以省掉中間複製貼上的部分。

contract A { // ...

}
contract DeployA {

  function Deploy() public {
    mapping(address => address) addr2Contract; 
    address contractAaddress = new A();            addr2Contract[msg.sender] = contractAaddress ;
  } 
}

/*

Reference:
https://docs.soliditylang.org/en/v0.8.9/contracts.html#creating-contracts

*/