客户端与进程内部件共享地址空间,因此,在调用进程内部件的方法时能够使用客户端的栈来传递参数。这对进程外部件就不可行了,方法的参数必须在两个进程之间跨越边界传递。这就叫做调度。
客户端和进程外部件通过 proxy/stub 机制进行通讯,如图 8.14 所示。proxy 和 stub 负责正向调度和反向调度部件方法的参数;它们对客户端是完全透明的。
图 8.14 客户端与进程外部件
调度比在进程内传递参数要慢,当实际参数通过引用传递时尤其是这样的。由于不能将一个指针直接传给另一个地址空间,因此必须调度一份副本给部件的地址空间。当方法完成后,还得将该数据复制回来。
如果考虑调度的问题,某些方法参数在进程外部件中应该声明为 ByVal,而在进程内部件中则应该声明为 ByRef,如下所述。
在声明由进程外部件提供的对象的方法时,总是使用 ByVal 来声明存放对象引用的参数。
这是由于,如果对象引用的跨进程调度是单向传递,则开销明显要少。用 ByRef 来定义一个参数则意味着不仅对象引用必须调度给部件,在方法结束之后还要将其返回给客户端。
只有在需要用另一个对象的引用来代替客户端的对象引用的时候,才需要在声明存放对象引用的参数时使用 ByRef。
如果所声明的方法要用到客户端的对象的一个属性值,可以将该参数定义为该属性的数据类型而不是该对象类。调度对象引用比调度简单数据类型所需的开销明显要多很多。
如果参数是用于传递大的字符串或者 Variant 数组,则在 Visual Basic 过程中经常用 ByRef 来声明这些参数,即使该过程并不对这个参数做任何改动。与传递一个四字节的指针相比,复制整个的字符串或者数组比传递副本的指针要慢得多。
在自己进程的地址空间内(指在自己的程序或进程内部件中)总是这样处理的。因为得到该参数的方法可以使用指针直接访问该数据。
跨进程的调度打破了这种惯例。ByRef 的方法参数的数据首先被复制到部件的地址空间,然后将指向这份本地数据副本的指针传递给方法。
方法使用本地副本的指针进行修改。当方法结束后,所有的 ByRef 参数的数据都被复制回客户端的地址空间。因此,在每次方法调用中,用 ByRef 定义的一个参数要跨越进程两次。
如果希望用户将大字符串或 Variant 数组传递给部件中的方法,并且该方法不会改变该数据,那么对进程外部件就用 ByVal 声明该参数,对进程内部件则用 ByRef 进行声明。
重点 如果在进程外部件中将方法的参数声明为 ByRef,使用该部件的开发者就无法避免调度的影响,无论在该参数两边加上圆括号,还是在调用方法时使用 ByVal 关键词都不会起什么作用。在这种情况下 Visual Basic 将复制该数据,但由于自动化无法知道该数据是复制的,它会在进程间来回调度这份副本。因此该数据将复制三次。
属性使对象易于使用,但进程外部件的属性设置可能会很慢。在客户端使用进程外部件时,由于跨进程调度的额外开销,调用带五个参数的方法要快于先设置五个属性,然后调用方法。
若部件的用户要频繁地在调用某个方法之前设置一组属性,则可以在方法中添加一组可选的参数,每个参数对应一个属性。如果提供了某项参数,方法会在处理前先设置相应的属性。
如果为可选的参数提供了要设置的属性名,那么这些参数可以用作命名的参数(named parameters,或 named arguments),如下面的代码段所示:
'
假想的Widget
类的代码段。Public Load As Double
Public Torque As Double
'...
其它属性的声明...
Public Sub Spin(ByVal Iterations As Long, _
Optional Load As Variant, _
Optional Torque As Variant)
'
在旋转Widget
之前,用提供的参数设置'
属性的值。使用自引用对象Me
'
可以区分参数和属性。If Not IsMissing(Load) Then Me.Load = Load
If Not IsMissing(Torque) Then Me.Torque = Torque
'...
旋转Widget
的代码...
End Sub
'
客户端的代码,调用Spin
方法'
将该Widget
旋转10000
次。注意使用'
命名的参数来设置Torque
的用法。Dim wdgNew As New Widget
wdgNew.Spin 10000, Torque:=27.6