文章目錄
- 映射類型
- 可迭代映射
映射類型
映射類型使用語法 mapping(KeyType KeyName? => ValueType ValueName?)
,映射類型的變量聲明使用語法 mapping(KeyType KeyName? => ValueType ValueName?) VariableName
。
KeyType
可以是任何內置值類型、bytes
、string
或任何合約類型或枚舉類型。其他用戶定義的復雜類型,如映射、結構體或數組類型是不允許的。
ValueType
可以是任何類型,包括映射、數組和結構體。
KeyName
和 ValueName
是可選的(因此 mapping(KeyType => ValueType)
也是有效的),它們可以是任何有效的標識符,但不能是類型。
你可以將映射看作哈希表,它在內部初始化,每個可能的鍵都會映射到一個值,該值的字節表示是全零,即類型的默認值。相似之處僅限于此,鍵數據不會存儲在映射中,只有它的 keccak256
哈希值用于查找值。
由于這個原因,映射沒有長度或鍵值是否已設置的概念,因此無法在沒有額外信息的情況下刪除映射。
映射只能有存儲數據位置,因此只能作為狀態變量、作為函數中的存儲引用類型,或者作為庫函數的參數。它們不能作為合約函數的公共參數或返回參數。這些限制也適用于包含映射的數組和結構體。
你可以將映射類型的狀態變量標記為公共的,Solidity 會為你自動創建一個 getter。KeyType
變為 getter 的一個參數,并使用 KeyName
(如果指定的話)。如果 ValueType
是值類型或結構體,getter 返回與該類型匹配的 ValueType
(如果指定了 ValueName
)。如果 ValueType
是數組或映射,則 getter 會有一個參數對應每個 KeyType
,并遞歸處理。
在下面的示例中,MappingExample
合約定義了一個公共的 balances
映射,鍵類型為 address
,值類型為 uint
,將以太坊地址映射到無符號整數值。由于 uint
是值類型,getter 返回一個與該類型匹配的值,在 MappingUser
合約中,你可以看到它返回指定地址的值。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;// 定義一個包含地址和余額的映射的合約
contract MappingExample {// 聲明一個公共的映射,address => uint,記錄每個地址的余額mapping(address => uint) public balances;// 更新函數:允許發送者更新他們的余額function update(uint newBalance) public {balances[msg.sender] = newBalance; // 將調用者的余額更新為 newBalance}
}// 定義一個合約用于與 MappingExample 合約進行交互
contract MappingUser {// 一個函數,用來調用 MappingExample 合約的 update 方法,并返回當前合約地址的余額function f() public returns (uint) {// 創建 MappingExample 合約的實例MappingExample m = new MappingExample();// 調用 update 函數,將余額設置為 100m.update(100);// 返回當前合約地址的余額return m.balances(address(this)); // 返回 MappingExample 合約中當前合約地址的余額}
}
下面的示例是一個簡化版的 ERC20 代幣。_allowances
是一個映射類型,嵌套在另一個映射類型內部。我們為映射提供了可選的 KeyName
和 ValueName
。這不會影響合約的功能或字節碼,它僅僅是在 ABI 中為映射的 getter 輸入和輸出設置了名稱字段。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.18;// 定義一個包含映射的智能合約
contract MappingExampleWithNames {// 定義一個 public 映射,映射地址(address)到余額(uint)。映射的鍵為 `user`,值為 `balance`。// 這個映射會自動生成一個 getter 函數,可以根據地址(address)查詢對應的余額。mapping(address user => uint balance) public balances;// 更新余額的函數,接受一個 `newBalance` 參數。// 這個函數會將調用者(msg.sender)的余額更新為 `newBalance`。function update(uint newBalance) public {// 使用調用者的地址(msg.sender)作為鍵,更新其對應的余額值。balances[msg.sender] = newBalance;}
}
在下面的示例中,_allowances 映射用于記錄某人被授權從你的賬戶中提取的金額:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;contract MappingExample {// 定義一個私有映射 `_balances`,將每個地址映射到一個無符號整數(余額)。mapping(address => uint256) private _balances;// 定義一個私有映射 `_allowances`,它是一個二層映射,記錄每個地址(owner)允許另一個地址(spender)提取的金額。mapping(address => mapping(address => uint256)) private _allowances;// 定義事件,當轉賬發生時觸發。event Transfer(address indexed from, address indexed to, uint256 value);// 定義事件,當批準時觸發,表明某個地址被授權從另一個地址提取一定金額。event Approval(address indexed owner, address indexed spender, uint256 value);// 查詢某個地址被授權的提取金額function allowance(address owner, address spender) public view returns (uint256) {return _allowances[owner][spender];}// 從一個賬戶向另一個賬戶轉賬,同時檢查授權金額是否足夠function transferFrom(address sender, address recipient, uint256 amount) public returns (bool) {// 確保 sender 允許 msg.sender(調用者)提取足夠的金額require(_allowances[sender][msg.sender] >= amount, "ERC20: Allowance not high enough.");// 減少授權金額_allowances[sender][msg.sender] -= amount;// 執行轉賬_transfer(sender, recipient, amount);return true;}// 批準另一個地址(spender)從調用者的賬戶中提取指定金額(amount)function approve(address spender, uint256 amount) public returns (bool) {// 確保 spender 地址不是零地址require(spender != address(0), "ERC20: approve to the zero address");// 設置授權金額_allowances[msg.sender][spender] = amount;// 觸發批準事件emit Approval(msg.sender, spender, amount);return true;}// 內部轉賬函數,用于更新賬戶余額,并觸發轉賬事件function _transfer(address sender, address recipient, uint256 amount) internal {// 確保 sender 和 recipient 不是零地址require(sender != address(0), "ERC20: transfer from the zero address");require(recipient != address(0), "ERC20: transfer to the zero address");// 確保 sender 有足夠的余額require(_balances[sender] >= amount, "ERC20: Not enough funds.");// 執行轉賬:從 sender 減去金額,給 recipient 增加金額_balances[sender] -= amount;_balances[recipient] += amount;// 觸發轉賬事件emit Transfer(sender, recipient, amount);}
}
可迭代映射
你不能直接迭代映射,即不能枚舉它們的鍵。不過,你可以在其基礎上實現一個數據結構并對其進行迭代。例如,下面的代碼實現了一個 IterableMapping
庫,User
合約將數據添加到該庫中,并且 sum
函數會迭代這個數據結構以求和所有值。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.8;// 定義 IndexValue 結構體,用于存儲每個鍵對應的索引和數值
struct IndexValue { uint keyIndex; // 鍵的索引位置uint value; // 鍵對應的值
}// 定義 KeyFlag 結構體,用于標記鍵是否被刪除
struct KeyFlag { uint key; // 鍵的值bool deleted; // 是否已刪除標志
}// 定義 itmap 結構體,包含了一個映射和一個存儲鍵的數組,以及當前大小
struct itmap {mapping(uint => IndexValue) data; // 存儲鍵值對的映射KeyFlag[] keys; // 存儲鍵的數組uint size; // 數據大小
}// 定義一個類型 Iterator,實質上是 uint 類型,用于遍歷
type Iterator is uint;// 定義 IterableMapping 庫,提供操作 itmap 類型數據的函數
library IterableMapping {// 插入數據到 itmap 中,如果已存在則更新數據function insert(itmap storage self, uint key, uint value) internal returns (bool replaced) {uint keyIndex = self.data[key].keyIndex; // 獲取當前鍵的索引self.data[key].value = value; // 更新該鍵對應的值if (keyIndex > 0) {return true; // 如果鍵已經存在,返回 true,表示數據已替換} else {// 如果鍵不存在,分配一個新的索引keyIndex = self.keys.length;self.keys.push(); // 在數組末尾添加一個新元素self.data[key].keyIndex = keyIndex + 1; // 設置新鍵的索引self.keys[keyIndex].key = key; // 將鍵添加到鍵數組中self.size++; // 增加數據大小return false; // 返回 false,表示插入了新的鍵值對}}// 從 itmap 中刪除指定的鍵function remove(itmap storage self, uint key) internal returns (bool success) {uint keyIndex = self.data[key].keyIndex; // 獲取該鍵的索引if (keyIndex == 0) {return false; // 如果鍵不存在,返回 false}delete self.data[key]; // 刪除數據映射中的鍵值對self.keys[keyIndex - 1].deleted = true; // 將對應的 KeyFlag 標記為已刪除self.size--; // 減小數據大小return true; // 返回 true,表示刪除成功}// 檢查 itmap 中是否包含指定的鍵function contains(itmap storage self, uint key) internal view returns (bool) {return self.data[key].keyIndex > 0; // 如果該鍵存在,返回 true}// 初始化遍歷,返回一個迭代器function iterateStart(itmap storage self) internal view returns (Iterator) {return iteratorSkipDeleted(self, 0); // 跳過已刪除的項,返回起始迭代器}// 檢查當前迭代器是否有效function iterateValid(itmap storage self, Iterator iterator) internal view returns (bool) {return Iterator.unwrap(iterator) < self.keys.length; // 如果迭代器位置小于鍵數組長度,則有效}// 獲取下一個迭代器,跳過已刪除的項function iterateNext(itmap storage self, Iterator iterator) internal view returns (Iterator) {return iteratorSkipDeleted(self, Iterator.unwrap(iterator) + 1); // 跳到下一個有效項}// 獲取當前迭代器對應的鍵和值function iterateGet(itmap storage self, Iterator iterator) internal view returns (uint key, uint value) {uint keyIndex = Iterator.unwrap(iterator); // 獲取迭代器的索引key = self.keys[keyIndex].key; // 獲取鍵value = self.data[key].value; // 獲取值}// 跳過已刪除的項,返回有效的迭代器位置function iteratorSkipDeleted(itmap storage self, uint keyIndex) private view returns (Iterator) {while (keyIndex < self.keys.length && self.keys[keyIndex].deleted) // 如果該項被標記為刪除,則跳過keyIndex++;return Iterator.wrap(keyIndex); // 返回跳過已刪除項后的迭代器}
}// User 合約使用 IterableMapping 庫進行數據操作
contract User {itmap data; // 聲明一個 itmap 類型的變量來保存數據// 使用 IterableMapping 庫來操作 itmap 類型的數據using IterableMapping for itmap;// 插入數據到 itmap 中function insert(uint k, uint v) public returns (uint size) {// 調用 IterableMapping 庫的 insert 函數插入數據data.insert(k, v);// 返回當前數據的大小return data.size;}// 計算所有存儲數據的總和function sum() public view returns (uint s) {// 遍歷 itmap 中的所有數據并計算總和for (Iterator i = data.iterateStart(); // 初始化迭代器data.iterateValid(i); // 檢查迭代器是否有效i = data.iterateNext(i) // 獲取下一個有效項) {(, uint value) = data.iterateGet(i); // 獲取當前項的值s += value; // 將當前值累加到總和}}
}