Pull to refresh

Типизированные массивы должны умереть

Level of difficultyEasy
Reading time5 min
Views4.5K
Original author: Mathieu Guindon

Хаха :). Не, я серьезно. Позвольте мне объяснить.

Примечание: речь в статье идет про язык VBA (Visual Basic for Applications).

Variant потрясающий

Много написано о том, почему следует избегать Variant и использовать вместо него более конкретный тип данных. Действительно, нет смысла использовать Variant (явно или нет), когда можно работать с Long, Date или String. Это справедливо для всех внутренних типов данных.

Использование Variant для объектов, вместо интерфейса класса, делает любое обращение к нему, по сути, поздно связанным (late binding, прим. переводчика), то есть он может быть разрешен только на этапе выполнения, так как фактический тип класса/интерфейса не известен во время компиляции… и именно поэтому VBE не будет показывать список автодополнений или информацию о параметрах для всего, где используется Variant. Это, определенно, нехорошо, тем более, что Object – лучший вариант для явного позднего связывания.

Подожди, мне показалось, ты сказал, что он потрясающий?

Ну да. Понимаете, его природа делает его идеальным типом данных для возврата в любой UDF (пользовательской функции) Excel: когда функция завершается без ошибки, она может возвращать подтип Variant, а в случае ошибки, подпрограмма обработки ошибок в UDF может заставить функцию вернуть на лист Excel значение Variant/Error, и да, это действительно потрясающе.

Но подождите, это еще не все!

Массивы. Variant отлично подходит для массивов – потому что типизированные массивы должны умереть.


Хорошо. Что такое типизированный массив?

Это одна из тех языковых причуд, которые делают VBA таким... я бы сказал, восхитительным.

Под "типизированным" я подразумеваю массив, который таким образом объявлен, так что старый добрый "массив строк" подходит под это определение:

Option Explicit
Private DynamicallySizedArray() As String
Private FixedSizeArray(1 To 10) As String

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

Это важно, потому что As String(10) - это синтаксическая ошибка, а не массив.

То же самое происходит при объявлении параметра:

Private Sub DoSomething(ByRef Values() As String)
End Sub

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

Поскольку параметр value у property всегда передается по значению (даже если в нем написано ByRef!), типизированные массивы сразу же становятся проблемой при определении свойств:

Public Property Let SomeValues([ByVal] Values() As String)
End Property

А что, если мы захотим вернуть типизированный массив? Это будет выглядеть примерно так:

Public Function GetSomeValues() As String()
End Function

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

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

ReDim

Раз уж мы начали, нужно поговорить и о ReDim. Совсем недавно я работал над системой типов RD3 (RubberDuck 3, прим. переводчика) и перешел к реализации ReDim для массивов, и я был удивлен (нет 😅) тем, как много VBA перекладывает на этап выполнения. Мы получим ошибку компиляции если попытаемся использовать ReDim с переменной в блоке With или с массивом, переданным через ParamArray, но все это приводит к ошибке на этапе выполнения, если указан модификатор Preserve:

  • Когда мы пытаемся изменить число измерений массива

  • Когда мы пытаемся изменить нижнюю границу массива

  • Когда мы пытаемся изменить верхнюю границу измерения, которая не является последним измерением массива

Невозможно присвоить массиву

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

Не знаю, почему автор утверждает так, но это не верное утверждение

спасибо @slepmog за пример:

Dim a1(1 To 10) As Long
Dim a2(1 To 10) As Long  ' То же количество измерений, размеры измерений совпадают, все значения соответствуют значениям по умолчанию для объявленного типа (ноль)
  
a1 = a2  ' Ошибка компиляции

Это поведение, зависящее от контекста, либо занимает место в памяти, либо приводит к проблемам, которые компилятор не обнаружит, пока не станет слишком поздно (хотя диагностика RD3 может это изменить).


Variant/Массив

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

Переменная типа Variant может содержать что угодно, включая массивы. Проще говоря, мы объявляем переменные типа Variant и используем имена идентификаторов во множественном числе: ByVal Things As Variant.

Внезапно, работа с массивами в VBA становится намного проще - потому что Variant/Array ведет себя так, как мы ожидаем: в основе лежат указатели, так что разница едва заметна, но, добавив всего один "слой" перенаправления указателей, "скрыв" наш массив за Variant, мы можем делать все то, что можно делать с обычным массивом, и писать сигнатуры функций, не задумываясь о том, где поставить круглые скобки.

Public Function GetSomeValues() As String()
End Function

Превращается в:

Public Function GetSomeValues() As Variant
End Function

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

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

Не знаю, почему автор утверждает так, но это не верное утверждение

спасибо @slepmog за пример:

Private Sub Main()
    Dim a(1 To 10) As Long
    a(5) = 1
  
    ChangeArray a
  
    MsgBox a(5)  ' Результат: 1
End Sub

Private Sub ChangeArray(ByVal v As Variant) ' На самом деле передается копия массива
    v(5) = 42
End Sub

Без Variant, обернутого вокруг массива, при попытке передать его по значению будет скопирован весь массив, и VBA отказывается это делать, требуя передачи массивов по ссылке.

Другими словами, массивы, помещенные в Variant, будут передаваться как ссылки, независимо от того, делаем мы это ByRef или ByVal, и это именно то, чего хочет VBA.

Private Sub DoSomething(ByVal Values As Variant)
End Sub

Это также то, чего хотим мы, потому что наше ментальное представление о передаче массива в процедуру работает так же: если мы просто принимаем тот факт, что на самом деле мы передаем указатель, то правила вокруг ByRef/ByVal остаются относительно простыми, и не нужно беспокоиться о коде, который компилируется, но может вызвать сбои во время выполнения, потому что когда мы присваиваем указатель на массив, мы присваиваем его переменной типа Variant, а не массиву, поэтому здесь нет никаких ограничений, это "просто работает".


Заключение

Массивы чрезвычайно полезны в VBA, но их использование на уровне абстракции, для которого они изначально предназначались, сопряжено с множеством подводных камней и ограничений, из-за которых с ними сложно работать. Работая с Variant, мы по-прежнему имеем дело с массивами, но уже без ограничений, присущих переменным массивов.

Так что дело не в том, что типизированные массивы должны умереть, просто массивы в VBA гораздо менее раздражающие, когда VBA не осознает, что перед ним массив.

Мой телеграм: Дневник VBAшника

Tags:
Hubs:
+7
Comments14

Articles