# 複合數據類型
## 映射(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;```|