Defining Custom Variants in Delphi

One powerful built-in type of the Delphi language is the Variant type. Variants represent values whose type is not determined at compile time. Instead, the type of their value can change at runtime.

Variants can mix with other variants and with integer, real, string, and boolean values in expressions and assignments; the compiler automatically performs type conversions. By default, variants can’t hold values that are records, sets, static arrays, files, classes, class references, or pointers.

You can, however, extend the Variant type to work with any particular example of these types. All you need to do is create a descendant of the TCustomVariantType class that indicates how the Variant type performs standard operations. To create a Variant type:

  1. Map the storage of the variant’s data on to the TVarData record.
  2. Declare a class that descends from TCustomVariantType. Implement all required behavior (including type conversion rules) in the new class.
  3. Write utility methods for creating instances of your custom variant and recognizing its type.

The above steps extend the Variant type so that the standard operators work with your new type and the new Variant type can be cast to other data types. You can further enhance your new Variant type so that it supports properties and methods that you define.

When creating a Variant type that supports properties or methods, you use TInvokeableVariantType or TPublishableVariantType as a base class rather than TCustomVariantType.

Variants store their data in the TVarData record type. This type is a record that contains 16 bytes. The first word indicates the type of the variant, and the remaining 14 bytes are available to store the data.

While your new Variant type can work directly with a TVarData record, it is usually easier to define a record type whose members have names that are meaningful for your new type, and cast that new type onto the TVarData record type.

For example, the VarConv unit defines a custom variant type that represents a measurement. The data for this type includes the units (TConvType) of measurement, as well as the value (a double). The VarConv unit defines its own type to represent such a value:

TConvertVarData = packed record VType: TVarType; VConvType: TConvType; Reserved1, Reserved2: Word; VValue: Double; end;

This type is exactly the same size as the TVarData record. When working with a custom variant of the new type, the variant (or its TVarData record) can be cast to TConvertVarData, and the custom Variant type simply works with the TVarData record as if it were a TConvertVarData type.

If your new custom Variant type needs more than 14 bytes to store its data, you can define a new record type that includes a pointer or object instance. For example, the VarCmplx unit uses an instance of the class TComplexData to represent the data in a complex-valued variant.

It therefore defines a record type the same size as TVarData that includes a reference to a TComplexData object:

TComplexVarData = packed record VType: TVarType; Reserved1, Reserved2, Reserved3: Word; VComplex: TComplexData; Reserved4: LongInt; end;

Object references are actually pointers (two Words), so this type is the same size as the TVarData record. As before, a complex custom variant (or its TVarData record), can be cast to TComplexVarData, and the custom variant type works with the TVarData record as if it were a TComplexVarData type.

Custom variants work by using a special helper class that indicates how variants of the custom type can perform standard operations. You create this helper class by writing a descendant of TCustomVariantType. This involves overriding the appropriate virtual methods of TCustomVariantType.

One of the most important features of the custom variant type for you to implement is typecasting. The flexibility of variants arises, in part, from their implicit typecasts.

There are two methods for you to implement that enable the custom Variant type to perform typecasts: Cast, which converts another variant type to your custom variant, and CastTo, which converts your custom Variant type to another type of Variant.

When implementing either of these methods, it is relatively easy to perform the logical conversions from the built-in variant types. You must consider, however, the possibility that the variant to or from which you are casting may be another custom Variant type. To handle this situation, you can try casting to one of the built-in Variant types as an intermediate step.

In addition to the use of Double as an intermediate Variant type, there are a few things to note in this implementation:

  • The last step of this method sets the VType member of the returned TVarData record. This member gives the Variant type code. It is set to the VarType property of TComplexVariantType, which is the Variant type code assigned to the custom variant.
  • The custom variant’s data (Dest) is typecast from TVarData to the record type that is actually used to store its data (TComplexVarData). This makes the data easier to work with.
  • The method makes a local copy of the source variant rather than working directly with its data. This prevents side effects that may affect the source data.

When casting from a complex variant to another type, the CastTo method also uses an intermediate type of Double (for any destination type other than a string). Note that the CastTo method includes a case where the source variant data does not have a type code that matches the VarType property. This case only occurs for empty (unassigned) source variants.

To allow the custom variant type to work with standard binary operators (+, -, *, /, div, mod, shl, shr, and, or, xor listed in the System unit), you must override the BinaryOp method. BinaryOp has three parameters: the value of the left-hand operand, the value of the right-hand operand, and the operator.

Implement this method to perform the operation and return the result using the same variable that contained the left-hand operand. There are several things to note in this implementation: This method only handles the case where the variant on the right side of the operator is a custom variant that represents a complex number.

If the left-hand operand is a complex variant and the right-hand operand is not, the complex variant forces the right-hand operand first to be cast to a complex variant. It does this by overriding the RightPromotion method so that it always requires the type in the VarType property:

function TComplexVariantType.RightPromotion(const V: TVarData; const Operator: TVarOp; out RequiredVarType: TVarType): Boolean; begin { Complex Op TypeX } RequiredVarType := VarType; Result := True; end;

The addition operator is implemented for a string and a complex number (by casting the complex value to a string and concatenating), and the addition, subtraction, multiplication, and division operators are implemented for two complex numbers using the methods of the TComplexData object that is stored in the complex variant’s data.

This is accessed by casting the TVarData record to a TComplexVarData record and using its VComplex member. Attempting any other operator or combination of types causes the method to call the RaiseInvalidOp method, which causes a runtime error.

The TCustomVariantType class includes a number of utility methods such as RaiseInvalidOp that can be used in the implementation of custom variant types. BinaryOp only deals with a limited number of types: strings and other complex variants.

It is possible, however, to perform operations between complex numbers and other numeric types. For the BinaryOp method to work, the operands must be cast to complex variants before the values are passed to this method.

We have already seen (above) how to use the RightPromotion method to force the right-hand operand to be a complex variant if the left-hand operand is complex. A similar method, LeftPromotion, forces a cast of the left-hand operand when the right-hand operand is complex:

function TComplexVariantType.LeftPromotion(const V: TVarData; const Operator: TVarOp; out RequiredVarType: TVarType): Boolean; begin { TypeX Op Complex } if (Operator = opAdd) and VarDataIsStr(V) then RequiredVarType := varString else RequiredVarType := VarType; Result := True; end;

This LeftPromotion method forces the left-hand operand to be cast to another complex variant, unless it is a string and the operation is addition, in which case LeftPromotion allows the operand to remain a string.