繼承多態與庫合約

繼承多態與庫合約

學習目標

  • 掌握 Solidity 繼承機制與多態
  • 理解 C3 線性化算法
  • 掌握庫合約(library)的定義和使用

繼承基礎

繼承定義

  • 使用 is 關鍵字
  • 繼承的實現方式是代碼拷貝:部署後變成一個合約

可見性與繼承

  • private:子合約不可見,但不能定義同名成員
  • internal:子合約可見
  • public:完全可見
  • event 和 modifier 重名也會衝突
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.2 <0.9.0;

contract Base {
    string private name;
    event MyEvent();
    modifier myMod() {
        _;
    }
    function foo() private {}
}

contract ContractA is Base {
    string private name; // private 可以重名(不可見但存在衝突)

    // function foo() private {} // 即使 private 也不能同名
}

構造函數與繼承

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.2 <0.9.0;

abstract contract Base {
    constructor(string memory _name) {}
}

contract ContractA is Base {
    string name;
    constructor() Base(name) {}
}

多態(Polymorphism)

virtual 與 override

  • 父合約函數標記 virtual 表示可被覆蓋
  • 子合約函數用 override 進行覆蓋
  • abstract 合約可以有未實現的函數

覆蓋規則

  • 如果從繼承路徑中繼承了同一函數的不同版本,必須覆蓋
  • override 需要列出所有被覆蓋的父合約

C3 線性化

爲什麼需要線性化

  • 多重繼承中,合約間的繼承關係構成圖結構
  • 需要確定:存儲佈局堆疊順序、構造函數執行順序、super 指向誰

線性化規則

  • 父在子前
  • 兄在弟前(從右到左遍歷)
  • 結果確定(deterministic)

算法描述

  1. 從當前合約開始找父合約,從右向左遍歷
  2. 如果某合約的所有子合約都已遍歷過,該合約入列;否則回退找兄弟
  3. 到根合約結束遍歷,輸出並倒置

構造函數執行順序 = 線性化順序

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.2 <0.9.0;

contract A {
    uint public x = 5;
    constructor() {}
}

contract B is A {
    constructor() {
        x += 10;
    }
}

contract C is A {
    constructor() {
        x *= 10;
    }
}

contract D is C, B {
    // 線性化:D → B → C → A
    // 構造順序:A → C → B → D
    // 計算過程:x=5 → x*=10=50 → x+=10=60
    // 最終 x = 60
}

super 關鍵字

  • super 指向線性化序列中的前驅合約(不是直接父合約)
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.2 <0.9.0;

contract A {
    event Log(string message);

    function bar() public virtual {
        emit Log("A.bar called");
    }
}

contract B is A {
    function bar() public virtual override {
        emit Log("B.bar called");
        super.bar(); // 調用 C.bar()(不是 A.bar!)
    }
}

contract C is A {
    function bar() public virtual override {
        emit Log("C.bar called");
        super.bar(); // 調用 A.bar()
    }
}

contract D is B, C {
    // 線性化:D → C → B → A
    function bar() public override(B, C) {
        super.bar();
    }
    // 調用順序:D.bar → C.bar → B.bar → A.bar
}

多重繼承的應用建議

  1. 少用繼承,多用組合(composition)+ 接口(interface)
  2. 但 Solidity 中組合受技術條件制約
  3. 不要濫用繼承

應用案例

  • ERC721:NFT 標準,基礎功能
  • ERC721URIStorage:Token 與外部資源的綁定映射
  • ERC721Enumerable:遍歷能力

庫合約(Library)

定義與約束

  • 使用 library 關鍵字定義
  • 不能有成員變量、不能有 Ether、不能繼承、不能 selfdestruct
  • 不能有構造函數
library LibraryName {
    // 可以定義 struct、enum、constant
    function f() internal pure {
        // 函數實現
    }
}

調用方式

直接調用

import "LibraryName.sol";
LibraryName.functionName(args);

using 語法糖

using LibraryName for Type;
// 之後可以用 value.functionName() 的形式調用
// 等價於 LibraryName.functionName(value)

示例:SafeMath 和 Math 庫

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

library SafeMath {
    function add(uint x, uint y) internal pure returns (uint) {
        uint z = x + y;
        require(z >= x, "uint overflow");
        return z;
    }
}

library Math {
    function sqrt(uint y) internal pure returns (uint z) {
        if (y > 3) {
            z = y;
            uint x = y / 2 + 1;
            while (x < z) {
                z = x;
                x = (y / x + x) / 2;
            }
        } else if (y != 0) {
            z = 1;
        }
    }
}

contract TestSafeMath {
    using Math for uint;

    uint public MAX_UINT = 2 ** 256 - 1;

    function testAdd(uint x, uint y) public pure returns (uint) {
        return SafeMath.add(x, y); // 直接調用
    }

    function testSquareRoot(uint x) public pure returns (uint) {
        return x.sqrt(); // using 語法糖
    }
}

示例:Array 庫

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

library Array {
    function remove(uint[] storage arr, uint index) public {
        require(arr.length > 0, "Can't remove from empty array");
        arr[index] = arr[arr.length - 1];
        arr.pop();
    }
}

contract TestArray {
    uint[] public arr;

    function testArrayRemove() public {
        for (uint i = 0; i < 3; i++) {
            arr.push(i);
        }

        Array.remove(arr, 1);

        assert(arr.length == 2);
        assert(arr[0] == 0);
        assert(arr[1] == 2);
    }
}

庫的內部機制

  1. internal 函數:inline 到調用者代碼中(Embedded Library),不需要單獨部署
  2. public 函數:庫單獨部署爲合約,通過 delegatecall 調用(Linked Library)
  3. 與代理模式的對比
  4. 代理模式:通過兼容存儲佈局,delegatecall 借用代碼
  5. 庫合約:通過傳遞 storage 參數,delegatecall 借用代碼
  6. 庫的鏈接是編譯時完成,不能運行時動態切換(不支持升級)

庫中的 struct 和 mapping

Library 的一個特殊能力:public/external 函數可以接受 storage 參數(普通合約不行)。這使得 mapping 可以作爲參數傳遞。

// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;

library TransferOperation {
    function transfer(
        address from,
        address to,
        uint amount,
        mapping(address => uint) storage balanceOf
    ) public {
        uint current = balanceOf[from];
        require(current >= amount, "not enough balance!");
        uint toc = balanceOf[to];

        current -= amount;
        toc += amount;
        balanceOf[from] = current;
        balanceOf[to] = toc;
    }
}
// 庫中定義 struct 並操作
pragma solidity >=0.7.0 <0.9.0;

library LibraryShape {
    struct Rectangle {
        uint width;
        uint height;
    }

    function area(Rectangle storage s) public view returns (uint) {
        return s.width * s.height;
    }
}

contract Draw {
    using LibraryShape for LibraryShape.Rectangle;
    LibraryShape.Rectangle shape;

    function getArea() public view returns (uint) {
        return shape.area(); // using 語法糖
    }
}

綜合示例:IterableMapping

解決 mapping 無法遍歷的問題,通過庫實現可遍歷的 mapping。

設計要點:
1. 數據在數組中從索引 1 開始存放(因爲未賦值的 value 的 index 默認爲 0)
2. 數組數據不刪除(不用 pop),而是標記 deleted
3. mapping 中的 value 刪除時必須用 delete(所有位變爲 0)

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

library IterableMapping {
    struct itmap {
        mapping(uint => IndexValue) data;
        KeyFlag[] keys;
        uint size;
    }

    struct IndexValue {
        uint keyIndex;
        uint value;
    }

    struct KeyFlag {
        uint key;
        bool deleted;
    }

    function insert(itmap storage self, uint key, uint value) public returns (bool replaced) {
        uint keyIndex = self.data[key].keyIndex;
        self.data[key].value = value;
        if (keyIndex > 0) return true;
        else {
            uint index = self.keys.length;
            self.keys.push(KeyFlag(key, false));
            self.data[key].keyIndex = index + 1; // 從 1 開始
            self.keys[index].key = key;
            self.size++;
            return false;
        }
    }

    function remove(itmap storage self, uint key) internal returns (bool success) {
        uint keyIndex = self.data[key].keyIndex;
        if (keyIndex == 0) return false;
        delete self.data[key];
        self.keys[keyIndex - 1].deleted = true;
        self.size--;
        return true;
    }

    function contains(itmap storage self, uint key) internal view returns (bool) {
        return self.data[key].keyIndex > 0;
    }

    function iterate_start(itmap storage self) internal view returns (uint) {
        uint keyIndex = 0;
        while (keyIndex < self.keys.length && self.keys[keyIndex].deleted)
            keyIndex++;
        return keyIndex;
    }

    function iterate_valid(itmap storage self, uint keyIndex) internal view returns (bool) {
        return keyIndex < self.keys.length;
    }

    function iterate_next(itmap storage self, uint keyIndex) internal view returns (uint) {
        keyIndex++;
        while (keyIndex < self.keys.length && self.keys[keyIndex].deleted)
            keyIndex++;
        return keyIndex;
    }

    function iterate_get(itmap storage self, uint keyIndex) internal view returns (uint key, uint value) {
        key = self.keys[keyIndex].key;
        value = self.data[key].value;
    }
}

主題測試文章,只做測試使用。發佈者:Walker,轉轉請注明出處:https://walker-learn.xyz/archives/7500

(0)
Walker的頭像Walker
上一篇 5天前
下一篇 5天前

相關推薦

  • 合約交互與 ABI

    合約交互與 ABI 學習目標 掌握合約間調用方式、接口定義、ABI 數據結構、Web3.js 訪問合約的方法。 合約間調用基礎 EOA(外部賬號)發起調用,可能觸發合約間的調用鏈 調用者必須持有被調用合約的地址 方式一:同文件內直接調用 當兩個合約在同一個文件中時,可以直接通過合約類型和地址進行調用: // SPDX-License-Identifier: …

    1天前
    400
  • 引用類型詳解:數組、結構體、映射、字符串

    引用類型詳解:數組、結構體、映射、字符串 學習目標 掌握 Solidity 中四種引用類型(數組、結構體、映射、字符串/變長字節數組)的定義和使用方法。 前置知識 已學習值類型(整型、布爾、地址、定長字節數組等)。 數組(Array) storage 中的數組 Solidity 中數組分爲兩種: 靜態數組 T[K]:長度固定,編譯時確定 動態數組 T[]:長…

  • Gas機制與轉賬設計

    Gas機制與轉賬設計 學習目標 理解區塊鏈的經濟模型與激勵機制 掌握 Gas、Gas Price、Gas Fee 的概念與關係 理解 Ether 單位與轉換 掌握合約轉賬設計(receive / fallback / payable) 區塊鏈的經濟系統 爲什麼需要經濟模型? 計算與存儲資源是稀缺的:每個節點都要執行和存儲所有交易 共識和 trustless …

    22小時前
    300
  • Solidity 值類型詳解

    Solidity 值類型詳解 學習目標 掌握 Solidity 的整型、布爾、地址、定長字節數組、枚舉等值類型 理解 EVM 256 位機器架構對類型設計的影響 掌握類型轉換規則(隱式 vs 顯式) 瞭解溢出問題的歷史與解決方案 理解 Solidity 中所有類型的默認值機制 一、整型(int / uint) 1.1 基本概念 Solidity 提供有符號整…

  • 存儲位置與拷貝機制:storage、memory、calldata

    存儲位置與拷貝機制:storage、memory、calldata 學習目標 理解 EVM 中三種數據存儲位置的特點,以及引用類型在不同存儲位置之間賦值時的拷貝規則。 前置知識 已學習值類型和引用類型(數組、結構體、映射、字符串)。 三種存儲位置 storage —— 持久化存儲 類似數據庫,數據永久保存在區塊鏈上 成員變量(狀態變量)默認存儲在 stora…

簡體中文 繁體中文 English