Henry的VB.NET之旅(一)—失踪的窗体
韩睿
很久没有新作,架不住朋友们的询问与追问,只得拿些旧作来补上近日没空写作的不足。惭愧ing!这一系列与Henry手记系列不同,是一个角色形式的情景故事,最初发表于《计算机世界》,适用于VB.net初学者,老鸟们就不用在我这一系列的文章上浪费时间了。我希望自己有时间以后,能不断继续这一系列的文章,加入大量的工程解决方案。同时,我的Henry手记也会继续写下去,以回报长期以来支持我的所有朋友。
背景:我叫Henry,刚从大学计算机专业毕业,新加入索易公司的.NET开发小组;负责带我的头目是大李,.NET高手,是个幽默又有耐心的家伙;老白是我们部门的头头,经常带给我们古怪又难缠的需求。
第一天上班,白经理就带着我去.NET技术小组报到。“大李,这是新来的同事,你要多帮帮他。”老白指着我对一个眼镜后面透着闪闪目光的年轻人说。
“没问题,他会做得很好的。”大李难道会看相不成?还是人家客气呀?不管了,我只要自己努力就对了。
20分钟后,我已经在分配给我的一台P4电脑,WindowsXP平台下打开了Visual Studio.NET的集成编译环境(IDE)开始欣赏了。一个多漂亮的编译环境呀,可以自选使用时的风格是VB6的还是VC6的。我选择的是RAD性能好的VB6风格。
“你刚开始接触.NET,在系统培训之前先加深点印象,看看VB.NET与VB6有什么不同吧。你创建两个窗体,每个窗体上各有一个按钮,点击时隐藏其所在的那个窗体,切换出另一个窗体来。”大李的声音在耳边响起,打断了我对IDE的欣赏。但他提的要求也太简单了吧,在VB6中只需要两句写在按钮的单击响应中的语句就行了:
两个窗体:Form1与Form2。Form1上的按钮为: Command1,Form2上的按钮为: Command2
Private Sub Command1_Click()
Me.Hide
Form2.Show
End Sub
Private Sub Command2_Click()
Me.Hide
Form1.Show
End Sub
于是,我毫不犹豫地在VS.NET中内开始我的第一个VB.NET的项目了。首先,新建了一个VB.NET的Windows应用程序项目,自动生成了一个Windows Form:Form1,在其上加入一个按钮控件:Button1;然后,添加了一个Windows Form:Form2,在其上加入了一个按钮控件:Button2。
在设计窗口双击Button1后,编译器会切换到代码窗口的Button1的单击响应代码中。
在代码段中故计重施,当我想写Form2.Show时,在Form2之后打个点,出现的智能感知菜单中,却没有Show这一方法。真奇怪,这怎么可能?难道VB.NET改动有这么大。我抬头看了看站在我身旁的大李。
大李立刻面露微笑,看来是正中他的圈套。他手指向屏幕,点了点Form1中的代码的开头处:
Public Class Form1
Inherits System.Windows.Forms.Form
不笨的我立刻就明白了,现在的Form1是一个从System.Windows.Forms.Form继承下来的类了,调用的时候当然需要先实例化了,哈,也不难嘛。马上动手:
Private Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button1.Click
Me.Hide()
Dim frm2 As New Form2()‘实例化Form2类,得到一个对象
frm2.Show()
End Sub
在Form2的Button2的单击事件中也相似地写了一段代码,运行一下,和VB6的效果差不多了。正在得意中,大李走过来,在我的Form1中添加了一个文本框textbox1,清空其中的文字。然后运行了一下程序。在运行中,他在文本框里写了几个字:“Henry的作品”,然后点击Button1切换到Form2,接着点击Button2,重新出现的Form1的文本框中却变成了空白。
“这……”我一时语塞,慢慢地想起点什么,“应该是程序通过New构造方法,每次点击按钮都会重新实例化一次Form类吧?所以每次重新出现的窗体都不是以前曾经出现过的那个。那以前的窗体也就失踪了,调不出来了。”
“失踪?”大李不禁笑了起来,“对呀,失踪在内存中。你每实例化一次,系统就会开辟一块内存给一个对象,并会有相应的线程对其进行控制与管理。如果作为进程中主线程的第一个Form1被Hide起来,没有关闭它,那么即使你关闭后来新创建的所有窗体,进程也不会被中止。那么,用什么方法去找回你失踪的窗体,或者说怎么样就只使用一个对象,不再每次实例化呢?”
“全局对象!”我脱口而出,感觉到大李不置可否,只好硬着头皮继续说:“利用全局变量,使Form1与Form2只实例化一次,这样就可以了。”
然后,在大李的注视下,我开始实施我的计划:
再创建一个模块文件:module1.vb,并且同时在:
解决方案管理器-〉项目名.sln -〉右点鼠标 -〉属性 -〉通用属性->常规->启动对象 -〉改为Module1。让程序启动后先运行Module1中的代码指令,然后编写代码如下:
'Module1.vb
Module Module1
Public frm1 As New Form1() ‘定义Form1的公用变量实例
Public frm2 As New Form2()‘定义Form1的公用变量实例
Sub Main()
Application.Run(frm1)‘这句话表明程序启动了modele后,会接下来运行frm1,即form1的一个实例。主线程就成为运行期的frm1了。
End Sub
End Module
这样一来,frm1、frm2就成为全局变量了,在Button1与Button2的单击响应代码中只要写:frm1.show就可以了。
再运行程序,可以了,“Henry的作品”一直都会保持在Form1中的文本框里。
大李老哥没等我欢笑起来就再次出手,在frm1隐藏起来的时候,关闭了Form2窗口(frm2实例),桌面上什么窗体都没有了,进程又一次被陷入在无法中止的情况下。My God,面向对象的VB.NET,爱你难道真的这么难……
Henry的VB.NET之旅(二)—构造与析构
韩睿
大李在Form1窗体的实例被隐藏的时候,关闭了Form2窗体的实例,使我失去了对主线程的人工控制,进程无法正常关闭了。只好使用Ctrl+Alt+Del调出系统进程管理器,强行中止了该进程。为了避免抬头看见大李的笑脸,我只好低头想办法。
有了,我只要能截获Form2实例关闭的消息,不就可以再调出隐藏的主线程窗体了吗?在Form2的基类事件(Base Class Event)中重载Closing方法进行处理:
Private Sub Form2_Closing(ByVal sender As Object, ByVal e As _
System.ComponentModel.CancelEventArgs) Handles MyBase.Closing
frm1.Show()
End Sub
哈,很方便,我关闭了Form2窗体的实例后,被隐藏的那个frm1又出现了。
“嗯,”大李终于发话了,“你再点击一下Form1窗体上的Button1试试。”
我背心一凉,隐隐感觉大李等待着的就是这个时候。无奈的我还只能照他说的去做。果然,弹出一个出错窗口:
“你关闭了frm2这个Form2的实例,也就结束了这个对象的生存期,”大李看来是蓄势已久了,“这就是出错提示中所说的‘无法访问名为Form2的已处置对象’。当我们关闭一个窗口的时候,会发出一个终止响应,并将该窗口对象,就象上面定义的frm2,送入终止队列,公共语言运行库的垃圾回收器跟踪着这个对象的生存期,此时就会调用此对象基类,比如Form2的Dispose方法,用于销毁对象并收回资源。所以……”
“所以我只要判断一下frm2是否被销毁就行了,如果销毁了,我就再构造一个实例不就行了?”我恍然大悟道。
大李第一次微笑地点了点头说:“用frm2.IsDisposed就可以来判断了。”
我心领神会地写道:
Private Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button1.Click
If frm2 Is Nothing Or frm2.IsDisposed Then ‘判断对象是否被销毁
frm2 = New Form2()
End If
Me.Hide()
frm2.Show()
End Sub
这下完善多了,两个窗体之间的切换也不会有这么多别扭的问题了。我转过身,看到大李已经找了把椅子坐在我的身边。
“你来说说,对VB.NET的窗体实例的创建与销毁的过程吧。”
我整理了一下凌乱的思路,长吁了一口气,开始说:“一个窗体类,比如Form1类是通过调用其基类,就是Form类的New方法来创建实例、Dispose方法来销毁实例。”
“没错。”大李边说话,一边在我的程序中点击开来被代码窗口自动折叠起来的" Windows 窗体设计器生成的代码 ":
Public Sub New()
MyBase.New()
'该调用是 Windows 窗体设计器所必需的。
InitializeComponent()
'在 InitializeComponent() 调用之后添加任何初始化
End Sub
'窗体重写处置以清理组件列表。
Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
If disposing Then
If Not (components Is Nothing) Then
components.Dispose()
End If
End If
MyBase.Dispose(disposing)
End Sub
大李开始解说道:“MyBase 关键字的行为类似于引用类的当前实例的基类的对象变量。MyBase 常用于访问在派生类中被重写或隐藏的基类成员。在这段代码中,MyBase指的当然就是System.Windows.Forms.Form类了。构造对象时用的New方法是显式调用的,没什么好解说的。但析构的方法值得一说。”
他看了我一想,继续说:“Form.Dispose方法是重写自Control.Dispose方法的,那么Control.Dispose方法的含义又是怎么样的?它的作用就是:释放由Control占用的非托管资源,还可以另外再释放托管资源。当它参数中的disposing 为 true 则释放托管资源和非托管资源;为 false 则仅释放非托管资源。 Form类的disposing为true。在关闭窗体时自动调用dispose的功能是得益于.net的公共语言运行库,运行库自动处理对象布局和管理对对象的引用,当不再使用对象时释放它们。其生存期以这种方式来管理的对象称为托管数据。自动内存管理消除了内存泄漏以及其他一些常见的编程错误。任何类型的 Dispose 方法都应该释放它拥有的所有资源。它还应该通过调用其父类型的 Dispose 方法释放其基类型拥有的所有资源。该父类型的 Dispose 方法应该释放它拥有的所有资源并同样也调用其父类型的 Dispose 方法,从而在整个基类型层次结构中传播该模式。要确保始终正确地清理资源,Dispose 方法应该可以被多次安全调用而不引发任何异常。”
“可是,如果系统问题或应用程序调用上出了问题,不能正常调用Dispose怎么办?”我想起了什么,问道。
“如果通过Dispose还释放不干净或没有调用Dispose,系统的垃圾回收器会调用对象的 Finalize 方法进行清除。由于执行 Finalize 方法会大大减损性能,所以我们不会一开始就用它去进行清除工作。”大李稍微解释了一下。
我终于想起了一个重要的问题:“如果总是在模块中定义的全局变量来处理,由于访问范围太大,会不会有安全性的问题?”
“当然,我们可以试试其他的解决方案。”大李总是有备而言。
Henry的VB.NET之旅(三)—共享成员
韩睿
我开始佩服起大李来了,同时对VB.NET充满了好奇与了解的渴望。
“除了全局变量外,还有什么好办法吗?”我迫不急待地想知道结果。
“可以使用共享成员。”大李依然是那么淡淡然,“VB.NET现在是支持真正的面向对象编程,可以继承、使用多态、共享成员和静态成员。共享成员就是在所有类和所定义派生类的实例之间共享的方法、属性、字段和事件。所有使用类创建的对象都可以访问相同的数据、共享实现过程,并且收到相同的激发事件。”
“这么好呀,那怎么实现呢?”我好象又回到了课堂上,满脑都是无知的符号。
“很简单,在Public或Private成员修饰符后加上Shared关键字即可了。Shared关键字指示一个或多个被声明的编程元素将被共享。共享元素不关联于某类或结构的特定实例。可以通过使用类名或结构名称或者类或结构的特定实例的变量名称限定共享元素来访问它们。你来看这段代码
Public Class ShareClass
Public Shared SharedValue As String
Public Shared Sub ShareMethod()
MsgBox("This is a shared method.")
End Sub
End ClassSub
TestShared()
Dim Shared1 As New ShareClass() ' 创建了类的一个实例.
Dim Shared2 As New ShareClass() '创建了类的第二个实例.
Shared1.SharedValue = "Share Value 1" '给共享字段赋值.
Shared2.SharedValue = "Share Value 2" ' 重写共享字段的值
ShareClass.ShareMethod() '不用创建实例就可以调用类的方法.
End Sub
“有两个要点,一个是共享成员如果是方法或属性,我们不用创建实例就可以直接用‘类名.共享成员’的方法进行调用;第二个要点,共享字段是唯一的,你来说说运行后Shared1.SharesValue的值是多少?”
我头脑里转了两圈,既然说共享字段是唯一的,那么Shared1.SharedValue和Shared2.SharedValue其实对应的是同一个共享字段,所以么,我立刻大声说:“是Share Value 2。因为被Shared2.SharedValue修改了。”
“没错!”大李又一次绽放了会心的微笑,真是对我的最高奖赏。“那你还不动手修改一下你的程序?”
好!说做就做。既然共享属性在其调用的时候是不用实例化的,那么我最好能借助构造一个共享属性来实现。并且要能够判断其实例是否存在或是否被销毁,如果不存在就构造一个新的窗体实例,如果存在就对其进行调用:
Private Shared m_vb6FormDefInstance As Form1
Public Shared Property DefInstance() As Form1
Get
If m_vb6FormDefInstance Is Nothing OrElse _
m_vb6FormDefInstance.IsDisposed Then‘判断窗体实例是否存在
m_vb6FormDefInstance = New Form1()
End If
DefInstance = m_vb6FormDefInstance
End Get
Set(ByVal Value As Form1)
m_vb6FormDefInstance = Value
End Set
End Property
在Form2中也加入相似的代码,只不过把Form1改为Form2。调用它是如此的方便:
Form1中的代码
Form2中的代码
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Me.Hide()
Form2.DefInstance.Show()
End Sub
Private Sub Button1_Click(ByVal sender As _
System.Object, ByVal e As System.EventArgs) _
Handles Button1.Click
Me.Hide()
Form1.DefInstance.Show()
End Sub
Private Sub Form2_Closing(ByVal sender As Object,_
ByVal e As System.ComponentModel.CancelEventArgs) _
Handles MyBase.Closing
Form1.DefInstance.Show()
End Sub
赶紧运行一下,不对呀,第一个窗体里文本框改为“Henry的作品”后,再切换仍是不见了?我疑惑地抬头看了看大李。
“你再好好想想,第一次启动的是什么?”大李模糊地说了一句。
“第一次启动的是作为主线程的Form1窗体呀!”我嘟啷着说。
“那么Form1.DefInstance在哪呢?”大李这句话真是惊醒梦中人呀!
我立刻动手:
Module Module1
Sub Main()
Application.Run(Form1.DefInstance)
End Sub
End Module
并把Module1设为启动项目。运行运行……哈哈,一切都如意!我开始喜欢上VB.NET了。
Henry的VB.NET之旅(四)—类和结构
韩睿
刚上班,就被大李找去了。“Henry,昨天对窗体的操作给你最大的体会是什么?”
“当然有体会,最深的印象就是VB.NET中类是无所不在了,连窗体都成为了一个类。”我深有感触地说。
“没错,类是我们用来构造VB.NET应用程序时的最基本的编程结构了。你也学习过最基本的面向对象编程了,那么你能告诉我,结构与类有什么相似之处与不同之处吗?”
“好的。”我口中回答着,心里还是有点不以为然,“结构和类,都是对成员的封装方式,但是类可以支持继承……”
大李一边点着头,一边听我说,听到我最后支吾着没了声音才抬起头“还有呢?”
“没了。”我这时才开始心中发慌。
“呵呵,相同之处我想你心中还是明白的,它们都含有成员,包括构造函数、方法、属性、字段、常量、枚举和事件。都可以实现接口,都有共享的构造函数。”
“对不起,最后那一句,都有构造函数是什么意思?结构的构造函数我从来没有自己定义过。”
大李立刻写下了这一段代码:
Structure SHenry
Public x, y As Integer
Public Sub New(ByVal x As Integer, ByVal y As Integer)
Me.x = x
Me.y = y
End Sub
End Structure
Sub main()
Dim H1 As SHenry = New SHenry()
Dim H2 As SHenry = New SHenry(2, 1)
End Sub
“真的呢,可以实现结构的构造函数!”我还一直没有注意过这个问题。“可是,你只定义过一个带参数的构造函数,H2的实例化我是明白的,可是H1怎么也能通过编译器检测呢?”其实我心中更想问的是结构怎么也能实例化成对象,那和类真的很象呀。
“原因么,”大李推了推眼镜,“每个结构都隐式地具有 Public 无参数实例构造函数,该构造函数产生结构的默认值。所以你平时不写构造函数,也一样可以New出一个结构来,对吧?事实上,我们反而在结构类型声明中不可能声明无参数实例构造函数。只能声明‘参数化’实例构造函数。”
“都可以用new来实例化,结构和类在内存分配上难道也是一样的吗?”这个问题我一直挺不明白,正好借这个话题问一下。
“在这上面,差别可就大了。”看到大李喜笑颜开的样子,我就知道问到点子上了,立刻摆开架势,作认真倾听状。
“简单来说,结构是值类型,而类是引用类型。因此,结构使用堆栈分配,类使用堆分配。”
看到我迷茫的双眼,大李笑了笑,在电脑上飞快地写了个示例:
Class CHenry
Public z As Integer = 0
‘能对非静态成员初始化也是一个区别
End Class
Sub main()
Dim H1 As SHenry = New SHenry(0, 2) '赋给H1.x=0
Dim H2 As SHenry = H1
H2.x = 26
Dim R1 As New CHenry() 'R1.z也是等于0
Dim R2 As CHenry = R1
R2.z = 26
Console.WriteLine("H1.x= " & H1.x & ",H2.x= " & H2.x)
Console.WriteLine("R1.z= " & R1.z & ",R2.value= " & R2.z)
End Sub
“你看一下结果应该是什么?”大李一边说,一边运行了程序:
H1.x= 0,H2.x= 26
R1.z= 26, R2.value= 26
大李看着我瞪圆的双眼,慢慢地说:“这就是值类型和引用类型的差别。结构的实例 H2.x 赋值并不影响H1.x,这是因为虽然它们同属于一种SHenry结构,但它们都有各自的存储空间。相反,给 R2.z赋值26后; 则会影响R1 和 R2 都引用的对象”
“说得更清楚一点,类作为引用类型,是存储在运行时的堆上,只能通过引用该存储来访问它们,不能直接访问。引用类型的变量总是包含该类型的值引用,或包含空引用。空引用不引用任何内容;除分配空引用外,对空引用进行的任何操作都是无效的。引用类型的变量赋值只会创建引用的一个副本,而不是所引用的值的副本。它们实际上都是会指向同一块存储区的。”大李手指了指运行的结果。
“结构是直接存储在堆栈上,要么在数组中,要么在另一个类型中。当包含结构实例的位置被销毁时,结构实例也会被销毁。值类型总是可以直接访问。我们不能创建对值类型的引用,也不能引用已销毁的值类型实例。值类型的变量总是包含此类型的值。与引用类型不同,值类型的值不能为空引用,也不能引用派生相近程度较大的类型的对象。值类型的变量赋值会创建所赋的值的副本,当然会新开辟一块内存区来保存值。”
“哦,我明白了。它们还有什么区别没有?”我对结构和类的区别第一次有了深刻的感觉。
“当然有很多,比如所有的结构成员都默认为 Public,类变量和常量默认为 Private,其他的类成员默认为 Public;结构成员不能声明为 Protected,类成员可以;结构过程不能处理事件,类过程可以;结构变量声明不能指定初始值、New 关键字或数组初始大小,类变量声明可以。”大李喝了口水,停了一下,然后继续说。
“结构从不终止,所以公共语言运行库从不在任何结构上调用 Finalize 方法;类可由垃圾回收器终止,垃圾回收器会跟踪未完成的引用直到某个特定的实例,当检测到没有剩下的活动引用时,垃圾回收器将在类上调用 Finalize。”
“这个我可以理解,因为结构是值类型,是由系统统一管理内存,不是引用,所以不会对内存造成危害。”我接着说了两句。
“还有,你刚才也提到了它们之间一个很重要的区别:结构是不可继承的,而类可以继承。其实结构自身是从 ValueType 类隐式继承下来的。数据类型可分为值类型和引用类型。值类型要么是堆栈分配的,要么是在结构中以内联方式分配的。引用类型是堆分配的。引用类型和值类型都是从最终的基类 Object 派生出来的。当值类型需要充当对象时,就在堆上分配一个包装,该包装能使值类型看上去像引用对象一样,并且将该值类型的值复制给它。该包装被加上标记,以便系统知道它包含一个值类型。这个进程称为装箱,其反向进程称为取消装箱。装箱和取消装箱能够使任何类型像对象一样进行处理。”
“哦,我明白为什么结构也能被实例化成对象了!”我心中喜不自禁。“类的继承我在用C++和Java时也经常使用,但是VB6是不支持继承的,上次您提到VB.NET可以支持继承了,它是怎么做到的呀!”
Henry的VB.NET之旅(五)—类的继承
韩睿
“类的继承?”大李点点头,“以前很多人批评VB的功能不完善时,都会加上这个条件。但是现在VB.NET中已经是比较完善的面向对象编程方法了。刚才我们也提到过,类与 VB.NET 的结构类似,让我们可以定义封装一组相关项的数据类型。然而与结构的一个重要区别就是,VB.NET 类可以继承和扩展其他类的特性。作为新类的基础的类称为‘基类’。从基类派生的类称为‘派生类’。派生类继承基类中定义的所有字段、属性、方法和事件。你看,我们只要对类进行一次开发和调试,就可以将其作为其他类的基础重复使用。”
“类的继承的概念我学过的,在VB.NET中是如何来实际演练呢?”我还是坚持着问。
“我写段代码给你看一下吧,俗话不是说,百闻不如一见吗?”大李调侃了两句,噼噼啪啪地开始敲打起键盘来:
Public Class CBaseHenry
Private x As Integer = 10
Protected y As Integer
Sub New()
Console.WriteLine("基类的构造")
End Sub
Protected Overrides Sub Finalize()
Console.WriteLine("基类的析构")
MyBase.Finalize()
End Sub
Public Overridable Function GetY(ByVal x As Integer) As Integer
Me.y = me.x + x ‘private类型的me.x只能在基类中使用
Console.WriteLine("基类的GetY方法,结果为:" & Me.y)
Return Me.y
End Function
Public Sub OldY()
Console.WriteLine("基类的OldY方法")
End Sub
End Class
“你看,这是一个基类,我再给你写个继承自该类的派生类。”他一边说一边继续写:
Public Class CDerivedHenry
Inherits CBaseHenry
Sub New()
MyBase.New()‘注意:这句话要放在sub内的第一句
Console.WriteLine("派生类的构造")
End Sub
Protected Overrides Sub Finalize()
Console.WriteLine("派生类的析构")
MyBase.Finalize()
End Sub
Public Overrides Function GetY(ByVal x As Integer) As Integer
Me.y = x * 10‘protected类型的me.y却可以在派生类中使用
Console.WriteLine("派生类的GetY方法,结果为:" & Me.y)
Return Me.y
End Function
Public Sub NewY()
Console.WriteLine("派生类的新方法")
End Sub
End Class
“你看清了吧,我们通过声明Inherits关键字,来标识该类的基类是谁,如果没有标识的类,比如CBaseHenry,VB.Net就会视其为派生自Object基类,这是种隐式继承,和结构是隐式继承自ValueType 类的方式是一样的。”大李指着屏幕跟我解说着。
“那这些overrides与overload标识是什么意思呀?”我隐隐感觉到它们的意义,却没法清楚地说出来。
“我们使用继承,也要允许派生类中的某些属性或方法有与基类不同的行为,我们想‘重写’它,但基类也得要同意你才能重写呀,否则不就会发生命名冲突了吗?因为派生类的对象是可以使用基类的公用成员的,那怎么知道哪些基类的方法与属性是被派生类重写了呢?所以就必须有一个约定:我们在基类中用Overridable修饰符来标识允许基类中的属性或方法在其派生类中被重写,没有标识的其实就是默认为有NotOverridable修饰符来隐式地标识,用于提醒编译器该属性或方法不可被重写;然后在派生类重写时,我们就用Overrides修饰符来标识哪些是重写自基类中定义的 Overridable 属性或方法。”大李娓娓道来,“我们来演练一下对派生类的操作吧。”
Public Sub Main()
Dim obj As CDerivedHenry = New CDerivedHenry()
obj.GetY(4) '调用派生类的GetY方法
obj.OldY() '调用基类的oldY方法
obj.NewY() '调用派生类的NewY方法
End Sub
然后大李按了一下F5键,在“输出”窗口中就出现了如下的运行结果:
基类的构造 ‘开始运行New CDerivedHenry()
派生类的构造
派生类的GetY方法,结果为:40 ‘obj.GetY(4)的运行结果=4*10,不等于基类的(4+10)
基类的OldY方法 ‘obj.OldY()的运行结果
派生类的新方法 ‘obj.NewY()的运行结果
派生类的析构
基类的析构
“你看,你只在Main中实例化了一个派生类的对象,为什么先会出现‘基类的构造’?”大李问。
“这个么,”我开始在代码中查看起来,“是这句了。”我手指着派生类里的这段代码:
Sub New()
MyBase.New()‘注意:这句话要放在sub内的第一句
Console.WriteLine("派生类的构造")
End Sub
“没错,我们必须要注意这一点,就是派生类的构造函数与析构函数都必须重新编写。New 构造函数一般用于打开文件、连接到数据库、初始化变量以及处理任何需要在可使用对象前完成的其他任务。我们必须在Sub New 构造函数中的第一行代码使用语句 MyBase.New(),来调用类层次结构中该类的基类的构造函数,以获得基类的性质。析构则是在Sub Finalize中执行完对派生类的的清理任务,如保存状态信息、关闭文件和与数据库的连接,以及执行在释放对象前必须完成的其他任务之后,在析构函数的最后一句使用语句 MyBase.Finalize() 显式调用其基类的 Sub Finalize 方法,以析构MyBase.New()构造的内容。所以你从程序运行结果中也可以很清楚的看出这一顺序。”大李敲击着屏幕,象是给我提个醒。
“明白了,我会记着的。”我诚恳地点头回应着。
大李突然想起了什么,抬头对我说:“讲到类的继承,我们还得看一下重载与隐藏的问题。”
Henry的VB.NET之旅(六)—重载与隐藏
韩睿
大李坐在转椅上左右晃动着,手中的一个硬币在他指间灵巧地翻滚着。“hi, Henry。你对重写与重载的意思理解是怎么样的呢?”
我正盯着他手中的硬币发愣呢,“哦,重写,就是您刚才举的示例中,在派生类中用Overrides重新编写有Overridable标识的基类的方法或属性;重载么,就是我们可以用同样的名称,在一个类中用不同的参数列表来创建多个方法和属性,在调用时就可以适应不同参数类型的要求。”
“在一个类中创建?”大李的左眉向上一挑,我就知道我可能说错了。但是,好象没有说错什么呀。
“那好,你看一下,这样的写法会有什么结果?”
Public Class CBaseHenry
Public Sub oldY()
Console.WriteLine("基类的oldY方法")
End Sub
End Class
Public Class CDerivedHenry
Inherits CBaseHenry
Public Overloads Sub oldY(ByVal j As Integer)
Console.WriteLine("派生类的oldY方法")
End Sub
End Class
Public Sub Main()
Dim obj As CDerivedHenry = New CDerivedHenry()
……
然后大李写了obj.oldY这时出现的智能感知菜单上出现了参数列表:
“咦,第二个oldY()无参数过程签名应该是基类定义的呀。为什么在派生类的实例中也会出现呀!”我不由好奇起来。
“没错。还记得我上次曾显式编写构造函数的事吗?我用了一个mybase.new()用于继承下基类的性质。现在没有写,其实是一种默认的隐式调用。”大李一说我就明白了,其实现在的派生类应该是内含两个oldY方法了,无参数的oldY()其实就是基类的方法,是由于Overloads和Mybase.new()双重作用的影响。
“那你再看这样的变化情况。”大李象是看出我已经明白了这个问题。他在派生类中添加了一个用Overloads标识的与基类形式一致的无参数oldY()。
Public Class CBaseHenry
Public Sub oldY()
Console.WriteLine("基类的oldY方法")
End Sub
End Class
Public Class CDerivedHenry
Inherits CBaseHenry
Public Overloads Sub oldY(ByVal j As Integer)
Console.WriteLine("派生类的oldY方法1")
End Sub
Public Sub oldY()
Console.WriteLine("派生类的oldY方法2")
End Sub
End Class
Public Sub Main()
Dim obj As CDerivedHenry = New CDerivedHenry()
obj.oldY()
End Sub
“好的,现在你再说说看,现在的obj.oldY()的运行结果会打印出什么?”大李手按在F5运行键上问我。
“应该是派生类的,不对,基类,呃,派生类……”我一下子就晕了。
“呵呵。”大李也不禁发笑起来。然后点击了一下F5键。结果是:
派生类的oldY方法2
“这就叫隐藏,我们用overloads方式,隐藏了基类的同名方法。以防用户发生混淆。一般来说,隐藏有两种情况,一种是通过范围来实现。比如你定义一个全局变量x,但在一个方法中,你又定义了一个局部变量x,在方法中使用x时,使用的是局部变量的那一个,也就是用局部变量x在方法中隐藏了全局变量x。另一种情况,就是通过继承来隐藏,方法么,除了刚才用的overloads,还可以用Shadows关键字来实现。”
“Shadows?阴影?倒是很贴切的名字。这个是怎么用的呢?”我兴趣由然而生。
“Shadows功能很强的。”说着,大李又开始修改刚才的代码了。
Public Class CBaseHenry
Public Sub oldY()
Console.WriteLine("基类的oldY方法")
End Sub
End Class
Public Class CDerivedHenry
Inherits CBaseHenry
Public Shadows Sub oldY(ByVal j As Integer)
Console.WriteLine("派生类的oldY方法")
End Sub
End Class
Public Sub Main()
Dim obj As CDerivedHenry = New CDerivedHenry()
……
再写到obj.oldY时,出现的智能感知菜单上就剩下一个方法了。
“哈,真的呢,基类的方法看不到了。但是,这和重写的效果岂不是一样了吗?”我不由地又想起一个问题,如果是一样的作用,要Shadows干什么呀。
“还是有区别的。”大李开始扳着指头数起来,“最明显的区别在于,隐藏适用于任何元素类型,你可以在派生类中用Public Shadows oldY as Integer来隐藏基类的oldY()方法;而重写只能适用于方法与属性,而且声明参数与修饰符都要求完全一致。”
“还要注意一点,当派生类又派生出一个子类时,重写与隐藏都会被继承下去。当然,如果在派生类中是用private来修饰Shadows成员的话,它的子类就会继承它基类的成员了。”
“我倒是感觉,现在VB.NET对继承的处理功能真的很强大,有继承,对于编程者来说,真的是件好事。”我不由感叹道。
“不错,继承层次体系是有很多好处,但是事物总是有其双面性,继承也有不少问题,其中最麻烦的,就是‘脆弱的基类’。”大李紧锁眉头认真地说。
“哦,什么叫‘脆弱的基类’?”我不解地问。
Henry的VB.NET之旅(七)—脆弱的基类
韩睿
“既然说是脆弱,当然是指它象蛋壳一样不堪一击喽。这个问题其实很好理解。程序总是由人来设计与编写的,所以工作开始时考虑不到某些问题当然也是很正常的事。所以可能在工作进行了一段时间后发现基类需要变更。你想,如果我在基类中更改了成员的数据类型,以及那些允许重写的那些方法和属性,那派生类及其子类还能正常工作吗?尤其是当一个团队中的多个开发人员一起来创作基类和派生类时,就更是要命了。很多情况下,大家可能已经把基类和一些派生类编译成二进制形式进行提交了。更改基类,重新编译再分布,会牵一发而动全身,导致项目的崩溃。所以我们把这叫做‘脆弱的基类’。也就是说,它是整个工程中最薄弱最致命的环节。”大李眉头一直紧锁着,想必是回想起了自己受打击的经历。
“这么严重呀,现在的软件工程设计方法会不会对这个有很好的解决方案?”我努力想缓解一下大李的严肃神情。
“如果对项目的前期设计考虑尽可能周详,在工程实施中对项目的代码控制与相关性分析做得踏实,会起到很好的效果。但是不管一个人如何努力,有时还是无法避免对基类进行不可预见的更改。我们摸索过很久,有了一些处理的手段。”
“真是成事在人呀,我们现在有什么解决之道?”我也一下子振奋起来了。
“呵呵,并不是什么完美解决方案。只能在某种程度上减轻危害。我们常用的一个方法,最直接的思想就是,把有可能发生的更改全都放在派生类中进行,不在基类中做。”
“这具体是什么意思呀,我还是不太明白。”我不好意思地挠挠头。
大李微笑着点点头,看来是知道我不会明白的了。“我们在基类中使用的是抽象类,它内含的方法与属性只有定义,没有进行实现,而把实现部分都放在派生类中做。这样一来,抽象类自身是无法被实例化的。但是它的好处不言而喻,就是有可能发生的实现上的更改都会只涉及到它的派生类了。VB.NET中就提供了这样的手段。”
说着,大李就打开VS.NET集成编译环境,顺手写了一小段代码:
Public MustInherit Class CBaseHenry
Public MustOverride Sub subX(ByVal x As Integer)
Public MustOverride Function fcnY(ByVal y As Integer) As Long
End Class
Public Class CDerivedHenry
Inherits CBaseHenry
Public Overrides Sub subX(ByVal x As Integer)
'写入实现的代码
End Sub
Public Overrides Function fcnY(ByVal y As Integer) As Long
'写入实现的代码
End Function
End Class
“这里要注意两个问题,一个是关键字,我们用MustInherit来修饰类名,使类成为抽象类,在它的成员中,把方法和属性前加入MustOverride修饰符表示它们必须在派生类中加以实现。第二个要注意的是,派生类必须对所有用MustOverride标识的基类方法和属性都进行实现,只重写了subX,不写fcnY编译器会报错的。”
“这的确可以解决一部分问题,但好象只能解决在基类中进行实现的代码有更改的问题,对于数据类型的更改好象没有什么效果。”我看了好一会,发出了这样的疑问。
“所以我刚才说,是在某种程度上进行解决嘛。”大李也不由笑了起来,“不过你提的这个问题,倒不是太麻烦,我们可以在派生类中用Shadows来解决呀!(详见本报上一期《重载与隐藏》)”
这倒是个不错的主意,我心中暗暗评价了一番。突然我又想到一个问题:“如果基类要做功能扩展,怎么办呀?”
“如果是要做扩展,最安全的方法是添加新成员,而不是对基类的大肆修改。一般是往派生类添加设计时缺失的新成员。最好不要使用Overloads关键字来命名与基类相同的成员,那样往往会带给你意想不到的问题。最好重新定义新成员,命名上也要尽量与基类已有的成员名区分开来。其实,也可以往抽象类基类中添加新成员的定义,但这样一来,需要为基类制定版本,虽然不会对应用程序造成毁灭性的危害,但是应该要能够完全地控制与管理自己的代码。我们一般是不希望扩展基类的。”
我已经大意上领会了大李的一片苦心:“您的意思,是不是指基类的脆弱问题实际上是客观存在的,我们所做的就是要最大程度的减小这个问题带来的危害?”
大李眼中闪过一丝赞许的笑意,颌首道:“没错,对于一个应用程序的设计者来讲,想使用面向对象方法来开发,必须要在设计的时候精心策划类的层次结构。一般来说,是有这样几个准则需要把握的:
第一,遵循先通用,再专用的原则。先设计好层次结构中每一级别的类中的通用部分,也就是提供给派生类继承的成员和标识为Public的成员;
第二,在定义数据类型和存储区时要有预留量,以避免以后更改困难。例如,即使当前数据可能仅需要Integer类型就够了,在设计时我们使用 Long 类型的变量。当然,最好能物尽其用,也不要盲目放大;
第三,在一个项目中,必须统一管理与分配团队中使用的所有的命名,以减少命名冲突,这一点其实事关重大;
第四,要使用提供可行的最低访问权限的访问修饰符声明类成员。内部类成员应声明为 Private;仅在类内与派生类才需要的成员应标记为Protected;Friend 数据成员可以从类的外部访问,但仅限于该模块是定义该类的项目的一个组成部分;使用Public标识的成员,只能是实例化时真正需要的内容,而且经常用在类层次结构的底部。”
“也就是说,一个规范的操作,标准的命名体系可以决定基类的强壮与否?”我不禁感触了一声。
“不对,应该这样说,可以决定的是给脆弱的基类穿上多厚的防护衣。因为基类始终都是脆弱的。”大李更正道。
我连声赞同:“对,对。我现在是真正明白为什么总有人提编程规范的事情,我一直认为是增强代码的可读性,没想到,对程序自身还有这么大的帮助。”
“当然,其实你认真想一下,Overrides关键字的作用,不管要不要注明,编译器都可以很方便地判断方法或属性是否在基类中,签名是否匹配,但是VB.NET要求我们必须标注,就是强制开发人员注明重载基类方法或属性的意图,使开发过程更合理与有效。此外,还有更重要的就是,我们要在工程实践中不断地学习与磨练,了解更多的知识,获得更多的经验,这样才会成长为一名合格的程序设计师。就拿继承来说吧,在.NET中其实支持三种继承方式:实现继承、接口继承、可视继承。我们其实只用了第一种继承方式,你看,要学的东西是不是很多?”大李友好地拍了拍我的肩膀。
Henry的VB.NET之旅(八)—接口
韩睿
“还有两种继承方法?”我是真正被VB.NET的强大功能所折服了。求知的渴望驱使着我向大李露出了一个最灿烂的笑容,“行了,别傻笑了,我告诉你不就成了。”大李不禁也笑了起来。
“刚才我说到‘脆弱的基类’时,就提到实现继承最大的问题,就在于基类与派生类之间的关系过于紧密,还记得吧?基类实现细节往往会泄露出来,这不是我们愿意看到的封装情况,所以有很多程序设计师就想尽方法来改进这一问题,其中最出名的,就是COM了。”
“COM?我在VB6中经常用呀,是一种组件形式呀。”我不是太明白大李的话,这和面向对象的继承有什么关系?
“你说的不错,正是通过组件的这种封装方式,我们就可以把实现继承局限于在组件内部使用,而我们使用组件时,就不用理会它内部是什么,怎么实现的。这就可以避免不可预测地对基类的修改。我们把利用组件的组织程序方法称为面向组件程序设计,但这也是一种面向对象的设计方法,不过是更具强制性。组件支持组件内部的实现继承,还支持接口继承。”
“接口继承,我不是太清楚。不过,接口我是清楚的,就是组件开放的属性、方法与事件、公用变量的定义方法。我在VB6中也接触过接口编程,不是太方便,好象要把定义了方法却没有写实现过程的类编译成DLL文件,VB6会自动将它创建为接口,不过只能是隐藏的,不是显式定义的。”我回忆了一下说。
大李微微点了点头说:“从面向对象的观点来看,接口就是对象的外观,而对象实际的工作方式就是实现。把接口与实现分离开就是我们要进行封装的动机。用户只能通过接口来操作,但是看不到具体的实现的代码。”
大李顿了一顿,然后接着说:“VB.NET 以前的 Visual Basic 版本可以使用接口,但不能直接创建它们。VB.NET 却是允许可以用 Interface 语句定义真正的接口的喔!”
此言一出,真让我大吃一惊。“我们也可以直接定义接口吗?”
“当然,”大李说,“在VB.NET中,和类一样,接口也可以定义属性、方法和事件。但正如我刚才说到的,与类不同的是,接口并不提供实现。现在的接口,是由类来实现的,并从类中被定义为单独的实体。”
大李手指在桌面上重重的敲了一下,加强了一下语气:“我们可以这样来理解,接口表示的是一种约定。实现接口的类必须严格按其定义来实现接口的每个方面。有了接口,就可以将功能定义为一些紧密相关成员的小组。可以在不危害现有代码的情况下,开发接口的增强型实现,从而使兼容性问题最小化。也可以在任何时候通过开发附加接口和实现来添加新的功能。虽然接口实现可以进化,但接口本身一旦被发布就不能再更改。对已发布的接口进行更改会破坏现有的代码。若把接口视为约定,很明显约定双方都各有其承担的义务。接口的发布者同意不再更改该接口,接口的实现者则同意严格按设计来实现接口。”
“也就是说,在VB.NET中,接口是用类来实现的,就象是个抽象类,只是关键字用的是Interface,不是class,对吗?”我还是很好奇。
“接口的实现可以是类,也可以是结构。接口的定义用的是Interface关键字,实现时用的是Implements关键字”大李淡淡的一句话,使我在心中开始回忆起类和结构的差别来(详见前文《类和结构》)。
大李接着跟我解说:“接口的成员包括其成员声明引入的成员和从其基接口继承的成员。只有嵌套类型、方法、属性和事件才能作为接口成员。方法和属性不能有实体。接口成员隐式地定为是Public,而且不能指定访问修饰符。接口自已倒是可以添加修饰符。”大李跟着看了我一眼,就在电脑上开始写起示例来:
Public Interface IHenry
Sub subX(ByVal x As Integer)
Function fcnY(ByVal y As Integer) As Long
Property proZ() As String
End Interface
Public Class CHenry
Implements IHenry
Private z1 As String
Sub subX(ByVal x As Integer) Implements IHenry.subX
'填写实现代码
End Sub
Function fcnY(ByVal y As Integer) As Long Implements IHenry.fcnY
'填写实现代码
End Function
Property proZ() As String Implements IHenry.proZ
Get
Return z1
End Get
Set(ByVal Value As String)
z1 = Value
End Set
End Property
End Class
大李指着代码说:“你看,我用Interface定义了一个接口IHenry(笔者注:一般来说,接口的命名第一个字母为I,这并没有什么强制的含义,但却是一个通用的命名规则),内含三个方法与属性的定义,但并没有实现;实现代码写在CHenry类中。你可以按我刚才说的约定的思路来理解,IHenry接口其实就是一个合约的提纲,CHenry是该合约的一个操作版本,只要在CHenry中实现了接口IHenry定义的所有的方法,不管是怎么实现的,有没有加入新的方法,都可认为CHenry是支持IHenry接口的一个实现类。”
“一个实现类?也就是说接口可以有多个实现喽?”我不解地问。
“当然,我刚才说过,接口其实就是一个对象的外观,在VB.NET中有很多很重要的接口,定义了很多种类型的对象,比如说你所熟悉的Windows Form的控件,它们的基类大部分是Component类,而Component类就是IComponent接口的一个实现类,IComponent类还有其它三个实现类,那就是:Control、HttpApplication和MarshalByValueComponent类,分别完成不同的功能,但它们都要实现IComponent接口所定义的方法与属性,且参数定义与返回类型都要与接口定义时的签名一致。换个角度来看这个问题,我们如何创建一个组件,且让系统都识别,靠的就是对接口的实现。要成为组件的类必须实现 IComponent 接口,并提供不需要参数或只需一个类型为 IContainer 的参数的基本构造函数就行了。”
“哦,我明白了,通过接口,我们可以定义下某种对象的基本外观,然后可以自由地进行实现与扩展,却不涉及对原型的直接修改。太棒了!”我一下子高兴了起来。
“是呀,在VB.NET中可以显式定义接口,使得接口编程也成为很棒的编程方式了。刚才所说的,在不同的类中实现同一个接口,不就是一种用接口实现的多态性吗?另外,在类和结构中也可以实现多个接口。”大李写了如下的代码给我看:
Interface IOne
Sub SubOne()
End Interface
Interface ITwo
Sub SubTwo()
End Interface
Class CHenry
Implements IOne
Implements ITwo
Sub SubOne() Implements IOne.SubOne
'实现代码
End Sub
Sub SubTwo() Implements ITwo.SubTwo
'实现代码
End Sub
End Class
“真有意思,”我饶有兴致地看着代码,“也就是说CHenry就具备了IOne与ITwo所定义的外观特点了。”
我一下子想起了抽象类的定义,不由好奇地说:“如果把接口看成一个基类,那么用于实现的类不就可以看成是它的一个派生类了?这是不是就是接口继承呀?”
Henry的VB.NET之旅(九)—接口继承
韩睿
大李拍了拍我的肩膀说:“你真有想象力,不过的确,有很多文献把这种用Implements来实现接口的方法就称为接口继承。其实,接口自己也是可以进行继承的,在VB.NET中把接口间的继承形式称为接口继承。”
我不禁跟着笑了起来:“接口继承要成为继承,当然要用Inherits,对吧?”
大李点点头说:“既然你都清楚了,那你来模拟一个下拉框Combobox的接口吧。”
“Combobox?”我不禁一愣,不过一会就想明白了,“是不是要让它符合有文本框供文字输入与下拉列表供选择列表项的组合形式这样的外观?”
大李跟着提醒了我一句:“接口与VB.NET中的类继承还是有不同的,它可以支持从多个接口进行多重继承,VB.NET中的类只支持单一基类的继承。”
见大李没什么别的意见,我就开始写起代码来:
Interface IControl
Sub Paint()
End Interface
Interface ITextBox
Inherits IControl
‘在文本框设置文本
Sub SetText(ByVal text As String)
End Interface
Interface IListBox
Inherits IControl
‘在下拉列表设置列表项
Sub SetItems(ByVal items() As String)
End Interface
Interface IComboBox
Inherits ITextBox, IListBox
End Interface
Class CHenry
Implements IComboBox
Sub SetText(ByVal text As String) Implements ITextBox.SetText
'实现代码
End Sub
Sub SetItems(ByVal items() As String) Implements IListBox.SetItems
'实现代码
End Sub
……
写到这,发现CHenry类中的Implements IcomboBox的ICombobox下面还有一条波浪线,说明接口并没有实现完毕,可是我已经把IComboBox继承的两个基接口中的方法都已经实现了呀。把鼠标靠近波浪线一看,系统提示“必须为接口IControl实现sub Paint()”,于是我就继续写:
Sub Paint() Implements IControl.Paint
'实现代码
End Sub
End Class
我转回头问大李:“接口的实现类中是不是要把接口的所有基接口都要实现一遍呀?”
大李点点头说:“如果象这个演练中的情况,当然是要把基接口中没有实现的方法进行实现。但也要注意,实现接口的类或结构会隐式地实现该接口的所有基接口。如果一个接口在基接口的可传递闭包中多次出现,它的成员只参与一次构成派生接口。实现派生接口的类型只需实现一次多次定义的基接口方法。所以你也可以用Sub Paint() Implements ITextbox.Paint或是Sub Paint() Implements IListBox.Paint来代替,但只能用这三个定义中的一个。你再来看这段代码。”大李开始修改起刚写好的代码来:
Interface IControl
Sub Paint()
End Interface
Interface ITextBox
Inherits IControl
‘在文本框设置文本
Sub SetText(ByVal text As String)
Shadows Sub Paint()
End Interface
Interface IListBox
Inherits IControl
‘在下拉列表设置列表项
Sub SetItems(ByVal items() As String)
End Interface
Interface IComboBox
Inherits ITextBox, IListBox
End Interface
Sub test(ByVal x As IComboBox)
x.Paint()
End Sub
“这里的x.Paint()是哪一个接口的方法?IControl是ITextBox?”大李一脸笑意,真是气人。但是,我应该可以回答上来的,我按类的隐藏的概念回忆了一下(详见前文《重载和隐藏》),哈,明白了,它当然是调用它直接被派生的那个基类中的方法呀。
“是ITextBox中的方法吧!”
“可以呀,不错!”大李简单地夸了我一句,然后喝了口水,继续说:“基接口的成员名称在继承分层结构的一条路径中被隐藏,但它在其它的路径中不会被隐藏,比如我们可以从IlistBox中去继承Icontrol中的Sub Paint()。”
“可是,在您的这个示例中的sub test里,x是接口的实例吗?可是,接口还没有实现呀?”我还是有问题要问。
“test方法其实可以接受任何将 IComboBox 实现为小部件参数的对象,即使对接口 IComboBox 的实现可能相差很大。”大李回答道。
“是不是说我们在使用的时候,可以用实现IComboBox接口的类,比如CHenry的一个实例去代替x?”
大李笑着说:“基本上差不多了,你自己慢慢考虑吧。还有个问题比较有意思:实现类中用于实现接口的方法或属性名倒不用与接口中定义的名字一样,只要参数列表与返回类型一致就行了。比如在CHenry中的sub Paint()如果更名为sub xxx()也是可以的,只要后面跟着Implements IControl.Paint就行了。命名一定要有规划,不然,接口继承中也会带来命名重复造成的问题,我们来看一下。”
Interface IHenry1
Property yyy() As Integer
End Interface
Interface IHenry2
Sub yyy(ByVal i As Integer)
End Interface
Interface IHenryDerived
Inherits IHenry1
Inherits IHenry2
End Interface
Sub test(ByVal x As IHenryDerived)
x.yyy(1)
x.yyy = 10
End Sub
“你看,在sub test()中,无论你按IHenry2中的定义方式来使用x.yyy(1),还是用IHenry1中的方式来使用x.yyy=10,集成编译器都会在它们下方打上波浪线,表示出错,是什么错呢?”大李一边问我,一边把鼠标靠近波浪线,出现了编译器的出错提示:
“yyy”在继承接口“IHenry1”与“IHenry2”之间不明确
“所以,我一直强调命名规则,对吧?”大李看了我一眼,“其实解决方法倒用不着去更改基接口中的方法与属性名。”
Sub test()
Dim x As IHenryDerived
CType(x, IHenry1).yyy = 10
CType(x, IHenry2).yyy(1)
End Sub
“哦,用强制类型转换就可以了。”我又学到一招,不禁暗自窃喜。但是我心里总是有一个不大不小的疙瘩,说来说去,这接口与抽象类可真的太象了。赶紧得问问:“大李哥,这接口与……”
“抽象类?”大李一口就接了上来:“别急,小伙子,看看几点了,该下楼吃午饭了。”
Henry的VB.NET之旅(十)—何时用接口
韩睿
大李没告诉我接口与抽象类的区别,什么时候用接口,什么时候用实现继承。弄得我中饭也没吃好,老在琢磨这事,这不,一吃完饭,我就冲上楼,一个房间一个房间转,到处找大李。
过了好一阵,这老哥才和几个同事说说笑笑地回到办公室,我立即走上前,半请半拉地把他拽到电脑旁。“大李哥,我实在想得头晕,既然在VB.NET中接口有了这么大的发展空间,在形式上与抽象类如此相似,那么它们有什么区别?什么时候用接口呢?”
听着我一连串的问题,大李微笑着摇摇头,拍拍我的肩膀说:“小兄弟,不光是你弄不清,其实就是很有经验的程序设计师也对什么时候用接口,什么时候用抽象类而头痛咧。”
此话一出,我更是疑惑重重。不过反而安下心来,老鸟们都弄不清的问题,我不清楚也不必心中不安了。哈……
大李看着我忽忧忽喜的表情露出了一丝诧异,不过他没有理会,继续说:“但是这个问题我们还是有必要好好分析一下,让我们更明白接口与抽象类的具体含义。”
“我们早说过,抽象类是一种不能实例化而必须从中继承的类。抽象类可以完全实现,但更常见的是部分实现或者根本不实现,从而封装继承类的通用功能,它可以提供已实现的成员,因此,可以用抽象类确保特定数量的相同功能,但不能用接口这样做。”
“接口是完全抽象的成员集合,它的成员都无法在接口定义时实现,我们可以将它看作是为操作定义合同,接口的实现完全留给开发者去做。它们之间的区别,如果认真分析,还是有不少的:在VB.NET中,类只能是从一个基类继承,所以如果要使用抽象类为一组类提供多态性,这些类必须都是从那个类继承的;接口就不一样了,它不但可以用一个类或结构实现多个接口,一个接口还可以有多个实现。”
“也就是说,它们在提供多态性的问题上是有差别的?”我好象听懂了点什么。
“这是一个重要的区别,我们也可以从多态性的角度来考虑是要使用接口还是用抽象类。”大李同意了我的观点,“如果预计要创建组件的多个版本,我们应该创建抽象类。这是因为,抽象类提供简单易行的方法来控制组件版本。通过更新基类,所有继承类都随更改自动更新。这是好处,当然也是问题,对吧?(详见前文《脆弱的基类》)另一方面,接口一旦创建就不能更改。如果需要接口的新版本,必须创建一个全新的接口。所以,如果创建的功能将在大范围的全异对象间使用,则使用接口。”
我想了一下,接着大李的话说:“能不能这样说,抽象类主要用于关系密切的对象,而接口最适合为不相关的类提供通用功能。”
大李对我伸出了大拇指:“不错,小伙子悟性很好呀!你想,我上午跟你说,要创建控件,首先就是要对一些接口进行实现以让系统能够识别(详见前文《接口》)。而各个控件之间的联系其实关联性并不大。所以,它们的基础大都是接口。但是,我们要注意一点,在组件设计时,如果要在组件的所有实现间提供通用的已实现功能,则使用抽象类。这是因为我们刚才说过的原因,抽象类允许部分实现类,而接口不包含任何成员的实现。”
“唔,明白了,它们之间的区别有点明白了。”我默默地点了点头。
“另外,有个通用的设计思想,如果要设计小而简练的功能块,则使用接口。如果要设计大的功能单元,则使用抽象类。”大李又补充了一条建议。
“看来设计的问题还是蛮大的,一般来说,怎么设计接口呢?”我接着问。
“为什么你所看到的编程书籍也好,程序例程也好,极少有对接口的描述,而对类实现继承的例子比比皆是?这就从一个侧面给我们提了一个醒,如果使用适当,接口可以成为很有用的工具。但如果使用不当,它们会变得非常棘手,甚至妨碍有效的编程。接口的设计与使用其实是一项高明的艺术。”大李郑重其事的说。
“艺术?”我惊叫了一声。
“没错,艺术!”大李又加重了一下语气,“通过接口与实现的方式,我们可以将同一类型的程序运用在不同的对象上面,而且不必修改原有类,相对子程序必须通过修改源程序代码才能够达到重用的目的,接口与实现不仅是伟大的进步,也是境界极高的程序设计艺术。”
“哦,这倒是真的。”我回想起今天看到的接口的例程。
“但是,最大的问题还是集中在接口设计上。”大李接着说,“接口一旦被定义和接受,就必须保持不变,以保护为使用该接口而编写的应用程序。接口发布后,就不能对其进行更改。这是我们进行组件设计的一个重要原则,叫做‘接口不变性’。”
我点了点头:“接口不变性,这个我可以理解了。”
“我已经反反复复强调过,创建一个接口就是在创建一个定义,接口定义发布后则永远不能更改。接口不变性,就是为了保护为使用接口而编写的现有系统。当接口设计与需求有所出入,确认需要大幅度变更时,我们应该创建新的接口。一般命名方式是,新接口可以通过在原来的接口名称后追加一个数字‘2’来命名,以显示出它与现有接口的关系。然后通过接口继承来创建新接口。”
“可是,如果需求分析得不好,岂不是会出现一大堆的派生接口了?”我不免有点顾虑。
“这是肯定的,而且过于频繁地生成新接口,会因未使用的接口实现而使组件变得很庞大。有经验的设计师,在充分分析需求后,设计出的接口往往很小且相互独立,减少了性能问题发生的可能。”
“这种分解能力倒真的是艺术呀!”我不禁为之叹服。
“当然,一般来说,我们会把确定哪些属性和方法属于接口的设计过程称为‘接口分解’。基本准则是,应在一个接口中将紧密相关的几个功能聚合为一组。功能太多会使接口不便于运行,而过于细分功能将导致额外的系统开销并降低使用的简易性。掌握分解的这个度的能力是需要不断的在技术上进行磨炼,以及在对每个项目深入分析与理解的基础上才能得到的。”
“明白了。”我大声地回答着,真希望自己能早一天成为接口设计大师。
大李笑着拍了拍我:“明白了就好。其实,与设计接口相比,创建大的实现继承树更容易出错。”
“当然,”我脑海里浮现起实现继承的各个环节,“这是不是说,在某些时候适当使用接口还是很有益的。”
“看来你真的明白了,那你再来说一下,接口与类实现继承相比,好处有什么?”大李回过身开始找茶杯。
我低下头,努力地转动了一下脑子:“我试着说一下吧,总体而言,接口是一种非常有效的编程方法,它让对象的定义与实现分离,从而可以在不破坏现有应用程序的情况下使对象得以完善与进化。接口消除了实现继承的一个大问题,就是在对设计实施后再对其进行更改时很可能对代码产生破坏。即使实现继承允许类从基类继承实现,在类首次发布时仍然会使我们不得不为设计做很多的抉择。如果原有的设想不正确,并非总可以在以后的版本中对代码进行安全的更改。比如,我们定义了一个基类的方法,它需要一个 Integer 参数,而后来又确定该参数应该为 Long 数据类型。我们无法安全更改原始类,因为为从原始类派生的类所设计的应用程序可能无法进行正确编译。这一问题会扩大化,因为单个基类会影响几百个子类。”
“那用重载原始类并采用一个Long类型的参数,不就能解决这个问题了吗?”大李提了个问题。
“这个么?”我想了一下,“可是,这样不一定能达到满意的效果,因为一个派生类可能需要对采用整数的方法进行重写,如果取 Long 数据类型的方法不能被重写,该派生类可能无法正常运行。”
“那用接口怎么做?”大李不依不挠地继续问。
“办法就是发布接受新数据类型的更新接口。”我一下子就回答出来了。
“看来你已经掌握了接口操作的基本环节了。”大李的评语真让我高兴。“我再帮你总结一下,使用接口继承而不用类继承的主要原因有:在应用程序要求很多可能不相关的对象类型以提供某种功能的情况下,用接口适用性会更强;接口比基类更灵活,因为可以定义单个实现来实现多个接口;在无需从基类继承实现的情况下,接口更好;在无法使用类继承的情况下接口是很有用的。例如,结构无法从类继承,但它们可以实现接口。”
我抿着嘴用力点了点头,同时在心里默默地记忆着大李所说的每一条准则。
“回去好好想想,多写几个小程序来练习一下,明天我们还要欣赏VB.NET提供的强大的可视继承的表现呢。”
Henry的VB.NET之旅(十一)—可视继承
韩睿
闭关修练了一晚上VB.NET的继承程序的我,早上进入公司时自信满满。白经理看到我,问了一句:“学习得怎么样?对VB.NET是不是已经可以上手了?”我点了点头:“应该可以做点事了吧。”“好的,”白经理拍了拍我肩膀说,“再跟大李好好学习几天,会有机会的。”
得到领导的指示,我冲了杯咖啡,恭敬地走向大李,放在他的桌上。大李嘴角一翘,笑着说:“别这么客气,来,坐。让我看看你昨天继承学习得怎么样。今天我们来看一下VB.NET提供的新的继承方式,可视化继承。”
“可视化?是不是就是指在可视化编辑器里就可以实现的继承方式?”我跟着问了一句。
大李点头说:“当然,就是看得见的继承。一般指的就是对窗体的继承。”
我想了一下说:“在VB.NET中,窗体都已经是类的形式了,如果要实现窗体继承,是不是生成一个基类窗体,然后通过声明Inherits的方式在新窗体中实现对基类窗体的继承?”
“大体上是这样的,但今天我们来看一下怎么通过可视化编译环境来实现这个过程。首先,是建一个基类窗体。”大李一边说,一边打开VB.NET编译器,从菜单中选择从“文件”菜单中依次选择“新建”和“项目”,打开“新建项目”对话框。创建了一个名为 “可视继承”的 Windows 应用程序。
然后右击解决方案资源管理器中的“可视继承”项目节点并选择“属性”。在项目的属性中,将输出类型从“Windows 应用程序”更改为“类库”,然后单击“确定”。
在“工具箱”的“Windows 窗体”选项卡上将一个按钮添加到窗体中。命名为btnProtected。然后在它的“属性”窗口中,将 Text 属性设置为 “保护”,同时将 Modifiers 属性设置为 Protected。
接着大李双击 btnProtected 按钮,切换到代码窗口,书写了这样的代码:
Private Sub btnProtected_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles btnProtected.Click
MsgBox("protected的按钮")
End Sub
接着切换回设计窗口,添加了第二个按钮btnPrivate,并将 Text 属性设置为 “私有”, Modifiers 属性设置为 Private。
双击 btnPrivate 按钮,添加了如下代码:
Private Sub btnPrivate_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles btnPrivate.Click
MsgBox("private的按钮")
End Sub
最后从“生成”菜单中选择“生成”,在项目的bin文件夹内生成了一个“可视继承.dll”文件。
“是不是这样就可以完成基类窗体的建造了?”
“是呀。”大李手也没停地说,“我们再通过集成环境来建一个派生的窗体吧。”
大李从“文件”菜单中选择“添加项目”中的“新建项目”,创建了一个名为“派生窗体”的Windows 应用程序。右击项目节点,选择“添加”中的“继承的窗体”。
在弹出的“添加新项”对话框中,可以看到已经选定了“继承的窗体”,大李点击了“打开”。又弹出一个“继承选择器”对话框中,从 “可视继承.dll”中选择“Form1”作为要从其中继承的窗体,然后单击“确定”。
于是在“派生窗体”项目中创建了一个从 “可视继承”项目中的窗体Form1派生出的窗体,命名为“InheritanceForm”。
随即在Windows 窗体设计器中,派生窗体的继承而来的按钮左上角会带有一个箭头标志符号。
大李拖动了一下“保护”按钮,并任意地更改了一下按钮的大小。然后,转过头来对我说:“你来试试改变一下‘私有’按钮的大小吧。”我不以为然的接过鼠标,可是,任凭我如何点击,拖拽,它仍岿然不动。
“啊?出了什么问题?”我大吃一惊。
“自已想想喽。”大李开始品味我倒给他的咖啡了。
我开始在大脑里搜索起这两天学习的内容,慢慢地转过弯来,“私有”按钮在基类窗体内修饰符被大李设为“Private”,也就是说它在派生类中也不可被更改。我小心翼翼地向大李说明我的想法。
“没错,别怕说错嘛。”大李点了点头,“在窗体中的控件属性也会跟着修饰符的设置而决定派生类能否对其进行更改。这个按钮被设为了Private,它的大小与位置属性当然在派生类中也无法被更改喽。”
看到我若有所思地点着头,大李给我出了道题:“你来试试改变一下在派生窗体中,点击‘保护’按钮时弹出消息框的内容,就改为‘派生的保护按钮’吧。”
我跟大李在基类窗体中操作地一样,双击了该按钮,切换到代码窗口。写了这句代码:
Private Sub btnProtected_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles btnProtected.Click
MsgBox("派生的保护按钮")
End Sub
接着,我右击解决方案资源管理器中的 “派生窗体” 项目并选择“设为启动项目”。然后,右击解决方案资源管理器中的 “派生窗体”项目并选择“属性”。在“派生窗体 属性页”对话框中,将“启动对象”设置为继承的窗体InheritanceForm。
按 F5 键运行应用程序,满怀欣喜地点击了一下“保护”按钮。咦,结果弹出了两个消息框,第一个弹出的是基类窗体中写的按钮消息内容“Protected的按钮”,然后才弹出刚写的消息“派生的保护按钮”。我一下就愣住了,“我不是已经改写了click事件响应程序的代码了吗?为什么基类的消息没有屏蔽掉?”
大李悠悠然喝下一口咖啡,放下杯子。拍了拍我说:“屏蔽?你能屏蔽基类私有的成员吗?”
“啊?”我赶紧看了一下基类窗体中的click事件处理程序。“真的呢,是private sub!”我傻傻地笑起来。
“对于窗体编程而言,默认的是对自身的处理,没有考虑到对它的继承,所以控件的事件处理程序都是以private来修饰的,用于封装。但是修改也很容易,不是吗?”大李又拍了拍我,起身出门了。
我拼命地搓了搓脸,开始回忆了一下实现继承的方法,终于开始动手改程序。
首先,在基类窗体中写:
Protected Overridable Sub btnProtected_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnProtected.Click
MsgBox("protected的控件")
End Sub
我正准备到派生类中改写代码,大李又回来了,他站到我身后说:“你先别急着写,看一下代码窗口上方的两个下拉框,左边一个是类名,你选择一下InheritanceForm类的overrides,在右边的方法名称下拉框中就会出现可重写的方法的名称了。”
我按大李的话做,果然,在方法名称下拉框中看到了btnProtected_Click,点击一下,就会出现该方法的空白代码段,我往代码段里添加了一句消息框的内容:
Protected Overrides Sub btnProtected_Click(ByVal sender As Object, _
ByVal e As System.EventArgs)
MsgBox("派生的保护按钮")
End Sub
运行!点击一下“保护”按钮,果然只出现了一个消息框:“派生的保护按钮”。
Henry的VB.NET之旅(十二)—事件驱动
韩睿
“可视继承原来这么方便!”我心中默默地感叹一声。一会儿之后,我抬头扫了一眼屏幕上的代码后,突然发现了一个新问题,按钮的单击事件程序现在已经变成了:
VB.NET程序
VB程序
Private Sub btnProtected_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnProtected.Click
MsgBox("派生的保护按钮")
End Sub
Private Sub Command1_Click()
……
End Sub
而原来的VB程序是那么的简单。现在的参量Sender和e是干什么用的?那个Handles又是什么东东?还好有老师,我立刻起身,厚着脸把大李请过来,把疑问一一说给他听。
大李推了推眼镜,反问我道:“你应该对Windows编程中的事件驱动程序设计很熟悉吧?”(注:此处的Windows是指Win9X及其以上的操作系统)
“是呀!”看来反问是大李授课的开篇词了,但我也只好先往他的圈套里跳,“事件驱动就是说应用程序的执行流程是由外界发生的事件所确定的。也就是接受到任务才工作的模式。事件就是一个信号,它告知应用程序有重要情况要发生。实际上的执行情况是,各个应用程序把负责不同工作的对象在其运行期间送入Windows操作系统,让这些对象等待Windows产生的事件,然后加以处理。”
“VB程序员一般也只需要象你这样理解就行了。”大李的话真让我触动,“应该说是Windows先产生消息,应用程序中的窗口程序window procedure能接收来自windows的消息,并将其转化为事件,这个我们以后再说。现在来看看事件驱动程序的组成,主要是有事件、对象和事件处理程序三个要素。对象就是完成任务的主体,比如你说的Button1;事件么,就是对象要执行的任务,比如单击,就是click事件;那么事件处理程序就是Button1_Click这段程序了。”
“这个我知道呀!我只是想问一下Sender……”我疑惑地回应道。
大李哥摇摇手,打断了我的话。“如果你真正清楚事件驱动的话,就明白了。你看一下,Sender是什么类型的变量?”
“object呀!”我无奈地问道着,“但是,……”我隐约感觉到了什么。
大李微笑着说,“sender as object,就一语道破它的来源与用途。object是支持 .NET 框架类层次结构中的所有类,并为派生类提供低级别服务。这是 .NET 框架中所有类的最终超类;它是类型层次结构的根。一般来说,sender在形参中表示引发事件的源头,就是我刚才所说的三要素中的‘对象’。如果在控件引发的事件中写代码的话,一般都不需要再重新指派,因为它已经默认为是该控件了。当自己写代码来调用某事件程序时,就要注明sender是何物了。”
“也就是说,sender是提供给在事件处理程序代码内部或外部进行调用的吗?”我仍有点不明白。
“可以这样说,你难道看不出VB.NET提供给我们的是更全面、更直接的控制吗?再说e,表示的是事件数据,就是一个事件激发所需要的状态信息。在事件引发时不向事件处理程序传递状态信息的事件会将e设为Eventargs。如果事件处理程序需要状态信息,则应用程序必须从此类派生一个类来保存数据。比如Mousedown事件,系统需要判断mouse的位置、判断是左中右哪个键点击了、判断点击了几下等等,因此该e就必需是System.Windows.Forms.MouseEventArgs类的实例。”大李继续解说着。
“哦,也就是说e是和事件密切相关的喽?”我开始用实际操作来证实一下,我在代码窗口的“类名”下拉列表中选择了Button1,在其右边的“方法名称”下拉列表中选择了MosueDown方法,出现的就是Button1.MouseDown事件处理程序代码段:
Private Sub Button1_MouseDown(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles Button1.MouseDown
End Sub
果然,看到e的类型变为System.Windows.Forms.MouseEventArgs。我在代码段内写下e,然后在其后打一个点,出现了一个智能感知菜单(如图1所示):
与button1.click事件相比,button1.Mosuedown事件中的e的属性和方法多出了五个描述事件特征的属性,见表1所述。
Button
获取曾按下的是哪个鼠标按钮。
Clicks
获取按下并释放鼠标按钮的次数。
Delta
获取鼠标轮已转动的制动器数的有符号计数。制动器是鼠标轮的一个凹口。
X
获取鼠标单击的 x 坐标。
Y
获取鼠标单击的 y 坐标。
表1 多出的e的属性
“我明白了,那么Handles关键字是不是就是用于声明代码是要处理哪个事件的处理过程呢?”我终于开了点窍。
“是的,可不要小看Handles,好好利用它,你的程序会变得简洁和灵活。”大李边说边给我写了一段代码:
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _
Handles Button1.Click, Button2.Click
Select Case sender.name
Case "Button1"
MsgBox("你点击的是button1")
Case "Button2"
MsgBox("你点击的是button2")
End Select
End Sub
“在Handles后面写了两个事件的声明,就可以在同一段代码中对两个对象进行控制。另外,利用Sender和e我们还可以更方便地构建与调用自己的事件处理程序。“大李边写边说。
“是吗?怎么实现?”我不由被激发出了浓浓的兴趣。
Henry的VB.NET之旅(十三)—标准事件处理程序
韩睿
“感兴趣就好!”大李微微点点头,然后问我道,“你知道程序是怎么知道哪些对象可以引发何种事件,以及怎么样把事件和事件处理程序关联起来?”
见到我迷茫的眼神,大李用鼠标点击打开被Region合起来的“Windows 窗体设计器生成的代码”,指着中间的一句自动生成的代码:
Friend WithEvents Button1 As System.Windows.Forms.Button
“当你在设计窗口往窗体上添加了一个按钮后,会自动在代码中加入这句代码,Friend 关键字授予对一个或多个所声明的编程元素的友元访问权限。那么WithEvent是干什么的?”
我立刻在帮助中查找,有了:
WithEvents 关键字指示被声明的对象变量引用可以引发事件的类实例。
“明白了,”我也不能总傻听着,“在声明对象的时候用WithEvents来进行标识,再在事件处理程序中的过程声明结尾处使用 Handles 关键字将就可以处理由使用 WithEvents 关键字声明的对象变量所引发的事件了。”
“没错,你看来我写一段代码,使用的就是WithEvent-Handles的方法来处理事件。类名为CHenry吧,它内含一个事件EventHR。”
Module Module1
Public Class CHenry
Public Event EventHR() ' 声明一个事件
Sub CauseSomeEvent()
RaiseEvent EventHR() ' 引发事件
End Sub
End Class
WithEvents Obj As New CHenry() '模块或类级别的声明
Sub Obj_EventHR() Handles Obj.EventHR '在Handles之后声明事件
MsgBox("事件处理器捕捉到了事件.") '处理事件.
End Sub
Sub Main()
Obj.CauseSomeEvent() '调用对象去引发事件
End Sub
End Module
“你要注意的是怎么样通过RaiseEvent来引发事件,另外一个很简单的问题也需要注意的是,事件处理程序的命名一般是用‘对象名_事件名’的方式。”大李边写程序边指点说。
“事件的声明和引发是只能在一个层次内,还是可以在派生类里引发基类的事件?”我不由好奇地问。
“你开始学会思考了,”大李不知道是不是在嘲笑我,“VB.NET要求必须在声明事件的范围内引发事件。派生类不能引发从基类继承的事件,但是可以处理基类引发的事件。我们可以来看一个示例。”大李马上对刚写的代码进行了修改:
Module Module1
Public Class CHenry
Public Event EventHR() ' 声明一个事件
Sub CauseSomeEvent()
RaiseEvent EventHR() ' 引发事件
End Sub
End Class
Public Class Class2‘从Chenry派生而来的类
Inherits CHenry
Sub Obj_EventHR() Handles MyBase.EventHR
MsgBox("事件处理器捕捉到了事件.") '处理事件.
End Sub
End Class
WithEvents Obj As New Class2() '模块或类级别的声明
Sub Main()
Obj.CauseSomeEvent() '调用对象去引发事件
End Sub
End Module
“还记得我跟你说过MyBase的意义了吧?(注:详见《构造与析构》一篇)我们可以添加 Handles MyBase.<event name> 语句来声明派生类中的事件处理程序。问一个基本的问题,你看obj是Class2类的一个实例对吧?但为什么它也具有Chenry类才有的CauseSomeEvent方法?”大李说得我眼球都快掉下来了。
“当然是因为Class2是CHenry类的派生类,继承了它的方法呗。”我神情极为沮丧。
“哈哈,别急呀,在VB.NET中我们最常碰到的就是面向对象的问题。不断回顾有好处,温故而知新!”大李最后的微笑对我倒不啻为一句忠告。要学好VB.NET,我还真得好好复习一下面向对象的内容。
大李话题一转,淡淡地跟我说:“WithEvents 语句和 Handles 子句提供了标准的陈述性指定事件处理程序的方法。也就是如何把对象的事件和某一个事件处理程序进行关联。WithEvents 所声明对象引发的事件可以由任何过程用命名此事件的 Handles 子句来处理。换句话说,有Handles子句标识的事件处理程序也只能处理由WithEvents声明的对象。虽然 Handles 子句是关联事件与事件处理程序的标准方法,它仅限于在编译时关联事件与事件处理程序。还有一种方法可以允许在运行时动态地将事件与一个或更多的事件处理程序连接或者断开,而并不要求使用 WithEvents 来声明对象变量。”
“是吗?”我一下子从沉思中惊醒过来,好奇心又一次袭来。
Henry的VB.NET之旅(十四)—动态关联事件与处理程序
韩睿
“要解释新的事件处理程序的方法,我们需要先说几个重要的问题。”大李开始严肃起来,我也只得挪动一下身子,表现出正襟危坐的架势。
“我们首先讨论一下事件是怎么产生的。事件是对象发送的消息,以发信号通知操作的发生。操作可能是由用户交互,例如鼠标单击引起的,也可能是由某些其他的程序逻辑触发的。引发事件的对象叫做事件发送方(啊,听到这,Henry突然明白了事件处理程序中的第一个参量为什么叫Sender了,就是指事件发送的那个对象呀)。捕获事件并对其作出响应的对象叫做事件接收方。在事件通讯中,事件发送方类不知道哪个对象或方法将接收到它引发的事件。所需要的是在源和接收方之间存在一个媒介,或类似指针的机制。.NET 框架定义了一个特殊的类型Delegate,也就是委托,该类型提供函数指针的功能。”
“啊,我早就听说过委托的,就是不知道它是什么意思咧!”我做出期盼状。
“委托就是可用于调用其他对象方法的对象。与其他的类不同,委托类具有一个签名,并且它只能对与其签名匹配的方法进行引用。”大李继续说着。
““噢,委托也就是一个函数指针喽。”我好象理解了一点。
“差不多,委托可以等效于一个类型安全函数指针或一个回调。但不同于函数指针,Visual Basic.NET 委托是基于 System.Delegate 类的引用类型,它可以引用我们先前说过的共享方法(详见《共享成员》一篇)和实例方法。”
“明白了一点。”我一边点头一边说,“我们想要动态调用事件处理程序,是不是就要利用委托来声明是哪个程序用于处理事件呀?”
大李惊异地看着我,露出了几分赞许。
“委托是.NET中的一个重要的类型,我们以后还需要详细讨论。现在我们需要关注的就是如何来通过操作委托来实现将事件与事件处理程序动态联系起来。”大李接着就开始修改代码:
Module Module1
Public Class CHenry
Public Event EventHR() ' 声明一个事件
Sub CauseSomeEvent()
RaiseEvent EventHR() ' 引发事件
End Sub
End Class
Dim obj As New CHenry()
Sub Obj_EventHR() '在Handles之后声明事件
MsgBox("事件处理器捕捉到了事件.") '处理事件.
End Sub
Sub Main()
AddHandler obj.EventHR, AddressOf Obj_EventHR
obj.CauseSomeEvent() '调用对象去引发事件
End Sub
End Module
“看到有什么不同吗?”大李转过身来问我。
“主要有两个不同,首先是obj的定义不再用WithEvents来标识了,因此事件处理程序obj_EventHR()也就不能通过Handles关键字来声明事件,也就是说EventHR事件与事件处理程序没有用WithEvent-Handles进行关联;其次,是使用了AddHandle和Addressof……”说到这,我也没有词了,只能语焉不详。
“呵呵,我来帮你接着说。”大李拍了拍我的肩膀,接着说“先说Addressof吧,AddressOf 运算符创建的是一个指向指定的过程的过程委托。我们刚才说过委托相当于一个函数指针,那么AddressOf就是委托的操作符,通过它能得到委托的引用。”
见到我稍稍明白了,大李又接着说:“光看AddHandle能够将obj.EventHR事件与Obj_EventHR事件处理程序关联起来的作法,你肯定不了解我所说的动态关联好处在哪里。因为我没提到另一个方法RemoveHandler。它的使用方法和AddHandle是一样的,比如:
RemoveHandler obj.EventHR, AddressOf Obj_EventHR
你看,AddHandler 与 RemoveHandler 在一起就可以提供比 Handles 子句更大的灵活性,只要我们善于利用它们,就可以动态地添加、移除和更改与某事件关联的事件处理程序。而且比 Handles 要强大的是,AddHandler 允许将多个事件处理程序与单个事件进行关联。”
大李停了一停,接着说:“你要注意的一点就是AddressOf后面跟着的委托签名应该与相应的事件数据类相一致,我们看一个例子。”
AddHandler TextBox.MouseDown,AddressOf TextBoxMouseDownHandler
‘错误的示例1:
Private Sub TextBoxMouseDownHandler( )
End Sub
‘错误的示例2
Private Sub TextBoxMouseDownHandler(ByVal sender As Object, ByVal e As EventArgs)
End Sub
‘正确的示例:
Private Sub TextBoxMouseDownHandler(ByVal sender As Object, ByVal e As MouseEventArgs)
End Sub
“事件是一个文本框中的鼠标按下事件,我们不用自带的标准关联事件处理方法,而用AddHandler来实现,那么AddressOf之后相应的方法的参数声明,应该与MouseDown事件对应的事件的委托MouseEventhandler具有相同的签名,也就是参数声明上要保持一致,一个object变量,一个System.Windows.Forms.MouseEventArgs变量。”
我到这一步才算明白了个大概,事件处理程序可以通过AddHandler和RemoveHandler方法在我们需要的时候动态地建立或断开事件与事件处理程序的关联关系。可是,对于大李刚说的“事件的委托MouseEventhandler”,我还是不太理解。
Henry的VB.NET之旅(十五)—动态事件处理方法
韩睿
“大李哥,”我不免为自己问了如此多的问题而不好意思起来,“您刚才所说的事件的委托是怎么回事呀?”
大李显然是明白我会提问的:“我所说过事件驱动设计中的三要素你还记得吧?”
“当然,就是指对象、事件和事件处理程序。”我流利地回答道。
“对象和事件处理程序我们已经分析过了。要想彻底了解事件驱动程序的来龙去脉,我们必须了解事件的构成。”见到我又一次茫然地摇头,他就接着说,“事件功能是由三个互相联系的元素提供的:提供事件数据的类、事件委托和引发事件的类。我说过,事件就是一个信号,它告知应用程序有重要情况要发生。那么我们可以想象一下,事件要发生,就会含有独特的信息,比如,事件发送源是什么,会发生什么样的事件。提供事件数据的类就是用于记录这些信息的。该类必须从 System.EventArgs 派生出,这个我们已经说过了;事件的委托,这其实我也说过了,就好比是指向事件接收方的一个指针,由于指定对象的事件是独特的,所以它的委托也是事先定义好的,就比如MouseDown事件,它对应的委托就是MouseEventHandler。我们看一下你在代码窗口选Label1对象的MouseDown方法,会自动生成这段空的事件处理程序:
Friend WithEvents Label1 As System.Windows.Forms.Label
Private Sub Label1_MouseDown(ByVal sender As Object, ByVal e As _
System.Windows.Forms.MouseEventArgs) Handles Label1.MouseDown
End Sub
它其实就等同于:
AddHandler Label1.MouseDown, AddressOf Label1_MouseDown
Private Sub Label1_MouseDown(ByVal sender As Object, ByVal e As _
System.Windows.Forms.MouseEventArgs)
End Sub
隐含的意思,就是通过先前已经声明为全局变量的事件委托MouseEventHandler来实现事件处理程序参数的定义。对于MouseDown事件而言,为什么自动加上的事件数据类参量是MouseEventArgs类的,就是委托的作用了。
第三个要素是引发事件的类。该类必须提供事件的声明和引发事件的方法。其中引发事件的方法必须命名为On加上事件名的形式,比如事件是EventHR,那么引发事件的方法就一定叫OnEventHR。”
一边说着,大李开始修改起刚才用于标准事件处理程序的代码来:
Module Module1
'事件数据类
Public Class HenryEventArgs
Inherits EventArgs
End Class
'声明委托
Public Delegate Sub HenryEventHandler(ByVal sender As Object, _
ByVal e As HenryEventArgs)
Public Class CHenry
' 声明一个事件
Public Event EventHR As HenryEventHandler
Protected Overridable Sub OnEventHR(ByVal e As HenryEventArgs)
'调用委托
RaiseEvent EventHR(Me, e)
End Sub
Public Sub start()
Dim e As HenryEventArgs
OnEventHR(e)
End Sub
End Class
Dim obj As New CHenry()
Sub obj_EventHR(ByVal sender As Object, ByVal e As HenryEventArgs)
MsgBox("事件处理器捕捉到了事件.") '处理事件.
End Sub
Sub Main()
AddHandler obj.EventHR, AddressOf obj_EventHR
obj.start()
End Sub
End Module
大李指着写好的代码对我说:“你看,这段代码与先前的不同之处有不少。首先EventHR事件我定义为一个委托类型,不再是光秃秃的一个事件了。这样的话,EventHR事件就具有了承载事件信息的能力,事件发送源是Sender;事件数据是HenryEventArgs类的对象。其他的你应该都能很快就明白了,对吧?”
“嗯,还有一个问题,为什么引发事件的方法OnEventHR要用保护的方法呢?”我还是有些不解。
“这个问题问得好!”大李点了点头,“这是为了让派生类必须始终调用基类的OnEventHR方法以确保注册的委托接收到事件。你只要试一下,继承CHenry得到一个派生类进行事件处理,就会明白了。”
哦,一个事件驱动程序设计方法的讲述的确让我大开眼界,打开了一扇了解VB.NET的广阔天空的大门,继续努力,我还得加油呀!