鳥の巣箱

ネトゲしたり、機械いじったり、ソフト書いたり、山篭ったり、ギャンブルしたりする人

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であるべきなのかを考えてみたほうがいいかもしれません。
場合によってはジェネリックスなどで対応できることもありますし。そもそもコーディングのときに型を決定できないという事がそうそうあることではない気がする