I’ve never used variant type. I’m curious why it’s necessary? Doesn’t it go contrary to the strong typing that pascal is famous for?
Also if a variant type variable can hold different types how do you know what type it contains at any given moment before trying to use it?
Variant is a form of runtime polymorphism. Polymorphism is when you have the same code (from a callers perspective) handling different data types transparently. Generally you can distinguish between runtime polymorphism, where the type of data is only known at runtime, and compiletime polymorphism, where the compiler already knows the type.
First as an example of the latter, the most simple form is function overloading
procedure Foo(x: integer);
begin
WriteLn('Integer: ', x);
end;
procedure Foo(x: String);
begin
WriteLn('String: ', x);
end;
You can now use the function Foo on an integer or string parameter, and the compiler will figure out which of those implementations to call. So you have the same function for multiple data types that is decided on compiletime, i.e. compiletime polymorphism. Also the compiler does not create any runtime code for checking if x is really an integer during runtime, because the full disambiguation happens at compiletime. Besides this, when you write Foo(3.14) the compiler can see that there is no definition for floats, and throws an error.
So compiletime polymorphism is usually very efficient and allows the compiler to perform full typechecks.
Runtime polymorphism on the other hand is when the type is only known at runtime, e.g. with variant:
procedure Foo(v : Variant);
begin
Case varType(v) of
varString:
Writeln('String: ', String(v));
varInteger:
Writeln('Integer: ', Integer(v));
else
Writeln('Unable to determine variant type');
end;
end;
Like before this function accepts strings and integers, but it uses the case statement to check which code to execute. This has two side effects. First the case statement is runtime code, so it will be compiled in and require extra execution time, so this code is less efficient as the compiletime example above. Second because variant can be a lot of different types (anything except records, objects and enums), even contain no data at all, you can also call Foo(3.14) and your code compiles fine. It will just during execution print a runtime error that this type is not supported.
So compared to compiletime polymorphism, runtime polymorphism is less efficient and has no compiler error checking. Also usually requires much more and less readable code. Therefore whenever you need polymorphism, but compiletime polymorphism is fully sufficient, you should use it.
Variant and array of const are especially bad, because they allow for all kinds of data. With your own variant records or even non variant records with just two fields (for managed types useful).
For exa.ple I have created a union type, which can hold one of two types:
https://github.com/Warfley/ObjPasUtils/tree/master/src/dynamictypes#tuniont-uUsing this the function would look like this:
procedure Foo(u : TUnion<String, Integer>);
begin
If u.IsFirst then // first type is String
WriteLn('String :', u.First)
else if u.IsSecond then // second is Integer
WriteLn('Integer: ', u.Second)
else
WriteLn('Error u is empty);
end;
This now only allows for strings and integers, meaning Foo(3.14) will throw a compiler error. But still allows for empty values (Foo(None)) and also still adds runtime overhead. So still worse than the compiletime polymorphism alternative