Traceback в VBA? ЧТО?

O_o

O_o

Как часто вы ловите ошибки в VBA?
А как часто вам приходится пытаться понять откуда ноги растут?

Если макрос состоит из одной процедуры, это немного другая история…
Но вот если у вас полноценный стек вызовов, когда:

Main() -> NestedSub1 -> NestedFunc -> NestedSub2 ... -> NestedSubN

как отловить, в каком произошла ошибка?
Окей, вы скажите «Поставим On Error GoTo Catch и в Catch: Debug.Print "Function name"», да?

949f3f1c2e877e2d04ed4ae1ce3ce526.png

А если эту функцию вызывают несколько разных Sub/Function, как понять в каком из них произошла ошибка?

В «нормальных» (не обрезанных) языках программирования для этого придуманы Traceback’и, которые после ошибки выводят информацию о вызовах, которые привели программу к ошибке. VBA, к сожалению, лишен этой плюшки. Так как же быть?

Изобретать костыли, конечно

Предлагаю вашему вниманию не самый умный и ленивый и прямолинейный, но все таки достаточно наглядный и действенный способ.

Итак, что нам потребуется:

  1. Модуль Exception.

  2. Три процедуры: 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

Описание:

  1. Делаем «проверки на дурака».

  2. Далее, вычисляем стартовый индекс: если LastN больше Trace или Trace - LastN < 1, тогда стартовый индекс 1 (т.к. коллекция стартует с 1), иначе индекс = Trace - LastN + 1.

  3. Дальше в цикле вытаскиваем значения из Trace, начиная со стартового индекса.

  4. Потом извлекаем текст ошибки в подготовленный шаблон #{number}: [{source}] {message}.

  5. И в итоге выводим результат в вбансоль.

Как пользоваться?

А, как я и писал выше, все очень просто:

  1. В начале каждой (ну или только отслеживаемой) процедуры вызываем:
    Exception.PushTrace "" ' подставить свои данные

  2. В конце каждой процедуры, непосредственно перед чистым выходом (без ошибки), это важно:
    Exception.PopTrace

  3. На основную процедуру вешаем 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’шную (и не только) всячину.

Habrahabr.ru прочитано 872 раза