下载本文的代码:/Files/dczsf/Predicates.rar
本页内容
深入探讨谓词 | |
System.Predicate 和 System.Action | |
System.Converter | |
您要从此转到何处? |
不是我懒,必须手动迭代集合的所有成员并对每个成员执行操作真的是很麻烦。我希望我能够只需告诉集合要对每个成员做什么,然后让它自己执行迭代。嗯,猜猜会发生什么?在我最近对 Microsoft® .NET Framework 的探索中,我恰好发现了该软件及其他恼人的数组和列表问题的解决方案。结果,.NET Framework 2.0 的 System.Array 和 System.Collections.Generic.List 类提供许多方法(例如 Find、FindAll 和 FindLast),用户无需编写代码来遍历数组或列表的每个元素,就可以找到他们所要查找的一个或多个项目。用户能够“走遍”整个数据结构以确定每个项目是否符合一组条件,而无需手动编写样板代码来遍历每一行。另外,因为谓词(本专栏的重点)仅仅是要调用的过程的地址,实际上,它允许或拒绝集合中的每个项目,所以用户可在运行时轻松地更改搜索条件。
谓词利用 .NET Framework 2.0 中新增的一般功能(Framework 的先前版本中缺乏这些功能,使得这类解决方案变得更加困难)。在形式上,.NET Framework 文档这样定义 System.Predicate 委托:
实际上,这个定义表示充当谓词的函数所采用的参数必须是单值(其类型必须与要处理的数组或列表中的数据类型相同),而且必须返回 Boolean 值。返回值指示传送到过程的值是否满足将其包括在内的特定条件。
下面是一个简单的示例:想像您已用一些随机数填充了一个字节数组,并想要检索一个包含所有小于 50 的值的数组。您可以迭代原始数组中的每个项目,将每个值与 50 进行比较,然后将相应的值复制到新数组中。您也可调用 Array.FindAll 方法,以传送数组和 System.Predicate 委托实例的地址。Array.FindAll 方法使用您所提供的判定函数返回相应的数组作为其返回值。
可使用以下函数作为谓词:
然后,可使用类似下列项目的代码来检索数组:
虽然这个示例有点不太自然,但它确实显示出详细内容。如果您需要多次使用某一特定谓词,则您可能希望创建一个引用它的变量,如下所示:
然后,您可调用 Array.FindAll 方法,如下所示:
虽然在其他方面您必须编写的代码数量不是特别大,但我好像经常编写迭代对象集合的代码。无论是在编写代码还是执行代码时,使用谓词都可节省时间。
即使 System.Array 类提供所有与谓词相关的方法作为共享方法,System.Collections.Generic.List 类也会提供类似的方法作为实例方法。因此,您可为 List 对象修订先前的代码,如下所示:
用随机字节填充该 List,然后……
方法 | Array | List | 说明 |
---|---|---|---|
ConvertAll | 将一种类型的数组或列表转换为另一种类型的数组或列表。(此方法实际上不使用 System.Predicate 委托。而是使用相似的 System.Converter 委托。) | ||
Exists | 确定所指定的数组或列表中是否含有任何与指定的谓词所定义的条件相匹配的元素。 | ||
Find | 搜索与指定谓词所定义的条件相匹配的元素,并返回整个 Array 或 List 中第一个符合条件的元素。 | ||
FindAll | 检索所有与指定谓词定义的条件相匹配的元素。 | ||
FindIndex | 搜索与指定谓词定义的条件相匹配的元素,并返回数组中第一个以零为基础的索引。 | ||
FindLast | 搜索与指定谓词定义的条件相匹配的元素,并返回整个数组或列表中最后一个符合条件的元素。 | ||
FindLastIndex | 搜索与指定谓词定义的条件相匹配的元素,并返回数组中最后一个以零为基础的索引。 | ||
ForEach | 对指定数组或列表中的每个元素执行指定的操作。(此方法实际上不使用 System.Predicate 委托。而是使用相似的 System.Action 委托。有关详细信息,请参阅示例。) | ||
RemoveAll | 从列表中删除所有与指定谓词定义的条件相匹配的元素。 | ||
TrueForAll | 确定数组或列表中的每一个元素是否都与指定谓词定义的条件相匹配。 |
System.Array 和 System.Collections.Generic.List 类都提供许多可以利用谓词的方法,如图 1 所示。(实际上,ConvertAll 和 ForEach 方法不使用 System.Predicate 委托。这些方法与使用谓词的方法类似,因此在这里我将它们包括在内。其中的概念与您已经看见的相同,但这些方法使用的却是 System.Action 或 System.Converter 委托。)
为了演示所有这些方法,我构建了一个简单的示例应用程序,如图 2 所示。此示例使用与 C:\Windows 文件夹中所有文件对应的 System.IO.FileInfo 对象填充 Array 和 List 实例,并允许您试验涉及委托的各种方法,并将结果显示在窗体的 ListBox 控件中。(为了演示 System.Action 委托,ForEach 方法示例还允许您确定输出位置。)
图 2 FindAll(使用 IsLarge)的结果
窗体的类定义四个变量,可用于整个应用程序:
List 和 Array 变量包含文件信息。各种过程赋值给 action 和 match 变量,允许不同的过程使用不同的条件来匹配文件和处理文件。这些变量都可充当委托实例,也就是说,代码将过程的地址分配给每个变量,这样一来使用委托的 Array 和 List 方法便可将这些变量作为参数进行传送。
窗体在加载时调用 RefillFileInformation 方法,使用文件信息填充 List 和 Array 实例,然后在窗体的 ListBox 中显示 List 的内容,正如您在图 3 中所看到的一样。这个过程使用 List.ForEach 方法在 ListBox 内显示项目:
DisplayFullList 过程必须属于 System.Action 委托类型(即,它必须是接受单一参数的子例程),反过来它可将每个项目添加到窗体上的 ListBox 中:
正如您从结果猜测到的,List.ForEach 方法为列表中的每个项目调用 DisplayFullList 方法,而 DisplayFullList 方法在 ListBox 控件中显示项目。
样例窗体包含两个 GroupBox 控件,允许您为 match 和 action 变量指定委托实例。例如,单击“Small Files (<500 bytes)”(小文件(<500 字节))时,相应的 CheckedChanged 事件处理程序会运行以下代码:
单击“Large Files (>1MB)”(大文件 (>1MB))时,会运行以下代码:
单击“Display”(显示)组框内的两个选项中的任何一个时,将会运行以下代码:
IsSmall 过程与您先前看见的 System.Predicate 过程非常相似(IsLarge 过程只是修改了大小条件)。IsSmall 和 IsLarge 过程的要点只是确定数组或列表中的给定项目是否满足特定条件:
System.Action 委托的两个实例与以下代码段类似(代码迭代数组或列表时,样例应用程序使用它们来确定如何处理每一个 FileInfo 对象):
TrueForAll、Exists、Find、FindAll、FindLast、RemoveAll、FindIndex 和 FindLastIndex 方法都使用 System.Predicate 委托的实例来执行任务。图 4 显示从使用所有这些方法的样例应用程序提取的代码行。图 5 显示调用 FindAll 方法(使用 IsLarge 谓词仅匹配大于 1MB 的文件)的结果。
图 5 将谓词与 List 和 Array 一起使用
List 和 Array 类的 ForEach 方法使用 System.Action 委托来描述对数据结构的每个元素所执行的操作。您不必编写循环来迭代数据结构的每个元素,您可使用 ForEach 方法来替您做这项工作。当然,编写循环不是一件繁重的任务,所以它实际上不是使用 ForEach 的益处。在我的测试中,使用 ForEach 也不比手动循环提高性能。不,使用 ForEach 的真正益处在于:您只需更改为数据结构中每个元素调用的过程的地址,便可更改为每个元素执行的操作。
假设样例项目中的操作变量引用 System.Action 委托(描述应为每个 FileInfo 对象执行的操作)的实例,则单击窗体中的 ForEach 按钮会运行以下代码:
根据操作变量的值,代码将在列表框或输出窗口中显示文件信息。更改行为不会要求在代码内包括一个决定,操作变量会精确定义您要对数据结构的每个元素执行的操作。样例窗体允许您选择 DisplayInListBox 或 DisplayInOutputWindow 作为操作变量的值。
最近,我需要将整数数组转换为字符串数组,所以我在数组上调用 String.Join 方法。我花了很长时间,试图找到某个更简单的方法:只需编写一行代码,即可将数组内的每个项目从整数转换为字符串。我最终编写出以下代码(给定数组的名称为 integerValue 和 stringValues):
我想要的只是能够调用一个过程来达到预期目的。遗憾的是,我不了解 Array.ConvertAll 方法。使用这个方法,您可提供 System.Converter 委托实例来为每个单个项目执行转换,然后调用 ConvertAll 方法完成工作。您不必为创建输出数组或用值填充数组而担心。
对于先前的示例,我可能已创建了一个转换器过程,如下所示:
为调用该过程,我可能已编写了以下代码:
调用 ConvertAll 方法时需要加点小心;您必须提供输入和输出类型,以及要转换的数组和 System.Converter 委托实例的地址。
因为 List 类提供 ConvertAll 方法作为实例方法,您只需提供输出类型。因此,调用 List 实例的 ConvertAll 方法会略微容易一些,并且看起来可能与以下内容类似:
样例窗体提供一个相似示例,将 FileInfo 对象转换为字符串(通过返回 FileInfo 对象的 FullName 属性)。样例使用以下转换器:
单击样例窗体上的 ConvertAll 按钮运行以下代码:
调用 ConvertAll 过程时必须提供泛型输出类型,这看起来好像很奇怪。也就是说,您将期望您能调用如下所示的过程:
因为编译器需要知道转换的输出类型,所以您必须在编写代码时提供此信息。因为它是一个共享的方法,因此编译器没有关于输入或输出类型的任何信息,调用 Array.ConvertAll 方法需要您提供输入和输出类型:
您需要多尝试使用这些方法,然后才能消化理解这些语法;但是,一旦您掌握了其中的概念,调用 ConvertAll 方法即可在您创建代码时以及运行时为您节省时间。
正如您可能已猜测到的,泛型已逃到 .NET Framework 2.0 的许多角落。如果您还没有仔细研究这个重要的新功能,请花点时间了解更多信息。我在 2005 年 9 月专栏中引入了泛型,并且您还会在 MSDN®online 上找到关于使用和创建泛型过程的许多其他信息。如果在 .NET Framework 中偶然遇到要求您提供泛型实例的方法(如此处所示的示例),请不要逃跑,请坐下来尝试设计出详细的内容。通过在应用程序中利用泛型,您可节省许多时间和精力。