Открытые и вариантные массивы

altМногие начинающие Delphi-программисты избегают применения открытых и вариантных массивов. Причина этому скорей всего — отсутствие соответствующей информации в литературе. За свою практику я видел до фига книг по ObjectPascal/Delphi и во многих из них такая важная тема не упоминается. А ведь зря!

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

Открытые массивы (Open array parameters)

В англо-язычной литературе для обозначения открытых массивов применяется выражение «Open array parameters». Если его перевести дословно, то получится что-то вроде: «Параметры открытого массива». На нашем родном языке – это звучит как-то не очень, поэтому здесь и дальше по тексту статьи я буду употреблять термин «открытый массив».

Для себя ты можешь использовать какой угодно термин. Суть от этого не изменится. Главное запомни, что открытый массив – это параметр для процедуры/функции. От типичного массива он, прежде всего, отличается тем, что он лишь описывает базовый тип элементов без указания информации о размерности. Не буду попросту разглагольствовать, а лучше сразу приведу пример использования:

procedure ListAllIntegers(const AnArray: array of Integer);
var
I: Integer;
begin
for I := Low(AnArray) to High(AnArray) do
WriteLn(‘Integer at ‘, I, ‘ is ‘, AnArray[I]);
end;
Обрати внимание на то, как я описал параметр типа «массив». Не хватает привычной информации о размере массива. С виду даже может показаться, что мы имеем дело с динамическим массивом, однако это не так. Это просто открытый массив. Ты можешь без проблем объявить массив (любой размерности) типа Integer (например, array [0..1] или array [42..937]) и передать его в процедуру ListAllIntegers. Никто также не запрещает тебе передать в процедуру динамический массив. Во всех случаях, ошибок не возникнет.

Помимо примера описания открытого массива в виде параметра, в этом коде демонстрируется использование псевдо функций Low и High. Первая возвращает нижнюю границу массива, а вторая соответственно верхнюю. Наверняка тебя интересует, а почему я обозвал эти функции «псевдо», т.е. нереальными? Дело все в том, что они являются лишь синтаксическим элементом, которые всего лишь принимают форму функции. Фактически, они полагаются на работу компилятора, который просто заменяет их определенным машинным кодом. Вот таки вот биты байты. Ok, едем дальше. Ой! Стоп, совсем замотался и забыл упомянуть еще одну полезную функцию, которая пригодится при работе с массивами. Имя этой функции – length(). Юзай ее, когда требуется узнать число элементов массива.

Теперь начнем проводить первые эксперименты. Попробуй написать и выполнить нижеприведенный код:

var
NonZero: array[7..9] of Integer;
begin
NonZero[7] := 17;
NonZero[8] := 325;
NonZero[9] := 11;
ListAllIntegers(NonZero);
end.
Результатом выполнения должен быть следующий текст:

Integer at 0 is 17
Integer at 1 is 325
Integer at 2 is 11
Почему так получилось? И опять все просто, главное знать теорию. Внутри процедуры или функции, открытый массив всегда считается zero-based (начинающийся с нуля). В связи с этим, если скормить функции Low() такой массив, то она недолго думая вернет 0. Как же тогда поступит функция High()? Не пытайся гадать, а просто запомни, что она все сделает правильно и подстроится под возникшие обстоятельства (обрати внимание, что все это относится в применении High() и Low() к открытым массивам). Не забудем в этот раз и про Length(). Для открытых массивов, эта функция всегда будет возвращать High + 1.

Slice

Очень часто может возникнуть задача, что для работы тебе потребуется лишь одна часть массива. Что делать в такой ситуации? Создавать новый массив? Конечно же нет! Просто воспользуйся функцией Slice, которая позволяет отломить «кусочек» массива. Например:

const
Months: array[1..12] of Integer = (31, 28, 31, 30, 31, 30,
31, 31, 30, 31, 30, 31);
begin
ListAllIntegers(Slice(Months, 6));
end;
Потрудись набрать и выполнить код. Результатом его выполнения будет всего лишь 6 значений массива, а не 12.

Internals

Как это работает? Как функция может узнать размер массива? На практике это достаточно просто реализуемо. Открытый массив фактически является комбинацией из двух параметров:

1. Указателя на адрес начала массива
2. Число, которое соответствует верхней границе массива (для массивов, которые начинаются с нуля).

Руководствуясь вышесказанным, описываем процедуру примерно так:
procedure ListAllIntegers(const AnArray: Pointer; High: Integer);
Когда ты передаешь открытый массив, компилятор который знает размер массива, передаст свой адрес и подгонит значение, возвращаемое функцией High (т.е. значение верхней границы) внутри процедуры или функции. Для статических массивов (например, array [7..9] of Integer) будет использоваться размер, полученный при помощи значения, полученное функцией Hight. Для динамических же массивов будет происходить компилирование кода с целью получения размера массива в режиме Runtime.

Обычно, открытый массив передается с модификатором const. Если передавать открытый массив без этой примочки (я про const), то массив будет полностью скопирован в локальное «хранилище» процедуры или функции. Сам массив передается по ссылке, но в случае отсутствия const, в процедуре/функции (само собой локально) будет происходить выделение памяти в стэке, а затем производится копирование массива в «локальное хранилище». Ссылка будет использоваться как адрес источника.

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

Конструкторы открытых массивов

Порой реально обламывает объявлять массив с одной лишь целью – заполнить несколькими значениями и передать в качестве параметра. Куда ни шло такое делать в C# или PHP (как пример). Там не требуется описывать переменные в определенной секции (как var в Pascal/Delphi). Захотел и объявил переменную – никаких трабл. В дельфячем мире все немного не так, но это не повод ругать Delphi и отчаиваться. Например, для того чтобы передать массив в качестве параметра, что называется «на ходу» не нужно объявлять переменную. Достаточно воспользоваться конструктором открытого массива. Синтаксис весьма прост. От тебя лишь требуется заключить значения создаваемого массива в квадратные скобки. Вот пример:
ListAllIntegers([17, 325, 11]);
Как видишь, в выше приведенном примере массив [17, 325, 11] создается прямо, не отходя от кассы. Компилятор гарантирует, что такой массив будет существовать во время работы процедуры и значение верхней границе массива (полученное при помощи High) будет корректным. Подход полностью прозрачен для кода внутри процедуры. После окончания работы процедуры, массив будет дропнут.

«Бабе мороженое, детям цветы»

Где-то по тексту статьи я уже упоминал, что открытые массивы в плане синтаксиса очень похожи на своих динамических собратьев (динамические массивы). Они реально похожи, но ты, ни в коем случае не должен их путать. Знай, что есть открытые массивы, а есть динамические – тип массивов, поддерживаемых языком Delphi. Отличительной особенностью динамических массивов выделяется возможность изменение размера массива при помощи функции SetLength(). По традиции рассмотрим пример.

type
TIntegerArray = array of Integer;
С точки зрения синтаксиса, этот пример похож на те, в которых мы рассматривали объявление открытых массивов, но это только внешне. В реале они отличаются. Функции/процедуры, которые принимают открытые массивы могут без проблем работать и с динамическими. Т.е. функции без разницы, передашь ли ты ей array[0..11] of Month или array of Month. В обоих случаях она отработает верно, но при работе с динамическим массивом, переданным в качестве параметра, ты лишаешься, возможности пользоваться функцией SetLength(), которая позволяет изменять размер массива.

Если тебе действительно требуется передать динамический массив, то придется объявить отдельную функцию/процедуру для работы с этим типом массивов. Примеры ниже:

type
TMonthArray = array of Month;
procedure AllKinds(const Arr: array of Month);
procedure OnlyDyn(Arr: TMonthArray);
Процедура AllKinds принимает и успешно работает как со статическим массивом, так и с динамическим. Правда, для динамических массивов нельзя изменять размер. Вторая процедура (OnlyDyn) наоборот, работает лишь с одним типом массивов – динамическим. Следовательно, внутри нее можно без проблем юзать SetLength. Однако стоит учитывать, что будет использоваться копия массива и все изменения не будут отражены в оригинале. Если тебе вдруг потребуется изменить размер, то объявляй параметр в процедуре как var. Например: var Arr: TMontArray.

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

procedure Sum(const Items: array[1..7] of Integer);
function MoveTo(Spot: record X, Y: Integer; end);
Первым делом ты должен объявить тип и только потом использовать спецификацию как параметр типа:

type
TWeek = array[1..7] of Integer;
TSpot = record
X, Y: Integer;
end;
procedure Sum(const Items: TWeek);
function MoveTo(Spot: TSpot);
Именно поэтому массив данных в списке параметров так же не может быть описанием типа для динамического массива. Это всегда описание открытого массива.

Вариантные массивы

Вариантный массив – особый вид открытых массивов. Вместо передачи лишь одного типа, ты можешь передавать несколько. Применение вариантных массивов хорошо показано в функции Format. Обязательно взгляни на ее описание в справке по Delphi:
function Format(const Format: string; const Args: array of const): string;


Первый параметр – строка, определяющая вид форматирования. Второй – вариантный массив (array of const). Таким образом, ты можешь передавать целый диапазон значений. В плане использования это будет выглядеть примерно так:
var
Res: string;
Int: Integer;
Dub: Double;
Str: string;
begin
Int := Random(1000);
Dub := Random * 1000;
Str := ‘Teletubbies’;
Res := Format(‘%4d %8.3f %s’, [Int, Dub, Str]);
end;
Запомни: «Array of const», официально принято называть – вариантный открытый массив. Пусть тебя не смущает слово «вариантный». Оно не имеет ничего общего с типом Variant. Но вариантный массив может содержать значения типа Variant. TVarRec (см. ниже) немного похож на Variant, который хранится внутри. Скажу больше, даже имя внутренней записи Variant похоже: TVarData.

Внутренности вариантного массива

Внутри вариантного массива находится открытый массив из TVarRec. Посмотреть описание TVarRec ты можешь в online-справке по Delphi. Этот вариант записи, содержащей поле с именем VType и наложение других типов, некоторые из которых являются указателями. Компилятор создает TVarRec для кажого элемента открытого массива, а затем заполняет поле VType соответствующим типом элемента и помещает значение или указатель на него в одном из других полей. После этого массив из TVarRec передается в функцию.

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

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

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

Насущные проблемы

Обрати внимание, что значения TVarRec, переданные как указатели, существуют только во время жизни процедуры/функции. Как только процедура или функция завершают выполнение, эти значения становятся недоступными. Не пытайся поддаваться соблазну вернуть эти указатели из функции/процедуры обратно или хранить TVarRecs в массиве, за их пределами, если четко не уверен, что управляешь этими значениями самостоятельно.

Если перед тобой встала задача скопировать TVarRecs в массив или внешнюю переменную функции (она может быть оформлена в виде var-параметра), не забудь сделать копию (т.е. копию в стеке) значения и заменить указатель в TVarRec на копию. После окончания работы с данными, ты также должен самостоятельно позаботиться об удалении их копии. Например, так:

type
TConstArray = array of TVarRec;
// Copies a TVarRec and its contents. If the content is referenced
// the value will be copied to a new location and the reference
// updated.
function CopyVarRec(const Item: TVarRec): TVarRec;
var
W: WideString;
begin
// Copy entire TVarRec first.
Result := Item;
// Now handle special cases.
case Item.VType of
vtExtended:
begin
New(Result.VExtended);
Result.VExtended^ := Item.VExtended^;
end;

vtPChar:
Result.VPChar := StrNew(Item.VPChar);

// A little trickier: casting to AnsiString will ensure
// reference counting is done properly.
vtAnsiString:
begin
// Nil out first, so no attempt to decrement
// reference count.
Result.VAnsiString := nil;
AnsiString(Result.VAnsiString) := AnsiString(Item.VAnsiString);
end;

// VPointer and VObject don’t have proper copy semantics so it
// is impossible to write generic code that copies the contents.

end;
end;
// Creates a TConstArray out of the values given. Uses CopyVarRec
// to make copies of the original elements.
function CreateConstArray(const Elements: array of const): TConstArray;
var
I: Integer;
begin
SetLength(Result, Length(Elements));
for I := Low(Elements) to High(Elements) do
Result[I] := CopyVarRec(Elements[I]);
end;
// TVarRecs created by CopyVarRec must be finalized with this function.
// You should not use it on other TVarRecs.
procedure FinalizeVarRec(var Item: TVarRec);
begin
case Item.VType of
vtExtended: Dispose(Item.VExtended);
vtString: Dispose(Item.VString);

end;
Item.VInteger := 0;
end;
// A TConstArray contains TVarRecs that must be finalized. This function
// does that for all items in the array.
procedure FinalizeVarRecArray(var Arr: TConstArray);
var
I: Integer;
begin
for I := Low(Arr) to High(Arr) do
FinalizeVarRec(Arr[I]);
Arr := nil;
end;
Функции, приведенные выше, помогут тебе управлять TVarRecs за пределами процедур/функций, для которых они были созданы. Еще ты даже можешь использовать TConstArray, там, где объявлен открытый массив. Пример представлен в виде небольшой программы, текст который ты можешь видеть ниже:

program VarRecTest;
{$APPTYPE CONSOLE}
uses
SysUtils,
VarRecUtils in ‘VarRecUtils.pas’;
var
ConstArray: TConstArray;
begin
ConstArray := CreateConstArray([1, ‘Hello’, 7.9, IntToStr(1234)]);
try
WriteLn(Format(‘%d — %s — %0.2f — %s’, ConstArray));
Writeln(Format(‘%s — %0.2f’, Copy(ConstArray, 1, 2)));
finally
FinalizeConstArray(ConstArray);
end;
ReadLn;
end.
Результаты будут ожидаемыми, но не очень интересными. Если быть точным, то программа выведет:

1 — Hello — 7.90 — 1234
Hello — 7.90
Эта небольшая программа также демонстрирует, то, как ты можешь использовать Copy для применения лишь для части записей TConstArray. Copy создаст копию динамического массива, но, не копию содержимого. В связи с этим, ты не должен пытаться использовать Copy, а затем применять FinalizeConstArray для этой копий. В программе, приведенной выше, копия будет удалена автоматически, временем жизни копии будет управлять компилятор.

Скворец пропел «Конец»

Открытые и вариантные массивы – мощная возможность языка, но нужно использовать ее с осторожностью. Я надеюсь, мне удалось показать некоторые из возможных трудностей и способы их преодоления. Удачи в программировании!

Понравилась статья? Поделиться с друзьями: