C#集合 -- 自定义集合与代理

时间:2021-09-29 21:54:10
C#集合 -- 自定义集合与代理

前面章节所讨论的集合都可以直接实例化,因此我们可以非常方便地使用这些集合类。但是如果你试图在集合添加或移除元素时添加控制,它们就不适用了。对于强类型集合,在某些情况下,你需要添加这样的控制:

  • 添加或移除元素时,触发事件
  • 更新由于添加或移除元素对应的属性
  • 识别添加或删除元素的误操作并抛出异常

.NET Framework为上述目的提供了集合类,它们位于System.Collections.ObjectModel命名空间下。这些代理或包装类类通过在扩展类实现所需的方法从而实现了ILIst<T>或IDictionary<TKey,TValue>类。每个Add,Remove和Clear操作都被标记为虚方法,从而当它们被重写时可以充当一个入口的作用。

可自定义集合类通常都作为public的集合使用。比如,System.Windows.Form类中的集合控件。

Collection<T>类与CollectionBase类

C#集合 -- 自定义集合与代理

Collection<T>是List<T>的可自定义的包装器。

与IList<T>和IList实现一样,它还定义了四个额外的虚方法和一个protected属性

public class Collection<T>: IList<T>, IList, IReadOnlyList<T>
{
IList<T> items; protected IList<T> Items {
get { return items; }
} // ... protected virtual void ClearItems() {
items.Clear();
} protected virtual void InsertItem(int index, T item) {
items.Insert(index, item);
} protected virtual void RemoveItem(int index) {
items.RemoveAt(index);
} protected virtual void SetItem(int index, T item) {
items[index] = item;
} //...
}

虚方法提供了一个入口,通过该入口,你可以勾住该入口以更改或增强默认的行为。而Items属性允许实现者直接访问“内部列表”--通过这种方式在内部实现变化而不触发虚方法。

虚方法需要重写;它们可以不用考虑直到有需求更改集合的默认行为。下面的例子演示了一个集合应有的“骨架”:

public class Animal
{
public string Name;
public int Popularity;
public Animal (string name, int popularity)
{
Name = name; Popularity = popularity;
}
}
public class AnimalCollection : Collection <Animal>
{
// AnimalCollection is already a fully functioning list of animals.
// No extra code is required.
}
public class Zoo // The class that will expose AnimalCollection.
{ // This would typically have additional members.
public readonly AnimalCollection Animals = new AnimalCollection();
}
class Program
{
static void Main()
{
Zoo zoo = new Zoo();
zoo.Animals.Add (new Animal ("Kangaroo", 10));
zoo.Animals.Add (new Animal ("Mr Sea Lion", 20));
foreach (Animal a in zoo.Animals) Console.WriteLine (a.Name);
}
}

AnimalCollection除了只是一个List<Animal>之外,没有其他任何多余的功能;它的角色是为了将来的扩展需要。为了证实这点,现在我们需要添加Zoo属性到Animal,从而AnimalCollection可以引用Zoo对象,从而表明Animal属于哪个Zoo;并且AnimalCollection类重写Collection<Animal>的每个虚方法以自动更新所影响的属性。

public class Animal
{
public string Name;
public int Popularity;
public Zoo Zoo { get; internal set; }
public Animal(string name, int popularity)
{
Name = name; Popularity = popularity;
}
} public class AnimalCollection : Collection <Animal>
{
Zoo zoo;
public AnimalCollection (Zoo zoo) { this.zoo = zoo; }
protected override void InsertItem (int index, Animal item)
{
base.InsertItem (index, item);
item.Zoo = zoo;
}
protected override void SetItem (int index, Animal item)
{
base.SetItem (index, item);
item.Zoo = zoo;
}
protected override void RemoveItem (int index)
{
this [index].Zoo = null;
base.RemoveItem (index);
}
protected override void ClearItems()
{
foreach (Animal a in this) a.Zoo = null;
base.ClearItems();
}
} public class Zoo
{
public readonly AnimalCollection Animals;
public Zoo() { Animals = new AnimalCollection (this); }
}

Collection<T>还有一个构造器方法,该方法接收IList<T>参数。与其他集合类不一样,该集合是一个代理而不是一个备份,这就意味着后续的更改会直接反映到包装的Collection<T>中(尽管并没有触发Collection<T>的虚方法)。相反,对COllection<T>所做的更改会影响到集合具体实现类。

CollectionBase

CollectionBase是非generic的Collection<T>,它在.NET Framework 1.0中就已经存在。它提供了与Collection<T>大多数功能,但是它使用起来非常笨拙。在CollectionBase类中,没有IntertItem, RemoveITem, SetITem和ClearItem方法,取代它们的是OnInsert, OnInsertComplete, OnSet, OnSetComplete, OnRemove, OnRemoveComplete, OnClear和OnClearComplete方法。因为CollectionBase是非generic的,当你继承该类时,你必须实现类型化的方法--至少,需要一个类型化的索引器和类型化的Add方法。

KeyedCollection<TKey,TItem>和DictionaryBase

C#集合 -- 自定义集合与代理

KeyedCollection<TKey,TItem>继承Collection<T>。它既添加又删除了一些功能。添加的方法包括通过键获取元素;删除了内部列表的代理功能。

以键为基础的集合与OrderedDictionary类相似,因为它使用一个哈希表构建了一个线性集合。但是,与OrderedDictionary不同,它并没有实现IDictionary,因而不支持key/value对这样的概念。键并不是从元素自身获取,而是通过一个抽象的方法GetKeyFromItem。这就意味着遍历这样的以键为基础的集合与遍历一个普通的集合一样。

Collection<TItem>加通过键快速查找的元素就是KeyedCollection<TKey,TItem>。

因为它继承Collection<T>,一个键集合继承了Collection<T>的所有功能,除了通过构造器设定一个内部集合实例。

Collection<T>的构造器如下:

public Collection() {
items = new List<T>();
} public Collection(IList<T> list) {
if (list == null) {
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.list);
}
items = list;
}

KeyedCollection的构造器如下:

protected KeyedCollection(): this(null, defaultThreshold) {}

protected KeyedCollection(IEqualityComparer<TKey> comparer): this(comparer, defaultThreshold) {}

protected KeyedCollection(IEqualityComparer<TKey> comparer, int dictionaryCreationThreshold) {
if (comparer == null) {
comparer = EqualityComparer<TKey>.Default;
} if (dictionaryCreationThreshold == -1) {
dictionaryCreationThreshold = int.MaxValue;
} if( dictionaryCreationThreshold < -1) {
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.dictionaryCreationThreshold, ExceptionResource.ArgumentOutOfRange_InvalidThreshold);
} this.comparer = comparer;
this.threshold = dictionaryCreationThreshold;
}

其他成员的定义如下面的代码所示:

public abstract class KeyedCollection <TKey, TItem> : Collection <TItem>
// ...
protected abstract TKey GetKeyForItem(TItem item);
protected void ChangeItemKey(TItem item, TKey newKey);
// Fast lookup by key - this is in addition to lookup by index.
public TItem this[TKey key] { get; }
protected IDictionary<TKey, TItem> Dictionary { get; }
}

GetKeyFromItem是抽象方法,由具体的实现类实现。当元素的键属性发生变化时,必须调用ChangeItemKey方法,以更新内部的字典实例(Dictionary<TKey,TItem> dict;)。Dictionary属性返回用于实现查询的内部字典实例,该实例在向集合中插入第一个元素时自动创建。该行为可以通过构造器函数中的threshold参数来改变,如果执行了threshold,那么只有当达到临界点之后,才会创建内部的字典实例。而不指定创建临界点的好处是对于通过Dictionary属性的Keys属性,获取ICollection的键而言,有一个有效的字典会非常有用。那么,该集合就可以作为一个公开的属性传递给调用者。

使用KeyedCollection<>的场景是通过索引或者名字获取集合元素。为了证实这点,请看下面的例子:

public class Animal
{
string name;
public string Name
{
get { return name; }
set {
if (Zoo != null) Zoo.Animals.NotifyNameChange (this, value);
name = value;
}
}
public int Popularity;
public Zoo Zoo { get; internal set; }
public Animal (string name, int popularity)
{
Name = name; Popularity = popularity;
}
}
public class AnimalCollection : KeyedCollection <string, Animal>
{
Zoo zoo;
public AnimalCollection (Zoo zoo) { this.zoo = zoo; }
internal void NotifyNameChange (Animal a, string newName)
{
this.ChangeItemKey (a, newName);
}
protected override string GetKeyForItem (Animal item)
{
return item.Name;
}
// The following methods would be implemented as in the previous example
protected override void InsertItem (int index, Animal item)...
protected override void SetItem (int index, Animal item)...
protected override void RemoveItem (int index)...
protected override void ClearItems()...
} public class Zoo
{
public readonly AnimalCollection Animals;
public Zoo() { Animals = new AnimalCollection (this); }
}
class Program
{
static void Main()
{
Zoo zoo = new Zoo();
zoo.Animals.Add (new Animal ("Kangaroo", 10));
zoo.Animals.Add (new Animal ("Mr Sea Lion", 20));
Console.WriteLine (zoo.Animals [0].Popularity); // 10
Console.WriteLine (zoo.Animals ["Mr Sea Lion"].Popularity); // 20
zoo.Animals ["Kangaroo"].Name = "Mr Roo";
Console.WriteLine (zoo.Animals ["Mr Roo"].Popularity); // 10
}
}

DictionaryBase

KeyedCollection的非generic的版本是DictionaryBase类。该历史类的方法与之有很大不同。与CollectionBase一样,它也是通过笨拙的钩子方式实现了IDictionary,这些钩子方法是:OnInsert, OnInsertComplete, OnSet, OnSetComplete, OnRemove, OnRemoveComplete, OnClear和OnClearComplete方法。采用KeyedCollection方式实现IDictonary的好处是,你不需要子类来实现通过键获取元素。而DictionaryBase存在的目的就是为了创建子类,所以Dinctionary根本没有任何优点。正是因为这点,在后续的Framwork版本中才引入了KeyedCollection。所以如果你的程序需要保证对以前系统的兼容性,那么使用DictionaryBase类;否则使用KeyedCollection类。

ReadonlyCollection<T>

C#集合 -- 自定义集合与代理

ReadOnlyCollection<T>是一个包装器或代理,它提供了一个只读的集合。这非常适用于一个类对外公开一个只读的集合而对内却可以任意更改。

一个只读集合可以在构造器函数中接收一个输入集合,然后对该集合保持一个固定的引用。它并不会对输入集合生成一个静态的拷贝,所以对于输入集合后续的更改会在通过只读包装器中属性中体现。

为了演示这点,假设你希望你的类提供一个只读的公开的属性Names

public class Test
{
public List<string> Names { get; private set; }
}

上面的代码值完成了一半的工作。尽管其他的类型不能设置Name属性,它们还是可以调用该集合的Add,Remove方法。而ReadOnlyCollection就解决了这点:

public class Test
{
List<string> names;
public ReadOnlyCollection<string> Names { get; private set; }
public Test()
{
names = new List<string>();
Names = new ReadOnlyCollection<string> (names);
}
public void AddInternally() { names.Add ("test"); }
}

现在,只有Test类的成员可以修改names集合:

Test t = new Test();
Console.WriteLine (t.Names.Count); // 0
t.AddInternally();
Console.WriteLine (t.Names.Count); // 1
t.Names.Add ("test"); // Compiler error
((IList<string>) t.Names).Add ("test"); // NotSupportedException