What is the use of declaring an ErrObject variable if there can only ever exist one error object?

廉价感情. 提交于 2021-01-28 05:09:56

问题


We all know there can only ever be one error object in VBA.
While helping a co-worker with error handling and why he shouldn't use On Error Resume Next I had an idea:
Store the error object somewhere to later reference back to it.

Consider this piece of test code:

Sub Test()
    Dim t As ErrObject
    On Error Resume Next
    Err.Raise 1
    Set t = Err
    On Error GoTo 0
    Debug.Print t.Number
    On Error Resume Next
    Err.Raise 1
    Debug.Print t.Number
End Sub

It will print 0 to the immediate window because On Error GoTo 0 resets the error object and then prints 1 since it still holds a reference to the only error object (?).

If we create a new class and give it some properties pertaining to the ErrObject like so:

(TestClass)
Option Explicit
Public oError As ErrObject
Private Sub Class_Initialize(): End Sub
Private Sub Class_Terminate()
    If Not oError Is Nothing Then Set oError = Nothing
End Sub
Public Property Get Error()
    Error = oError
End Property
Public Property Set Error(ByVal ErrorObject As ErrObject)
    Set oError = ErrorObject
End Property

And create our instance like this:

Sub Test2()
    Dim t As TestClass
    On Error Resume Next
    Set t = New TestClass
    Err.Raise 1
    Set t.Error = Err
    On Error GoTo 0
    Debug.Print t.oError.Number
    On Error Resume Next
    Err.Raise 1
    Debug.Print t.oError.Number
End Sub

We still get 0 and 1 as output respectively.

This bringst me to my question: What is the use of declaring a variable as ErrObject when we cannot create a new object itself but it simply becomes another pointer to the only error object in VBA?


回答1:


None whatsoever.

Err is often treated as some kind of global ErrObject instance, but the truth is, it's a function that returns one - as revealed in the object browser:

And that function is implemented in such a way, that you always get the same object.

Objects need to expose an interface to be usable, and so the object returned by the Err function exposes that of the ErrObject class - it doesn't mean the ErrObject class exists so that it can be instantiated or encapsulated by user code: it merely provides an interface to access the properties of the current run-time error state.

When you encapsulate an ErrObject like you did, you're essentially just giving yourself another way (besides the Err function) to access the ErrObject instance - but it's still the exact same object holding the properties of the current run-time error state.

And when an object's properties change, your encapsulated copy that points to that object is going to start reporting the new values, and the old ones you meant to "remember" are overwritten.

Note that this is true for any object, not just ErrObject.

Say I have a class that does what you're doing with the ErrObject reference, but with a Collection:

Private coll As Collection

Public Property Set InternalCollection(ByVal c As Collection)
    Set coll = c
End Property

Public Property Get InternalCollection() As Collection
    Set InternalCollection = coll
End Property

If I make an instance of that class (let's call it Class1) and assign c to its InternalCollection, and then add items to c...

Dim c As Collection
Set c = New Collection
With New Class1
    Set .InternalCollection = c

    c.Add 42
    .InternalCollection.Add 42

    Debug.Print .InternalCollection.Count
End With

The output is 2, because c and InternalCollection (/the encapsuated coll reference) are the very same object, and that's what's happening with your encapsulated ErrObject.

The solution is to not encapsulate the ErrObject itself, but rather pull its values into backing fields for get-only properties that encapsulate the state of the ErrObject:

Private errNumber As Long
Private errDescription As String
'...

Public Sub SetErrorInfo() 'note: an ErrObject argument would be redundant!
    With Err
        errNumber = .Number
        errDescription = .Description
        '...
    End With
End Sub

Public Property Get Number() As Long
    Number = errNumber 
End Property

Public Property Get Description() As String
    Description = errDescription
End Property

'...

Now, whether that's useful is up for debate - IMO if the state is consumed at a moment where the global error state already contains the same information, there's no need to do this.

The class could pretty easily be [ab]used as a return type for a Function that returns Nothing to indicate success, and the encapsulated error state in case of failure - the problem is that the language is designed around raising errors rather than returning them; it's too easy to "fire-and-forget" such a function without verifying its return value, and since at the call site the actual runtime error state isn't going to trip an On Error statement, carrying error state as program data isn't idiomatic, it makes a "surprising" API that can easily result in code that ends up ignoring all errors.

Idiomatic error handling deals with the global runtime error state as soon as possible, and either recovers in the same scope, or lets the error state bubble up the call stack to where it can be handled. And until the error is handled, the ErrObject state is accessible through the global Err function.



来源:https://stackoverflow.com/questions/55066376/what-is-the-use-of-declaring-an-errobject-variable-if-there-can-only-ever-exist

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!