VBA Excel错误处理 - 尤其是在函数中 - 专业的Excel开发风格

9
我买了Rob Bovey的书《专业Excel开发》,它让我眼界大开。
我正在重新调整我的代码,加入错误处理。然而,有很多我不理解的地方。我特别需要知道如何在函数中正确使用它。我使用Bovey的错误处理程序的重新抛出版本(在底部)。当我开始时,我使用基本的布尔值(非重新抛出)方法,并将我的子程序转换为布尔函数。(附言:根据答案,我将切换回布尔方法。)
我需要指导如何将函数融入到这个方案中。我希望它们能返回它们的真实值(例如字符串或双精度数),或者在某些情况下失败时返回-1,这样我就可以将它们嵌套在其他函数中,而不仅仅返回一个错误处理布尔值。
这是一个典型的对bDrawCellBorders(myWS)进行子程序调用的示例,它看起来在入口点内工作得很好。(也就是说,它是一个只被转换为函数的子程序,以便它可以返回一个布尔值给错误处理方案。)
Sub UpdateMe()  ' Entry Point

    Const sSOURCE As String = "UpdateMe()"

    On Error GoTo ErrorHandler

    Set myWS = ActiveCell.Worksheet
    Set myRange = ActiveCell
    myWS.Unprotect

' lots of code

    If Not bDrawCellBorders(myWS) Then ERR.Raise glHANDLED_ERROR    ' Call subroutine

' lots of code

ErrorExit:
    On Error Resume Next
    Application.EnableEvents = True
    myWS.Protect AllowFormattingColumns:=True
    Exit Sub

ErrorHandler:
    If bCentralErrorHandler(msMODULE, sSOURCE,,True) Then  ' Call as Entry Point
        Stop
        Resume
    Else
        Resume ErrorExit
    End If
End Sub

然而,我不知道如何将这个扩展到真实的函数。这是基于书中的一个例子,该例子是为一个子程序编写的,我只是将其改成了一个函数。
问题:
- 我该如何调用它?就像 x = sngDoSomeMath(17) 这样简单吗? - 它的错误处理会正常工作吗? - 在哪里或哪些地方应该使用 bReThrow=true 调用错误处理程序?
代码:
Public Function sngDoSomeMath(ByVal iNum As Integer) As Single
    
Dim sngResult As Single

Const sSOURCE As String = "sngDoSomeMath()"

On Error GoTo ErrorHandler

' example 1, input did not pass validation. don't want to 
' go up the error stack but just inform the
' calling program that they didn't get a good result from this 
' function call so they can do something else
If iNum <> 42 Then
    sngResult = -1    'function failed because I only like the number 42
    GoTo ExitHere
End If

' example 2, true error generated
sngResult = iNum / 0

sngDoSomeMath = lResult

ExitHere:
    Exit Function
ErrorHandler:

' Run cleanup code
'  ... here if any

' Then do error handling

If bCentralErrorHandler(msMODULE, sSOURCE, , , True) Then ' The true is for RETHROW
    Stop
    Resume
End If

End Function

错误处理程序例程:
'
' Description:  This module contains the central error
'               handler and related constant declarations.
'
' Authors:      Rob Bovey, www.appspro.com
'               Stephen Bullen, www.oaltd.co.uk
'
' Chapter Change Overview
' Ch#   Comment
' --------------------------------------------------------------
' 15    Initial version
'
Option Explicit
Option Private Module

' **************************************************************
' Global Constant Declarations Follow
' **************************************************************
Public Const gbDEBUG_MODE As Boolean = False    ' True enables debug mode, False disables it.
Public Const glHANDLED_ERROR As Long = 9999     ' Run-time error number for our custom errors.
Public Const glUSER_CANCEL As Long = 18         ' The error number generated when the user cancels program execution.


' **************************************************************
' Module Constant Declarations Follow
' **************************************************************
Private Const msSILENT_ERROR As String = "UserCancel"   ' Used by the central error handler to bail out silently on user cancel.
Private Const msFILE_ERROR_LOG As String = "Error.log"  ' The name of the file where error messages will be logged to.


''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Comments: This is the central error handling procedure for the
'           program. It logs and displays any run-time errors
'           that occur during program execution.
'
' Arguments:    sModule         The module in which the error occured.
'               sProc           The procedure in which the error occured.
'               sFile           (Optional) For multiple-workbook
'                               projects this is the name of the
'                               workbook in which the error occured.
'               bEntryPoint     (Optional) True if this call is
'                               being made from an entry point
'                               procedure. If so, an error message
'                               will be displayed to the user.
'
' Returns:      Boolean         True if the program is in debug
'                               mode, False if it is not.
'
' Date          Developer       Chap    Action
' --------------------------------------------------------------
' 03/30/08      Rob Bovey       Ch15    Initial version
'
Public Function bCentralErrorHandler( _
       ByVal sModule As String, _
       ByVal sProc As String, _
       Optional ByVal sFile As String, _
       Optional ByVal bEntryPoint As Boolean, _
       Optional ByVal bReThrow As Boolean = True) As Boolean

    Static sErrMsg As String

    Dim iFile As Integer
    Dim lErrNum As Long
    Dim sFullSource As String
    Dim sPath As String
    Dim sLogText As String

    ' Grab the error info before it's cleared by
    ' On Error Resume Next below.
    lErrNum = ERR.Number
    ' If this is a user cancel, set the silent error flag
    ' message. This will cause the error to be ignored.
    If lErrNum = glUSER_CANCEL Then sErrMsg = msSILENT_ERROR
    ' If this is the originating error, the static error
    ' message variable will be empty. In that case, store
    ' the originating error message in the static variable.
    If Len(sErrMsg) = 0 Then sErrMsg = ERR.Description

    ' We cannot allow errors in the central error handler.
    On Error Resume Next

    ' Load the default filename if required.
    If Len(sFile) = 0 Then sFile = ThisWorkbook.Name

    ' Get the application directory.
    sPath = ThisWorkbook.Path
    If Right$(sPath, 1) <> "\" Then sPath = sPath & "\"

    ' Construct the fully-qualified error source name.
    sFullSource = "[" & sFile & "]" & sModule & "." & sProc

    ' Create the error text to be logged.
    sLogText = "  " & sFullSource & ", Error " & _
               CStr(lErrNum) & ": " & sErrMsg

    ' Open the log file, write out the error information and
    ' close the log file.
    iFile = FreeFile()
    Open sPath & msFILE_ERROR_LOG For Append As #iFile
    Print #iFile, Format$(Now(), "mm/dd/yy hh:mm:ss"); sLogText
    If bEntryPoint Or Not bReThrow Then Print #iFile,
    Close #iFile

    ' Do not display or debug silent errors.
    If sErrMsg <> msSILENT_ERROR Then

        ' Show the error message when we reach the entry point
        ' procedure or immediately if we are in debug mode.
        If bEntryPoint Or gbDEBUG_MODE Then
            Application.ScreenUpdating = True
            MsgBox sErrMsg, vbCritical, gsAPP_NAME
            ' Clear the static error message variable once
            ' we've reached the entry point so that we're ready
            ' to handle the next error.
            sErrMsg = vbNullString
        End If

        ' The return vale is the debug mode status.
        bCentralErrorHandler = gbDEBUG_MODE

    Else
        ' If this is a silent error, clear the static error
        ' message variable when we reach the entry point.
        If bEntryPoint Then sErrMsg = vbNullString
        bCentralErrorHandler = False
    End If

    'If we're using re-throw error handling,
    'this is not the entry point and we're not debugging,
    're-raise the error, to be caught in the next procedure
    'up the call stack.
    'Procedures that handle their own errors can call the
    'central error handler with bReThrow = False to log the
    'error, but not re-raise it.
    If bReThrow Then
        If Not bEntryPoint And Not gbDEBUG_MODE Then
            On Error GoTo 0
            ERR.Raise lErrNum, sFullSource, sErrMsg
        End If
    Else
        'Error is being logged and handled,
        'so clear the static error message variable
        sErrMsg = vbNullString
    End If

End Function
3个回答

16

那是Rob写的一本很棒的书。

我个人关于错误处理(适用于过程或函数)的建议是基于KISS原则(即“保持简单,傻瓜”)。

了解你对错误处理程序的期望是什么?

通常这就是我希望/期望从我的错误处理程序中获得的内容……

  1. 发生错误的行数
  2. 错误号码
  3. 错误信息
  4. 如果适用,重置事件

让我们分解一下上面的内容。由于您现在已经知道您的错误处理程序的外观,因此请考虑以下示例。

Sub Sample()
    Dim i As Integer, j As Integer

    On Error GoTo Whoa

    Application.ScreenUpdating = False

    i = 1111111111

    For j = 1 To i
        Debug.Print ThisWorkbook.Sheets(1).Cells(i, 1).Value
    Next i

LetsContinue:
    Exit Sub
Whoa:
    MsgBox Err.Description
    Resume LetsContinue
End Sub

这是一个非常基本的错误处理程序,但对我没有太大的帮助。因此,让我们现在调整它使其更有用。如果您运行上面的代码,您会得到如下截图中显示的错误消息,如果您注意到,它并没有提供太多帮助。

enter image description here

现在我们来解决我在上面提到的所有问题所涉及的点

  1. 发生错误的代码行

有一个名为ERL的属性,很少有人知道它。您实际上可以使用它来获取发生错误的代码行号。为此,您必须确保给您的代码编号。请看这个例子。

Sub Sample()
    Dim i As Integer, j As Integer

10      On Error GoTo Whoa

20      Application.ScreenUpdating = False

30      i = 1111111111

40      For j = 1 To i
50          Debug.Print ThisWorkbook.Sheets(1).Cells(i, 1).Value
60      Next j

LetsContinue:
70      Exit Sub
Whoa:
80      MsgBox Erl
90      Resume LetsContinue
End Sub

当您运行上述代码时,您将得到以下结果

enter image description here

现在我知道错误发生在第30行,即i = 1111111111

下一步是继续进行:

  1. 错误编号
  2. 错误消息

可以从Err.NumberErr.Description中检索错误编号和错误消息。现在让我们结合ErlErr.NumberErr.Description

请参考此示例

Sub Sample()
    Dim i As Integer, j As Integer

10      On Error GoTo Whoa

20      Application.ScreenUpdating = False

30      i = 1111111111

40      For j = 1 To i
50          Debug.Print ThisWorkbook.Sheets(1).Cells(i, 1).Value
60      Next j

LetsContinue:
70      Exit Sub
Whoa:
80      MsgBox "The Error Happened on Line : " & Erl & vbNewLine & _
           "Error Message : " & Err.Description & vbNewLine & _
           "Error Number : " & Err.Number
90      Resume LetsContinue
End Sub
当您运行此代码时,您将获得类似于以下内容的东西。

enter image description here

您可以选择进一步自定义错误消息,使其更加用户友好。例如:
'~~> Message you want to deliver to the user in case the error happens
Const sMsg As String = "Please take a screenshot of this message and contact the developer for a resolution"
'~~> Title of your message box
Const sTitle As String = "Oopsie Daisies"

'~~> Change the above as applicable

Sub Sample()
    Dim i As Integer, j As Integer

10      On Error GoTo Whoa

20      Application.ScreenUpdating = False

30      i = 1111111111

40      For j = 1 To i
50          Debug.Print ThisWorkbook.Sheets(1).Cells(i, 1).Value
60      Next j

LetsContinue:
70      Exit Sub
Whoa:
80      MsgBox "The Error Happened on Line : " & Erl & vbNewLine & _
           "Error Message : " & Err.Description & vbNewLine & _
           "Error Number : " & Err.Number & vbNewLine & vbNewLine & _
           sMsg, vbCritical, sTitle
90      Resume LetsContinue
End Sub

这里输入图片描述

继续下一个:

如果适用,重置事件

当您使用事件并发生错误时,如果没有错误处理,则代码会中断。不幸的是,这不会重置事件。重置事件在错误处理程序中非常重要。

如果您注意到上面的代码中我们设置了 Application.ScreenUpdating = False。当代码中断时,该事件不会被重置。您必须在错误处理程序 LetsContinue 中处理它。请参见此示例。

'~~> Message you want to deliver to the user in case the error happens
Const sMsg As String = "Please take a screenshot of this message and contact the developer for a resolution"
'~~> Title of your message box
Const sTitle As String = "Oopsie Daisies"

'~~> Change the above as applicable

Sub Sample()
    Dim i As Integer, j As Integer

10      On Error GoTo Whoa

20      Application.ScreenUpdating = False

30      i = 1111111111

40      For j = 1 To i
50          Debug.Print ThisWorkbook.Sheets(1).Cells(i, 1).Value
60      Next j

LetsContinue:
70      Application.ScreenUpdating = True
80      Exit Sub
Whoa:
90      MsgBox "The Error Happened on Line : " & Erl & vbNewLine & _
           "Error Message : " & Err.Description & vbNewLine & _
           "Error Number : " & Err.Number & vbNewLine & vbNewLine & _
           sMsg, vbCritical, sTitle
100     Resume LetsContinue
End Sub

和Philippe一样,我也强烈建议您在VBA中使用MZ-Tools。我现在已经用它用了很多年了...

希望这可以帮助你。


3
有趣的答案。如果我是你,我会实现一些错误对象和一个error.add方法,该方法将自动将错误添加到txt文件、数据库甚至邮件中。这样可以避免“截屏”步骤,让你像专业人士一样管理调试任务! - Philippe Grondier
1
真的 :) 我想到了电子邮件选项,但我不确定用户是否可以在那台电脑上访问电子邮件。如果用户需要,我相信您建议的内容可以很容易地被整合进去。我的建议围绕着“保持简单”的原则。用户可以采纳以上建议并将其提升到更高的层次 :) - Siddharth Rout
1
Siddharth - 你在这个网站上是一个不可忽视的力量!感谢你帮助了如此多的人。你的回答是一个基本错误检查模型的优秀解决方案,很多人在查看这个问题时都不需要再去深究了。然而,我需要更强大的东西来满足我的公司客户需求,这就是为什么我正在尝试理解Bovey的错误处理技术的细节之处。我喜欢错误日志被发送到我的电子邮箱的想法。 - Shari W
@ShariW:你真是太好了 :) 关于邮件的问题,我唯一关心的是你将如何发送邮件?通过Outlook?CDO?Lotus Notes?你知道客户在使用哪种邮件吗?如果知道,那么我也可以给你提供相应的代码示例 :) - Siddharth Rout
邮件将是锦上添花。现在我必须为所有的函数配备错误处理,以便与我正在使用的处理程序系统配合工作。 - Shari W
@ShariW:你没有在上一条评论中回答我的问题 :) - Siddharth Rout

5

我需要在这种特定技术上得到更多帮助,所以我直接去找了Mr. Bovey,并且他很慷慨地回复我。他允许我将他的回复发布到StackOverflow社区。

以下说明涉及到他在书中描述的函数“布尔错误处理”技术作为首选的错误处理方法,而不是备选的“重新抛出方法”。


嗨,Shari,

关于VBA函数的错误处理问题,你可能会遇到三种情况:

1)函数非常简单,不需要错误处理程序。如果这样的函数发生错误,它将溢出到调用过程的错误处理程序中,但这种情况极少发生。

2)一个非平凡的函数需要一个错误处理程序,并使用书中描述的布尔返回值系统。函数需要返回的任何其他值都通过ByRef参数返回。我编写的大多数函数都属于这种情况。有些事情你不能直接用这种函数做,例如将它们直接馈送到另一个函数的参数中,但为了实现防弹错误处理,我认为这是一个好的权衡。

3)一个非平凡的函数需要一个错误处理程序,并且必须返回与其错误状态无关的值。这是一种罕见的情况,因为我可以通过重构代码将99%以上的这些情况转换为第二种情况。如果你无法这样做,那么你唯一的选择就是选择一个超出正常返回值范围的任意返回值,并将其用于指示发生了错误。如果调用此函数的人看到这个任意错误标志值,它就知道不能继续执行。

Rob Bovey Application Professionals http://www.appspro.com/


代码示例(Shari W)


' Show how to call a function using this error handling method.
Const giBAD_RESULT As Integer = -1

Function TestMath()   ' An Entry Point

    Dim sngResult As Single
    Dim iNum As Integer

    ' Call the function, actual result goes in sngResult but it returns the error handling boolean.
    ' A true error like Div 0 will go to error handler.

    ' Set Up Error Handling for Entry Point
    Application.EnableCancelKey = xlErrorHandler
    Dim bUserCancel As Boolean
    Const sSOURCE As String = "TestMath()"
    On Error GoTo ErrorHandler
    ' End Error Set Up

    iNum = 0   ' Try 0 to create error
    If Not bDoSomeMath(iNum, sngResult) Then ERR.Raise glHANDLED_ERROR
    ' If function does parameter checking and wants to return a bad input code, check for that.
    If sngResult = giBAD_RESULT Then
        MsgBox ("Bad input to bDoSomeMath " & iNum)
    Else
        MsgBox ("I believe the answer is " & sngResult)
    End If

ErrorExit:
    On Error Resume Next
    Exit Function

ErrorHandler:
    If bCentralErrorHandler(msMODULE, sSOURCE, , True) Then
        Stop
        Resume
    Else
        Resume ErrorExit
    End If
End Function
Function bDoSomeMath(ByVal iNum As Integer, ByRef sngResult As Single) As Boolean

    ' Error handling Set Up
    Dim bReturn As Boolean
    Const sSOURCE As String = "bDoSomeMath()"
    On Error GoTo ErrorHandler
    bReturn = True
    ' End Error Set Up

    If iNum < 0 Or iNum > 1000 Then
        sngResult = giBAD_RESULT   'function failed because I only like the numbers 0 to 1000
        GoTo ErrorExit
    Else
        sngResult = 100 / iNum   ' generate a true error by iNum = 0
    End If

ErrorExit:
    On Error Resume Next
    bDoSomeMath = bReturn
    Exit Function

ErrorHandler:
    bReturn = False
    If bCentralErrorHandler(msMODULE, sSOURCE, , , True) Then
        Stop
        Resume
    Else
        Resume ErrorExit
    End If

End Function

2

在这里可以找到一份关于VBA错误处理管理的建议

相同的工具(MZ-Tools)和方法(标准/通用错误处理程序),可用于构建自动化错误报告系统,也适用于Excel。


MZ-Tools是一个很棒的工具。我用Rob Bovey的版本替换了它们标准的简单ErrorHandler(如上所述)。只需单击一下,我就可以将标题和错误处理程序粘贴到例程中,即使我忘记从它们开始。但是我需要更多关于函数顶级错误处理的具体信息!谢谢。 - Shari W
+1 MZ Tools上的好建议 :) - Siddharth Rout
谢谢大家,但我仍然希望使用这些方法的人能够加入并纠正我!那个网站上的示例和随附MZ-Tools的示例都太基础了(我已经在MZ-Tools选项中替换了它们)。 - Shari W

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接