4.代码组织与封装

4.代码组织与封装

通过前面的内容,我们已经可以处理基本的数据类型,以及使用一些语句结构来控制代码的执行,但我们知道,要想开发出功能强大的应用软件,我们还有很多工作要做。

本章,我们将讨论对于自定义代码的封装和组织方式,主要内容包括:

  • 模块
  • 函数与子程序
  • 参数
  • 封装自已的转换函数

4.1. 模块

模块(Module)应该是VB.NET的一个特点了,对于一些非对象类代码,我们完全可以将它们定义在模块中,这样,只要引用了模块所在的命名空间,我们就可以使用模块中的内容,而不需要像面向对象代码那样,必须通过类或其实例才能使用其中的代码。

前面的示例中,我们也可以看到,实际上,VB.NET的程序就从一个指定模块中的Main()子程序中启动的;而在开发中,我们也可以定义自己的模块,下面,我们通过菜单“项目”->“添加模块”打开添加新项目的对话框,我们将模块的名称重新命名为“MDemo.vb”,然后,我们就可以在资源管理器中看到新的模块文件了,如下图。

图像说明文字

点击MDemo.vb,我们可以看到其中的内容,如下面的代码。

Module MDemo

End Module

接下来,我们就可以在这里添加自己的代码了,比如函数和子程序。

4.2. 函数与子程序

函数(Function)和子程序(Sub)是VB中最基本的代码封装单位,我们可以将特定功能的代码定义在函数或子程序中,然后可以在项目的其它地方调用。那么,它们有什么区别呢?其实很简单,我们先看函数的定义。

Function <函数名> (<参数>) As <返回值类型>
' 函数代码
End Function

我们可以看到,函数定义在Function和End Function之间。在Function语句中,我们需要指定函数的名称、函数返回值的类型,以及所需要的参数,当然,参数并不是必须的。如下面的代码,我们定义一个Adder()函数,它的功能是计算参数一到参数二的累加结果。

Module MDemo
    ''计算累加
    Function Adder(iMin As Integer, iMax As Integer) As Integer
        Dim iSum As Integer = 0
        For i = iMin To iMax
            iSum += i
        Next
        Return iSum
    End Function
End Module

我们可以看这个函数的一些元素:

  • 函数名是Adder。
  • 参数定义在参数名后面的一对小括号()中,我们共定义了两个参数(iMin和iMax),它们都是Integer类型的。请注意,如果没有参数,也同样需要这对小括号。
  • 这个函数的返回值类型是Integer,使用As关键字指定。
  • 函数的返回值。在函数内部,我们需要Return语句返回函数执行的结果,在VB6中它是通过赋值给函数的名称来完成的。此外,执行Return语句后,会立即结束函数,也就是说不会执行Return语句后面的代码,请注意灵活应用这一特性。

接下来,我们就可以在Main()子程序中调用这个函数了,如下面的代码。

Sub Main()
    Dim iSum As Integer
    iSum = Adder(1, 100)
    Console.WriteLine("1到100累加结果是{0}", iSum)
End Sub

通过对代码的封装,我们可以简化很多工作,特别是在开发中需要多次使用的代码,这可以极大地提高项目开发及维护等工作的效率。

再来看一看子程序,它到底和函数有什么不同呢?实际上,子程序与函数的不同只在于:(1)子程序没有返回值;(2)没了。^_^

定义子程序时,我们使用Sub关键字,它的基本结构如下:

Sub <子程序名> (<参数>)
' 子程序代码
End Sub

如下面的代码,我们定义了一个SayHello()子程序。

Sub SayHello (sName As String)
    Console.WriteLine("Hello, {0}.", sName)
End Sub

在这个子程序中,我们定义了一个参数sName,它的类型是字符串(String),在子程序中,我们在控制台输出一个文本,在Main()子程序中,我们可以使用如下代码来调用这个子程序。

SayHello ("Tom")

接下来,既然大家已经知道了子程序和函数的共同点和那么一点点的区别,我们也就可以省点事,从现在开始,我们将子程序和函数统称为“方法”,这是一个面向对象编程的概念,稍后我们就会看到,在类中定义的方法就是子程序或函数。

4.3. 参数

本节,我们将单独讨论方法中参数的定义和使用。

首先,我们需要了解,在方法中的参数,可以没有,也可以有一个或多个;每一个参数定义都应包含参数名称和参数类型,多个参数应使用逗号(,)分隔;除了这些,我们再来看一看非常重要的几个关于参数的概念,如按值或按引用传递、可选参数和参数数组。

4.3.1. 按值或按引用传递

按值传递时,会传递变量的副本,在方法中,当我们修改这个数据时,实际上是在修改副本的数据,而原数据不会改变。我们如下面的示例,首先,我们定义一个方法(Hello/MDemo.vb),其功能是对传入的数据进行加一运算。

Sub AddOne(ByVal iNum As Integer)
    iNum += 1
End Sub

接下来,我们在Main()方法中调用它,如下面的代码。

Dim iNum As Integer = 99
AddOne(iNum)
Console.WriteLine(iNum)

从代码上看,我们是想对iNum进行加1操作,但是,如果大家执行代码就会发现,iNum并没有被加1,结果显示依然是99。

其原因就是,我们定义的AddOne()方法,其参数定义为按值传递(ByVal),这样,在方法中加1的操作并不能反映到方法外定义的iNum变量上,因为在方法内只是在对iNum的复本在进行操作。

与按值传递相对应的是按引用(ByRef)传递,当我们将AddOne()方法的参数修改为按引用传递时,可以再次执行代码,就会发现,变量iNum的值才真正进行了加1的操作。

那么,我们在定义参数的传递方式时,应该如何选择呢?我们还是应该根据按值或按引用传递的特点,以及代码的需求、性能等方面综合考虑。

首先,我们看按值传递参数。按值传递会完全复制变量的数据,所以,可以有效保护原数据的安全;但是,如果原数据很大(比如一个包含了很多数据的对象),那么,数据的复制操作就会影响整个程序的性能,而我们必须要考虑,是需要高性能,还是需要对数据的绝对保护。

再看按引用传递参数。按引用传递参数,传递的效率非常高,因为是直接将数据内存区域的引用进行传递,并不对数据进行复制;也正是因为这样,在方法内,我们可以对原数据直接进行修改,这样,就有可能意外地影响原数据的内容。

所以,我们应该从代码的目的和功能、执行效率,以及安全性等方面综合考虑方法的参数是按值传递,或者是按引用传递。

4.3.2. 可选参数

当我们设置方法的参数时,有些参数可以设置为一个合适的值,这样,在调用方法时,我们就可以不设置此参数的值。在方法中设置可选参数时应注意:

  • 使用Optional关键字指定可选参数。
  • 可选参数必须在必选参数的后面。

如下面的代码,我们在AddPrefix()方法的第二个参数设置了可选参数。

Function AddPrefix(str1 As String, Optional sPrefix As String = "")
    Return sPrefix & str1
End Function

接下来,我们可以通过下面的代码来测试运行结果。

Console.WriteLine(AddPrefix("1","No."))
Console.WriteLine(AddPrefix("2"))

4.3.3. 参数数组

参数数组可以帮助我们动态地定义参数的数量,但有一些限制,即只能定义在参数列表的最后。定义一个参数数组时,我们使用ParamArray关键字,如下面的代码,我们定义了一个方法PrintStrings(),它的功能是在控制台中显示参数所带入的内容。

Sub PrintStrings(ParamArray msg() As String)
    For Each item As String In msg
        Console.WriteLine(item)
    Next
End Sub

接下来,我们可以测试以下几种调用参数数组的方式。

PrintStrings()
PrintStrings("abc")
PrintStrings("abc", "def", "ghi")

我们可以看到,参数数组可以支持带入零个或多个参数,这对某些功能,特别是在开发阶段,不能确定参数个数的情况下,使用参数数组是一个比较常用,而且有效的选择。

4.4. 封装自已的转换函数

关于子程序和函数的定义,实际并不复杂,接下来,我们就来点实际的操作。在第2章的学习中,我们已经讨论了关于数据类型的转换问题,对于VB.NET中的数据类型转换,我们可以分为两个主要类型:

  • VB内置转换函数和Convert类方法。它们的表现是一致的,即当参数可以转换为目标类型时,就返回转换后的数据;如果参数不能转换成目标类型,则会产生异常。
  • ToParse()方法,只能将字符串形式的数据转换为特定的值数据类型,虽然不会产生异常,但支持的数据类型范围有限,而且使用起来的代码也不够简洁。

综合以上两种情况,我们将封装自己的数据类型转换函数,其指导原则是:

  • 不会产生异常,也就是说,无论如何转换都会有一个结果;我们约定,如果不能正确转换为目标类型,则返回目标类型的默认值。
  • 使用VB风格的数据类型缩写,但转换函数名都以“To”开头。

此外,对于本书的封装代码,我们会创建在一个名为“VBChef”的项目中,当VB代码是原料时,我们就是制作出各种软件的厨师(Chef)。^^

下面,我们就来看看将Object类型转换为String类型的函数定义(VBChef/MCommon.vb文件)。

''转换为String类型
Function ToStr(ByRef obj As Object) As String
    If obj Is Nothing Then
        Return ""
    Else
        Return obj.ToString()
    End If
End Function

对于代码中的一些内容,你也许还不太明白,不过没关系,这个函数的功能就是;当参数obj是空对象(Nothing)时,就返回空字符串;否则将对象的ToString()方法定义返回其字符串形式。

关于Object类型和ToString()方法,我们会在面向对象编程相关内容中继续讨论,大家不必着急。

对于返回数值类型的转换方法,我们以Integer类型为例,如下面的函数,就是将Object类型数据转换为Integer类型的函数。

''转换为Integer类型
Function ToInt(ByRef obj As Object) As Integer
    If obj Is Nothing Then Return 0
    Dim result As Integer = 0
    If Integer.TryParse(obj.ToString(), result) Then
        Return result
    Else
        Return 0
    End If
End Function

代码中,当obj为空时,就返回0值;然后,我们使用obj的字符串形式(obj.ToString())来进行转换,转换工作使用了Integer.TryParse()方法,转换成功即返回转换结果,否则返回0值。

对于以上两个转换函数,我们可以看到,无论是否转换成功,我们都可以得到一个目标类型的数据,在某些场合下,这比处理转换失败时的异常情况要简单地多,而且在大多情况下,这样做也是比较合理的。

不过,如果在程序中要严格区分空值和有效数据时,比如数据库中0值和空值(NULL)就是严格区分的两个不同概念。此时,我们使用另外一个版本的转换函数,即返回结果为可空类型;如下面的代码,我们还以Integer类型为例。

''转换为Integer类型或Nothing
Function ToIntNullable(ByRef obj As Object) As Integer?
    If obj Is Nothing Then Return Nothing
    Dim result As Integer = 0
    If Integer.TryParse(obj.ToString(), result) Then
        Return result
    Else
        Return Nothing
    End If
End Function

接下来,我们可以在Main()方法中测试这个函数。如下面的代码。

Imports VBChef.vbchef

Module Module1
    Sub Main()
        Dim str1 As String = "123"
        Dim result As Integer? = ToIntNullable(str1)
        If result Is Nothing Then
            Console.WriteLine("转换失败")
        Else
            Console.WriteLine(result)
        End If
    End Sub
End Module

请注意,由于我们将封装的代码定义了vbchef命名空间,所以,在使用之前必须使用Imports语句引用它。

关于可空类型,它是由不可空类型添加问号(?)生成的一种类型,可以保存原类型的数据,也可以是空值(Nothing)。如代码中使用的Integer?类型就是可空Integer类型,它可以保存32位整数,也可以是Nothing,在使用这种类型的变量时,必须首先判断它是否为空值,然后才能正确使用。

关于空值,我们必须要区分的是Nothing表示空引用(空对象),而数据库中空值(NULL)的概念,在.NET Framework中定义为DBNull.Value值。

目录

  • 前言
  • 1.概述
  • 2.基本数据类型
  • 3.程序流程控制
  • 4.代码组织与封装
  • 5.枚举
  • 6.结构
  • 7.面向对象编程
  • 8.类的继承
  • 9.接口
  • 10.设计模式基础
  • 11.事件与委托
  • 12.泛型
  • 13.异常处理
  • 14.数组与集合
  • 15.字符串与散列算法
  • 16.日期与时间
  • 17.文件系统
  • 18.图形图像
  • 19.网络
  • 20.数据库应用基础
  • 21.创建数据层组件
  • 22.获取系统信息
  • 23.Windows窗体应用代码封装
  • 24.ASP.NET应用开发基础