お世話になっています。
Delphi2007,XPです。
Record型で宣言した変数に設定されている、フィールド数を求めるにはどうしたらよいでしょうか。
自分でフィールドを宣言しておいて、どうしてフィールド数を求める必要があるのか?と言われそうですが、知りたいので、よろしくお願いします。
(例)
AData = record
X:Integer;
Y:Integer;
Z:Integer;
end;
の場合、'3'を求めたいです。
よろしくお願いします。
使ったことはありませんが...
http://docwiki.embarcadero.com/RADStudio/XE2/ja/%E6%A7%8B%E9%80%A0%E5%8C%96%E5%9E%8B%E3%81%AE%E6%83%85%E5%A0%B1
こんにちは,Mr.XRAYです.
Delphi 2007って,RTTI(実行時型情報)使えましたっけ?
もし,Delphi 2010以降であれば,
以下のコードでレコード型のメンバー数が取得できるんですが.
// usesにRttiが必要
// 動作確認は,Windows XP(SP3) + Delphi XE
procedure TForm1.Button2Click(Sender: TObject);
var
LContext : TRttiContext;
LType : TRttiType;
FFields : TArray<TRttiField>;
begin
LContext := TRttiContext.Create;
// TTestRecordを,対象のレコード型の型名とする
LType := LContext.GetType(TypeInfo(TTestRecord)) as TRttiType;
FFields := LType.GetFields;
ShowMessage(InttoStr(Length(FFields)));
end;
>Delphi 2007って,RTTI(実行時型情報)使えましたっけ?
Novさんが教えてくれたリンクで,試しにやってみたんですが,
もしかしたら,TypeInfoだけでも可能かも知れません.
TypeInfoで,列挙型の情報は取得もできますから.
間違っていたらゴメンなさい.
>もしかしたら,TypeInfoだけでも可能かも知れません.
TypInfo.pasの中を見てみましたが,それらしきものはありませんね.
う〜ん,残念だ.
ありがとうございます。
教えてもらった内容を参考にして調べてみましたが、わかりませんでした。
usesにRttiを書くと、エラーが発生します。
Delphi2007では無理なんですかね?
もうちょっと調べてみます。
>Delphi2007では無理なんですかね?
無理っぽいですね.勇気ある徹底かな?
人生,時にはあきらめも肝心かと(笑)
>Delphi2007では無理なんですかね?
だめですか...
一応、XE2でTypeInfoを試しましたが、RecSizeは取得できたものの、
どうもTTypeDataの記述が実際のRTTIと違っているのか、ManagedFldCountが
取得できませんでした(2007なら行けるかも?)。
失礼しました。ManagedFldCountは関係ないですね。
XE2のコードでは、コメントアウトされている、RecFldCntが、目的の値のようです。
メモリダンプでは見れるのに、構造体が対応していないのは、可変長フィールドがあるからかな...
XE2の場合、下記方法でとりあえず取得できましたが、2007では分かりません。
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, TypInfo;
type
TTest = packed record
w, x, y, z: Integer;
end;
TForm1 = class(TForm)
Memo1: TMemo;
procedure FormCreate(Sender: TObject);
private
{ Private 宣言 }
public
{ Public 宣言 }
pt: TTest;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.FormCreate(Sender: TObject);
var
pti: PTypeInfo;
ptd: PTypeData;
pNumOps: PByte;
pRecFldCnt: PInteger;
begin
Memo1.Clear;
pti := TypeInfo(TTest);
Memo1.Lines.Add(Format('%s(%d)', [String(pti.Name), Integer(pti.Kind)]));
ptd := GetTypeData(pti);
// tkRecord: (
// RecSize: Integer;
// ManagedFldCount: Integer;
// {ManagedFields: array[0..ManagedFldCnt - 1] of TManagedField;
// NumOps: Byte;
// RecOps: array[1..NumOps] of Pointer;
// RecFldCnt: Integer;
// RecFields: array[1..RecFldCnt] of TRecordTypeField;
// RecAttrData: TAttrData;
// RecMethCnt: Word;
// RecMeths: array[1..RecMethCnt] of TRecordTypeMethod});
Memo1.Lines.Add(Format('RecSize = %d', [ptd.RecSize]));
pNumOps := PByte(ptd)+SizeOf(Integer)*2+SizeOf(TManagedField)*ptd.ManagedFldCount;
pRecFldCnt := PInteger(pNumOps+SizeOf(pNumOps^)+SizeOf(Pointer)*pNumOps^);
Memo1.Lines.Add(Format('RecFldCnt = %d', [pRecFldCnt^]));
end;
usesの表記が2007は違いますよね。
uses
Windows, SysUtils, Classes, Graphics, Controls, Forms, StdCtrls, TypInfo;
かも。
どうも,
>2007では分かりません。
やってみました.
TypeInfoでも取得できるんですね.失礼しました.
Memo1.Lines.Add(Format('RecSize = %d', [ptd.RecSize])); //RecSizeが未定義エラー
となりますね.Delphi 2010では取得できます.
Delphi 2009では,コンパイルはエラーなしでできますが,RecSizは0となりました.
調べたら,やはり,PTypeDataがDelphi 2010でかなり拡張されています.
Delphi 2007には,ManagedFldCountもありません.
以上,一応報告まで,
> Memo1.Lines.Add(Format('RecSize = %d', [ptd.RecSize])); //RecSizeが未定義エラー
検証ありがとうございました。
ということで無駄に長いコメントになってしまいましたね(反省)。
ということで,
(1) 実行時型情報の機能を使用してレコード型の情報も取得できる
RTTI.pasあるいは,TypInfo.pasのどちらのルーチンを使用しても可能
(2) ただし,このレコード型の情報が取得できるのは,Delphi 2010以降
(3) Delphi 2009以前のDelphiで取得する方法は,現在のところ不明
(4) Delphi 2007でも,Delphi 2010と同じコードを実装すれば可能かもしれないが,
かなりの困難が予想される
ということでしょうか.
質問者じゃないのに,勝手にまとめてしまいました.(ゴメンなさい)
人生,時にはあきらめも肝心かと(笑) --> しつこい!
ありがとうございます。
Novさんの2012/05/04(金) 02:50:37の書き込み(遅い時間にありがとうございます)について、
37: pti := TypeInfo(TTest);
で、
TTest型は型情報を持っていません。
というエラーが出て、コンパイルができませんでした。
Mr.XRAYさんには、まとめていただいて、ありがとうございました。
おっしゃるとおり、Delphi2007ではあきらめます。
この前、試しにXE2の試用版をインストールしてみたんですが、2007で作ったプログラムのソースをコンパイルすると、かなりエラーが出て、コンパイル完了できなかったので、XE2を購入するかどうかを考えているところです。
>37: pti := TypeInfo(TTest);
>で、
>TTest型は型情報を持っていません。
つまり,レコード型の情報は取得できないということですね.
つまり,由緒正しいエラーです(笑)
話はそれますが.
>2007で作ったプログラムのソースをコンパイルすると、かなりエラーが出て、
Delphi 2009でUnicoed化されましたからね.
どこかでエラーが発生すると,それが原因でまたエラーとなることがあります.
どこか数か所直すだけで,エラーがどんどん減っていくもんです.
基本的なことが理解できてしまえば,そんなに苦労はしないかも知れません.
今は情報がいっぱいありますから.
文字列関係の扱いに要注意ですね.
>おっしゃるとおり、Delphi2007ではあきらめます。
大変いい心掛けです(笑)
お役に立ちませんで,申し訳ない.
Delphi 2010以降にすれば解決できる問題ということがわかったので、とりあず解決ということにします。
ありがとうございました。
高級言語指向的な解決策が出されていますが、
低級言語的な扱いでも回避策が有りそうな気がします。
ただしその場合だと汎用性が下がるためもう少し目的をはっきりさせないとなりません。
TListに複数タイプのオブジェクトを格納して適当に取り出したオブジェクトがどのタイプかを判定するとかが今回の目的ですかね?
>低級言語的な扱いでも回避策が有りそうな気がします。
実行時型情報が得られないと前に進まないので発想を変えて
もしレコード型の要素がIntegerだけなのだったら
これでいけますけど
たぶんこれじゃ駄目なのでしょうけどね
// レコード型
type AData = record
X:Integer;
Y:Integer;
Z:Integer;
end;
// Formに変数として実装
private
{ Private 宣言 }
FData : AData;
// 全体で必要なメモリ量を1要素が使うメモリ量で割る
var
i,j : Integer;
begin
i := SizeOf(FData);
j := SizeOf(Integer);
caption := IntToStr(i div j);
end;
ありがとうございます。
takeさんのソースで、"3"を取得できました。
ただ、ご察しの通り、実際のプログラムでは、Record型の要素はIntegerだけではないので、これは使えないと思います。せっかくなのに、ごめんなさい。
ところで、なぜ、このようなことがしたかったかというと、その目的は、Record型の変数をファイルに保存したり、ファイルから読み込んだ値をRecord型の変数に代入するのに必要だと考えたからです。
[現在]
保存するときは、
AssignFile( stf, FileName );
Rewrite( stf );
Writeln( stf,x );
Writeln( stf,y );
Writeln( stf,z );
CloseFile( stf );
※実際は、数値型の場合はIntToStrで変換しています。
読み込む時は、
AssignFile( ltf, FileName );
Reset( ltf );
while Eof( ltf )=false do
Begin
Readln( ltf, LoadLine );
sl.add(LoadLine)
End;
CloseFile( ltf );
として、一旦StringList型の変数SLに代入してから、
x:=sl[0];
y:=sl[1];
z:=sl[2];
というようにしています。
今は、これで問題ないのですが、プログラムを拡張して、Record型の要素数が増えた時に、古いファイルを開くと、都合が悪いのです。
例えば、
今後、Record型の要素に"w"が増えた場合に、
AssignFile( ltf, FileName );
Reset( ltf );
while Eof( ltf )=false do
Begin
Readln( ltf, LoadLine );
sl.add(LoadLine)
End;
CloseFile( ltf );
として、一旦StringList型の変数SLに代入してから、
x:=sl[0];
y:=sl[1];
z:=sl[2];
w:=sl[3];
とすると、古いファイルには、sl[3]に対応する値は存在しないために、エラーが発生すると思います。
そこで、
x:=sl[0];
y:=sl[1];
z:=sl[2];
w:=sl[3];
を実行する直前に、Record型の要素の数を数え、sl.countよりも多い場合は、
その数の差分、
sl.add('');
を行って、sl.countをRecord型の要素の数と同じにしたいのです。
もしかしたら、
Record型の変数をファイルに保存したり、ファイルから読み込んだ値をRecord型の変数に代入するこの方法に問題があるのかもしれません。
もし、Record型の変数をファイルに入出力するよい方法があれば、教えていただければありがたいです。
自分が構造体(もしくはバイナリデータ)をファイルに保存するときは、先頭の1バイトにバージョンを書き込み、以降に構造体を保存します。
読み込むときはこの先頭の1バイトを読み込んでから判断するようにしています。バージョン管理は面倒ですが...
また、読込の安全性を確保するには、シグネチャを書き込むのもありかと。
えーと私なりに仕様をまとめますと。
レコード型のデータ「1個」だけをファイルに入出力したい。
バージョンによってレコード型の内容は異なる(増えたり減ったりする)
バージョンの異なるデータを開く可能性を考慮する。
サンプルを書く前に確認です。
その1
整数を文字にしているという事は、
保存されたデータはメモ帳で編集可能にしたいということでしょうか?
データ構造的にバイナリにしてしまった方が楽ですが、もちろん可能です。
その2
不確定長文字列は扱いますか?
これを入れるか入れないかでサンプルを変えるつもりです。
その3
レコードデータの量はどの程度ですか?
大きさによってベストパフォーマンスを出す方法は変わります。
個人的にはデータ管理はオブジェクト型から派生させた方が楽だと思いますが。
ちなみに私が公開してるソフトだとバージョン管理は面倒なので、
バージョンチェック後、合わない場合は「互換性がありません」で、終了させてます ^^;
補足
バージョンによってデータが減るのはやらないほうが良いです。
増える一方の方が扱いやすいですし、そうあるべきだと思います。
構造型になっている変数を保存するなら
クラスにしてオブジェクトをファイルストリームに出力して保存が
一番手っ取り早いですね。
でもクラスを拡張すると古いファイルは読み込めなくなるので意味なしかな
もっと簡単にやる場合はTStringListを使うと便利です。
メモ帳で書いてますのでデバッグしてませんが
// 保存
var
t : TStringList;
begin
t := TStringList.Create;
x := 1;
y := 2;
z := 3;
t.Values['x'] = IntToStr(x);
t.Values['y'] = IntToStr(y);
t.Values['z'] = IntToStr(z);
t.SaveToFile('save.ini');
t.free;
// 読み込み
var
t : TStringList;
begin
t := TStringList.Create;
t.LoadFromFile('save.ini');
x := StrToIntDef(t.Values['x'],x);
y := StrToIntDef(t.Values['y'],y);
z := StrToIntDef(t.Values['z'],z);
t.free;
// save.iniの中身
x=1
y=2
z=3
じゃあこのレコード型が配列だったらどうするかというと
プログラムは書きませんがsave.iniのイメージはセクションを使ってこんな感じ
[0]
x=1
y=2
z=3
[1]
x=
y=
z=
>もし、Record型の変数をファイルに入出力するよい方法があれば、
バージョン管理は他のファイルを利用するとした場合。
下記はあくまでテストです、file of 型が肝です。
FileA:file of TA_AData;の所をFileA:file of TAData;としても
もちろんOKです、型の単位で読み書きします,
私にとって簡単なのはこれです。
通常はFileStreamを使ってバージョンも含めて1つのファイルにします。
implementation
{$R *.dfm}
type
TAData = record
x, y, z: Integer;
end;
TBData = record
w, x, y, z: Integer;
end;
TCData = record
x, y, z: Integer;
st:string[80];
end;
TA_AData=array[1..10] of TAData;
TA_BData=array[1..10] of TBData;
TA_CData=array[1..10] of TCData;
var
AData:TA_CData;
BData:TA_CData;
CData:TA_CData;
FileA:file of TA_AData;
FileB:file of TA_BData;
FileC:file of TA_CData;
VarNo:integer;
procedure TForm1.BitBtn1Click(Sender: TObject);
var i:integer;
begin
VarNo:=3;
for i := 1 to 10 do
begin
CData[i].x:=i;
CData[i].y:=i;
CData[i].z:=i;
CData[i].st:=IntToStr(i);
end;
AssignFile(FileC, 'C:\TestGoGo');
Rewrite( FileC );
write(FileC,CData);
CloseFile(FileC);
end;
end.
返事が遅くなりまして、すみません。
たくさんのご指導、ありがとうございます。
仕様に関しては、データは増やす一方で、データの並び順も変えずに、項目を追加するときは、必ずいちばん後ろにする、と考えています。
takeさんのソースは、よかったです。
今回のタイトルとは関係ないけど、
t : TStringList;
は、
t.Values['x'] = IntToStr(x);
t.Values['y'] = IntToStr(y);
t.Values['z'] = IntToStr(z);
という使い方ができるということがわかって、よかったです。
連想配列みたいなものですよね?
TSさんのソースは、
私が書いたものと同じ
AssignFile();
を使ってくれたので、わかりやすかったです。
TA_CData=array[1..10] of TCData;
CData:TA_CData;
FileC:file of TA_CData;
という宣言の仕方を知らなかったので、よかったです。
いただいたソースを参考にして、自分でもソースを考えてみます。
返事が遅くなりまして、すみません。
takeさんのご指導を参考にして、
t:TStringList
に変数を代入して、(t.count)からフィールド数を求めようと思いましたが、わざわざRecord型で宣言した変数をTStringList型の変数に入れてまでフィールド数を求めるなら、直接Record型変数の宣言部分を数えたほうが早いかな、と思いました。
TSさんのご指導を参考にして、
file of型を利用したらファイルへの読み書きが簡単になると思ったんですが、これだと、文字列の文字数を限定しないといけないようですね。文字数は無制限(メモリの許す限り)にしたいので、これもやめとこうと思いました。
結局、Mr.XRAYさんのご指導を参考にして、
Delphi2007ではあきらめようと思います。
たくさんご指導していただきまして、ありがとうございました。
>file of型を利用したらファイルへの読み書きが簡単になると思ったんですが
>これだと、文字列の文字数を限定しないといけないようですね。
おっしゃる通りです。
>通常はFileStreamを使ってバージョンも含めて1つのファイルにします。
FileStreamだとその制限はありません。
そんなにハードルは高くないと思います。
今gooで「Delphi FileStream」検察をしたら。
http://u670.com/delphi_tips@/tips0050.html
http://delphi.about.com/library/fcl/system/io/blfclfilesream.htm
http://blog.livedoor.jp/toymap/archives/17293855.html
基本的には初期化して変数とデータ量を指定して読み書きをすると言う
使い方です。
私の場合最初(1)にバージョン(2)にデータの数量(3)実際のデータ
後(2)(3)の繰り返しです、バイナリーだろうが何だろうが関係ありません。
読み込む時はどういう順番で書き込んだか分かっていないと読み込めません。
上記何を言っているのか分からなかったらFileStreamで調べて下さい。
ありがとうございます。
TSさんのご指導によって、FileStreamで調べてみて、ソースを考えました。
例えば、
バージョン①では、
AData = record
X:Integer;
Y:Integer;
Z:Integer;
end;
バージョン②では、
AData = record
X:Integer;
Y:Integer;
Z:Integer;
W:String;
end;
だとすると、
バージョン①のファイルを書き込むとき、
ファイルには、
(1)バージョン ①
バージョン①は、フィールド数が[X],[Y],[Z]の3個なので、(2)(3)を3回繰り返して、
(2)データの数量 [X] はIntegerで、サイズは4バイト
(3)実際のデータ 811(例)
(2)データの数量 [Y] はIntegerで、サイズは4バイト
(3)実際のデータ 1351(例)
(2)データの数量 [Z] はIntegerで、サイズは4バイト
(3)実際のデータ 092(例)
という風にファイルの書き込めばいいのですね。
ファイルへの書き込みはわかったのですが、最初の質問に戻ってしまいました。
最初の質問は、「Record型のフィールド数を求めるには?」だったんですが、
結局、バージョン情報とそのデータのサイズと内容を個数分ファイルへ書き込んむとき、
バージョン①では、
AData = record
X:Integer;
Y:Integer;
Z:Integer;
end;
なので、
バージョン情報①、データのサイズと内容を3個分
バージョン②では、
AData = record
X:Integer;
Y:Integer;
Z:Integer;
W:String;
end;
なので
バージョン情報②、データのサイズと内容を4個分
をファイルに書き込む、つまり、Record型のフィールド数を求めるには、ソースコードを直接見て数えて、その数にあわせて読み書きのソースコードを書くしかない、ということですよね。
例えば、ストリングリスト型の変数の場合、
t : TStringList;
ならば、
t.countでtの要素数がわかるので、
for i:=1 to t.count begin
(2)t[i]のサイズを書き込み
(3)t[i]の内容を書き込み
end;
とすれば、ソースコードを見てtの要素数を数えなくても、要素数分のデータのサイズと内容を書き込めますが、
record型の変数の場合、
AData = record
X:Integer;
Y:Integer;
Z:Integer;
end;
で、AData.count
みたいなことをしても、フィールド数はわかりませんよね。
前回あきらめようと思ったことは、ファイルへの読み書きではなく、このAData.countみたいに、簡単にRecord型の変数のフィールド数を求めることをあきらめようと思って書きました。
ご指導いただいた内容を参考にして、Record型のデータをファイルへ読み書きします。
ありがとうございました。
バージョン管理をすると言う事はプログラマーはデータ構造は
知っていると言う事です、通常はそうだと思います。
Record型の変数のフィールド数だけ分かってもデータの種類の分からないと
意味が無いと思います。
下記はイメージです、実際に動作させていませんので注意を
して下さい。。
例えばバージョン1ではADataのタイプがTADataでバージョン2
ではTBDataなら読み込みのサイズをバージョンに合わせて変える
だけです。
type
TAData = Record
X:Integer;
Y:Integer;
end;
TBData = Record
X:Integer;
Y:Integer;
Z:Integer;
St1:string[8];
end;
var
varNo:integer;
//AData:array of TAData; //ver1
AData:array of TBData; //ver2
..
procedure Read_Data;
var F:TFileStream;
i:integer;
Ini:TIniFile;
IniFilename:string;
Path1:String;
count:integer;
begin
Path1:=ExtractFilePath(Application.ExeName);
Filename:=ExtractFilePath(Application.ExeName)+'\test01.dat';
if fileExists(Filename) then
begin
F:=TfileStream.Create(Filename,fmOpenRead);
try
F.Read(verNo,sizeof(verNo));
F.Read(Count,sizeof(Count));
Setlength(AData,Count +1)
i:=1;
while i <= Count do
begin
if verNo=1 then
F.Read(AData[i],sizeof(TAData))
else
F.Read(AData[i],sizeof(TBData));
inc(i);
end;
finally
F.Free;
end;
end
end;
F.Read(AData[i],sizeof(TAData)) ここにバグがあります。
ADataではなく読み込み用にCData:array of TAData; を用意して
追加したデータの初期化をしないといけませんでした。
そうですよね。
やっぱり、自分でフィールドを宣言しておいて、どうしてフィールド数を求める必要があるのか?ということですよね。
普通のプログラマーは、データ構造をきちんと頭に入れておくべきですね。
変な質問をしてしまったのに、詳しくご指導いただいて、ありがとうございました。
とても、参考になりました。
>自分でフィールドを宣言しておいて、
>どうしてフィールド数を求める必要があるのか?ということですよね。
>普通のプログラマーは、データ構造をきちんと頭に入れておくべきですね。
バージョンアップでフィールド数が増減することぐらい
よくあることですよ
その際にも前のデータを保存したファイルは読みたいので
ファイルの中のフィールドと実在するフィールドを
比較しながら取り出したりしてます。
この辺はライブラリ化してますので
自分の場合はあまり考えなくても使えますけどね
この質問の件で問題があるとしたら
レコード型ではなくてクラス型(TPersistent)で
データを保持したほうがいいんじゃないかな?
とは思いました。
1つの変数のために、1つのクラスを作って、ファイルの読み書きもそクラスで行うようにすれば、規模の大きい変数の管理も楽になりそうな感じがします。
ありがとうございました。
ツイート | ![]() |