Variant型について
DelphiにはVariantという型が存在します。
これは非常に便利な型です。
コンパイル時に、型を一意に決定できないデータの操作が必要な場合に使えて、実行時に型を変更できます。
例えばこんなプログラムを動かしてみます。
program Project1; {$APPTYPE CONSOLE} {$R *.res} uses System.SysUtils; var int, dbl, str : Variant; ans : Variant; begin try int := 10; dbl := 10.1; str := '10.5'; Writeln('int+dbl = ', int+dbl, #10); Writeln('int+str = ', int+str, #10); Writeln('dbl+str = ', dbl+str, #10); while True do except on E: Exception do Writeln(E.ClassName, ': ', E.Message); end; end.
それぞれVariant型の変数に、整数、小数、文字列の数値を代入しています。
これを加算した結果を出力するものですが、結果はこうなります。
int+dbl = 20.1 int+str = 20.5 dbl+str = 20.6
Variant型の非常にありがたいのは、文字列として格納された数字であっても、Variant型なら加算時に数値変換してくれているという点です。
ただ、もちろんですが
str := 'test';
のように、数値変換できない文字列を加算しようとした場合は、
$755FB512 で初回の例外が発生しました。例外クラスは EVariantTypeCastError メッセージは 'UnicodeString型から Double 型へのバリアント型変換はできません'。 プロセス Project1.exe (17036)
といった例外が発生します。キャストエラーです。
こういった例外に対しての処理などは当然必要になるでしょうが、使い方によっては便利な方であることも確かです。
Variantの欠点と、使う上で注意
では、Variant型の欠点とはなんなのか。
一つはメモリを消費することです。
Variant型は32bitプラットフォームでは16Byteレコードとして格納されます。64bitの場合は24Byteです。
変数一つにこれだけのメモリを必要とします。
Variant型と配列の扱い
Variantはすべての型を格納できるわけではありません。
通常の静的配列をVariantに割り当てることはできません。
例えば以下のようなコード
program Project1; {$APPTYPE CONSOLE} {$R *.res} uses System.SysUtils; var IntArr : Array[0..9] of Integer; V : Variant; I : Integer; begin try V := IntArr; for I := 0 to 9 do begin V[I] := I; Writeln(V[I], #13); end; while True do except on E: Exception do Writeln(E.ClassName, ': ', E.Message); end; end.
静的配列をVariant型へ代入しようとしていますが、これはビルドできません。
[dcc32 エラー] Project1.dpr(23): E2010 'Variant' と 'array[0..9] of Integer' には互換性がありません
この場合も同様です。
var IntArr : Array[0..9] of Integer; Varr : Array[0..9] of Variant; V : Variant; I : Integer; begin try Varr := IntArr; for I := 0 to 9 do begin V[I] := I; Writeln(V[I], #13); end; while True do except on E: Exception do Writeln(E.ClassName, ': ', E.Message); end; end.
動的配列は格納できます。ですが、ポインタを代入したわけではないので代入元の配列の要素は書き換わりません。
例を見てみます。
var IntArr : Array of Integer; V : Variant; I : Integer; begin try SetLength(IntArr, 10); V := IntArr; for I := 0 to 9 do begin V[I] := I; Writeln('V[',I,'] = ',V[I], #13); Writeln('IntArr[',I,'] = ',IntArr[I], #13); end; while True do except on E: Exception do Writeln(E.ClassName, ': ', E.Message); end;
V[0] = 0 IntArr[0] = 0 V[1] = 1 IntArr[1] = 0 V[2] = 2 IntArr[2] = 0 V[3] = 3 IntArr[3] = 0 V[4] = 4 IntArr[4] = 0 V[5] = 5 IntArr[5] = 0 V[6] = 6 IntArr[6] = 0 V[7] = 7 IntArr[7] = 0 V[8] = 8 IntArr[8] = 0 V[9] = 9 IntArr[9] = 0
動的配列IntArrをVariant型に代入しています。
その後、Variant型配列に対して値を代入していますが、IntArrそのものの中身は書き換わっていないのがわかります。
なぜかと言うと、Variant型に対して動的配列を代入した時にはVarArrayCreateメソッドが呼び出されるからです。
VarArrayCreateメソッドは、要素数を引数として与えることでVariant配列を返してくれるメソッドです。
なので、新しい配列としてメモリが確保されるからです。
相互に代入することはできるので、それでデータを保持することは可能です。
var IntArr : Array of Integer; V : Variant; I : Integer; begin try SetLength(IntArr, 10); for I := 0 to 9 do begin IntArr[I] := I; Writeln('IntArr[',I,'] = ',IntArr[I], #13); end; V := IntArr; for I := 0 to 9 do begin Writeln('V[',I,'] = ',V[I], #13); V[I] := V[I]+I; end; IntArr := V; for I := 0 to 9 do begin Writeln('IntArr[',I,'] = ',IntArr[I], #13); end; while True do except on E: Exception do Writeln(E.ClassName, ': ', E.Message); end; end.
IntArr[0] = 0 IntArr[1] = 1 IntArr[2] = 2 IntArr[3] = 3 IntArr[4] = 4 IntArr[5] = 5 IntArr[6] = 6 IntArr[7] = 7 IntArr[8] = 8 IntArr[9] = 9 V[0] = 0 V[1] = 1 V[2] = 2 V[3] = 3 V[4] = 4 V[5] = 5 V[6] = 6 V[7] = 7 V[8] = 8 V[9] = 9 IntArr[0] = 0 IntArr[1] = 2 IntArr[2] = 4 IntArr[3] = 6 IntArr[4] = 8 IntArr[5] = 10 IntArr[6] = 12 IntArr[7] = 14 IntArr[8] = 16 IntArr[9] = 18
Variant型をパラメータとして与える場合
パラメーターの種類については以前の記事を読んでください。
birdhouse.hateblo.jp
それぞれのパラメータに対してVariantを与えてみます。
procedure Add(val :Integer); begin val := val + 1; writeln('Add : ', val); end; procedure Add_var(var val:Integer); begin val := val + 1; writeln('Add_var : ', val); end; procedure Add_const(const val:Integer); begin Writeln('Add_const : ', val+1); end; procedure Add_const_ref(const [Ref] val:Integer); begin Writeln('Add_const : ', val+1); end; procedure Add_out(out val:Integer); begin val := val +1; Writeln('Add_out : ', val); end; var V : Variant; begin try V := 1; Add(V); Add_var(V); Add_const(V); Add_const_ref(V); Add_out(V); while True do except on E: Exception do Writeln(E.ClassName, ': ', E.Message); end; end.
はい。このコードですが、コンパイルできません。
[dcc32 エラー] Project1.dpr(44): E2033 変数実パラメータと変数仮パラメータとは同一の型でなければなりません
このエラーが出ます。
エラーが出るのはAdd_varとAdd_outに対してVariantを与えた時の2行です。
constには与えることができます。
さて、先程のコードを少し変更します。
program Project1; {$APPTYPE CONSOLE} {$R *.res} uses System.SysUtils, System.Variants; procedure Add(val :Integer); begin val := val + 1; writeln('Add : ', val); end; procedure Add_const(const val:Integer); begin Writeln('Add_const : ', val+1); end; procedure Add_const_ref(const [Ref] val:Integer); var cash : PInteger; begin cash := @val; cash^ := cash^ +1; Writeln('Add_const : ', val); end; var V : Variant; I : Integer; begin try for I := 0 to 2 do begin V := 1; Writeln('before main : ', V); case I of 0 : Add(V); 1 : Add_const(V); 2 : Add_const_ref(V); end; Writeln('after main : ', V); end; while True do except on E: Exception do Writeln(E.ClassName, ': ', E.Message); end; end.
Add_const_refでの処理内容が
手続き、関数のパラメータについて – 鳥の巣箱
ここで紹介したのと同様に強制参照にすることでvarと同様の動作をするパターンになっています。
これを実行してみましょう。
before main : 1 Add : 2 after main : 1 before main : 1 Add_const : 2 after main : 1 before main : 1 Add_const_ref : 2 after main : 1
結果はこんな感じです。
ここで見てみると、Add_const_refでの動作が通常のconstと同じになっており、[Ref]をつけた意味がなくなっています。
これも配列のときと同様に、パラメータに代入するときに_VarToIntegerが呼び出されているからです。
アドレスを確認するとわかりますが、[Ref]がついていてもVariantのポインタが渡されるわけではないので呼び出し元に影響を与えることはありません。
program Project1; {$APPTYPE CONSOLE} {$R *.res} uses System.SysUtils, System.Variants; procedure Add(val :Integer); begin val := val + 1; writeln('Add : ', val); end; procedure Add_const(const val:Integer); begin Writeln('Add_const : ', val+1); end; procedure Add_const_ref(const [Ref] val:Integer); var cash : PInteger; begin cash := @val; cash^ := cash^ +1; Writeln('Add_const_ref pointer : '+ IntToHex(Integer(Pointer(@cash)))); Writeln('Add_const_ref : ', val); end; var V : Variant; I : Integer; begin try for I := 0 to 2 do begin V := 1; Writeln('before main : ', V); Writeln('main variant pointer : '+ IntToHex(Integer(@V))); case I of 0 : Add(V); 1 : Add_const(V); 2 : Add_const_ref(V); end; Writeln('after main : ', V); end; while True do except on E: Exception do Writeln(E.ClassName, ': ', E.Message); end; end.
before main : 1 main variant pointer : 00431380 Add : 2 after main : 1 before main : 1 main variant pointer : 00431380 Add_const : 2 after main : 1 before main : 1 main variant pointer : 00431380 Add_const_ref pointer : 0019FF1C Add_const_ref : 2 after main : 1
ここでわかることとしては、基本的にVariantをパラメータとして与える場合、その値のコピーが生成されてそれが渡されるということです。
[Ref]をつけようが関係ありません。
配列を含んだVariantをパラメータとして渡す場合、配列そのものがコピーされることになるのでメモリ効率が悪くなります。
そういった点も考慮して設計する必要があります。
あとがき
とりあえずVariantについてざっと今知ってることを書きました。
多分これ以外にも色々な事が絡んでくると思います。
そのへんはまぁ、気がついたらまた書き足していくとします。
Variantは便利な型ではありますが、通常の型と同様に考えていると思わぬエラーにつながることが多々あります。
Variantを使う前に、まず本当にVariantであるべきなのかを考えてみたほうがいいかもしれません。
場合によってはジェネリックスなどで対応できることもありますし。そもそもコーディングのときに型を決定できないという事がそうそうあることではない気がする
ディスカッション
コメント一覧
まだ、コメントがありません