關於 web service, unity, blogger 等軟體工程筆記

C# 回傳內部資料集合的幾種方式,考慮封裝與設計需求

Edit icon 沒有留言
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 ,會有少量的額外記憶體配置。(可參考 C# Source code 的實作)

自訂介面

自行定義讀取的介面:

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:
}

Reference

沒有留言: