4. 错误处理
在编程中有一条铁律:程序绝不能无故“崩溃”。程序运行过程中可能发生的任何错误都必须得到处理,并生成有意义的错误信息。
如果我们回顾之前处理的税款示例,当用户在子女数量字段中输入任意内容时会发生什么?让我们来看这个示例:
这就是所谓的“意外崩溃”。在语句 <span style="color: #000000">children=cint(wscript.arguments(1)) 处发生了“崩溃”,因为 arguments(1) 包含字符串“xyzt”。
在使用性质不明的变量之前,必须先验证其确切的子类型。可以通过以下几种方式实现:
- 使用函数 vartype 或 typename 测试变量中数据的实际类型
- 使用正则表达式验证变量内容是否符合特定模式
- 允许错误发生,随后进行捕获并处理
我们将探讨这些不同的方法。
4.1. 确定数据的精确类型
需要提醒的是,函数 vartype 或 varname 可用于确定数据的精确类型。但这并不总能提供实质性帮助。 例如,当我们读取键盘输入的数据时,函数 vartype 和 typename 会告诉我们这是字符串,因为所有键盘输入的数据都被视为字符串。但这并不能告诉我们该字符串是否可以被视为有效的数字。 因此,我们需要使用其他函数来获取此类信息:
isNumeric(表达式) | 如果表达式可作为数字使用,则返回 true |
isDate(表达式) | 如果表达式可作为日期使用,则返回真 |
isEmpty(var) | 如果变量 var 未被初始化,则返回 true |
isNull(var) | 如果变量 var 包含无效数据,则返回 true |
isArray(var) | 如果 var 是数组,则返回 true |
isObject(var) | 如果 var 是对象,则返回 true |
以下示例要求用户通过键盘输入数据,直到该数据被识别为数字:
程序
' 读取数据直至被识别为数字
Option Explicit
Dim fini, nombre
' 在输入数据不正确时循环
' 循环由一个初始值为假的布尔变量控制(表示未结束)
fini=false
Do While Not fini
' 请求输入数字
wscript.stdout.write "Tapez un nombre : "
' 读取该数值
nombre=wscript.stdin.readLine
' 读取时类型必然为字符串
wscript.echo "Type de la donnée lue : " & typename(nombre) & "," & vartype(nombre)
' 检测读取数据的实际类型
If isNumeric(nombre) Then
fini=true
Else
wscript.echo "Erreur, vous n'avez pas tapé un nombre. Recommencez svp..."
End If
Loop
' 确认
wscript.echo "Merci pour le nombre " & nombre
' 结束
wscript.quit 0
结果
函数 isNumeric 无法判断表达式是否为整数。要获取此信息,需要进行额外测试。以下示例要求输入一个大于 0 的整数:
程序
' 读取数据,直到该数据被识别为大于0的整数
Option Explicit
Dim fini, nombre
' 只要输入的数据不正确,就循环
' 循环由一个初始值为假的布尔变量控制(表示未结束)
fini=false
Do While Not fini
' 请求输入数字
wscript.stdout.write "Tapez un nombre entier >0: "
' 读取该数值
nombre=wscript.stdin.readLine
' 检测读取数据的实数类型
If isNumeric(nombre) Then
' 是否为正整数(即数值等于其整数部分)?
If (nombre-int(nombre))=0 And nombre>0 Then
fini=true
End If
End If
' 可能的错误信息
If Not fini Then wscript.echo "Erreur, vous n'avez pas tapé un nombre entier >0. Recommencez svp..."
Loop
' 确认
wscript.echo "Merci pour le nombre entier >0 : " & nombre
' 结束
wscript.quit 0
结果
注释:
- int(nombre) 返回一个数的整数部分。一个数等于其整数部分即为整数。
- 值得注意的是,我们不得不使用条件判断 If (数字 - int(数字)) = 0 且 数字 > 0,因为条件判断 If 数字 = int(数字) 且 数字 > 0 并未产生预期结果。它无法识别正整数。具体原因请读者自行探究。
- If (数字 - int(数字)) = 0 这一判断并非完全可靠。让我们来看以下运行示例:
实数并非以精确值表示,而是以近似值表示。在此,运算 nombre-int(nombre) 得出的结果为 0,其精度取决于计算机的计算能力。
4.2. 正则表达式
正则表达式允许我们验证字符串的格式。因此,我们可以检查表示日期的字符串是否符合 dd/mm/yy 格式。为此,我们使用一个模式并将字符串与该模式进行比较。因此,在此示例中,j、m 和 a 必须是数字。 因此,有效的日期格式模式为“\d\d/\d\d/\d\d”,其中符号 \d 表示一个数字。模式中可使用的符号如下(微软文档):
字符 | 描述 |
\ | 将后续字符标记为特殊字符或字面字符。例如,“n”表示字符“n”。“\n”表示换行符。字符序列“\\”表示“\”,而“\("表示“(”。 |
^ | 表示输入的开头。 |
$ | 表示输入的结尾。 |
* | 表示前一个字符零次或多次出现。因此,“zo*”匹配“z”或“zoo”。 |
+ | 匹配前一个字符一次或多次。因此,“zo+”匹配“zoo”,但不匹配“z”。 |
? | 匹配前一个字符零次或一次。例如,“a?ve?”匹配“lever”中的“ve”。 |
. | 匹配除换行符以外的任何单个字符。 |
(模式) | 搜索 modèle 并记录匹配结果。可以通过 Item [0]...[n] 从获得的 Matches 集合中提取匹配的子字符串。 若要查找包含括号 ( ) 内的字符的匹配项,请使用 "\(" 或 "\)"。 |
x|y | 匹配 x 或 y。例如,“z|foot”匹配“z”或“foot”。“(z|f)oo”匹配“zoo”或“foo”。 |
{n} | n 是一个非负整数。它精确匹配 n 倍的该字符。例如,“o{2}”不匹配“Bob,”中的“o”,而是匹配“fooooot”中的前两个“o”。 |
{n,} | n 是一个非负整数。匹配至少 n 倍的该字符。 例如,“o{2,}”不匹配“Bob”中的“o”,而是匹配“fooooot”中的所有“o”。“o{1,}”等同于“o+”,而“o{0,}”等同于“o*”。 |
{n,m} | m 和 n 是非负整数。 表示该字符出现次数不少于 n 次,且不超过 m 次。例如,“o{1,3}”匹配“foooooot”中的前三个“o”,而“o{0,1}”等同于“o?”。 |
[xyz] | 字符集。匹配所列出的任意一个字符。例如,“[abc]”匹配“plat”中的“a”。 |
[^xyz] | 否定字符集。匹配所有未列出的字符。例如,“[^abc]”匹配“plat”中的“p”。 |
[a-z] | 字符范围。匹配指定字符序列中的任何字符。例如,“[a-z]”匹配“a”到“z”之间的所有小写字母。 |
[^m-z] | 负字符集。匹配指定字符集中的所有非字符。例如,“[^m-z]”匹配“m”和“z”之间的所有非字符。 |
\b | 匹配表示单词的边界,即单词与空格之间的位置。例如,“er\b”匹配“lever”中的“er”,但不匹配“verbe”中的“er”。 |
\B | 匹配不代表单词的边界。“en*t\B”匹配“bien entendu”中的“ent”。 |
\d | 表示一个数字字符。等同于 [0-9]。 |
\D | 匹配不代表数字的字符。等同于 [^0-9]。 |
\f | 对应于换行符。 |
\n | 表示换行符。 |
\r | 对应回车符。 |
\s | 表示任何空白字符,包括空格、制表符、分页符等。等同于“[ \f\n\r\t\v]”。 |
\S | 匹配任何非空白字符。等同于“[^ \f\n\r\t\v]”。 |
\t | 匹配一个制表符。 |
\v | 匹配垂直制表符。 |
\w | 匹配任何代表单词的字符,包括下划线。等同于“[A-Za-z0-9_]”。 |
\W | 匹配任何不代表单词的字符。等同于“[^A-Za-z0-9_]”。 |
\num | 匹配 num,其中 num 是一个正整数。指代存储的匹配项。例如,"(.)\1" 匹配两个连续的相同字符。 |
\n | 对应于 n,其中 n 是一个八进制转义值。八进制转义值必须包含 1、2 或 3 个数字。例如,“\11”和“\011”都对应于一个制表符。 "\0011" 等同于 "\001" & "1"。八进制转义值不得超过 256。如果超过,则表达式中仅考虑前两位数字。允许在正则表达式中使用 ASCII 代码。 |
\xn | 等同于 n,其中 n 是一个十六进制转义值。 十六进制转义值必须包含两个数字。例如,“\x41”对应“A”。“\x041”等同于“\x04”和“1”。允许在正则表达式中使用代码 ASCII。 |
模板中的一个元素可以出现一次或多次。下面通过几个关于 \d 符号的示例来说明,该符号代表 1 个数字:
模板 | 含义 |
\d | 一个数字 |
\d? | 0 或 1 个数字 |
\d* | 0 个或更多数字 |
\d+ | 1 个或多个数字 |
\d{2} | 2个数字 |
\d{3,} | 至少 3 个数字 |
\d{5,7} | 5 到 7 位数字 |
现在设想一个能够描述字符串预期格式的模式:
要查找的字符串 | 模式 |
日期格式为 dd/mm/yy | \d{2}/\d{2}/\d{2} |
时长格式为 hh:mm:ss | \d{2}:\d{2}:\d{2} |
一个无符号整数 | \d+ |
一个可能为空的空格序列 | \s* |
一个无符号整数,其前后可能有空格 | \s*\d+\s* |
一个可能带符号且前后可能有空格的整数 | \s*[+|-]?\s*\d+\s* |
一个无符号实数,其前后可能有空格 | \s*\d+(.\d*)?\s* |
一个可能带符号且前后带有空格的实数 | \s*[+|]?\s*\d+(.\d*)?\s* |
包含单词“just”的字符串 | \bjuste\b |
可以指定在字符串中搜索模式的位置:
模式 | 含义 |
^模式 | 模式位于字符串开头 |
模式$ | 模式结束字符串 |
^模式$ | 模式开头和结尾 |
模式 | 从字符串开头开始,在整个字符串中搜索该模式。 |
要搜索的字符串 | 模式 |
以感叹号结尾的字符串 | !$ |
以句点结尾的字符串 | \.$ |
以 // 序列开头的字符串 | ^// |
仅包含一个单词(前后可能有空格)的字符串 | ^\s*\w+\s*$ |
一个字符串,包含两个单词,前后可能有空格 | ^\s*\w+\s*\w+\s*$ |
包含单词 secret 的字符串 | \bsecret\b |
模型的子集可以被“提取”。因此,我们不仅可以验证一个字符串是否符合特定模型,还可以从该字符串中提取出模型中用圆括号括起的子集对应的元素。 例如,如果要解析一个包含日期格式 dd/mm/yy 的字符串,并希望从中提取日期中的 dd、mm、yy 这三个元素,则应使用正则表达式 (\d\d)/(\d\d)/(\d\d)。
让我们通过这个示例,看看如何使用 VBScript 进行操作。
- 首先,我们需要创建一个 RegExp(正则表达式)对象
- 然后设定待测试的模式
- 可能需要设置不区分大小写(默认情况下是区分大小写的)。在此处,这并不重要。
- 可能需要对字符串中的模式进行多次搜索(默认不进行多次搜索)
只有当所用的模式不涉及字符串的开头或结尾时,全局搜索才有意义。
- 此时,系统将搜索字符串中所有与该模式匹配的内容:
execute 对象的 RegExp 方法返回一个 match 类型的对象集合。该对象具有 value 属性,其值为 chaine 中与模式匹配的元素。 如果设置了 modele.global=true,则可能存在多个匹配项。因此,方法 execute 的返回结果是一个匹配项集合。
- 匹配项的数量由 correspondances.count 给出。如果该数值为 0,则表示该模型在任何地方均未被找到。 第 i 个匹配项的值由 correspondances(i).value 给出。如果模式包含括号内的子模式,则 correspondances(i) 中对应模式中第 j 个括号的元素即为 correspondances(i).submatches(j)。
以下示例展示了上述内容:
程序
' 正则表达式
' 需要验证字符串是否包含日期(格式为 dd/mm/yy)
Option Explicit
Dim modele
' 定义模式
Set modele=new regexp
modele.pattern="\b(\d\d)/(\d\d)/(\d\d)\b" ' une date n'importe où dans la chaîne
modele.global=true ' on recherchera le modèle plusieurs fois dans la chaîne
' 由用户提供待匹配的字符串
Dim chaine, correspondances, i
chaine=""
' 循环处理,直到字符串不等于“end”
Do While true
' 提示用户输入文本
wscript.stdout.writeLine "Tapez un texte contenant des dates au format jj/mm/aa et fin pour arrêter : "
chaine=wscript.stdin.readLine
' 若字符串=“end”则结束
If chaine="fin" Then Exit Do
' 将读取的字符串与日期模板进行比对
Set correspondances=modele.execute(chaine)
' 是否找到匹配项
If correspondances.count<>0 Then
' 至少有一个匹配项
For i=0 To correspondances.count-1
' 显示匹配项 i
wscript.echo "J'ai trouvé la date " & correspondances(i).value
' 获取匹配项 i 的子元素
wscript.echo "Les éléments de la date " & i & " sont (" & correspondances(i).submatches(0) & "," _
& correspondances(i).submatches(1) & "," & correspondances(i).submatches(2) & ")"
Next
Else
' 无匹配项
wscript.echo "Je n'ai pas trouvé de date au format jj/mm/aa dans votre texte"
End If
Loop
' 结束
wscript.quit 0
结果
利用正则表达式,验证键盘输入是否为正整数的程序可以编写如下:
程序
' 读取数据直至被识别为数字
Option Explicit
Dim fini, nombre
' 定义正整数(但可以为零)的模板
Dim modele
Set modele=new regexp
modele.pattern="^\s*\d+\s*$"
' 只要输入的数据不正确,就循环
' 循环由一个初始值为假的布尔变量控制(表示循环未结束)
fini=false
Do While Not fini
' 请求输入数字
wscript.stdout.write "Tapez un nombre entier >0: "
' 读取该数值
nombre=wscript.stdin.readLine
' 验证读取数据的格式
Dim correspondances
Set correspondances=modele.execute(nombre)
' 模型是否已通过验证?
If correspondances.count<>0 Then
' 这是一个整数,但它是否大于0?
nombre=cint(nombre)
If nombre>0 Then
fini=true
End If
End If
' 可能的错误信息
If Not fini Then wscript.echo "Erreur, vous n'avez pas tapé un nombre entier >0. Recommencez svp..."
Loop
' 确认
wscript.echo "Merci pour le nombre entier >0 : " & nombre
' 结束
wscript.quit 0
结果
找到能够验证字符串是否符合特定模式的正则表达式,有时是一项真正的挑战。以下程序可用于练习。它需要一个模式和一个字符串,然后会指出该字符串是否符合该模式。
程序
' 正则表达式
' 需要验证字符串是否符合模式
Option Explicit
' 定义模式
Dim modele
Set modele=new regexp
modele.global=true ' on recherchera le modèle plusieurs fois dans la chaîne
' 由用户提供要搜索模式的字符串
Dim chaine, correspondances, i
Do While true
' 要求用户输入正则表达式
wscript.stdout.write "Tapez le modèle à tester et fin pour arrêter : "
modele.pattern=wscript.stdin.readLine
' 完成了吗?
If modele.pattern="fin" Then Exit Do
' 要求用户输入与模板进行比对的字符串
Do While true
' 要求用户输入一个模式
wscript.stdout.writeLine "Tapez la chaîne à tester avec le modèle [" & modele.pattern & "] et fin pour arrêter : "
chaine=wscript.stdin.readLine
' 完成了吗?
If chaine="fin" Then Exit Do
' 将读取的字符串与日期模板进行比较
Set correspondances=modele.execute(chaine)
' 是否找到匹配项
If correspondances.count<>0 Then
' 至少有一个匹配项
For i=0 To correspondances.count-1
' 显示匹配项 i
wscript.echo "J'ai trouvé la correspondance " & correspondances(i).value
Next
Else
' 未找到匹配项
wscript.echo "Je n'ai pas trouvé de correspondance"
End If
Loop
Loop
' 完成
wscript.quit 0
结果
4.3. 拦截运行时错误
处理运行时错误的另一种方法是允许错误发生,收到通知后再进行处理。通常,当运行时发生错误时,WSH 会显示一条错误消息并终止程序。以下两条语句可让我们修改这种行为:
该语句告知系统(WSH),我们将自行处理错误。执行此语句后,系统将直接忽略所有错误。
该语句将恢复正常的错误处理机制。
当 on error resume next 语句生效时,我们必须自行处理可能出现的错误。Err 对象可协助我们完成此操作。该对象具有多种属性和方法,其中我们重点关注以下两项:
- number:一个整数,表示最近发生的错误编号。0 表示“无错误”
- description:如果未执行 on error resume next 语句,系统本会显示的错误信息
让我们来看以下示例:
程序
' 未处理的错误
Option Explicit
Dim nombre
nombre=cdbl("abcd")
wscript.echo "nombre=" & nombre
结果
现在处理错误:
程序
' 已处理的错误
Option Explicit
Dim nombre
' 我们自行处理错误
On Error Resume Next
nombre=cdbl("abcd")
' 是否发生错误?
If Err.number<>0 Then
wscript.echo "L'erreur [" & err.description & "] s'est produite"
On Error GoTo 0
wscript.quit 1
End If
' 无错误 - 恢复正常运行
On Error GoTo 0
wscript.echo "nombre=" & nombre
wscript.quit 0
结果
让我们用这种新方法重写输入大于0的整数的程序:
程序
' 读取数据直至其被识别为数字
Option Explicit
Dim fini, nombre
' 只要输入的数据不正确,就循环
' 循环由一个初始值为假的布尔变量控制(表示尚未结束)
fini=false
Do While Not fini
' 请求输入数字
wscript.stdout.write "Tapez un nombre entier >0: "
' 读取该数字
nombre=wscript.stdin.readLine
' 验证读取数据的格式
On Error Resume Next
nombre=cdbl(nombre)
If err.number=0 Then
' 无错误,为数字
' 恢复到正常的错误处理模式
On Error GoTo 0
' 是否为大于0的整数
If (nombre-int(nombre))=0 And nombre>0 Then
fini=true
End If
End If
' 恢复到常规错误处理模式
On Error GoTo 0
' 可能的错误消息
If Not fini Then wscript.echo "Erreur, vous n'avez pas tapé un nombre entier >0. Recommencez svp..."
Loop
' 确认
wscript.echo "Merci pour le nombre entier >0 : " & nombre
' 结束
wscript.quit 0
结果
Tapez un nombre entier >0: 4.5
Erreur, vous n'avez pas tapé un nombre entier >0. Recommencez svp...
Tapez un nombre entier >0: 4,5
Erreur, vous n'avez pas tapé un nombre entier >0. Recommencez svp...
Tapez un nombre entier >0: abcd
Erreur, vous n'avez pas tapé un nombre entier >0. Recommencez svp...
Tapez un nombre entier >0: -4
Erreur, vous n'avez pas tapé un nombre entier >0. Recommencez svp...
Tapez un nombre entier >0: 1
Merci pour le nombre entier >0 : 1
注释:
- 有时这是唯一可用的方法。此时,一旦可能引发错误的指令序列执行完毕,切记要恢复到正常的错误处理模式。
4.4. 应用于税款计算程序
我们重新使用已编写的税款计算程序,这次用于验证传递给程序的参数是否有效:
程序
' 计算纳税人的税款
' 调用该程序时需提供三个参数:已婚 子女 工资
' 已婚:已婚时为字符 O,未婚时为 N
' 子女:子女数量
' 工资:年薪(不包含分)
' 不进行数据有效性验证,但
' 会验证是否确实有三个
' 变量必须声明
Option Explicit
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
' 错误信息
wscript.echo syntaxe & vbCRLF & vbCRLF & "erreur : nombre d'arguments incorrect"
' 以错误代码 1 终止
wscript.quit 1
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
' 错误
wscript.echo syntaxe & vbCRLF & vbCRLF & "erreur : argument marie incorrect"
' 退出
wscript.quit 2
End If
' 获取值
Dim marie
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
' 错误
wscript.echo syntaxe & vbCRLF & vbCRLF & "erreur : argument enfants incorrect"
' 退出
wscript.quit 3
End If
' 获取值
Dim enfants
enfants=cint(wscript.arguments(1))
' 工资必须是大于等于0的整数
modele.pattern="^\d{1,9}$"
Set correspondances=modele.execute(wscript.arguments(2))
If correspondances.count=0 Then
' 错误
wscript.echo syntaxe & vbCRLF & vbCRLF & "erreur : argument salaire incorrect"
' 退出
wscript.quit 4
End If
' 获取值
Dim salaire
salaire=clng(wscript.arguments(2))
' 在 3 个数组中定义计算税款所需的数据
Dim limites, coeffn, coeffr
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)
' 计算份额数
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
impot=int(revenu*coeffr(i)-nbParts*coeffn(i))
' 显示结果
wscript.echo "impôt=" & impot
' 无错误退出
wscript.quit 0
结果
C:\>cscript impots2.vbs
Syntaxe : pg marié enfants salaire
marié : caractère O si marié, N si non marié
enfants : nombre d'enfants (entier >=0)
salaire : salaire annuel sans les centimes (entier >=0)
erreur : nombre d'arguments incorrect
C:\>cscript impots2.vbs a b c
Syntaxe : pg marié enfants salaire
marié : caractère O si marié, N si non marié
enfants : nombre d'enfants (entier >=0)
salaire : salaire annuel sans les centimes (entier >=0)
erreur : argument marie incorrect
C:\>cscript impots2.vbs o b c
Syntaxe : pg marié enfants salaire
marié : caractère O si marié, N si non marié
enfants : nombre d'enfants (entier >=0)
salaire : salaire annuel sans les centimes (entier >=0)
erreur : argument enfants incorrect
C:\>cscript impots2.vbs o 2 c
Syntaxe : pg marié enfants salaire
marié : caractère O si marié, N si non marié
enfants : nombre d'enfants (entier >=0)
salaire : salaire annuel sans les centimes (entier >=0)
erreur : argument salaire incorrect
C:\>cscript impots2.vbs o 2 200000
impôt=22504