几乎在 Visual Basic 中执行的所有操作都与对象关联。如果您第一次接触面向对象的编程,则下列术语和概念将帮助您入门。
类和对象
单词“类”和“对象”在面向对象的编程中使用得非常多,很容易将它们混淆。一般来说,“类”是一些内容的抽象表示形式,而“对象”是类所表示的内容的可用示例。共享类成员是此规则的一个例外,这种成员可在类的实例和声明为共享类类型的对象变量中使用。
字段、属性、方法和事件
类由字段、属性、方法和事件组成。字段和属性表示对象包含的信息。字段类似于变量,因为可以直接读取或设置它们。例如,如果有一个名为 Car 的对象,则可以在名为 Color 的字段中存储其颜色。
属性的检索和设置方法与字段类似,但是属性是使用 Property Get 和 Property Set 过程实现的,这些过程对如何设置或返回值提供更多的控制。在存储值和使用此值的过程之间的间接层帮助隔离数据,并使您得以在分配或检索值之前验证这些值。
方法表示对象可执行的操作。例如,Car 对象可以有 StartEngine、Drive 和 Stop 方法。通过向类中添加过程(Sub 例程或函数)来定义方法。
事件是对象从其他对象或应用程序接收的通知,或者是对象传输到其他对象或应用程序的通知。事件使对象得以在每当特定情况发生时执行操作。Car 类的一个事件示例是 Check_Engine 事件。因为 Microsoft Windows 是事件驱动的操作系统,所以事件可来自其他对象、应用程序或用户输入(如鼠标单击或按键)。
封装、继承和多态性
字段、属性、方法和事件只是面向对象编程全部内容的一半。真正的面向对象的编程需要对象支持三种特性:封装、继承和多态性。
“封装”意味着将一组相关属性、方法和其他成员视为一个单元或对象。对象可以控制更改属性和执行方法的方式。例如,对象在允许属性更改前可验证值。通过隐藏对象的实现细节(一种称为“数据隐藏”的做法),封装还使在以后对实现进行更改变得更容易。
“继承”描述基于现有类创建新类的能力。新类继承基类的所有属性、方法和事件,而且可用其他属性和方法自定义该新类。例如,可基于 Car 类创建名为 Truck 的新类。Truck 类从 Car 类继承 Color 属性,而且可有其他属性,如 FourWheelDrive。
“多态性”意味着可以有多个可互换使用的类,即使每个类以不同方式实现相同属性或方法。多态性是面向对象编程的精华,因为它允许使用同名的项,而不管此时在使用什么类型的对象。例如,假设给定基类 Car,多态性使程序员能够为任意数量的派生类定义不同的 StartEngine 方法。名为 DieselCar 的派生类的 StartEngine 方法可以与基类中同名的方法完全不同。其他过程或方法可用完全相同的方式使用派生类的 StartEngine 方法,不管此时使用什么类型的 Car 对象。
重载、重写和隐藏
可以使用字段和属性在对象中存储信息。虽然从客户端应用程序角度来看,字段和属性几乎无法区别,但在类中声明它们的方式不同。属性使用 Property 过程控制如何设置或返回值,而字段只是类所公开的公共变量。
向类添加字段
在类定义中声明一个公共变量,如下面的代码所示:
Class ThisClass
Public ThisField As String
End Class
向类添加属性
在类中声明一个局部变量来存储属性值。因为属性不会自行分配任何存储区,所以该步骤是必需的。若要保护它们的值不被直接修改,用于存储属性值的变量应当声明为 Private。
根据需要以修饰符(如 Public 和 Shared)作为属性声明的开头。使用 Property 关键字声明属性名称,并声明属性存储和返回的数据类型。
在属性定义中定义 Get 和 Set 属性过程。Get 属性过程用于返回属性值,基本等效于语法中的函数。它们不接受参数,可用于返回在类中声明的、用于存储属性值的私有局部变量的值。Set 属性过程用于设置属性的值,它们有参数(通常称为 Value),该参数的数据类型与属性本身的数据类型相同。每当属性值更改时,Value 均会被传递给 Set 属性过程,在该过程中可以验证它并将其存储在一个局部变量中。
根据需要使用 End Get 和 End Set 语句终止 Get 和 Set 属性过程。
使用 End Property 语句终止属性块。
注意 如果正在 Visual Studio 集成开发环境 (IDE) 下工作,可以指示它去除空的 Get 和 Set 属性过程。键入 Property PropName As DataType(其中,PropName 是属性名称,DataType 是特定数据类型,如 Integer),相应的属性过程将出现在代码编辑器中。
下面的示例在类中声明一个属性:
Class ThisClass
Private m_PropVal As String
Public Property One() As String
Get
Return m_PropVal '' Return the value stored in the local variable.
'' Optionally, you can use the syntax One = PropVal to return
'' the property value.
End Get
Set(ByVal Value As String)
m_PropVal = Value '' Store the value in a local variable.
End Set
End Property
End Class
当创建 ThisClass 的一个实例并设置 One 属性的值时,将调用 Set 属性过程且该值在 Value 参数中传递,该参数存储在名为 m_PropVal 的局部变量中。当检索此属性值时,将像函数那样调用 Get 属性过程并返回存储在局部变量 m_PropVal 中的值。
属性和属性过程
可以使用属性和字段在对象中存储信息。属性使用属性过程控制如何设置或返回值,而字段只是公共变量。属性过程是在属性定义中声明的代码块,使您可以在设置或检索属性值时执行代码。Visual Basic .NET 有两种类型的属性过程:Get 属性过程用于检索属性值;Set 属性过程用于向属性赋值。例如,存储银行帐户余额的属性可能会在 Get 属性过程中使用代码以在返回可用余额之前记入利息并检查服务费。用于可用余额的 Set 属性过程会提供验证代码以防止不正确地更新余额。简而言之,属性过程允许对象保护和验证自己的数据
只读和只写属性
大多数属性有 Get 和 Set 这两个属性过程以使您可以同时读取和修改所存储的值。然而,您可以使用 ReadOnly 或 WriteOnly 修饰符来限制对属性的读取或修改。只读属性不能有 Set 属性过程,它们可用于需要公开但不允许修改的项。例如,可以使用只读属性来提供计算机处理器的速度。只写属性不能有 Get 属性过程,它们可用于需要存储但不对其他对象公开的数据。例如,只写属性可以用于存储密码。
注意 在将对象分配给属性时,Visual Basic 的早期版本支持使用 Let 属性过程。Visual Basic .NET 消除了对 Let 属性过程的需要,因为可以像处理其他任何种类的分配那样处理对象分配。
属性过程与字段
属性与字段都可在对象中存储和检索信息。它们的相似性使得在给定情况下很难确定哪个是更好的编程选择。
在以下情况下使用属性过程:
需要控制设置或检索值的时间和方式时。
属性有定义完善的一组值需要进行验证时。
设置值会导致对象状态中发生某些可察觉更改(如一个可见属性)时。
设置属性会导致更改其他内部变量或其他属性的值时。
必须先执行一组步骤,然后才能设置或检索属性时。
在以下情况下使用字段:
值为自验证类型时。例如,如果将 True 或 False 以外的值分配给 Boolean 变量,就会发生错误或自动数据转换。
在数据类型所支持范围内的任何值均有效时。Single 或 Double 类型的很多属性属于这种情况。
属性是 String 数据类型,且对于字符串的大小或值没有任何约束时。
类方法
类的方法就是在该类中声明的 Sub 或 Function 过程。例如,若要为名为 Account 的类创建 Withdrawal 方法,可以向该类模块中添加此 Public 函数:
Public Function WithDrawal(ByVal Amount As Decimal, _
ByVal TransactionCode As Byte) As Double
'' Add code here to perform the withdrawal,
'' return a transaction code,
'' or to raise an overdraft error.
End Function
共享方法
共享方法可以直接从类变量调用,而不必首先创建该类的实例。当不希望方法与类的特定实例关联时,共享方法很有用。共享方法不能用 Overridable、NotOverridable 或 MustOverride 修饰符声明。模块中声明的方法是隐式共享的,不能显式使用 Shared 修饰符。
示例
Class ShareClass
Shared Sub SharedSub()
MessageBox.Show("Shared method.")
End Sub
End Class
Sub Test()
'' Create an object variable, but not an instance.
Dim S As ShareClass
S.SharedSub '' Call the method.
End Sub
保护实现详细信息
由类内部使用的实用工具过程应声明为 Private、Protected 或 Friend。限制这类方法的可访问性可防止它们被其他开发人员使用,而且使您得以将来在不影响使用这些对象的代码的情况下进行更改。
隐藏对象实现的详细信息是“封装”的另一方面。封装使您得以提高方法的性能,或完全改变实现方法的方式,而不必更改使用该方法的代码。
属性与方法
属性和方法都作为接受参数的过程实现,在这一点上它们很相似。通常,属性存储对象的数据,而方法是可要求对象执行的操作。对象的一些特性明显是属性,比如 Name,而有些明显是方法,比如 Move 和 Show。在其他情况中,哪些类成员应是属性哪些应是方法并不明显。例如,集合类的 Item 方法存储和检索数据,可作为索引属性实现。另一方面,将 Item 作为方法实现也是合理的。
属性语法与方法语法
决定如何实现类成员的一个方法是考虑要如何使用它。虽然从参数化属性检索信息的语法与作为函数实现的方法所用的语法几乎相同,但修改这样的值的语法却稍有不同。例如,如果将类的成员实现为属性,则下面的语法描述将如何使用它:
ThisObject.ThisProperty(Index) = NewValue
如果将类成员实现为方法,则要修改的值必须是参数。下面的代码片段描述等效的语法用法:
ThisObject.ThisProperty(Index,NewValue)
错误信息
选择如何实现类成员时要考虑的另一个因素是,当错误地使用类时将生成何种消息。如果有人无意中试图为只读属性分配一个值,则将返回一条错误信息,该错误信息不同于响应对方法的类似调用所返回的错误信息。正确实现的类成员返回更容易解释的错误信息。
默认属性
接受参数的属性可声明为类的默认属性。“默认属性”是当未给对象命名特定属性时 Microsoft Visual Basic .NET 将使用的属性。因为默认属性使您得以通过省略常用属性名使源代码更为精简,所以默认属性非常有用。
最适宜作为默认属性的是那些接受参数并且您认为将最常用的属性。例如,Item 属性就是集合类默认属性的很好的选择,因为它被经常使用。
下列规则适用于默认属性:
一种类型只能有一个默认属性,包括从基类继承的属性。此规则有一个例外。在基类中定义的默认属性可以被派生类中的另一个默认属性隐藏。
如果基类中的默认属性被派生类中的非默认属性隐藏,使用默认属性语法仍可以访问该默认属性。
默认属性不能是 Shared 或 Private。
如果某个重载属性是默认属性,则同名的所有重载属性必须也指定 Default。
默认属性必须至少接受一个参数。
示例
下面的示例将一个包含字符串数组的属性声明为类的默认属性:
Class Class2
'' Define a local variable to store the property value.
Private PropertyValues As String()
'' Define the default property.
Default Public Property Prop1(ByVal Index As Integer) As String
Get
Return PropertyValues(Index)
End Get
Set(ByVal Value As String)
If PropertyValues Is Nothing Then
'' The array contains Nothing when first accessed.
ReDim PropertyValues(0)
Else
'' Re-dimension the array to hold the new element.
ReDim Preserve PropertyValues(UBound(PropertyValues) + 1)
End If
PropertyValues(Index) = Value
End Set
End Property
End Class
访问默认属性
可以使用缩写语法访问默认属性。例如,下面的代码片段同时使用标准和默认属性语法:
Dim C As New Class2()
'' The first two lines of code access a property the standard way.
C.Prop1(0) = "Value One" '' Property assignment.
MessageBox.Show(C.Prop1(0)) '' Property retrieval.
'' The following two lines of code use default property syntax.
C(1) = "Value Two" '' Property assignment.
MessageBox.Show(C(1)) '' Property retrieval.
重载属性和方法
重载是在一个类中用相同的名称但是不同的参数类型创建一个以上的过程、实例构造函数或属性。
当对象模型指示对于在不同数据类型上进行操作的过程使用同样名称时,重载非常有用。例如,可显示几种不同数据类型的类可以具有类似如下所示 Display 过程:
Overloads Sub Display(ByVal theChar As Char)
'' Add code that displays Char data.
End Sub
Overloads Sub Display(ByVal theInteger As Integer)
'' Add code that displays Integer data.
End Sub
Overloads Sub Display(ByVal theDouble As Double)
'' Add code that displays Double data.
End Sub
如果不使用重载,那么即使每个过程执行相同的操作,也需要为它们创建不同的名称,如下所示:
Sub DisplayChar(ByVal theChar As Char)
'' Add code that displays Char data.
End Sub
Sub DisplayInt(ByVal theInteger As Integer)
'' Add code that displays Integer data.
End Sub
Sub DisplayDouble(ByVal theDouble As Double)
'' Add code that displays Double data.
End Sub
因为重载提供了对可用数据类型的选择,所以它使得属性或方法的使用更为容易。例如,可以用下列任一代码行调用前面讨论过的重载 Display 方法:
Display("9"C) '' Call Display with a literal of type Char.
Display(9) '' Call Display with a literal of type Integer.
Display(9.9R) '' Call Display with a literal of type Double.
在运行时,Visual Basic .NET 根据指定参数的数据类型调用正确的过程。
注意 重载、重写和隐藏操作是容易混淆的类似概念。有关更多信息,请参见介绍 Visual Basic 中的对象。
重载规则
用同样名称添加两个或更多属性或方法可以创建类的一个重载成员。除了重载派生成员,每一个重载成员必须具有不同的参数列表。当重载属性或过程时,下面的项不能用作区分特征:
应用于成员或成员参数的修饰符,如 ByVal 或 ByRef。
参数名
过程的返回类型
重载时关键字 Overloads 是可选的,但如果任一重载成员使用了该 Overloads 关键字,则其他所有同名重载成员也必须指定该关键字。
派生类可以用具有相同参数和参数类型的成员重载继承成员,该过程称作“按名称和签名隐藏”。如果按名称和签名隐藏时使用了 Overloads 关键字,将使用该成员的派生类实现而非基类中的实现,并且该成员的所有其他重载对于该派生类的实例都将可用。
如果用一个具有相同参数和参数类型的成员重载继承成员时,省略了 Overloads 关键字,则该重载称为“按名称隐藏”。按名称隐藏替代一个成员的继承实现,使所有其他重载对于该派生类及由其派生的类的实例都不可用。
Overloads 和 Shadows 修饰符不能同时被同一个属性或方法所使用。
示例
下面的示例创建接受美元金额的 String 或 Decimal 表示形式并返回包含销售税的字符串的重载方法。
使用此示例创建重载方法
打开新项目,添加名为 TaxClass 的类。
向 TaxClass 类中添加下面的代码。
Public Class TaxClass
Overloads Function TaxAmount(ByVal decPrice As Decimal, _
ByVal TaxRate As Single) As String
TaxAmount = "Price is a Decimal. Tax is $" & _
(CStr(decPrice * TaxRate))
End Function
Overloads Function TaxAmount(ByVal strPrice As String, _
ByVal TaxRate As Single) As String
TaxAmount = "Price is a String. Tax is $" & _
CStr((CDec(strPrice) * TaxRate))
End Function
End Class
向窗体中添加下面的过程。
Sub ShowTax()
Const TaxRate As Single = 0.08 '' 8% tax rate
Dim strPrice As String = "64.00" '' $64.00 Purchase as a String.
Dim decPrice As Decimal = 64 '' $64.00 Purchase as a Decimal.
Dim aclass As New taxclass()
''Call the same method with two diferent kinds of data.
MessageBox.Show(aclass.TaxAmount(strPrice, TaxRate))
MessageBox.Show(aclass.TaxAmount(decPrice, TaxRate))
End Sub
向窗体中添加按钮,并从该按钮的 Button1_Click 事件调用 ShowTax 过程。
运行该项目并单击窗体上的该按钮,以测试重载 ShowTax 过程。
在运行时,编译器选择与所用参数匹配的适当重载函数。单击该按钮时,首先将使用类型为字符串的 Price 参数调用被重载的方法,并显示信息:“Price is a String.Tax is $5.12”。第二次将使用类型为 Decimal 的值调用 TaxAmount,并显示信息“Price is a Decimal.Tax is $5.12”。
重写属性和方法
派生类继承其基类中定义的属性和方法。这很有用,因为它意味着当这些项适合于您要使用的类时,可以重用它们。如果继承成员不能按原样使用,则可以选择使用 Overrides 关键字定义新实现,假设基类中的属性或方法使用 Overridable 关键字标记,或通过重新在派生类中定义成员来隐藏该成员。
实际上,重写的成员经常用于实现多态性。有关多态性的更多信息,请参见多态性。
下列规则适用于重写方法。
仅可重写在其基类中用 Overridable 关键字进行标记的成员。
默认情况下,属性和方法为 NotOverridable。
重写的成员必须具有与从基类继承的成员相同的参数。
成员的新实现可通过在方法名称前指定 MyBase 来调用父类中的原始实现。
注意 重载、重写和隐藏操作是容易混淆的类似概念。有关更多信息,请参见介绍 Visual Basic 中的对象。
示例
假设您要定义类以处理工资单。您可以定义一个一般 Payroll 类,其中包含计算普通周工资单的 RunPayroll 方法。然后可将 Payroll 用作更专用的 BonusPayroll 类的基类,分发雇员奖金时可使用该 BonusPayroll 类。
BonusPayroll 类可继承并重写在基类 Payroll 中定义的 PayEmployee 方法。
下面的示例定义基类 Payroll 和派生类 BonusPayroll,该派生类重写继承方法 PayEmployee。过程 RunPayroll 创建 Payroll 对象和 BonusPayroll 对象,然后将其传递给函数 Pay,该函数执行这两个对象的 PayEmployee 方法。
Const BonusRate As Decimal = 1.45
Const PayRate As Decimal = 14.75
Class Payroll
Overridable Function PayEmployee(ByVal HoursWorked As Decimal, _
ByVal PayRate As Decimal) As Decimal
PayEmployee = HoursWorked * PayRate
End Function
End Class
Class BonusPayroll
Inherits Payroll
Overrides Function PayEmployee(ByVal HoursWorked As Decimal, _
ByVal PayRate As Decimal) As Decimal
'' The following code calls the original method in the base
'' class, and then modifies the returned value.
PayEmployee = MyBase.PayEmployee(HoursWorked, PayRate) * BonusRate
End Function
End Class
Sub RunPayroll()
Dim PayrollItem As Payroll = New Payroll()
Dim BonusPayrollItem As New BonusPayroll()
Dim HoursWorked As Decimal = 40
MessageBox.Show("Normal pay is: " & _
PayrollItem.PayEmployee(HoursWorked, PayRate))
MessageBox.Show("Pay with bonus is: " & _
BonusPayrollItem.PayEmployee(HoursWorked, PayRate))
End Sub
重写修饰符
可使用 NotOverridable 和 MustOverride 修饰符控制如何在派生类中重写属性和方法。
NotOverridable 修饰符定义无法在派生类中重写的基类的方法。所有方法都为 NotOverridable,除非用 Overridable 修饰符进行标记。当不希望允许在派生类中再次重写 overridden 方法时,可使用 NotOverridable 修饰符。
用 MustOverride 修饰符定义的方法在基类中没有实现,必须在派生类中实现。包含 MustOverride 方法的类必须使用 MustInherit 修饰符进行标记。
示例
MustInherit Class BaseClass
Public MustOverride Sub aProcedure()
End Class
Class DerivedClass
Inherits BaseClass
Public NotOverridable Overrides Sub aProcedure()
'' Override a procedure inherited from the base class
'' and mark it with the NotOverridable modifier so that
'' it cannot be overridden in classes derived from this class.
End Sub
End Class
用集合管理对象
集合提供一种管理各种对象的理想方法。可以在集合中添加和移除对象、根据索引或键检索它们,以及使用 For Each...Next 语句循环通过集合中的项。但是,集合极强的灵活性可能会破坏类的可靠性。
可以采用三种常规方法使用集合来实现对象管理。请考虑组织小部件并将其公开给客户端组件的组件。若要使用集合实现此组件,则可以:
在 WidgetRepository 类中,将 Widgets 变量声明为集合,并使其成为公共变量。
通过继承 CollectionBase 类来实现您自己的 WidgetsCollection 类。授予 WidgetRepository 类一个 WidgetsCollection 类的属性。
通过编写适当的类和方法在 WidgetRepository 类中实现集合类型功能来实现此功能。如果要在类中拥有集合类型功能,但无法从任何集合类型类继承时,这种方法最为有用。例如,如果希望从集合类之外的其他类继承。
可以向类中添加集合以管理可能由该类公开的对象。实现此功能的最简单的方法是将类型为 Collection 的公共变量添加到类中。请考虑名为 WidgetRepository 的假设类,该类管理和公开小部件。可以创建 WidgetCollection 变量作为小部件的集合,如下面的示例中所示:
Public Class WidgetRepository
Public WidgetCollection As New Collection()
'' Insert code to implement additional functionality.
End Class
现在此类有一个公共集合,可以向其添加 Widget 对象。但这种方法有一些问题。首先,此集合不是强类型的。如果执行下面代码将会发生什么呢?
Dim myString As String = "This is not a Widget object!"
WidgetCollection.Add(myString)
答案是它将被添加到该集合中,即使它不是 Widget 对象。事实上,任何对象都可添加到集合中,不管是什么类型。如果接着尝试使用 For Each...Next 语句处理小部件,问题随即发生,如下所示:
Dim aWidget As Widget
For Each aWidget in WidgetCollection
'' Insert code to process Widgets
Next
在该示例中,程序在运行时引发 ArgumentException 异常,因为集合的一个成员不是 Widget 类型。
另一个问题涉及公开集合的成员。因为 Collection 类接受并返回 Object 类型的成员,所以当 Option Strict 语句为 On 时,无法使用集合所管理的对象的任何功能。若要将该集合提供的引用转换为适当的类型,则必须改用 CType 函数,如下所示:
Dim myWidget As Widget
MyWidget = CType(WidgetCollection(1), Widget)
请注意,当 Option Strict 为 Off 时,将隐式执行转换,尽管晚期绑定将导致性能的下降。
如果需要避免这些问题,或您的类要求更可靠类型的集合,应考虑使用 CollectionBase 创建您自己的集合类。有关详细信息,请参见演练:创建您自己的集合类。
事件和事件处理程序
Visual Studio 项目很容易被看作一系列顺序执行的过程。事实上,多数程序都是事件驱动的,即执行流程是由外界发生的事件所确定的。
事件是一个信号,它告知应用程序有重要情况发生。例如,用户单击窗体上的某个控件时,窗体引发一个 Click 事件并调用一个处理该事件的过程。事件还允许在不同任务之间进行通信。比方说,您的应用程序脱离主程序执行一个排序任务。若用户取消这一排序,应用程序可以发送一个取消事件让排序过程停止。
事件术语和概念
本节描述 Visual Basic .NET 中与事件一起使用的术语和概念。
声明事件
使用 Event 关键字在类、结构、模块和接口内部声明事件,如以下示例所示:
Event AnEvent(ByVal EventNumber As Integer)
引发事件
事件就像是通告已发生重要情况的消息。广播该消息的行为叫引发事件。在 Visual Basic .NET 中,使用 RaiseEvent 语句引发事件,如以下示例所示:
RaiseEvent AnEvent(EventNumber)
必须在声明事件的范围内引发事件。例如,派生类不能引发从基类继承的事件。
事件发送器
任何能引发事件的对象都是事件发送者,也称事件源。窗体、控件和用户定义的对象都是事件发送器。
事件处理程序
事件处理程序是相应事件发生时调用的过程。您可以将任何有效子例程用作事件处理程序。可是,不能将函数用作事件处理程序,因为它不能将值返回给事件源。
Visual Basic 采用标准命名约定对事件处理程序进行命名,即用下划线把事件发送器和事件的名称组合起来。例如,名为 button1 的按钮的单击事件应命名为 Sub button1_Click。
注意 建议使用此命名约定定义您自己事件的事件处理程序。但这并不是必选的方法。您可以使用任何有效的子例程。
关联事件与事件处理程序
在事件处理程序生效之前,首先必须使用 Handles 或 AddHandler 语句将它与事件关联。
WithEvents 语句和 Handles 子句提供了陈述性指定事件处理程序的方法。WithEvents 所声明对象引发的事件可以由任何子例程用命名此事件的 Handles 子句来处理。虽然 Handles 子句是关联事件与事件处理程序的标准方法,它仅限于在编译时关联事件与事件处理程序。
AddHandler 和 RemoveHandler 语句要比 Handles 子句更灵活。它们允许在运行时动态地将事件与一个或更多的事件处理程序连接或者断开,而并不要求使用 WithEvents 来声明对象变量。
在某些情况下,比如使用窗体或控件所关联的事件,Visual Basic .NET 会自动引出一个空事件处理程序并将它与某个事件关联。例如,在设计模式下双击窗体上的命令按钮时,Visual Basic .NET 会为命令按钮创建一个空事件处理程序和一个 WithEvents 变量,如以下示例所示:
Friend WithEvents Button1 As System.Windows.Forms.Button
Protected Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button1.Click
End Sub
接口概述
和类一样,接口也定义了一系列属性、方法和事件。但与类不同的是,接口并不提供实现。它们由类来实现,并从类中被定义为单独的实体。
接口表示一种约定,实现接口的类必须严格按其定义来实现接口的每个方面。
有了接口,就可以将功能定义为一些紧密相关成员的小组。可以在不危害现有代码的情况下,开发接口的增强型实现,从而使兼容性问题最小化。也可以在任何时候通过开发附加接口和实现来添加新的功能。
虽然接口实现可以进化,但接口本身一旦被发布就不能再更改。对已发布的接口进行更改会破坏现有的代码。若把接口视为约定,很明显约定双方都各有其承担的义务。接口的发布者同意不再更改该接口,接口的实现者则同意严格按设计来实现接口。
Visual Basic .NET 以前的 Visual Basic 版本可以使用接口,但不能直接创建它们。Visual Basic .NET 允许用 Interface 语句定义真正的接口,并允许用改进版本的 Implements 关键字来实现这些接口。
继承的基础知识
Inherits 语句用于基于现有类(称为“基类”)来声明新类(称为“派生类”)。派生类继承并可扩展基类中定义的属性、方法、事件、字段和常数。下面一节描述一些继承规则,以及一些可用来更改类继承或被继承方式的修饰符:
默认情况下,所有类都是可继承的,除非用 NotInheritable 关键字标记。类可以从项目中的其他类继承,也可以从项目引用的其他程序集中的类继承。
与允许多重继承的语言不同,Visual Basic .NET 只允许类中有单一继承,即派生类只能有一个基类。虽然类中不允许有多重继承,但类“可以”实现多个接口,这样可以有效地实现同一目的。
若要防止公开基类中的受限项,派生类的访问类型必须与其基类一样或比其基类所受限制更多。例如,Public 类无法继承 Friend 或 Private 类,而 Friend 类无法继承 Private 类。
继承修饰符
Visual Basic .NET 引入了下列类级别语句和修饰符以支持继承:
Inherits 语句 — 指定基类。
NotInheritable 修饰符 — 防止程序员将该类用作基类。
MustInherit 修饰符 — 指定该类仅适于用作基类。无法直接创建 MustInherit 类的实例,只能将它们创建为派生类的基类实例。(其他编程语言,如 C++ 和 C#,使用术语“抽象类”来描述这样的类。)
重写派生类中的属性和方法
默认情况下,派生类从其基类继承方法。如果继承的属性或方法需要在派生类中有不同的行为,则可以“重写”它,即,可以在派生类中定义该方法的新实现。下列修饰符用于控制如何重写属性和方法:
Overridable — 允许某个类中的属性或方法在派生类中被重写。
Overrides — 重写基类中定义的 Overridable 属性或方法。
NotOverridable — 防止某个属性或方法在继承类中被重写。默认情况下,Public 方法为 NotOverridable。
MustOverride — 要求派生类重写属性或方法。当使用 MustOverride 关键字时,方法定义仅由 Sub、Function 或 Property 语句组成。不允许有其他语句,尤其是不能有 End Sub 或 End Function 语句。必须在 MustInherit 类中声明 MustOverride 方法。
有关重写方法的更多信息,请参见重写属性和方法
MyBase 关键字
当重写派生类中的方法时,可以使用 MyBase 关键字调用基类中的方法。例如,假设您正在设计一个重写从基类继承的方法的派生类。重写的方法可以调用基类中的该方法,并修改返回值,如下面的代码片段中所示:
Class DerivedClass
Inherits BaseClass
Public Overrides Function CalculateShipping(ByVal Dist As Double, _
ByVal Rate As Double) As Double
'' Call the method in the base class and modify the return value.
Return MyBase.CalculateShipping(Dist, Rate) * 2
End Function
End Class
下面的列表描述对使用 MyBase 的限制:
MyBase 引用直接基类及其继承成员。它无法用于访问类中的 Private 成员。
MyBase 是关键字,不是实际对象。MyBase 无法分配给变量,无法传递给过程,也无法用在 Is 比较中。
MyBase 限定的方法不需要在直接基类中定义,它可以在间接继承的基类中定义。为了正确编译 MyBase 限定的引用,一些基类必须包含与调用中出现的参数名称和类型匹配的方法。
不能使用 MyBase 来调用 MustOverride 基类方法。
MyBase 无法用于限定自身。因此,下面的代码是非法的:
MyBase.MyBase.BtnOK_Click() '' Syntax error.
MyBase 无法用在模块中。
如果基类在不同的程序集中,则不能使用 MyBase 来访问标记为 Friend 的基类成员。
MyClass 关键字
MyClass 关键字使您得以调用在类中实现的 Overridable 方法,并确保调用此类中该方法的实现,而不是调用派生类中重写的方法。
MyClass 是关键字,不是实际对象。MyClass 无法分配给变量,也无法传递给过程,而且也无法用在 Is 比较中。
MyClass 引用包含类及其继承成员。
MyClass 可用作 Shared 成员的修饰符。
MyClass 无法用在标准模块中。
MyClass 可用于限定这样的方法,该方法在基类中定义但没有在该类中提供该方法的实现。这种引用的意义与 MyBase.Method 相同。
基于继承的多态性
大部分面向对象的编程系统都通过继承提供多态性。基于继承的多态性涉及在基类中定义方法并在派生类中使用新实现重写它们。
例如,可以定义一个类 BaseTax,该类提供计算某个州/省的销售税的基准功能。从 BaseTax 派生的类,如 CountyTax 或 CityTax,如果适合的话可实现如 CalculateTax 这样的方法。
多态性来自这样一个事实:可以调用属于从 BaseTax 派生的任何类的某个对象的 CalculateTax 方法,而不必知道该对象属于哪个类。
下面示例中的 TestPoly 过程演示基于继承的多态性:
Const StateRate As Double = 0.053 '' %5.3 State tax
Const CityRate As Double = 0.028 '' %2.8 City tax
Public Class BaseTax
Overridable Function CalculateTax(ByVal Amount As Double) As Double
Return Amount * StateRate '' Calculate state tax.
End Function
End Class
Public Class CityTax
'' This method calls a method in the base class
'' and modifies the returned value.
Inherits BaseTax
Private BaseAmount As Double
Overrides Function CalculateTax(ByVal Amount As Double) As Double
'' Some cities apply a tax to the total cost of purchases,
'' including other taxes.
BaseAmount = MyBase.CalculateTax(Amount)
Return CityRate * (BaseAmount + Amount) + BaseAmount
End Function
End Class
Sub TestPoly()
Dim Item1 As New BaseTax()
Dim Item2 As New CityTax()
ShowTax(Item1, 22.74) '' $22.74 normal purchase.
ShowTax(Item2, 22.74) '' $22.74 city purchase.
End Sub
Sub ShowTax(ByVal Item As BaseTax, ByVal SaleAmount As Double)
'' Item is declared as BaseTax, but you can
'' pass an item of type CityTax instead.
Dim TaxAmount As Double
TaxAmount = Item.CalculateTax(SaleAmount)
MsgBox("The tax is: " & Format(TaxAmount, "C"))
End Sub
在此示例中,ShowTax 过程接受 BaseTax 类型的名为 Item 的参数,但还可以传递从形状类派生的任何类,如 CityTax。这种设计的优点在于可添加从 BaseTax 类派生的新类,而不用更改 ShowTax 过程中的客户端代码。
类和对象
单词“类”和“对象”在面向对象的编程中使用得非常多,很容易将它们混淆。一般来说,“类”是一些内容的抽象表示形式,而“对象”是类所表示的内容的可用示例。共享类成员是此规则的一个例外,这种成员可在类的实例和声明为共享类类型的对象变量中使用。
字段、属性、方法和事件
类由字段、属性、方法和事件组成。字段和属性表示对象包含的信息。字段类似于变量,因为可以直接读取或设置它们。例如,如果有一个名为 Car 的对象,则可以在名为 Color 的字段中存储其颜色。
属性的检索和设置方法与字段类似,但是属性是使用 Property Get 和 Property Set 过程实现的,这些过程对如何设置或返回值提供更多的控制。在存储值和使用此值的过程之间的间接层帮助隔离数据,并使您得以在分配或检索值之前验证这些值。
方法表示对象可执行的操作。例如,Car 对象可以有 StartEngine、Drive 和 Stop 方法。通过向类中添加过程(Sub 例程或函数)来定义方法。
事件是对象从其他对象或应用程序接收的通知,或者是对象传输到其他对象或应用程序的通知。事件使对象得以在每当特定情况发生时执行操作。Car 类的一个事件示例是 Check_Engine 事件。因为 Microsoft Windows 是事件驱动的操作系统,所以事件可来自其他对象、应用程序或用户输入(如鼠标单击或按键)。
封装、继承和多态性
字段、属性、方法和事件只是面向对象编程全部内容的一半。真正的面向对象的编程需要对象支持三种特性:封装、继承和多态性。
“封装”意味着将一组相关属性、方法和其他成员视为一个单元或对象。对象可以控制更改属性和执行方法的方式。例如,对象在允许属性更改前可验证值。通过隐藏对象的实现细节(一种称为“数据隐藏”的做法),封装还使在以后对实现进行更改变得更容易。
“继承”描述基于现有类创建新类的能力。新类继承基类的所有属性、方法和事件,而且可用其他属性和方法自定义该新类。例如,可基于 Car 类创建名为 Truck 的新类。Truck 类从 Car 类继承 Color 属性,而且可有其他属性,如 FourWheelDrive。
“多态性”意味着可以有多个可互换使用的类,即使每个类以不同方式实现相同属性或方法。多态性是面向对象编程的精华,因为它允许使用同名的项,而不管此时在使用什么类型的对象。例如,假设给定基类 Car,多态性使程序员能够为任意数量的派生类定义不同的 StartEngine 方法。名为 DieselCar 的派生类的 StartEngine 方法可以与基类中同名的方法完全不同。其他过程或方法可用完全相同的方式使用派生类的 StartEngine 方法,不管此时使用什么类型的 Car 对象。
重载、重写和隐藏
可以使用字段和属性在对象中存储信息。虽然从客户端应用程序角度来看,字段和属性几乎无法区别,但在类中声明它们的方式不同。属性使用 Property 过程控制如何设置或返回值,而字段只是类所公开的公共变量。
向类添加字段
在类定义中声明一个公共变量,如下面的代码所示:
Class ThisClass
Public ThisField As String
End Class
向类添加属性
在类中声明一个局部变量来存储属性值。因为属性不会自行分配任何存储区,所以该步骤是必需的。若要保护它们的值不被直接修改,用于存储属性值的变量应当声明为 Private。
根据需要以修饰符(如 Public 和 Shared)作为属性声明的开头。使用 Property 关键字声明属性名称,并声明属性存储和返回的数据类型。
在属性定义中定义 Get 和 Set 属性过程。Get 属性过程用于返回属性值,基本等效于语法中的函数。它们不接受参数,可用于返回在类中声明的、用于存储属性值的私有局部变量的值。Set 属性过程用于设置属性的值,它们有参数(通常称为 Value),该参数的数据类型与属性本身的数据类型相同。每当属性值更改时,Value 均会被传递给 Set 属性过程,在该过程中可以验证它并将其存储在一个局部变量中。
根据需要使用 End Get 和 End Set 语句终止 Get 和 Set 属性过程。
使用 End Property 语句终止属性块。
注意 如果正在 Visual Studio 集成开发环境 (IDE) 下工作,可以指示它去除空的 Get 和 Set 属性过程。键入 Property PropName As DataType(其中,PropName 是属性名称,DataType 是特定数据类型,如 Integer),相应的属性过程将出现在代码编辑器中。
下面的示例在类中声明一个属性:
Class ThisClass
Private m_PropVal As String
Public Property One() As String
Get
Return m_PropVal '' Return the value stored in the local variable.
'' Optionally, you can use the syntax One = PropVal to return
'' the property value.
End Get
Set(ByVal Value As String)
m_PropVal = Value '' Store the value in a local variable.
End Set
End Property
End Class
当创建 ThisClass 的一个实例并设置 One 属性的值时,将调用 Set 属性过程且该值在 Value 参数中传递,该参数存储在名为 m_PropVal 的局部变量中。当检索此属性值时,将像函数那样调用 Get 属性过程并返回存储在局部变量 m_PropVal 中的值。
属性和属性过程
可以使用属性和字段在对象中存储信息。属性使用属性过程控制如何设置或返回值,而字段只是公共变量。属性过程是在属性定义中声明的代码块,使您可以在设置或检索属性值时执行代码。Visual Basic .NET 有两种类型的属性过程:Get 属性过程用于检索属性值;Set 属性过程用于向属性赋值。例如,存储银行帐户余额的属性可能会在 Get 属性过程中使用代码以在返回可用余额之前记入利息并检查服务费。用于可用余额的 Set 属性过程会提供验证代码以防止不正确地更新余额。简而言之,属性过程允许对象保护和验证自己的数据
只读和只写属性
大多数属性有 Get 和 Set 这两个属性过程以使您可以同时读取和修改所存储的值。然而,您可以使用 ReadOnly 或 WriteOnly 修饰符来限制对属性的读取或修改。只读属性不能有 Set 属性过程,它们可用于需要公开但不允许修改的项。例如,可以使用只读属性来提供计算机处理器的速度。只写属性不能有 Get 属性过程,它们可用于需要存储但不对其他对象公开的数据。例如,只写属性可以用于存储密码。
注意 在将对象分配给属性时,Visual Basic 的早期版本支持使用 Let 属性过程。Visual Basic .NET 消除了对 Let 属性过程的需要,因为可以像处理其他任何种类的分配那样处理对象分配。
属性过程与字段
属性与字段都可在对象中存储和检索信息。它们的相似性使得在给定情况下很难确定哪个是更好的编程选择。
在以下情况下使用属性过程:
需要控制设置或检索值的时间和方式时。
属性有定义完善的一组值需要进行验证时。
设置值会导致对象状态中发生某些可察觉更改(如一个可见属性)时。
设置属性会导致更改其他内部变量或其他属性的值时。
必须先执行一组步骤,然后才能设置或检索属性时。
在以下情况下使用字段:
值为自验证类型时。例如,如果将 True 或 False 以外的值分配给 Boolean 变量,就会发生错误或自动数据转换。
在数据类型所支持范围内的任何值均有效时。Single 或 Double 类型的很多属性属于这种情况。
属性是 String 数据类型,且对于字符串的大小或值没有任何约束时。
类方法
类的方法就是在该类中声明的 Sub 或 Function 过程。例如,若要为名为 Account 的类创建 Withdrawal 方法,可以向该类模块中添加此 Public 函数:
Public Function WithDrawal(ByVal Amount As Decimal, _
ByVal TransactionCode As Byte) As Double
'' Add code here to perform the withdrawal,
'' return a transaction code,
'' or to raise an overdraft error.
End Function
共享方法
共享方法可以直接从类变量调用,而不必首先创建该类的实例。当不希望方法与类的特定实例关联时,共享方法很有用。共享方法不能用 Overridable、NotOverridable 或 MustOverride 修饰符声明。模块中声明的方法是隐式共享的,不能显式使用 Shared 修饰符。
示例
Class ShareClass
Shared Sub SharedSub()
MessageBox.Show("Shared method.")
End Sub
End Class
Sub Test()
'' Create an object variable, but not an instance.
Dim S As ShareClass
S.SharedSub '' Call the method.
End Sub
保护实现详细信息
由类内部使用的实用工具过程应声明为 Private、Protected 或 Friend。限制这类方法的可访问性可防止它们被其他开发人员使用,而且使您得以将来在不影响使用这些对象的代码的情况下进行更改。
隐藏对象实现的详细信息是“封装”的另一方面。封装使您得以提高方法的性能,或完全改变实现方法的方式,而不必更改使用该方法的代码。
属性与方法
属性和方法都作为接受参数的过程实现,在这一点上它们很相似。通常,属性存储对象的数据,而方法是可要求对象执行的操作。对象的一些特性明显是属性,比如 Name,而有些明显是方法,比如 Move 和 Show。在其他情况中,哪些类成员应是属性哪些应是方法并不明显。例如,集合类的 Item 方法存储和检索数据,可作为索引属性实现。另一方面,将 Item 作为方法实现也是合理的。
属性语法与方法语法
决定如何实现类成员的一个方法是考虑要如何使用它。虽然从参数化属性检索信息的语法与作为函数实现的方法所用的语法几乎相同,但修改这样的值的语法却稍有不同。例如,如果将类的成员实现为属性,则下面的语法描述将如何使用它:
ThisObject.ThisProperty(Index) = NewValue
如果将类成员实现为方法,则要修改的值必须是参数。下面的代码片段描述等效的语法用法:
ThisObject.ThisProperty(Index,NewValue)
错误信息
选择如何实现类成员时要考虑的另一个因素是,当错误地使用类时将生成何种消息。如果有人无意中试图为只读属性分配一个值,则将返回一条错误信息,该错误信息不同于响应对方法的类似调用所返回的错误信息。正确实现的类成员返回更容易解释的错误信息。
默认属性
接受参数的属性可声明为类的默认属性。“默认属性”是当未给对象命名特定属性时 Microsoft Visual Basic .NET 将使用的属性。因为默认属性使您得以通过省略常用属性名使源代码更为精简,所以默认属性非常有用。
最适宜作为默认属性的是那些接受参数并且您认为将最常用的属性。例如,Item 属性就是集合类默认属性的很好的选择,因为它被经常使用。
下列规则适用于默认属性:
一种类型只能有一个默认属性,包括从基类继承的属性。此规则有一个例外。在基类中定义的默认属性可以被派生类中的另一个默认属性隐藏。
如果基类中的默认属性被派生类中的非默认属性隐藏,使用默认属性语法仍可以访问该默认属性。
默认属性不能是 Shared 或 Private。
如果某个重载属性是默认属性,则同名的所有重载属性必须也指定 Default。
默认属性必须至少接受一个参数。
示例
下面的示例将一个包含字符串数组的属性声明为类的默认属性:
Class Class2
'' Define a local variable to store the property value.
Private PropertyValues As String()
'' Define the default property.
Default Public Property Prop1(ByVal Index As Integer) As String
Get
Return PropertyValues(Index)
End Get
Set(ByVal Value As String)
If PropertyValues Is Nothing Then
'' The array contains Nothing when first accessed.
ReDim PropertyValues(0)
Else
'' Re-dimension the array to hold the new element.
ReDim Preserve PropertyValues(UBound(PropertyValues) + 1)
End If
PropertyValues(Index) = Value
End Set
End Property
End Class
访问默认属性
可以使用缩写语法访问默认属性。例如,下面的代码片段同时使用标准和默认属性语法:
Dim C As New Class2()
'' The first two lines of code access a property the standard way.
C.Prop1(0) = "Value One" '' Property assignment.
MessageBox.Show(C.Prop1(0)) '' Property retrieval.
'' The following two lines of code use default property syntax.
C(1) = "Value Two" '' Property assignment.
MessageBox.Show(C(1)) '' Property retrieval.
重载属性和方法
重载是在一个类中用相同的名称但是不同的参数类型创建一个以上的过程、实例构造函数或属性。
当对象模型指示对于在不同数据类型上进行操作的过程使用同样名称时,重载非常有用。例如,可显示几种不同数据类型的类可以具有类似如下所示 Display 过程:
Overloads Sub Display(ByVal theChar As Char)
'' Add code that displays Char data.
End Sub
Overloads Sub Display(ByVal theInteger As Integer)
'' Add code that displays Integer data.
End Sub
Overloads Sub Display(ByVal theDouble As Double)
'' Add code that displays Double data.
End Sub
如果不使用重载,那么即使每个过程执行相同的操作,也需要为它们创建不同的名称,如下所示:
Sub DisplayChar(ByVal theChar As Char)
'' Add code that displays Char data.
End Sub
Sub DisplayInt(ByVal theInteger As Integer)
'' Add code that displays Integer data.
End Sub
Sub DisplayDouble(ByVal theDouble As Double)
'' Add code that displays Double data.
End Sub
因为重载提供了对可用数据类型的选择,所以它使得属性或方法的使用更为容易。例如,可以用下列任一代码行调用前面讨论过的重载 Display 方法:
Display("9"C) '' Call Display with a literal of type Char.
Display(9) '' Call Display with a literal of type Integer.
Display(9.9R) '' Call Display with a literal of type Double.
在运行时,Visual Basic .NET 根据指定参数的数据类型调用正确的过程。
注意 重载、重写和隐藏操作是容易混淆的类似概念。有关更多信息,请参见介绍 Visual Basic 中的对象。
重载规则
用同样名称添加两个或更多属性或方法可以创建类的一个重载成员。除了重载派生成员,每一个重载成员必须具有不同的参数列表。当重载属性或过程时,下面的项不能用作区分特征:
应用于成员或成员参数的修饰符,如 ByVal 或 ByRef。
参数名
过程的返回类型
重载时关键字 Overloads 是可选的,但如果任一重载成员使用了该 Overloads 关键字,则其他所有同名重载成员也必须指定该关键字。
派生类可以用具有相同参数和参数类型的成员重载继承成员,该过程称作“按名称和签名隐藏”。如果按名称和签名隐藏时使用了 Overloads 关键字,将使用该成员的派生类实现而非基类中的实现,并且该成员的所有其他重载对于该派生类的实例都将可用。
如果用一个具有相同参数和参数类型的成员重载继承成员时,省略了 Overloads 关键字,则该重载称为“按名称隐藏”。按名称隐藏替代一个成员的继承实现,使所有其他重载对于该派生类及由其派生的类的实例都不可用。
Overloads 和 Shadows 修饰符不能同时被同一个属性或方法所使用。
示例
下面的示例创建接受美元金额的 String 或 Decimal 表示形式并返回包含销售税的字符串的重载方法。
使用此示例创建重载方法
打开新项目,添加名为 TaxClass 的类。
向 TaxClass 类中添加下面的代码。
Public Class TaxClass
Overloads Function TaxAmount(ByVal decPrice As Decimal, _
ByVal TaxRate As Single) As String
TaxAmount = "Price is a Decimal. Tax is $" & _
(CStr(decPrice * TaxRate))
End Function
Overloads Function TaxAmount(ByVal strPrice As String, _
ByVal TaxRate As Single) As String
TaxAmount = "Price is a String. Tax is $" & _
CStr((CDec(strPrice) * TaxRate))
End Function
End Class
向窗体中添加下面的过程。
Sub ShowTax()
Const TaxRate As Single = 0.08 '' 8% tax rate
Dim strPrice As String = "64.00" '' $64.00 Purchase as a String.
Dim decPrice As Decimal = 64 '' $64.00 Purchase as a Decimal.
Dim aclass As New taxclass()
''Call the same method with two diferent kinds of data.
MessageBox.Show(aclass.TaxAmount(strPrice, TaxRate))
MessageBox.Show(aclass.TaxAmount(decPrice, TaxRate))
End Sub
向窗体中添加按钮,并从该按钮的 Button1_Click 事件调用 ShowTax 过程。
运行该项目并单击窗体上的该按钮,以测试重载 ShowTax 过程。
在运行时,编译器选择与所用参数匹配的适当重载函数。单击该按钮时,首先将使用类型为字符串的 Price 参数调用被重载的方法,并显示信息:“Price is a String.Tax is $5.12”。第二次将使用类型为 Decimal 的值调用 TaxAmount,并显示信息“Price is a Decimal.Tax is $5.12”。
重写属性和方法
派生类继承其基类中定义的属性和方法。这很有用,因为它意味着当这些项适合于您要使用的类时,可以重用它们。如果继承成员不能按原样使用,则可以选择使用 Overrides 关键字定义新实现,假设基类中的属性或方法使用 Overridable 关键字标记,或通过重新在派生类中定义成员来隐藏该成员。
实际上,重写的成员经常用于实现多态性。有关多态性的更多信息,请参见多态性。
下列规则适用于重写方法。
仅可重写在其基类中用 Overridable 关键字进行标记的成员。
默认情况下,属性和方法为 NotOverridable。
重写的成员必须具有与从基类继承的成员相同的参数。
成员的新实现可通过在方法名称前指定 MyBase 来调用父类中的原始实现。
注意 重载、重写和隐藏操作是容易混淆的类似概念。有关更多信息,请参见介绍 Visual Basic 中的对象。
示例
假设您要定义类以处理工资单。您可以定义一个一般 Payroll 类,其中包含计算普通周工资单的 RunPayroll 方法。然后可将 Payroll 用作更专用的 BonusPayroll 类的基类,分发雇员奖金时可使用该 BonusPayroll 类。
BonusPayroll 类可继承并重写在基类 Payroll 中定义的 PayEmployee 方法。
下面的示例定义基类 Payroll 和派生类 BonusPayroll,该派生类重写继承方法 PayEmployee。过程 RunPayroll 创建 Payroll 对象和 BonusPayroll 对象,然后将其传递给函数 Pay,该函数执行这两个对象的 PayEmployee 方法。
Const BonusRate As Decimal = 1.45
Const PayRate As Decimal = 14.75
Class Payroll
Overridable Function PayEmployee(ByVal HoursWorked As Decimal, _
ByVal PayRate As Decimal) As Decimal
PayEmployee = HoursWorked * PayRate
End Function
End Class
Class BonusPayroll
Inherits Payroll
Overrides Function PayEmployee(ByVal HoursWorked As Decimal, _
ByVal PayRate As Decimal) As Decimal
'' The following code calls the original method in the base
'' class, and then modifies the returned value.
PayEmployee = MyBase.PayEmployee(HoursWorked, PayRate) * BonusRate
End Function
End Class
Sub RunPayroll()
Dim PayrollItem As Payroll = New Payroll()
Dim BonusPayrollItem As New BonusPayroll()
Dim HoursWorked As Decimal = 40
MessageBox.Show("Normal pay is: " & _
PayrollItem.PayEmployee(HoursWorked, PayRate))
MessageBox.Show("Pay with bonus is: " & _
BonusPayrollItem.PayEmployee(HoursWorked, PayRate))
End Sub
重写修饰符
可使用 NotOverridable 和 MustOverride 修饰符控制如何在派生类中重写属性和方法。
NotOverridable 修饰符定义无法在派生类中重写的基类的方法。所有方法都为 NotOverridable,除非用 Overridable 修饰符进行标记。当不希望允许在派生类中再次重写 overridden 方法时,可使用 NotOverridable 修饰符。
用 MustOverride 修饰符定义的方法在基类中没有实现,必须在派生类中实现。包含 MustOverride 方法的类必须使用 MustInherit 修饰符进行标记。
示例
MustInherit Class BaseClass
Public MustOverride Sub aProcedure()
End Class
Class DerivedClass
Inherits BaseClass
Public NotOverridable Overrides Sub aProcedure()
'' Override a procedure inherited from the base class
'' and mark it with the NotOverridable modifier so that
'' it cannot be overridden in classes derived from this class.
End Sub
End Class
用集合管理对象
集合提供一种管理各种对象的理想方法。可以在集合中添加和移除对象、根据索引或键检索它们,以及使用 For Each...Next 语句循环通过集合中的项。但是,集合极强的灵活性可能会破坏类的可靠性。
可以采用三种常规方法使用集合来实现对象管理。请考虑组织小部件并将其公开给客户端组件的组件。若要使用集合实现此组件,则可以:
在 WidgetRepository 类中,将 Widgets 变量声明为集合,并使其成为公共变量。
通过继承 CollectionBase 类来实现您自己的 WidgetsCollection 类。授予 WidgetRepository 类一个 WidgetsCollection 类的属性。
通过编写适当的类和方法在 WidgetRepository 类中实现集合类型功能来实现此功能。如果要在类中拥有集合类型功能,但无法从任何集合类型类继承时,这种方法最为有用。例如,如果希望从集合类之外的其他类继承。
可以向类中添加集合以管理可能由该类公开的对象。实现此功能的最简单的方法是将类型为 Collection 的公共变量添加到类中。请考虑名为 WidgetRepository 的假设类,该类管理和公开小部件。可以创建 WidgetCollection 变量作为小部件的集合,如下面的示例中所示:
Public Class WidgetRepository
Public WidgetCollection As New Collection()
'' Insert code to implement additional functionality.
End Class
现在此类有一个公共集合,可以向其添加 Widget 对象。但这种方法有一些问题。首先,此集合不是强类型的。如果执行下面代码将会发生什么呢?
Dim myString As String = "This is not a Widget object!"
WidgetCollection.Add(myString)
答案是它将被添加到该集合中,即使它不是 Widget 对象。事实上,任何对象都可添加到集合中,不管是什么类型。如果接着尝试使用 For Each...Next 语句处理小部件,问题随即发生,如下所示:
Dim aWidget As Widget
For Each aWidget in WidgetCollection
'' Insert code to process Widgets
Next
在该示例中,程序在运行时引发 ArgumentException 异常,因为集合的一个成员不是 Widget 类型。
另一个问题涉及公开集合的成员。因为 Collection 类接受并返回 Object 类型的成员,所以当 Option Strict 语句为 On 时,无法使用集合所管理的对象的任何功能。若要将该集合提供的引用转换为适当的类型,则必须改用 CType 函数,如下所示:
Dim myWidget As Widget
MyWidget = CType(WidgetCollection(1), Widget)
请注意,当 Option Strict 为 Off 时,将隐式执行转换,尽管晚期绑定将导致性能的下降。
如果需要避免这些问题,或您的类要求更可靠类型的集合,应考虑使用 CollectionBase 创建您自己的集合类。有关详细信息,请参见演练:创建您自己的集合类。
事件和事件处理程序
Visual Studio 项目很容易被看作一系列顺序执行的过程。事实上,多数程序都是事件驱动的,即执行流程是由外界发生的事件所确定的。
事件是一个信号,它告知应用程序有重要情况发生。例如,用户单击窗体上的某个控件时,窗体引发一个 Click 事件并调用一个处理该事件的过程。事件还允许在不同任务之间进行通信。比方说,您的应用程序脱离主程序执行一个排序任务。若用户取消这一排序,应用程序可以发送一个取消事件让排序过程停止。
事件术语和概念
本节描述 Visual Basic .NET 中与事件一起使用的术语和概念。
声明事件
使用 Event 关键字在类、结构、模块和接口内部声明事件,如以下示例所示:
Event AnEvent(ByVal EventNumber As Integer)
引发事件
事件就像是通告已发生重要情况的消息。广播该消息的行为叫引发事件。在 Visual Basic .NET 中,使用 RaiseEvent 语句引发事件,如以下示例所示:
RaiseEvent AnEvent(EventNumber)
必须在声明事件的范围内引发事件。例如,派生类不能引发从基类继承的事件。
事件发送器
任何能引发事件的对象都是事件发送者,也称事件源。窗体、控件和用户定义的对象都是事件发送器。
事件处理程序
事件处理程序是相应事件发生时调用的过程。您可以将任何有效子例程用作事件处理程序。可是,不能将函数用作事件处理程序,因为它不能将值返回给事件源。
Visual Basic 采用标准命名约定对事件处理程序进行命名,即用下划线把事件发送器和事件的名称组合起来。例如,名为 button1 的按钮的单击事件应命名为 Sub button1_Click。
注意 建议使用此命名约定定义您自己事件的事件处理程序。但这并不是必选的方法。您可以使用任何有效的子例程。
关联事件与事件处理程序
在事件处理程序生效之前,首先必须使用 Handles 或 AddHandler 语句将它与事件关联。
WithEvents 语句和 Handles 子句提供了陈述性指定事件处理程序的方法。WithEvents 所声明对象引发的事件可以由任何子例程用命名此事件的 Handles 子句来处理。虽然 Handles 子句是关联事件与事件处理程序的标准方法,它仅限于在编译时关联事件与事件处理程序。
AddHandler 和 RemoveHandler 语句要比 Handles 子句更灵活。它们允许在运行时动态地将事件与一个或更多的事件处理程序连接或者断开,而并不要求使用 WithEvents 来声明对象变量。
在某些情况下,比如使用窗体或控件所关联的事件,Visual Basic .NET 会自动引出一个空事件处理程序并将它与某个事件关联。例如,在设计模式下双击窗体上的命令按钮时,Visual Basic .NET 会为命令按钮创建一个空事件处理程序和一个 WithEvents 变量,如以下示例所示:
Friend WithEvents Button1 As System.Windows.Forms.Button
Protected Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button1.Click
End Sub
接口概述
和类一样,接口也定义了一系列属性、方法和事件。但与类不同的是,接口并不提供实现。它们由类来实现,并从类中被定义为单独的实体。
接口表示一种约定,实现接口的类必须严格按其定义来实现接口的每个方面。
有了接口,就可以将功能定义为一些紧密相关成员的小组。可以在不危害现有代码的情况下,开发接口的增强型实现,从而使兼容性问题最小化。也可以在任何时候通过开发附加接口和实现来添加新的功能。
虽然接口实现可以进化,但接口本身一旦被发布就不能再更改。对已发布的接口进行更改会破坏现有的代码。若把接口视为约定,很明显约定双方都各有其承担的义务。接口的发布者同意不再更改该接口,接口的实现者则同意严格按设计来实现接口。
Visual Basic .NET 以前的 Visual Basic 版本可以使用接口,但不能直接创建它们。Visual Basic .NET 允许用 Interface 语句定义真正的接口,并允许用改进版本的 Implements 关键字来实现这些接口。
继承的基础知识
Inherits 语句用于基于现有类(称为“基类”)来声明新类(称为“派生类”)。派生类继承并可扩展基类中定义的属性、方法、事件、字段和常数。下面一节描述一些继承规则,以及一些可用来更改类继承或被继承方式的修饰符:
默认情况下,所有类都是可继承的,除非用 NotInheritable 关键字标记。类可以从项目中的其他类继承,也可以从项目引用的其他程序集中的类继承。
与允许多重继承的语言不同,Visual Basic .NET 只允许类中有单一继承,即派生类只能有一个基类。虽然类中不允许有多重继承,但类“可以”实现多个接口,这样可以有效地实现同一目的。
若要防止公开基类中的受限项,派生类的访问类型必须与其基类一样或比其基类所受限制更多。例如,Public 类无法继承 Friend 或 Private 类,而 Friend 类无法继承 Private 类。
继承修饰符
Visual Basic .NET 引入了下列类级别语句和修饰符以支持继承:
Inherits 语句 — 指定基类。
NotInheritable 修饰符 — 防止程序员将该类用作基类。
MustInherit 修饰符 — 指定该类仅适于用作基类。无法直接创建 MustInherit 类的实例,只能将它们创建为派生类的基类实例。(其他编程语言,如 C++ 和 C#,使用术语“抽象类”来描述这样的类。)
重写派生类中的属性和方法
默认情况下,派生类从其基类继承方法。如果继承的属性或方法需要在派生类中有不同的行为,则可以“重写”它,即,可以在派生类中定义该方法的新实现。下列修饰符用于控制如何重写属性和方法:
Overridable — 允许某个类中的属性或方法在派生类中被重写。
Overrides — 重写基类中定义的 Overridable 属性或方法。
NotOverridable — 防止某个属性或方法在继承类中被重写。默认情况下,Public 方法为 NotOverridable。
MustOverride — 要求派生类重写属性或方法。当使用 MustOverride 关键字时,方法定义仅由 Sub、Function 或 Property 语句组成。不允许有其他语句,尤其是不能有 End Sub 或 End Function 语句。必须在 MustInherit 类中声明 MustOverride 方法。
有关重写方法的更多信息,请参见重写属性和方法
MyBase 关键字
当重写派生类中的方法时,可以使用 MyBase 关键字调用基类中的方法。例如,假设您正在设计一个重写从基类继承的方法的派生类。重写的方法可以调用基类中的该方法,并修改返回值,如下面的代码片段中所示:
Class DerivedClass
Inherits BaseClass
Public Overrides Function CalculateShipping(ByVal Dist As Double, _
ByVal Rate As Double) As Double
'' Call the method in the base class and modify the return value.
Return MyBase.CalculateShipping(Dist, Rate) * 2
End Function
End Class
下面的列表描述对使用 MyBase 的限制:
MyBase 引用直接基类及其继承成员。它无法用于访问类中的 Private 成员。
MyBase 是关键字,不是实际对象。MyBase 无法分配给变量,无法传递给过程,也无法用在 Is 比较中。
MyBase 限定的方法不需要在直接基类中定义,它可以在间接继承的基类中定义。为了正确编译 MyBase 限定的引用,一些基类必须包含与调用中出现的参数名称和类型匹配的方法。
不能使用 MyBase 来调用 MustOverride 基类方法。
MyBase 无法用于限定自身。因此,下面的代码是非法的:
MyBase.MyBase.BtnOK_Click() '' Syntax error.
MyBase 无法用在模块中。
如果基类在不同的程序集中,则不能使用 MyBase 来访问标记为 Friend 的基类成员。
MyClass 关键字
MyClass 关键字使您得以调用在类中实现的 Overridable 方法,并确保调用此类中该方法的实现,而不是调用派生类中重写的方法。
MyClass 是关键字,不是实际对象。MyClass 无法分配给变量,也无法传递给过程,而且也无法用在 Is 比较中。
MyClass 引用包含类及其继承成员。
MyClass 可用作 Shared 成员的修饰符。
MyClass 无法用在标准模块中。
MyClass 可用于限定这样的方法,该方法在基类中定义但没有在该类中提供该方法的实现。这种引用的意义与 MyBase.Method 相同。
基于继承的多态性
大部分面向对象的编程系统都通过继承提供多态性。基于继承的多态性涉及在基类中定义方法并在派生类中使用新实现重写它们。
例如,可以定义一个类 BaseTax,该类提供计算某个州/省的销售税的基准功能。从 BaseTax 派生的类,如 CountyTax 或 CityTax,如果适合的话可实现如 CalculateTax 这样的方法。
多态性来自这样一个事实:可以调用属于从 BaseTax 派生的任何类的某个对象的 CalculateTax 方法,而不必知道该对象属于哪个类。
下面示例中的 TestPoly 过程演示基于继承的多态性:
Const StateRate As Double = 0.053 '' %5.3 State tax
Const CityRate As Double = 0.028 '' %2.8 City tax
Public Class BaseTax
Overridable Function CalculateTax(ByVal Amount As Double) As Double
Return Amount * StateRate '' Calculate state tax.
End Function
End Class
Public Class CityTax
'' This method calls a method in the base class
'' and modifies the returned value.
Inherits BaseTax
Private BaseAmount As Double
Overrides Function CalculateTax(ByVal Amount As Double) As Double
'' Some cities apply a tax to the total cost of purchases,
'' including other taxes.
BaseAmount = MyBase.CalculateTax(Amount)
Return CityRate * (BaseAmount + Amount) + BaseAmount
End Function
End Class
Sub TestPoly()
Dim Item1 As New BaseTax()
Dim Item2 As New CityTax()
ShowTax(Item1, 22.74) '' $22.74 normal purchase.
ShowTax(Item2, 22.74) '' $22.74 city purchase.
End Sub
Sub ShowTax(ByVal Item As BaseTax, ByVal SaleAmount As Double)
'' Item is declared as BaseTax, but you can
'' pass an item of type CityTax instead.
Dim TaxAmount As Double
TaxAmount = Item.CalculateTax(SaleAmount)
MsgBox("The tax is: " & Format(TaxAmount, "C"))
End Sub
在此示例中,ShowTax 过程接受 BaseTax 类型的名为 Item 的参数,但还可以传递从形状类派生的任何类,如 CityTax。这种设计的优点在于可添加从 BaseTax 类派生的新类,而不用更改 ShowTax 过程中的客户端代码。