引言
定义和使用类型和方法来处理非相关类型时,使用泛型将为您提供更好的编译时验证。这些类型和方法的示例包括用于存储任何类型数据的集合类型,以及对不同数据类型的数组进行排序的排序方法。在某些情况下,使用泛型还可以改善性能。Visual Basic 2005 支持在其中创建和使用泛型。本文解释了与定义和使用泛型有关的各种概念,还介绍了使用泛型为 Visual Basic 开发人员带来好处的几种情形。
泛型
让我们从常用的集合类开始进行说明,如下面的代码示例 1 所示。您可以在其中存储任意类型的元素,但为了处理任何非相关类型的元素,我们将元素作为 System.Object 类型的对象进行存储和处理。
示例 1:Class MyCollectionSub Add(Byval Key As String, Byval Element As Object) ...End Sub...End Class
使用该集合存在两个问题:
|
1. |
如果您需要使用该集合,但只想存储整数,应该怎么办?您仍然可以使用该集合,但可能会无意中将其他类型的元素存储到该集合中,如果您的代码不希望在集合对象中发现非整数元素,则可能造成运行失败。 |
|
2. |
在 System.Object 与结构、枚举和原始类型(例如 Integer、Date 等等)之间转换会增加系统开销,造成性能下降。 |
通过创建自己的集合并明确定义为只存储 Integer 类型的元素就可以解决这两个问题,如代码示例 2 所示。
示例 2:
Class MyIntegerCollection
Sub Add(Byval Key As String, Byval Element As Integer)
...
End Sub
...
End Class
现在您创建了明确用于存储整数的集合,但如果您需要明确用于存储日期的集合,该怎么办?这种情况下,您需要创建名为 MyDateCollection 的另一个集合,它与 MyIntegerCollection 集合非常类似,但专门用来处理日期。这样,如果您希望进行编译时类型检查(即,严格的类型检查)并获得更好的性能,您需要为每一种元素类型定义一个单独的集合类型。这将导致代码膨胀、效率下降,最后还将带来维护问题。
泛型集合可以帮助您避免重复代码,同时仍能提供严格类型检查的优点和更好的性能。代码示例 3 所示就是一个泛型集合,在该集合中,元素类型 (ElementType) 被参数化,您可以向其传递希望存储在该集合中的任意类型的元素。代码示例 3 包含 MyCollection(Of Integer)、MyCollection(Of Date) 等集合。在该示例中,ElementType 被指定为 MyCollection 类的类型参数,并可以在此类中作为普通类型使用。
示例 3:
Class MyCollection(Of ElementType)
Sub Add(Byval key As String, Byval Element As ElementType)
'注意,泛型中使用了类型参数 ElementType
'使用方法与其他类型相同
'
Dim Element2 As ElementType
...
End Sub
...
End Class
Dim IntegerCol As New MyCollection(Of Integer)
Dim DateCol As New MyCollection(Of Date)
注意,MyCollection(Of Integer) 明确定义为只接受整数,同样,MyCollection(Of Date) 只接受日期。而且,它们的实现可以处理类型参数类型 (ElementType) 而不是 System.Object。此外,当您使用 MyCollection(Of Integer) 时,用于处理 ElementType 的实现的中间语言 (IL) 在运行时进行实时编译以处理 Integer 类型,而无需转换为 System.Object。
除了类以外,您还可以将结构、接口和代理定义为泛型。并且,可以为泛型指定任意数量的类型参数。代码示例 4 所示就是一个泛型集合,该集合还可以明确定义为只接受任何类型的关键字。
示例 4:
Class MyCollection(Of KeyType, ElementType)
Sub Add(Byval key As KeyType, Byval Element As ElementType)
...
End Sub
...
End Class
泛型方法
与集合类似,有时您需要定义并使用各种方法,以处理各种不同数据类型的元素。Sort 方法就是一个例子,它可以处理不同类型的元素,例如整数、日期、字符串等等。为了处理不同的非相关类型,Sort 方法被定义为获取 System.Object 的数组,而且在其实现中也将对象作为 System.Object 类型进行处理,如代码示例 5 所示。
示例 5: Sub Sort(Byval Arr() As Object) ... End Sub
这与非泛型集合具有同样的问题,即严格的类型检查和性能问题。如果按照代码示例 6 所示,通过对 Sort 方法进行泛型处理即可解决这些问题。请注意,泛型 Sort 方法已使用普通参数前显示的类型进行了参数化。
示例 6: Sub Sort(Of T)(Byval Arr() As T) ... End Sub
现在您可以通过将类型参数值传递给类型参数 T,然后传入一般参数值来调用泛型 Sort 方法,如示例 7 所示。
示例 7:
Dim IntArray() As Integer = {2, 4, 1, 0}
Sort(Of Integer)(IntArray)
Dim StringArray() As String = {"A", "C", "B"}
Sort(Of String)(StringArray)
您也可以调用泛型 Sort 方法,但不为类型参数 T 提供类型参数值,而让编译器根据传递给一般参数 Arr 的参数值进行隐式推断。代码示例 8 显示了一种不同的方式,可以调用泛型 Sort 和 Compare 方法而不显式指定类型参数值。请注意编译器能够或不能推断类型参数值的情形。
示例 8: '无错误 – 编译器发现一般参数值的类型为 'Integer(),从而推断出类型参数的类型参数值为 Integer ' Sort(IntArray) '错误 – 编译器无法从值 Nothing 推断出任何类型 ' Sort(IntArray) Function Compare(Of T)(Byval Arg1 As T, Byval Arg2 As T) As Integer End Function '有效 – 编译器从传递给一般参数的参数值类型推断出 T 类型参数值的类型为 Integer ' ' Compare(20, 40) '错误 – 编译器无法推断出 T 的类型参数值,因为使用 '传递给一般参数的参数类型(Integer 和 String),\ '无法推断出 Integer(基于第一个参数) '或 String(基于第二个参数)类型参数,因此 '含义模糊。 ' Compare(20, "A")
约束
从前面的示例中,我们知道类型参数可作为一般类型用于定义它们的泛型类型或方法中。但是在该类型参数类型的对象中,可以访问哪些成员?什么样的转换才是合法的呢?例如,在代码示例 9 所示的 Sort 方法的实现中,您可以从传入的 Arr 数组中的对象访问哪些成员呢?无论传入什么样的类型参数,都可以访问 System.Object 的成员,因为所有托管对象都继承自 System.Object。在代码示例 9 中,System.Object.ToString 可以从变量 Element(其类型为类型参数 T)中进行访问。
示例 9: Sub Sort(Of T)(Byval Arr() As T) '执行排序任务 ... '在调试中显示元素 #If DEBUG Then For each Element As T In Arr '注意,可以从包含 T 类型对象的 Element 中直接访问 System.Object 的成员 ' ' Debug.WriteLine(Element.ToString) Next #End If End Sub
如果要比较 Sort 方法的 Arr 数组中的元素,该怎么办?您不能使用常用的比较运算符,因为根据调用过程中传入的类型,T 可能是任意类型,而常用的运算符并非对所有类型都适用。您可能会将元素转换为 IComparable 接口,然后对其调用 CompareTo 方法。但是,不能保证传递给 T 的类型可以实现 IComparable。通过将其指定为类型参数 T 的条件(如代码示例 10 所示),就可以保证这一点,从而告诉编译器只有实现 IComparable 接口的类型才能作为类型参数 T 的参数。为类型参数指定的这种条件被称为约束。在代码示例 10 中,我们添加接口 IComparable 作为类型参数 T 的约束,这样 T 类型的对象就可以转换为 IComparable。事实上,您可以从 T 类型的对象中直接访问 IComparable.CompareTo 方法,而无需将它们转换为 IComparable。代码示例 10 显示了在转换和不转换为 IComparable 这两种情况下,如何访问 IComparable.CompareTo 成员。
示例 10:
Sub Sort(Of T As IComparable)(Byval Arr() As T)
...
'将 T 类型的对象转换为 IComparable 后访问 IComparable.CompareTo 成员
'
'
CompareResult = CType(Arr(i), IComparable).CompareTo(Arr(j))
'可以直接从 T 类型的对象中访问 IComparable.CompareTo 成员,
'因为 T 具有 IComparable 接口约束。 '
CompareResult = Arr(i).CompareTo(Arr(j))
...
End Sub
在没有 IComparable 约束的情况下,如果传入的类型未实现 Icomparable,则转换到 IComparable 可能会在运行时失败,如代码示例 11 所示。事实上,在没有该约束的情况下,直接从 T 类型的对象中访问 CompareTo 将产生编译错误。
示例 11:
Sub Sort(Of T)(Byval Arr() As T)
...
'如果传递给 T 的类型未实现 IComparable,
'转换可能会失败
'
CompareResult = CType(Arr(i), IComparable).CompareTo(Arr(j))
'编译错误 – 不能从 T 类型的对象中访问 IComparable.CompareTo 成员,
'因为 T 没有 IComparable 约束
' '
CompareResult = Arr(i).CompareTo(Arr(j))
...
End Sub
Visual Basic 提供了两种约束 — 类型约束和 New 约束。
类型约束
如果要确保传递给某个类型参数的所有类型参数均继承或实现该类型,可以使用类型约束。可以将类和接口指定为类型约束。您可以从类型参数类型的对象中直接访问约束类或接口的成员。也可以将类型参数类型的对象转换为其约束类型。代码示例 12 将泛型 MyCollection 类型的元素类型约束为 Control,从而只允许为类型参数 ElementType 传递 Control 类型或继承自该类型(只有 Controls)的类型参数。使用该约束,您可以从 ElementType 类型的对象中访问 Control 的成员,如代码示例 12 所示。
示例 12:
Class MyCollection(Of KeyType, ElementType As System.Winforms.Forms.Control)
Sub Add(Byval Key As KeyType, Byval Element As ElementType)
'注意,Control 类中定义的 Name 属性可以从
'对象 Element 中访问,该对象的类型
'为类型参数 ElementType,因为 ElementType 具有类约束 Control。
'
Debug.WriteLine("Adding control " & Element.Name)
'注意,Element(其类型为类型参数 ElementType)
'可以转换为 Control,因为 ElementType 具有类约束
'Control '
Dim c As System.Winforms.Forms.Control = Element
...
End Sub
End Class
'编译错误,因为类型参数 Integer 不是继承自
'Control
Dim col1 As MyCollection(Of String, Integer)
'存放各种按钮的特定集合
Dim col2 As MyCollection(Of String, System.Winforms.Forms.Button)
代码示例 13 显示了使用接口类型作为约束的情况。现在,我们将泛型 MyCollection 类型的关键字类型约束为 IComparable。这样就允许 MyCollection 类提供可以对关键字进行排序的 Sort 方法。由于类型参数 KeyType 被约束为 IComparable,现在可以从 KeyType 类型的对象(在 Compare 函数中)直接访问 IComparable 中定义的 CompareTo 方法。
示例 13:
Class MyCollection(Of KeyType As IComparable, ElementType As System.Winforms.Forms.Control)
Private Function Compare(Byval Key1 As KeyType, Byval Key2 As KeyType) As Integer
'注意,IComparable 接口中定义的 CompareTo 函数
'可以从 Key1 对象(其类型为类型参数 KeyType)进行访问,因为
'KeyType 具有接口约束 IComparable
'。 '
Return Key1.CompareTo(Key2)
End Function
Public Function Sort() As ElementType()
....
End Function
...
End Class
New 约束
有时,您可能需要创建类型参数类型的实例。例如,您可能希望定义类型工厂来创建和跟踪特定类型的实例。而且,您可能希望对其进行泛型处理,并将工厂元素类型作为类型参数传递给它,如代码示例 14 所示。因此,您需要创建类型参数类型 T 的实例。您可以使用 New T 创建传入类型的实例,但不能保证传入的类型具有构造函数,并且该类型不是一个接口或 MustInherit 类。为了对作为参数传递给类型参数 T 的类型施加此类约束,可以为其指定 New 约束。为类型参数指定 New 约束表示,作为类型参数传递给此类型参数的类型应具有公开的、不带参数的构造函数,并且不能声明为 MustInherit。实际上,应该能够通过不带参数的构造函数为该类型创建实例。除了满足这些条件的类以外,还可以将结构或枚举类型作为类型参数传递给这种类型参数。
代码示例 14 定义了一个泛型类 Factory,使用作为可以创建的该类型参数传递的类型的新实例。注意,由于类型参数 T 具有 New 约束,传递给类型参数的类型对象可以使用 New T 创建。
示例 14:
Class Factory(Of T As New)
Function CreateInstance() As T
'注意,类型参数类型 T 可以使用 New 关键字构造,
'因为 T 具有 New 约束
'
Dim NewInstance As T = New T
...
Return NewInstance
End Function
End Class
Dim ExceptionsFactory As New Factory(Of Exception)
多重约束
您还可以选择为同一个类型参数指定多重约束。在这种情况下,作为类型参数传递的类型必须满足所有约束。由于 CLR 不允许多重继承,因此只能为每个类型参数指定一个类类型约束。而且,由于多重接口可由同一个类型实现或继承,因此可以为同一个类型参数指定多重接口约束。只能为每个类型参数指定一个 New 约束。注意,可以为同一个类型参数指定不同类型的约束。
如果类型参数中同时存在类约束和接口约束,则只能从类型参数类型的对象中直接访问类成员。要访问接口成员,您需要将对象转换为约束接口类型。如果不存在类约束而存在多重接口约束,则可以从类型参数类型的对象中直接访问所有接口成员。如果由于成员名称出现在多个接口中而导致含义模糊,您可以通过将对象转换为相应的接口并访问该成员使其明确化。
代码示例 15 显示了具有多重约束的类型参数。注意,指定多重约束时需要使用大括号 ({})。只指定一个约束时,也可以选择使用大括号。
示例 15:
Interface I1
Sub A()
Sub B()
End Interface
Interface I2
Sub A()
Sub C()
End Interface
Class C1
Sub Z()
End Sub
End Class
Class C2(Of T1 As {C1, I1, New}, T2 As {I1, I2, New})
Sub Test()
Dim Obj1 As New T1
'C1.Z 可以从类型参数 T1 进行访问,
'因为它具有类约束 C1
'
Obj1.Z()
'错误 - I1.A 无法从 T1 进行访问,因为虽然
'它具有接口约束 I1,但其类约束不允许
'直接访问接口约束成员
'
Obj1.A()
Dim Obj2 As New T2
'错误 - 在接口约束 I1 和 I2 中含义模糊
'
Obj2.A
'通过转换到接口 I1 并调用 A 使 A 明确化
'
CType(Obj2, I1).A
'I1.B 可以从类型参数 T2 进行访问,
'因为它具有接口约束 I1
'
Obj2.B
'I2.C 可以从类型参数 T2 进行访问,
'因为它具有接口约束 I2
'
Obj2.C
End Sub
End Class
高级主题
嵌套类型
只能在泛型类型的主体中使用泛型类型参数,但请注意,也可以在泛型类型嵌套的任意类型中使用这些参数。代码示例 16 定义了泛型类型 MyCollection 及嵌套的 Enumerator 类型,嵌套的类型使用了外部类型的类型参数 KeyType。由于外部类型的类型参数可以在嵌套类型中使用,因此嵌套类型 MyCollection(of String, Integer).Enumerator 和 MyCollection(Of String, Double).Enumerator 不是等效的(它们的外部类型的类型参数 Integer 和 Double 不相同),而是两种截然不同的类型。
示例 16:
Class MyCollection(Of KeyType, ElementType)
Class Enumerator
'注意,外部类型的类型参数 KeyType
'可以在其嵌套类型中使用
'
Dim key As KeyType
End Class
...
End Class
'错误 – MyCollection(Of String, Integer).Enumerator 和 MyCollection(Of
'String, Double).Enumerator 是两种截然不同的非相关类型,
'因为传递给外部类型的参数不相同。
'
Dim x As MyCollection(Of String, Integer).Enumerator = _
New MyCollection(Of String, Double).Enumerator
您也可以将嵌套类型声明为泛型类型,如代码示例 17 所示。
示例 17: Class MyCollection1 Class Enumerator(Of EnumEleType) End Class End Class Dim x As MyCollection1.Enumerator(Of Integer) Class MyCollection2(Of Keytype, ElementType) Class Enumerator(Of EnumEleType) Dim EnumEle As EnumEleType Dim key As KeyType Dim Element As ElementType End Class End Class Dim y As MyCollection2(Of String, Integer).Enumerator(Of Double)
类型参数的重载
在 Visual Basic 2005 中,您可以根据类型参数的数量重载类型。下面的代码示例 18 定义了零个(无泛型重载)、一个和两个类型参数时的 MyCollection 类型重载。在代码中引用名称 MyCollection 时,根据提供的类型参数的数量,编译器会将其与相应的重载类型相匹配。代码示例 18 显示了如何在代码中使用这些重载类型。注意,如果不同的重载位于同一个命名空间,但在不同的程序集中进行定义,编译器仍将它们视为重载。
示例 18: Class MyCollection ... End Class Class MyCollection(Of ElementType) Shared Sub Test(Byval Col As MyCollection(Of ElementType)) End Sub ... End Class Class MyCollection(Of KeyType, ElementType) Shared Sub Print(Byval Col() As MyCollection(Of KeyType, ElementType)) End Sub ... End Class Dim x As MyCollection '引用该类型但不提供任何类型参数 Dim y As MyCollection(Of Integer) '引用该类型并提供 1 个类型参数 Dim z As MyCollection(Of Date, Double) '引用该类型并提供 2 个类型参数 Call MyCollection(of Integer).Print(Nothing) '引用该类型并提供 1 个类型参数
您还可以根据类型参数的数量对方法进行重载。还可以将此类重载与 Visual Basic 的早期版本支持的一般参数的方法重载相结合。代码示例 19 显示了对一个和两个类型参数的两种方法重载。
示例 19: Class MyComparer Shared Function Compare(Of T)(Arg1 As T, Arg2 As T) As Integer End Function Shared Function Compare(Of T1, T2)(Arg1 As T1, Arg2 As T2) AsInteger End Function End Class Call MyComparer.Compare(Of Integer)(10, 20) Call MyComparer.Compare(Of Integer, Date)(10, #1/1/2004#)
泛型和多态
假设您想定义一个 PrintControl 方法。在 Visual Basic 2005 中有两种方式供您选择。
|
1. |
您可以定义一个非泛型方法,它使用 Control 类型的参数,如示例 20 所示。 |
|
2. |
您也可以定义一个泛型方法,如示例 21 所示。 |
哪一种方法更好呢?是泛型方法还是非泛型方法?每一种方法都可以将 Button、TextBox 以及从 Control 继承的其他控件作为参数。在本例中,泛型方法并不能提供任何额外的好处,反而增加了定义过程的复杂性。因此,非泛型 PrintControl 方法是本例中的最佳选择。在本例中,您可以使用一般的继承关系并定义一个非泛型方法,因为您希望允许的类型集与满足此继承关系的类型集完全相同。
示例 20: Sub PrintControl(Byval c As Control) ... End Sub 示例 21: Sub PrintControl(Of T As Control)(Byval c As T) ... End Sub
但是,如果您只希望打印支持 IPrint 接口的控件,该怎么办?可以通过三种方式定义 PrintControl。
|
1. |
可以按照代码示例 20 所示定义一个非泛型方法,但该方法可能传递无法实现 IPrint 的控件,而此类冲突在编译时并不能检测出来。相反,在实现该方法的过程中,尝试将控件转换为 IPrint 时将导致运行时错误。 |
|
2. |
另一种选择是定义一个非泛型方法,它使用 IPrint 类型作为参数,如下面的代码示例 22 所示。但是,该方法可能传递能够实现 IPrint 的对象,而不是控件。第三种选择是按照代码示例 23 所示定义一个泛型方法。该方法只能通过实现 IPrint 的控件调用,因此所有无效的调用都能在编译时捕获。 |
示例 22:
Sub PrintControl(Byval c As IPrint)
...
End Sub
示例 23:
Sub PrintControl(Of T As {Control, IPrint})(Byval c As T)
...
End Sub
在本例中,泛型方法是最佳选择,因为任何常见的多态类型都不能帮助非泛型方法提供与泛型方法相同的编译时验证。
结论
泛型可用于定义非相关类型的常见功能,同时还能保证类型的安全性和性能。现在,Visual Basic 开发人员可以采用明确定义的方式来使用常用类型(例如 Collection 和 HashTable)和常用函数(例如 Sort),从而获得更好的编译时错误检测功能,同时还能改善性能。此外,严格的类型检查还在 IDE 中提供了前所未有的 IntelliSense 体验。方法调用过程中的类型参数推断使得泛型函数的使用非常简单,使用常用函数(例如 System.Array.Sort)的泛型重载对开发人员来说是透明的。这为新代码和旧代码的重新编译提供了方便,甚至无需进行任何修改。
浙公网安备 33010602011771号