4. 错误处理
在编程中,有一条绝对规则:程序绝不能意外“崩溃”。程序执行过程中可能发生的所有错误都必须得到处理,并生成有意义的错误信息。
如果回到前面讨论的税费示例,当用户输入任意数量的子女数时会发生什么?让我们来看这个示例:
这就是所谓的“野性崩溃”。语句 enfants=cint(wscript.arguments(1)) 导致了“崩溃”,因为 arguments(1) 包含字符串“xyzt”。
在使用性质不明的变体之前,必须先验证其确切的子类型。实现此目的有多种方法:
- 使用 vartype 或 typename 函数测试变体中数据的实际类型
- 使用正则表达式验证变体内容是否符合特定模式
- 允许错误发生,然后进行拦截并处理
我们将探讨这些不同的方法。
4.1. 确定数据项的确切类型
请记住,vartype 和 varname 函数可让您确定一段数据的确切类型。但这并不总是很有帮助。 例如,当我们读取键盘输入的数据时,vartype 和 typename 函数会告诉我们它是字符串,因为所有键盘输入的数据都被视为字符串。但这并不能告诉我们,例如,该字符串是否可以被视为一个有效的数字。因此,我们需要使用其他函数来获取此类信息:
isNumeric(expression) | 如果表达式可作为数字使用,则返回 true |
isDate(表达式) | 如果表达式可作为日期使用,则返回 true |
isEmpty(var) | 如果变量 var 未被初始化,则返回 true |
isNull(var) | 如果变量 var 包含无效数据,则返回 true |
isArray(var) | 如果 var 是数组,则返回 true |
isObject(var) | 如果 var 是对象,则返回 true |
以下示例会提示用户通过键盘输入数据,直到系统识别出该数据为数字:
程序
' read data until it is recognized as a number
Option Explicit
Dim fini, nombre
' loop until the data entered is correct
' the loop is controlled by a finite boolean, set to false at the start (= it's not finite)
fini=false
Do While Not fini
' we ask for the number
wscript.stdout.write "Tapez un nombre : "
' we read it
nombre=wscript.stdin.readLine
' the type is necessarily string when read
wscript.echo "Type de la donnée lue : " & typename(nombre) & "," & vartype(nombre)
' test the actual type of data read
If isNumeric(nombre) Then
fini=true
Else
wscript.echo "Erreur, vous n'avez pas tapé un nombre. Recommencez svp..."
End If
Loop
' confirmation
wscript.echo "Merci pour le nombre " & nombre
' and end
wscript.quit 0
结果
isNumeric 函数无法告诉我们一个表达式是否为整数。要获取此信息,需要进行额外的测试。以下示例要求输入一个大于 0 的整数:
程序
' read data until it is recognized as an integer >0
Option Explicit
Dim fini, nombre
' loop until the data entered is correct
' the loop is controlled by a finite boolean, set to false at the start (= it's not finite)
fini=false
Do While Not fini
' we ask for the number
wscript.stdout.write "Tapez un nombre entier >0: "
' we read it
nombre=wscript.stdin.readLine
' test the actual type of data read
If isNumeric(nombre) Then
' is it a positive integer (number equal to its integer part)?
If (nombre-int(nombre))=0 And nombre>0 Then
fini=true
End If
End If
' possible error msg
If Not fini Then wscript.echo "Erreur, vous n'avez pas tapé un nombre entier >0. Recommencez svp..."
Loop
' confirmation
wscript.echo "Merci pour le nombre entier >0 : " & nombre
' and end
wscript.quit 0
结果
评论:
- int(number) 返回一个数的整数部分。一个数等于其整数部分即为整数。
- 值得注意的是,我们不得不使用条件判断 If (number - int(number)) = 0 And number > 0,因为条件判断 If number = int(number) And number > 0 并未产生预期结果。它未能检测到正整数。具体原因留待读者自行探索。
- 条件判断 If (number - int(number)) = 0 并非完全可靠。让我们来看以下示例:
实数无法精确表示,只能近似表示。在此,运算 number-int(number) 返回了计算机精度下的 0。
4.2. 正则表达式
正则表达式允许我们验证字符串的格式。通过这种方式,我们可以验证表示日期的字符串是否符合 dd/mm/yy 格式。为此,我们使用一个模式并将字符串与该模式进行比较。因此,在此示例中,d、m 和 y 必须是数字。 因此,有效日期格式的模式为 "\d\d/\d\d/\d\d",其中符号 \d 表示一个数字。模式中可使用的符号如下(微软文档):
字符 | 描述 |
\ | 将后续字符指定为特殊字符或字面量。例如,“n”对应字符“n”。“\n”对应换行符。字符序列“\\”对应“\”,而“\(" 对应“(”。 |
^ | 匹配输入的开头。 |
$ | 匹配输入的结尾。 |
* | 匹配前一个字符零次或多次。因此,“zo*”匹配“z”或“zoo”。 |
+ | 匹配前一个字符一次或多次。因此,“zo+”匹配“zoo”,但不匹配“z”。 |
? | 匹配前一个字符零次或一次。例如,"a?ve?" 匹配 "lever" 中的 "ve"。 |
. | 匹配除换行符以外的任意单个字符。 |
(pattern) | 搜索该模式并存储匹配结果。可以通过 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”,但不匹配“verb”中的“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 符号的示例,该符号代表单个数字:
模式 | 含义 |
\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 |
您可以指定在字符串中的哪个位置搜索该模式:
pattern | 含义 |
^模式 | 模式位于字符串开头 |
pattern$ | 该模式结束字符串 |
^模式$ | 该模式同时作为字符串的开头和结尾 |
pattern | 从字符串开头开始,在字符串的任意位置搜索该模式。 |
搜索字符串 | 模式 |
以感叹号结尾的字符串 | !$ |
以句点结尾的字符串 | \.$ |
以 // 序列开头的字符串 | ^// |
由单个单词组成的字符串,其前后可选地带有空格 | ^\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(正则表达式)对象
- 然后,我们将要测试的模式设置为
- 您可能希望忽略大小写(默认情况下,它是区分大小写的)。在此处,这并不重要。
- 您可能希望在字符串中多次搜索该模式(默认情况下不会这样做)
只有当所用的模式不涉及字符串的开头或结尾时,全局搜索才有意义。
- 然后,我们会在字符串中搜索该模式的所有匹配项:
RegExp 对象的 execute 方法会返回一个匹配对象的集合。该对象具有一个 value 属性,其值为与模式匹配的字符串元素。如果你已将 pattern.global 设置为 true,则可能存在多个匹配项。这就是为什么 execute 方法的结果是一个匹配项集合。
- 匹配项的数量由 `matches.count` 给出。如果该数值为 0,则表示未在任何位置找到该模式。第 i 个匹配项的值由 `matches(i).value` 给出。如果模式中包含括号内的子模式,那么 `matches(i)` 中对应于模式中第 j 个括号的元素即为 `matches(i).submatches(j)`。
以下示例展示了上述内容:
程序
' regular expression
' we want to check that a string contains a date in dd/mm/yy format
Option Explicit
Dim modele
' we define the
Set modele=new regexp
modele.pattern="\b(\d\d)/(\d\d)/(\d\d)\b" ' a date anywhere in the chain
modele.global=true ' we will search for the model several times in the chain
' the user specifies the string in which to search for the model
Dim chaine, correspondances, i
chaine=""
' loop as long as string<>"end"
Do While true
' the user is asked to type a text
wscript.stdout.writeLine "Tapez un texte contenant des dates au format jj/mm/aa et fin pour arrêter : "
chaine=wscript.stdin.readLine
' fini si chaine=fin
If chaine="fin" Then Exit Do
' the string read is compared with the date template
Set correspondances=modele.execute(chaine)
' was a match found
If correspondances.count<>0 Then
' we have at least one match
For i=0 To correspondances.count-1
' the i correspondence is displayed
wscript.echo "J'ai trouvé la date " & correspondances(i).value
' we retrieve the sub-elements of correspondence 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
' no correspondence
wscript.echo "Je n'ai pas trouvé de date au format jj/mm/aa dans votre texte"
End If
Loop
' finish
wscript.quit 0
结果
使用正则表达式,验证键盘输入是否确实为正整数的程序可以编写如下:
程序
' read data until it is recognized as a number
Option Explicit
Dim fini, nombre
' we define the model of a positive integer (which may be zero)
Dim modele
Set modele=new regexp
modele.pattern="^\s*\d+\s*$"
' loop until the data entered is correct
' the loop is controlled by a finite boolean, set to false at the start (= it's not finite)
fini=false
Do While Not fini
' we ask for the number
wscript.stdout.write "Tapez un nombre entier >0: "
' we read it
nombre=wscript.stdin.readLine
' test the format of the data read
Dim correspondances
Set correspondances=modele.execute(nombre)
' has the model been checked?
If correspondances.count<>0 Then
' it's an integer, but is it >0?
nombre=cint(nombre)
If nombre>0 Then
fini=true
End If
End If
' possible error msg
If Not fini Then wscript.echo "Erreur, vous n'avez pas tapé un nombre entier >0. Recommencez svp..."
Loop
' confirmation
wscript.echo "Merci pour le nombre entier >0 : " & nombre
' and end
wscript.quit 0
结果
寻找能够验证字符串是否符合特定模式的正则表达式,有时确实是一项挑战。以下程序可供您练习。它会要求您输入一个模式和一个字符串,然后指出该字符串是否符合该模式。
节目
' regular expression
' we want to check that a chain corresponds to a model
Option Explicit
' we define the
Dim modele
Set modele=new regexp
modele.global=true ' we will search for the model several times in the chain
' the user specifies the string in which to search for the model
Dim chaine, correspondances, i
Do While true
' the user is asked to type in a template
wscript.stdout.write "Tapez le modèle à tester et fin pour arrêter : "
modele.pattern=wscript.stdin.readLine
' finished?
If modele.pattern="fin" Then Exit Do
' the user is asked for the strings to be compared with the model
Do While true
' the user is asked to type in a template
wscript.stdout.writeLine "Tapez la chaîne à tester avec le modèle [" & modele.pattern & "] et fin pour arrêter : "
chaine=wscript.stdin.readLine
' finished?
If chaine="fin" Then Exit Do
' the string read is compared with the date template
Set correspondances=modele.execute(chaine)
' was a match found
If correspondances.count<>0 Then
' we have at least one match
For i=0 To correspondances.count-1
' the i correspondence is displayed
wscript.echo "J'ai trouvé la correspondance " & correspondances(i).value
Next
Else
' no correspondence
wscript.echo "Je n'ai pas trouvé de correspondance"
End If
Loop
Loop
' finish
wscript.quit 0
结果
4.3. 处理运行时错误
处理运行时错误的另一种方法是允许错误发生,收到错误通知后再进行处理。通常,当运行时发生错误时,WSH 会显示一条错误消息,程序随即停止。以下两条语句允许我们更改这种行为:
该语句告知系统(WSH)我们将自行处理错误。在此语句之后,系统将直接忽略任何错误。
该语句将我们带回正常的错误处理流程。
当 on error resume next 语句生效时,我们必须自行处理可能发生的任何错误。Err 对象可协助我们完成此操作。该对象具有多种属性和方法,其中我们将重点关注以下两项:
- number:一个整数,表示最近发生的错误编号。0 表示“无错误”
- description:如果我们未执行 on error resume next 语句,系统本会显示的错误信息
让我们来看以下示例:
程序
' unhandled error
Option Explicit
Dim nombre
nombre=cdbl("abcd")
wscript.echo "nombre=" & nombre
结果
现在让我们处理这个错误:
程序
' managed error
Option Explicit
Dim nombre
' we manage mistakes ourselves
On Error Resume Next
nombre=cdbl("abcd")
' was there a mistake?
If Err.number<>0 Then
wscript.echo "L'erreur [" & err.description & "] s'est produite"
On Error GoTo 0
wscript.quit 1
End If
' no error - returns to normal operation
On Error GoTo 0
wscript.echo "nombre=" & nombre
wscript.quit 0
结果
让我们使用这种新方法重写输入大于 0 的整数的程序:
程序
' read data until it is recognized as a number
Option Explicit
Dim fini, nombre
' loop until the data entered is correct
' the loop is controlled by a finite boolean, set to false at the start (= it's not finite)
fini=false
Do While Not fini
' we ask for the number
wscript.stdout.write "Tapez un nombre entier >0: "
' we read it
nombre=wscript.stdin.readLine
' test the format of the data read
On Error Resume Next
nombre=cdbl(nombre)
If err.number=0 Then
' no error it's a number
' returns to normal error handling mode
On Error GoTo 0
' is it an integer >0
If (nombre-int(nombre))=0 And nombre>0 Then
fini=true
End If
End If
' returns to normal error handling mode
On Error GoTo 0
' possible error msg
If Not fini Then wscript.echo "Erreur, vous n'avez pas tapé un nombre entier >0. Recommencez svp..."
Loop
' confirmation
wscript.echo "Merci pour le nombre entier >0 : " & nombre
' and end
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. 应用于税费计算程序
我们将重新审视之前编写的税费计算程序,这次要验证传递给程序的参数是否有效:
程序
' calculating a taxpayer's tax liability
' the program must be called with three parameters: married children salary
' married: character Y if married, N if unmarried
' children: number of children
' salary: annual salary without cents
' no verification of data validity is performed, but we do
' check that there are three of them
' mandatory variable declaration
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)"
' we check that there are 3 arguments
Dim nbArguments
nbArguments=wscript.arguments.count
If nbArguments<>3 Then
' error msg
wscript.echo syntaxe & vbCRLF & vbCRLF & "erreur : nombre d'arguments incorrect"
' stop with error code 1
wscript.quit 1
End If
' retrieve arguments and check their validity
' an argument is passed to the program without spaces in front and behind it
' use regular expressions to check data validity
Dim modele, correspondances
Set modele=new regexp
' marital status must be among the characters oOnN
modele.pattern="^[oOnN]$"
Set correspondances=modele.execute(wscript.arguments(0))
If correspondances.count=0 Then
' error
wscript.echo syntaxe & vbCRLF & vbCRLF & "erreur : argument marie incorrect"
' we leave
wscript.quit 2
End If
' the value
Dim marie
If lcase(wscript.arguments(0)) = "o"Then
marie=true
Else
marie=false
End If
' children must be an integer >=0
modele.pattern="^\d{1,2}$"
Set correspondances=modele.execute(wscript.arguments(1))
If correspondances.count=0 Then
' error
wscript.echo syntaxe & vbCRLF & vbCRLF & "erreur : argument enfants incorrect"
' we leave
wscript.quit 3
End If
' the value
Dim enfants
enfants=cint(wscript.arguments(1))
' salary must be an integer >=0
modele.pattern="^\d{1,9}$"
Set correspondances=modele.execute(wscript.arguments(2))
If correspondances.count=0 Then
' error
wscript.echo syntaxe & vbCRLF & vbCRLF & "erreur : argument salaire incorrect"
' we leave
wscript.quit 4
End If
' the value
Dim salaire
salaire=clng(wscript.arguments(2))
' we define the data needed to calculate the tax in 3 tables
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)
' the number of shares is calculated
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
' calculate the family quota and taxable income
Dim revenu, qf
revenu=0.72*salaire
qf=revenu/nbParts
' tax calculation
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))
' the result is displayed
wscript.echo "impôt=" & impot
' leave without error
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