谓词和操作
Ken Getz
下载本文的代码:AdvBasics2006_09.exe (178KB)

本页内容
![]() |
深入探讨谓词 |
![]() |
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 委托:
Public Delegate Function Predicate(Of T)(obj As T) As Boolean
实际上,这个定义表示充当谓词的函数所采用的参数必须是单值(其类型必须与要处理的数组或列表中的数据类型相同),而且必须返回 Boolean 值。返回值指示传送到过程的值是否满足将其包括在内的特定条件。
下面是一个简单的示例:想像您已用一些随机数填充了一个字节数组,并想要检索一个包含所有小于 50 的值的数组。您可以迭代原始数组中的每个项目,将每个值与 50 进行比较,然后将相应的值复制到新数组中。您也可调用 Array.FindAll 方法,以传送数组和 System.Predicate 委托实例的地址。Array.FindAll 方法使用您所提供的判定函数返回相应的数组作为其返回值。
可使用以下函数作为谓词:
Private Function IsSmall(ByVal value As Byte) As Boolean Return value < 50 End Function
然后,可使用类似下列项目的代码来检索数组:
Private Function GetSmallBytes(ByVal values() As Byte) As Byte() Return Array.FindAll(values, AddressOf IsSmall) End Function
虽然这个示例有点不太自然,但它确实显示出详细内容。如果您需要多次使用某一特定谓词,则您可能希望创建一个引用它的变量,如下所示:
Dim pred As New System.Predicate(Of Byte)(AddressOf IsSmall)
然后,您可调用 Array.FindAll 方法,如下所示:
Dim smallValues() As Byte = Array.FindAll(values, pred)
虽然在其他方面您必须编写的代码数量不是特别大,但我好像经常编写迭代对象集合的代码。无论是在编写代码还是执行代码时,使用谓词都可节省时间。
即使 System.Array 类提供所有与谓词相关的方法作为共享方法,System.Collections.Generic.List 类也会提供类似的方法作为实例方法。因此,您可为 List 对象修订先前的代码,如下所示:
Dim valueList As New List(Of Byte)
' 用随机字节填充该 List,然后……
Dim smallValues As List(Of Byte) = valueList.FindAll(AddressOf IsSmall)
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)的结果
窗体的类定义四个变量,可用于整个应用程序:
Private fileList As New List(Of FileInfo) Private fileArray() As FileInfo Private action As System.Action(Of FileInfo) Private match As System.Predicate(Of FileInfo)
List 和 Array 变量包含文件信息。各种过程赋值给 action 和 match 变量,允许不同的过程使用不同的条件来匹配文件和处理文件。这些变量都可充当委托实例,也就是说,代码将过程的地址分配给每个变量,这样一来使用委托的 Array 和 List 方法便可将这些变量作为参数进行传送。
窗体在加载时调用 RefillFileInformation 方法,使用文件信息填充 List 和 Array 实例,然后在窗体的 ListBox 中显示 List 的内容,正如您在图 3 中所看到的一样。这个过程使用 List.ForEach 方法在 ListBox 内显示项目:
fileList.ForEach(AddressOf DisplayFullList)
DisplayFullList 过程必须属于 System.Action 委托类型(即,它必须是接受单一参数的子例程),反过来它可将每个项目添加到窗体上的 ListBox 中:
Private Sub DisplayFullList(ByVal file As FileInfo) completeListBox.Items.Add( _ String.Format("{0}({1} 字节)", file.Name, file.Length)) End Sub
正如您从结果猜测到的,List.ForEach 方法为列表中的每个项目调用 DisplayFullList 方法,而 DisplayFullList 方法在 ListBox 控件中显示项目。
样例窗体包含两个 GroupBox 控件,允许您为 match 和 action 变量指定委托实例。例如,单击“Small Files (<500 bytes)”(小文件(<500 字节))时,相应的 CheckedChanged 事件处理程序会运行以下代码:
match = New System.Predicate(Of FileInfo)(AddressOf IsSmall)
单击“Large Files (>1MB)”(大文件 (>1MB))时,会运行以下代码:
match = New System.Predicate(Of FileInfo)(AddressOf IsLarge)
单击“Display”(显示)组框内的两个选项中的任何一个时,将会运行以下代码:
action = New System.Action(Of FileInfo)(AddressOf DisplayInListBox) ' 或 action = New System.Action(Of FileInfo)(AddressOf DisplayInOutputWindow)
IsSmall 过程与您先前看见的 System.Predicate 过程非常相似(IsLarge 过程只是修改了大小条件)。IsSmall 和 IsLarge 过程的要点只是确定数组或列表中的给定项目是否满足特定条件:
Private Function IsSmall(ByVal file As FileInfo) As Boolean Return file.Length < 500 End Function
System.Action 委托的两个实例与以下代码段类似(代码迭代数组或列表时,样例应用程序使用它们来确定如何处理每一个 FileInfo 对象):
Private Sub DisplayInListBox(ByVal file As FileInfo) AddStringToListBox(String.Format("{0}({1} 字节)", _ file.Name, file.Length)) End Sub Private Sub DisplayInOutputWindow(ByVal file As FileInfo) Debug.WriteLine(String.Format("{0}({1} 字节)", _ file.Name, file.Length)) End Sub
System.Predicate 和 System.Action
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 按钮会运行以下代码:
fileList.ForEach(action) ' 或 Array.ForEach(fileArray, action)
根据操作变量的值,代码将在列表框或输出窗口中显示文件信息。更改行为不会要求在代码内包括一个决定,操作变量会精确定义您要对数据结构的每个元素执行的操作。样例窗体允许您选择 DisplayInListBox 或 DisplayInOutputWindow 作为操作变量的值。
System.Converter
最近,我需要将整数数组转换为字符串数组,所以我在数组上调用 String.Join 方法。我花了很长时间,试图找到某个更简单的方法:只需编写一行代码,即可将数组内的每个项目从整数转换为字符串。我最终编写出以下代码(给定数组的名称为 integerValue 和 stringValues):
Dim stringValues(integerValues.Length – 1) As String For i As Integer = 0 To integerValues.Length - 1 stringValues(i) = integerValues(i).ToString Next Return String.Join(", ", stringValues)
我想要的只是能够调用一个过程来达到预期目的。遗憾的是,我不了解 Array.ConvertAll 方法。使用这个方法,您可提供 System.Converter 委托实例来为每个单个项目执行转换,然后调用 ConvertAll 方法完成工作。您不必为创建输出数组或用值填充数组而担心。
对于先前的示例,我可能已创建了一个转换器过程,如下所示:
Private Function MyConverter(ByVal value As Integer) As String Return value.ToString() End Function
为调用该过程,我可能已编写了以下代码:
stringValues = Array.ConvertAll(Of Integer, String)( integerValues, AddressOf MyConverter)
调用 ConvertAll 方法时需要加点小心;您必须提供输入和输出类型,以及要转换的数组和 System.Converter 委托实例的地址。
因为 List 类提供 ConvertAll 方法作为实例方法,您只需提供输出类型。因此,调用 List 实例的 ConvertAll 方法会略微容易一些,并且看起来可能与以下内容类似:
stringValues = integerValues.ConvertAll(Of String)( AddressOf MyConverter)
样例窗体提供一个相似示例,将 FileInfo 对象转换为字符串(通过返回 FileInfo 对象的 FullName 属性)。样例使用以下转换器:
Private Function FileInfoToString(ByVal file As FileInfo) As String Return file.FullName End Function
单击样例窗体上的 ConvertAll 按钮运行以下代码:
Dim fileNames As List(Of String) = fileList.ConvertAll(Of String)(AddressOf FileInfoToString)
调用 ConvertAll 过程时必须提供泛型输出类型,这看起来好像很奇怪。也就是说,您将期望您能调用如下所示的过程:
' 此代码将不会编译: fileNames = fileList.ConvertAll(AddressOf FileInfoToString)
因为编译器需要知道转换的输出类型,所以您必须在编写代码时提供此信息。因为它是一个共享的方法,因此编译器没有关于输入或输出类型的任何信息,调用 Array.ConvertAll 方法需要您提供输入和输出类型:
fileNames = Array.ConvertAll(Of FileInfo, String)( fileArray, AddressOf FileInfoToString)
您需要多尝试使用这些方法,然后才能消化理解这些语法;但是,一旦您掌握了其中的概念,调用 ConvertAll 方法即可在您创建代码时以及运行时为您节省时间。
您要从此转到何处?
正如您可能已猜测到的,泛型已逃到 .NET Framework 2.0 的许多角落。如果您还没有仔细研究这个重要的新功能,请花点时间了解更多信息。我在 2005 年 9 月专栏中引入了泛型,并且您还会在 MSDN®online 上找到关于使用和创建泛型过程的许多其他信息。如果在 .NET Framework 中偶然遇到要求您提供泛型实例的方法(如此处所示的示例),请不要逃跑,请坐下来尝试设计出详细的内容。通过在应用程序中利用泛型,您可节省许多时间和精力。
请将您的问题和意见发送至 basics@microsoft.com。
Ken Getz 是 MCW Technologies 的高级顾问和 AppDev (http://www.appdev.com/) 的课件作者。他是《ASP .NET Developers Jumpstart》(ASP .NET 开发人员入门)(Addison-Wesley, 2002)、《Access Developer's Handbook》(Access 开发人员手册)(Sybex, 2002) 和《VBA Developer's Handbook, 2nd Edition》(VBA 开发人员手册,第 2 版)(Sybex, 2001) 的合著者。可通过 keng@mcwtech.com 与他联系。Ken 非常感谢 Russ Nemhauser 为本专栏提供了有趣的 IM 和启发。
本文摘自 2006 年 9 月发行的 MSDN 杂志。