nxhujiee / vb.net语法 / 2.2 VB.NET语法基础

分享

   

2.2 VB.NET语法基础

2010-04-07  nxhujiee

正如语言是人们之间交流的基础,要想开发软件,就必须掌握一种计算机编程语言。在前面的章节中已经编写了一些简单的程序,但要想真正地开发出界面美观、功能完善的软件,就必须更进一步地学习计算机编程语言。

本节介绍VB.NET的基础语法。

2.2.1 基本数据类型

我们从小学就开始学习数学,知道有整数、小数、有理数、无理数、实数和虚数等许多数的类型。但在大多数计算机编程语言中,并没有这么多种数的类型,基本的数只有两种:整数和小数。但根据数表示范围的不同,这两大类又可以再进一步细分。

在VB.NET中,数字分为如表2-5所示的几种类型。

表2-5 VB.NET数字类型

VB类型

CLR类型

存储空间

取值范围

Byte

System.Byte

个字节

0255(无符号)

Decimal

System.Decimal

16 个字节

0+/-79 228 162 514 264 337 593 543 950 335之间的整数

0+/-7.922 816 251 426 433 759 354 395 033 5之间的小数

Double

System.Double

个字节

负值取值范围为-1.797 693 134 862 315 70E+308-4.940 656 458 412 465 44E-324

正值取值范围为4.940 656 458 412 465 44E-3241.797 693 134 862 315 70E+308

Integer

System.Int32

个字节

-2 147 483 6482 147 483 647

Long

System.Int64

个字节

-9 223 372 036 854 775 8089 223 372 036 854 775 807

Short

System.Int16

个字节

-32 76832 767

Single

System.Single

个字节

负值取值范围为-3.402 823 5E+38-1.401 298E-45

正值取值范围为1.401 298E-453.402 823 5E+38

表2-5中的第一列是VB.NET中提供的数据类型,第二列是.NET Framework中的公共语言运行时(Common Language Runtime,CLR)所提供的数据类型。在使用VB.NET编程时,可以同时使用这两种数据类型。

 技术内幕

事实上,每种语言都有自己的数据类型,比如VB.NET中的Integer,在C#中对应的数据类型为int。但不管是VB.NET还是C#,或是另外一种编程语言,只要这个语言编出来的程序是运行于.NET之上的,则都要被转换为NET Framework中的公共语言运行时所提供的数据类型。正是由于有这个特点,才使得.NET Framework支持混合语言开发,即VB.NET代码可以调用C#代码,这在以前的软件运行平台上是很难实现的。

在这些数据类型中,用得最多的是整数(Integer和Long)和小数(Double和Single),小的整数用Integer,大的整数用Long。小的小数用Single,值很大的小数用Double。

另一种非常重要的类型是String,它代表由字符组成的字符串。在前面的程序中已看到过它的身影,由于其特殊的重要性,将在2.2.6节中专门介绍其使用方法。

知道了VB.NET拥有的数据类型,那么这些数据类型有什么用?它们是怎样用在编程中的?

可以用一句话概括为:

数据类型在编程中主要用于定义常量和变量。

解释一下:

在程序中会用到许许多多的数据,每种数据一定属于某种数据类型。打个比方,一周有7天,那么,这个“7”就是一个数据,它的类型是Integer(整型)。在软件中,数据本身用常量和变量来表达,由此可以得出一个结论:

变量和常量一定属于一种数据类型。

下面介绍变量和常量的知识。

1.常量

所谓常量,就是在程序运行过程中始终不变的量。比如,公历一年有12个月,这个概念可以用以下的定义来表达:

Public Const MonthsPerYear As Integer = 12

一旦这样定义了之后,就可以在代码中使用MonthsPerYear这个标识符来代替12这个数字。把上述信息用标签控件(假设其为Label1)表达,可以这样写代码:

Label1.Text= "公历一年有" + cstr(MonthsPerYear) + "个月"

这句完全等同于:

Label1.Text= "公历一年有" + cstr(12) + "个月"

要定义一个常量,应按照以下格式:

Public Const 常量名 As 数据类型=值

在上面的格式中,数据类型可以是.NET支持的所有数据类型。下面是一个字符串类型常量的例子:

Public Const ErrorMsg As String= "出错了!"

 试一试

请定义一个常量来表示“中国有56个民族”。

 注意

在程序中用常量代替数字和字符串,是个好习惯。

为什么要这么做?想想就明白了:

假如程序中在许多地方都要用到一个特殊意义的数字,比如999,那么,在需要使用这个特殊意义的场合使用一个单词(比如MaxNumber)来表达无疑是更易于阅读与理解的。

比对一下以下两段代码:

(1)

Private const MaxNumber As Integer=999

'……

If i>MaxNumber then

Msgbox "数据太大了!"

End if

(2)

If i>999 then

Msgbox "数据太大了!"

End if

哪个更易懂?

再想想下面这个情况:某个程序中多次需要将一个数与最大的三位数999进行比较,因此在某个程序代码中“999”这个数字出现了10次。现在,程序版本需要升级,可以支持四位数字的比较,因而原有代码中的“999”就必须修改为“9999”,必须修改10处代码。在修改过程中还可能发现代码中出现的某个“999”只是一个普通的数字999,并不具备“最大的三位数”这个含义,不应被改为“9999”。这样一来,修改代码工作就很麻烦了,必须仔细地阅读其前后的代码,才能确定是否应该将此“999”改为“9999”。

而如果在需要表达“最大的数字”这个含义的地方不用具体的数字,而用一个常量MaxNumber来表达(即按第1种方式编程),则只需要修改常量的定义就行了,上面出现的问题全部都可以避免。

字符串常量是用双引号定界的,那么,如果需要在字符串中包含双引号,应怎样处理?

例如,要把以下这句话:

He said "Good Morning!"

定义为字符串常量,必须这样做:

Public const WhatHeSaid as String=" He said "" Good Morning! "" "

注意需要把双引号连续写两次。

2.变量

在使用计算机语言编程时,经常需要定义变量。所谓变量,可以这样理解,它就是一个存放信息的容器,容器的大小和类型是固定的,而容器中存放的具体东西可以不同。

例如,以下代码定义了一个整型变量:

Dim i as Integer

这表明i是一个可用于存放整数的容器,可以把任何一个整数放到容器中。如:

i=100

上述代码意味着这个“整数瓶子”中装的数是100,再来一句:

i=200

这意味着这个“整数瓶子”现在装的数是200,原来的“100”被丢弃。如果接着写一句代码:

i=i+1

则意味着先取出“整数瓶子”中装的数,把这个整数加一,之后再重新装到瓶子中,最终瓶子内保存的数是201。

下面从软件技术的角度来解释变量。

每个变量一定属于一种数据类型,表2-5中列出了VB.NET常用的数字类型,并给出了每种数据类型所占的存储空间。定义一个变量,其实就是在内存中分配一个刚好可以容纳这个数据类型的空间(比如Integer占4个字节);向变量赋值,其实就相当于把这个值写到这个变量所代表的内存空间中。请看以下代码:

Dim i as Integer计算机执行完上述语句之后,其内存分配如图2-45所示。对于VB.NET,如果定义一个整数变量时没有指定其初始值,则自动设置为“0”。

 技术内幕

在VB.NET中,定义数字类型的变量(不管是整数还是小数)都会自动初始化为0值;定义一个字符串变量,则自动初始化为空串;定义一个对象类型的变量,则自动初始化为Nothing。

对于C/C++这样的语言,定义一个变量只会根据这个变量的类型而在内存中分配一个空间,但并不会自动给定一个初值。因此,在用C/C++编程时,在定义一个变量的同时给定一个初始值是一个良好的习惯。

执行以下语句给变量i赋值:

i=100

上述代码执行之后,内存中的情况如图2-46所示。

变量之间可以相互赋值,请看以下代码:

Dim i As Integer = 100

Dim j As Integer

j = i

上述代码执行完毕以后,其内存布局如图2-47所示。

从图2-47中可以看到,变量i赋值给变量j之后,两个变量的值都是100了,而且这两个变量所代表是两个独立的内存区域。与数学中的等号含义不同,此处的“=”在软件中称为赋值运算符,语句“j = i”表示把变量i所代表的内存区域中的值传给j所代表的区域,但并不意味着在任何时候i一定等于j

在编程过程中,心中有一个明晰的内存布局图是非常重要的,对于排除程序错误大有帮助。在后面介绍类时,还会对变量的内存布局进行更多的介绍。

3.强类型语言

所谓强类型的计算机语言,是指使用这种语言时,所有的变量都必须先声明后使用。早期的VB(6.0版本以前)可以不先定义变量而直接使用它,以下代码在VB中是正确的:

i=100

i=i+1

除非显式地将以下命令放到代码文件开头(注意不要放在Sub过程或函数体内):

Option Explicit On

VB才会报告“变量未定义”错误。

在新的VB.NET中,这个开关是默认打开的,这就意味着在VB.NET中,所有变量都必须先声明后使用。

 技术内幕

除一些特殊情况外,不同类型的变量不能相互直接赋值。如果确实需要这样做,就必须对变量进行类型转换。VB.NET可以自动完成许多转换工作,比如可以把一个整数直接赋值给字符串类型的变量。

VB.NET提供了一个新的命令用于设置类型转换是否自动进行:

Option Strict { On | Off } '强制类型转换开关

此开关默认是关闭的,当此开关打开时,VB.NET将不会自动进行类型转换。

打开此开关后,以下代码将无法通过编译:

Dim i As Integer=100

Dim s As String

s = i

必须修改为:

Dim i As Integer=100

Dim s As String

s =Ctr(i)

cstr()函数将整数转为字符串。若将Option Strict一句删除,原来的代码就可以通过编译了。

4.枚举(Enum)

文本框: 
图2-48 消息框在程序中使用消息框是非常常见的,图2-48是一个消息框的示例。

VB.NET中,可使用MsgBox()函数显示消息框。

 技术探索

请使用MSDN查找MsgBox()函数的使用方法。

现在的问题是:怎样知道用户单击的是哪个按钮?

这是由MsgBox()函数的返回值来决定的。

MsgBox()函数返回如表2-6所示的值。

表2-6 MsgBox()函数返回值

  

 

MsgBoxResult.OK

1

按下确定按钮

MsgBoxResult.Cancel

2

按下取消按钮

MsgBoxResult.Abort

3

按下中止按钮

MsgBoxResult.Retry

4

按下重试按钮

MsgBoxResult.Ignore

5

按下忽略按钮

MsgBoxResult.Yes

6

按下按钮

MsgBoxResult.No

7

按下按钮

可以看到,MsgBox()函数其实是返回一系列数字的,但这些数字都有一个名称来表达(比如MsgBoxResult.OK)。这种类似于数字常量的数值称为枚举。MsgBoxResult是此枚举类型的名字。枚举类型的使用方法如下:

Dim ret As MsgBoxResult

ret = MsgBox("是否退出?", MsgBoxStyle.YesNo Or MsgBoxStyle.Information,_

"询问操作")

If ret = MsgBoxResult.Yes Then

'单击了Yes按钮

Else

'单击了No按钮

End If

除了VB.NET预先定义的枚举类型之外,还可以定义自己的枚举类型,例如,可以为一组与一周中的7天相对应的整型常数声明一个枚举,然后在代码中使用这七天的名称而不是它们的整数值。

Public Enum WeekDays

Sunday = 0

Monday = 1

Tuesday = 2

Wednesday = 3

Thursday = 4

Friday = 5

Saturday = 6

End Enum

现在,WeekDays.Sunday就表示0这个数字了。

一般用Enum语句在类的外部或模块的声明部分定义枚举类型,这样在整个类或项目中就都可以使用这个数据类型了。

 注意

在VB.NET中,几乎所有的代码(除了一些特殊类型的语句,如前面介绍的Option语句)都必须放在类(Class)或模块(Module)中,没有独立存在的函数和Sub过程。

5.数组(Array)

数组用于存放一批具有相同类型的元素,这点与一般的变量是不一样的。一个定义为Integer类型的变量只能存入一个整数,而一个整数数组则可以存入一组整数。

以下代码定义了一个数组:

Dim myIntegers() As Integer = {99, 32, 100, 16}

这个数组按如图2-49所示的形式存储数字。

文本框: 
图2-49 数组元素的存储方式数组中的元素按顺序存放,每个元素都有一个位置编号(称为数组元素下标),从0开始。

访问数组元素是通过下标进行的,格式为:

数组名(元素下标值)

例如,以下代码取出数组中的第3个元素值并用消息框显示出来:

MsgBox(myIntegers(2))

依次访问全部数组元素有两种方法:

(1)可以使用循环语句来访问全部的数组元素:

Dim myIntegers() As Integer = {99, 32, 100, 16}

Dim i As Integer

For i = 0 To myIntegers.GetLength(0) - 1

MsgBox(myIntegers(i))

Next

注意数组其实是一个对象,GetLength()是它的一个方法,用于获取数组中的元素个数。

上述代码把所有的数组元素遍历了一遍,并用消息框显示出来。

(2)可以使用For Each语句来访问全部的数组元素:

Dim myIntegers() As Integer = {99, 32, 100, 16}

Dim i As Integer

For Each i In myIntegers

MsgBox(myIntegers(i))

Next

使用For Each语句除了可以访问数组,还可以访问集合(将在2.2.7节中介绍)。

6.结构(Structure)

可以合并不同类型的变量来创建“结构”。声明结构后,它成为一种复合的数据类型,进而可以声明该类型的变量。

除变量外,结构还可以有属性、方法和事件。

想让单个变量包含有几个相关信息时结构很有用。例如,可能想将一个学生的姓名、年龄和学号信息放在一起。可以对这些信息分别定义几个变量,再将这些变量组合为一个结构以简化信息的处理。

(1)定义结构

使用Structure语句作为结构声明的开始,并使用End Structure语句作为结构声明的结束。在这两条语句之间必须至少声明一个成员。成员可以属于任何数据类型。

如果要将学生的姓名、年龄和学号信息一起保存在单个变量中,可以为这些信息声明一个结构,如下所示:

Public Structure Student

Public SerialNo As String '学号

Public Name As String '姓名

Public Age As Integer '年龄

End Structure

(2)使用结构变量

创建了结构后,即可声明该类型的变量:

Dim Stu1,Stu2 As Employee

通过以下方式来使用结构变量:

变量名.结构成员名

示例代码如下:

Dim Stu1,Stu2 As Student

Stu1.SerialNo = "2004110107"

Stu1.Name = "张三"

Stu1.Age =20

Stu1和Stu2都是Employee类型的变量,因而可以相互赋值:

Stu2 = Stu1

其结果是Stu2中的所有成员值都与Stu1相同。

 注意

虽然赋值后Stu1和Stu2的所有成员值是一样的,但这两个变量完全是相互独立的,可以给Stu1的Name属性赋予一个新值(如“李四”),这并不会引起Stu2的Name属性值改变,它仍是“张三”。

2.2.2 语句与控制结构

在前面的学习中,读者已经看到并亲自动手编写过一些程序代码了。从这些代码中可以发现一个个单词构成一个语句,多条语句被包在“Sub…End Sub”内部(也可能是“Function…End Function”内部),多个Sub和Function又被包在“Class…End Class”内部。

在计算机编程语言(以VB.NET为例)中,把单词称为标志符(Identifier),由标志符构成语句(Statement),由语句构成函数(Function)或过程(Sub),由函数或过程构成(Class),由类构成程序(Programme)。

在面向对象的程序中,类是最重要的组成元素,但要编写一个类,还得从写好一条条的语句开始。

1.语句的基本概念

一条计算机语句完成一个特定的功能,在VB.NET中,一行就是一条语句,语句由标识符构成,标识符不区分大小写。

如果某行语句太长了,可以加入换行符(即一个空格加一个下划线),例如:

Private Sub Button1_Click(ByVal sender As System.Object, _

ByVal e As System.EventArgs) _

Handles Button1.Click

上述示例中,虽分为三行,却只是一条语句。

 注意

不能在一个字符串内部换行,以下代码是不合法的:

Dim str As String = "我们一定能学好VB.NET,开发出好软件, _

让比尔 盖茨也破产!"

 技术内幕

在C#、Java等语言中,语句是以分号“;”作为结尾的,可以在标识符和字符串之外的任意地方按下回车键把语句分成多行。

在一个语句前面加上单引号“'”,会让此语句成为注释语句。

注释语句主要用于说明程序代码的作用。

 注意

在计算机语言中使用的各种标点符号都是英文半角的,在代码中不要使用中文的标点符号。

 提示

(1)在VS .NET代码编辑器中选中多行代码后,单击“文本编辑器”工具栏上的  按钮,会自动把选中的代码改为注释语句,单击  按钮,则可以取消注释。

如果您在VS .NET工具栏上没看到这两个按钮,请在工具栏上单击鼠标右键,从弹出菜单中选择“文本编辑器”,调出此工具栏。

(2)在程序中加入适当的注释是非常重要的,它使您在过了一段时间之后仍能很快地回忆起当初写这段代码时的情况,从而为扩充软件功能、查找并修改错误带来很多方便。

一般而言,需要在类文件的开头说明类的用途,在函数或过程的开头说明其功能,一些重要的语句也需要加上注释。

再次强调,不要因为偷懒而不写注释,在第一次写代码时花点时间写注释,不仅有利于理清思路,而且,这些时间将在后面的程序维护工作中成倍地节省回来!

2.控制结构

现在的许多软件系统规模都十分庞大,有的包含多达数百万行代码。但不管其结构多么复杂,都是由三种最基本的控制结构组成的。

这三种控制结构就是顺序选择循环

顺序结构如图2-50所示。

图2-50中,A和B都代表程序代码块,程序运行时,计算机先执行完A代码块再执行B代码块。

实际程序中的大部分代码都是这样顺序执行的。

再看看如图2-51所示的选择结构。

图2-51在执行A和B代码块之前,先进行一个条件判断,如果条件满足,则执行A代码块,否则,执行B代码块。

所有的计算机高级语言都有实现选择结构的“选择语句”。

图2-52所示为循环结构。

  

图2-50 顺序结构 图2-51 选择结构 图2-52 循环结构

所谓循环结构,就是不断地反复执行一段代码,由图2-52可见,只要满足一个特定的条件,代码块A将被不断地执行。

所有的计算机高级语言也都提供了自己的“循环语句”。

计算机科学家已经证明:

再复杂的程序结构,最终都可以分解为顺序、选择与循环三种最基本的程序结构。

不同的计算机语言实现这三种结构的方式略有不同,其中有的计算机语言还做了适当扩充。下面介绍VB.NET中实现选择结构和循环结构的语句。

(1)选择语句

选择语句用于实现选择结构,其基本语法是:

If 条件 Then

'执行语句块A

Else

'执行语句块B

End If

举个例子,把下面一句话变成计算机的语句结构:

如果天气晴朗,我们就去爬香山,否则,我们待在家里看电视。

用程序设计语言表达,其结果就是:

If 天气晴朗 Then

我们去爬香山

Else

我们待在家里看电视

End If

其中,“天气晴朗”就是判断条件。凡是用做条件判断的语句一定有两个值:真或者假。在VB.NET中,真用True表示,假用False表示。

所以,上面这个例子又可以写成:

If 天气晴朗=True Then

我们去爬香山

Else

我们待在家里看电视

End If

“天气晴朗=True”,这种等式称为“逻辑表达式”。条件语句就是依据逻辑表达式的值是True还是False来决定执行哪个语句块的,“True”表示条件成立,“False”表示条件不成立。如果逻辑表达式中要表达的意思是True,则可以省略“=True”这一部分。比如上面的例子中,可以写为“天气晴朗=True”,也可以简写为“天气晴朗”。

对于单个条件可以用上面的方式表达,但如果需要有两个条件进行判断,那又如何处理呢?比如张三对李四说:

如果天气晴朗并且您有空,我们就去爬香山,否则,我们待在家里看电视。

这时,计算机接受的语句为:

If 天气晴朗 And 李四有空 Then

张三李四一块去爬香山

Else

张三李四待在家里看电视

End If

And是一种逻辑运算符。只有当And连接的两个逻辑表达式都为真时,整个逻辑表达式的结果才为真。可以把And对应为中文的“并且”这个词,以便于理解它的含义。

还有另外两个逻辑运算符:Not和Or。

Not逻辑运算符实例:

If Not(天气下雨) Then

我们去爬香山

Else

我们待在家里看电视

End If

Or逻辑运算符实例:

If 我发工资了 or 有人请客 Then

我就去大饭店吃顿好的

Else

我回宿舍泡康师傅方便面吃

End If

看了上面的实例,读者一定知道“Not”相当于中文中的“不”,而“Or”则相当于中文中的“或”。

现在看一个真正的计算机实例:

Dim testGrade As Integer

testGrade = CInt(InputBox("请输入《.NET软件开发技术》这门课的考核成绩分数", _

"输入成绩"))

If testGrade > 60 Then

MsgBox("不错,您过关了!", , "老爸评语")

Else

MsgBox("这么好玩的课程您居然没过?少玩点游戏,多编程!", , "教师忠告")

End If

 试一试

将上述代码放到一个Button对象的Click事件中运行,看看结果。

上述代码中,InputBox()函数用于显示一个输入框供用户输入信息。由于InputBox()返回的是一个字符串,而变量testGrade是整型的,所以,需要使用VB.NET内部函数CInt()将字符串转为整数。

另外,注意一下MsgBox()中出现了连续两个逗号,为什么?

在MSDN中查找MsgBox()函数的定义:

Public Function MsgBox( _

ByVal Prompt As Object, _

Optional ByVal Buttons As MsgBoxStyle = MsgBoxStyle.OKOnly, _

Optional ByVal Title As Object = Nothing _

) As MsgBoxResult

可以看到从第二个参数开始都是以一个单词“Optional”开头的,在前面有“Optional”修饰的函数参数称为可选参数,在使用MsgBox()函数时,可以不设置其值,但其位置必须留出来,所以,才会出现了代码中有连续两个逗号的情况。

可选参数都有一个默认值,从MsgBox() 函数的声明中可以看到,Buttons参数的默认值是“MsgBoxStyle.OKOnly”。

在选择语句中,Else语句可有可无。此外,If语句可以嵌套,如下所示:

If 条件 Then

If 条件 Then

'执行语句块A

Else

'执行语句块B

End If

Else

If 条件 Then

'执行语句块C

Else

'执行语句块D

End If

End If

嵌套层数没有限制,但一般而言,嵌套超过3层的条件语句非常难以阅读。

使用选择语句时,特别要注意Else子句与哪个If配套。

If 条件 Then

If 条件 Then

'执行语句块A

Else

'执行语句块B

End If

End If

上面的Else与第二层的If配套。

有一个基本原则:

Else子句总与离它最近的If配套。

当一个变量可能有多个固定的取值时,使用If语句有不方便的地方,如要将1到7这七个数字与星期的中英文名称对应起来,这时,可用Select Case语句。

假设WeekDayNums变量存入了1到7这七个值之一,则以下代码可以获取对应数字的星期中英文名称:

Dim WeekDays As Integer

Dim ChineseName, EnglishName As String

WeekDays = 5

Select Case WeekDays

Case 1

ChineseName = "星期一"

EnglishName = "Monday"

Case 2

ChineseName = "星期二"

EnglishName = "Tuesday"

Case 3

ChineseName = "星期三"

EnglishName = "Wednesday"

Case 4

ChineseName = "星期四"

EnglishName = "Thursday"

Case 5

ChineseName = "星期五"

EnglishName = "Friday"

Case 6

ChineseName = "星期六"

EnglishName = "Saturday"

Case 7

ChineseName = "星期日"

EnglishName = "Sunday"

Case Else

ChineseName = "无效星期数字"

EnglishName = "Invalid Week Number"

End Select

这段代码一目了然,没什么好说的,特别注意最后一个Case子句:

Case Else

这句表明,如果WeekDays变量的值不是1到7之间的数字,则由此分支中的语句进行处理。

(2)循环语句

VB.NET中的循环语句有许多种,我们只介绍两种:For循环与While循环。

For循环语句常用于指定次数的循环,其格式为:

For 循环变量=初值 to 终值 step 增量值

'语句

Next 循环变量

其含义是:多次重复执行语句,循环变量从初值开始,每次增加一个“增量值”,直到循环变量等于或大于终值时才停止。

实例:

求以下算术式的和。

1+2+3+...+100=?

分析:

显然必须把加法语句循环一百次,示例代码如下。

Dim i As Integer

For i = 1 To 100 Step 1

'完成相加的语句

Next

需要有一个变量用于存放加法的结果,因此,在定义变量i的语句后面,再定义一个新变量Result,并初始化为0:

Dim Result As Integer = 0

当循环开始时,i =1,让

Result=Result+i

这样,Result中就存入了第一个数字“1”,第二次循环时,i=2:

Result=Result+i

又加入第二个数字“2”,现在Result中包含的结果是:1+2,即3,由此不断循环100次,最终:

Result=1+2+3+...+100

完整的代码如下:

Dim i As Integer

Dim Result As Integer = 0

For i = 1 To 100 Step 1

'完成相加的语句

Result = Result + i

Next

MsgBox("1+2+...+100=" & Result)

上述代码中字符串与Result变量之间连接是采用“&”运算符,此运算符完成的功能是把两个字符串连接在一起构成一个新字符串。如果要连接的不是字串变量(本例中Result是整数),它会自动把整数转为字符串,再与“1+2+...+100=”连接起来。

如果用“+”取代上面的“&”运算符,则VB.NET在运行时会报错(如图2-53所示)。

图2-53 类型转换错误

这是因为“+”运算符在发现它左右有数字时,总是尝试把另一个操作数也变成数字,而“1+2+3+…+100=”显然没法转为数字,所以出错了。

解决方法是采用VB.NET的内部函数CStr()将Result变量转为字符串,如下所示:

MsgBox("1+2+...+100=" + CStr(Result))

 试一试

如果要求出100以内的奇数之和,应怎样修改代码?

另一种常见的循环语句是While循环语句,其格式为:

While 条件表达式

[语句]

End While

只要给定条件表达式值为True就执行循环体内的语句。

While循环与For循环是等价的,从1加到100的例子,可以使用While循环改写如下:

Dim i As Integer = 1

Dim Result As Integer = 0

While i <= 100

Result = Result + i

i = i + 1

End While

MsgBox("1+2+...+100=" + CStr(Result))

While循环有一个特性,就是它可以很方便地执行不定次数的循环,以下实例不断地要求用户输入一个整数,计算机自动地计算从1加到此整数的和,直到用户输入“-1”为止。

Dim InputNum As Integer = 0

Dim Result As Integer = 0

Dim i As Integer

While InputNum <> -1

InputNum = CInt(InputBox("请输入一个大于0的整数,要结束输入请输入-1"))

i = 1

Result = 0

While i <= InputNum

Result += i

i += 1

End While

MsgBox("从1加到" & InputNum & "的结果是:" & Result)

End While

上面代码中有一个以前没见过的语句:

Result += i

“+=”称为复合运算符,上面的语句相当于:

Result=Result+i

 技术探索

除了“+=”之外,还有“*=”、“/=”这样的复合运算符,请自行查阅MSDN了解这些运算符,并学会使用。

还有一种很有意思的循环,称为“死循环”。它的样子是这样的:

While True

[语句]

End While

除非在“[语句]”部分中有退出的代码,否则它将永远运行下去。

要退出一个循环,可以使用Exit语句。

While循环的上一个例子可以用“死循环”的方式改写如下:

Dim InputNum As Integer = 0

Dim Result As Integer = 0

Dim i As Integer

While True

InputNum = CInt(InputBox("请输入一个大于0的整数,要结束输入请输入-1"))

If InputNum = -1 Then

Exit While

End If

i = 1

Result = 0

While i <= InputNum

Result += i

i += 1

End While

MsgBox("从1加到" & InputNum & "的结果是:" & Result)

End While

注意,上面的示例代码中使用一个条件语句检测是否用户输入了“-1”,一旦确认,就调用Exit While语句退出循环。

 提示

任何一个“死循环”都一定要提供一个退出的出口,不然,您的程序就会失去响应。

在上面这个例子中,循环的次数是未知的,何时结束完全由InputNum这个变量来决定,因此,可把这个变量称为“哨兵”,这种类型的循环为“哨兵监控”的循环,而前面介绍的已知次数的循环可称为“计数器”类型的循环。

 技术探索

VB.NET还提供了Do…Loop While和Do Until…Loop循环语句,请自行查阅MSDN文档,了解相关的知识。

各种类型的循环语句都是等价的,可以用一种类型的循环语句替换另一种类型的循环语句而不会影响到程序的功能。

至此,介绍完了VB.NET中最基本的编程元素。下面将介绍如何把多个语句组合成函数和过程,并进一步装配出一个类。

2.2.3 对象与类

在上一节中学习了基本的VB.NET编程元素——语句,多个语句组合在一起,共同完成一个功能,称为Sub过程或函数。

多个Sub过程或函数加上多个变量声明,以及其他一些组成元素,就构成了类。

在本小节中将介绍Sub过程、函数和类的基础知识。

1.Sub过程

在VB.NET中,完成某个功能的多条语句可以放在一起,包含在Sub语句和End Sub语句中,这样的一个代码集合称为“Sub过程”。每次调用Sub过程时都执行Sub过程中的语句,从Sub语句后的第一个可执行语句开始,到遇到的第一个End Sub、Exit Sub或Return语句结束。

声明Sub过程的语法如下所示:

Sub 过程名[(参数列表)]

'VB.NET语句

End Sub

来看一个Sub过程实例,这个Sub过程接收一个图像文件的完整路径名,并把这个图像显示在窗体上。

新建一个Windows应用程序项目(取名SubAndFunction,源代码见配套光盘),切换到Form1的代码视图,在End Class语句之前的空白处输入以下代码:

'将指定的图片画到窗体上

Private Sub DrawPicToForm(ByVal fileName As String)

Dim img As Bitmap

'装入图片

img = New Bitmap(fileName)

'获取窗体的绘图对象

Dim g As Graphics

g = Me.CreateGraphics

'将图片绘制到窗体上去,左上角坐标为(0,0)

g.DrawImage(img, New Point(0, 0))

'释放绘图对象

g.Dispose()

End Sub

往窗体上拖入一个Button,设其名字为“btnDrawPic”,再从工具箱中选择OpenFileDialog组件拖到窗体上,设置其Filter属性为“所有图像文件|*.jpg;*.gif;*.bmp;*.ico;*.wmf|所有文件|*.*”。

 提示

OpenFileDialog组件用于显示标准的Windows打开文件对话框(参见图2-54),而其Filter属性用于指定显示哪种类型的文件,其格式为:

提示信息一|*.文件扩展名一|提示信息二|*.文件扩展名二|……

如果想在一项中同时显示多种扩展名的文件,可以串联多项“*.文件扩展名”,中间以分号间隔(必须是英文的分号)。

给按钮btnDrawPic的Click事件编码如下:

Private Sub btnDrawPic_Click(ByVal sender As System.Object,_

ByVal e As System.EventArgs) Handles btnDrawPic.Click

Dim fileName As String

If Me.OpenFileDialog1.ShowDialog = DialogResult.OK Then

'如果用户选择了一个文件,并单击了“打开”按钮

fileName = Me.OpenFileDialog1.FileName

'显示图片

Me.DrawPicToForm(fileName)

End If

End Sub

程序运行时,单击btnDrawPic按钮,在对话框窗口中选取一个图片,如图2-54所示。

单击【打开】按钮之后,可以看到图片出现在窗体上,如图2-55所示。

 

图2-54 使用OpenFileDialog组件选取图片 图2-55 调用Sub过程绘制图片

在这个例子中,所有绘制图片的功能都由Sub DrawPicToForm()过程完成。在按钮的单击事件处理代码btnDrawPic_Click中,从OpenFileDialog控件的FileName属性中取出用户选择文件的完整路径名,并将其作为参数传给Sub DrawPicToForm()过程。

Sub过程主要用于封装完成某一特定功能的代码。在本例中,需要向窗体上绘图时,只需要简单地按“DrawPicToForm(图像文件路径名)”的格式调用就行了,不用重复地在不同地方书写同样的代码。

细心的读者可能会发现,所有的控件事件处理代码也都是Sub过程,只不过后面多了个Handles子句。Handles关键字将在第3章介绍。

2.函数

在现实中,如果某公司有一项工作任务需要完成,那么总经理通常会把这个任务交给部门经理去完成,并往往要求部门经理在完成这个任务后提交一份总结报告。而部门经理也往往不是亲自动手去做事,而是合理地把工作分配给不同的员工去完成,并在完成后向他汇报。这就是人类社会的协同工作方式。

在软件中可以看到类似的组织方式。

函数就相当于每个人需要完成的工作,在程序中体现为完成某种功能的代码集合。公司员工在完成工作后需要向上级汇报结果,相应地,函数在执行完毕后也会有一个返回值。函数之间可以相互调用,这类似于部门经理分派员工工作任务。

函数与Sub过程的区别就在于:

函数有返回值,Sub过程没有。

 提示

在VB.NET中区分函数与Sub过程两个不同的概念,而在C++、C#和Java中,没有过程这个概念,Sub过程全部以返回void值的函数代替。

来看一个实例:编写一个函数计算两数之和。

Private Function AddTwoNumber(ByVal x As Single, _

ByVal y As Single) As Single

Dim result As Single

result = x + y

Return result

End Function

仔细看看上面的代码,可以发现函数与Sub过程有以下不同点:

(1)函数的关键字是Function(过程是Sub)。

(2)函数声明最后有一个As子句,表明函数返回值的类型是单精度实数。

(3)代码的最后有一个Return语句,把结果返回给了函数调用者。

使用此函数的实例如下:

MsgBox(AddTwoNumber(20, 30) + 100)

其结果是150。

可以看到,AddTwoNumber()函数可以用在表达式中,可以把它当成一个数字来使用。

如果某个函数返回一个对象,比如String类型的对象,则函数本身就可以当成一个字符串使用,下面是一个实例:

'将一个字符串反序

Private Function ReverseStr(ByVal str As String) As String

Return StrReverse(str)

End Function

使用实例:

MsgBox(ReverseStr("abcd").ToUpper)

代码中“ReverseStr("abcd")”可以看成是一个String类型的对象,因而可以调用其ToUpper方法把字符串中的小写字母全部变为大写。这行代码的最终结果为:“DCBA”。

现在可以给出函数的语法格式:

Private/Public Function 函数名(函数参数列表) As 返回值类型

'各种代码

Return 返回值

End Function

3.类的定义

在面向对象程序中,类是最基本的组成单位。换句话说,是由类构成了整个程序,就如同砖块构建起了整栋大楼。

那么,类到底是什么?

可以把类看成是现实事物在计算机中的一种反映,举个典型的例子——电视机。

一台电视机具有以下的特性:

(1)生产厂家

(2)是否支持数字信号(即是否可当电脑显示器用)

(3)价格

……

一台电视机必须拥有以下的操作功能:

(1)打开、关闭

(2)选台

(3)显示当前频道数

……

如何在计算机中表达电视机这一概念?

可以使用变量来表示电视机所具有的特性,如表2-7所示。

表2-7 电视机特性

 

  

 

 

生产厂家

Producer

String

是否支持数字信号

IsDigital

Boolean

True表示数字电视

价格

Price

Single

用小数表示金额

当前频道

CurChannel

Integer

当前的频道

使用函数或Sub过程来表达电视机所拥有的功能,如表2-8所示。

表2-8 电视机功能

操作功能

函数或Sub过程名

 

打开

Sub Open()

关闭

Sub Close()

选台

Sub ChooseChannel()

Channel(整型)

把上述的变量和函数(或Sub过程)用两个语句“Class”和“End Class”包围起来,再起一个名字,就形成了一个类,如下所示:

'电视机类

Public Class TV

'电视机的特性

Public Producer As String '生产厂家

Public IsDigital As Boolean '是否支持数字信号

Public Price As Single '价格

Public curChannel As Integer '当前频道

'电视机的操作功能

'打开

Public Sub Open()

End Sub

'关闭

Public Sub Close()

End Sub

'选台

Public Sub ChooseChannel(ByVal Channel As Integer)

End Sub

End Class

这个框架搭好以后,就可以往里面写代码了。

现在看看如何在电脑中“模拟出”人使用电视机的过程:

Dim myTV As TV

myTV = New TV '我买了一台电视机

myTV.Producer = "海信" '是海信生产的电视机

myTV.Price = 2005 '2005元买的

myTV.IsDigital = True '是数字电视

'打开电视

myTV.Open()

'选择第10频道

myTV.ChooseChannel(10)

'我在看电视……

myTV.Close() '关闭电视机

可以看到在代码中并不能直接使用一个类,必须先定义一个TV类的对象变量myTV,然后再用New关键字创建一个类的实例,之后才能使用这个类变量。

一定要区分类与类变量。类变量是一种变量,它的类型是某个具体的类。一个类可以创建许多个实例,类变量可以指向它们。一个比较容易理解的例子是可以把类看成是一个大印章,它可以在一张纸上盖出无数个大印。这些盖出的印就是印章类的实例(又可称为印章对象)。

在上面的例子中,印章本身是前面写的TV类及其全部源代码,而myTV就是一个对象变量,它可以代表由印章(TV类)盖出的一个印(TV类的一个实例),印章盖印的过程是用New来表达的。

这些概念可能比较抽象难懂,看一个实例可能更易于理解。

打开VS .NET,创建一个“Windows应用程序”项目,起名为UseClass(参见本书配套光盘)。

从“项目”菜单中选择“添加类”,给类起名字“TV”,如图2-56所示。

单击【打开】按钮之后,VS .NET将会在项目中创建好一个空的类框架代码,在此代码框架中敲入上面介绍的代码,如图2-57所示。

图2-56 使用VS .NET创建类

接着,按如图2-58所示设计窗体。

 

图2-57 给类书写代码 图2-58 “电视机”示例主窗体

注意,在打开电视机之前,是不能选台的,所以,程序运行时,【选台】和【关闭】按钮是灰色的。

切换到代码视图,在所有Sub过程的外部书写以下代码:

Private myTV As TV

这就在窗体类中定义了一个成员变量,此变量可以被窗体中的所有函数和Sub过程所访问。

给【打开】按钮的事件编码:

Private Sub btnOpen_Click(ByVal sender As System.Object, _

ByVal e As System.EventArgs) Handles btnOpen.Click

myTV = New TV '我买了一台电视机

myTV.Producer = "海信" '是海信生产的电视机

myTV.Price = 2005 '2005元买的

myTV.IsDigital = True '是数字电视

myTV.curChannel = 10 '设置10频道

myTV.Open() '打开电视机电源

'显示当前频道数

Me.lblChannel.Text = CStr(myTV.curChannel)

'让按钮可用

Me.btnClose.Enabled = True

Me.btnChooseChannel.Enabled = True

'不允许第二次打开电视机电源

Me.btnOpen.Enabled = False '让自己变灰

End Sub

当程序运行后,单击【打开】按钮,运行结果如图2-59所示。

给【选台】按钮添加代码如下:

myTV.curChannel = CInt(Me.txtChannel.Text)

'显示当前频道数

Me.lblChannel.Text = CStr(myTV.curChannel)

编译运行,在文本框中输入一个数字,单击【选台】按钮,可以看到当前频道显示出相应的数字,如图2-60所示,这是通过直接设置类的curChannel变量值来实现的。

 

图2-59 正在看电视…… 图2-60 换了一个台

从上面例子可以看到,通过使用一个TV类对象在计算机中虚拟出了一台电视机,可以“打开”、“关闭”和“选台”,所有这些功能都是通过函数和 Sub 过程来表达的。而当前频道属于一种信息,在TV类对象中以变量来表达。

这个例子展示了许多重要的东西:

(1)它直观地展示了现实中的事物是如何被抽象成计算机可以处理的一个类的。

(2)现实事物中的功能往往用类的函数和Sub过程来表示,而现实事物的属性可以使用变量来表示。

在一个类中声明的函数和Sub过程,在面向对象理论中往往称为“方法(Method)”,而声明的变量,则称为类的“数据成员(Data Member)”或“字段(Field)”,有时也称为“成员变量(Member Variable)”。另外,在C#、Java、C++这些面向对象的语言中,不区分函数和Sub过程,全部以函数称之。

4.给类设置属性

前面TV类提供了一个curChannel字段,这是一个类型为Integer的整数,但实际上,电视台的频道数是有限的,而且不可能有小于0的频道数。如何在TV类中表现这种附加的信息呢?这就引入了另一个非常重要的概念——对象的属性。

在TV类中,如何表达电视台数是有限的这一条件?

解决方法是定义一个常量:

Const MAX_CHANNEL As Integer = 100

现在即可定义一个curChannel属性,先把以下这条语句删除:

Public curChannel As Integer '当前频道

加入以下代码:

Private _curChannel As Integer = 0

Public Property curChannel() As Integer '当前频道

Get

Return _curChannel

End Get

Set(ByVal Value As Integer)

'电视频道数必须大于0,小于最大的频道数

If Value >= 0 And Value <= MAX_CHANNEL Then

_curChannel = Value

Else

_curChannel = 0

End If

End Set

End Property

上述代码中有一些有趣的东西。

Property是一个关键字,表明curChannel现在不是一个简单的变量,而是一个属性了。一个属性由两个特殊的过程来实现。

(1)Get过程:当读此属性时,会执行此过程。

(2)Set过程:当向此属性赋值时,会执行此过程。注意到这一过程有一个Value参数,在使用属性的代码中对此属性所赋的值被Value参数所捕获。

一个属性并不能真正保存东西,真正能保存信息的是变量,所以,绝大多数属性都使用一个类的私有数据成员变量来保存此信息。比如上例中_curChannel变量就是一个用于保存信息的变量:

Private _curChannel As Integer = 0

_curChannel的默认值在定义时直接用赋值运算符指定为0。

特别要注意这个变量被声明为私有的(Private)。

 技术内幕

如果一个类的数据成员被声明为私有的,则类外部的任何代码都不能访问此数据成员。

例如,以下类声明了一个公有和一个私有的数据成员:

Class ShowPrivateAndPublic

Private i As Integer

Public j As Integer

End Class

可以使用这个类创建一个对象,并访问这个变量的j字段,如下所示:

Dim obj As ShowPrivateAndPublic

obj = New ShowPrivateAndPublic

obj.j = 100

但是不能写出以下代码:

Obj.i=100

因为i是私有的,所以不能在类的外部访问变量i。但在类ShowPrivateAndPublic中如果有Sub过程和函数,则其中的代码可以访问变量i

再回来看看例子中实现属性的代码,当需要获取属性的值时,仅须简单地返回_curChannel变量值就行了,而当设置属性值时,先判断一下值是否有效,有效就设置_curChannel变量为期望的值,否则,一律置为0。

可以在属性的Get和Set过程中设置程序断点,从而看到属性的代码执行流程。

 提示

在调试(DEBUG)状态下,给某句代码设置断点后,VS .NET在执行到此句时会停在此句,可以在此时检查程序的运行状况(比如变量值)。程序断点是排除程序错误的有效工具。

用鼠标在某行代码的最左端单击,可以定义一个断点。

参见图2-61,在代码中定义的两个断点以褐色的小圆点表示,同时相应的行被高亮选中:

图2-61 设置断点

按F5键运行程序,单击【打开】按钮,会看到如图2-62所示的情形。

图2-62 程序中断

由于【打开】按钮的事件处理代码中有一句:

myTV.curChannel = 10  '设置10频道

所以,应该会执行TV类curChannel属性的Set过程。由于在Set过程中事先设置了一个断点,所以,程序在Set过程中指定的代码处中断,VS .NET自动显示对应的代码,并用一个小小的黄色图标指明将要执行的下一条语句。

把鼠标移到Value这一过程参数上,可以看到VS .NET会弹出一个小提示窗口,显示Value=10,这就说明在【打开】按钮的事件处理代码中给属性赋的值10被属性Set过程的Value参数接收了。

 提示

当程序处于中断提示状态时,可以在VS .NET的命令窗口中执行任何有效的VB.NET命令。最常用的是显示特定对象某个变量或属性的值,可以用一个问号来显示当前变量的值,如图2-63所示。

图2-63 使用命令窗口跟踪变量值

命令窗口的功能是非常强大的,它让我们可以随时执行一个有效的VB.NET命令,灵活应用命令窗口可以为调试程序带来很大的方便。

现在请按F5键,读者会发现程序暂停到了Get过程,这是由另一句代码引发的:

'显示当前频道数

Me.lblChannel.Text = CStr(myTV.curChannel)

这就说明了读取属性值时,会自动调用Get过程。

 提示

使用属性最容易出错的地方就是没有定义一个对应的私有变量,并给Get 和 Set过程书写代码。在这种情况下,程序会照常运行,不会引发错误,但属性值在任何情况下都不会变(保持VB.NET的默认值,比如Integer类型的属性值为0 ,String类型的属性值为空串)。

现在有一个问题,属性和类的公有成员变量都可以用来存储信息,那么什么时候用公有变量,什么时候用属性?

以一个表示公司员工的类为例说明:

一般而言,当需要对类的某个信息做出某种限定(比如规定姓名不能为空,工资金额不可能是负数),或者是某个信息依赖于类的其他信息计算得出(比如年龄,它可以根据人的出生年月与当前年现场计算出来)时,使用属性比较合适。

一些信息如果没有什么限制,就可以使用公有变量。

 试一试

请仔细分析以下现实事物的特性,设计类来表达这些特性。注意正确决定哪些特性用属性,哪些特性用类的公有成员变量。

汽车、书、U盘

 技术探索

在本节中介绍的是既可以读也可以写的属性。事实上,VB.NET还提供只读与只写属性,分别以ReadOnly和WriteOnly两个关键字表达。请在MSDN中通过查找这两个关键字掌握只读与只写属性的使用方法。

5.名字空间与类

在前面读者已经接触过名字空间的概念了,本节在原有基础上加深读者对这一概念的认识。

.NET Framework使用名字空间(Name Space)来管理所有的类。如果把类比喻成书的话,则名字空间就是用于放书的地方,书保存放在书架上,类放在名字空间里。

当我们去图书馆查找一本书时,需要指定这本书的编号,编号往往规定了书放在哪个书库的哪个书架上。通过逐渐缩小的范围:图书馆→书库→书架,最终可以在某个书架中找到这本书。

可以采用类似的方法来管理类,通过逐渐缩小的范围:最大的名字空间→子名字空间→孙名字空间→……,最终找到一个类。

在代码中可使用Namespace关键字定义自己的名字空间:

'父名字空间

Namespace NS1

Public Class Class1

End Class

'子名字空间

Namespace NS2

Public Class Class2

End Class

End Namespace

End Namespace

上述代码定义了一个父名字空间NS1,其中放置了一个类Class1,又定义了一个子名字空间NS2,其中放置了另一个类Class2。

在同一项目中,可以这样来使用这两个类:

Dim o1 As New NS1.Class1

Dim o2 As New NS1.NS2.Class2

如果上述代码放在一个类库项目(项目名称为ExampleClassLibrary)中,VS .NET默认将项目名作为顶级的名字空间。所以,如果要在其他项目中使用Class1和Class2,就必须这样来使用两个类:

Dim o1 As New ExampleClassLibrary.NS1.Class1

Dim o2 As New ExampleClassLibrary.NS1.NS2.Class2

也可以使用Imports语句来简化代码的书写:

Imports ExampleClassLibrary.NS1

Imports ExampleClassLibrary.NS1.NS2

'……

Dim o1 As New Class1

Dim o2 As New Class2

 注意

类库项目编译之后将生成一个DLL文件,另一个项目(比如Windows应用程序)可以使用这个DLL文件中声明为Public的类。使用方法是从“项目”菜单中选择“添加引用”命令,如图2-64所示。

在图2-64中单击【浏览】按钮选中DLL文件,单击【确定】按钮,即将类库引用加入到了项目中,现在即可在项目中使用Imports语句来引入类库中定义的名字空间(参见上面的代码)。

图2-64 添加项目引用

事实上,所有的.NET Framework中的类都必须通过名字空间来引用。在“解决方案资源管理器”的项目节点中就有一个“引用”子节点,其中列出了项目中所有引用的类库,参见图2-65。

在图2-65中双击类库节点ExampleClassLibrary,即可打开“对象浏览器”查看类库中的所有名字空间。

 

图2-65 项目引用的类库节点 图2-66 通过对象浏览器查看名字空间

6.使用图形来表示类

在UseClass这个实例中,读者是通过阅读源代码来了解类的定义的。但在一个真实的软件中,可能有几十个甚至上百个类,每个类可能提供数十种方法,这些类之间又存在着复杂的关系(比如将在第3章中介绍的继承和多态),要阅读这么多的代码才能了解类的功能,显然是非常麻烦且低效的。

想想电子工程师是怎么设计电路板的?他们使用抽象的符号来表达复杂的电路回路。我们在高中都学过电阻、电容等标准的图形符号,并都会画物理电路图来描述电路,参见图2-67。

软件工程师也有自己的图,国际上的OMG(Object Management Group,对象管理组织)制定了一整套用于描述面向对象软件系统的标准图形及相关标准,即统一建模语言(Unified Modeling Language,UML),在后面的章节中,将经常使用这种语言来描述程序。因此,先提前在本节介绍一些UML的基础知识,本书第11章有更为详尽的介绍。

在UML中,类是用类图来表示的,前面设计的TV类用UML表示,如图2-68所示。

把这个图与TV类的源代码相互对照,可以很容易明白这个图的含义。

展示一个类的所有数据成员和方法的图在UML中称为类图。一个类图是一个有三栏的方框,第一个方框写上类的名字,第二个方框列出类中所有字段,第三个方框列出所有的方法。

图2-68中的“+”表示Public,“-”表示Private,还有一个“#”号图中没有,它表示Protected。有关Protected的含义将在第3章介绍。

2.2.4 变量的类型

以下代码定义了一个结构和一个类用于表示一个点的坐标(xy):

Public Structure ValPoint

Dim x As Integer

Dim y As Integer

End Structure

Public Class RefPoint

Public x As Integer

Public y As Integer

End Class

一旦定义好了这两个数据类型,就可以使用它们来定义变量。使用结构ValPoint的示例代码如下:

'测试值变量

Dim p1 As ValPoint

p1.x = 100

p1.y = 100

MsgBox(CStr(p1.x))

运行上述代码(可以参见本章示例代码RefTypeAndValueType),可以看到程序正确地显示p1.x的值为100。

现在使用类变量,示例代码如下:

'测试引用变量

Dim p1 As RefPoint

p1.x = 100

p1.y = 100

MsgBox(CStr(p1.x))

可以看到除了第一句以外,所有代码都是与使用结构变量ValPoint一样的。

运行,看看出现了什么情况?

系统报告出现了一个错误:“未将对象引用设置到对象的实例”(图2-69)。

图2-69 NullReferenceException异常

为什么会出现这种情况?

原来,变量是分类型的,定义为ValPoint类型的变量称为值类型(Value Type)的变量,定义为RefPoint类型的变量称为引用类型(Reference Type)的变量。如果一个变量是值类型的,那么一旦定义完后就可以直接使用了,而引用类型的变量则必须先创建(New)一个对象,之后才能使用,否则就会出现以上的错误。

1.引用类型与值类型变量

引用类型与值类型到底差别何在?这涉及计算机内存(Memory)的分配方式。

先来看值变量:

Dim p1 As ValPoint

此代码执行之后,内存布局如图2-70所示。

从图2-70可以看到,如果定义了一个ValPoint类型的变量,那么计算机会在内存中分配一块空白区域,大小能容纳值类型所定义的所有成员(ValPoint中有两个类型为Integer的整型变量成员)。而变量名p1就代表了这块内存区域,可以通过p1.x直接访问内存中x的值。

但引用类型的变量就不是这样了,当计算机执行完以下定义变量语句时:

Dim p1 As RefPoint

内存布局如图2-71所示。

虚线框表示此内存并未真正分配。从图2-71中可以看到,定义一个引用变量并不会导致计算机为此变量分配足够容纳此变量所包含所有成员数据的内存空间,而只是分配了一个固定大小的空间(4个字节)并将其内容设为0。这个被初始化为0的空间即p1本身,其内存代表真实的数据在内存中的位置(又称为内存地址)。

正因为可容纳xy的内存还未分配,p1.x是不存在的,所以,MsgBox(CStr(p1.x))会引发NullReferenceException异常。

修正错误很简单,只需要增加一句就行了,代码如下所示:

'测试引用变量

Dim p1 As RefPoint

p1 = New RefPoint

p1.x = 100

p1.y = 100

MsgBox(CStr(p1.x))

第2句代码使用New关键字创建一个RefPoint对象,其实质是让计算机给变量分配可容纳真实数据的内存空间,执行之后,内存布局如图2-72所示。

现在,p1.x就可以正确地访问了。

 提示

在.NET编程中,一定要注意.NET Framework所提供的数据类型是引用类型还是值类型。有一个简单的判断方法:凡是类类型的变量一定是引用类型的。使用它之前一定要先创建一个对象出来。其他的类型都是值类型的,可以直接使用。

引用变量的内存布局是十分重要的,如果理解不清,在今后的编程工作中会引发许多隐含的错误。

请看以下问题:

Dim p1, p2 As RefPoint

p1 = New RefPoint

p1.x = 100

p1.y = 100

p2 = p1

问:

p2.x会不会引发错误?如果没错,它的值是多少?

答案:

p2.x不会引发错误,它的值等于100。

变量赋值之后,其内存布局如图2-73所示。

可以看到,p2=p1,其实就是把p1的内容原样复制一份给p2,所以,现在p1和p2都指向同一个对象。p2.xp1.x都是同一个内存区域。因而,可以通过任意一个变量(p1或p2)来修改x的值,另一个变量会同时感知到此值的变化。

 试一试

理解了本章所介绍的内存变量分配模型,请做以下练习:

1 Dim p1, p2 As RefPoint

2 p1 = New RefPoint

3 p1.x = 100

4 p1.y = 100

5 p2 = p1

6 p2 = New RefPoint

7 MsgBox(CStr(p2.x))

问:

(1)p2.x现在的值是多少?

(2)如果把第6句移到第3句前面,则p2.x的值又是多少?

(3)请绘出这两种情况下的内存分配模型。

上机编程验证您的推测。

现在再来看一下数组。

Dim ps(100) As ValPoint

Dim i As Integer

For i = 0 To 99

ps(i).x = i

ps(i).y = i

Next

上述代码定义了一个有100个ValPoint元素的数组,并且通过循环初始化了数组元素。

再看以下代码:

Dim ps(100) As RefPoint

Dim i As Integer

For i = 0 To 99

ps(i).x = i

ps(i).y = i

Next

能实现同样的目的吗?

文本框: 
图2-74 展示变量的作用区域请编程验证您的推测。

2.类内部信息的相互传送

先看如图2-74所示的示例(参见示例VariantScope)。

当单击按钮时,窗体上的标签会显示出单击的次数。

按钮对象名为“btnClickMe”,显示次数是一个标签对象lblClickCount,这个功能如何实现?

经过思索,读者可能会写出以下代码:

1 Private Sub btnClickMe_Click(ByVal sender As System.Object, _

ByVal e As System.EventArgs) Handles btnClickMe.Click

2 Dim ClickCount As Integer

3 '单击次数加1

4 ClickCount += 1

5 '用标签(Label)控件显示单击次数

6 Me.lblClickCount.Text = ClickCount

7 End Sub

然而很遗憾,不管单击按钮多少次,标签总显示为“1”。

 试一试

(1)现在把第2句代码移到Sub外面,再运行试一试。

(2)注释掉移动到Sub外部的代码。把原来的第2句代码改为:

Static ClickCount As Integer

再试一试。

试验结果表明程序运行结果正常。

上述两个试验蕴涵着重要的程序设计基础知识。

最初定义的变量是在Sub过程内部,称为“局部动态变量”,其特点是它的值在离开Sub过程后会被丢弃,下次执行时又会重新设置(取其默认值0),所以标签总显示为“1”。

第一次试验中把这个变量定义在Sub过程之外,则此变量现在成了类的一个“成员变量”。在类内部的Sub过程可以访问并修改这个变量,变量的值能够在Sub过程运行结束之后继续保存,再次运行Sub过程时,ClickCount变量的值就是上次运行的结果。

第二次试验中变量还是定义在Sub过程之内,所不同之处在于使用“Static”关键字定义,在本例中,其效果与类的“成员变量”是一样的。这种变量称为“局部静态变量”。

读者可能会奇怪:类成员变量与局部静态变量都可以记录下上次Sub过程执行的结果,那么它们在使用上有何差别?

实情是这样的:

类的成员变量可以被类的任何方法(包括函数和Sub过程)所访问,而局部静态变量只能被本身(即它所定义于其中的函数或Sub过程)所访问。

在实际开发中,经常需要解决信息的流动问题,这种流动可以是在类与类之间的,也可以是类内部的。类与类之间一般通过声明为Public的属性和方法相互交换信息,而类内部的信息交换则主要通过类成员变量实现。如果对这些问题理解不清,在实际开发时,可能会引发许多难以排除的错误,因此,读者一定要清晰地理解这些概念。

3.类的共享数据成员

类的成员(包括变量、函数和Sub过程)也可以是静态的。其方法就是在定义成员时加上Shared关键字(注意不是Static)。

Public Class AllStaticMemberClass

Public Shared Counter As Integer

Public Shared Sub StaticSub()

'代码略

End Sub

Public Shared Function StaticFunction() As Integer

'代码略

End Function

End Class

要访问类的静态成员,有两种方法。

(1)使用“类名.成员名”的方法,如:

AllStaticMemberClass.Counter = 0 '访问共享变量

MsgBox(AllStaticMemberClass.StaticFunction()) '访问共享函数

AllStaticMemberClass.StaticSub() '访问共享Sub过程

(2)使用“对象.成员名”的方法:

Dim obj As New AllStaticMemberClass

obj.Counter = 0 '访问共享变量

MsgBox(obj.StaticFunction) '访问共享函数

obj.StaticSub() '访问共享Sub过程

需要注意的是:凡声明为Shared的类成员,不管这个类生成了多少个对象,它们都共享同一个类成员。

 提示

当有些功能需要被许多个类所使用时,将这些功能封装成Shared的函数和Sub过程是比较好的选择,可以避免创建对象所带来的性能损失。

2.2.5 函数与方法重载

在前面所介绍的程序中消息框用得很多,读者可能已经习惯了使用MsgBox来向用户提示一些信息。

如果使用C#,您会发现C#中没有MsgBox函数,而代之以MessageBox函数。事实上,MsgBox是VB.NET对于MessageBox函数的封装。换句话说,MsgBox内部调用MessageBox来显示消息框。

 技术内幕

MsgBox的历史非常悠久,早在VB 3.0(20世纪90年代中期推出)时就提供这一函数了。除了MsgBox之外,早期的VB还提供了一系列的函数,比如CStr、CInt、Left、Kill等。为了兼容早期的版本,在VB.NET中仍然提供这些函数,只不过把这些函数全部放到了Microsoft.Visual- Basic名字空间中。

此名字空间是被VB.NET默认引入的,所以可以直接使用其中的函数。其他的编程语言,比如C#,则必须显式引入此名字空间(C#使用using语句)后才能使用其中的函数。

可以把一个整数直接传给MsgBox函数,比如以下代码是正确并且等价的:

MsgBox(100)

MsgBox("100")

细心的读者一定会觉得奇怪,数字与字符串怎么可以作为参数传给同一个函数?这在以前的VB中是不可以的。这种情况由“重载(Overloads)”这一面向对象的语言特性实现。

请打开MSDN,查询MessageBox类的Show()方法,您会发现多达12个Show()方法的定义,下面列出3个:

Overloads Public Shared Function Show(String) As DialogResult

Overloads Public Shared Function Show(IWin32Window, String) As DialogResult

Overloads Public Shared Function Show(String, String) As DialogResult

……

正是这12个名字相同的函数,让软件工程师可以方便地把不同数目、不同类型的数据传送给MessageBox.Show()函数,以显示出相应的消息框。

上述12个函数即为“函数重载”特性的表现形式。

那么,构成重载情况的函数拥有哪些特性?请记住以下规则:

(1)函数名相同。

(2)参数类型不同,或参数个数不同。

符合以上特性的函数构成重载关系。

当在代码中使用重载函数时,根据传入的参数由计算机自动选择最合适的一个函数运行。

在VB.NET中,除了函数,Sub过程也是可以重载的。以下是一个实例(参见配套光盘上的实例FunctionOverLoads)。

定义四个重载的IknowWhatYouGiveMe():

'重载的函数

Public Sub IKnowWhatYouGiveMe()

MsgBox("您什么也没有给我!!!")

End Sub

Public Sub IKnowWhatYouGiveMe(ByVal arg As Integer)

MsgBox("您传入了一个整数:" & arg)

End Sub

Public Sub IKnowWhatYouGiveMe(ByVal arg As String)

MsgBox("您传入了一个字符串:" & arg)

End Sub

Public Sub IKnowWhatYouGiveMe(ByVal arg As Control)

MsgBox("您传入了一个控件:" & arg.GetType.Name)

End Sub

在按钮的单击事件中可以这样调用重载的Sub过程:

1 Private Sub btnUseOverloadsFunc_Click(ByVal sender As System.Object, _

ByVal e As System.EventArgs) Handles btnUseOverloadsFunc.Click

2 '调用重载的Sub过程

3 IKnowWhatYouGiveMe()

4 IKnowWhatYouGiveMe(100)

5 IKnowWhatYouGiveMe("Hello.VB.NET")

6 IKnowWhatYouGiveMe(New TextBox)

7 End Sub

特别注意第4个重载的IknowWhatYouGiveMe() 过程其参数是一个Control对象。类Control是所有可视化窗体控件的祖先,可以把任何一个由它继承而来的类对象传给它。第6句动态创建了一个文本框对象作为参数,可以通过Control类对象的GetType方法获取对象的类型(是Type类的一个对象),再调用Type类对象的Name方法获取其名称(这一句涉及许多面向对象的知识,初学者可以不必深究,在掌握了更多的.NET知识之后就会明白其中的奥秘)。

 试一试

修改第6句参数,传入:

(1)一个标签(Label)对象。

(2)Me关键字。

看看运行结果。

 试一试

编写一个重载的Add()方法,用于把两数相加,接收整数和小数两种不同的参数。

2.2.6 字符串使用

字符串可能是编程中用得最多的一种类型了。

以下代码定义了一个字符串变量:

Dim str As String

str = "Hello"

上述两行代码可以合并为一行:

Dim str As String = "Hello"

字符串变量存入的数据以双引号分隔,如果这些数据中本身就包含了双引号,则要双写此双引号。以下代码将一句话——他说:“您好!”,存入到字符串变量Str中。

Dim str As String = "他说:""您好!"""

可以使用“+”或“&”运算符连接两个字符串。

str = "Hello." + "World!"

str = "Hello." & "World!"

上述两句完全可以相互替换。

有时,可能还需要在字符串中换行,以下代码将一首诗存入到字符串中并在控制台中输出:

Module Module1

Sub Main()

Dim str As String

str = " 画" & ControlChars.NewLine

str += "远看山有色," & ControlChars.NewLine

str += "近听水无声。" & ControlChars.NewLine

str += "春去花还在," & ControlChars.NewLine

文本框: 
图2-75 在字符串中加入回车str += "人来鸟不惊。"

Console.WriteLine(str)

'屏幕暂停,回车退出

Console.ReadLine()

End Sub

End Module

运行结果如图2-75所示。

代码中的ControlChars.NewLine是VB.NET提供的特殊字符常量,表示新行。常用的字符常数如表2-9所示,所有这些常数均位于Microsoft.VisualBasic名字空间的Constants模块中,VB.NET可以直接使用。

2-9 VB.NET的字符常数

 

 

 

 

CrLf

vbCrLf

Chr(13) + Chr(10)

回车/换行组合符

Cr

vbCr

Chr(13)

回车符

Lf

vbLf

Chr(10)

换行符

NewLine

vbNewLine

Chr(13) + Chr(10)

新行符

NullChar

vbNullChar

Chr(0)

值为0的字符

Tab

vbTab

Chr(9)

制表符

Back

vbBack

Chr(8)

退格字符

 提示

上述示例是一种新的项目类型,称为控制台程序,有关它的知识请参看本节的扩充阅读资料。

String变量与其他类型的变量连接时,需要进行类型转换,可以使用CStr() 函数完成此功能。以下代码将整数转为字符串类型:

Dim str As String

str = "1+100=" + CStr(1 + 100)

字符串由字符组成,可以通过字符串变量的Chars()属性访问单个字符,以下代码逆序输出字符串中的每个字符:

Dim str As String

str = "尺千三下直流飞"

Dim c As Char

Dim i As Integer

For i = str.Length - 1 To 0 Step -1

c = str.Chars(i)

Console.WriteLine(c)

Next

字串中的字符按Unicode进行编码,每个字符占两个字节。

可以使用String类的SubString方法获取子字符串,参看以下代码:

Dim str As String

str = "子在川上曰:逝者如斯夫!"

Dim str1, str2 As String

str1 = str.Substring(0, 6)

str2 = str.Substring(6)

第一句SubString从头开始,取6个字符,注意字符位置是从0开始的。运行完后,str1=“子在川上曰:”。

第二句从第7个字符开始,由于省略了第二个参数,就直接截取到字符串结束。运行完后,str2=“逝者如斯夫!”。

String类对象具有一个很独特的特性,即字符串内容的恒定性。

一个String对象一旦创建,其内容就是不可改变的。上述代码中的SubString方法并不是把Str本身给截短了,而是将Str中的一段取出来,生成一个新的字符串传给Str1(和Str2)。

 技术探索

.NET提供了另外一个类StringBuilder。与String类不一样,这个类定义的变量其本身内容是可以改变的。请从MSDN中搜索资料,学会使用这个类。

 扩充阅读

控制台应用程序简介

控制台程序类似于传统的DOS程序,它运行时会打开一个黑底白字的窗口,以字符的方式显示程序运行结果。控制台程序一般没有可视化的主窗体。

在VS .NET中有一个项目模板可用于创建控制台程序,如图2-76所示。

图2-76 创建控制台程序

与Windows Form程序不一样,控制台程序并不需要创建一个“主窗体”,它由一个特殊的Sub Main()过程开始执行。此Sub Main()过程通常放在一个Module(模块)中(注:Sub Main()过程也可以放在任何一个类中,但一个程序只能有一个Sub Main()过程)。

放在Module中的函数和Sub过程可以自动被项目中的其他类直接使用(不需要也不能创建一个Module类型的对象)。

因此,可以把Module看成是一个其内部成员全都为共享(Shared)公有成员的特殊类,也许将其比做项目的公有函数库更易于理解。

类Console代表控制台。它提供了ReadLine、WriteLine等方法以输入和输出数据。在控制台应用程序中,此类的输出通过DOS窗口进行。而在Windows Form应用程序中,如果使用这些方法输出信息,则这些信息将显示在VS .NET的输出窗口中。如果Windows Form不是通过VS .NET启动的,则用户将看不见这些输出的信息。

2.2.7 递归

在中学语文课本中,有一个著名的“愚公移山”的寓言:愚公认为太行与王屋两座大山挡住了他的路,于是发动全家人上山挖土。愚公对智叟说,虽然他年纪大了,肯定看不到大山给搬走了,但他有儿子。他的名言可谓流传千古:“子又生孙,孙又生子,子子孙孙无穷匮也,而山不加增,何苦而不平?”。事情的结果是愚公的精神感动了天帝,他派了两个大力神把两座大山给搬走了。

愚公的故事有着多方面的寓意,有趣的是,愚公这种做事的方式在计算机中也可以看到,这就是一种重要的编程方法——函数或Sub过程自己调用自己,这种方法称为递归。

文本框: 
图2-77 “递归”的鱼理解在软件技术中的递归概念对于初学者而言是不容易的,因为它往往与人们常规的思维方式不同。有趣的是,在艺术家那里,递归往往以一种更易于理解的方式呈现,请看如图 2-77所示的画。

图2-77是著名的荷兰设计师M.C.Escher(1898—1972)的作品,在画中他绘制了一群鱼,有趣的是,鱼身上的鱼鳞又是一条鱼……这种“用鱼来绘制鱼”的表现方法,就是递归思想在艺术中的表现。

回到软件技术,以下代码就是非常明显的递归代码:

Sub DoNotRunMe()

'……

DoNotRunMe()

End Sub

可以看到,这种代码在执行过程中又会调用自己,于是又执行一次自己,在新的一次调用中再次重复这个过程……这是一个无穷无尽的过程,理论上讲可以运行到宇宙末日。这个过程像不像愚公说的“子又生孙,孙又生子”?所以说,计算机可算是当代最大的愚公了。

然而,一段永远也不会结束的代码是没有实际意义的,就是愚公移山,也有它终止的一日,那就是山给移走了。因此,如果在编程中使用递归,就一定要给它设定一个结束条件。

愚公移山之所以能够成功,关键在于“山不加增”,而“子子孙孙无穷匮”,如果山也同步加增,则永远也无法移走大山。因此,要使递归最终得以结束,一定要在每次调用自己时让某个变量(在故事中就是山的高度)随之变化,当其达到一个最终界限时(故事中的山的高度为零),则结束调用过程(愚公移山成功)。

现在我们来看如图2-78所示的有趣实例(示例项目Recursion,配套光盘中有)。

您一定对这首著名的“儿歌”很熟悉:

从前有座山,山里有座庙。

庙里有两个和尚,在讲故事。

讲什么故事呢?……

从前有座山,山里有座庙。

庙里有两个和尚,在讲故事。

讲什么故事呢?……

这就是一个很典型的递归。示例Recursion用递归模拟了这个小时候常唱的儿歌。

这个程序是如何写出来的?下面介绍一下开发过程。

在窗体上放了一个RichTextBox控件用于显示递归的结果,使用一个NumericUpDown控件updnTimes来限制递归调用次数。

实现代码如下:

'定义一个Story变量用于存放故事主体

Private Story As String = ""

'故事主体

Private Sub WriteStory()

Story = "从前有座山,山里有座庙。" & vbCrLf

Story += "庙里有两个和尚,在讲故事。" & vbCrLf

Story += "讲什么故事呢?……" & vbCrLf

End Sub

以下代码实现递归:

'用于实现递归调用的Sub过程

Private Sub DoRecursion(ByVal times As Integer)

'结束条件

If times = 0 Then

Exit Sub

End If

'每次递归调用时要完成的工作

Me.RichTextBox1.AppendText("第 " & CStr(times) & " 次" & vbCrLf)

Me.RichTextBox1.AppendText(Story)

'递归调用,参数减1

DoRecursion(times - 1)

End Sub

在这个递归调用的Sub过程中,参数times就是用于控制递归深度的控制变量,每次调用时它减1。减到0时,递归结束。

在按钮单击事件中运行递归过程:

Private Sub btnExecute_Click(ByVal sender As System.Object, _

ByVal e As System.EventArgs) Handles btnExecute.Click

'清空文本

Me.RichTextBox1.Text = ""

'老和尚开始没完没了地讲故事,直到小和尚不耐烦为止

DoRecursion(Me.updnTimes.Value)

End Sub

运行的结果如图2-78所示。

图2-78 递归的实例

 试一试

将Sub DoRecursion()中干活的两句:

Me.RichTextBox1.AppendText("第 " & CStr(times) & " 次" & vbCrLf)

Me.RichTextBox1.AppendText(Story)

移到

DoRecursion(times - 1)

之后,会发生什么情况?您能想得清楚吗?

递归最能体现出“计算机的思维”与“人的常规思维”的不同。理解它也是初学编程者最头痛的问题之一。然而,一旦您真正理解了它,就会发现这种思维方法是很巧妙的。而在实际开发中,递归是非常常用的编程技巧,在本书的后面,有不少地方使用了递归的编程方法。希望读者好好体会。

2.2.8 .NET中的集合

在高中代数中学过集合的概念,集合是若干有着相同特性的元素的整体。

在程序设计中,集合有着非常多的应用。本节介绍.NET中最常使用的两个集合数据类型:ArrayList和HashTable。

1.ArrayList

ArrayList与数组非常相像,放在ArrayList中的元素也是通过下标访问的,但与数组不同的是,ArrayList中的元素可以随时增加和删除,所以,ArrayList又可以被看成是一个“动态数组”。

以下是一个使用实例(见配套光盘中的UseCollection)。

'使用ArrayList

Private Sub btnUseArrayList_Click(ByVal sender As System.Object, _

ByVal e As System.EventArgs) Handles btnUseArrayList.Click

'创建ArrayList对象

Dim arr As New ArrayList

'向ArrayList中增加元素

arr.Add("张三")

arr.Add("李四")

arr.Add("王五")

'显示ArrayList内容

ShowArrayList(arr)

'删除第二项(注意下标从0开始)

arr.RemoveAt(1)

'再次显示ArrayList内容

ShowArrayList(arr)

End Sub

'显示ArrayList内容

Private Sub ShowArrayList(ByVal arr As ArrayList)

Dim i As Integer

For i = 0 To arr.Count - 1

MsgBox("下标:" & i & " 值:" & arr(i))

Next

End Sub

可以看到集合元素通过下标访问,上述代码中删除了ArrayList中间的一个元素,后面的元素自动补上。除了调用Add方法增加元素,还可以使用Insert和AddRange等方法。同样ArrayList也提供了多种删除元素的手段,请读者自行参阅MSDN。

需要指出的是,ArrayList中可以存放多种数据,除了例子中的字符串数据,也可以是数字,甚至是一个窗体变量。但一般而言,最好在ArrayList中存入同一种数据类型的信息,使得代码便于阅读和排错。

2.HashTable

存放在HashTable中的数据是一种“键∣值(Key-Value)”形式的数据。换句话说,HashTable中的每个数据都有一个惟一的标志。与ArrayList不同,要访问HashTable中的元素,必须给定一个键(Key)。

以下是一个实例(见配套光盘中的UseCollection):

'使用HashTable

Private Sub btnUseHashTable_Click(ByVal sender As System.Object, _

ByVal e As System.EventArgs) Handles btnUseHashTable.Click

'创建HashTable对象

Dim hst As New Hashtable

'向HashTable中增加元素

hst.Add("Monday", "星期一")

hst.Add("Tuesday", "星期二")

hst.Add("Wednesday", "星期三")

hst.Add("Thursday", "星期四")

hst.Add("Friday", "星期五")

hst.Add("Saturday", "星期六")

hst.Add("Sunday", "星期日")

'访问HashTable中的元素

MsgBox("英文:Monday的中文意思是:" & hst("Monday"))

'遍历HashTable的所有元素

ShowHashTable(hst)

MsgBox("现在删除了Wednesday")

'删除星期三

hst.Remove("Wednesday")

MsgBox("开始查看删除了Wednesday之后HashTable中的内容")

'再次遍历HashTable的所有元素

ShowHashTable(hst)

End Sub

'显示HashTable内容

Private Sub ShowHashTable(ByVal hst As Hashtable)

Dim myDE As DictionaryEntry

For Each myDE In hst

MsgBox("key:" & myDE.Key & " Value:" & myDE.Value & _

" Hash值:" & myDE.Key.GetHashCode())

Next myDE

End Sub

运行这个示例,读者会发现HashTable中的元素与ArrayList中的元素访问方式是不一样的,它必须通过一个给定的Key来访问。而要遍历一个HashTable,其方法也与ArrayList不一样,必须使用For Each语句。特别要注意在For Each语句中,用到的变量myDE是DictionaryEntry类型的,这是因为由于Hashtable 的每个元素都是一个键/值对,因此元素类型既不是键的类型,也不是值的类型,而是 DictionaryEntry 类型。

另外,ShowHashTable()过程中用到了一个GetHashCode()方法,这个方法由类Object提供,用于返回一个对象的哈希(Hash)值。所谓哈希值,就是一个很长的数字,这个数字由特定的数学算法(称为哈希函数)生成。不同的对象拥有不同的哈希值,HashTable就是利用这个值实现元素的快速定位的。

 提示

因为哈希函数针对不同的数据能生成不同的关键字,因此在海量数据检索中有着重要的应用。假设需要在上亿条记录中查找特定的记录,如果只能从头开始一条一条地顺序查找,那一定是非常低效的。然而,如果在保存信息时就是按照这些信息的哈希值进行安排的,则不管信息有多少,都可以在一个相对恒定的时间内得到结果。

计算机专业课《数据结构》中介绍了哈希函数的设计方法以及哈希查找原理。

由于HashTable是通过Key来访问元素的,这就要求在往HashTable中增加元素时,必须先判断一下是否已存在此Key。如果硬要往HashTable中增加相同的Key(比如上例中向HashTable中加入两个Key为“Sunday”的元素),.NET Framework将抛出一个异常表明发生了错误,如图2-79所示。

图2-79 不允许向HashTable中加入相同Key的元素

解决方法是在向HashTable中添加新元素时,先判断一下Key是否已存在。下面这个方法可以安全地向HashTable添加元素:

'安全地向HashTable中增加元素的Sub过程

Private Sub AddElementToHashTable(ByVal key As Object, _

ByVal value As Object,_

ByVal hst As Hashtable)

'判断参数是否有效

If (key Is Nothing) OrElse (hst Is Nothing) Then

Exit Sub

End If

'是否已存在此Key?

If hst.ContainsKey(key) Then

Exit Sub

End If

'将元素加入到HashTable中

hst.Add(key, value)

End Sub

阅读AddElementToHashTable()过程时要注意以下几点:

(1)第一段代码先检测参数是否有效。这段代码看似多余,但却能保证后面的代码运行正常,在开发大规模的软件时是非常必要的,这种编码方式称为“防卫性编码”。其中的OrElse是VB.NET提供的关键字,可以用Or代替,其差别在于使用Or时,其两边的表达式必须都被计算出来后才可以得出整个逻辑表达式的值,而使用OrElse时,只要有一个表达式计算后确认其结果为True,就不再计算后面的表达式,这就意味着程序使用OrElse会比Or运行得快一点。

 提示

与Or类似,And也有对应的一个“AndAlso”关键字,只要它所连接的表达式中发现有一个结果为False,就不会再计算后面的表达式值。请读者自行查阅MSDN了解详情。

(2)判断某个Key是否已存在是通过HashTable对象的ContainsKey()方法实现的。

(3)Exit Sub用于提前退出Sub过程,后面的代码不会再执行。

将原来代码中调用Add()方法向HashTable中添加元素改为使用AddElementToHashTable(),就可以放心地向HashTable中增加元素而不会出错了。

3.何时使用ArrayList和HashTable

本节中介绍了两种典型的集合:ArrayList和HashTable,那么在具体编程时如何选用呢?

一般而言,如果需要按下标访问元素,则使用ArrayList,ArrayList可以起到与数组一样的作用,而其使用要比数组方便得多。如果经常需要查找特定的元素,则HashTable要好得多。

除了这两个常用的集合,.NET Framework还提供了其他的集合类,如Stack,Queue,SortedList等,这些类都位于System.Collections名字空间中。还有一些特殊的集合类,如ListDictionary,StringCollection等,放在System.Collections.Specialized名字空间中。

 技术探索

请通过查询System.Collections和System.Collections.Specialized名字空间了解.NET Framework提供了哪些集合类,以及这些集合类的特点。

2.2.9 VB.NET开发实践:多窗体编程

在前面介绍的程序中,大都只有一个窗体。但在实际的程序中,往往会有多个窗体,并且这些窗体之间还有着复杂的关系。掌握多窗体编程的技巧对于开发真正的软件系统是非常重要的。

1.在一个窗体中访问另一个窗体的控件

新建一个Windows应用程序VisitOtherForm(示例源代码参见配套光盘),删除其中的Form1,新建两个Form:主窗体frmMain和辅助窗体frmOther。

在主窗体上放置两个按钮,btnShowOtherForm和btnGetUerInputText,分别用于显示辅助窗体和获取辅助窗体中用户的输入,还有一个标签lblUserInput,用来显示辅助窗体中输入的信息,如图2-80所示。

辅助窗体中有一个文本框txtUserInput,可用于输入信息,如图2-81所示。

 

图2-80 设计主窗体 图2-81 设计辅助窗体

这个实例运行的结果如图2-82所示。

图2-82 实例运行效果

分析一下主窗体与辅助窗体之间的信息传送是如何实现的。

在主窗体中定义了一个窗体级的变量:

'定义辅助窗体的变量

Private frm As frmOther = Nothing

在btnShowOtherForm按钮单击事件中编写代码显示辅助窗体:

Private Sub btnShowOtherForm_Click(ByVal sender As System.Object, _

ByVal e As System.EventArgs) Handles btnShowOtherForm.Click

If frm Is Nothing Then '辅助窗体还没有创建

frm = New frmOther

End If

'显示辅助窗体

frm.Show()

End Sub

注意一下If条件语句,在前面定义frm变量时它被初始化为Nothing,因此可以通过这点来判断是否已创建了一个窗体对象。如果没有创建就直接显示此窗体,会引发一个NullException- Exception异常错误,这种异常在前面已经见过了。

那为何不直接创建窗体然后显示它,非得要先判断一下呢?这是因为用户可能多次单击这个按钮,如果每次都新建一个窗体,则屏幕上会有多个辅助窗体,只有最后出现的那个辅助窗体的用户输入才能被主窗体获取。

 试一试

注释掉If和End If语句,保留当中的New语句,编译运行看看结果。

在btnGetUerInputText按钮单击事件处理程序中书写代码,获取用户在辅助窗体中输入的信息:

Private Sub btnGetUerInputText_Click(ByVal sender As System.Object, _

ByVal e As System.EventArgs) Handles btnGetUerInputText.Click

'如果辅助窗体还没创建,则退出此Sub过程

If frm Is Nothing Then

Exit Sub

End If

'获取辅助窗体的控件内容

Me.lblUserInput.Text = frm.txtUserInput.Text

End Sub

可以看到,其中的关键部分只有一句:

Me.lblUserInput.Text = frm.txtUserInput.Text

可以通过frm变量来直接访问另一窗体上控件的内容。

 注意

上述代码在同一个项目内部是可以正常工作的,但如果两个窗体分属于不同的Assembly(即程序集),则必须把辅助窗体中文本框txtUserInput的Modifier属性由Friendly改为Public。

2.使用自定义属性在窗体间相互传送信息

上面的例子通过在主窗体中直接访问辅助窗体的控件实现了窗体间信息的传送,但这需要将辅助窗体的内部数据暴露给外界,违反了面向对象的设计原则。较好的方法是给窗体添加自定义的公有属性,外界通过给这些属性赋值实现信息传送。

请看图2-83所示的示例(参见配套光盘中的UsePropertyBetweenForm项目)。

图2-83 UsePropertyBetweenForm实例运行效果

当实例运行时,每单击一次按钮,就会创建一个辅助窗体的对象,在其标签中显示一条信息,并且修改每个窗体的标题。

与上一个例子直接访问辅助窗体的Label控件不同,主窗体向辅助窗体传送的信息是通过给辅助窗体的Title自定义属性赋值来实现的。

另外,如何在程序中跟踪窗体创建的个数?这是通过在辅助窗体中设置一个共享的公有变量实现的:

'窗体计数器

Public Shared counter As Integer = 0

在什么地方增加计数?因为创建一个对象时,会自动调用它的构造过程New(),所以,在Sub New()过程中完成这件事是最方便的了。

Public Sub New()

MyBase.New()

'该调用是 Windows 窗体设计器所必需的

InitializeComponent()

'在 InitializeComponent() 调用之后添加任何初始化

counter += 1 '窗体计数器加1

End Sub

辅助窗体的Title属性是一个自定义的属性:

'定义一个自定义的属性

Public Property Title() As String

Get

Return Me.lblInfo.Text

End Get

Set(ByVal Value As String)

Me.lblInfo.Text = Value

End Set

End Property

只要给这个自定义属性赋值,其值就会自动地用标签显示出来,外界调用者甚至不知道辅助窗体中有一个标签存在。同时,如果需要对所赋的值进行一些预处理(例如将小写字母全部改为大写字母),或者是这一赋值会同时影响多个控件,都可以直接在属性的Set过程中实现。由于有这些方便性,因此,通过自定义属性在窗体间传送信息是推荐的方式。

主窗体中的代码就比较简单了:

Private Sub btnCreateForm_Click(ByVal sender As System.Object, _

ByVal e As System.EventArgs) Handles btnCreateForm.Click

'创建辅助窗体对象

frm = New frmOther '注意在frmOther的构造函数中自动对创建对象的个数计数

'访问窗体计数器,并显示在窗体标题栏上

'counter是Shared变量,通过 类名.变量名 来访问

frm.Text = "第 " & frmOther.counter & " 个窗体"

'通过向自定义属性 Title 赋值实现信息的传送

frm.Title = "主窗体向辅助窗体传送的第 " & frmOther.counter & " 条信息"

'显示辅助窗体

frm.Show()

End Sub

 技术探索

上面所介绍的两个例子都是从主窗体向辅助窗体传送或查询信息,是单向的,能不能实现双向的传送,即辅助窗体也能主动地向主窗体传送信息?

在看下面的提示之前,自己想一想,怎么实现?

 提示

可以在辅助窗体中增加一个主窗体类型的变量,在主窗体创建辅助窗体对象时,把对主窗体对象的引用——“Me”赋值给此变量。

这个实例就留给读者去设计并实现吧。请务必完成这一练习,以真正理解信息是如何在对象之间相互传送的。

3.小结

至此已介绍了许多VB.NET编程的知识,这些知识都是精选出来的,是开发Windows Form应用程序所必须掌握的知识。多窗体编程还有许多技巧,我们将在第3章学习更多VB.NET知识之后再介绍。

在下面的一节中将把学到的知识应用于实际,开发出一个功能强大的文本编辑器,这将是读者所接触到的第一个真正实用的Windows Form应用程序。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多
    喜欢该文的人也喜欢 更多

    ×
    ×

    ¥.00

    微信或支付宝扫码支付:

    开通即同意《个图VIP服务协议》

    全部>>