C# 回傳內部資料集合的幾種方式,考慮封裝與設計需求
Game Jam 活動上遇到的有趣問題,如何設計回傳資料集合 (data collection) 的函式 (function)?筆記以下幾種方式,範例採用背包類別 (ItemBag class),要將其類別的內部道具資料,暴露給其他類別使用。
附註:System.Collection,中文單純來看翻譯成集合,但有使用其類別都知道其功用類似於 C++ std containers,用容器又似乎比較貼近其功能...以下筆記通篇採用集合來描述
考慮以下背包類別實作宣告,其資料 this.items
要能讓其他類別讀取:
using System.Collections.Generic;
public partial class ItemBag
{
// Main data
List<Item> items = new List<Item>();
// Singleton pattern
static ItemBag current = null;
public static ItemBag Current
{
get
{
if (current == null)
{
current = new ItemBag();
}
return current;
}
}
}
以下筆記幾種方式可以使用,應考慮設計需求來決定採用哪種方式,個人偏好資料封裝只能讓其他人讀取,但不能直接修改 this.items
集合資料,若要修改其集合資料,會額外開函式來操作。
方法 | 資料封裝 | 沒有額外記憶體配置 | 實作 IEnumerable |
---|---|---|---|
直接回傳集合 | Y | Y | |
複製資料 | Y | Y | |
參數化 | Y | (取決於參數) | Y |
迭代器模式 | Y | Y | Y |
唯讀集合 | Y | Y | |
自訂介面 | Y | Y | (取決於實作) |
直接回傳集合 (return collection reference)
第一種做法是直接回傳 items
,直接回傳資料集合,也是封裝最不安全的一種:
public List<Item> Items
{
get
{
return this.items;
}
}
因為其他類別可以取得該集合後,直接清除資料,或是直接新增道具,可能使得 ItemBag 內部資料狀態與行為不一致:
// Clear items…
ItemBag.Current.Items.Clear();
複製資料 (Clone)
第二種做法是回傳複製過後的資料,這樣不管其他類別怎麼使用,也不會增減背包的道具資料,當然到道具本身 (Item) 會不會被其他類別修改,取決於是否可被異動 (mutable class),抑或是確保是 strcut Item:
public List<Item> Items
{
get
{
return this.items.Clone() as List<Item>;
}
}
或是使用陣列 (Array):
public Item[] Items
{
get
{
return this.items.ToArray();
}
}
但缺點是呼叫此函式會額外配置記憶體 (heap memory allocation),如果每個 frame 都呼叫此函式,將會可能增加記憶體回收 (garbage collection, GC) 的次數,進而影響程式效能 (GC 需要花時間)。
參數化 (Parameterization)
要求其他類別提供資料集合參照 (collection reference),再將道具資料複製到該集合中:
public void GetItems(List<Item> output)
{
var requireCapacity = output.Count + this.items.Count;
if (output.Capacity < requireCapacity)
{
output.Capacity = requireCapacity;
}
for (var i = 0; i < this.items.Count; i++)
{
output.Add(this.items[i]);
}
}
與上次複製資料相似,只是並不是每次不需要重新配置新的記憶體來放置資料 (如果參數給的集合可用空間不夠大...),而是由使用類別提供。
迭代器模式 (Iterator pattern)
採用回傳 IEnumerable
public IEnumerable<Item> GetItems()
{
for (var i = 0; i < this.items.Count; i++)
{
yield return this.items[i];
}
}
這能限制其他類別只能透過其迭代子 (iterator) 來讀取道具資料:
foreach (var item in ItemBag.Current.GetItems())
{
// TODO: item
}
要注意的以下的實作,雖然 System.Collection 中有許多集合類別都有實作 IEnumable 介面,這樣不會有編譯錯誤 (compiler errors)
public IEnumerable<Item> GetItems()
{
return this.items;
}
但直接回傳 this.items 的參考,相當於是直接回傳集合,還是有可能被轉型使用…:
var items = ItemBag.Current.GetItems() as List<Item>;
items.Clear();
IEnumerable 通常會讓資料集合類別實作,例如 ItemBag 算是道具的資料集合,因此可以改寫成以下,移除 GetItems:
public partial class ItemBag : IEnumerable<Item>
{
public IEnumerator<Item> GetEnumerator()
{
return this.items.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return this.items.GetEnumerator();
}
}
而其他類別使用的方式便可以改成:
foreach (var item in ItemBag.Current)
{
// TODO: item
}
IEnumerable, IEnumerator 是 C# .net framework 對於迭代器模式 (iterator pattern) 的介面宣告,C# 有大量好用的函式都依賴此介面 (例如 System.Linq),更多可以參考之前的筆記:C# IEnumerator, IEnumerable, and Yield。
唯讀集合 (ReadOnlyCollection)
回傳唯讀的結構,確保資料不會被修改:
using System.Collections.ObjectModel;
public ReadOnlyCollection<Item> GetItems()
{
return this.items.AsReadOnly();
}
但這個每次都 new 一份 ReadOnlyCollection
自訂介面
自行定義讀取的介面:
public partial class ItemBag
{
public int ItemCount
{
get
{
return this.items.Count;
}
}
public Item GetItem(int index)
{
return this.items[index];
}
}
以上範例並沒有實作 IEnumerable 介面,將會少了許多 C# .net framework 函式庫的資源,例如 C# foreach 語法糖 (or 糖衣語法, syntactic sugar) 就無法使用:
for (var i = 0; i < ItemBag.Current.ItemCount; i++)
{
var item = ItemBag.Current.GetItem(i);
// TODO:
}
沒有留言: