65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 ICollection<T> 实现 C# 泛型集合

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.53/5 (21投票s)

2007年11月8日

CPOL

10分钟阅读

viewsIcon

249477

downloadIcon

1867

一篇解释如何使用 ICollection 实现 C# 泛型集合的文章,附带一个业务逻辑层示例。

Screenshot - GenericsExample.jpg

引言

在构建三层 Web 应用程序的过程中,我发现需要一个泛型集合类,它能够以类型安全的方式存储我的业务逻辑层中的任何业务对象的集合。我已经编写了一个非泛型集合类(很可惜,每个业务对象都有一个),它继承自 CollectionBase。随着越来越多的业务对象的需求出现,我立即停止了手中的工作,开始寻找更动态的解决方案。

我记得 C++ 有一种叫做模板类(template classes)的东西,它可以接受动态变量类型。在我看来,实现一个可枚举(即可以使用 `foreach` 语句进行遍历)的泛型集合类在 C# 中应该不难。我认为最好的方法是编写一个实现了泛型接口 `ICollection` 的泛型类。我开始在互联网上寻找示例,发现这方面的实现示例很少,而且有些网站甚至说它不值得研究,因为没有人实现 `ICollection`。我心想,一定有人会实现 `ICollection`。

经过一番搜索,我找到了一些不完整或/和无法正常工作的实现该接口的代码示例。我从所有找到的信息中学习到了我能学到的东西,并提出了一个我认为是合理、可行的 `ICollection` 以及 `IEnumerator` 实现,以便通过 `foreach` 语句进行枚举。本文旨在提供一个关于如何实现 `ICollection` 和 `IEnumerator` 的可靠示例,以创建通用、类型安全且可扩展的集合类。

什么是泛型?

泛型是在 .NET 2.0 中首次引入 C# 语言的。它们提供了一种类型安全的方法来访问集合和/或函数调用中使用的数据。使用泛型可以显著减少由于类型转换或装箱/拆箱引起的运行时错误,因为泛型操作中使用的类型是在编译时进行评估的。如果您是 C++ 的爱好者,您会发现泛型与 C++ 中的模板类的工作方式非常相似,但也有一些区别。

C++ 模板与 C# 泛型有什么区别?

C++ 模板类和 C# 泛型之间的主要区别之一是,在 C# 中,您不能在泛型类中使用算术运算符(自定义运算符除外)。

C# 不允许类型参数具有默认类型,也不允许您使用类型参数(`T`)作为泛型类型的基类。

这些只是初步观察到的一些区别,但如果您熟悉 C++ 的话,注意到它们很重要。尽管 C++ 在这方面更灵活,但 C# 模型通过限制基于泛型约束的操作,可以减少运行时错误。

深入了解 ICollection<T>

在我们开始之前,让我们先看一下 .NET Framework 中 `ICollection` 的定义。

public interface ICollection<T> : IEnumerable<T>, IEnumerable
{
      void Add(T item);

      bool Remove(T item);
      void Clear();
      bool Contains(T item);
      void CopyTo(T[] array, int arrayIndex);
      int Count { get; }
      bool IsReadOnly { get; }
}

您可以看到,为了在我们的泛型集合中扩展 `ICollection`,我们必须实现两个属性和五个方法。我们还必须实现 `IEnumerator` 和 `IEnumerator` 所要求的两个方法;`ICollection` 扩展了这两个接口以允许 `foreach` 枚举。稍后将详细介绍。现在,让我们定义我们将要收集的对象。

示例业务逻辑层

在我们实际编写集合类之前,我们将编写一些基础代码来配合使用它。本文附带的示例源代码包含两个类,它们构成了一个简单的业务逻辑层,我们将与我们的泛型集合类一起使用。

BusinessObjectBase 类

这是我们所有其他业务对象将从中派生的基类。该类只有一个名为 `UniqueId`(`Guid`)的属性,以及一个初始化该属性的默认构造函数。尽管这里使用的基类很简单,但可以为业务对象基类添加很多功能。就目前而言,我们只关心它如何被其他类派生,因为我们将用它来限制可用于我们泛型集合的类型。

//Abstract base class for all business object in the Business Logic Layer
public abstract class BusinessObjectBase
{
    protected Guid? _UniqueId;
    //local member variable which stores the object's UniqueId
   
    //Default constructor
    public BusinessObjectBase()
    {
        //create a new unique id for this business object
        _UniqueId = Guid.NewGuid();
    }

    //UniqueId property for every business object
    public Guid? UniqueId
    {
        get
        {
            return _UniqueId;
        }
        set
        {
            _UniqueId = value;
        }
    }
}

这并不复杂。基类是抽象的,以确保它必须被继承才能实例化,并且它只包含一个属性来唯一标识它派生的每个业务对象。

Person 类

此类将代表我们将在集合中存储的个人。如下所示,`Person` 类继承自抽象类 `BusinessObjectBase`。这将在我们创建泛型集合类时很重要。

public class Person : BusinessObjectBase
{
    private string _FirstName = "";
    private string _LastName = "";

    //Paramaterized constructor for immediate instantiation
    public Person(string first, string last)
    {
        _FirstName = first;
        _LastName = last;
    }

    //Default constructor
    public Person()
    {
        //nothing
    }
 
    //Person' First Name 
    public string FirstName 
    {
        get
        {
            return _FirstName;
        } 
        set
        {
            _FirstName = value;
        }
    }

    //Person's Last Name
    public string LastName
    {
        get
        {
            return _LastName;
        }
        set
        {
            _LastName = value;
        }
    }
}

此类同样很简单,只包含 `FirstName` 和 `LastName` 属性,以及一个立即初始化这些属性的构造函数,还有一个默认的无参数构造函数。现在我们已经看到了我们将要收集的对象,让我们继续了解我们将如何收集它们。

BusinessObjectCollection<T> 类

这里将发生大部分魔法。我们在这里从 `ICollection` 实现的方法将负责将对象添加和删除到集合中,检查集合是否包含给定对象的实例,并实例化一个 `IEnumerator` 对象以使用 `foreach` 语句枚举集合(稍后将介绍)。下面是 `BusinessObjectCollection` 的定义。

using System;
using System.Collections.Generic; 
//needed for non-generic explicit interface
//implementation requirements from ICollection
using System.Collections;
using System.Text;

namespace GenericsExample.Business
{
    public class BusinessObjectCollection<T> : 
           ICollection<T> where T : BusinessObjectBase
    {
        //inner ArrayList object
        protected ArrayList _innerArray;
        //flag for setting collection to read-only
        //mode (not used in this example)
        protected bool _IsReadOnly;

        // Default constructor
        public BusinessObjectCollection()
        {
            _innerArray = new ArrayList();
        }

        // Default accessor for the collection 
        public virtual T this[int index]
        {
            get
            {
                return (T)_innerArray[index];
            }
            set
            {
                _innerArray[index] = value;
            }
        }

        // Number of elements in the collection
        public virtual int Count
        {
            get
            {
                return _innerArray.Count;
            }
        }

        // Flag sets whether or not this collection is read-only
        public virtual bool IsReadOnly
        {
            get
            {
                return _IsReadOnly;
            }
        }

        // Add a business object to the collection
        public virtual void Add(T BusinessObject)
        {
            _innerArray.Add(BusinessObject);
        }

        // Remove first instance of a business object from the collection 
        public virtual bool Remove(T BusinessObject) 
        {
            bool result = false;

            //loop through the inner array's indices
            for (int i = 0; i < _innerArray.Count; i++)
            {
                //store current index being checked
                T obj = (T)_innerArray[i];

                //compare the BusinessObjectBase UniqueId property
                if (obj.UniqueId == BusinessObject.UniqueId)
                {
                    //remove item from inner ArrayList at index i
                    _innerArray.RemoveAt(i);
                    result = true;
                    break;
                }
            }

            return result;
        }

        // Returns true/false based on whether or not it finds
        // the requested object in the collection.
        public bool Contains(T BusinessObject)
        {
            //loop through the inner ArrayList
            foreach (T obj in _innerArray)
            {
                //compare the BusinessObjectBase UniqueId property
                if (obj.UniqueId == BusinessObject.UniqueId)
                {
                    //if it matches return true
                    return true;
                }
            }
            //no match
            return false;
        }
 
        // Copy objects from this collection into another array
        public virtual void CopyTo(T[] BusinessObjectArray, int index)
        {
            throw new Exception(
              "This Method is not valid for this implementation.");
        }

        // Clear the collection of all it's elements
        public virtual void Clear()
        {
            _innerArray.Clear();
        }

        // Returns custom generic enumerator for this BusinessObjectCollection
        public virtual IEnumerator<T> GetEnumerator()
        {
            //return a custom enumerator object instantiated
            //to use this BusinessObjectCollection 
            return new BusinessObjectEnumerator<T>(this);
        }

        // Explicit non-generic interface implementation for IEnumerable
        // extended and required by ICollection (implemented by ICollection<T>)
        IEnumerator IEnumerable.GetEnumerator()
        {
            return new BusinessObjectEnumerator<T>(this);
        }
    }
}

您会注意到类声明的第一行

public class BusinessObjectCollection<T> : 
               ICollection<T> where T : BusinessObjectBase

上面的语法只是告诉编译器,只允许从 `BusinessObjectBase` 派生的类型出现在集合中。这就是泛型集合真正派上用场的地方。当我们在上面使用泛型类型约束时,我们就省去了在运行时担心类型安全错误的步骤。如果我们尝试创建一个不派生自 `BusinessObjectBase` 的对象集合,我们会得到一个编译器错误。这可能听起来有限制,但这正是我们想要的结果,因为我们可能希望向我们的集合类添加特殊处理,而这些处理只在我们处理继承了 `BusinessObjectBase` 的类时才起作用。

您还可以添加其他泛型类型约束,例如 `new()` 约束,它会告诉编译器,集合将存储的泛型类型必须有一个公共的无参数构造函数。这通常用于获得在泛型类内部实例化泛型类型实例的能力。有关泛型类型约束的更多信息,请访问 MSDN 网站 此处

您会注意到我使用了一个 `ArrayList` 对象作为驱动自定义泛型集合的内部数据结构。由于这是一个非泛型类型,如果您尝试创建包含值类型(如整数)的泛型集合,可能会导致一些装箱/拆箱。但是,由于我们使用了约束来只允许派生自 `BusinessObjectBase` 的类型,所以我们不必担心这个问题。`ArrayList` 对象仍然会将一些类型向下转换为 `object` 基类,但性能损失很小,甚至没有。您也可以使用另一个泛型列表类型作为后备元素,例如 `List`,甚至是一个强类型数组。使用 `List` 和 `ArrayList` 的性能几乎相同,即使数据量很大(我测试了 20,000 个元素)。然而,使用强类型数组所需的重新加维操作非常昂贵,在向集合中添加相同数量的元素时,占用了总操作时间的 70-80%。

该类的其余部分只是 `ICollection` 接口的基本实现,其中 `Remove()` 和 `Contains()` 方法包含一些自定义逻辑,它们会检查 `BusinessObjectBase` 类的 `UniqueId` 字段,以确定对象是否存在于集合中。

但是,有一些代码值得注意,它与我们的下一个类有关

 // Returns custom generic enumerator for this BusinessObjectCollection
public virtual IEnumerator<T> GetEnumerator()
{
   //return a custom enumerator object
   //instantiated to use this BusinessObjectCollection 
   return new BusinessObjectEnumerator<T>(this);
}

// Explicit non-generic interface implementation for IEnumerable
// extended and required by ICollection (implemented by ICollection<T>)
IEnumerator IEnumerable.GetEnumerator()
{
   return new BusinessObjectEnumerator<T>(this);
}

这两个实现是 `foreach` 循环用来遍历我们的自定义集合的。我们必须定义一个自定义泛型 `Enumerator` 对象来执行实际的遍历。如果您以前从未处理过泛型接口实现,您可能会想为什么这里有两个 `GetEnumerator()` 的实现。第二个方法是对 `IEnumerable` 的显式非泛型接口实现。为什么我们必须这样做?答案很简单,因为 `IEnumerator` 实现了非泛型的 `IEnumerable` 接口。而且,由于我们实现了 `ICollection`,我们必须考虑它的非泛型根。

现在我们知道将如何收集业务对象了,让我们继续了解我们将如何使用自定义泛型 `Enumerator` 来检索它们。

BusinessObjectEnumerator<T> 类

下面是我们用于枚举 `BusinessObjectCollection` 类实例的自定义泛型 `Enumerator` 的代码定义。代码很简单易懂,有几个主要关注点,我将在下面进行介绍。

 public class BusinessObjectEnumerator<T> : IEnumerator<T> where T : BusinessObjectBase 
{
    protected BusinessObjectCollection<T> _collection; //enumerated collection
    protected int index; //current index
    protected T _current; //current enumerated object in the collection

    // Default constructor
    public BusinessObjectEnumerator()
    {
        //nothing
    }

    // Paramaterized constructor which takes
    // the collection which this enumerator will enumerate
    public BusinessObjectEnumerator(BusinessObjectCollection<T> collection)
    {
        _collection = collection;
        index = -1;
        _current = default(T);
    }

    // Current Enumerated object in the inner collection
    public virtual T Current
    {
        get
        {
            return _current;
        }
    }

    // Explicit non-generic interface implementation for IEnumerator
    // (extended and required by IEnumerator<T>)
    object IEnumerator.Current
    {
        get
        {
            return _current;
        }
    }

    // Dispose method
    public virtual void Dispose()
    {
        _collection = null;
        _current = default(T);
        index = -1;
    }

    // Move to next element in the inner collection
    public virtual bool MoveNext()
    {
        //make sure we are within the bounds of the collection
        if (++index >= _collection.Count)
        {
            //if not return false
            return false;
        }
        else
        {
             //if we are, then set the current element
             //to the next object in the collection
            _current = _collection[index];
        }
        //return true
        return true;
    }

    // Reset the enumerator
    public virtual void Reset()
    {
        _current = default(T); //reset current object
        index = -1;
    }
}

首先,您应该注意此类声明的第一行

public class BusinessObjectEnumerator<T> : IEnumerator<T> where T : BusinessObjectBase 

如您在此处所见,我们实现了相同的泛型约束,以确保我们将要枚举的对象类型派生自 `BusinessObjectBase`。

我们还定义了三个成员变量,我们将用它们来控制枚举的流程。它们是

protected BusinessObjectCollection<T> _collection; //enumerated collection
protected int index; //current index
protected T _current; //current enumerated object in the collection

第一个是正在遍历的 `BusinessObjectCollection` 的内部实例。此变量通过 `BusinessObjectEnumerator` 类中的参数化构造函数设置。当 `foreach` 循环调用 `BusinessObjectCollection` 类的 `GetEnumerator()` 方法时,`BusinessObjectEnumerator` 本身会被实例化。每次找到一个项目时,`foreach` 循环就使用上面找到的 `MoveNext()` 方法移动到集合中的下一个项目。

另外两个成员变量只是对正在读取的类型为 `T` 的当前对象的引用,以及一个索引,用来告诉我们在集合中的位置。其余的 `IEnumerator` 实现都比较容易理解,所以我不会在这里浪费您的时间或让您的眼睛疲劳来解释所有内容。

现在您已经定义了所有类并使它们协同工作,剩下的就是编写一些“驱动”程序或示例程序来演示自定义泛型集合的功能。我包含了一个示例控制台应用程序,演示了此处解释的泛型类的用法,以及类本身的源代码。我还添加了另一个我编写的控制台应用程序,用于测试使用不同泛型和非泛型类型、进行装箱/拆箱和类型转换时,不同集合操作所需的时间。

结论

自定义泛型集合可以为您的应用程序带来巨大优势,并为您节省大量时间和麻烦。通过创建一个类型安全的环境来收集您的对象,您可以从一开始就无需担心运行时实际存储在集合中的内容。您还可以通过不硬编码数据类型来自由地重用和扩展您的代码。您可以轻松地删除上述类中的约束,以创建适合您确切需求的解决方案,并且将来也可以扩展以满足您的需求。

您可能会想,“嗯,这一切都很好,但为什么不直接使用 `List` 并完成它呢?”嗯,您可以!在 C# 中有许多不同的方法来实现类型安全和多态数据结构。这只是一个例子。我希望本文能对某些人有所帮助,并期待收到社区的反馈。

参考文献

© . All rights reserved.