Skip to content

5. 函数与过程

5.1. VBScript 的预定义函数

一种语言的丰富性很大程度上源于其函数库,这些函数可以封装在对象中,并以方法的形式存在。从这一角度来看,VBScript 可以说功能较为匮乏。

下表列出了 VBScript 中的非对象函数。我们在此不作详细说明。这些函数的名称通常能反映其作用。读者可查阅文档以获取特定函数的详细信息。

Abs
Array
Asc
Atn
CBool
CByte
CCur
CDate
CDbl
Chr
CInt
CLng
转换
Cos
CreateObject
CSng
日期
DateAdd
DateDiff
DatePart
DateSerial
DateValue
衍生数学
评估
Exp
Filter
FormatCurrency
FormatDateTime
FormatNumber
FormatPercent
GetLocale
GetObject
GetRef
十六进制
小时
InputBox
InStr
InStrRev
国际,固定
IsArray
IsDate
IsEmpty
IsNull
IsNumeric
IsObject
加入
LBound
LCase
长度
LoadPicture
日志
LTrim;RTrim;以及修边
数学
Minute
Month
MonthName
MsgBox
现在
10月
替换
RGB
Rnd
Round
ScriptEngine
ScriptEngineBuildVersion
ScriptEngineMajorVersion
ScriptEngineMinorVersion
第二
SetLocale
签名
Sin
空格
分隔
Sqr
StrComp
字符串
Tan
时间
Timer
TimeSerial
TimeValue
TypeName
UBound
UCase
VarType
工作日
WeekdayName

5.2. 模块化编程

描述一个问题的编程解决方案,就是描述计算机能够执行的一系列基本操作,这些操作能够解决该问题。根据编程语言的不同,这些基本操作的复杂程度各异。例如:

  • 从键盘或磁盘读取数据
  • 将数据输出到屏幕、打印机、磁盘等
  • 计算表达式
  • 在文件中移动
  • ……

描述一个复杂的问题可能需要数千条甚至更多的这类基本指令。因此,人类的大脑很难对一个程序有一个整体的把握。 鉴于难以全面把握问题,我们将其分解为更易解决的子问题。考虑以下问题:对通过键盘输入的数值列表进行排序,并在屏幕上显示排序后的列表。

首先,我们可以将解决方案描述为以下形式:

  début
        lire les valeurs et les mettre dans un tableau T
        trier le tableau T
        écrire les valeurs triées du tableau T à l'écran
  fin

我们将该问题分解为3个子问题,以便于解决。算法描述通常比前文更为规范,因此该算法可表述为:

   début
         lire_tableau(T)
         trier_tableau(T)
         écrire_tableau(T)
   fin

其中 T 代表一个数组。运算

    . lire_tableau(T)
    . trier_tableau(T)
    . écrire_tableau(T)

是必须通过基本操作来描述的非基本操作。这通常在所谓的模块中实现。数据 T 被称为模块的参数。它是调用程序传递给被调用模块的信息(输入参数),或是从被调用模块接收到的信息(输出参数)。 因此,模块的参数就是调用程序与被调用模块之间交换的信息。

模块 lire_tableau(T)
模块 trier_tableau(T)
模块 écrire_tableau(T)

模块 lire_tableau(T) 可描述如下:


début
    écrire "Tapez la suite de valeurs à trier sous la forme val1 val2 ... : "
    lire valeurs
    construire tableau T à partir de la chaîne valeurs
fin

至此,我们已对模块lire_tableau进行了充分的描述。事实上,这三个必要操作在VBScript中都有直接对应的实现。最后一个操作需要使用split函数。如果VBScript没有这个函数,那么操作3就必须进一步分解为在VBScript中具有直接对应实现的基本操作。

écrire_tableau(T)模块可描述如下:


début
    construire chaîne texte "valeur1,valeur2,...." à partir du tableau T
    écrire texte
fin

模块 écrire_tableau(T) 可描述如下(假设 T 中元素的索引从 0 开始):

début
      N<-- indice dernier élément du tableau T
      pour IFIN variant de N à 1
      faire
           //查找 T 中最大元素的索引 IMAX
           // IFIN 是 T 中最后一个元素的索引

          chercher_max(T, IFIN, IMAX) 

           // 将 T 中最大的元素与 T 的最后一个元素互换

          échanger (T, IMAX, IFIN)

      finfaire
FIN

此处算法再次使用了非基本操作:


 . chercher_max(T, IFIN, IMAX)
 . échanger(T, IMAX, IFIN)

chercher_max(T, IFIN, IMAX) 返回数组 T 中最大元素的索引 IMAX,该数组的最后一个元素的索引为 IFIN。

交换(T, IMAX, IFIN) 交换数组 T 中索引为 IMAX 和 IFIN 的两个元素。

因此,需要描述新的非基本操作。

模块 chercher_max(A, IFIN, IMAX)
   début
        IMAX<--0

        pour i variant de 1 à IFIN
        faire
           si T[i]>T[IMAX] alors
             début
                IMAX<--i
             fin
        finfaire
    fin
交换模块(T IMAX, IFIN)
  début
       temp<----T[IMAX]
       T[IMAX]<---T[IFIN]
       T[IFIN]<---temp
  fin

初始问题已通过VBScript的基本操作进行了完整描述,因此现在可以将其转换为该语言。 需要注意的是,基础操作在不同语言中可能存在差异,因此问题分析在某个阶段必须考虑所使用的编程语言。一种语言中存在的对象在另一种语言中可能不存在,从而导致所用算法的改变。因此,如果某种语言具备排序函数,那么在此处不使用它便是荒谬的。

此处应用的原则即所谓的“自顶向下分析法”。若将解决方案的框架表示出来,则如下所示:

这是一个树形结构。

5.3. VBScript 函数与过程

完成模块化分析后,程序员可将算法的各个模块转换为 VBScript 代码 fonctionsprocéduresfonctionsprocédures 都支持输入/输出参数,但 fonction 返回的结果允许在表达式中使用,而 procédure 则不返回此类结果。

5.3.1. VBScript 函数和过程的声明

VBScript 过程的声明如下

sub nomProcédure([Byref/Byval] param1, [Byref/Byval] param2, ...)
    instructions
end sub

而函数的声明如下

function nomFonction([Byref/Byval] param1, [Byref/Byval] param2, ...)
    instructions
end sub

为了返回结果,该函数必须包含一条将结果赋值给名为该函数的变量的语句:

函数名=结果

函数或子程序的执行有两种结束方式:

  1. 遇到函数结束语句(end function)或子程序结束语句(end sub)时
  2. 遇到函数退出语句(exit function)或子程序退出语句(exit sub)

对于函数,需注意在函数以 end function 或 exit function 结束之前,必须已将结果赋值给一个与函数同名的变量。

5.3.2. 函数或子程序的参数传递模式

在函数或子程序的输入/输出参数声明中,需指定调用程序向被调用程序传递参数的模式(byRef,byVal):

sub nomProcédure([Byref/Byval] param1, [Byref/Byval] param2, ...)

function nomFonction([Byref/Byval] param1, [Byref/Byval] param2, ...)

当未指定 byRefbyVal 传输模式时,将使用 byRef 模式。

实际参数、形式参数

假设有一个由以下代码定义的 VBScript 函数

function nomFonction([Byref/Byval] paramForm1, [Byref/Byval] paramForm2, ...)
...
end function

在函数或存储过程定义中使用的参数 parmamFormi 称为形式参数。可以通过以下语句从主程序或其他模块调用上述函数:

résultat=nomFonction(paramEff1, paramEff2, ...)

在调用函数或过程时使用的参数称为实际参数。当函数开始执行时,形式参数将接收相应实际参数的值。 关键字 byRefbyVal 用于确定这些值的传递模式。

按值传递模式 (byVal)

当形式参数指定此传递模式时,形式参数和实际参数即为两个不同的变量。 在函数或过程执行之前,实际参数的值会被复制到形式参数中。如果函数或过程在执行过程中修改了形式参数的值,这不会改变相应实际参数的值。这种传递方式非常适合函数或过程的输入参数。

引用传递模式 (byRef)

如果未指定参数的传递模式,则默认采用此传递模式。当形式参数指定此传递模式时,形式参数与相应的实际参数是同一个变量。因此,如果函数修改了形式参数,实际参数也会随之改变。此传递模式非常适合:

  • 输出参数,因为其值必须传递给调用程序
  • 需要费力复制输入参数的情况,例如数组

以下程序展示了参数传递的示例:

程序


Sub proc1(byval i, ByRef j, k)
  ' i 按值传递(byval)——此时实际参数与形式参数不同
  ' j 按值传递(byval)——此时实际参数与形式参数是相同的
  ' k 的传递模式未指定。默认情况下,它是按引用传递
  i=i+1
  j=j+1
  k=k+1
  affiche "dans proc1",i,j,k
End Sub

 Sub affiche(byval msg, ByVal i, ByVal j, ByVal k)
  ' 显示 i、j 和 k 的值
  wscript.echo msg & " i=" & i & " j=" & j & " k=" & k
End Sub


' ------------- 函数和过程调用

' 初始化 i 和 j
  i=4:j=5 : k=6

' 验证
  affiche "dans programme principal, avant l'appel à proc1 :",i,j,k

' 调用过程 proc1
  proc1 i,j,k

' 验证
  affiche "dans programme principal, après l'appel à proc1 :",i,j,k

' 结束
  wscript.quit 0

结果

dans programme principal, avant l'appel à proc1 : i=4 j=5 k=6
dans proc1 i=5 j=6 k=7
dans programme principal, après l'appel à proc1 : i=4 j=6 k=7

注释

  • 在 VBScript 脚本中,函数和过程没有特定的位置。它们可以出现在源代码的任何位置。通常,我们会将它们集中放在开头或结尾,并确保主程序构成一个连续的代码块。

5.3.3. 函数和过程的调用语法

假设有一个过程 p,其形式参数为 pf1、pf2、...

  • 对过程 p 的调用形式如下
p pe1, pe2, ...

参数周围不加圆括号

  • 如果过程 p 不接受任何参数,则可以随意使用调用 p 或 p(),以及声明 sub p 或 sub p()

设函数 f 接受形式参数 pf1、pf2、...

  • 对函数 f 的调用采用以下形式
résultat=f(pe1, pe2, ...)

参数周围的圆括号是必需的。如果函数 f 不接受任何参数,则可以无差别地使用调用 ff() 以及声明 function ffunction f()

  • 调用程序可以忽略函数 f 的返回值。此时,函数 f 被视为一个过程,并遵循过程的调用规则。因此,调用函数 f 时应写为 f pe1, pe2, ...(不带括号)。

如果该函数或过程是对象的方法,规则似乎会有所不同且不尽相同。

  • 因此,我们可以写成 MyFile.WriteLine "这是一个测试。" 或 MyFile.WriteLine("这是一个测试。")
  • 但虽然可以写成 wscript.echo 4,却不能写成 wscript.echo(4)。

我们将遵循以下规则:

  • 作为过程使用的过程或函数的参数周围不加括号
  • 函数参数周围应使用括号

5.3.4. 一些函数示例

以下是函数定义和使用的几个示例:

程序


 Function plusgrandque(byval i, ByVal j)
  ' 如果 i>j,则布尔值为真,否则为假

  ' 数据验证
  If isnumeric(i) And isnumeric(j) Then
    If i>j Then
      plusgrandque=true
    Else
      plusgrandque=false
    End If
  Else
    wscript.echo "Arguments (" & i & "," & j & ") erronés"
    plusgrandque=false
  End If
  Exit Function
End Function

 Function rendUnTableau(byval n)
  ' 返回一个包含 n 个元素的数组
  tableau=array()
  ' 验证参数 n 的有效性
  If isnumeric(n) And n>=1 Then
    ReDim Preserve tableau(n)
    For i= 0 To n-1
      tableau(i)=i
    Next
  Else
    wscript.echo "Argument [" & n & "] erroné"
  End If
  ' 返回结果
  rendUnTableau=tableau
End Function

 Function argumentsVariables(byref arguments)
  ' 参数是一个数字数组,返回其和
  somme=0
  For i=0 To ubound(arguments)
    somme=somme+arguments(i)
  Next
  argumentsVariables=somme
End Function

  ' 两个无参数函数,以两种不同方式声明
  Function sansParametres1
    sansParametres=4
  End Function

  Function sansParametres2()
    sansParametres=4
  End Function


' ------------- 函数和过程的调用

' 调用函数 plusgrandque
  wscript.echo "plusgrandque(10,6)=" & plusgrandque(10,6)
  wscript.echo "plusgrandque(6,10)=" & plusgrandque(6,10)
  wscript.echo "plusgrandque(6,6)=" & plusgrandque(6,6)
  wscript.echo "plusgrandque(6,'a')=" & plusgrandque(6,"a")

' 调用函数 rendUnTableau
  monTableau=rendUnTableau(10)
  For i=0 To ubound(monTableau)
    wscript.echo monTableau(i)
  Next
  monTableau=rendUnTableau(-6)
  For i=0 To ubound(monTableau)
    wscript.echo monTableau(i)
  Next

' 调用函数 argumentsVariables
  wscript.echo "somme=" & argumentsVariables(array(-1,2,7,8))
  wscript.echo "somme=" & argumentsVariables(array(-1,10,12))

' 无参数函数调用
  res=sansParametres1
  res=sansParametres1()
  sansParametres1
  sansParametres1()

  res=sansParametres2
  res=sansParametres2()
  sansParametres2
  sansParametres2()

' 结束
  wscript.quit 0

结果

plusgrandque(10,6)=Vrai
plusgrandque(6,10)=Faux
plusgrandque(6,6)=Faux
Arguments (6,a) erronés
plusgrandque(6,'a')=Faux
0
1
2
3
4
5
6
7
8
9

Argument [-6] erroné
somme=16
somme=21
somme=10

注释

  • 函数 rendUnTableau 表明,一个函数可以返回多个结果,而不仅仅是一个。只需将它们放入一个数组变量中,并将该变量作为结果返回即可。
  • 反之,函数 argumentsVariables 则说明可以编写一个接受可变数量参数的函数。同样,只需将参数放入一个数组变量中,并将该变量作为函数的参数即可。

5.3.5. 函数的输出参数或结果

假设应用程序分析表明需要一个具有输入参数 Ei 和输出参数 Sj 的模块 M。需要提醒的是,输入参数是调用程序提供给被调用程序的信息,而输出参数则是被调用程序提供给调用程序的信息。在 VBScript 中,针对输出参数有多种解决方案:

  • 如果只有一个输出参数,可以将其设为函数的返回值。这样就不再有输出参数,而仅仅是一个函数返回值。
  • 如果有 n 个输出参数,其中一个可以作为函数返回值,其余 n-1 个仍为输出参数。也可以不使用函数,而使用一个具有 n 个输出参数的存储过程。还可以使用一个函数,该函数将返回一个数组,其中包含要返回给调用程序的 n 个值。 需注意,被调用程序通常通过值传递方式将结果返回给调用程序。而当输出参数采用引用传递时,则可避免这种值复制操作。因此,采用引用传递方案能节省时间。

5.4. Vbscript数值排序程序

我们曾通过研究键盘输入数值排序的算法,开启了关于模块化编程的讨论。以下是该算法的VBScript语言实现:

程序

' 主程序
Option Explicit

Dim T        ' le tableau de valeurs à trier

' 读取值
T=lire_tableau

' 对值进行排序
trier_tableau T

' 显示排序后的值
ecrire_tableau T

' 结束
wscript.quit 0

' ---------- 函数与过程

' -------- lire_tableau
Function lire_tableau
     ' 请求值
    wscript.stdout.write "Tapez les valeurs à trier sous la forme val1 val2  ... valn : "
     ' 读取这些值
    Dim valeurs
    valeurs=wscript.stdin.readLine
     ' 将它们放入数组
    lire_tableau=split(valeurs," ")
End Function

' -------- ecrire_tableau
Sub ecrire_tableau(byref T)
     ' 显示数组 T 的内容
    wscript.echo join(T," ")
End Sub

' -------- trier_tableau
Sub trier_tableau (byref T)
     ' 将数组 T 按升序排序

     ' 查找数组 T 的最大索引 [0..ifin]
     ' 将 T[imax] 与数组 T[0..ifin] 的最后一个元素交换
     ' 随后,我们用一个元素少一个的数组重新开始

    Dim ifin, imax, temp
    For ifin=ubound(T) To 1 Step -1
         ' 查找数组 T[0..ifin] 的 imax 索引
        imax=chercher_max(T,ifin)
         ' 将该最大值与数组 T[0..ifin] 的最后一个元素互换
        temp=T(ifin):T(ifin)=T(imax):T(imax)=temp
    Next
End Sub

' -------- chercher_max
Function chercher_max(byRef T, ByVal ifin)
     ' 查找数组 T[0..ifin] 的最大索引
    Dim i, imax
    imax=0
    For i=1 To ifin
        If cdbl(T(i))>cdbl(T(imax)) Then imax=i
    Next

     ' 返回结果
    chercher_max=imax
End Function

结果

Tapez les valeurs à trier sous la forme val1 val2  ... valn : 10 9 8 7 6 1
1 6 7 8 9 10

注释:

  • 在初始算法中被识别出的模块 échanger 并未在此作为 VBScript 模块实现,因为其被认为过于简单,无需专门为此创建模块。

5.5. 模块化形式的程序 IMPOTS

我们重新编写了该税款计算程序,此次采用模块化形式

程序

' 计算纳税人的税额
' 该程序需通过三个参数调用:已婚 子女 工资
' 已婚:已婚用字母 O,未婚用字母 N
' 子女:子女数量
' 工资:年薪(不包含分)

' 变量声明
Option Explicit
Dim erreur

' 通过验证参数有效性来获取参数
Dim marie, enfants, salaire
erreur=getArguments(marie,enfants,salaire)
' 错误?
If erreur(0)<>0 Then wscript.echo erreur(1) : wscript.quit erreur(0)

' 获取计算税款所需的数据
Dim limites, coeffR, coeffN
getData limites,coeffR,coeffN

' 显示结果
wscript.echo "impôt=" & calculerImpot(marie,enfants,salaire,limites,coeffR,coeffN)

' 若无错误则退出
wscript.quit 0

' ------------ 函数与过程

' ----------- getArguments
Function getArguments(byref marie, ByRef enfants, ByRef salaire)
     ' 需从主程序中获取三个作为参数传递的值
     ' 参数在传递给程序时前后不带空格
     ' 将使用正则表达式来验证数据的有效性

     ' 返回一个包含两个值的错误数组变量
     ' error(0):错误代码,若无错误则为0
     ' 错误(1):若发生错误则返回错误信息,否则返回空字符串

    Dim syntaxe
    syntaxe= _
    "Syntaxe : pg marié enfants salaire" & vbCRLF & _
    "marié : caractère O si marié, N si non marié" & vbCRLF & _
    "enfants : nombre d'enfants (entier >=0)" & vbCRLF & _
    "salaire : salaire annuel sans les centimes (entier >=0)"

     ' 检查参数是否为3个
    Dim nbArguments
    nbArguments=wscript.arguments.count
    If nbArguments<>3 Then
         ' 错误信息
        getArguments= array(1,syntaxe & vbCRLF & vbCRLF & "erreur : nombre d'arguments incorrect")
         ' 结束
        Exit Function
    End If

    Dim modele, correspondances
    Set modele=new regexp

     ' 婚姻状况必须包含在字符范围内 oOnN
    modele.pattern="^[oOnN]$"
    Set correspondances=modele.execute(wscript.arguments(0))
    If correspondances.count=0 Then
         ' 错误信息
        getArguments=array(2,syntaxe & vbCRLF & vbCRLF & "erreur : argument marie incorrect")
         ' 退出
        Exit Function
    End If
     ' 获取值
    If lcase(wscript.arguments(0)) = "o"Then
        marie=true
    Else
        marie=false
    End If

     ' 子女数必须是 >=0 的整数
    modele.pattern="^\d{1,2}$"
    Set correspondances=modele.execute(wscript.arguments(1))
    If correspondances.count=0 Then
         ' 错误
        getArguments= array(3,syntaxe & vbCRLF & vbCRLF & "erreur : argument enfants incorrect")
         ' 退出
        Exit Function
    End If
     ' 获取值
    enfants=cint(wscript.arguments(1))

     ' 工资必须是大于等于0的整数
    modele.pattern="^\d{1,9}$"
    Set correspondances=modele.execute(wscript.arguments(2))
    If correspondances.count=0 Then
         ' 错误
        getArguments= array(4,syntaxe & vbCRLF & vbCRLF & "erreur : argument salaire incorrect")
         ' 退出
        Exit Function
    End If
     ' 获取值
    salaire=clng(wscript.arguments(2))

     ' 已正常完成
    getArguments=array(0,"")
End Function

' ----------- getData
Sub getData(byref limites, ByRef coeffR, ByRef coeffN)
     ' 在3个表格中定义计算税款所需的数据
    limites=array(12620,13190,15640,24740,31810,39970,48360, _
    55790,92970,127860,151250,172040,195000,0)
    coeffr=array(0,0.05,0.1,0.15,0.2,0.25,0.3,0.35,0.4,0.45, _
    0.5,0.55,0.6,0.65)
    coeffn=array(0,631,1290.5,2072.5,3309.5,4900,6898.5,9316.5, _
    12106,16754.5,23147.5,30710,39312,49062)
End Sub

' ----------- calculerImpot
Function calculerImpot(byval marie,ByVal enfants,ByVal salaire, ByRef limites, ByRef coeffR, ByRef coeffN)

     ' 计算份额数
    Dim nbParts
    If marie=true Then
        nbParts=(enfants/2)+2
    Else
        nbParts=(enfants/2)+1
    End If
    If enfants>=3 Then nbParts=nbParts+0.5

     ' 计算家庭分额和应税收入
    Dim revenu, qf
    revenu=0.72*salaire
    qf=revenu/nbParts

     ' 计算税额
    Dim i, impot
    i=0
    Do While i<ubound(limites) And qf>limites(i)
        i=i+1
    Loop
    calculerImpot=int(revenu*coeffr(i)-nbParts*coeffn(i))
End Function

评论

  • 函数 getArguments 用于获取纳税人的信息(配偶、子女、工资)。在此,这些信息作为参数传递给 VBScript 程序。如果情况发生变化,例如这些参数来自图形用户界面,则只需重写 getArguments 过程,无需修改其他部分。
  • 函数 getArguments 可以检测参数中的错误。当发生这种情况时,原本可以在函数 getArguments 中通过 wscript.quit 语句终止程序的执行。但在函数或存储过程中绝不能这样做。 如果函数或过程检测到错误,必须以某种方式将其报告给调用程序。是否终止执行应由调用程序决定,而非由该过程决定。在本例中,调用程序可能会决定要求用户重新输入错误的数据,而不是终止执行。
  • 在此,函数 getArguments 返回一个数组变量,其中第一个元素是错误代码(无错误时为 0),第二个元素是错误消息(若发生错误)。通过检查返回结果,调用程序即可判断是否发生错误。
  • 过程 getData 用于获取计算税款所需的数据。 此处这些数据直接在 getData 过程内定义。如果这些数据来自其他来源(例如文件或数据库),则只需重写 getData 过程,无需修改其他过程。
  • 无论数据通过何种方式获取,一旦所有数据均已获取,函数 calculerImpot 即可用于计算税款。
  • 因此值得注意的是,模块化编程允许在不同场景中(重复)使用某些模块。这一概念在过去的二十年里,在面向对象编程中得到了大力发展。