ZetCode

Visual Basic 中的面向对象编程

最后修改于 2023 年 10 月 18 日

在 Visual Basic 教程的这一部分,我们将讨论 Visual Basic 中的面向对象编程。

有三种广泛使用的编程范式。过程式编程、函数式编程和面向对象编程。 Visual Basic 支持过程式编程和面向对象编程。

面向对象编程 (OOP) 是一种编程范式,它使用对象及其交互来设计应用程序和计算机程序。(维基百科)

OOP 中有一些基本的编程概念

抽象 是通过为问题建模合适的类来简化复杂的现实。多态 是对不同的数据输入以不同方式使用运算符或函数的过程。封装 隐藏了类中其他对象的实现细节。继承 是一种使用已经定义的类来形成新类的方法。

对象

对象是 Visual Basic OOP 程序的基本构建块。一个对象是数据和方法的组合。在 OOP 程序中,我们创建对象。这些对象通过方法相互通信。每个对象都可以接收消息、发送消息和处理数据。

创建对象有两个步骤。首先,我们创建一个类。一个是对象的模板。它是一个蓝图,描述了该类的对象共享的状态和行为。一个类可以用来创建许多对象。在运行时从一个类创建的对象称为该特定类的实例

Program.vb
Option Strict On

Module Example

    Class Being

    End Class

    Sub Main()

        Dim b as New Being
        Console.WriteLine(b.ToString())

    End Sub

End Module

在我们的第一个例子中,我们创建一个简单的对象。

Class Being

End Class

这是一个简单的类定义。模板的主体是空的。它没有任何数据或方法。

Dim b as New Being

我们创建一个新的 Being 类的实例。为此,我们有 New 关键字。变量 b 是指向已创建对象的句柄。

Console.WriteLine(b.ToString())

对象的 ToString 方法给出了对象的一些基本描述。

$ dotnet run
Example+Being

我们没有获得太多信息,因为类的定义是空的。我们得到对象类名和创建此对象实例的模块名。

对象属性

对象属性是捆绑在一个类实例中的数据。对象属性称为实例变量成员字段。实例变量是在类中定义的变量,该类的每个对象都有一个单独的副本。

Program.vb
Option Strict On

Module Example

    Class Person

        Public Name As String

    End Class

    Sub Main()

        Dim p1 as New Person
        p1.Name = "Jane"

        Dim p2 as New Person
        p2.Name = "Beky"

        Console.WriteLine(p1.Name)
        Console.WriteLine(p2.Name)

    End Sub

End Module

在上面的 Visual Basic 代码中,我们有一个带有成员字段的 Person 类。

Class Person
    Public Name As String
End Class

我们声明一个 Name 成员字段。Public 关键字指定该成员字段将在 Class End Class 块之外可访问。

Dim p1 as New Person
p1.Name = "Jane"

我们创建一个 Person 类的实例。并将 Name 变量设置为“Jane”。我们使用点运算符来访问对象的属性。

Dim p2 as New Person
p2.Name = "Beky"

我们创建 Person 类的另一个实例。在这里,我们将变量设置为“Beky”。

Console.WriteLine(p1.Name)
Console.WriteLine(p2.Name)

我们将变量的内容打印到控制台。

$ dotnet run
Jane
Beky

我们看到程序的输出。Person 类的每个实例都有一个 Name 成员字段的单独副本。

方法

方法是在类的正文中定义的函数/过程。它们用于对对象的属性执行操作。方法是 OOP 范式的封装 概念中的重要组成部分。例如,我们可能在 AccessDatabase 类中有一个 Connect 方法。我们不需要被告知 Connect 如何准确地连接到数据库。我们只知道它用于连接到数据库。这对于划分编程中的职责至关重要,尤其是在大型应用程序中。

Program.vb
Option Strict On

Module Example

    Class Circle

        Public Radius As Integer

        Public Sub SetRadius(ByVal Radius As Integer)
            Me.Radius = Radius
        End Sub

        Public Function Area() As Double
            Return Me.Radius * Me.Radius * Math.PI
        End Function

    End Class

    Sub Main()

        Dim c As New Circle
        c.SetRadius(5)

        Console.WriteLine(c.Area())

    End Sub

End Module

在代码示例中,我们有一个 Circle 类。我们定义了两种方法。

Public Radius As Integer

我们有一个成员字段。它是圆的 RadiusPublic 关键字是一个访问说明符。它表示变量可以从外部完全访问。

Public Sub SetRadius(ByVal Radius As Integer)
    Me.Radius = Radius
End Sub

这是 SetRadius 方法。这是一个普通的 Visual Basic 过程。Me 变量是一个特殊的变量,我们用它来从方法中访问成员字段。

Public Function Area() As Double
    Return Me.Radius * Me.Radius * Math.PI
End Function

Area 方法返回圆的面积。Math.PI 是一个内置常量。

$ dotnet run
78.5398163397448

运行示例。

访问修饰符

访问修饰符 设置方法和成员字段的可见性。Visual Basic 有五个访问修饰符:PublicProtectedPrivateFriendProtectedFriendPublic 成员可以从任何地方访问。

Protected 成员只能在类本身以及继承和父类中访问。Friend 成员可以从同一程序集(exe 或 DLL)内部访问。ProtectedFriend 是受保护和友好修饰符的并集。

访问修饰符保护数据免受意外修改。 它们使程序更加健壮。

Program.vb
Option Strict On

Module Example

    Class Person

        Public Name As String
        Private Age As Byte

        Public Function GetAge() As Byte
            Return Me.Age
        End Function

        Public Sub SetAge(ByVal Age As Byte)
            Me.Age = Age
        End Sub

    End Class

    Sub Main()

        Dim p as New Person
        p.Name = "Jane"

        p.setAge(17)

        Console.WriteLine("{0} is {1} years old",
           p.Name, p.GetAge)

    End Sub

End Module

在上面的程序中,我们有两个成员字段。一个声明为 Public,另一个声明为 Private

Public Function GetAge() As Byte
    Return Me.Age
End Function

如果成员字段是 Private,则访问它的唯一方法是通过方法。如果我们要修改类之外的属性,则该方法必须声明为 Public。这是数据保护的一个重要方面。

Public Sub SetAge(ByVal Age As Byte)
    Me.Age = Age
End Sub

SetAge 方法使我们能够从类定义外部更改私有 Age 变量。

Dim p as New Person
p.Name = "Jane"

我们创建一个 Person 类的新实例。因为 Name 属性是 Public,所以我们可以直接访问它。但是,不建议这样做。

p.setAge(17)

SetAge 方法修改 Age 成员字段。由于它声明为 Private,因此不能直接访问或修改它。

Console.WriteLine("{0} is {1} years old", p.Name, p.GetAge)

最后,我们访问这两个成员来构建一个字符串。

$ dotnet run
Jane is 17 years old
Program.vb
Option Strict On

Module Example

    Class Base

        Public Name As String = "Base"
        Protected Id As Integer = 5323
        Private IsDefined As Boolean = True

    End Class

    Class Derived
        Inherits Base

        Public Sub Info()
            Console.WriteLine("This is Derived Class")
            Console.WriteLine("Members inherited:")
            Console.WriteLine(Me.Name)
            Console.WriteLine(Me.Id)
            'Console.WriteLine(Me.IsDefined)
        End Sub

    End Class

    Sub Main()

        Dim drv As Derived = New Derived
        drv.Info()

    End Sub

End Module

在前面的程序中,我们有一个派生类,它继承自 Base 类。Base 类有三个成员字段。所有都具有不同的访问修饰符。IsDefined 成员未被继承。Private 修饰符会阻止这种情况。

Class Derived
    Inherits Base

Derived 类继承自 Base 类。

Console.WriteLine(Me.Name)
Console.WriteLine(Me.Id)
'Console.WriteLine(Me.IsDefined)

PublicProtected 成员被 Derived 类继承。它们可以被访问。Private 成员未被继承。访问成员字段的行被注释掉了。如果我们取消注释该行,它将无法编译。

$ dotnet run
This is Derived Class
Members inherited:
Base
5323

运行程序,我们收到此输出。PublicProtected 成员被继承,而 Private 成员未被继承。

方法重载

方法重载允许创建多个具有相同名称的方法,但这些方法在输入类型上彼此不同。

方法重载有什么用?Qt4 库为使用提供了很好的例子。QPainter 类有三种方法来绘制一个矩形。它们的名称是 drawRect,它们的参数不同。一个接受对浮点矩形对象的引用,另一个接受对整数矩形对象的引用,最后一个接受四个参数,x、y、width、height。如果开发 Qt 的 C++ 语言没有方法重载,那么库的创建者将不得不将这些方法命名为 drawRectRectFdrawRectRectdrawRectXYWH。使用方法重载的解决方案更优雅。

Program.vb
Option Strict On

Module Example

    Class Sum

        Public Function GetSum() As Integer
            Return 0
        End Function

        Public Function GetSum(ByVal x As Integer) As Integer
            Return x
        End Function

        Public Function GetSum(ByVal x As Integer,
            ByVal y As Integer) As Integer
            Return x + y
        End Function

    End Class

    Sub Main()

        Dim s As Sum = New Sum

        Console.WriteLine(s.getSum())
        Console.WriteLine(s.getSum(20))
        Console.WriteLine(s.getSum(20, 30))

    End Sub

End Module

我们有三个名为 GetSum 的方法。它们的输入参数不同。

Public Function GetSum(ByVal x As Integer) As Integer
    Return x
End Function

这一个接受一个参数。

Console.WriteLine(s.getSum())
Console.WriteLine(s.getSum(20))
Console.WriteLine(s.getSum(20, 30))

我们调用所有三种方法。

$ dotnet run
0
20
50

构造函数

构造函数是一种特殊的方法。当创建对象时,它会自动调用。构造函数的目的是启动对象的状态。 Visual Basic 中构造函数的名称是 New。构造函数是方法,因此它们也可以被重载。

Program.vb
Option Strict On

Module Example

    Class Being

        Sub New()
            Console.WriteLine("Being is being created")
        End Sub

        Sub New(ByVal name As String)
            Console.WriteLine("Being {0} is created", name)
        End Sub

    End Class

    Sub Main()

        Dim b As New Being
        Dim t As New Being("Tom")

    End Sub

End Module

我们有一个 Being 类。此类有两个构造函数。第一个不接受参数,第二个接受一个参数。

Sub New(ByVal name As String)
    Console.WriteLine("Being {0} is created", name)
End Sub

此构造函数接受一个 String 参数。

Dim b As New Being

创建 Being 类的一个实例。这次在对象创建时调用不带参数的构造函数。

$ dotnet run
Being is being created
Being Tom is created

在下一个例子中,我们初始化该类的数据成员。初始化变量是构造函数的典型工作。

Program.vb
Option Strict On

Module Example

    Class MyFriend

        Private Born As Date
        Private Name As String

        Sub New(ByVal Name As String, ByVal Born As Date)
            Me.Name = Name
            Me.Born = Born
        End Sub

        Public Sub GetInfo()
            Console.WriteLine("{0} was born on {1}",
                Me.Name, Me.Born.ToShortDateString)
        End Sub

    End Class

    Sub Main()

        Dim name As String = "Lenka"
        Dim born As Date = #5/3/1990#

        Dim fr As MyFriend = New MyFriend(name, born)
        fr.GetInfo()

    End Sub

End Module

我们有一个 Friend 类,带有数据成员和方法。

Private Born As Date
Private Name As String

我们在类定义中有两个变量。Private 关键字是一个访问修饰符。这是一种封装形式。Private 关键字是限制最严格的修饰符。它只允许相关对象访问该变量。没有后代,也没有其他对象。

Sub New(ByVal Name As String, ByVal Born As Date)
    Me.Name = Name
    Me.Born = Born
End Sub

在构造函数中,我们启动了两个数据成员。Me 变量是一个处理程序,用于引用对象变量。

Dim fr As MyFriend = New MyFriend(name, born)
fr.GetInfo()

我们使用两个参数创建一个 Friend 对象。然后我们调用该对象的 GetInfo 方法。

$ dotnet run
Lenka was born on 5/3/1990

类常量

Visual Basic 允许创建类常量。这些常量不属于具体的对象。它们属于类。按照惯例,常量用大写字母书写。

Program.vb
Option Strict On

Module Example

    Class Math

        Public Const PI As Double = 3.14159265359

    End Class

    Sub Main()
         Console.WriteLine(Math.PI)
    End Sub

End Module

我们有一个带有 PI 常量的 Math 类。

Public Const PI As Double = 3.14159265359

Const 关键字用于定义常量。

$ dotnet run
3.14159265359

运行示例。

ToString() 方法

每个对象都有一个 ToString 方法。它返回对象的易于阅读的表示形式。默认实现返回对象的类型的完全限定名称。请注意,当我们将 Console.WriteLine 方法与一个对象作为参数一起调用时,会调用 ToString

Program.vb
Option Strict On

Module Example

    Class Being

        Public Overrides Function ToString As String
            Return "This is Being Class"
        End Function

    End Class

    Sub Main()

        Dim b as New Being
        Dim o As New Object

        Console.WriteLine(o.ToString())
        Console.WriteLine(b.ToString())
        Console.WriteLine(b)

    End Sub

End Module

我们有一个 Being 类,我们在其中重写 ToString 方法的默认实现。

Public Overrides Function ToString As String
    Return "This is Being Class"
End Function

创建的每个类都继承自基本 ObjectToString 方法属于此 Object 类。我们使用 Overrides 关键字来通知我们正在重写一个方法。

Dim b as New Being
Dim o As New Object

我们创建两个对象。一个自定义定义和一个内置对象。

Console.WriteLine(o.ToString())
Console.WriteLine(b.ToString())

我们在两个对象上调用 ToString 方法。

Console.WriteLine(b)

正如我们之前所指定的,在对象上调用 Console.WriteLine 将调用其 ToString 方法。

$ dotnet run
System.Object
This is Being Class
This is Being Class

这是我们运行脚本时得到的结果。

继承

继承是一种使用已经定义的类来形成新类的方法。新形成的类被称为派生类,我们从中派生的类被称为类。继承的重要优点是代码重用和程序复杂性的降低。派生类(后代)重写或扩展了基类(祖先)的功能。

Program.vb
Option Strict On

Module Example

    Class Being
        Sub New()
            Console.WriteLine("Being is created")
        End Sub
    End Class

    Class Human
        Inherits Being

        Sub New()
            Console.WriteLine("Human is created")
        End Sub

    End Class

    Sub Main()

        Dim h As New Human

    End Sub

End Module

在此程序中,我们有两个类。一个基类 Being 和一个派生类 Human 类。派生类继承自基类。

Class Human
    Inherits Being

在 Visual Basic 中,我们使用 Inherits 关键字来创建继承关系。

Dim h As New Human

我们实例化派生 Human 类。

$ dotnet run
Being is created
Human is created

我们可以看到两个构造函数都被调用了。首先,调用基类的构造函数,然后调用派生类的构造函数。

接下来是一个更复杂的示例。

Program.vb
Option Strict On

Module Example

    Class Being

        Dim Shared Count As Integer = 0

        Sub New()
            Count = Count + 1
            Console.WriteLine("Being is created")
        End Sub

        Sub GetCount()
            Console.WriteLine("There are {0} Beings", Count)
        End Sub

    End Class

    Class Human
        Inherits Being

        Sub New()
            Console.WriteLine("Human is created")
        End Sub

    End Class

    Class Animal
        Inherits Being

        Sub New
            Console.WriteLine("Animal is created")
        End Sub

    End Class

    Class Dog
        Inherits Animal

        Sub New()
            Console.WriteLine("Dog is created")
        End Sub

    End Class


    Sub Main()

        Dim h As New Human
        Dim d As New Dog
        d.GetCount()

    End Sub

End Module

我们有四个类。继承层次结构更加复杂。HumanAnimal 类继承自 Being 类。而 Dog 类直接继承自 Animal 类,并间接继承自 Being 类。我们还引入了 Shared 变量的概念。

Dim Shared Count As Integer = 0

我们定义一个 Shared 变量。共享成员是所有类实例共享的成员。在其他编程语言中,它们被称为静态成员。

Sub New()
    Count = Count + 1
    Console.WriteLine("Being is created")
End Sub

每次实例化 Being 类时,我们都会将 Count 变量增加 1。通过这种方式,我们跟踪创建的实例数。

Class Animal
    Inherits Being
...
Class Dog
    Inherits Animal
...

Animal 继承自 Being,而 Dog 继承自 Animal。间接来说,Dog 也继承自 Being

Dim h As New Human
Dim d As New Dog
d.GetCount

我们从 HumanDog 类创建实例。我们调用 Dog 对象的 GetCount 方法。

$ dotnet run
Being is created
Human is created
Being is created
Animal is created
Dog is created
There are 2 Beings

Human 对象调用两个构造函数:Dog 对象调用三个构造函数。实例化了两个 beings。

抽象类和方法

抽象类不能被实例化。如果一个类包含至少一个抽象方法,则它也必须被声明为抽象。抽象方法不能被实现,它们仅仅声明了方法签名。当我们从一个抽象类继承时,派生类必须实现所有抽象方法。此外,这些方法必须使用相同或更少限制的可见性来声明。

接口不同,抽象类可以有具有完整实现的方法,也可以有已定义的成员字段。因此,抽象类可以提供部分实现。程序员经常将一些常见功能放入抽象类中。并且这些抽象类稍后被子类化以提供更具体的实现。例如,Qt 图形库有一个 QAbstractButton,它是按钮小部件的抽象基类,提供按钮的常见功能。按钮 Q3ButtonQCheckBoxQPushButtonQRadioButtonQToolButton 继承自此基抽象类。

正式来说,抽象类用于强制执行协议。协议是一组操作,所有实现对象都必须支持这些操作。

Program.vb
Option Strict On

Module Example

    MustInherit Class Drawing
        Protected x As Integer = 0
        Protected y As Integer = 0

        Public MustOverride Function Area() As Double

        Public Function GetCoordinates() As String
            Return String.Format("x: {0}, y: {1}", Me.x, Me.y)
        End Function

    End Class

    Class Circle
        Inherits Drawing

        Private Radius As Integer

        Sub New(ByVal x As Integer, ByVal y As Integer,
            ByVal r As Integer)
            Me.x = x
            Me.y = y
            Me.Radius = r
        End Sub

        Public Overrides Function Area() As Double
            Return Me.Radius * Me.Radius * Math.PI
        End Function

        Public Overrides Function ToString() As String
            Return String.Format("Circle, at x: {0}, y: {1}, radius: {2}",
                Me.x, Me.y, Me.Radius)
        End Function

    End Class

    Sub Main()

        Dim c as New Circle(12, 45, 22)

        Console.WriteLine(c)
        Console.WriteLine("Area of circle: {0}", c.Area())
        Console.WriteLine(c.GetCoordinates())

    End Sub

End Module

我们有一个抽象基类 Drawing。该类定义了两个成员字段,定义了一个方法并声明了一个方法。其中一种方法是抽象的,另一种方法是完全实现的。Drawing 类是抽象的,因为我们无法绘制它。我们可以绘制一个圆、一个点或一个正方形。Drawing 类具有我们可绘制的对象的某些常见功能。

MustInherit Class Drawing

在 Visual Basic 中,我们使用 MustInherit 关键字来定义一个抽象类。

Public MustOverride Function Area() As Double

抽象方法前面有一个 MustOverride 关键字。

Class Circle
    Inherits Drawing

Circle 是 Drawing 类的子类。它必须实现抽象的 Area() 方法。

$ dotnet run
Circle, at x: 12, y: 45, radius: 22
Area of circle: 1520.53084433746
x: 12, y: 45

这是 Visual Basic 中 OOP 描述的第一部分。