Программирование с помощью массивов в Excel VBA: передавать их или не передавать? - PullRequest
7 голосов
/ 23 января 2010

Вопрос: Мне интересно, какое оптимальное решение для работы с массивами в Excel 2003 VBA

Справочная информация: У меня есть макрос в Excel 2003, который содержит более 5000 строк. Я построил его за последние 2 года, добавляя новые функции в качестве новых процедур, которые помогают сегментировать код и отлаживать, изменять или добавлять эту функцию. Недостатком является то, что я использую большую часть одной и той же базовой информации в нескольких процедурах, что требует от меня загрузки ее в массивы с небольшими различиями несколько раз. Теперь у меня проблемы с продолжительностью выполнения, поэтому я могу полностью переписать.
Этот файл используется для захвата нескольких элементов производственных потоков (до 4 различных установок с общим количеством до 10 отдельных потоков, до 1000 шагов в каждом) с информацией, относящейся к конкретному потоку, специфичному для вспомогательного потока для группировки / сортировки цели и данные (такие как движения, инвентарь, CT, ...)
Затем он прикрепит данные к нескольким листам, используемым для управления процессом, используя таблицы данных для просмотра, диаграммы и форматирование ячеек для обозначения возможностей / истории потока процесса.
Поток находится в файле Excel, в то время как производственные данные считываются с помощью 7 различных извлечений OO4O Oracle SQL, некоторые из которых многократно используются несколько раз

Массивы:
arrrFlow (от 1 до 1000, от 1 до 4) как тип записи с 4 строками
arrrSubFlow (от 1 до 1000, от 1 до 10) в качестве типа записи с 4 строками, 2 целыми числами и 1 одиночным
arrrData (от 1 до 1000, от 1 до 10) в качестве типа записи с 1 строкой, 4 целыми числами, 12 длинными и 1 одиночным
ArriSort (от 1 до 1000, от 1 до 4) как целое число (используется в качестве массива указателя для сортировки потока, подпотока и данных в порядке групп, подгрупп и шагов, оставляя исходные массивы в порядке шагов)

Возможности:
1) Перепишите макрос в одну большую процедуру, которая загружает данные в основные массивы, измеренные в процедуре, один раз
Pro: Измеряется в процедуре, а не в качестве публичной переменной в модуле и не передается.
Против: Сложнее отлаживать с помощью одной мега процедуры вместо нескольких меньших.

2) Хранить макрос с несколькими процедурами, но передавая массивы
Pro: Легче отлаживать код с помощью нескольких небольших процедур.
Con: проходящие массивы (дорого?)

3) Хранить макрос с несколькими процедурами, но с массивами, являющимися переменными Public Dim в модуле
Pro: Легче отлаживать код с помощью нескольких небольших процедур.
Con: публичные массивы (дорого?)

Итак, каков приговор сообщества? Кто-нибудь знает цену использования Public Arrays против Passing Arrays? Стоит ли платить за то, что мои действия будут сосредоточены на одной функции?

UPDATE:
Я загружаю данные инвентаризации на отдельном уровне (несколько на шаг), перемещает данные на агрегированном уровне (по одному на шаг) и начало смены инвентаризации на агрегированном уровне. Я объединяю данные инвентаризации, шаг за шагом помещая их в категории «Рабочее состояние» («Выполнить», «Ожидание», ...), и создаю цели на основе данных, уже находящихся на листах.

У меня есть технологическая карта, которая показывает рабочие потоки по типу, в настоящее время 3 продукта имеют похожий, но не совсем одинаковый поток, а 2 продукта - это разные потоки, которые похожи, но опять-таки не совпадают друг с другом. Каждому набору шагов в разных потоках я назначил группу и подгруппу.

Я размещаю эти данные на нескольких листах, некоторые в порядке шагов, некоторые в порядке групп / подгрупп. Мне также нужны данные, суммированные по группам и продуктам, группам / подгруппам и продуктам, части линии, продукту и продукту.

Я использую типы записей, поэтому у меня фактически есть читаемый трехмерный массив, arrSubFlow (1,1) .strStep (имя шага 1-го шага 1-го устройства), arrData (10,5) .lngYest (движение вчера для 10-й шаг 5-го устройства).

Мой основной пункт оптимизации будет в разделе, где я каждый раз создаю 10 страниц с нуля. С объединением ячеек, границ, заголовков, ... Это очень трудоемкий процесс. Я добавлю раздел, который будет сравнивать мои данные со страницей, чтобы увидеть, нужно ли их менять, и, если это так, только тогда, если это будет сделано заново, в противном случае я буду очищать каждый раздел данных и записывать только данные, которые изменяются на листе. Это будет огромно, основываясь на моих данных регистрации времени. Однако всякий раз, когда я обновляю код, я всегда стараюсь улучшить и другие аспекты кода. Я рассматриваю загрузку данных в Структуру (Массив, RecordSet, Collection) один раз как небольшую часть оптимизации, но в большей степени это касается целостности данных, поэтому у меня нет возможности загрузить их по-другому для разных листов.

Основные проблемы, с которыми я сталкиваюсь сейчас, это:
* В них уже вложены большие средства, но это не является достаточной причиной, чтобы не менять
* Не знаю, стоит ли их много обходить, так как ByRef
* Я использую функцию сортировки для создания отсортированного массива «Pointer», который позволяет мне оставлять массив в порядке пошагового потока, и в то же время легко ссылаться на него по порядку группы / подгруппы.

Поскольку я всегда пытаюсь создать свой код на данный момент и в будущем, я не против обновления массивов либо для RecordSets, либо для коллекций, а не просто ради того, чтобы изменить их для изучения чего-то интересного. Мои массивы работают и, исходя из моих исследований, они добавляют секунды к времени выполнения, а не существенные суммы для этого 2-минутного отчета. Поэтому, если в будущем будет проще обновить другую структуру, чем двумерные массивы типов записей, сообщите мне об этом, но знает ли кто-нибудь стоимость передачи массива в процедуру, если вы не выполняете передачу ByVal?

Ответы [ 2 ]

11 голосов
/ 25 января 2010

Вы предоставили много подробностей, но все еще довольно сложно понять, что происходит, не видя некоторого кода.В вашем вопросе я могу выделить как минимум 4 большие темы, которые вы переплетаете: производство, доступ к данным, VBA и лучшие практики кодирования.Мне трудно точно сказать, что вы спрашиваете, потому что объем ваших вопросов огромен.В любом случае, я ценю ваши попытки написать лучший код на VBA.

Мне сложно понять, что именно вы планируете делать с массивами.Вы говорите:

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

Я не уверен, что вы имеете в виду здесь.Используете ли вы массивы для представления строки данных, которые вы извлекли из базы данных?Если это так, вы можете рассмотреть возможность использования модулей класса вместо обычных «макро» модулей.Это позволит вам работать с полноценными объектами вместо массивов значений (или ссылок, в зависимости от обстоятельств).Классы требуют больше работы для установки и использования, но они значительно упрощают работу с вашим кодом и значительно помогут вам сегментировать ваш код.

Как уже указывал пользователь Emtucifor, могут существовать такие объекты, какADO Recordset объекты (которые могут требовать установки Access ... не уверен), которые могут сильно помочь.Или вы можете создать свой собственный.

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

В редакторе VBA перейдите на Insert > Class Module.В окне «Свойства» (по умолчанию в левом нижнем углу экрана) измените имя модуля на WorkLogItem.Добавьте следующий код в класс:

Option Explicit

Private pTaskID As Long
Private pPersonName As String
Private pHoursWorked As Double

Public Property Get TaskID() As Long
    TaskID = pTaskID
End Property

Public Property Let TaskID(lTaskID As Long)
    pTaskID = lTaskID
End Property

Public Property Get PersonName() As String
    PersonName = pPersonName
End Property

Public Property Let PersonName(lPersonName As String)
    pPersonName = lPersonName
End Property

Public Property Get HoursWorked() As Double
    HoursWorked = pHoursWorked
End Property

Public Property Let HoursWorked(lHoursWorked As Double)
    pHoursWorked = lHoursWorked
End Property

Приведенный выше код даст нам строго типизированный объект, специфичный для данных, с которыми мы работаем.Когда вы используете многомерные массивы для хранения ваших данных, ваш код выглядит следующим образом: arr(1,1) - это идентификатор, arr(1,2) - это PersonName, а arr(1,3) - это HoursWorked.Используя этот синтаксис, трудно понять, что к чему.Предположим, вы по-прежнему загружаете свои объекты в массив, но вместо этого используете WorkLogItem, который мы создали выше.Это имя, вы могли бы сделать arr(1).PersonName, чтобы получить имя человека.Это делает ваш код намного проще для чтения.

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

Далее, добавьте новый модуль класса и назовите его ProcessWorkLog.Поместите туда следующий код:

Option Explicit

Private pWorkLogItems As Collection

Public Property Get WorkLogItems() As Collection
    Set WorkLogItems = pWorkLogItems
End Property

Public Property Set WorkLogItems(lWorkLogItem As Collection)
    Set pWorkLogItems = lWorkLogItem
End Property

Function GetHoursWorked(strPersonName As String) As Double
    On Error GoTo Handle_Errors
    Dim wli As WorkLogItem
    Dim doubleTotal As Double
    doubleTotal = 0
    For Each wli In WorkLogItems
        If strPersonName = wli.PersonName Then
            doubleTotal = doubleTotal + wli.HoursWorked
        End If
    Next wli

Exit_Here:
    GetHoursWorked = doubleTotal
        Exit Function

Handle_Errors:
        'You will probably want to catch the error that will '
        'occur if WorkLogItems has not been set '
        Resume Exit_Here


End Function

Приведенный выше класс будет использоваться для того, чтобы "что-то сделать" с коллекцией WorkLogItem.Первоначально мы просто настроили его для подсчета общего количества отработанных часов.Давайте проверим код, который мы написали.Создайте новый модуль (на этот раз не модуль класса; просто «обычный» модуль).Вставьте следующий код в модуль:

Option Explicit

Function PopulateArray() As Collection
    Dim clnWlis As Collection
    Dim wli As WorkLogItem
    'Put some data in the collection'
    Set clnWlis = New Collection

    Set wli = New WorkLogItem
    wli.TaskID = 1
    wli.PersonName = "Fred"
    wli.HoursWorked = 4.5
    clnWlis.Add wli

    Set wli = New WorkLogItem
    wli.TaskID = 2
    wli.PersonName = "Sally"
    wli.HoursWorked = 3
    clnWlis.Add wli

    Set wli = New WorkLogItem
    wli.TaskID = 3
    wli.PersonName = "Fred"
    wli.HoursWorked = 2.5
    clnWlis.Add wli

    Set PopulateArray = clnWlis
End Function

Sub TestGetHoursWorked()
    Dim pwl As ProcessWorkLog
    Dim arrWli() As WorkLogItem
    Set pwl = New ProcessWorkLog
    Set pwl.WorkLogItems = PopulateArray()
    Debug.Print pwl.GetHoursWorked("Fred")

End Sub

В приведенном выше коде PopulateArray() просто создает коллекцию WorkLogItem.В своем реальном коде вы можете создать класс для анализа ваших листов Excel или объектов данных для заполнения коллекции или массива.

Код TestGetHoursWorked() просто демонстрирует, как использовались классы.Вы замечаете, что ProcessWorkLog создается как объект.После создания экземпляра коллекция WorkLogItem становится частью объекта pwl.Вы замечаете это в строке Set pwl.WorkLogItems = PopulateArray().Затем мы просто вызываем написанную нами функцию, которая действует на коллекцию WorkLogItems.

Почему это полезно?

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

Все, что вам нужно сделать, это добавить свойство к WorkLogItem, например, так:

Private pHoursOnBreak As Double

Public Property Get HoursOnBreak() As Double
    HoursOnBreak = pHoursOnBreak
End Property

Public Property Let HoursOnBreak(lHoursOnBreak As Double)
    pHoursOnBreak = lHoursOnBreak
End Property

Конечно, вам нужно будет изменить свой метод для заполнения вашей коллекции (пример метода, который я использовал, был PopulateArray(), но вы, вероятно, должны иметь отдельный класс только для этого). Затем вы просто добавляете новый метод в класс ProcessWorkLog:

Function GetHoursOnBreak(strPersonName As String) As Double
     'Code to get hours on break
End Function

Теперь, если мы хотим обновить наш метод TestGetHoursWorked(), чтобы он возвращал результат GetHoursOnBreak, все, что нам нужно сделать, это добавить следующую строку:

    Debug.Print pwl.GetHoursOnBreak("Fred")

Если вы передадите массив значений, представляющих ваши данные, вам нужно будет найти каждое место в вашем коде, где вы использовали массивы, а затем соответствующим образом обновить его. Если вместо этого вы используете классы (и их экземпляры), вы можете намного проще обновить свой код для работы с изменениями. Кроме того, когда вы разрешаете использование класса несколькими способами (возможно, одной функции требуется только 4 из свойств объектов, а другой функции потребуется 6), они все равно могут ссылаться на один и тот же объект. Это предотвращает использование нескольких массивов для различных типов функций.

Для дальнейшего прочтения я бы настоятельно рекомендовал бы получить копию Справочник разработчика VBA, 2-е издание . Книга полна замечательных примеров и лучших практик, а также множества примеров кода. Если вы вкладываете много времени в VBA для серьезного проекта, стоит потратить время на изучение этой книги.

3 голосов
/ 23 января 2010

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

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

  • Существует объект, который естественным образом обрабатывает объекты типа записи, с которыми вы работаете, который называется Recordset. В редакторе VBA перейдите в Инструменты -> Ссылки и добавьте Microsoft ActiveX Data Objects 2.X Library (самая высокая на вашем компьютере). Вы можете объявить объект типа ADODB.Recordset, затем сделать Recordset.Fields.Append, чтобы добавить к нему поля, затем .Open его и, наконец, .AddNew, установить значения полей и .Update. Это естественный объект для передачи в программах в качестве входного или выходного параметра. Он имеет естественные функции обхода и позиционирования (.Eof, .Bof, .AbsolutePosition, .MoveNext, .MoveFirst, .MovePrevious) и поддерживает поиск и фильтрацию (.Filter = "Field = 'abc'", .Find и т. Д.).

  • Я не рекомендую использовать общедоступные переменные, хотя без понимания того, что вы делаете, я не могу вам здесь очень хорошо посоветовать.

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

  • Если вы хотите улучшить производительность своего кода, нажимайте ctrl-break в случайное время, пока он работает, и врывайтесь в код. Затем нажмите Ctrl-L, чтобы просмотреть стек вызовов. Запишите, что находится в списке каждый раз. Если какой-либо элемент обнаруживается большую часть времени, это узкое место, и именно здесь вы должны провести время, пытаясь его оптимизировать. Однако я не советую пытаться оптимизировать то, что у вас есть, пока вы не примете решения более высокого уровня (например, переключитесь ли вы на набор записей).

Мне действительно нужно больше информации, чтобы помочь вам лучше.

Если вам интересно, я разработаю некоторый демонстрационный код, который покажет, насколько полезен объект Recordset. Вставить данные из набора записей в диапазон Excel очень легко с помощью Recordset.GetRows или .GetString (хотя может потребоваться некоторая транспонирование массива, что тоже не сложно).

ОБНОВЛЕНИЕ: Если ваша цель состоит в том, чтобы ускорить ваш процесс, то, прежде чем что-то делать, я думаю, лучше вооружиться знанием того, что занимает больше всего времени. Не могли бы вы нажать Ctrl-Break около 10 раз и каждый раз записывать стек вызовов, а затем сказать мне, какие элементы наиболее часто встречаются в стеке вызовов?

Что касается обновления скорости форматирования ячеек, вот мой опыт:

  1. Слияние - самая медленная операция, которую вы можете сделать. Старайтесь избегать этого, если это вообще возможно. Использование «центра по всему выбору» является одной из альтернатив. Другой - это не слияние, а использование некоторого сочетания правильного определения размера, границ, цвета фона ячейки и отключения линий сетки для всей книги.

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

  3. Сохраните файл шаблона с уже примененными границами и форматированием.Допустим, вы поместили в него одну строку с форматированием для определенного раздела.За один шаг дублируйте эту строку на столько строк, сколько необходимо для этого раздела, скажем, 20 строк, и все они будут иметь одинаковое форматирование.Дублирование строк НАМНОГО быстрее, чем применение форматирования ячейка за ячейкой.

Кроме того, я бы не стал автоматически использовать классы.Хотя ОО - это здорово, и я делаю это сам (черт, я только что на днях построил 8 классов для того, чтобы смоделировать иерархическую структуру, чтобы я мог легко разобрать ее части, когда они мне нужны), на практике это может быть медленнее.Простой набор открытых переменных в классе быстрее, чем использование методов получения и установки.Определяемый пользователем тип является даже более быстрым, чем класс, но вы можете столкнуться с ошибками, пытаясь обойти UDT в классах (они должны быть объявлены в общедоступном модуле, не относящемся к классу, и даже тогда они могут создавать проблемы).

Эрик

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...