第十五章 疯狂Caché 错误处理(二)

传统的错误处理

传统错误处理的工作原理

对于传统的错误处理,Caché提供了该功能,以便应用程序可以有一个错误处理程序。错误处理程序处理应用程序运行时可能发生的任何错误。特殊变量指定发生错误时要执行的ObjectScript命令。这些命令可以直接处理错误,也可以调用例程来处理它。

要设置错误处理程序,基本流程如下:

  1. 创建一个或多个例程以执行错误处理。编写代码以执行错误处理。这可以是整个应用程序的通用代码,也可以是特定错误条件的特定处理。这允许对应用程序的每个特定部分执行自定义错误处理。
  2. 在应用程序中建立一个或多个错误处理程序,每个错误处理程序都使用特定的适当错误处理程序。

如果发生错误且尚未建立错误处理程序,则行为取决于Caché 会话的启动方式:

  1. 如果在终端提示符下登录到Caché,并且没有设置错误陷阱,则Caché会在主设备上显示一条错误消息,并返回终端提示符,程序堆栈完好无损。程序员稍后可以恢复该程序的执行。
  2. 如果在应用程序模式下调用了Caché,但尚未设置错误陷阱,则Caché会在主设备上显示错误消息,并执行HALT命令。

内部错误捕获行为

要充分利用Caché错误处理和围绕$ZTRAP特殊变量(以及$ETRAP)的作用域问题,了解Caché如何将控制从一个例程转移到另一个例程是很有帮助的。

每次发生以下任何一种情况时,Caché都会构建一个称为“上下文帧”的数据结构:

  • 一个例程使用DO命令调用另一个例程。(这种框架也称为“DO框架”。)
  • XECUTE命令参数会导致执行ObjectScript代码。(这种框架也称为“XECUTE框架”。)
  • 执行用户定义的函数。

框架构建在调用堆栈上,调用堆栈是进程地址空间中的私有数据结构之一。

Caché将以下元素存储在例程的帧中:

  • $ZTRAP特殊变量的值(如果有)
  • $ETRAP特殊变量的值(如果有)
  • 从子例程返回的位置

当例程A使用DO^B调用例程B时,Caché会在调用堆栈上构建一个DO帧,以保留A的上下文。

当例程B调用例程C时,Caché会向调用堆栈添加一个DO帧,以保留B的上下文,依此类推。

调用堆栈上的帧

如果在终端提示符下使用DO命令调用上图中的例程A,则在调用堆栈的底部存在未在图中描述的额外DO帧。

当前上下文级别

可以使用以下命令返回有关当前上下文级别的信息:

  • $STACK特殊变量包含当前相对堆栈级别。
  • $ESTACK特殊变量包含当前堆栈级别。可以在任何用户指定的点处将其初始化为0(零级)。
  • $STACK函数返回有关当前上下文和已保存在调用堆栈上的上下文的信息

$STACK特殊变量

$STACK特殊变量包含当前保存在进程的调用堆栈上的帧数。$STACK值实质上是当前执行的上下文的上下文级别编号(从零开始)。因此,当启Caché 存映像时,但在处理任何命令之前,$STACK的值为0。

/// d ##class(PHA.TEST.ObjectScript).TestSTRACK()
ClassMethod TestSTRACK()
{
STA
  WRITE !,"Context level in routine STA = ",$STACK
  DO A
  WRITE !,"Context level after routine A = ",$STACK
  QUIT
A
  WRITE !,"Context level in routine A = ",$STACK
  DO B
  WRITE !, "Context level after routine B = ",$STACK
  QUIT
B
  WRITE !,"Context level in routine B = ",$STACK
  XECUTE "WRITE !,""Context level in XECUTE = "",$STACK" 
  WRITE !,"Context level after XECUTE = ",$STACK
  QUIT
}
DHC-APP> d ##class(PHA.TEST.ObjectScript).TestSTRACK()
 
Context level in routine STA = 1
Context level in routine A = 2
Context level in routine B = 3
Context level in XECUTE = 4
Context level after XECUTE = 3
Context level after routine B = 2
Context level after routine A = 1

$ESTACK特殊变量

$ESTACK特殊变量类似于$STACK特殊变量,但在错误处理中更有用,因为可以使用新命令将其重置为0(并保存其先前的值)。因此,进程可以重置特定上下文中的$ESTACK,以将其标记为$ESTACK 0级上下文。如果出现错误,错误处理程序可以测试$ESTACK的值,以将调用堆栈展开回该上下文。

/// d ##class(PHA.TEST.ObjectScript).TestESTACK()
ClassMethod TestESTACK()
{
Main
   WRITE !,"Initial: $STACK=",$STACK," $ESTACK=",$ESTACK
   DO Sub1
   WRITE !,"Return: $STACK=",$STACK," $ESTACK=",$ESTACK
   QUIT
Sub1
     WRITE !,"Sub1Call: $STACK=",$STACK," $ESTACK=",$ESTACK
     NEW $ESTACK
     WRITE !,"Sub1NEW: $STACK=",$STACK," $ESTACK=",$ESTACK
     DO Sub2
     QUIT
Sub2
     WRITE !,"Sub2Call: $STACK=",$STACK," $ESTACK=",$ESTACK
     QUIT
}
DHC-APP> d ##class(PHA.TEST.ObjectScript).TestESTACK()
 
Initial: $STACK=1 $ESTACK=1
Sub1Call: $STACK=2 $ESTACK=2
Sub1NEW: $STACK=2 $ESTACK=0
Sub2Call: $STACK=3 $ESTACK=1
Return: $STACK=1 $ESTACK=1

$STACK函数

$STACK函数返回有关当前上下文和已保存在调用堆栈上的上下文的信息。对于每个上下文,$STACK函数提供以下信息:

  • 上下文类型(DOXECUTE或用户定义函数)
  • 上下文中处理的最后一个命令的条目引用和命令编号
  • 包含上下文中处理的最后一个命令的源例程行或XECUTE字符串
  • 上下文中发生的任何错误的$ECODE值(仅当$ECODE为非空时在错误处理期间可用)

发生错误时,所有上下文信息都会立即保存在您的进程错误堆栈中。然后,$STACK函数可以访问上下文信息,直到错误处理程序清除$ECODE的值。换句话说,当$ECODE的值非空时,$STACK函数返回有关保存在错误堆栈上的上下文的信息,而不是返回相同指定上下文级别的活动上下文的信息。

当发生错误并且错误堆栈已经存在时,除非错误堆栈上的上下文级别已经存在有关另一个错误的信息,否则 Caché 存会在发生错误的上下文级别记录有关新错误的信息。在这种情况下,信息被放在错误堆栈的下一级(不管那里可能已经记录了哪些信息)。

因此,根据新错误的上下文级别,错误堆栈可以扩展(添加一个或多个上下文级别),或者可以重写现有错误堆栈上下文级别的信息以容纳关于新错误的信息。

请记住,可以通过清除$ECODE特殊变量来清除进程错误堆栈。

错误码

发生错误时,Caché会将$ZERROR$ECODE特殊变量设置为描述错误的值。

$ZERROR

Caché将$ZERROR设置为包含以下内容的字符串:

  • 用尖括号括起来的Caché错误代码。
  • 发生错误的标签、偏移量和例程名称。
  • (对于某些错误):其他信息,如导致错误的项的名称。

%Exception.SystemException类的AsSystemError()方法以与$ZERROR相同的格式返回相同的值。

以下示例显示了Caché遇到错误时设置$ZERROR的消息类型。在下面的示例中,在例程MyTest的标签PrintResult的行偏移量2处调用未定义的局部变量abc$ZERROR包含:

<UNDEFINED>PrintResult+2^MyTest *abc

在行偏移量3处调用不存在的类时发生以下错误:

<CLASS DOES NOT EXIST>PrintResult+3^MyTest *%SYSTEM.XXQL

在行偏移量4处调用现有类的不存在的方法时发生以下错误:

<METHOD DOES NOT EXIST>PrintResult+4^MyTest *BadMethod,%SYSTEM.SQL

还可以将特殊变量$ZERROR显式设置为最多128个字符的任意字符串;例如:

 SET $ZERROR="Any String"

$ZERROR值旨在错误后立即使用。因为$ZERROR值可能不会在例程调用中保留,所以希望保留ZERROR值以供以后使用的用户应该将其复制到变量中。强烈建议用户在使用后立即将`ZERROR设置为空字符串(“”`)。

$ECODE

发生错误时,Caché会将$ECODE设置为逗号括起来的字符串的值,该字符串包含与错误对应的ANSI标准错误代码。例如,当引用未定义的全局变量时,Caché会将$ECODE集设置为以下字符串:

,M7,

如果错误没有对应的ANSI标准错误代码,则Caché会将$ECODE设置为逗号括起来的字符串的值,该字符串包含前面带有字母Z的Caché错误代码。如果进程已耗尽其符号表空间,则Caché会将错误代码<store>放在$ZERROR特殊变量中,并将$ECODE设置为以下字符串:

,ZSTORE,

错误发生后,错误处理程序可以通过检查$ZERROR特殊变量或$ECODE特殊变量的值来测试特定的错误代码。

注意:错误处理程序应该检查$ZERROR而不是$ECODE特殊变量以查找特定错误。

使用$ZTRAP处理错误。

要使用$ZTRAP处理错误,可以将$ZTRAP特殊变量设置为一个位置,指定为带引号的字符串。将$ZTRAP特殊变量设置为条目引用,该引用指定在发生错误时将控制权转移到的位置。然后在该位置编写$ZTRAP代码。

$ZTRAP设置为非空值时,它优先于任何现有的$ETRAP错误处理程序。Caché隐式执行NEW $ETRAP命令,并将$ETRAP设置为等于“”

在程序中设置$ZTRAP

在程序中,只能将$ZTRAP特殊变量设置为该程序中的行标签(私有标签)。

不能将$ZTRAP设置为过程块内的任何外部例程。

显示$ZTRAP值时,Caché不返回私有标签的名称。相反,它返回该私有标签所在的过程顶部的偏移量。

在例程中设置$ZTRAP

在例程中,可以将$ZTRAP特殊变量设置为当前例程中的标签、外部例程或外部例程中的标签。如果外部例程不是程序块代码,则只能引用该例程。下面的示例将LogErr^ErrRou建立为错误处理程序。发生错误时,Caché执行^ErrRou例程中LogErr标签中的代码:

  SET $ZTRAP="LogErr^ErrRou"

显示$ZTRAP值时,Caché会显示标签名称和(适当时)例程名称。

标签名称的前31个字符必须是唯一的。标签名称和例程名称区分大小写。

在例程中,$ZTRAP有三种形式:

  • SET $ZTRAP="location"
  • SET $ZTRAP="*location" 它在调用它的发生错误的上下文中执行。
  • SET $ZTRAP="^%ETN"它在调用它的发生错误的上下文中执行系统提供的错误例程%ETN。不能从过程块执行^%ETN(或任何外部例程)。指定代码为[Not ProcedureBlock],或使用如下例程调用%ETN入口点back^%ETN
ClassMethod MyTest() as %Status
  {
  SET $ZTRAP="Error"
  SET ans = 5/0      /* divide-by-zero error */
  WRITE "Exiting ##class(User.A).MyTest()",!
  QUIT ans
Error
  SET err=$ZERROR
  SET $ZTRAP=""
  DO BACK^%ETN
  QUIT $$$ERROR($$$CacheError,err)
  } 

编写$ZTRAP代码

$ZTRAP指向的位置可以执行各种操作来显示、记录和/或更正错误。无论希望执行什么错误处理操作,$ZTRAP代码都应该从执行两个任务开始:

  • $ZTRAP设置为另一个值,可以是错误处理程序的位置,也可以是空字符串(“”)。(必须使用SET,因为不能k $ZTRAP。) 这样做是因为如果在错误处理期间发生另一个错误,该错误将调用当前的$ZTRAP错误处理程序。如果当前错误处理程序是您所在的错误处理程序,这将导致无限循环。

  • 将变量设置为$ZERROR。如果希望稍后在代码中引用$ZERROR值,请引用此变量,而不是$ZERROR本身。之所以这样做,是因为$ZERROR包含最新的错误,并且$ZERRR值可能不会在例程调用(包括内部例程调用)中保留。如果在错误处理期间出现另一个错误,则$ZERROR值将被该新错误覆盖。

/// w ##class(PHA.TEST.ObjectScript).TestMyErrorDieFor()
ClassMethod TestMyErrorDieFor()
{
	SET $ZTRAP=""
	SET $ZTRAP="Error"
	w 1,!
	SET ans = 5/0      /* divide-by-zero error */
	WRITE "Exiting ##class(User.A).MyTest()",!
	QUIT ans
Error
	//SET $ZTRAP=""
	w 2,!
	SET ans = 5/0
	q 0
}

强烈建议用户在使用后立即将$ZERROR设置为空字符串(“”)。

以下示例显示了这些基本的$ZTRAP代码语句:

MyErrHandler
  SET $ZTRAP=""
  SET err=$ZERROR
  /* 使用err作为要处理的错误来处理代码时出错 */

使用$ZTRAP

应用程序中的每个例程都可以通过设置$ZTRAP来建立自己的$ZTRAP错误处理程序。发生错误陷阱时,Caché会执行以下步骤:

  1. 将特殊变量$ZERROR设置为错误消息。
  2. 将程序堆栈重置为设置错误陷阱时(执行设置$ZTRAP=执行时)的状态。换句话说,系统删除堆栈上的所有条目,直到它到达设置错误陷阱的点。(如果将$ZTRAP设置为以星号(*)开头的字符串,则不会重置程序堆栈。)
  3. $ZTRAP的值指定的位置恢复程序。$ZTRAP的值保持不变。

注意:可以将变量$ZERROR显式设置为最多128个字符的任意字符串。通常会将$ZERROR设置为空字符串,但也可以将$ZERROR设置为一个值。

使用错误陷阱解除新命令的堆栈。

发生错误陷阱并删除程序堆栈条目时,Caché还会将所有堆叠的新命令移回包含集合$ZTRAP=的子例程级别。但是,在该子例程级别执行的所有新命令都将保留,无论它们是在设置$ZTRAP之前还是之后添加到堆栈中的。

Class PHA.TEST.ObjectScriptTwo Extends %RegisteredObject [ Not ProcedureBlock ]
{
/// w ##class(PHA.TEST.ObjectScriptTwo).TestErrorStack()
ClassMethod TestErrorStack()
{
Main
  SET A=1,B=2,C=3,D=4,E=5,F=6
  NEW A,B
  SET $ZTRAP="ErrSub"
  NEW C,D
  DO Sub1
  RETURN
Sub1()
  NEW E,F
  WRITE 6/0    // Error: division by zero
  RETURN
ErrSub()
  WRITE !,"Error is: ",$ZERROR
  
  RETURN
}
}
DHC-APP>w ##class(PHA.TEST.ObjectScriptTwo).TestErrorStack()
 
Error is: <DIVIDE>Sub1+2^PHA.TEST.ObjectScriptTwo.1
E=5
F=6
W ##CLASS(PHA.TEST.ObjectScriptTwo).TestErrorStack()
^

Sub1中的错误激活错误陷阱时,堆叠在Sub1中的EF的先前值被移除,但ABCD保持堆叠。

$ZTRAP控制选项流

调用$ZTRAP错误处理程序来处理错误并执行任何清理或错误日志记录操作后,错误处理程序有三个流控制选项:

  • 处理错误并继续应用程序。
  • 将控制传递给另一个错误处理程序
  • 终止应用程序

继续应用程序

$ZTRAP错误处理程序处理完错误之后,可以通过发出GOTO继续应用程序。不必清除$ZERROR$ECODE特殊变量的值即可继续正常的应用程序处理。但是,应该清除$ZTRAP(通过将其设置为空字符串),以避免在发生另一个错误时可能出现的无限错误处理循环。

在完成错误处理后,$ZTRAP错误处理程序可以使用GOTO命令将控制转移到应用程序中的预定重新启动或继续点,以恢复正常的应用程序处理。

当错误处理程序处理了错误时,$ZERROR特殊变量被设置为一个值。错误处理程序完成时,不一定清除此值。当调用错误处理程序的下一个错误发生时,$ZERROR值将被覆盖。因此,$ZERROR值只能在错误处理程序的上下文中访问。在任何其他上下文中访问$ZERROR不会产生可靠的结果。

将控制传递给另一个错误处理程序

如果错误条件不能由$ZTRAP错误处理程序纠正,可以使用特殊形式的ZTRAP命令将控制转移给另一个错误处理程序。命令ZTRAP $ZERROR重新发出错误条件的信号,并使Caché使用错误处理程序将调用堆栈展开到下一个调用堆栈级别。在Caché将调用堆栈展开到下一个错误处理程序的级别之后,处理将在该错误处理程序中继续进行。下一个错误处理程序可能已由$ZTRAP$ETRAP设置。

下图显示了$ZTRAP错误处理例程中的控制流。

https://juejin.im/post/5ed05cc6e51d45786b18244c

「点点赞赏,手留余香」

    还没有人赞赏,快来当第一个赞赏的人吧!
0 条回复 A 作者 M 管理员
    所有的伟大,都源于一个勇敢的开始!
欢迎您,新朋友,感谢参与互动!欢迎您 {{author}},您在本站有{{commentsCount}}条评论