繼承多態與庫合約
學習目標
- 掌握 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)
算法描述
- 從當前合約開始找父合約,從右向左遍歷
- 如果某合約的所有子合約都已遍歷過,該合約入列;否則回退找兄弟
- 到根合約結束遍歷,輸出並倒置
構造函數執行順序 = 線性化順序
// 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
}
多重繼承的應用建議
- 少用繼承,多用組合(composition)+ 接口(interface)
- 但 Solidity 中組合受技術條件制約
- 不要濫用繼承
應用案例
- 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);
}
}
庫的內部機制
- internal 函數:inline 到調用者代碼中(Embedded Library),不需要單獨部署
- public 函數:庫單獨部署爲合約,通過 delegatecall 調用(Linked Library)
- 與代理模式的對比:
- 代理模式:通過兼容存儲佈局,delegatecall 借用代碼
- 庫合約:通過傳遞 storage 參數,delegatecall 借用代碼
- 庫的鏈接是編譯時完成,不能運行時動態切換(不支持升級)
庫中的 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