Traceback в VBA? ЧТО?
O_o
Как часто вы ловите ошибки в VBA?
А как часто вам приходится пытаться понять откуда ноги растут?
Если макрос состоит из одной процедуры, это немного другая история…
Но вот если у вас полноценный стек вызовов, когда:
Main() -> NestedSub1 -> NestedFunc -> NestedSub2 ... -> NestedSubN
как отловить, в каком произошла ошибка?
Окей, вы скажите «Поставим On Error GoTo Catch
и в Catch: Debug.Print "Function name"
», да?
А если эту функцию вызывают несколько разных Sub/Function, как понять в каком из них произошла ошибка?
В «нормальных» (не обрезанных) языках программирования для этого придуманы Traceback’и, которые после ошибки выводят информацию о вызовах, которые привели программу к ошибке. VBA, к сожалению, лишен этой плюшки. Так как же быть?
Изобретать костыли, конечно
Предлагаю вашему вниманию не самый умный и ленивый и прямолинейный, но все таки достаточно наглядный и действенный способ.
Итак, что нам потребуется:
Модуль
Exception
.Три процедуры:
PushTrace
,PopTrace
,PrintTrace
Собсна все.
Начнем изобретать с процедуры PushTrace
.
PushTrace
Что она будет делать? Копить в коллекцию Trace
переданные вами данные, например «Модуль.Процедура» в которой мы сейчас находимся.
Public Sub PushTrace(ParamArray Args() As Variant)
If this.Trace Is Nothing Then Set this.Trace = New Collection
' Разделитель и время в начале можно выбрать на свое усмотрение
this.Trace.Add DateTime.Now & ": " & Strings.Join(Args, " ")
End If
Забыл уточнить, в верху модуля надо объявить приватные UDT TException
и переменную this
:
Private Type TException
Trace As Collection
End Type
Private this As TException
Идем далее…
PopTrace
Что будет делать PopTrace
? Правильно — удалять последнюю запись в коллекции Trace
:
Public Sub PopTrace()
If this.Trace Is Nothing Then Exit Sub
this.Trace.Remove this.Trace.Count ' В принципе, можно еще возвращать значение,
' но я не знаю зачем :D
End If
PrintTrace
Ну сейчас придется немного поднапрячься… Как вы уже догадались, PrintTrace печатает стек вызовов. Чтобы не засорить кучей мусора вбансоль (immediate window), я решил добавить опциональный аргумент LastN со значением по умолчанию 10. Это значит, что печататься будут только LastN (если не указано — 10) последних вызовов:
Public Sub PrintTrace(Optional ByVal LastN As Long = 10)
If this.Trace Is Nothing Then Exit Sub
If this.Trace.Count = 0 Then Exit Sub
If LastN < 1 Then Exit Sub
Dim Index As Long
If LastN > this.Trace.Count Or this.Trace.Count - LastN < 1 Then
Index = 1
Else
Index = this.Trace.Count - LastN + 1
End If
Dim TraceString As String
Dim T As Variant
Dim Sep As String
Do While this.Trace.Count > 0
If this.Trace.Count < Index Then Exit Do
T = this.Trace(Index)
Index = Index + 1
TraceString = TraceString & Sep & T
Sep = VbNewLine
Loop
Dim ErrMsg As String
ErrMsg = "#{number}: [{source}] {message}"
ErrMsg = Strings.Replace(ErrMsg, "{number}", Err.Number)
ErrMsg = Strings.Replace(ErrMsg, "{source}", Err.Source)
ErrMsg = Strings.Replace(ErrMsg, "{message}", Err.Description)
TraceString = TraceString & Sep & Sep & ErrMsg
Debug.Print "Traceback:"
Debug.Print TraceString
End Sub
Описание:
Делаем «проверки на дурака».
Далее, вычисляем стартовый индекс: если
LastN
большеTrace
илиTrace - LastN < 1
, тогда стартовый индекс 1 (т.к. коллекция стартует с 1), иначе индекс =Trace - LastN + 1
.Дальше в цикле вытаскиваем значения из Trace, начиная со стартового индекса.
Потом извлекаем текст ошибки в подготовленный шаблон
#{number}: [{source}] {message}
.И в итоге выводим результат в вбансоль.
Как пользоваться?
А, как я и писал выше, все очень просто:
В начале каждой (ну или только отслеживаемой) процедуры вызываем:
Exception.PushTrace "
" ' подставить свои данные В конце каждой процедуры, непосредственно перед чистым выходом (без ошибки), это важно:
Exception.PopTrace
На основную процедуру вешаем
On Error GoTo Catch
и вCatch
вызываем:Exception.PrintTrace ' при этом можно ограничить вывод с помощью LastN
Пример:
' Точка входа
Public Sub Main()
On Error GoTo Catch
Exception.PushTrace "Module.Main" ' Положили в Trace
DoStuff
Exception.PopTrace ' Удалили из Trace
Exit Sub
Catch:
Exception.PrintTrace LastN:=3
End Sub
Public Sub DoStuff()
Exception.PushTrace "Module.DoStuff" ' Положили в Trace
DoOtherStuff
Exception.PopTrace ' Удалили из Trace
End Sub
Public Sub DoOtherStuff()
Exception.PushTrace "Module.DoOtherStuff" ' Положили в Trace
DoSomeERRORStuff
Exception.PopTrace ' Удалили из Trace
End Sub
Public Sub DoSomeERRORStuff()
Exception.PushTrace "Module.DoSomeERRORStuff" ' Положили в Trace
DoSomeOtherStuff ' Эта процедура выполнится без ошибки
' поэтому в Trace останется только
' текущая процедура, т.к. ниже ошибка
Err.Raise 9, Source:="DoSomeERRORStuff", Description:="THIS IS THE ERROR"
Exception.PopTrace ' Недостижимый код
End Sub
Public Sub DoSomeOtherStuff()
Exception.PushTrace "Module.DoSomeOtherStuff"
DoSomeOtherStuff
Exception.PopTrace ' Удалили из Trace
End Sub
На выходе получим что-то вроде:
Traceback:
01.01.1970 0:00:00: Module.DoStuff
01.01.1970 0:00:00: Module.DoOtherStuff
01.01.1970 0:00:00: Module.DoSomeERRORStuff
#9: [DoSomeERRORStuff] THIS IS THE ERROR
Обратите внимание, что так как мы поставили ограничитель LastN = 3
, вывелось только три последних Trace.
Как-то так, надеюсь будет полезно :)
P.S. Я, кстати, еще в телеграм иногда пишу всякую vba’шную (и не только) всячину.