第十五章 Caché 错误处理(一)

在发生错误时管理Caché 的行为称为错误处理或错误处理。错误处理执行以下一个或多个功能:

  • 更正导致错误的条件
  • 执行某些操作,允许在出现错误的情况下继续执行
  • 转移执行流程
  • 记录有关错误的信息

重要提示:CachéObjectScript错误处理的首选机制是try-catch机制。如果要使用传统的错误处理,则首选 Z T R A P 机 制 。 不 鼓 励 使 用 ZTRAP机制。不鼓励使用 ZTRAP使ETRAP。

/// d ##class(PHA.TEST.ObjectScript).TestCatch()
ClassMethod TestCatch(b)
{
	s t1 = $p($zts,",",2)
	
	f i = 1:1:100000 d
	.d ..TestCatch1()
	s t2= $p($zts,",",2)
	w t2-t1,!
	
	
	s t1 = $p($zts,",",2)
	
	f i = 1:1:100000 d
	.d ..TestCatch2()
	s t3= $p($zts,",",2)
	w t3-t1,!
}

/// d ##class(PHA.TEST.ObjectScript).TestCatch1()
ClassMethod TestCatch1()
{
	try{
		s a =a/0
	}catch{
		s a= "异常!"
	}
}

/// d ##class(PHA.TEST.ObjectScript).TestCatch2()
ClassMethod TestCatch2()
{
	s $zt="ErrTestCatch2"
	s a =a/0
ErrTestCatch2
	s a= "异常!"
}

经过测试$zttry-catch机制差别不大。

DHC-APP>d ##class(PHA.TEST.ObjectScript).TestCatch()
7.393
7.577
TRY-CATCH机制

Caché支持用于处理错误的try-catch机制。使用此机制,可以建立分隔的代码块,每个代码块称为一个try块;如果在try块期间发生错误,控制权将传递给try块的关联catch块,该块包含用于处理异常的代码。TRY块还可以包括抛出命令;这些命令中的每个命令都显式地从TRY块内发出异常,并将执行转移到CATCH块。

要以最基本的形式使用此机制,请在ObjectScript代码中包括一个try块。如果此块内发生异常,则执行关联CATCH块内的代码。try-catch块的形式为:

 TRY {
      protected statements
 } CATCH [ErrorHandle] {
      error statements
 }
 further statements
  • 其中:try命令标识用大括号括起来的ObjectScript代码语句块。Try不接受任何参数。此代码块是用于结构化异常处理的受保护代码。如果try块内发生异常,则Caché设置异常属性(oref.Nameoref.Codeoref.Dataoref.Location)、$ZERROR$ECODE,然后将执行转移到由catch命令标识的异常处理程序。这称为抛出异常。
  • 受保护的语句是属于正常执行的ObjectScript语句。(这些可以包括对抛出命令的调用。此方案将在下一节中介绍。)
  • catch命令定义异常处理程序,该异常处理程序是当try块中发生异常时要执行的代码块
  • ErrorHandle变量是异常对象的句柄。这可以是Caché为响应运行时错误而生成的异常对象,也可以是通过调用Throw命令显式发出的异常对象(将在下一节中介绍)。
  • 错误语句是在出现异常时调用的ObjectScript语句。
  • 其他语句是ObjectScript语句,如果没有异常,则跟随受保护语句的执行;如果存在异常且控制传递到CATCH块之外,则跟随错误语句的执行。

根据受保护语句执行期间的事件,会发生以下事件之一:

  • 如果没有发生错误,则继续执行出现在CATCH块之外的其他语句。
  • 如果确实发生错误,则控制传递到CATCH块并执行ERROR语句。然后,执行取决于CATCH块的内容:
    • 如果CATCH块包含THROWGOTO命令,则控制直接转到指定位置。
    • 如果CATCH块不包含THROWGOTO命令,则控制从CATCH块传出,并继续执行其他语句。

使用 THROWTRY-CATCH

当发生运行时错误时,Caché会发出隐式异常。要发出显式异常,可以使用Throw命令。Throw命令将执行从TRY块转移到CATCH异常处理程序。

Throw命令的语法为:

THROW expression

其中,expression 是从%Exception.AbstractException类继承的类的实例,Caché提供该类用于异常处理。

带有抛出的try/catch块的形式为:

 TRY {
      protected statements
      THROW expression
      protected statements
 }
 CATCH exception {
      error statements
 }
 further statements

其中,Throw命令显式地发出异常。try-catch块的其他元素如上一节所述。

抛出的效果取决于抛出发生的位置和抛出的参数:

  • TRY块内的引发会将控制权传递给CATCH块。
  • CATCH块内的抛出将控制向上传递到执行堆栈到下一个错误处理程序。如果异常是%Exception.SystemException对象,则下一个错误处理程序可以是任何类型(Catch或传统型);否则必须有一个Catch来处理异常,否则将抛出<NOCATCH>错误。

如果由于带有参数的引发而将控制传递到CATCH块,则ErrorHandle将包含该参数的值。如果由于系统错误而将控制传递到CATCH块,则ErrorHandle%Exception.SystemException对象。如果未指定ErrorHandle,则不会指示控制传递到CATCH块的原因。

例如,假设有将两个数字相除的代码:

div(num,div) public {	//d div^PHA.MOB.TEST(1,2)
 TRY {
  SET ans=num/div
  w ans,!
 } CATCH errobj {
	 b
  IF errobj.Name="<DIVIDE>" { SET ans=0 }
  ELSE { THROW errobj }
 }
 QUIT ans
DHC-APP>d div^PHA.MOB.TEST(1,0)
 
  b
  ^
<BREAK>div+5^PHA.MOB.TEST
DHC-APP 2d1>w errobj
1@%Exception.SystemException
DHC-APP 2d1>zw errobj
errobj=<OBJECT REFERENCE>[1@%Exception.SystemException]
+----------------- general information ---------------
|      oref value: 1
|      class name: %Exception.SystemException
| reference count: 2
+----------------- attribute values ------------------
|               Code = 18  <Set>
|               Data = ""  <Set>
|     InnerException = ""  <Set>
|           Location = "div+2^PHA.MOB.TEST"  <Set>
|               Name = "<DIVIDE>"  <Set>
|             iStack = $lb("^div+2^PHA.MOB.TEST^1","d^^^0")
+-----------------------------------------------------

如果发生被零除的错误,代码将专门设计为返回零作为结果。对于任何其他错误,抛出会将堆栈上的错误向上发送到下一个错误处理程序。

使用$$$ThrowOnError$$$ThrowStatus

Caché提供用于异常处理的宏。调用时,这些宏会向CATCH块抛出异常对象。

下面的示例在%Prepare()方法返回错误状态时调用$$$ThrowOnError()宏:

 #Include %occStatus
  ZNSPACE "SAMPLES"
  TRY {
    SET myquery = "SELECT TOP 5 Name,Hipness,DOB FROM Sample.Person"
    SET tStatement = ##class(%SQL.Statement).%New()
    SET status = tStatement.%Prepare(myquery)
    $$$ThrowOnError(status)
    WRITE "%Prepare succeeded",!
    RETURN
  }
  CATCH sc {
    WRITE "In Catch block",!
    WRITE "error code: ",sc.Code,!
    WRITE "error location: ",sc.Location,!
    WRITE "error data:",$LISTGET(sc.Data,2),!
  RETURN
  }
error code: 5540
error location: zOnAsStatus+1^%Exception.SQL.1
error data: 相对应的表中没有找到字段 'HIPNESS'^ SELECT TOP ? Name , Hipness ,

下面的示例在测试%Prepare()方法返回的错误状态值后调用$$$ThrowStatus

  #Include %occStatus
  ZNSPACE "SAMPLES"
  TRY {
    SET myquery = "SELECT TOP 5 Name,Hipness,DOB FROM Sample.Person"
    SET tStatement = ##class(%SQL.Statement).%New()
    SET status = tStatement.%Prepare(myquery)
    IF ($System.Status.IsError(status)) {
      WRITE "%Prepare failed",!
      $$$ThrowStatus(status) }
    ELSE {WRITE "%Prepare succeeded",!
      RETURN }
  }
  CATCH sc {
    WRITE "In Catch block",!
    WRITE "error code: ",sc.Code,!
    WRITE "error location: ",sc.Location,!
    WRITE "error data:",$LISTGET(sc.Data,2),!
  RETURN
  }
%Prepare failed
In Catch block
error code: 5540
error location: zOnAsStatus+1^%Exception.SQL.1
error data: 相对应的表中没有找到字段 'HIPNESS'^ SELECT TOP ? Name , Hipness ,


使用%Exception.SystemException%Exception.AbstractException

Caché提供用于异常处理的%Exception.SystemException%Exception.AbstractException类。%Exception.SystemException继承自%Exception.AbstractException类,用于系统错误。对于自定义错误,创建从%Exception.AbstractException继承的类。%Exception.AbstractException包含错误名称和发生位置等属性。

当在try块中捕获到系统错误时,Caché会创建%Exception.SystemException类的一个新实例,并将错误信息放入该实例中。引发自定义异常时,应用程序程序员负责使用错误信息填充对象。

异常对象具有以下属性:

  • Name — 错误名称,例如 <UNDEFINED>
  • Code — 错误代码
  • Location — 错误的标签+偏移量^例程位置
  • Data — 错误报告的任何额外数据,如导致错误的项的名称

try-catch的其他注意事项

下面描述了使用try-catch块时可能出现的情况。

try-catch块内退出

TRYCATCH块中的QUIT命令将控制从块传递到TRY-CATCH之后的下一条语句。

TRY-CATCH和执行堆栈

try块不会在执行堆栈中引入新级别。这意味着它不是新命令的作用域边界。错误语句的执行级别与错误的级别相同。如果受保护语句中有DO命令,并且DO目标也在受保护语句中,这可能会导致意外结果。在这种情况下,$ESTACK特殊变量可以提供有关相对执行级别的信息。

在传统错误处理中使用try-catch

TRY-CATCH错误处理与在执行堆栈的不同级别使用的$ZTRAP$ETRAP错误陷阱兼容。例外情况是$ZTRAP$ETRAP不能在TRY子句的受保护语句中使用。引发的用户定义错误仅限于try-catch。使用ZTRAP命令的用户定义的错误可以与任何类型的错误处理一起使用。

``Status`错误处理

Caché类库中的许多方法通过%status数据类型返回成功或失败信息。例如,用于保存%Persistent对象实例的%Save()方法返回一个%Status值,指示该对象是否已保存。

  • 成功执行方法会返回%Status1
  • 失败的方法执行返回%status作为包含错误状态以及一个或多个错误代码和文本消息的编码字符串。状态文本消息将根据的区域设置语言进行本地化。

可以使用%SYSTEM.Status类方法检查和操作%Status值。

Caché提供多个选项,用于以不同格式显示(写入)%STATUS编码字符串。

在下面的示例中,由于myquery文本中的错误,%Prepare失败:“ZOP”应该是“top”。此错误由IsError()方法检测,其他%SYSTEM.Status方法显示错误代码和文本:

  ZNSPACE "SAMPLES"
  SET myquery = "SELECT ZOP 5 Name,DOB FROM Sample.Person"
  SET tStatement = ##class(%SQL.Statement).%New()
  SET status = tStatement.%Prepare(myquery)
  IF ($System.Status.IsError(status)) {
      WRITE "%Prepare failed",!
      DO StatusError() }
  ELSE {WRITE "%Prepare succeeded",!
        RETURN }
StatusError()
  WRITE "Error #",$System.Status.GetErrorCodes(status),!
  WRITE $System.Status.GetOneStatusText(status,1),!
  WRITE "end of error display"
  QUIT

%Prepare failed
Error #5540
SQLCODE: -29消息:  相对应的表中没有找到字段 'ZOP'^ SELECT ZOP ?
end of error display

以下示例与上一个示例相同,不同之处在于%occStatus包含文件的$$$ISERR()宏会检测到状态错误。$$$ISERR()(反之亦然,$$$ISOK())只需检查%status是否=1:

#Include %occStatus
  ZNSPACE "SAMPLES"
  SET myquery = "SELECT ZOP 5 Name,DOB FROM Sample.Person"
  SET tStatement = ##class(%SQL.Statement).%New()
  SET status = tStatement.%Prepare(myquery)
  IF $$$ISERR(status) {
      WRITE "%Prepare failed",!
      DO StatusError() }
  ELSE {WRITE "%Prepare succeeded",!
        RETURN}
StatusError()
  WRITE "Error #",$System.Status.GetErrorCodes(status),!
  WRITE $System.Status.GetOneStatusText(status,1),!
  WRITE "end of error display"
  QUIT
%Prepare failed
Error #5540
SQLCODE: -29消息:  相对应的表中没有找到字段 'ZOP'^ SELECT ZOP ?
end of error display

某些方法(如%New())会生成,但不返回%Status%New()要么在成功时将OREF返回给类的实例,要么在失败时返回空字符串。可以通过访问%objlasterror系统变量来检索此类型方法的状态值,如下例所示。

  SET session = ##class(%CSP.Session).%New()
  IF session="" {
      WRITE "session oref not created",!
      WRITE "%New error is ",!,$System.Status.GetErrorText(%objlasterror),! }
  ELSE {WRITE "session oref is ",session,! }
session oref not created
%New error is 
错误 #5906: 缺少会话ID

创建%Status错误

可以使用error()方法从自己的方法调用系统定义的%Status错误。可以指定与要返回的错误消息相对应的错误号。

  WRITE "Here my method generates an error",!
  SET status = $System.Status.Error(20)
  WRITE $System.Status.GetErrorText(status),!
Here my method generates an error
错误 #20: 此文件已经存在

可以在返回的错误消息中包括%1%2%3参数,如下例所示:

  WRITE "Here my method generates an error",!
  SET status = $System.Status.Error(214,"3","^fred","BedrockCode")
  WRITE $System.Status.GetErrorText(status),!
Here my method generates an error
错误 #214:3个重复指针,第一个为指向BedrockCode的Global ^fred.

以将错误消息本地化为以首选语言显示。

  SET status = $System.Status.Error(30)
  WRITE "In English:",!
  WRITE $System.Status.GetOneStatusText(status,1,"en"),!
  WRITE "In French:",!
  WRITE $System.Status.GetOneStatusText(status,1,"fr"),!
In English:
the system is not part of the cluster
In French:
le système ne fait pas partie du cluster

可以使用通用错误代码83和5001指定与任何常规错误消息不对应的自定义消息。
可以使用AppendStatus()方法创建多条错误消息的列表。然后,可以使用GetOneErrorText()GetOneStatusText()按错误消息在此列表中的位置检索各个错误消息:

/// d ##class(PHA.TEST.ObjectScript).TestCatch3()
ClassMethod TestCatch3()
{
  SET st1 = $System.Status.Error(83,"my unique error")
  SET st2 = $System.Status.Error(5001,"my unique error")
  SET allstatus = $System.Status.AppendStatus(st1,st2)
  
  WRITE "All together:",!
  WRITE $System.Status.GetErrorText(allstatus),!!
  WRITE "One by one",!
  WRITE "First error format:",!
  WRITE $System.Status.GetOneStatusText(allstatus,1),!
  WRITE "Second error format:",!
  WRITE $System.Status.GetOneStatusText(allstatus,2),!
}
DHC-APP> d ##class(PHA.TEST.ObjectScript).TestCatch3()
All together:
错误 #83: 错误代码=my unique error
错误 #5001: my unique error
 
One by one
First error format:
错误代码=my unique error
Second error format:
my unique error


%SYSTEM.Error

%SYSTEM.Error类是一般错误对象。它可以从%STATUS错误、异常对象、$ZERROR错误或SQLCODE错误创建。可以使用%SYSTEM.Error类方法将%Status转换为异常,或将异常转换为%Status