# 複合數據類型 ## 映射(Mappings) 映射在Solidity中是一種特殊的數據結構,用於存儲鍵值對(key-value pairs)。它類似於其他編程語言中的哈希表或字典,但在Solidity中有其獨特的特性和限制。 ### 語法 ```solidity= 複製程式碼 mapping(keyType => valueType) [visibility] mappingName; ``` * keyType:鍵的類型,可以是整數、地址、字串等基本類型。但不能是映射、結構、動態數組等複雜類型。 * valueType:值的類型,可以是任何類型,包括基本類型、結構、數組甚至其他映射。 * visibility:可見性修飾符,如public、private等。 ### 範例 ```solidity= mapping(address => uint256) public balances; ``` * balances:映射每個地址(address)到其對應的餘額(uint256)。 * public:可見性修飾符,允許外部訪問。 ### 使用方法 #### 設置值: ```solidity= balances[msg.sender] = 100; ``` 將當前呼叫者的餘額設置為100。 #### 獲取值: ```solidity= uint256 userBalance = balances[userAddress]; ``` 獲取userAddress的餘額。 #### 刪除值: ```solidity= delete balances[userAddress]; ``` 將userAddress的餘額重置為類型的默認值,對於uint256即為0。 ### 特點與注意事項 1. 默認值:如果鍵不存在,映射會返回值類型的默認值。例如,uint256的默認值是0。 1. 不可遍歷:映射無法被遍歷,因為Solidity無法獲取所有鍵的列表。 1. Storage位置:映射只能宣告在storage中,不能在memory或calldata中使用。 ### 應用場景 1. Token餘額:追蹤每個地址的代幣餘額。 1. 授權:管理地址與權限之間的關係。 1. 資料關聯:將唯一的鍵(如ID)映射到特定的資料結構。 ### 重要概念詳解 #### 映射的不可遍歷性 由於映射無法遍歷,如果需要獲取所有鍵,可以考慮: 1. 輔助數組:在每次更新映射時,同步更新一個數組,儲存所有的鍵。 1. 事件(Events):在更新時觸發事件,讓前端或監聽器記錄相關資訊。 ## 結構(Structs) 結構允許您定義自定義的複合數據類型,將多個相關的變數組合在一起,方便管理和操作。 ### 語法 ```solidity= struct StructName { type1 member1; type2 member2; // 更多成員... } ``` ### 範例 ```solidity= struct Person { string name; uint256 age; } Person[] public people; ``` * Person:定義了一個包含name和age的結構。 * people:一個Person類型的動態數組,存儲多人資訊。 ### 使用方法 #### 新增資料: ```solidity= function addPerson(string memory _name, uint256 _age) public { people.push(Person(_name, _age)); } ``` 或 ```solidity= people.push(Person({name: _name, age: _age})); ``` #### 讀取資料: ```solidity= Person memory person = people[0]; string memory name = person.name; uint256 age = person.age; ``` ### 特點與注意事項 1. 數據位置:在函數中使用結構時,需要指定數據位置,如memory或storage。 1. 嵌套結構:結構可以包含其他結構,但需要注意深層次的嵌套可能增加複雜性和Gas成本。 1. 限制:結構不能包含var、mapping類型的成員(在某些情況下可能會有限制)。 ### 應用場景 * 資料模型:定義複雜的資料結構,如用戶資訊、產品詳細等。 * 狀態管理:追蹤合約內的狀態和關聯數據。 ### 重要概念 #### 結構的深層拷貝與引用 * Storage與Memory:在storage中,結構是引用類型;在memory中,是值類型(深拷貝)。 * 修改數據:在函數內修改結構時,需要注意數據位置,否則可能不會影響到原始資料。 ## 枚舉(Enums) **枚舉(Enumeration)** 是在Solidity中用於定義一組具名常量的數據類型。它可以讓您的代碼更具可讀性和可維護性,特別是在需要使用一組預定義選項的情況下。 ### 語法 ```solidity= enum EnumName { Option1, Option2, Option3, // 更多選項... } ``` * EnumName:枚舉的名稱。 * Option:枚舉中的各個選項,默認從0開始自增。 ### 範例 ```solidity= enum Status { Pending, Shipped, Delivered, Canceled } Status public orderStatus; ``` * Status:定義了一個包含四個狀態的枚舉。 * orderStatus:一個公開的狀態變量,用於追蹤訂單的狀態。 ### 使用方法 設置枚舉值: ```solidity= function setStatus(Status _status) public { orderStatus = _status; } ``` 或者直接使用枚舉的選項: ```solidity= orderStatus = Status.Shipped; ``` 比較枚舉值: ```solidity= if (orderStatus == Status.Delivered) { // 執行相應的邏輯 } ``` 獲取枚舉的整數值: ```solidity= uint256 statusValue = uint256(orderStatus); ``` 從整數轉換為枚舉值(不建議,可能導致錯誤): ```solidity= orderStatus = Status(2); // 將狀態設置為Delivered ``` ### 特點與注意事項 * 默認值:枚舉類型的默認值為其第一個選項,索引為0。 * 存儲方式:在區塊鏈中,枚舉實際上被存儲為uint8(如果選項少於256個)。 * 類型安全:枚舉提供了類型安全性,防止使用無效的值。 * 限制:不能直接獲取枚舉的長度或列出所有選項。 ### 應用場景 * 狀態機:追蹤合約或流程的當前狀態,如訂單狀態、投票階段等。 * 選項設置:定義一組預定的配置或模式。 * 角色管理:管理用戶或地址的角色類型。 ### 重要概念 #### 枚舉的底層表示 * 整數表示:每個枚舉選項在底層都被映射為整數,從0開始遞增。 * 類型轉換:可以將枚舉類型顯式轉換為uint類型,但需謹慎。 ### 最佳實踐 * 明確命名:使用清晰、描述性的枚舉名稱和選項名稱。 * 避免數值依賴:不要在代碼中依賴枚舉的底層整數值,應該使用枚舉選項本身。 * 限制範圍:在可能的情況下,添加檢查以確保枚舉值在有效範圍內,特別是在從外部輸入獲取值時。 * 避免過度使用:僅在選項數量有限且固定的情況下使用枚舉。 ### 注意事項 #### 升級合約時的問題 * 枚舉的順序:在升級合約或重新部署時,切勿更改枚舉選項的順序,因為這會改變底層整數值,導致數據不一致。 * 新增選項:如果需要新增枚舉選項,應該只在末尾添加,並確保與現有數據兼容。 #### 枚舉的最大選項數 * 選項限制:由於枚舉默認使用uint8,因此最多可以有256個選項。如果需要更多選項,可能需要考慮其他數據結構。 ## 陣列(Arrays) 陣列是Solidity中用於存儲相同類型元素的集合。陣列可以是固定長度的,也可以是動態長度的,並且可以是一維或多維的。 ### 語法 ```solidity= //固定長度陣列 type[length] arrayName; //動態長度陣列 type[] arrayName; ``` ### 使用方法 #### 固定長度陣列 ```solidity= //初始化 fixedArray = [10, 20, 30, 40, 50]; //存取元素 uint256 firstElement = fixedArray[0]; // 10 //修改元素 fixedArray[2] = 35; // 將第三個元素改為35 ``` #### 動態長度陣列 ```solidity= //新增元素 dynamicArray.push(100); //移除元素 dynamicArray.pop(); // 移除最後一個元素 //獲取長度 uint256 length = dynamicArray.length; //刪除元素 delete dynamicArray[1]; // 將索引1的元素重置為默認值 ``` ### 二維陣列 ```solidity= //新增子陣列 twoDArray.push([1, 2, 3]); //存取元素 uint256 value = twoDArray[0][1]; // 2 ``` ### 特點與注意事項 * 索引從0開始:陣列的索引是從0開始的。 * 越界訪問:嘗試訪問不存在的索引會導致運行時錯誤。 * 類型必須一致:陣列中的所有元素必須是相同的類型。 * 長度不可變:固定長度陣列的長度在編譯時就已確定,無法在運行時修改。 ### 重要概念 #### push() >用途:在陣列末尾新增元素。 #### pop() >用途:移除陣列末尾的元素。 >注意:僅適用於動態陣列,且在storage中。 ### 應用場景 * 批量處理:如批量轉賬、批量操作。 * 資料存儲:存儲同類型的資料集合,如地址列表、交易記錄等。 * 多維數據:處理矩陣、表格等複雜數據結構。 ### 最佳實踐 * 限制陣列長度:避免無限制地增長陣列,可能導致Gas成本過高。 * 優化Gas成本:在可能的情況下,使用映射替代大型陣列。 * 小心數據位置:明確指定memory或storage,避免意外的數據拷貝或引用。 * 驗證索引:在訪問陣列元素前,檢查索引是否在有效範圍內。 ### 注意事項 #### 陣列的數據位置與拷貝 * memory與storage的區別:memory中的陣列是臨時的,函數執行完後即被銷毀;storage中的陣列是永久存儲在區塊鏈上的。 * 拷貝行為:將storage陣列賦值給memory陣列時,會創建一個副本。 #### 不支持的操作 * 不支持的內置函數:Solidity的陣列不支持如sort()、concat()等高級函數,需要自行實現。 * 無法直接刪除指定索引的元素並縮短陣列長度:使用delete關鍵字只會重置元素值,但不會改變陣列長度。 * 在Solidity 0.6.0之後,已不建議直接修改length屬性 ## 陣列 VS 映射 | **比較項目** | **映射(Mapping)** | **陣列(Array)** | |----------------|---------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------| | **定義** | 一種儲存鍵值對(key-value pair)的資料結構,允許透過鍵快速存取對應的值。 | 一組有序的元素集合,透過索引(index)來存取個別元素。 | | **語法** | `mapping(keyType => valueType) mappingName` | - 動態陣列:`valueType[] arrayName`<br>- 固定大小陣列: `valueType[fixedSize] arrayName` | | **存取方式** | 使用鍵來存取或修改值,例如:`mappingName[key] = value;` | 使用索引來存取或修改元素,例如:`arrayName[index] = value;` | | **初始化** | 所有可能的鍵在預設情況下皆存在,且其對應的值為型別的默認值(如數字為 0,布林值為 false)。 | 需要明確初始化,每個元素未被賦值前不會自動存在。 | | **可列舉性** | 無法直接遍歷映射中的所有鍵或值,需要額外的輔助資料結構(如陣列)來記錄鍵的列表。 | 可以使用迴圈遍歷陣列中的所有元素。 | | **長度獲取** | 無法獲取映射的大小(鍵值對的數量)。 | 可以使用 `arrayName.length` 獲取陣列的長度。 | | **新增元素** | 直接為新鍵賦值即可新增鍵值對,例如:`mappingName[newKey] = value;` | - 動態陣列:使用 `arrayName.push(value);` 新增元素。<br>- 固定大小陣列:無法新增,大小固定。 | | **刪除元素** | 使用 `delete mappingName[key];` 刪除指定的鍵值對。 | - 使用 `delete arrayName[index];` 將元素重置為默認值,但不改變陣列長度。<br>- 動態陣列可使用 `pop()` 刪除最後一個元素,減少陣列長度。 | | **適用場景** | - 儲存關聯性資料,例如:帳戶地址到餘額的對應關係。<br>- 快速查找某個鍵是否存在。 | - 儲存有序數據,例如:投票者名單、交易記錄。<br>- 需要遍歷或排序的數據集合。 | | **限制** | - 無法遍歷或獲取所有鍵。<br>- 鍵類型有限制,只能使用值型別(不可使用映射、陣列等複雜型別)。 | - 固定大小陣列無法動態增減元素。<br>- 操作大型陣列可能會消耗較多 Gas。 | | **Gas 效率** | - 單次存取成本較低,適合頻繁查詢。<br>- 刪除操作實際上是將值重置為默認值,成本較低。 | - 動態陣列的 `push` 和 `pop` 操作成本較低。<br>- 遍歷或修改大量元素可能會消耗較多 Gas。 | | **安全性考量** | - 若未妥善管理,可能會有鍵值覆蓋的風險。<br>- 需要防範重入攻擊時的資料完整性問題。 | - 需注意陣列越界(index out of bounds)問題。<br>- 操作陣列時需考慮 Gas 上限,以避免交易失敗。 | | **範例應用** | ```mapping(address => uint256) public balances;``` | ```uint256[] public voterIds;```|