[C#.NET] 若要自訂義基底集合類別,應避免繼承List<T>

  • 22011
  • 0
  • C#
  • 2012-05-29

[C#.NET] 若要自訂義基底集合類別,應避免繼承List<T>

我相信或多或少都會使用到自訂義的集合類別,比如說,為了讓集合內的項目不重覆,Add時會先處理一些條件判斷式,才會真的加入到集合裡,來看一下程式碼。


public class CellPhone
{
    public string CountryCode { get; set; }

    public string Phone { get; set; }

    private string _FullCellPhone;

    public string FullCellPhone
    {
        get
        {
            _FullCellPhone = CountryCode + Phone;
            return _FullCellPhone;
        }
        internal set
        {
            _FullCellPhone = value;
        }
    }
}
繼承了List<CellPhone> ,並用 new 關鍵字取代舊的 Add 方法,這樣的寫法或許常常發生在你我身邊

public class CellPhones : List<CellPhone>
{
    public new void Add(CellPhone Item)
    {
        var query = from data in this
                    where data.Phone == Item.Phone
                    select data;
        var result = query.FirstOrDefault();
        if (result == null)
        {
            base.Add(Item);
        }
    }
}
寫個簡單的測試,確保集合內不會出現重複項目。

public void QueryTest2()
{
    CellPhones phones = new CellPhones()
    {
        new CellPhone(){CountryCode="+86",Phone="338-2817"},
    };

    phones.Add(new CellPhone() { CountryCode = "+86", Phone = "338-2817" });
    Assert.IsTrue(phones.Count == 1);
}
這樣看起來好像沒什麼太大的問題,可是他會帶來淺在的Bug,若有人已經很習慣用 Interface 實體化,既然集合類別是繼承 List<T>,當然可以使用 IList<T> 實體化。
當然這樣就不會通過測試了,IList<T>並不會呼叫新的 Add 方法

public void QueryTest3()
{
    IList<CellPhone> phones = new CellPhones()
    {
        new CellPhone(){CountryCode="+86",Phone="338-2817"},
    };

    phones.Add(new CellPhone() { CountryCode = "+86", Phone = "338-2817" });
    Assert.IsTrue(phones.Count == 1);
}

若要正確的實作一個集合類別,你可以跟我這樣做 實作IEnumerable<CellPhone>, ICollection<CellPhone> 兩個接口。


public class CellPhones : IEnumerable<CellPhone>, ICollection<CellPhone>
{
    List<CellPhone> _items = new List<CellPhone>();

    public int Count
    {
        get { return this._items.Count; }
    }

    public bool IsReadOnly
    {
        get
        {
            return false;
        }
    }

    public void Add(CellPhone Item)
    {
        var query = from data in this
                    where data.Phone == Item.Phone
                    select data;
        var result = query.FirstOrDefault();
        if (result == null)
        {
            this._items.Add(Item);
        }
    }

    public bool Remove(CellPhone Item)
    {
        return this._items.Remove(Item);
    }

    public void Clear()
    {
        this._items.Clear();
    }

    public bool Contains(CellPhone item)
    {
        return this._items.Contains(item);
    }

    public void CopyTo(CellPhone[] array, int arrayIndex)
    {
        this._items.CopyTo(array, arrayIndex);
    }

    public IEnumerator<CellPhone> GetEnumerator()
    {
        return this._items.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}
如此一來,用戶端或是其他設計師,使用Interface 實體化時,也能正確的呼叫我們所寫的 Add 方法

public void QueryTest4()
{
    ICollection<CellPhone> phones = new CellPhones()
    {
        new CellPhone(){CountryCode="+86",Phone="338-2817"},
    };

    phones.Add(new CellPhone() { CountryCode = "+86", Phone = "338-2817" });
    Assert.IsTrue(phones.Count == 1);
}

後記:

若你開發的是基底類別,由上面的說明可以得知,請乖乖的實作特定接口,例如文中所寫的的IEnumerable<CellPhone>, ICollection<CellPhone> 兩個接口,否則其他人不會知道你內部的實作過程,避免隱含 Bug 的發生。

若你真的只想繼承List<T>,請確定你寫的類別不會被其他人調用。


補充:

不僅是List<T>會有這樣的隱含Bug,我們在設計類別時應該要小心,我們不應該去繼承一個實作類別,因為實作類別裡已經繼承了多個接口(Interface)。

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo