C# 2.0 FlexKeyCollection






4.81/5 (10投票s)
2004年9月30日
13分钟阅读

64328

527
使用 C# 2.0 泛型实现灵活的业务对象集合
引言
.NET 2.0 版本将添加一些新功能,包括泛型。在 C++ STL 和 Microsoft 的 ATL 库上花了一些时间后,当 2.0 Beta SDK 可用时,我很有兴趣了解 .NET 泛型的细节。我决定创建一个泛型业务对象集合类,以便在未来的项目中 M 使用(也避免在典型的简单示例中浪费时间。 ;-) 我将尝试描述这项工作的成果。
背景
如果您没有花很多时间使用泛型,那么泛型可能会变得复杂和令人困惑。另一方面,它们可能非常强大。如果您不知道什么是泛型或 C++ 模板,那么另一个更简单的示例可能更适合入门。如果您想深入“更深层”的领域,那就来吧。这并不是您需要了解的关于 C# 泛型的全部内容,只是对一些功能的简要回顾以及一个您可能会觉得有用的真实示例。
我心目中的“业务对象”是一个包含状态的类,用于模拟某些业务实体。我通常更喜欢使用面向对象的(Object Oriented)设计来构建应用程序,而不是使用 ADO DataSets 来存储“业务对象”。在本篇回顾中,我将使用一个 Customer 对象作为示例,它将包含您期望的 Name、Address、PhoneNumber 等。此外,关系数据库始终是我项目的一部分,因此我希望拥有一些能与数据库持久化层良好配合的功能。
FlexKeyCollection 的通用设计目标
FlexKeyCollection 设计用于存储像我们 Customer 示例这样的典型业务对象。我已将集合的需求分为以下几类。如果您有使用的功能而 FlexKeyCollection
中没有,请发表评论。由于这是基于 Beta SDK 构建的,我希望在 .NET 2.0 版本发布后提供一个修订版本。
持久化交互(数据库检索/更新/插入/删除)
- 检查集合中的所有对象是否“有效”。
- 持久化已更改的对象(更新)
- 在插入新对象后更新对象键(插入)。
- 从数据库中删除对象后,将其从集合中删除。(删除)
GUI 交互
- 使用各种排序顺序显示集合。
- 显示过滤后的集合元素。
- 通过键检索集合对象进行编辑。
定义和实例化
从头开始,让我们回顾一下如何从 FlexKeyCollection 定义一个新集合。类定义如下所示
public class FlexKeyCollection<K, V> :
System.Collections.Generic.IEnumerable<V>
K(键)和 V(值)是您在创建新的 FlexKeyCollection 时必须指定的类型参数。
首先从 K(键)开始。Customer 通常会有一个唯一的 CustomerID 值。我经常使用数据库自动生成的整数值来表示。如果您使用的是 String?Double?GUID?还是其他数据类型的 CustomerID 呢?上面的 K 是泛型类型参数,它允许集合用户决定 Customer 键的数据类型。在我们的示例中,我将使用 **int**。我们的 V(值)是我们存储的 Customer 对象。因此,要创建一个包含 Customer 的新 FlexKeyCollection
,我们将编写
FlexKeyCollection<int, Customer> _custCollection;
现在我们已经定义了集合并指定了键和值类型,我们需要构造或实例化集合。FlexKeyCollection 构造函数如下所示
public FlexKeyCollection(Key<K, V> del)
这里有点混乱,但请坚持下去,这是值得的。构造函数中的 Key<K, V> del 是一个泛型委托,定义如下
public delegate K Key<K, V>(V value);
这基本上是一个函数指针,由您(集合的用户)提供给构造函数。每当您向集合中添加新 Customer 时,集合都会使用此指针来查找其键。您可以使用任何您喜欢的函数,只要它符合上述委托。因此,例如
假设我们的 Customer 类有一个 CustomerID 属性,该属性返回一个整数。Key 委托定义接受一个 V 值参数,并返回一个 K 值。根据上面的定义,K 是 int,V 是 Customer,所以我们的函数必须是这样的
public int Foo(Customer value)
请注意,我将函数命名为 Foo。函数名称无关紧要。所以,让我们用一个更好的名字来完成我们的函数。public int CustomerKey(Customer value){
// return unique Customer Key
return value.CustomerID;
}
现在我们有了委托函数,让我们完成 FlexKeyCollection
构造函数调用。
_custCollection = new FlexKeyCollection<int, Customer>(
new Key<int, Customer>(CustomerKey));
呼!一开始看起来很混乱,但只要稍加练习,阅读和编写泛型方法调用就会变得更容易,并带来丰厚的回报。我还有一件事想补充,这将使它更进一步。.NET 2.0 添加了一个名为匿名块的新功能。在上面的示例中,我们被迫定义一个新函数用作我们的 Key 委托。这意味着需要将此函数添加到我们的 Customer 类中。为了避免修改所有业务对象类以添加 Key 委托函数,让我们使用匿名块。使用此技术,我们无需将函数添加到所有业务对象类中。新的 FlexKeyCollection
构造函数调用将如下所示
_custCollection = new FlexKeyCollection<int, Customer>(
delegate (Customer cust){return cust.CustomerID;});
在这里,我们没有创建指向 CustomerKey 函数的新 Key 委托,而是直接添加了匿名块,它接收一个 Customer 参数并返回 CustomerID 键值。很简单,对吧?我花了一些时间试图弄清楚这一点,还没有弄清楚这种匿名委托的编写逻辑,但它确实有效!可能会出现一些问题。
- 我们知道需要 Key 委托,但这个匿名块没有指定委托名称“Key”?
- Key 委托要求我们传递一个 V(在本例中是 Customer)。好的,但它还要求我们返回一个 K 或整数。在匿名定义中没有指定返回类型?
现在我们已经定义并构造了集合,我们需要添加一些 Customer 对象。之前所做的一切都使添加 Customer 变得容易。
_custCollection.Add(new Customer());
由于集合知道如何通过 Customer(使用传递给构造函数的 Key 委托)检索键,因此添加操作非常简单。一个重要的考虑因素是键必须是唯一的。对于数据库来说,这通常不是问题,因为 CustomerID 定义了唯一约束。您会遇到的一个问题是创建尚未持久化到数据库并且尚未分配唯一键的新 Customer。一种解决方案是使用递减计数器,将每个新的 CustomerID 号设置为 counter--。对于大多数数据库,自动递增的 ID 号从 0 或 1 开始,因此从 -1 开始并递减将确保所有 CustomerID 都是唯一的,并且不会与现有的数据库记录冲突。这是一个很大的话题,所以我不会深入探讨,但当我们考虑将 Customer 集合持久化到数据库时,我们会重新讨论。
我们将再次使用匿名委托来实现排序、过滤和其他集合方法,因此对此的良好理解将帮助您有效地使用集合的其余部分。
排序
如果您不需要对集合进行排序或过滤,那么 .NET 2.0 框架类 Dictionary<K, V>
类可能更合适。如果您使用 Grid 或 ListBox 控件来显示对象组,那么通常点击列标题会根据该列的值对集合进行排序。这是 Dictionary<K, V> 无法提供的,而 FlexKeyCollection
可以轻松处理。FlexKeyCollection 中的排序和过滤是使用整数数组索引完成的,这在某些方面类似于数据库索引。如果您对细节感兴趣,请查看 FlexKeyCollection
的 Sort、Filter 和 Enumeration 方法。
重要提示
- 排序和过滤仅更改集合的枚举。因此,如果您调用 Sort() 方法,所有后续的
foreach(...)
枚举将按排序顺序返回对象。调用Filter()
方法将只返回枚举中过滤后的对象。this[K key] 集合索引器将返回集合中的任何对象,而不管排序或过滤。 - 当调用
Add()
或Remove()
方法时,排序和过滤会自动移除。这是因为由于缺少或添加的对象,排序和过滤索引已失效。为了在添加或删除对象后重新应用排序或过滤,请再次调用 Sort()/Filter() 方法。
**详细信息** 排序是通过 Array.Sort()
方法完成的,该方法使用快速排序算法。我的测试表明,在相对现代的硬件上,它可以在 1 秒内对 100 万个整数进行排序。过滤只是迭代整个集合以进行传递/失败测试。由于我的业务对象集合通常包含少于 100 个项目,因此这应该非常快。如果您决定将其用于大型数值计算,我建议进行测试以确保速度足够。特别是 Remove()
方法没有针对速度进行优化,因此频繁的 Add/Remove 以及非常大的集合的 Sort/Filter 可能会导致一些性能问题。
现在让我们看看如何使用排序和过滤方法。Sort 方法如下所示
public void Sort(Comparison<V> comp)
我们又看到了委托!Comparison<V>
是 System 的一部分;并定义为public sealed delegate int Comparison<V>(V x, V y);
与之前一样,这告诉我们 Comparison 委托接受 2 个 V 或 Customer 并返回一个整数。返回的整数必须与 IComparable
接口相同,其中
- x < y 返回 -1
- x = y 返回 0
- x > y 返回 +1
由于字符串、int、DateTime 等值类型实现了 IComparable
,我们可以使用它来实现我们的匿名 Comparison<V>
委托,以便按 Customer.Name
属性(字符串)排序,如下所示
_custCollection.Sort(delegate (Customer x, Customer y)
{return x.Name.CompareTo(y.Name);});
如果要按其排序的属性**不**实现 IComparable
,则需要手动执行 x <=> y 比较,而不是使用 CompareTo()
。只需确保返回正确的比较值,否则排序将无法按预期工作。按 Customer 对象的任何其他属性排序与上面行中更改“Name”一样简单。这意味着您可以按您喜欢的任何 Customer 属性对集合进行排序,例如当最终用户单击 Grid 或 ListBox 中的列标题时,调用 Sort()
并传入他们单击的任何属性,然后重新加载列表。
通过调用 ReverseSort()
可以简单地反转排序。这将向前和向后切换当前排序。
过滤
过滤遵循类似的技术,但不是使用 Comparison<V>
委托,过滤使用 System 中定义的 Predicate<V>
委托,如下所示
public sealed delegate bool Predicate<V>(V obj);
这里的 Predicate 委托函数接受一个 V Customer 参数并返回一个 bool。因此,过滤我们的 Customer 集合以查找北卡罗来纳州的客户将如下所示
_custCollection.Filter(delegate (Customer x)
{return x.State == 'NC';});
请注意,枚举过滤后的集合将**只**返回 Predicate 委托返回 true 的对象。由于**您**定义了过滤逻辑,因此您可以使其尽可能简单或复杂。关于将 Customer 集合持久化到数据库,有一个想法浮现:我们需要持久化新建、已更改和已删除的 Customer,因此我们可以创建一个过滤器,只获取新建、已更改或已删除的 Customer。这假设我们的 Customer 对象具有 IsNew
、IsChanged
和 IsDeleted
属性。
_custCollection.Filter(delegate (Customer x)
{return x.IsNew || x.IsChanged || x.IsDeleted;});
在简要讨论其他 FlexKeyCollection
方法时,我们将进一步探讨这一点。此外,通过调用 SortAndFilter(Comparison<V> comp, Predicate<V> pred)
可以同时进行排序和过滤,其特性与单独的 Sort 和 Filter 相同。
排序和过滤改进?
我开始在 FlexKeyCollection
中构建方法,以按名称字符串存储排序和过滤。目的是避免每次使用时都重新构建排序/过滤索引。这增加了一些复杂性,主要集中在 Add/Remove 方法使用时更新存储的排序和过滤。我决定保持 FlexKeyCollection
“轻量级和简单”,并省略了存储的排序和过滤。由于 FlexKeyCollection
旨在存储少量对象(<100),因此按需构建排序和过滤所需的时间应该可以忽略不计。如果您需要此功能,请告诉我,我将努力完成“发布”版本。
我考虑的另一个改进是多属性排序。这将允许您按州和姓名对 Customer 集合进行排序。保持 FlexKeyCollection“整洁”是设计的一部分,因此如果需要,可以添加一个具有附加功能的“重型”版本。
FlexKeyCollection 剩余部分
一旦您理解了上面讨论的委托,大多数其他 FlexKeyCollection
方法都应该是直接的。还有一种方法我想回顾一下,叫做 ActionIf()
。方法签名如下
public void ActionIf(Predicate<V> pred, Action<V> act))
我们之前已经见过 Predicate 委托,但 Action<V>
是新的。同样,这是一个在 System 中定义的委托,如下所示
public sealed delegate void Action<V>(V obj);
希望这会变得有点乏味。基本上,每当 Predicate 委托返回 true 时,都会调用 Action 委托。很酷吧!因此,如果我们再次考虑我们的 Customer 集合持久化,我们可以这样写
_custCollection.ActionIf(delegate (Customer x)
{return x.IsNew || x.IsChanged || x.IsDeleted;},
delegate (Customer c){c.Save()};);
基本上,上面的一行代码将检查我们集合中的每个 Customer 是否为新建、已更改或已删除,然后**只**对这些 Customer 调用 Customer.Save()
方法!哇!我第一次看到这个的时候,确实费解了很久。我们有一行代码,有 3 个行尾分号和 2 对大括号!要正确掌握这种语法需要一些练习,但回报是巨大的!
关键问题
关于持久化我们的 Customer 集合,我们还需要审查最后一点。**不要跳过这一部分。**我们集合中的新 Customer 将具有“虚假”的 CustomerID 值,如上所述,使用自动递减计数器。这意味着一旦它们被持久化并从数据库分配了“真实”ID 值,我们就需要更新 CustomerID。由于我们的集合是以 CustomerID 键为索引的,因此更改该键值**会破坏我们的集合**!但别担心,有一个方法可以纠正这个问题。FlexKeyCollection
有一个方法 **RebuildKeyIndex()**。**必须调用**此方法以在键更改后进行更新。您不需要每次更改都调用它,只需在所有更改完成后调用即可。所以回到我们上面的 ActionIf()
调用,我们添加了一个 RebuildKeyIndex()
调用,如下所示
__custCollection.ActionIf(delegate (Customer x)
{return x.IsNew || x.IsChanged || x.IsDeleted;},
delegate (Customer c){c.Save()};);
_custCollection.RebuildKeyIndex();
这假设我们的 Customer 对象在 Save() 方法期间更新其 CustomerID 值。现在,我们持久化的 Customer 集合已更新键,并且可以再次正常工作。如果您坚持到这里,您可能会回想起我们对构造函数的初步回顾。在其中,我们传递了一个 Key<V>
委托。需要此委托才能重建 Key 索引。您可能还想知道为什么集合不能自动重建 KeyIndex?好吧,集合在其中包含的对象**任何**属性发生更改时都不会收到通知。这可能是 FlexKeyCollection 的一个改进,但它会限制可以存储在集合中的对象。
结论
.NET 2.0 版本正在添加一些强大的新功能,如泛型和匿名块。我希望这篇文章能给您提供一些关于如何使用这些新功能的想法。我知道在整理这篇文章的过程中,我学到了很多东西。
下载内容包含 FlexList
、FlexKeyCollection
、一个 FlexTest
业务对象类和一个用于测试集合的 ConsoleTest
类,以及一个 Build.txt 文件。我使用了**此处**免费提供的 .NET Version 2.0 SDK 来构建此程序。在此处插入标准的 Beta 软件免责声明 :-/
历史
- 初始 Beta 2004 年 9 月 29 日