Таблицы интерферируют с переменными диапазона VBA в зависимости от области

файл Excel включает VBA-кодированные пользовательские функции (UDFs), развернутые в таблицах (VBA listobjects). Теперь по причинам, которые ускользают от меня, если модуль UDF содержит ряд переменные, объявленные вне области действия любого sub или функции, я получаю очень резкое предупреждение при открытии файла:"автоматическая ошибка-катастрофический сбой".

"катастрофический" кажется преувеличением, потому что после того, как предупреждение отклонено, файл, кажется, работает правильно. Но я все равно хотел бы понять, в чем проблема. Мне удалось реплицировать проблему с примером MVC следующим образом. Я запускаю Excel 2016 (Обновлено) в Windows 10.

есть две таблицы (т. е. VBA listobjects):Таблица 1 списки "предметы" и Таблица 2 списки "компоненты" (обе таблицы были созданы путем выбора данных и нажатия кнопки Table на Insert tab). Таблица 2 имеет UDF под названием ITEM_NAME() в поле Item_Name что возвращает имя элемента в зависимости от идентификатора элемента, см. скриншот:

enter image description here

функции ITEM_NAME() по существу является оберткой вокруг индекса и соответствия функций обычного листа, как в следующем коде:

Option Explicit

Dim mrngItemNumber As Range
Dim mrngItemName As Range

Public Function ITEM_NAME(varItemNumber As Variant) As String
' Returns Item Name as a function of Item Number.
    Set mrngItemNumber = Sheets(1).Range("A4:A6")
    Set mrngItemName = Sheets(1).Range("B4:B6")
    ITEM_NAME = Application.WorksheetFunction.Index(mrngItemName, _
    Application.WorksheetFunction.Match(varItemNumber, mrngItemNumber))
End Function

Итак, повторяю, с этой настройкой я получаю ошибку автоматизации при открытии файла. Но ошибка исчезает, когда я делаю одно из следующих действий:

  1. переместить объявления в сферу действия функции. Это решение не является привлекательным, так как требует гораздо больше строк кода, по одной для каждого UDF, и их много.

  2. измените тип переменной с ряд к чему-то еще, например целое (таким образом, функция, очевидно, не будет работать).

  3. преобразовать таблицу 2 в обычный диапазон (т. е. удалить таблицу). Это также неудобное решение, так как я действительно хочу использовать функции таблицы для других целей в моем коде.

  4. удалить функции ITEM_NAME() из таблицы 2. (Очевидно, нет привлекательного варианта..)

что происходит? Почему я получаю сообщение об ошибке? И почему файл по-прежнему работает правильно, несмотря на предупреждение? Есть ли обходной путь, который я пропустил?

я подозреваю, что это может иметь какое-то отношение к взаимодействию объектов листа и listobjects, но не конечно. Возможная подсказка предоставляется в ответ другой вопрос:

если вы хотите ссылаться на таблицу без использования листа, вы можете использовать Хак Application.Range(ListObjectName).ListObject.

Примечание: этот хак зависит от того, что Excel всегда создает именованный диапазон для DataBodyRange таблицы с тем же именем, что и таблица.

аналогичные проблемы были сообщены в другом месте (at Stackoverflow и Microsoft Technet), но не с этим конкретным вкусом. Предлагаемые решения включают проверку сломанных ссылок или других процессов, запущенных в фоновом режиме, и я сделал это безрезультатно. Я также могу добавить, что не имеет значения, является ли функция ITEM_NAME вводится после создания таблицы 2, а не раньше; единственное отличие заключается в том, что он использует структурированные ссылки в этом случае (как на скриншоте выше.)

обновление: вдохновленный комментариями @SJR ниже, я попробовал следующую вариацию кода, где ListObject переменная объявляется для хранения таблицы "Items". Обратите внимание, что ряд объявления теперь находятся внутри области действия функции, и это только ListObject декларация снаружи. Это и генерирует ту же ошибку, автоматизации!

Option Explicit

Dim mloItems As ListObject

Public Function ITEM_NAME(varItemNumber As Variant) As String
' Returns Item Name as a function of Item Number.
    Dim rngItemNumber As Range
    Dim rngItemName As Range
    Set mloItems = Sheet1.ListObjects("Items")
    Set rngItemNumber = mloItems.ListColumns(1).DataBodyRange
    Set rngItemName = mloItems.ListColumns(2).DataBodyRange
    ITEM_NAME = Application.WorksheetFunction.Index(rngItemName, _
    Application.WorksheetFunction.Match(varItemNumber, rngItemNumber))
End Function

обновление 2:

я решил попробовать создать новую версию примере ВКМ, где, вдохновленный AndrewD по ответ ниже, я .ListObjects() установить диапазон, вместо использования .Range(). Это действительно сработало. Вероятно, я адаптирую это решение для своей работы (но см. мои комментарии под вопросом Эндрюда, объясняющие, почему я могу предпочесть .Range().)

чтобы дважды проверить, что это решение сработало, я приступил к созданию двух новых файлов, один из которых будет реплицировать мой собственный пример, как описано выше, и тот, где единственной разницей будет переключение на ListObjects(). В процессе я отметил, что я действительно отступил Range объявления в начале кода в моем исходном файле, например:

Option Explicit

    Dim mrngItemNumber As Range
    Dim mrngItemName As Range

Public Function ITEM_NAME(...

не задумываясь об этом, я создал новый файл, но без отступа. Так что это будет точная копия предыдущего файла (и приведенного выше примера), но без отступ. Но вот, с этим файлом Я не смог повторить ошибку автоматизации! После проверки обоих файлов я отметил, что единственное различие действительно было отступом, поэтому я снова положил отступ в новый файл, ожидая, что он снова создаст ошибку автоматизации. Но проблема так и не возникла. Итак, затем я удалил отступ из первого файла (используется для создания примера выше), и теперь ошибка автоматизации исчезла из этого файла. Вооружившись этим наблюдением, я вернулся к своему настоящему досье, где я впервые обнаружил проблему и просто удалил отступ там тоже. И это сработало.

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

спасибо всем, кто нашел время, чтобы посмотреть на это и поделиться ценным помыслы.

3 ответов


объявление переменных уровня модуля просто для сохранения двух строк в каждом UDF, которые в противном случае потребовались бы, действительно плохая практика кодирования. Однако, если это ваше мышление, почему бы не пойти до конца и сохранить четыре строк в UDF, избегая задание их в каждом, Как хорошо!

вы можете сделать это, используя псевдо-постоянные функции, как показано в следующем коде:

Option Explicit

Private Function rng_ItemNumber() As Range
    Set rng_ItemNumber = Sheet1.Range("A4:A6")
End Function
Private Function rng_ItemName() As Range
    Set rng_ItemName = Sheet1.Range("B4:B6")
End Function

Public Function ITEM_NAME(varItemNumber As Variant) As String
' Returns Item Name as a function of Item Number.
  With Application.WorksheetFunction
    ITEM_NAME = .Index(rng_ItemName, .Match(varItemNumber, rng_ItemNumber))
  End With
End Function

стоимость, конечно, есть накладные расходы на вызов функции.


если вы планируете использовать ListObject класс для окончательного дизайна, тогда почему бы не использовать его сейчас,и используйте динамические именованные диапазоны (жестко закодированные диапазоны в примере есть, поэтому он фактически работает как есть - они должны быть заменены именованными диапазонами):

Option Explicit

Private Function str_Table1() As String
    Static sstrTable1 As String
    If sstrTable1 = vbNullString Then
      sstrTable1 = Sheet1.Range("A4:B6").ListObject.Name
    End If
    str_Table1 = sstrTable1
End Function
Private Function str_ItemNumber() As String
    Static sstrItemNumber As String
    If sstrItemNumber = vbNullString Then
      sstrItemNumber = Sheet1.Range("A4:A6").Offset(-1).Resize(1).Value2
    End If
    str_ItemNumber = sstrItemNumber
End Function
Private Function str_ItemName() As String
    Static sstrItemName As String
    If sstrItemName = vbNullString Then
      sstrItemName = Sheet1.Range("B4:B6").Offset(-1).Resize(1).Value2
    End If
    str_ItemName = sstrItemName
End Function

Public Function ITEM_NAME(varItemNumber As Variant) As String
  'Returns Item Name as a function of Item Number.
  Dim ƒ As WorksheetFunction: Set ƒ = WorksheetFunction
  With Sheet1.ListObjects(str_Table1)
    ITEM_NAME _
    = ƒ.Index _
      ( _
        .ListColumns(str_ItemName).DataBodyRange _
      , ƒ.Match(varItemNumber, .ListColumns(str_ItemNumber).DataBodyRange) _
      )
  End With
End Function

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

обратите внимание, что использование статических переменных не требуется, но необходимо сократить время выполнения. (Статические переменные также могли быть использованы в первом примере, но я оставил их, чтобы сохранить его коротким.)

вероятно, нет необходимости извлекать имена таблиц в псевдо-константы, но я сделал это для полноты сакэ.


EDIT: (v2)

следуя двум блестящим предложениям Egalth, приводит к следующему коду, который устраняет необходимость в именованных диапазонах,или даже жестко адреса ячеек, в целом, как мы используем встроенный динамизм самой таблицы ListObject.

я также изменил имя параметра, чтобы соответствовать * имя заголовка соответствующего столбца, поэтому, когда пользователь нажимает Ctrl+ Shift+A появится подсказка, какой столбец использовать. (Этот совет и, при необходимости, дополнительная информация о том, как добавить IntelliSense tool-tips и/или получить описание, чтобы появиться в диалоговом окне аргументы функции, можно увидеть здесь.)

Option Explicit

Private Function str_Table1() As String
    Static sstrTable1 As String
    If sstrTable1 = vbNullString Then sstrTable1 = Sheet1.ListObjects(1).Name ' or .ListObjects("Table1").Name
    str_Table1 = sstrTable1
End Function
Private Function str_ItemNumber() As String
    Static sstrItemNumber As String
    If sstrItemNumber = vbNullString Then
      sstrItemNumber = Sheet1.ListObjects(str_Table1).HeaderRowRange(1).Value2
    End If
    str_ItemNumber = sstrItemNumber
End Function
Private Function str_ItemName() As String
    Static sstrItemName As String
    If sstrItemName = vbNullString Then
      sstrItemName = Sheet1.ListObjects(str_Table1).HeaderRowRange(2).Value2
    End If
    str_ItemName = sstrItemName
End Function

Public Function ITEM_NAME(ByRef Item_ID As Variant) As String
  'Returns Item Name as a function of Item Number.
  Dim ƒ As WorksheetFunction: Set ƒ = WorksheetFunction
  With Sheet1.ListObjects(str_Table1)
    ITEM_NAME _
    = ƒ.Index _
      ( _
        .ListColumns(str_ItemName).DataBodyRange _
      , ƒ.Match(Item_ID, .ListColumns(str_ItemNumber).DataBodyRange) _
      )
  End With
End Function

обратите внимание на использование .Value2. Я всегда использовал .Value2 С тех пор, как я узнал о перетаскивании производительности и других проблемах, вызванных неявным преобразованием типа, выполненным при использовании .Value (или при использовании его в качестве свойства по умолчанию).

* не забудьте обновить имена заголовков столбцов в коде, когда логика/дизайн проекта будет завершен.


EDIT: (перезагрузить)

перечитывая ваши собственные комментарии к вашему опубликованному вопросу, я отметил этот:

я мог бы принять этот подход в конечном итоге, но я все еще в процессе проектирования и перемещения столбцы вокруг много, поэтому номер индекса также может измениться

в то время как последний пример выше позволяет динамически изменять имена заголовков, перемещение/вставка столбцов изменяет индексы, требуя изменения кода.

Похоже, мы вернулись к использованию именованных диапазонов. Однако на этот раз нам нужны только статические, указывающие на столбец заголовки.

также оказывается, что для этого нового случая статические переменные являются плохо идея на стадии проектирования. Поскольку индексы столбцов кэшируются, вставка нового столбца разрывает UDF до сброса проекта.

я также включил сокращенную версию таблицы без листа ссылки Хак из цитаты в вашем опубликованном вопросе:

Option Explicit

Private Function str_Table1() As String
    str_Table1 = Sheet1.ListObjects(1).Name
End Function
Private Function str_ItemNumber() As String
    With Range(str_Table1).ListObject
      str_ItemNumber = .HeaderRowRange(.Parent.Range("A3").Column - .HeaderRowRange.Column + 1).Value2
    End With
End Function
Private Function str_ItemName() As String
    With Range(str_Table1).ListObject
      str_ItemName = .HeaderRowRange(.Parent.Range("B3").Column - .HeaderRowRange.Column + 1).Value2
    End With
End Function

Public Function ITEM_NAME(ByRef Item_ID As Variant) As String
  'Returns Item Name as a function of Item Number.
  Dim ƒ As WorksheetFunction: Set ƒ = WorksheetFunction
  With Range(str_Table1).ListObject
    ITEM_NAME _
    = ƒ.Index _
      ( _
        .ListColumns(str_ItemName).DataBodyRange _
      , ƒ.Match(Item_ID, .ListColumns(str_ItemNumber).DataBodyRange) _
      )
  End With
End Function

обратите внимание, что вы не можете использовать Item_name для одного из именованных диапазонов, так как он совпадает с UDF (случай игнорируется). Я предлагаю использовать конечное подчеркивание, например,Item_name_, для именованный диапазон.


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


OK. Этот обходной путь должны работа.

если когда это происходит, есть несколько вопросов и предостережений для решения.

Я напишу пояснения.

установить код ThisWorkbook модуль.

код:

Private Sub Workbook_BeforeClose(Cancel As Boolean)

  Dim rngCell As Range

  For Each rngCell In ActiveSheet.UsedRange.SpecialCells(xlCellTypeFormulas)
    With rngCell
      If .FormulaR1C1 Like "*ITEM_NAME*" _
      And Left$(.FormulaR1C1, 4) <> "=T(""" _
      Then
        .Value = "=T(""" & .FormulaR1C1 & """)"
      End If
    End With
  Next rngCell

End Sub

Private Sub Workbook_Open()

  Dim rngCell As Range

  For Each rngCell In ActiveSheet.UsedRange.SpecialCells(xlCellTypeFormulas)
    With rngCell
      If .FormulaR1C1 Like "*ITEM_NAME*" _
      And Left$(.FormulaR1C1, 4) = "=T(""" _
      Then
        .FormulaR1C1 = .Value
      End If
    End With
  Next rngCell

End Sub

на чисто кодовом уровне зачем объявлять переменные модульного уровня для хранения диапазонов, когда вы устанавливаете их каждый раз? Если вы кэшировали ссылки и только устанавливали их, если я ничего не мог понять...но тогда вы бы использовали статику, чтобы уменьшить область.

мое предпочтение было бы не беспокоиться о модульных (или локальных/статических) переменных, замените рабочий лист.Имя ссылка с листе.Кодовое имя (меньше шансов быть изменен и, если вы компилируете после переименования, вы получаете ошибку) и ссылаетесь на диапазоны таблиц через объект listobject и ListColumns (в случае изменения размера таблицы).

' Returns the item name for the requested item ID.
Public Function ITEM_NAME(ByVal ItemID As Variant) As String
    ITEM_NAME = Application.WorksheetFunction.Index( _
                      Sheet1.ListObjects("Table1").ListColumns("Item_name").DataBodyRange _
                    , Application.WorksheetFunction.Match( _
                          ItemID _
                        , Sheet1.ListObjects("Table1").ListColumns("Item_ID").DataBodyRange _
                        ) _
                    )
End Function

но самое надежное решение было бы избежать UDF и использовать =INDEX(Table1[Item_name],MATCH([@[Item_ID]],Table1[Item_ID]‌​)) (VLOOKUP может быть немного быстрее, но INDEX + MATCH более надежен).