Какой смысл объявлять переменную ErrObject, если может существовать только один объект error?

#vba #error-handling #ms-office

#vba #обработка ошибок #ms-office

Вопрос:

Мы все знаем, что в VBA может быть только один объект error.
Помогая коллеге с обработкой ошибок и объясняя, почему ему не следует использовать On Error Resume Next , у меня возникла идея:
сохраните объект error где-нибудь, чтобы позже ссылаться на него.

Рассмотрим этот фрагмент тестового кода:

 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
  

Он выведет 0 в немедленное окно, потому что On Error GoTo 0 сбрасывает объект error, а затем выводит 1, поскольку он все еще содержит ссылку на единственный объект error (?).

Если мы создадим новый класс и дадим ему некоторые свойства, относящиеся к ErrObject, например, так:

 (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
  

И создаем наш экземпляр следующим образом:

 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
  

Мы по-прежнему получаем 0 и 1 в качестве выходных данных соответственно.

Это подводит меня к моему вопросу: Какой смысл объявлять переменную как ErrObject, когда мы не можем создать сам новый объект, но он просто становится еще одним указателем на единственный объект error в VBA?

Ответ №1:

Вообще ничего.

Err часто рассматривается как своего рода глобальный ErrObject экземпляр, но правда в том, что это функция, которая возвращает единицу — как показано в обозревателе объектов:

введите описание изображения здесь

И эта функция реализована таким образом, что вы всегда получаете один и тот же объект.

Объектам необходимо предоставлять интерфейс для использования, и поэтому объект, возвращаемый Err функцией, предоставляет интерфейс ErrObject класса — это не означает, что ErrObject класс существует, чтобы его можно было создать или инкапсулировать пользовательским кодом: он просто предоставляет интерфейс для доступа к свойствам текущего состояния ошибки во время выполнения.

Когда вы инкапсулируете объект, ErrObject как вы это сделали, вы, по сути, просто предоставляете себе другой способ (помимо Err функции) доступа к ErrObject экземпляру — но это все тот же объект, содержащий свойства текущего состояния ошибки во время выполнения.

И когда свойства объекта изменяются, ваша инкапсулированная копия, которая указывает на этот объект, начнет сообщать новые значения, а старые, которые вы хотели «запомнить», будут перезаписаны.

Обратите внимание, что это верно для любого объекта, а не только ErrObject .

Допустим, у меня есть класс, который делает то, что вы делаете, с ErrObject ссылкой, но с 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
  

Если я создам экземпляр этого класса (назовем его Class1 ) и присвою c ему InternalCollection , а затем добавлю элементы в 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
  

Вывод — 2 , потому что c и InternalCollection (/ инкапсулированная coll ссылка) — это один и тот же объект, и это то, что происходит с вашим инкапсулированным ErrObject .

Решение состоит в том, чтобы не инкапсулировать ErrObject саму переменную, а вместо этого извлекать ее значения в резервные поля для свойств, доступных только для get, которые инкапсулируют состояние 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

'...
  

Теперь, полезно ли это, можно обсудить — IMO, если состояние используется в тот момент, когда глобальное состояние error уже содержит ту же информацию, нет необходимости это делать.

Класс мог бы довольно легко быть [ab] использован в качестве возвращаемого типа для a, Function который возвращает Nothing значение, указывающее на успех, и инкапсулированное состояние ошибки в случае сбоя — проблема в том, что язык разработан вокруг того, чтобы вызывать ошибки, а не возвращать их; слишком легко «запустить и забыть» такую функцию без проверки ее возвращаемого значения, и поскольку на сайте вызова фактическое состояние ошибки во время выполнения не приведет к отключению On Error инструкции, перенос состояния ошибки в виде программных данных не является идиоматическим, это создает «удивительный» API, который может легко привести к коду, который в конечном итоге игнорирует все ошибки.

Идиоматическая обработка ошибок имеет дело с глобальным состоянием ошибки во время выполнения как можно скорее и либо восстанавливается в той же области, либо позволяет состоянию ошибки всплывать в стеке вызовов туда, где оно может быть обработано. И до тех пор, пока ошибка не будет обработана, ErrObject состояние доступно через глобальную Err функцию.