#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
функцию.