Visual Basic快捷教程——函数与子程序

Posted 白马负金羁

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Visual Basic快捷教程——函数与子程序相关的知识,希望对你有一定的参考价值。

函数是结构化程序设计的基础。可以拿现实生活中的“社会分工”这个概念来做类比。假设做包子只需要用到两种原料:一是用来做包子皮的面粉,另外就是用来做包子馅料的猪肉。作为一名专门做包子的厨师,你当然不能自己生产面粉和猪肉,所以你通常会从磨坊直接购买面粉,从肉铺直接购买猪肉。在这两种原料都具备的时候,你的任务仅仅只是负责做包子。更进一步,你还可以假设磨坊只负责把麦子磨成面粉,而不负责种植麦子,种植麦子的工作则有农民专门负责。对于做包子的厨师来说,他把生产面粉和猪肉这两项工作承包给了磨坊和肉铺,所以厨师无需关心到底该如何使用碾磨机也无需关心该如何屠宰生猪。而磨坊又把种植麦子这个工作承包给了农民,所以磨坊工人也就无需关心给麦子施肥或浇水的事情。这就是所谓的“社会分工”。而函数的本质就是为了把程序切分成若干个相对独立的模块各司其职,而各个函数的作用也就是仅仅专门负责自己份内的那份功能。


我们还可以简单的把函数比喻成一个“黑盒子”。这个黑盒子对外只有两个接口,一个用来接收数据,一个用来输出数据。我们只要把数据送进黑盒子,就能得到计算结果,至于盒子内部究竟是如何工作的,我们都可不必关心。函数就是这样,外部程序所知道的仅限于给函数传入什么数据,以及函数输出什么数据,其他都无关紧要。


不同的函数可以接收不同输入,给出不同的输出,当然这跟内部的实际工序有关,即函数所执行的功能各异。这就好比现实生活中的化学反应过程:氢气和氧气可以反应生成水,水也可以分解成氧气和氢气。

化学反应的过程总是伴随着一定物质的输入和新物质的产出,至于什么物质生成什么新物质除了跟输入有关以外,还跟反应进行的条件有关。例如,木炭在氧气中燃烧可以生成二氧化碳,但是在某些条件下也可能产生一氧化碳,如下图所示,这取决于实际反映的条件。函数也是这样,同样的输入,也可以得到不同的结果,这就跟函数内部的实现有关。


经过上面的描述读者应该对函数有了一个初步的认识,可以明确函数就是接收输入,并在其内部处理数据,最后再输出结果的一个独立的代码单元。


一、函数


在Visual Basic中,函数是一组以Function开头和End Function结尾的封闭程序代码语句。当函数被调用时,便会开始执行函数体内所定义的程序代码。当执行函数内的程序代码语句时,遇到Exit Function、Return 或 End Function语句,便完成函数的执行。


注意除了自己定义的(以Function开头,以End Function结尾的)函数以外,Visual Basic中还提供了很多有用的内置函数,例如在字符串中搜索子串的 IndexOf 函数。本文主要讨论自定义的函数。


出于各司其职的目的,在实际编程中会考虑把那些能够独立实现某种功能的代码片段封装成一个独立的函数。此外,如果这些功能模块的使用比较频繁,那么通过函数调用的形式也可以大大精简代码冗余,从而使代码更为简明,增强其可读性。


函数的声明方法,如下所示:

[Accessmodifier][Proceduremodifiers][Shared][Shadows]
Function 函数名称(参数列表) As 返回数据类型
    函数主体
    ...
End Function
函数的默认权限是Public,若要修改权限则需要变更访问限定符(Accessmodifier),下表是函数名称前每个关键词的用法解释。注意,其中涉及了很多面向对象编程的内容,我们会在后续的文章中做详细解释。

1. 函数的参数传递

根据前面的函数声明规则,在函数名称后面有个参数列表,这就是要传给函数的一些参数,统称“参数列表”,而参数列表的每个参数由于是局部变量,所以在离开函数回到主程序时就会自动释放。参数列表的声明方法如下所示:

[Optional][ByVal|ByRef][ParamArray] 参数名称 As 参数数据类型
关于参数列表的参数传递机制用法,可以参见下表。

下面我们将对以上参数传递机制作详细介绍。


1.1  按值传递


调用函数的函数与被调用函数之间需要传递消息,这项工作就是通过参数传递来完成的。你可以想象做包子的厨师要让磨坊给他提供面粉的话,至少应该告诉对方,他需要多少斤的面粉,当磨坊收到这个信息后,才能准确地提供厨师所要的面粉。这个“多少斤”就是二者之间传递的消息,更准确地说是厨师(相当于调用函数的函数)向磨坊(被调用函数)传递的消息。


按值传递是最简单也最常用的一种消息传递方式。调用函数的函数(有可能是主函数)将参数值传给被调用函数,执行后被调用函数无论怎么修改参数值,都不会改动参数变量在调用函数之函数中的值。所以在按值传递参数时,被调用函数是在其内部重新复制了一个参数,复制参数与实际参数变量并不存放在同一个内存地址上。被调用函数内部仅对参数副本进行操作,因而不会改变调用函数之函数中的原参数变量值。


来看一个简单的例子,我们在文本框中输入一个整数值,然后单击【累加】按钮,程序将在弹出的对话框中给出输入的整数值加1后的数值。

Public Class Form1

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

        Dim num As Integer
        Try
            num = TextBox1.Text
        Catch ex As Exception
            MsgBox("请在文本框中输入一个整数!", vbExclamation, "提示")
            Return
        End Try

        Add_one(num)

    End Sub

    Public Function Add_one(ByVal num As Integer)
        num += 1
        MsgBox(TextBox1.Text & "加1之后的结果是" & num.ToString & ".", , "提示")
        Return 0
    End Function

End Class
上述代码的执行结果如下,可见我们得到了预期的效果。
但有时按值传递参数也会带来麻烦。由于被调用函数内对参数的任何操作在函数调用结束后都会“失效”,如果编程时没有注意到这一点就会引起意想不到的问题。例如,我们把上面代码中显示最终结果的语句调整一下位置,如下所示。

Public Class Form1

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

        Dim num As Integer
        Try
            num = TextBox1.Text
        Catch ex As Exception
            MsgBox("请在文本框中输入一个整数!", vbExclamation, "提示")
            Return
        End Try

        Add_one(num)
        MsgBox(TextBox1.Text & "加1之后的结果是" & num.ToString & ".", , "提示")
    End Sub

    Public Function Add_one(ByVal num As Integer)
        num += 1
        Return 0
    End Function

End Class
再次执行程序,并不意外,我们发现得到了一个错误的答案,因为被调用函数体内的操作并不影响调用函数的函数中参数变量的值。



1.2  引用传递


为了克服按值传递的问题,我们可以选择使用按引用传递。在使用这种参数传递方式时,调用函数的函数(有可能是主函数) 与 被调用的函数中的参数变量都存在同一个内存地址上。于是在被调用的函数中对参数进行修改会同时改变调用函数的函数中参数变量的值。就之前的累计器程序而言,只要把子函数的代码由

    Public Function Add_one(ByVal num As Integer)
        num += 1
        Return 0
    End Function
改成

    Public Function Add_one(ByRef num As Integer)
        num += 1
        Return 0
    End Function
就可以达到我们想要的结果了。读者可以在调整代码后自行执行程序,这里不再给出执行结果。

下面再给出一个利用引用传递的方法来传递参数的例子。这个程序的目的在于逐个搜索一段文本中出现的目标单词。

Public Class Form1

    Dim pos As Integer = -1

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

        Search_Next(pos)

        RBox_Text.SelectionStart = 0
        RBox_Text.SelectionLength = RBox_Text.TextLength
        RBox_Text.SelectionColor = Color.Black
        RBox_Text.SelectionBackColor = Color.WhiteSmoke

        If pos = -1 Then
            MsgBox("Cannot find the word '" & TxtBox_Word.Text & "' any more !",
                   MsgBoxStyle.Information, "提示")
        Else
            RBox_Text.SelectionStart = pos
            RBox_Text.SelectionLength = TxtBox_Word.Text.Length
            RBox_Text.SelectionColor = Color.Blue
            RBox_Text.SelectionBackColor = Color.Gold
        End If

    End Sub

    Public Function Search_Next(ByRef pos As Integer)
        pos = RBox_Text.Text.IndexOf(TxtBox_Word.Text, pos + 1)
        Return 0
    End Function

End Class
请读者完成编码后,执行上述程序。如下图所示,当我们要在文段中搜索“was”这个单词时,单击【搜索下一个】按钮,程序就会从当前位置开始搜索下一个出现的目标单词,并将其高亮显示。在后面,我们还会用其他方法来改写这个程序,这里暂且不表。


1.3  选择性传递


有时调用函数,会选择性地给出参数。你可以想象在厨师向磨坊购买面粉的例子中,厨师每次都要专门跟磨坊说他要买多少面粉可能有点麻烦。厨师为了图方便,就跟磨坊约定,如果不做特别说明,他每次都买100斤面粉。如果遇到特殊情况,比如这个月因为节庆的缘故,预定包子的人数陡增,厨师会特别跟磨坊说他要买200斤面粉。至于其他通常的情况,他则不做特别说明,磨坊则按约定默认卖100斤面粉给厨师。


在下面这个例子中,用户输入购买多少本图书,以及打多少折,然后程序会自动计算出用户需要支付多少钱。如果用户不输入折扣信息,则程序默认按原价出售图书。

Public Class Form1

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

        Dim num1, discount1 As Integer

        Try
            num1 = TxtBox_num1.Text
        Catch ex As Exception
            MsgBox("请在文本框中输入购买的数量!", vbExclamation, "提示")
            Return
        End Try

        If TxtBox_disc1.Text = Nothing Then
            output(num1)
        Else
            Try
                discount1 = TxtBox_disc1.Text
            Catch ex As Exception
                MsgBox("请在折扣栏中输入正整数!", vbExclamation, "提示")
                Return
            End Try
            output(num1, discount1)
        End If

    End Sub

    Public Function output(ByVal num1 As Integer,
                           Optional ByVal discount1 As Integer = 100)

        MsgBox("您一共需要支付" & (50 * num1 * discount1 / 100).ToString & "元。",
               MsgBoxStyle.Information, "账单")
        Return 0

    End Function

End Class
请读者完成编码后,执行上述程序。程序的执行结果如下,我们不做过多的解释。


1.4  ParamArray


ParamArray算是Visual Basic中提供的一种比较特别的参数传递方式,这与C/C++中传递指向数组的指针类似,但是在Visual Basic中并没有指针这个概念。从这种参数传递机制的名字可以看出,它是把数组(Array)当做参数(Parameter)来进行传递的。声明ParamArray参数的语法如下:

Public Function 函数名称 (ByVal ParamArray [数组名]() As [数组数据类型]) 
    函数主体
    ... ...
End Function
ParamArray的主要应用场景往往是在不确定参数个数的情况下来设法实现参数的传递。例如在下面这个例子中,我们要求得一些数字的和,但具有多少个数字事前并不清楚。

Public Function SumOfArray (ByVal ParamArray values_array() As Integer)
    Dim intSum As Integer = 0
    For Each x As Integer In values_array
        intSum = intSum + x
    Next
    return intSum
End Function
在调用ParamArray的函数时,给定参数的方法有两种,一种是给定数组参数,另一种则是手动给定多个参数,下面列举的两种用法所得之结果是完全相同的。首先,是分别指定各个参数的调用举例:

SumOfArray(22, 33, 44, 55)
或者,还可以将数组名作为参数来完成调用,例如:

Dim my_array() As Integer = {22, 33, 44, 55}
SumOfArray(my_array)
另外,需要特别提醒读者注意的是:ParamArray参数只能声明在参数列表的最后一个,而且调用的时候可以给定多个参数。
下面给出一个更加复杂的例子。这个程序实现了对图像进行水平方向上的镜像翻转的动能。我们需要读者从这个例子中学习或者注意的地方包括:

  • 当函数的参数列表中有多个参数时,ParamArray参数只能声明在参数列表中的最后一个。
  • 这个例子中用到了函数调用的嵌套,即被调用的函数中有调用了其他的函数。
  • 在Visual Basic中进行数字图像处理的一些技术和方法。

Imports System.IO
Imports System.Drawing.Imaging
Imports System.Runtime.InteropServices

Public Class Form1

    Dim pic_Aston As Image = Image.FromFile(Application.StartupPath & "\\pic\\Aston.jpg")

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

        Dim sourceImg As New Bitmap(pic_Aston)

        ' Lock the bitmap's bits. 
        Dim rect As Rectangle = New Rectangle(0, 0, sourceImg.Width, sourceImg.Height)

        Dim bmpData As BitmapData = sourceImg.LockBits(rect, _
        ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb)

        'Get the address of the first line of the bitmap. 
        Dim ptr As IntPtr = bmpData.Scan0

        ' Declare an array to hold the bytes of the bitmap. 
        Dim numBytes As Integer = sourceImg.Width * sourceImg.Height * 3
        Dim rgbValues(numBytes) As Byte

        ' Copy the RGB values into the array.
        Marshal.Copy(ptr, rgbValues, 0, numBytes)

        ImgProcess(sourceImg.Width, sourceImg.Height, rgbValues)

        ' Copy the RGB values back to the bitmap
        Marshal.Copy(rgbValues, 0, ptr, numBytes)

        ' Unlock the bits.
        sourceImg.UnlockBits(bmpData)

        PicBox_Dst.Image = sourceImg

    End Sub

    Public Function ImgProcess(ByVal width As Integer, ByVal height As Integer,
                               ByVal ParamArray rgbValues() As Byte)

        For i As Integer = 0 To height - 1
            For j As Integer = 0 To width / 2

                swap(rgbValues(j * 3 + i * width * 3), rgbValues((i + 1) * width * 3 - j * 3 - 3))
                swap(rgbValues(j * 3 + i * width * 3 + 1), rgbValues((i + 1) * width * 3 - j * 3 - 2))
                swap(rgbValues(j * 3 + i * width * 3 + 2), rgbValues((i + 1) * width * 3 - j * 3 - 1))

            Next
        Next

        Return 0
    End Function

    Public Function swap(ByRef a As Byte, ByRef b As Byte)

        Dim tmp As Byte
        tmp = a
        a = b
        b = tmp

        Return 0
    End Function

End Class
上述代码的执行结果如下图所示,当用户单击【水平翻转】按钮时,右侧图像框中就会显示出左侧图像的水平镜像结果。


2. 函数的返回值

调用函数的函数通过参数向被调用的函数传递消息。而被调用的函数则利用返回值来向调用该函数的函数反馈消息。这就好比厨师跟磨坊说要买100斤面粉,磨坊就给厨师送来100斤面粉的货,这里的“100斤面粉”就相当于是返回值。返回值的数据类型可以自己定义,而返回的关键字是Return(前面的例子中已经多次用到了它)。通常在程序结束或者在Exit Function前返回值。假设要返回整数1,就直接写Return 1,程序会根据定义的返回数据类型来返回适当的值。注意返回值使用的时机应该是被调用函数运算完成之后,需要把计算结果传回调用该函数的函数里的时候。


回忆前面曾经写过的那个“逐个搜索一段文本中出现的目标单词”的程序,彼时在Search_Next()函数中,我们采用的参数传递方式为按引用传递,这样调用函数的函数才能够得到下一个目标单词出现的位置。下面我们改写这个函数,采用按值传递的方式来传递参数,为了程序仍然可以得到预期的运行结果,我们把下一个目标单词出现的位置以返回值的形式传回。

Public Class Form1

    Dim pos As Integer = -1

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

        pos = Search_Next(pos)

        RBox_Text.SelectionStart = 0
        RBox_Text.SelectionLength = RBox_Text.TextLength
        RBox_Text.SelectionColor = Color.Black
        RBox_Text.SelectionBackColor = Color.WhiteSmoke

        If pos = -1 Then
            MsgBox("Cannot find the word '" & TxtBox_Word.Text & "' any more !",
                   MsgBoxStyle.Information, "提示")
        Else
            RBox_Text.SelectionStart = pos
            RBox_Text.SelectionLength = TxtBox_Word.Text.Length
            RBox_Text.SelectionColor = Color.Blue
            RBox_Text.SelectionBackColor = Color.Gold
        End If

    End Sub

    Public Function Search_Next(ByVal pos As Integer)
        pos = RBox_Text.Text.IndexOf(TxtBox_Word.Text, pos + 1)
        Return pos
    End Function

End Class

上述代码的执行结果与前面的情况无异,这里不再赘述。联系之前的例子,应该可以意识到利用ByRef方式传递参数可以实现类似返回值的功能。但是利用ByRef方式传递参数来替代普通的返回值语法有什么好处呢?这就涉及到在Visual Basic中如何返回多个返回值的问题。


2.1  返回多个返回值的技巧

在化学反应中,我们把参与反应的物质投入之后,在一定条件下,所得到的结果可以是多种物质的混合物。例如木炭在氧气中燃烧所得的就可能是一氧化碳和二氧化碳的混合物。但是在编写计算机程序时,通常函数的返回值就只能有一个,Visual Basic亦是如此。但如果要返回的值不止一个,该怎么办呢?通过ByRef方式来续用之前所传之参数就可以实现多个返回值,但这种方式存在一定风险,可能会因为马虎大意而改变参数值,从而导致原来的主程序发生错误,所以使用时必须要谨慎。

另外,用ArrayList的方式来存储多个值,并将其作为参数进行传递也能实现返回多个值的效果。我们前面介绍过如果用ByVal这种方法来传递参数,被调函数体内的参数变量改变并不会影响调用函数之函数内原有的参数变量值。但如果传递的是ArrayList,情况则又有不同了。在被调函数体内改变ArrayList类型参数的值,调用函数之函数内原有的ArrayList类型参数变量值也会随之改变。例如定义下面这样一个函数 InsertElement,在函数体内ArrayList类型参数变量值会被调整。

    Public Function InsertElement(ByVal mylist As ArrayList)

        mylist.Insert(0, "呼啸山庄,")
        mylist.Insert(1, "艾米莉·勃朗特 著,")
        mylist.Insert(2, "杨苡 译,")
        mylist.Insert(3, "译林出版社.")
        Return 0
    End Function
然后在其他函数内调用 InsertElement 函数,可以发现ArrayList类型的参数变量mylist也被改变了。
    Dim mylist As New ArrayList()
    Dim strBuf As String = ""
        
    InsertElement(mylist)
    For i As Integer = 0 To mylist.Count - 1
        strBuf = strBuf & mylist(i).ToString()
    Next


2.2  返回数组

说到返回多个返回值的技巧,你可能会想到是否也可以把多个结果放在一个数组里,然后返回一个数组。在Visual Basic中,要返回数组,只要于声明函数时在返回数据类型后面多加一个括号(),并在函数结束时返回数组名称。语法规则如下:

Public Function 函数名称 (参数列表) As 返回值数据类型()
    ' 函数主体
    ' ... ...
    Return [返回数组名称]
End Function
下面这个程序片段实现了前面InsertElement函数类似的功能。不同的是InsertElement函数通过传递ArrayList类型的参数变量的形式实现多返回值的效果。而此处的return_array函数则通过返回数组的形式实现多返回值的效果。

    Public Function return_array() As String()
        Dim my_array(3) As String
        my_array(0) = "呼啸山庄,"
        my_array(1) = "艾米莉·勃朗特 著,"
        my_array(2) = "杨苡 译,"
        my_array(3) = "译林出版社."
        Return my_array
    End Function
然后在其他函数内调用 return_array 函数,不难发现下面代码实现了之前程序一模一样的功能

Dim my_str(3) As String
my_str = return_array()

Dim strBuf As String = ""
For Each i As String In my_str
	strBuf = strBuf & i.ToString()
Next

2.3  退出函数

如果在函数执行过程中,想要根据某些判断条件相机退出当前正在执行的函数,这时就要用到Exit Function指令来协助实现。其用法如下:

Public Function 函数名称()
    '... ...
    Exit Function  '在想要跳出函数的地方使用该指令
    '... ...
End Function
Exit Function指令的含义与前面介绍循环结构时提到的Exit For有异曲同工之处,Exit For是跳出正在执行的循环体,而Exit Function是跳出当前函数。此外,因为当遇到Exit Function时,其后面的函数体代码段就不会被执行,所以通常都会要涉及一定的条件判断,即只有当满足某些条件时才会“提前”结束函数执行。


二、子程序


子程序(Subprogram或Subroutine)是指一个可以单独编译但不能单独执行的程序片段。子程序通常需要通过调用或触发的方式才能执行。子程序具有模块化的效果,可以将一个庞大复杂的系统拆解成为数众多的子程序,然后再交由不同的开发人员进行分工完成,从而加快整个系统的开发进度。


Visual Basic中,子程序是以Sub开头,以End Sub结尾的封闭的程序代码段。当Sub被调用时,便会开始执行Sub内所定义的程序代码,当执行Sub内代码遇到Exit Sub或End Sub时便结束Sub程序的执行。


需要说明的是,Sub程序没有返回值,这也是与Function程序的最大区别。Sub程序默认被设定为Public,若需要指定不同的调用范围,可以在声明中使用Protected、Friend、Protected Friend 或 Private来控制调用范围。Sub的声明方法如下所示:

[Accessmodifier][Proceduremodifiers][Shared][Shadows]
Sub 子程序的名称 [(参数列表)]
    'Sub程序主体
    '... ...
End Sub

SubAccessmodifier与Proceduremodifiers跟函数里的情况是一样的,前面已经介绍过,这里不再重复。如果要调用一个Sub,可以使用如下的语法形式:

Call  已经声明过的Sub名称(参数列表)
除此之外,像调用Function那样直接使用已经声明过的Sub名称并带上相应的参数列表(如果有的话)也可以实现对Sub的调用。Sub与Function的参数传递机制基本是一样的。下面通过一个例子来帮助读者熟悉Sub的用法。下面这个例子是用Sub改写之前程序得到的。被调用函数将会把与图书信息有关的若干个条目进行赋值。注意,因为Sub程序没有返回值,所以我们要改写的版本应该是利用 ArrayList类型参数替代返回值的版本。

    Public Sub InsertIntoList(ByVal mylist As ArrayList)

        mylist.Insert(0, "呼啸山庄,")
        mylist.Insert(1, "艾米莉·勃朗特 著,")
        mylist.Insert(2, "杨苡 译,")
        mylist.Insert(3, "译林出版社.")

    End Sub
当需要调用 InsertIntoList 子程序时,可以使用类似下面的代码:

    Dim mylist As New ArrayList()
    Dim strBuf As String = ""
    Call InsertIntoList(mylist)
    For i As Integer = 0 To mylist.Count - 1
        strBuf = strBuf & mylist(i).ToString()
    Next
读者可以将此与之前的代码进行比较,下面我们将总结函数与子程序的各种异同。


三、函数与子程序的比较


Function 和 Sub 类似,主要区别在于Function可以有返回值,但 Sub 则无返回值。而由于返回值的关系,Function 可以直接写在表达式中,可是 Sub 却不行。例如各有一个求N的阶乘的Function 和 Sub,名称都是 N_factorial(),只有在N_factorial()被声明成Function时才可以使用 x = N_factorial(5) 这样的语法。相反,如果N_factorial()Sub则不能写成上面那样的形式。


还有很重要的一点,因为 Sub 没有返回值,所以 Sub 无法实现递归(关于递归将在本章最后一节介绍)。以下列出了Function 和 Sub 的对比表。



四、结构化程序设计


使用函数(或子程序)能够极大程度地提高程序质量,简化开发的复杂度。这也是使用函数(或子程序)的好处。当然笼统的说,在Visual Basic中使用函数(或子程序)的好处就是能够实现结构化的程序设计,所以其他一些具体好处都是由结构化程序设计衍生发展而来。

1. 结构化程序设计

结构化程序设计的思想是指以模块化设计为中心,将待开发的软件系统划分为若干个相互独立的模块。每个模块彼此独立,相互不会影响。因此这样更便于把一个复杂的问题分解为若干个独立的小问题,这样当个问题将更加明确而易于实现,从而为整个开发过程奠定一个健康的流程。

2. 函数在结构化程序设计中的角色

函数是实现结构化程序设计的基础,每一个函数都可以看作是一个独立的模块,它们将独立完整的功能单元进行有效封装,从而对复杂系统的搭建和设计进行了简化。这也更便于大规模的流水作业。想象一下现如今的工厂是如何工作,无论是一个生产飞机的工厂、还是一个生产电子琴的工厂,他们都拥有一个条长长的流水线。流水线上的工作仅仅是进行零件的组装。而一件完整的产品往往被分割成许多独立的部分。例如飞机的发动机、机翼、座椅等都是单独生产的,生产发动机的部门只要为最后组装提供发动机即可,而无需关心机翼的形状如何。最后所有零件再被有序的组装到一起,一架看似复杂无比的飞机就这样被轻松的制造出来了。

3. 结构化程序设计的好处

结构化程序设计的好处除了明确问题本身,并简化问题的求解以外,可能好处还远不止如此。

  • 结构化程序设计方法提高了软件的复用率。

就像库函数一样,一个 IndexOf 函数被编写好之后,很多程序再需要进行输出操作的时候就不用再重复编写该功能函数了。我们为某个软件所编写的函数完全可以移植到另外一个软件中,只要需求相同,这些函数就可以共享。

  • 结构化程序设计提高了程序的可读性,便于排错和维护。

如果一个长达上万行代码的程序都被写在一个主函数中,那时我们程序一旦测试出错,那么找到错误的过程无异于大海捞针。而如果程序被分成许多精细的小函数,那么定位就要迅速的多。这就好比一个管理井井有条的仓库会把不同的东西分类摆放到不同的房间中,我们需要什么东西只需到指定的存储房间去寻找即可。但如果所有物品都堆在一起,那么找到一样东西就变得复杂许多了。

  • 结构化的程序设计方法对于软件后期的升级维护也大有裨益。

例如,一台计算机随着时间的推移可能硬盘显得有些不够大,这时我们只需换上一个新硬盘即可,而不用把整台计算机都换掉。同理,当一个软件系统需要升级的时候,可能我们需要做的只是升级他的某一个部分,而不一定要把整个软件都重新编写。这时如果程序被非常有条理的划分为几个独立的模块,那么我们就只需更换需要升级的模块即可。杀毒软件是现实中一种需要频繁更新的软件系统,这样它才能够应对层出不穷的病毒攻击。那么绝大多数更新我们只是在对杀毒软件的病毒库进行更新,而不是把整个软件都重新安装,这就是一个非常典型的说明结构化程序设计好处的例子。


五、递归


递归的函数调用,就是指自己调用自己的函数。针对某些特殊问题,使用递归进行求解,编码会更加明晰,易读性也更强。一个完整的递归程序必须具备两种要素:递归条件和终止条件。所谓递归条件就是程序应该在什么条件下调用自己,终止条件就是程序应该在什么条件下终止递归。在某些情况下,递归和循环二者之间可以相互转换(但这涉及到算法设计方面的一些问题,这里不具体展开)。递归程序(或递归的函数调用)的基本格式如下:

Function 函数名称(参数 As 数据类型)
    If 终止条件 Then
        Return 终止值
    Else
        Return 函数名称(调整后的参数)
    End If
End Function

下面我们举一个简单的例子来演示在Visual Basic中编写递归程序的方法。这个例子是改写自前文《Visual Basic快捷教程——流程控制 》中的欧几里得算法求解最大公约数的程序。之前我们采用的是循环的形式来编写的代码。而且前面也已经提到:在某些情况下,递归和循环二者之间可以相互转换。所以这里的例子,我们将采用递归的形式来改写原来的程序。

Public Class Form1

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

        Dim v1 As Integer = TextBox_v1.Text
        Dim v2 As Integer = TextBox_v2.Text

        MsgBox("A与B的最大公因数=" & GCD(v1, v2))
    End Sub

    Public Function GCD(ByVal v1 As Integer, ByVal v2 As Integer)
        If (v1 Mod v2 = 0) Then
            Return v2
        Else
            Return GCD(v2, v1 Mod v2)
        End If
    End Function

End Class


另外一个可以用递归来解决的经典问题就是著名的汉诺塔问题(Hanoi),该问题最初源于法国数学家爱德华·卢卡斯(Édouard Lucas)于1883年发明的一个休闲数学游戏。但也同时流传着一个与之相关的古老传说。据说在印度的一座神庙里,一块黄铜板上立着三根金刚石柱子。印度教的主神梵天创造世界的时候,在其中一根柱子上从下到上地穿好了由大到小的64 片黄金圆盘,这就是所谓的汉诺塔。不论白天黑夜,总有一个僧侣在按照下面的法则移动这些圆盘:一次只移动一片,而且不管在哪根柱子上,小圆盘必须置于大圆盘上面。僧侣们预言,当所有的圆盘都从梵天穿好的那根柱子上移到另外一根柱子上时,世界就将毁灭。后人已经无法确定卢卡斯是自创的这个游戏,还是他从别人那里听闻过这个传说。


汉诺塔是一个需要用递归方法进行求解的典型问题。我们考虑简化的情况,即只有3个盘子的汉诺塔模型,如下图所示。



下面以三阶汉诺塔的移动为例来看看这个问题应该如何求解。如下图所示为三阶汉诺塔的求解过程。如果想移动底部的大号盘子,则必须移开它上面的中号盘子,而若想移动中号盘子,又必须移动上面的小号盘子。所以首先将小号盘子从A移到C,再将中号盘子从A移到B。然后将小号盘子从C移到B,并将大号盘子从A移到C,然后再考虑将中号盘子从B移到C。所以需要将小号盘子从B移到A,然后再将中号盘子从B移到C,最后再将小号盘子从A移到C,则整个过程就完成了。当盘子数增加时,只要按着这样的递归规则来移动,最初的大问题就会逐个分解为规模更小的问题,求解难度也随之降低。


推广开来,当盘子数目n为1 时,只要将盘子从A直接移动到B上即可。当n > 1时,则需要利用C来作为辅助的腾挪空间。这时需要想办法将n − 1个较小的圆盘依照规则从A移到C,再将剩下的最大的盘子从A移到B。最后再将n − 1个小盘依照规则从C移到B。如此下去,n个圆盘的移动问题就可以分解为两次n − 1个圆盘的移动问题,这也就体现出了分而治之的策略。下面我们就在Visual Basic中实现求解汉诺塔问题的示例程序。

Public Class Form1

    Private Sub Btn_Start_Click(sender As Object, e As EventArgs) Handles Btn_Start.Click

        List_Output.Items.Clear()

        Dim ncycle As Integer
        Try
            ncycle = TxtBox_Num.Text
        Catch ex As Exception
            MsgBox("请在文本框中输入一个正整数!", vbExclamation, "提示")
            Return
        End Try

        Hanoi(ncycle, "A", "B", "C")
    End Sub

    Public Function Hanoi(ByVal n As Integer, ByVal Src As Char,
                          ByVal Mid As Char, ByVal Dst As Char)
        If n = 1 Then
            Moves(n, Src, Dst)
        Else
            Hanoi(n - 1, Src, Dst, Mid)
            Moves(n, Src, Dst)
            Hanoi(n - 1, Mid, Src, Dst)
        End If

        Return 0
    End Function

    Private Sub Moves(num As Integer, pos1 As String, pos2 As String)
        List_Output.Items.Add("Move dish " & num & " from " & pos1 & " to " & pos2 & ".")
    End Sub

End Class
上述代码的执行结果如下图所示。当用户在文本框中输入圆盘的数量后,单击【开始移动】按钮,程序就会自动模拟汉诺塔的移动过程并有右侧的框图里进行显示。




*本文中所使用的Visual Studio版本为2013。


(本文完)

以上是关于Visual Basic快捷教程——函数与子程序的主要内容,如果未能解决你的问题,请参考以下文章

Visual.Basic.6大学教程pdf

Visual Basic程序设计案例教程pdf

Visual Basic程序设计应用教程(第2版)

Visual.Basic程序设计教程(第四版)pdf

Visual Basic程序设计应用教程(第2版)pdf

Visual Basic从入门到精通pdf