Версия для печати


Урок 11. Вариантный тип и безопасные массивы
http://www.delphikingdom.com/asp/viewitem.asp?catalogID=1358

Антон Григорьев
дата публикации 10-06-2008 09:21

урок из цикла: Использование COM/DCOM в Delphi

Урок 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, описывающую т.н. безопасный массив. Безопасные массивы — это специальные системные структуры, имитирующие обычные массивы. Безопасными они называются потому, что система обладает всей информацией о том, как выделяется память этому массиву и, следовательно, может корректно (т.е. "безопасно") её освободить при необходимости. Основные свойства безопасных массивов следующие:

  1. Безопасные массивы могут иметь любое число размерностей.
  2. Нижняя граница размерности может быть отлична от нуля и задаётся отдельно для каждой размерности.
  3. Безопасные массивы могут хранить данные любых вариант-совместимых типов (т.е. типов, задающихся константами 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_XXXvarXXXДинамический тип
VT_EMPTYvarEmptyПеременная не имеет никакого значения
VT_NULLvarNullПеременная имеет пустое значение
VT_I2varSmallIntДвухбайтное целое со знаком
VT_I4varIntegerЧетырёхбайтное целое со знаком
VT_R4varSingleЧетырёхбайтное вещественное
VT_R8varDoubleВосьмибайтное вещественное
VT_CYvarCurrencyТип Currency (валюта)
VT_DATEvarDateДата-время в формате TVarDate (вещественное число, целая часть которого показывает число полных дней, прошедших с 30.12.1899, дробная часть — долю прошедшего неполного дня)
VT_BSTRvarOleStrСтрока типа BSTR
VT_DISPATCHvarDispatchУказатель на интерфейс IDispatch (этот интерфейс будет рассмотрен позже, в главе про автоматизацию)
VT_ERRORvarErrorЗначение типа SCODE (устаревший тип для кодирования ошибок, применявшийся до введения HRESULT)
VT_BOOLvarBooleanДвухбайтный логический тип. Значение 0 соответствует False, -1 ($FFFF) — True. Остальные значения не определены. В системе определены константы VARIANT_TRUE и VARIANT_FALSE, кодирующие эти значения. В Delphi для этого типа никакие специальные константы не определены, можно использовать True и False.
VT_VARIANTvarVariantВариантный тип. Может применяться только с VT_BYREF, т.е. в структуре хранится ссылка на другрую такую же структуру.
VT_UNKNOWNvarUnknownУказатель на IUnknown
VT_I1varShortIntОднобайтное целое со знаком
VT_UI1varByteОднобайтное беззнаковое целое
VT_UI2varWordДвухбайтное беззнаковое целое
VT_UI4varLongWordЧетырёхбайтное беззнаковое целое
VT_I8varInt64Восьмибайтное целое со знаком
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.