Thursday, August 12, 2004

 

VB.NET是怎样做到的

作者:Ninputer
出处:http://blog.joycode.com/ninputer/posts/19148.aspx

VB.net能够实现很多C#不能做到的功能,如When语句、Optional参数、局部Static变量、对象实例访问静态方法、Handles绑定事件、On Error处理异常、Object直接后期绑定等等。VB和C#同属.net的语言,编译出来的是同样的CIL,但为什么VB支持很多有趣的特性呢。我们一起来探究一下。

(一)局部静态变量

VB支持用Static关键字声明局部变量,这样在过程结束的时候可以保持变量的数值:

Public Sub Test1()

Static i As Integer

i += 1 '实现一个过程调用计数器

End Sub

我们实现了一个简单的过程计数器。每调用一次Test,计数器的数值就增加1。其实还有很多情况我们希望保持变量的数值。而C#的static是不能用在过程内部的。因此要实现过程计数器,我们必须声明一个类级别的变量。这样做明显不如VB好。因为无法防止其他过程修改计数器变量。这就和对象封装一个道理,本来应该是一个方法的局部变量,现在我要被迫把它独立出来,显然是不好的设计。那么VB是怎么生成局部静态变量的呢?将上述代码返汇编,我们可以清楚地看到在VB生成的CIL中,i不是作为局部变量,而是作为类的Field出现的:

.field private specialname int32 $STATIC$Test1$2001$i

也就是说,i被改名作为一个类的字段,但被冠以specialname。在代码中试图访问$STATIC$Test1$2001$i是不可能的,因为它不是一个有效的标识符。但是在IL中,将这个变量加一的代码却与一般的类字段完全一样,是通过ldfld加载的。我觉得这个方法十分聪明,把静态变量变成生命周期一样的类字段,但是又由编译器来控制访问的权限,让它成为一个局部变量。同时也解释了VB为什么要用两个不同的关键字来声明静态变量——Static和Shared。

由于局部静态变量的实质是类的字段,所以它和真正的局部变量还是有所不同的。比如在多线程条件下,对局部静态变量的访问就和访问字段相同。

(二)MyClass关键字

VB.net支持一项很有意思的功能——MyClass。大部分人使用MyClass可能仅限于调用本类其他构造函数时。其实MyClass可以产生一些很独特的用法。MyClass永远按类的成员为不可重写的状态进行调用,即当类的方法被重写后,用MyClass仍能得到自身的版本。下面这个例子和VB帮助中所举的例子非常相似

Public Class MyClassBase
Protected Overridable Sub Greeting()
Console.WriteLine("Hello form Base")
End Sub

Public Sub UseMe()
Me.Greeting()
End Sub

Public Sub UseMyClass()
MyClass.Greeting()
End Sub
End Class

Public Class MyClassSub
Inherits MyClassBase

Protected Overrides Sub Greeting()
Console.WriteLine("Hello form Sub")
End Sub
End Class

我们用一段代码来测试:

Dim o As MyClassBase = New MyClassSub()

o.UseMe()
o.UseMyClass()

结果是UseMe执行了子类的版本,而UseMyClass还是执行了基类本身的版本,尽管这是一个虚拟方法。观其IL,可以看到其简单的实现原理:

Me用的调用指令是callvirt

IL_0001: callvirt instance void App1.MyClassBase::Greeting()

而MyClass调用的是call

IL_0001: call instance void App1.MyClassBase::Greeting()

奇怪的是,如此简单的一个功能,我竟然无法用C#实现,C#怎样也不允许我按非虚函数的方式调用一个虚函数。C++可以用类名::方法名的方式访问自身版本的函数,但C#的类名只能用来访问静态的成员。这真是C#一个奇怪的限制。

(三)Handles和WithEvents

VB除了可以用C#那样的方法来处理事件响应以外,还有从VB5继承下来的独特的事件处理方式——WithEvents。

我喜欢称这种事件处理方式为静态的事件处理,书写响应事件的方法时就已经决定该方法响应的是哪一个事件,而C#则是在代码中绑定事件的。比如下面这个最简单的例子:

Public Class HandlerClass
Public WithEvents MyObj As EventClass


Private Sub MyObj_MyEvent(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyObj.MyEvent
MsgBox("hello")
End Sub

Public Sub New()
MyObj = New EventClass
End Sub
End Class

代码中用到的EventClass是这样的:

Public Class EventClass
Public Event MyEvent As EventHandler

Protected Overridable Sub OnMyEvent(ByVal e As EventArgs)
RaiseEvent MyEvent(Me, e)
End Sub

Public Sub Test()
OnMyEvent(New EventArgs)
End Sub
End Class

我们来复习一下,这段代码隐式地给EventClass编写了两个方法——Add_MyEvent(EventHandler)和Remove_MyEvent(EventHandler),实际上任何使用事件的上下文都是通过调用这两个方法来绑定事件和解除绑定的。C#还允许你书写自己的事件绑定/解除绑定的代码。

那么WithEvents是怎么工作的呢?VB.net的编译器在编译时自动将

Public WithEvents MyObj As EventClass

翻译成下面这个过程:

Private _MyObj As EventClass

Public Property MyObj() As EventClass
Get
Return _MyObj
End Get
Set(ByVal Value As EventClass)

If Not (Me._MyObj Is Nothing) Then
RemoveHandler _MyObj.MyEvent, AddressOf MyObj_MyEvent
End If

Me._MyObj = Value

If Me._MyObj Is Nothing Then Exit Property

AddHandler _MyObj.MyEvent, AddressOf MyObj_MyEvent

End Set
End Property

由此可见,当对WithEvents变量赋值的时候,会自动触发这个属性以绑定事件。我们所用的大部分事件响应都是1对1的,即一个过程响应一个事件,所以这种WithEvents静态方法是非常有用的,它可以显著增强代码可读性,同时也让VB.net中的事件处理非常方便,不像C#那样离开了窗体设计器就必须手工绑定事件。

不过在分析这段IL的时候,我也发现了VB.net在翻译时小小的问题,就是ldarg.0出现得过多,这是频繁使用Me或this的表现,所以我们在编码过程中一定要注意,除了使用到Me/this本身引用以外,使用它的成员时不要带上Me/this,比如Me.MyInt = 1就改成MyInt = 1,这样的小习惯会为你带来很大的性能收益。

(四)类型转换运算符

在Visual Basic 2005中将加入一个新的运算符——TryCast,相当于C#的as运算符。我一直希望VB有这样一个运算符。VB目前的类型转换运算符主要有CType和DirectCast。他们的用法几乎一样。我详细比较了一下这两个运算符,得出以下结论:

1、在转换成引用类型时,两者没有什么区别,都是直接调用castclass指令,除非重载了类型转换运算符CType。DirectCast运算符是不能重载的。

2、转换成值类型时,CType会调用VB指定的类型转换函数(如果有的话),比如将String转换为Int32时,就会自动调用VisualBasic.CompilerServices.IntegerType.FromString,而将Object转换为Int32则会调用FromObject。其他数值类型转换为Int32时,CType也会调用类型本身的转换方法实施转换。DirectCast运算符则很简单,直接将对象拆箱成所需类型。

所以在用于值类型时,CType没有DirectCast快速但可以支持更多的转换。在C#中,类型转换则为(type)运算符和as运算符。(type)运算符的工作方式与VB的DirectCast很相似,也是直接拆箱或castclass的,但是如果遇到支持的类型转换(如long到int),(type)运算符也会调用相应的转换方法,但不支持从String到int的转换。C#另一个运算符as则更加智能,它只要判断对象的运行实例能否转成目标类型,然后就可以省略castclass指令,直接按已知类型进行操作,而且编译器还可以自动对as进行优化,比如节省一个对象引用等。所以在将Object转换成所需的类型时,as是最佳选择。

由于as有很多优点,Visual Basic 2005将这一特性吸收了过来,用TryCast运算符就可以获得和as一样的效果,而且语法与DirectCast或CType一样。

(五)实现接口

VB.net采用的实现接口的语法是VB5发明的Implements,这个实现接口的语法在当今主流语言中独一无二。比如我有两个接口:

Interface Interface1
Sub Test()
End Interface

Interface Interface2
Sub Test()
End Interface

这两个接口有一个完全一样的成员Test。假设我需要用一个类同时实现两个接口会怎么样呢?先想想看,如果是Java,JScrip.NET这样的语言就只能用一个Test函数实现两个接口的Test成员。假如两个Test只是偶然重名,其内容必须要分别实现怎么办,于是一些解决接口重名的设计出现了……。在VB中,独特的Implements语句可以让你想怎么实现接口就怎么实现,比如下面的类Implementation用两个名字根本不一样的方法实现了两个接口。

Public Class Implementation
Implements Interface1, Interface2

Public Sub Hello() Implements Interface1.Test

End Sub

Private Sub Hi() Implements Interface2.Test

End Sub
End Class

也就是说,VB允许用任意名字的函数实现接口中的成员,而且访问器可以是任意的,比如想用Public还是Private都可以。

C#在处理重名成员上提供了显式实现(explicit implementation)的语法,其实现上述两个接口的语法为

public class Class1 : Interface1, Interface2
{
public Class1()
{
}
void Interface1.Test()
{
}

void Interface2.Test()
{
}

}

注意这里,C#只能用接口名.成员名的名字来命名实现方法,而且访问器只能是private,不能公开显式实现的方法。

在考察了IL以后,我发现.NET支持隐式实现和显式实现两种方式。其中隐式实现只要在类里面放一个与接口成员方法名字一样的方法即可——这一种VB不支持。而显式实现则在方法的描述信息里加入:

.override TestApp.Interface1::Test

无论是C#的显式实现还是VB的Implements语句都是这样的原理。也就是说.NET提供了换名实现接口成员的功能,但是只有VB将这个自由让给了用户,而其他语言还是采用了经典的语法。

(六)默认属性和属性参数

在原先的VB6里,有一项奇特的功能——默认属性。在VB6中,对象的名称可以直接表示该对象的默认属性。比如TextBox的默认属性是Text,所以下面的代码

Text1.Text = "Hello"

就可以简化为

Text1 = "Hello"

这种简化给VB带来了很多麻烦,赋值运算就需要两个关键字——Let和Set,结果属性过程也需要Let和Set两种。而且这种特征在后期绑定的时候仍能工作。到了VB.NET,这项功能被大大限制了,现在只有带参数的属性才可以作为默认属性。如

List1.Item(0) = "Hello"

可以简化为

List1(0) = "Hello"

这种语法让有默认属性的对象看起来像是一个数组。那么VB怎么判断一个属性是否是默认属性呢?看下列代码

Public Class PropTest
Public Property P1(ByVal index As Integer) As String
Get

End Get
Set(ByVal Value As String)

End Set
End Property

Default Public Property P2(ByVal index As Integer) As String
Get

End Get
Set(ByVal Value As String)

End Set
End Property
End Class

P1和P2两个属性基本上完全相同,唯一的不同是P2带有一个Default修饰符。反汇编这个类以后,可以发现两个属性完全相同,没有任何差异。但是PropTest类却被增加了一个自定义元属性System.Reflection.DefaultMemberAttribute。这个元属性指定的成员是InvokeMember所使用默认类型,也就是说后期绑定也可以使用默认属性。可是我试验将DefaultMember元属性手工添加到类型上却不能达到让某属性成为默认属性的功能。看来这项功能又是VB的一项“语法甜头”。但是,VB或C#的编译器对别人生成的类的默认属性应该只能通过DefaultMemberAttribute来判断,所以我将一个VB类只用DefaultMemberAttribute指定一个默认方法,不使用Default,然后将它编译以后给C#用,果然,C#将它识别为一个索引器(indexer)!

既然说到了C#的索引器,我们就顺便来研究一下VB和C#属性方面的不同。刚才的实验结果是VB的默认属性在C#中就是索引器。但是VB仍然可以用属性的语法来访问默认属性,而C#只能用数组的语法访问索引器。更特别的是,VB可以创建不是默认属性,但是带有参数的属性,如上面例子里的P1,而C#则不支持带参数的属性,如果将VB编写的,含有带参数属性的类给C#用,C#会提示“属性不受该语言支持,请用get_XXX和set_XXX的语法访问”。也就是说,带参数的属性是CLR的一项功能,但不符合CLS(通用语言规范),因此就会出现跨语言的障碍。这也更加深了我们对CLS的认识——如果你希望让你的代码跨语言工作,请一定要注意符合CLS。

(七)可选参数和按名传递

VB从4.0开始支持“可选参数”这一特性。就是说,函数或子程序的参数有些是可选的,调用的时候可以不输入。其实VB从1.0开始就有一些函数带有可选参数,只不过到了4.0才让用户自己开发这样的过程。在VB4里,可选参数可以不带默认值,而在VB.NET里,如果使用可选参数,则必须带有默认值。如

Public Sub TestOptional(Optional i As Integer = 1)

End Sub

调用的时候,既可以写成TestOptional(2),也可以写成TestOptional(),这种情况参数i自动等于1。如果过程有不止一个可选参数,则VB还提供一种简化操作的方法——按名传递参数。比如过程

Public Sub TestOptional(Optional i As Int32 = 1, Optional j As Int32 = 1, Optional k As Int32 = 1)

End Sub

如果只想指定k,让i和j使用默认值,就可以使用按名传递,如下

TestOptional(k := 2)

而且这种方式不受参数表顺序的限制

TestOptional(k := 2, i := 3, j := 5)

这些的确是相当方便的功能,C#就不支持上述两个特性。我们看看它是怎样在IL级别实现的。上述第一个方法在IL中的定义为

.method public instance void TestOptional([opt] int32 i) cil managed
{
.param [1] = int32(0x00000001)
.maxstack 8

可见,参数被加上了[opt]修饰符,而且.param指定了参数的默认值。这是只有VB能识别的内容,C#会跳过他们。在调用的时候,VB若发现参数被省略,则自动读取.param部分的默认值,并显式传递给过程。这一部分完全由编译器处理,而且没有任何性能损失,和手工传递所有参数是完全一样的。至于按名传递,VB会自动调整参数的顺序,其结果与传统方式的传递也没有任何的不同。这说明我们可以放心地使用这项便利。而且带有可选参数的过程拿到C#中,顶多变成不可选参数,也不会造成什么其他的麻烦。

PS.很多COM组件都使用了默认参数,而且有些过程的参数列表非常长,在VB里可以轻松地处理它们,而在C#中经常让开发者传参数传到吐血。

(八)On Error语句和When语句

本次讨论的是异常处理语句。VB.NET推荐使用Try...End Try块来进行结构化的异常处理,但是为了确保兼容性,它也从以前版本的BASIC中借鉴了On Error语句。其实On Error并不能算是VB的优点,因为使用它会破坏程序的结构,让带有异常处理的程序难以看懂和调试。但是我一直很惊叹于VB的工程师是怎样实现它的,因为On Error可以让异常的跳转变得很灵活,不像Try那样受到限制。首先看看Try是怎样实现的:

Public Function F1() As Integer
Try
Dim n As Integer = 2 \ n
Catch ex As Exception
MsgBox(ex.Message)
End Try
End Function

这是最简单的异常处理程序,通过Reflector反汇编(如果用ILDasm,不要选择“展开try-catch”),可以发现整个过程被翻译成19条指令。留意这一句:

.try L_0000 to L_0006 catch Exception L_0006 to L_0022

这就是典型的try块,在catch处直接指定要捕获的异常,然后指定catch区的位置,非常清晰。还要留意这两句:

L_0007: call ProjectData.SetProjectError

L_001b: call ProjectData.ClearProjectError

可以看出,这两句是在catch块的开头和末尾。深入这两个过程我发现它是在为Err对象记录异常。看来使用Err也是语法甜头,性能苦头,凭空添加了这两句(幸好都不太复杂)。

接下来我编写了一个与此功能类似的函数,用的是On语句处理异常:

Public Function F2() As Integer
On Error GoTo CATCHBLOCK
Dim n As Integer = 2 \ n
Exit Function
CATCHBLOCK:

MsgBox(Err.Description)

End Function

这不比上一个过程复杂,但是反汇编以后,它的IL代码竟然有47条指令,刚才才19条啊!最主要的改变是try部分,现在它是这样:

.try L_0000 to L_0022 filter L_0022 L_0036 to L_0060

注意,catch不见了,而出现了filter。我从没在C#生成的IL中见过filter。我查询了Meta Data一节的文档,filter大概能够进行一些过滤,满足一定条件才进入处理异常的块中,本例来说,L_0022指令开始就是过滤器,它是:

L_0022: isinst Exception
L_0027: brfalse.s L_0033
L_0029: ldloc.s V_4
L_002b: brfalse.s L_0033
L_002d: ldloc.3
L_002e: brtrue.s L_0033
L_0030: ldc.i4.1
L_0031: br.s L_0034
L_0033: ldc.i4.0
L_0034: endfilter

endfilter就是异常处理部分代码的开始。而L0030之前的代码是过滤器的判断部分,V_4和V_3是VB自己加入保存错误代码的变量。在整个反汇编中,我发现设计成处理异常部分的代码在IL里其实也是在try块中,也就是说程序的结构已经不是规整的try...catch块,产生异常的语句和处理异常的语句在一起,而真正处理异常的指令是一大堆繁冗拖沓的跳转语句。

下面看看我编写的第三个例子:

Public Function F3() As Integer
On Error Resume Next
Dim n As Integer = 2 \ n
End Function

这个值有2行的过程动用了VB强大的语法杀手——On Error Resume Next,它将忽略所有异常,让代码紧接产生异常的语句继续执行下去,猜猜这个功能产生了多少IL指令?答案是50条!比普通的On Error还要长。其实现我就不多说了,和前面的On语句差不多。不过50这个数字似乎提醒了大家,不要在程序里偷懒使用On Error处理异常,这样产生的代价是不可接受的。

最后一个例子是VB.NET的When语句,它可以实现对Catch部分的过滤:

Public Function F1() As Integer
Dim n As Integer = 0
Try
Dim m As Integer = 2 \ n
Catch ex As Exception When n = 0
MsgBox(ex.Message)
End Try
End Function

里面的When语句进行了对变量n的判断,仅当n = 0的时候才进入处理部分。听到“过滤”两个字,我们已经猜出,它是用try...filter来实现的。没错。这里的filter主要是进行ex是否是Exception型,n是否等于零等,当过滤成功,就会转移到异常处理段进行处理。这次VB生成的代码要比On Error语句规则得多,结构相当清晰。

本次我们还借助On Error语句和When语句了解到try filter结构,它是C#不能生成的,因此,我发现它不能被常见的反编译器反编译(因为反编译器的编写者只知道C#,呵呵)。而且用了On Error后程序结构变得异常混乱,这在产生负面作用的时候,是不是能够变相起到保护我们代码的作用呢?

(九)实例访问共享成员

大家都知道静态成员在VB中叫做共享成员,虽然刚接受起来有点别扭,但“共享成员”的确是名副其实的:

Public Class Class1
Public Shared i As Integer
'Other none-shared members
End Class

不但像在C#中那样,可以用Class1.i访问共享成员i,还可以用实例变量来访问:

Dim c1 As New Class1
c1.i = 100

就像i是c1的成员一样!当然只有一个i,任何实例去修改i的值都将导致所有i的值改变(因为其实只有一个)。甚至Me和MyClass也可以访问共享成员。

Me.i = 100
MyClass.i = 100

这在C#中是不可能做到的,一个纯正的C#程序员看到这些代码一定会觉得匪夷所思。为了揭示它的工作原理,我们可以做下列实验:

Dim c1 As Class1
c1.i = 100

注意,这里的c1为Nothing!,即使是Nothing的变量也可以访问共享成员,而且不会出错。接下来我们实验更极端的情况:

Dim o As Object = New Class1
o.i = 100

结果——失败,不能通过后期绑定访问共享成员。现在结果已经很明显,只有在VB明确了解对象类型的情况下,才能使用实例访问共享成员,VB会自动判断类型,然后将所有对共享成员访问的语句改写成

Class1.i = 100

这样的语法。Delphi也支持这一有趣的特征,而且李维在《Inside VCL》中将此说成Delphi.NET相对于.NET的扩展之一。

(按:虽然不怎么用,可看看也好。上学期有个用VB.Net升级串口通讯的小活儿,愣是没敢接,丢人。这个学期谈来谈去的VC活最终也黄啦,伤心呀,暑假Intern那里找呀???!!!)

(十) With语句

最近看到有人提到了VB特有的With语句,于是我就来研究一下。With语句是VB从Pascal中借鉴的语法,其作用就是多次使用同一个变量的成员。如下面对按钮的操作:

Button1.Text = "Button1"
Button1.Left = 100
Button1.ForeColor = Color.Blue

就可以简化为

With Button1
.Text = "Button1"
.Left = 100
.ForeColor = Color.Blue
End With

在早期的VB中,With语句就说比逐一使用其成员性能更好。那么在.NET中是不是也是这样?通过研究发现,With语句总是增加一个临时变量。比如上面那个例子中的With语句,就会产生如下效果:

Dim TempButton As Button
TempButton = Button1
TempButton.Text = "Button1"
TempButton.Left = 100
TempButton.ForeColor = Color.Blue

这说明,With语句用于大部分变量的时候不但没有增加性能,反而多增加了一个变量引用。因此,对于局部变量和类的普通字段,使用With是没有必要的。我记得网上有人提到过这个问题,还以次证明VB产生的代码效率不高。但是,这么说太绝对了,如果With语句应用在用窗体设计器添加的控件上,就会有效率的提升。为什么这么说呢?我在以前的《VB.NET是怎样做到的》讲解WithEvents原理的时候说过,由WithEvents定义的变量,其实质是一个属性,里面包含了对控件事件映射的代码。因此,若Button1那个变量是由窗体设计器生成的,包含WithEvents的变量,那么实际上需要通过窗体的属性来访问,就是说Button1.Text = “A”将被翻译成Me.get_Button1().set_Text(“A”)若对Button1的大量属性做这种操作,开销就比较大了。这时,如果用With语句,VB就会自动生成一个局部变量来接受Me.get_Button1()的结果,然后对其属性进行修改的语句全部都在这个局部变量上进行,需要修改的属性越多,这种修改的好处就越明显。

因此,我们得出的结论是,With语句通常不能增加效率,但是,在需要修改由窗体设计器生成的,带有WithEvents定义的控件的大量属性时,采用With可以获得性能收益。

(十一) 后期绑定
Visual Basic一大吸引人的地方是它支持直接的后期绑定。就是说可以直接在代码里访问那些要到运行时才能确定的对象成员。比如这样一段代码:

Dim f As Object = New Form2()
f.Show()

Object是没有Show方法的,但是这段代码可以正确运行。这个功能让VB有了很强的灵活性,它能够直接操作没有类型库的COM对象,而C#则很麻烦。在COM时代,VB的后期绑定是通过IDispatch实现的,而.NET时代,它是由Reflection实现的。为了揭开VB后期绑定的秘密,后面几篇就来讨论一下后期绑定的实现原理。

首先,对方法的调用是后期绑定最重要的一个环节,因为实现了后期绑定的方法调用,就能实现对属性的访问和对事件的操作,这基本上就是对象操作的全部内容。VB的编译器在发现后期绑定的调用后,会用Microsoft.VisualBasic.CompilerServices中的相关操作类实现后期绑定的方法调用。其中,LateBinding.InternalLateCall是这个操作的桥梁。我们来看看这个方法的实现。

这段代码是我从IL手工翻译来的,因为流行的反编译器都不能正确反编译VB的When语句。这段代码很可能有错,凑合着看吧:

[code]

DebuggerHidden()> _

Friend Function InternalLateCall( _

ByVal o As Object, _

ByVal objType As System.Type, _

ByVal name As String, _

ByVal args() As Object, _

ByVal paramnames() As String, _

ByVal CopyBack() As Boolean, _

ByVal IgnoreReturn As Boolean _

) As Object

'以下变量的名字已经被我安我的理解改变过

Dim binder As Microsoft.VisualBasic.CompilerServices.VBBinder

Dim flags As System.Reflection.BindingFlags

Dim result As Object

Dim correctIReflect As System.Reflection.IReflect

Dim members() As System.Reflection.MemberInfo

Dim argNumber As Integer

Dim firstMember As System.Reflection.MemberInfo

Dim firstMethodBase As System.Reflection.MethodBase

Dim params() As System.Reflection.ParameterInfo

Dim paramsNumber As Integer

Dim paramsInfo As System.Reflection.ParameterInfo

Dim paramsAttr() As Object



If IgnoreReturn Then

flags = &H4015D Or &H1000000

Else

flags = &H4015D

End If



If objType Is Nothing Then

If o Is Nothing Then

Throw ExceptionUtils.VbMakeException(91)

Else

objType = o.GetType()

End If

End If



correctIReflect = LateBinding.GetCorrectIReflect(o, objType)



If objType.IsCOMObject Then

LateBinding.CheckForClassExtendingCOMClass(objType)

End If



If name Is Nothing Then

name = ""

End If



binder = New VBBinder(CopyBack)



If Not objType.IsCOMObject() Then

'以下代码针对非COM对象



'通过Reflection获取指定名称成员

members = _

LateBinding.GetMethodsByName(correctIReflect, name, flags)



If members Is Nothing OrElse members.Length = 0 Then



Throw New System.MissingMemberException( _

Utils.GetResourceString( _

"MissingMember_MemberNotFoundOnType2", _

name, _

Utils.VBFriendlyName(objType, o)))



ElseIf LateBinding.MemberIsField(members) Then



Throw New System.MissingMemberException( _

Utils.GetResourceString( _

"ExpressionNotProcedure", _

name, _

Utils.VBFriendlyName(objType, o)))



End If



If members.Length = 1 Then

'member的长度为1,意味着只有一个重载

'因此下面的代码针对没有别名的情况



firstMember = members(0)

If firstMember.MemberType = 16 Then

'如果该成员是属性,则继续获取“GET”访问器的信息



firstMember = _

CType(firstMember, _

System.Reflection.PropertyInfo).GetGetMethod()



If firstMember Is Nothing Then



Throw New System.MissingMemberException( _

Utils.GetResourceString( _

"MissingMember_MemberNotFoundOnType2", _

name, _

Utils.VBFriendlyName(objType, o)))



End If

End If



'将成员确定为方法类型,准备调用

firstMethodBase = CType(firstMember, System.Reflection.MethodBase)

params = firstMethodBase.GetParameters()



argNumber = args.Length

paramsNumber = params.Length



If argNumber = paramsNumber Then

'没有任何缺省参数和参数数组的情况



Return LateBinding.FastCall( _

o, firstMethodBase, params, _

args, objType, correctIReflect)

Else

If CopyBack Is Nothing Then

If LateBinding.NoByrefs(params) Then

'没有按引用传递的参数

'判断最后一个参数是否是参数数组()

paramsInfo = params(paramsNumber - 1)

If paramsInfo.ParameterType.IsArray Then

'通过读取自定义Attribute来判断是否参数数组

paramsAttr = paramsInfo.GetCustomAttributes( _

GetType(System.ParamArrayAttribute), False)



If Not (paramsAttr Is Nothing _

OrElse paramsAttr.Length = 0) Then



'没有参数数组,继续用FastCall

Return LateBinding.FastCall( _

o, firstMethodBase, params, _

args, objType, correctIReflect)

End If

Else

'没有参数数组,继续用FastCall

Return LateBinding.FastCall( _

o, firstMethodBase, params, _

args, objType, correctIReflect)

End If

End If

End If

End If

End If

End If



'COM对象、有参数数组、有方法别名或者有ByRef参数需要回送值的情况

Try

'用VBBinder来进行所需的处理

result = binder.InvokeMember( _

name, flags, objType, correctIReflect, o, _

args, Nothing, Nothing, paramnames)



Exit Try

Catch misMember As System.MissingMemberException



Throw misMember



Catch ex As System.Exception When _

LateBinding.IsMissingMemberException(ex)



Throw New System.MissingMemberException( _

Utils.GetResourceString( _

"MissingMember_MemberNotFoundOnType2", _

name, _

Utils.VBFriendlyName(objType, o)))



Catch targetInv As _

System.Reflection.TargetInvocationException



Throw targetInv.InnerException



End Try



Return result

End Function
[/code]

关键部分我加了少量注释。总的来说,这段代码首先分解了传递的参数,通过反射从对象的类型中查找所需的方法。在调用的时候,它建立了一个VBBinder类的对象,VBBinder继承自System.Reflection.Binder类,是一个管理实参和型参结合的类。除了用VBBinder调用以外,我们还能看出,如果符合下列情况,将通过另一个方法LateBinding.FastCall来调用方法:

1、不是COM对象的方法

2、方法没有别名(没有重载)

3、没有使用参数数组和可选参数

4、没有按引用传递和接受返回参数的情况

FastCall运行速度可能要比VBBinder.InvokeMember快,它忽略别名和其它高级的用法。

后期绑定的桥梁方法InternalLateCall将通过以下条件进行判断:

1、不是COM方法
2、方法没有重载
3、传递的形参表和实参表完全匹配
4、如果不匹配,那么没有需要回传的ByRef参数和参数数组

符合上述条件,就采用LateBinding.FastCall来调用方法,否则采用一个VBBinder对象来处理。从这里我们可以看出VBBinder对象在后期绑定调用中能完成的任务,就是:

1、在多种重载中,查找正确的版本。
2、处理形参和实参结合的问题,包括类型转换、可选参数、参数数组等情况。
3、处理调用COM方法的事宜。

VBBinder是继承自Binder的一个类,而Binder是完成上述任务的抽象类。要实现一个Binder,必须重写BindToMethod、BindToField、SelectMethod、SelectProperty 和 ChangeType这四个抽象方法。其中,BindToMethod方法是后期绑定功能的重点。VB在实现的这个方法中,有非常严密而复杂的挑选和判断语句。我在这里就不贴出它的实现了,几乎都是长长的If语句和Select语句。总之,它的功能就是从多种候选的方法中,选出符合和传入参数的正确版本。

另一个重要的实现ChangeType方法,VB是借用内部针对Object类型的转换函数——CTypeHelper来实现的(看来VB为CType定义了很多新的功能)。而通过参数类型表来选择适当方法的SelectMethod,VB则没有实现它。VBBinder还有很多私有的工具方法,如GetDefaultMemberName就是获取表示默认属性的那个成员等等。

接下来我们看看实现方法调用的VBBinder.InvokeMember方法。它主要分成两个部分:

1、COM对象方法的调用。在COM对象方面,InvokeMember是通过调用包含正确信息IReflect对象(这个对象由InternalLateCall传入)的InvokeMember方法来实现调用的。有意思的是这里对COM对象的可选参数提供了支持,也就是说即使是后期绑定调用COM对象,VB便利的可选参数功能仍然得到支持。而在处理ByRef和需要回送参数值的情况时,VB首先通过RemotingServices.IsTransparentProxy来判断对象是否是一个跨越边界访问所用的透明代理。因为跨越边界访问(如Web Service)的方法调用是无法进行参数值回传的,所以对这种情况将自动取消接受参数值回传。此外由于对COM的访问可能会访问到非托管代码,所以,VB在正式调用前用SecurityPermission.Demand方法对调用堆栈中各级方法的权限进行了检查。如果试图通过后期绑定进行不安全的操作,就会抛出异常。

2、托管对象方法的调用。由于托管对象的方法可以重载,可以用缺省参数,还可以用参数数组等,所以这一部分的判断比较复杂。VB首先区分对象的默认属性/方法调用或一般方法调用,这里面判断的工具就是上面提到的工具方法。对于默认方法,VB将获取这个方法的信息,然后按照一般方法调用的步骤继续调用;对于一般方法调用,VB将寻找所有与调用方法同名的成员,然后通过上问提到的BindeToMethod方法找到正确的版本的MethodBase对象。最后,VB通过从该对象中获取的MethodInfo正式用反射调用对象。这中间还包括对静态方法和安全性的检查等复杂操作。

(十二)——数组的操作
VB6以前的数组是十分灵活的。而VB.NET在数组方面为了适应.NET Framework做了一些改变,但仍然有较强的灵活性。下面的代码展示了数组的常见操作。没有固定下标上界的数组如b()成为动态数组,而固定了上界的数组,如a(20)称为固定数组。

Dim a(10) As Integer
Dim b() As Integer = {1, 2, 3}
Dim c As Integer()
Dim d() As Integer
d = New Integer() {4, 5, 6}
Dim e(,) As Integer = {{1, 2}, {3, 4}}
Redim c(20)
Redim Preserve c(30)

我们不难看出,VB的数组定义语句可以将括号放在变量的后面,也可以放在类型的后面。如果希望用{}来初始化数组,那么数组就必须是动态的,不能指定下标上界。ReDim可以改变数组的任意一维的大小,但会清除数组的内容;ReDim Preserve只能改变数组最后一维的大小,并保留数组的内容。

首先看看动态数组。Dim a() As T() = {}语句先执行了数组的定义语句,然后它执行了和a = New T() {}语句一样的功能。如下代码所示:

a = New T() {1, 2, 3}

该语句执行的实际功能是

Dim tmp(2) As T
tmp(0) = 1
tmp(1) = 2
tmp(2) = 3
a = tmp

无论a是否是本地变量,都会建立一个临时的本地数组,然后赋值给a。如果a是一个类的字段,那么这样初始化带来的代价要小一些。这种语法和VB6的Array函数有些相似,但它是强类型的。

动态数组和固定数组都可以采用ReDim语句来重新指定下标的范围。ReDim语句在运行时实际创建了新的数组实例,如下所示(我不得不用C#,因为我无法用VB的语法表示它):

c = new int[21]; //ReDim c(20)

我们有时需要长度为0,但不是Nothing的数组,比如在反射中常常要处理没有参数的方法的参数表,就是这种类型的数组。C#中定义这种数组的语法是:

c = new int[0];

在VB中,实现上述功能的语法有点令人费解,它是:

ReDim c(-1)

不管怎么说,这个语法是有用的,每个VB的使用者都应该记住它。

最后我们看看ReDim Preserve语句。它可以更改一维数组的大小,也可以更改多维数组中最后一维的大小。但要注意的是,如果用它来改变不是最后一维的大小,出现的错误是运行时错误而不是编译错误。当VB的编译器遇到下列语句时

ReDim Preserve c(30)

它将自动被翻译成(C#,我无法用VB的语法表示它)

c = ((int[]) Utils.CopyArray(((Array) c), new int[31]));

其中Utils.CopyArray是Microsoft.VisualBasic.dll中的工具函数,我们看看它的实现方法。首先它判断是否正在使用ReDim Preserve修改最后一维的大小,代码是:

If (aryDest.Rank <> arySrc.Rank) Then
Throw ExceptionUtils.VbMakeException( _
New InvalidCastException( _
Utils.GetResourceString( _
"Array_RankMismatch")), 9)

End If
destRank = (aryDest.Rank - 2)
i = 0
Do While i <= destRank


If (aryDest.GetUpperBound <> _
arySrc.GetUpperBound) Then
Throw ExceptionUtils.VbMakeException( _
New ArrayTypeMismatchException( _
Utils.GetResourceString( _
"Array_TypeMismatch")), 9)

End If
i += 1
Loop

我们可以很清晰的看到,上述代码确认目标数组和源数组有相同的维数,以及除了最后一维一外有相同的下标上界。确认完毕后,调用下列代码来执行数组的复制:

If (arySrc.Rank > 1) Then '多维数组
srcRank = arySrc.Rank
lenSrcR = arySrc.GetLength((srcRank - 1))
lenDestR = aryDest.GetLength((srcRank - 1))
If (lenDestR = 0) Then
Return aryDest
End If
minLen = Math.Min(lenSrcR, lenDestR)
m = ((arySrc.Length / lenSrcR) - 1)
j = 0
Do While (j <= m)
Array.Copy( _
arySrc, (j * lenSrcR), aryDest, (j * lenDestR), minLen)
j += 1

Loop

Else '一维数组
Array.Copy(arySrc, aryDest, lenSrc)

End If

很清晰的看出,它是用Array.Copy对数组进行复制的。至此,我们就完全搞清楚ReDim Preserve的原理了。

5:46 PM | 评论 (0)

Thursday, May 06, 2004 #

开发兼容VB6的窗体图形方法
最近升级一些我以前写的VB6小工程,发现最大的阻碍之一是VB6的图形方法,如PSet、Circle、Line、CurrentX等方法和属性。他们根本无法自动升级,而以前的处理方法换用VB.NET的GDI+方法非常费时(要不然就不会不给自动升级了)。我想所有用过升级向导的人都会头疼图形方法的升级,因为原来VB的图形方法实在是很方便,许多效果简简单单就能画出来了。所以,我从现在起将计划一个让VB.NET尽量兼容VB6图形方法的项目,让所有图形方法都能用最接近的语法表示,这样就能帮助那些多少使用了图形的人更容易的实施升级。

首先将实现基于窗体的图形方法,以后计划实现基于PictureBox和Printer的图形方法。

10:51 PM | 评论 (0)

Saturday, May 01, 2004 #

五一节快乐 & BASIC 40岁生日
BASIC是1964年5月1日诞生在这个世界上的,今天就是她40岁生日。让我们一起祝她生日快乐。看看这片纪念BASIC生日的文章。同时祝大家五一节快乐!

12:05 AM | 评论 (3)

Friday, April 30, 2004 #

实现一个非零下标数组
最近VB FAQ提到了一个问题——VB.NET支持非零下标数组吗?答案是不支持。即使VB2005有这样的语法Dim a(0 To 10) As String,那个“0”也不能是别的数字。CLR在某种程度上支持非零下标数组,但C#这样有C传统的语言一开始就设计成只支持零下标,如果VB支持非零下标数组,各种语言的交互将会变得困难。关于这个问题,详细讨论请见这里。在FAQ中,另一篇帖子讨论了解决方法,就是用一个类模拟数组的行为,用默认属性(或索引器,C#)来模拟数组的下标。但是原文提到,没有泛型的情况下,我们需要为每一种类型创建一个非零下标的数组类。现在我们不但有了泛型,还有运算符重载,可以做一个更加清晰、漂亮的非零下标数组类。在动手以前,我发现C#还支持迭代器,可以令我的类更加方便,于是就用了C#,后面给出VB调用它的例子。

///
/// 表示一个下标可以不是零的数组
///

/// 数组元素的类型
public class VBArray : IEnumerable
{
private T[] arr;
private int _LBound;

public int LBound
{
get
{
return _LBound;
}
}

public int UBound
{
get
{
return _LBound + arr.Length - 1;
}
}

public VBArray(int LBound, int UBound)
{
if (UBound < LBound)
{
throw new ArgumentException("上界不能小于下界");
}
_LBound = LBound;
arr = new T[UBound - LBound + 1];
}

public T this[int index]
{
get
{
return arr[index - _LBound];
}
set
{
arr[index - _LBound] = value;
}
}

public void ReDim(int UBound)
{
if (UBound < _LBound)
{
throw new ArgumentException("上界不能小于下界");
}
arr = new T[UBound - _LBound + 1];
}

public static implicit operator T[](VBArray vbarr)
{
return vbarr.arr;
}

public IEnumerator GetEnumerator()
{
for (int i = 0; i < arr.Length; i++)
{
yield return arr[i];
}
}

}

以下代码为VB调用该类的例子。

Dim o As New VBArray(Of Integer)(3, 15)

o(3) = 5
o(4) = 6
o(10) = 10
o(15) = -1

MsgBox(o(10))

For Each x As Int32 In o
MsgBox
Next

通过重载的运算符,可以将VBArray转换为普通的数组,这样就可以在需要操作数组的上下文使用VBArray中的数值。



<< Home

This page is powered by Blogger. Isn't yours?