Версия для печати
Урок 11. Вариантный тип и безопасные массивы
http://www.delphikingdom.com/asp/viewitem.asp?catalogID=1358Антон Григорьев
урок из цикла: Использование COM/DCOM в Delphi
дата публикации 10-06-2008 09:21Урок 11. Вариантный тип и безопасные массивы В этом уроке мы рассмотрим вариантный тип, который широко используется в COM/DCOM (особенно в OLE). Понимание вариантного типа необходимо для того, чтобы разобраться с такими важными темами, как маршалинг и автоматизация.
В языках программирования, подобных Delphi, тип переменной определяется на этапе компиляции и остаётся неизменным в течение всего времени работы программы. Во многих же других языках тип переменной нигде явно не объявляется и может при необходимости динамически меняться. Чтобы такое было возможно, в той области памяти, которая выделена для хранения переменной, должно находиться не только текущее значение, но и информация о том, какой тип имеет переменная в данный момент. Структуры, предназначенные для таких переменных, называются вариантными типами. Из-за необходимости проверки на каждом шаге того, какой тип имеет переменная в данный момент, работа с переменными вариантных типов осуществляется медленнее, чем с обычными, но это компенсируется большей гибкостью.
В Windows определён тип, называемый VARIANT, предназначенный для хранения вариантных переменных. Он объявлен следующим образом:
typedef struct FARSTRUCT tagVARIANT VARIANT; typedef struct FARSTRUCT tagVARIANT VARIANTARG; typedef struct tagVARIANT { VARTYPE vt; unsigned short wReserved1; unsigned short wReserved2; unsigned short wReserved3; union { Byte bVal; // VT_UI1. short iVal; // VT_I2. long lVal; // VT_I4. float fltVal; // VT_R4. double dblVal; // VT_R8. VARIANT_BOOL boolVal; // VT_BOOL. SCODE scode; // VT_ERROR. CY cyVal; // VT_CY. DATE date; // VT_DATE. BSTR bstrVal; // VT_BSTR. DECIMAL FAR* pdecVal // VT_BYREF|VT_DECIMAL. IUnknown FAR* punkVal; // VT_UNKNOWN. IDispatch FAR* pdispVal; // VT_DISPATCH. SAFEARRAY FAR* parray; // VT_ARRAY|*. Byte FAR* pbVal; // VT_BYREF|VT_UI1. short FAR* piVal; // VT_BYREF|VT_I2. long FAR* plVal; // VT_BYREF|VT_I4. float FAR* pfltVal; // VT_BYREF|VT_R4. double FAR* pdblVal; // VT_BYREF|VT_R8. VARIANT_BOOL FAR* pboolVal; // VT_BYREF|VT_BOOL. SCODE FAR* pscode; // VT_BYREF|VT_ERROR. CY FAR* pcyVal; // VT_BYREF|VT_CY. DATE FAR* pdate; // VT_BYREF|VT_DATE. BSTR FAR* pbstrVal; // VT_BYREF|VT_BSTR. IUnknown FAR* FAR* ppunkVal; // VT_BYREF|VT_UNKNOWN. IDispatch FAR* FAR* ppdispVal; // VT_BYREF|VT_DISPATCH. SAFEARRAY FAR* FAR* pparray; // VT_ARRAY|*. VARIANT FAR* pvarVal; // VT_BYREF|VT_VARIANT. void FAR* byref; // Generic ByRef. char cVal; // VT_I1. unsigned short uiVal; // VT_UI2. unsigned long ulVal; // VT_UI4. int intVal; // VT_INT. unsigned int uintVal; // VT_UINT. char FAR* pcVal; // VT_BYREF|VT_I1. unsigned short FAR* puiVal; // VT_BYREF|VT_UI2. unsigned long FAR* pulVal; // VT_BYREF|VT_UI4. int FAR* pintVal; // VT_BYREF|VT_INT. unsigned int FAR* puintVal; // VT_BYREF|VT_UINT. }; };Тип хранящегося значения определяется полем vt. Тип VARTYPE совпадает с типом WORD, и для него определены константы вида VT_XXX, каждая из которых соответствует определённому типу (тип, определяемый полем vt, мы далее будем называть динамическим типом).
Значение переменной хранится в одном из полей раздела union структуры. Для незнакомых с C/C++ поясним, что все поля, входящие в объединение (union), физически располагаются в одной и той же области памяти, а размер объединения совпадает с размером наибольшего из полей. Таким образом, в каждый момент времени мы можем работать с любым из полей, интерпретируя одну и ту же область памяти как значение того или иного типа, но должны помнить, что изменение значения одного из полей объединения приведёт к изменению и всех остальных его полей.
Значение поля vt по сути дела определяет то, какое поле объединения должно использоваться. При использовании структуры VARIANT в чистом виде программист обязан самостоятельно следить, чтобы используемое поле соответствовало текущему значению vt.
Существуют два специальных динамических типа: VT_EMPTY и VT_NULL. Первый из них показывает, что структура не имеет никакого значения, второй — что структура имеет пустое (NULL) значение. В обоих случаях ни одно из полей объединения не должно использоваться. Несмотря на внешнее сходство, идеологически отсутствие значения и пустое значение — это принципиально разные понятия. Так, например, при использовании ADO при добавлении новых строк в таблицу те поля, для которых передано VT_NULL, получат пустое значение, а VT_EMPTY — значение по умолчанию, заданное для этого поля в свойствах таблицы.
Существует ещё две специальных константы VT_XXX: VT_ARRAY и VT_BYREF. VT_ARRAY должна объединяться с помощью операции арифметического или с какой-либо другой константой VT_XXX, показывая, что структура хранит массив элементов данного типа. Массивы, хранимые в вариантных типах, называются безопасными; мы рассмотрим их чуть позже в этом уроке, а пока отметим, что реально такой массив хранится, конечно же, вне структуры VARIANT, а в структуре хранится лишь ссылка на него. Значение VT_BYREF также обычно комбинируется с другой константой VT_XXX (хотя возможен вариант нетипизированной ссылки) и показывает, что в структуре хранится не само значение, а ссылка на это значение, хранящееся где-либо ещё.
Вообще, принято разделять два типа: VARIANT и VARIANTARG (хотя, как мы видим из их описания, эти идентификаторы — синонимы). Считается, что значения, передаваемые по ссылке, может содержать только структура VARIANTARG, а когда речь идёт о структуре VARINAT, то такой возможности как бы не существует. VARIANTARG используется в особых случаях — в интерфейсах диспетчеризации, которые мы рассмотрим позже. А до тех пор будем считать, что работаем со структурой VARIANT, и забудем о возможности хранить в ней какие-либо значения по ссылке.
Списки возможных значений поля vt и, как следствие, наборы полей, входящих в объединение, различаются в разных источниках. Это связано с тем, что не все возможные динамические типы данных применимы в той или иной ситуации, и "лишние" типы просто выкидываются. Кроме того, с появлением новых версий системы появляются и новые динамические типы, которые могут храниться в вариантной записи. Так, начиная с Windows NT 4 SP4 появился тип VT_RECORD, который позволил хранить в вариантном типе структуры. Нас, в основном, будет интересовать весьма ограниченный набор динамических типов — т.н. OLE-совместимые типы. К ним относятся VT_UI1, VT_I2, VT_I4, VT_R4, VT_R8, VT_BOOL, VT_CY, VT_DATE, VT_BSTR, VT_UNKNOWN, VT_DISPATCH, VT_VARIANT, VT_RECORD, а также массивы VT_SAFEARRAY с этими типами.
Динамические типы VT_BSTR и VT_ARRAY отличаются от, например, VT_I4 тем, что если в случае VT_I4 значение хранится в самой структуре VARIANT, то при использовании VT_BSTR и VT_ARRAY в структуре хранится только указатель на строку или массив, а сами данные хранятся вне её. Как легко догадаться, в случае типа VT_BSTR структура хранит указатель на строку типа BSTR, рассмотренного на предыдущем уроке. Программист должен заранее позаботиться о выделении памяти для этой строки.
При использовании VT_ARRAY в переменной хранится указатель на структуру SAFEARRAY, описывающую т.н. безопасный массив. Безопасные массивы — это специальные системные структуры, имитирующие обычные массивы. Безопасными они называются потому, что система обладает всей информацией о том, как выделяется память этому массиву и, следовательно, может корректно (т.е. "безопасно") её освободить при необходимости. Основные свойства безопасных массивов следующие:
- Безопасные массивы могут иметь любое число размерностей.
- Нижняя граница размерности может быть отлична от нуля и задаётся отдельно для каждой размерности.
- Безопасные массивы могут хранить данные любых вариант-совместимых типов (т.е. типов, задающихся константами VT_XXX), за исключением VT_EMPTY, VT_NULL и VT_ARRAY. Допускается тип VT_VARIANT, т.е. элементами безопасного массива могут быть вариантные записи.
Все манипуляции с безопасными массивами выполняются через специальные системные функции, которых в Windows API около тридцати. Эти функции позволяют создавать массивы, получать доступ к их элементам, информацию о размерностях массива и т.п. Полный список этих функций можно найти здесь; мы же не будем углубляться в изучение этих функций и подробно рассмотрим только одну из них — SafeArrayDestory.
Как видно из названия, функция SafeArrayDestory уничтожает безопасный массив. И в том, как она работает, в первую очередь проявляется "безопасность" таких массивов. Если, например, для массива элементов типа VT_I4 достаточно просто освободить память, выделенную для хранения элементов, то в случае VT_BSTR в массиве хранятся только указатели, и простое освобождение памяти, занимаемой массивом указателей, приведёт к утечкам памяти. Поэтому для каждого элемента типа VT_BSTR будет вызвана функция SysFreeString. Для вариантных элементов будет вызвана функция VariantClear (см. ниже), а для указателей на интерфейсы (VT_UNKNOWN и VT_DISPATCH) — Release. Таким образом, происходит полное освобождение всех вложенных структур за счёт одного только вызова функции SafeArrayDestroy.
Для типа VARIANT также существуют специальные системные функции, наиболее интересными из которых являются следующие три.
VariantInit. Эта функция инициализирует вариантную структуру. Память, выделенная для вариантной структуры, в общем случае может содержать любой мусор. Чтобы вручную не инициализировать каждое поле структуры правильным начальным значением, можно использовать VariantInit, после вызова которой структура будет содержать корректное значение VT_EMPTY.
VariantClear. Эта функция очищает вариантную структуру и заносит в неё значение VT_EMPTY. Очистка интеллектуальная, т.е. для строк вызывается SysFreeString, для интерфейсов — Release, для безопасных массивов — SafeArrayDestroy. Таким образом, как и в случае с безопасными массивами, для очистки вариантной структуры достаточно вызвать одну функцию независимо от типа значения и глубины вложенности.
VariantChangeType. Выполняет преобразование динамического типа. Функция достаточно интеллектуальна: при необходимости умеет округлять вещественные числа, преобразовывать числа и даты в строки и обратно с учётом системных настроек и т.п., т.е. умеет делать все имеющие смысл преобразования.
Системному типу VARIANT в Delphi соответствует тип TVarData, определённый следующим образом:
TVarData = packed record case Integer of 0: (VType: TVarType; case Integer of 0: (Reserved1: Word; case Integer of 0: (Reserved2, Reserved3: Word; case Integer of varSmallInt: (VSmallInt: SmallInt); varInteger: (VInteger: Integer); varSingle: (VSingle: Single); varDouble: (VDouble: Double); varCurrency: (VCurrency: Currency); varDate: (VDate: TDateTime); varOleStr: (VOleStr: PWideChar); varDispatch: (VDispatch: Pointer); varError: (VError: HRESULT); varBoolean: (VBoolean: WordBool); varUnknown: (VUnknown: Pointer); varShortInt: (VShortInt: ShortInt); varByte: (VByte: Byte); varWord: (VWord: Word); varLongWord: (VLongWord: LongWord); varInt64: (VInt64: Int64); varString: (VString: Pointer); varAny: (VAny: Pointer); varArray: (VArray: PVarArray); varByRef: (VPointer: Pointer); ); 1: (VLongs: array[0..2] of LongInt); ); 2: (VWords: array [0..6] of Word); 3: (VBytes: array [0..13] of Byte); ); 1: (RawData: array [0..3] of LongInt); end;Полю vt в типе TVarData соответствует поле VType. Константы для этого поля носят имена не VT_XXX, а varXXX. Ниже приведена таблица, в которой перечислены константы VT_XXX, соответствующие им константы varXXX и комментарий к типу.
VT_XXX varXXX Динамический тип VT_EMPTY varEmpty Переменная не имеет никакого значения VT_NULL varNull Переменная имеет пустое значение VT_I2 varSmallInt Двухбайтное целое со знаком VT_I4 varInteger Четырёхбайтное целое со знаком VT_R4 varSingle Четырёхбайтное вещественное VT_R8 varDouble Восьмибайтное вещественное VT_CY varCurrency Тип Currency (валюта) VT_DATE varDate Дата-время в формате TVarDate (вещественное число, целая часть которого показывает число полных дней, прошедших с 30.12.1899, дробная часть — долю прошедшего неполного дня) VT_BSTR varOleStr Строка типа BSTR VT_DISPATCH varDispatch Указатель на интерфейс IDispatch (этот интерфейс будет рассмотрен позже, в главе про автоматизацию) VT_ERROR varError Значение типа SCODE (устаревший тип для кодирования ошибок, применявшийся до введения HRESULT) VT_BOOL varBoolean Двухбайтный логический тип. Значение 0 соответствует False, -1 ($FFFF) — True. Остальные значения не определены. В системе определены константы VARIANT_TRUE и VARIANT_FALSE, кодирующие эти значения. В Delphi для этого типа никакие специальные константы не определены, можно использовать True и False. VT_VARIANT varVariant Вариантный тип. Может применяться только с VT_BYREF, т.е. в структуре хранится ссылка на другрую такую же структуру. VT_UNKNOWN varUnknown Указатель на IUnknown VT_I1 varShortInt Однобайтное целое со знаком VT_UI1 varByte Однобайтное беззнаковое целое VT_UI2 varWord Двухбайтное беззнаковое целое VT_UI4 varLongWord Четырёхбайтное беззнаковое целое VT_I8 varInt64 Восьмибайтное целое со знаком VT_RECORD — Структура. О способах хранения структур в вариантных типах мы поговорим на другом уроке. В структуре TVarData предусмотрены также типы, отсутствующие в стандартном типе VARIANT (например, varString). Это связано с тем, что разработчики Delphi расширили понятие вариантного типа и придали ему новые возможности, отсутствующие в Windows. Этот расширенный вариантный тип может быть использован кодом, написанным на Delphi, но при вызове системных функций программист обязан следить за тем, чтобы вариантные параметры имели динамический тип, совместимый с системным.
Разумеется, как и в случае с BSTR, в Delphi предусмотрен специальный тип для работы с вариантными структурами, при использовании которого все рутинные операции компилятор берёт на себя. Точнее, даже два таких типа: Variant и OleVariant. Разница между этими типами заключается в том, что OleVariant позволяет использовать только те динамические типы, которые совместимы с OLE, а Variant — также ещё и расширенные динамические типы, существующие только в Delphi. Так как предметом наших уроков является COM/DCOM, мы здесь не будем рассматривать Variant, а сосредоточимся только на OleVariant. Отметим только, что типы OleVariant и Variant совместимы по присваиванию, т.е. если V1 имеет тип OleVariant, а V2 — тип Variant, то допустимо присваивание V1 := V2. При этом если на момент выполнения этого действия V2 имеет не поддерживаемый OleVariant динамический тип, выполняется автоматическое приведение к нужному типу, если такое приведение возможно. В противном случае возникает исключение.
Помимо автоматизации работы с памятью OleVariant автоматизирует также и преобразования типов, т.е. на этапе компиляции переменные этого типа считаются совместимыми с типами Integer, Real, string, WideString и т.п., а реальная проверка совместимости текущего значения с требуемым типом выполняется на этапе выполнения. При необходимости также неявно может быть выполнено преобразование вариантного типа. Однако здесь следует иметь ввиду, что преобразование учитывает только системные настройки. Проиллюстрируем это простым примером. Пусть в системных настройках в качестве разделителя дробной и целой части вещественного числа используется запятая. Рассмотрим такой код:
var V: OleVariant; S1, S2: string; begin DecimalSeparator := '.'; V := 0.5; S1 := FloatToStr(V); S2 := V;В результате выполнения этого кода переменная S1 получит значение "0.5", а переменная S2 — значение "0,5". Это связано с тем, что в первом случае преобразование вещественного числа в строку выполняется функцией FloatToStr, которая учитывает значение переменной DecimalSeparator, а во втором случае — функцией VariantChangeType, для которой существуют только общесистемные настройки. Аналогичная ситуация возникает и при преобразованиях даты и времени.
При необходимости можно работать с отдельными полями структуры OleVariant напрямую, приводя соответствующую переменную к типу TVarData, например:
TVarData(V).VType := varDouble; TVarData(V).VDouble := 0.5;Однако при этом нужно внимательно следить за правильным выделением о освобождением памяти, иначе могут возникнуть утечки памяти и ошибки при автоматической финализации.
Безопасные массивы также поддерживаются компилятором Delphi, но называются они в данном случае вариантными массивами. Но если системный безопасный массив может использоваться отдельно от вариантного типа (для этого нужно вручную работать с указателем на SAFEARRAY), то указатель на вариантный массив в Delphi всегда хранится в вариантной переменной. Доступ к отдельным элементам осуществляется с помощью привычного для Delphi синтаксиса — индекса в квадратных скобках после имени переменной (то, что динамический тип является массивом, а также попадание индексов в нужный диапазон проверяется, разумеется, во время исполнения, а не компиляции).
Функций для работы с вариантными массивами в Delphi меньше, чем в системе для работы с безопасными массивами, и это понятно — нет нужды в функциях доступа к данным, удаления массива и низкоуровневого перераспределения — эти действия выполняются, в основном, неявно. Для создания вариантных массивов используются функции VarArrayCreate и VarArrayOf. Рассмотрим следующий пример (из справки Delphi):
var A: Variant; begin A := VarArrayCreate([0, 4], varVariant); A[0] := 1; A[1] := 1234.5678; A[2] := 'Hello world'; A[3] := True; A[4] := VarArrayOf([1, 10, 100, 1000]); WriteLn(A[2]); { Hello world } WriteLn(A[4][2]); { 100 } end;Здесь с помощью функции VarArrayCreate создаётся одномерный массив, индекс первого измерения принимает значения от 0 до 4. Элементы массива имеют вариантный тип, поэтому разным элементам массива присваиваются значения разных типов. Элемент с индексом 4 сам становится вариантным массивом, который создаётся с помощью функции VarArrayOf. Эта функция создаёт одномерный вариантный массив, индекс которого начинается с 0, а элементы принимают значения, переданные в качестве параметров.
Примечание: Не стоит путать двумерный вариантный массив и одномерный вариантный массив, каждый элемент которого также является вариантным массивом — по внутренней структуре это совершенно разные вещи. Даже обращение к элементам этих массивов должно выполняться по-разному. Для примера рассмотрим такой код:
var V1, V2, X: OleVariant; I: Integer; begin // Создаём двумерный вариантный массив с индексами [0..5, 0..5] V1 := VarArrayCreate([0, 5, 0, 5], varVariant); ... X := V1[2, 3]; // Это - правильное обращение к элементу X := V1[2][3]; // А здесь будет ошибка выполнения // Создаём одномерный вариантный массив с индексом [0..5] V2 := VarArrayCreate([0, 5], varVariant); // Делаем каждый элемент одномерным массивом с индексом [0..5] for I := 0 to 5 do V2[I] := VarArrayCreate([0, 5], varVariant); ... X := V2[0, 1]; // Здесь будет ошибка X := V2[0][1]; // А это - правильно end;В COM/DCOM широко используется понятие VARIANT-совместимого типа. Это синоним понятия "OLE-совместимый тип". Например, Integer, Double и IUnknown являются VARIANT-совместимыми, а Extended и LongBool — не являются. Тип string тоже не является VARIANT-совместимым, так как под этим термином подразумевается совместимость с системным типом VARIANT, а не с типов Variant в Delphi.