カスタム属性とRTTIを使ったCSVファイルのロード@Delphi2010。
1月1エントリ
順調に達成してるな…!と思ったらすでに4月に破綻してた。
そんなことよりカスタム属性やろうぜ!
先日カスタム属性とRTTIを利用したCSV読み込みクラスを作る機会があったので、それらの利用サンプルとして多少汎用化しつつ公開してみる。C#はいくらでもサンプルあるけどDelphiだと手軽なサンプルあんまり無いしね。
ちなみにCSVの読み込み部分は手抜きのためTStringListを利用してるので、すごくRFC4180準拠じゃない。あとエラー処理はわりと省きまくった。
まず実装
unit Lyna.CSVLoader; interface uses SysUtils, Classes, Generics.Collections, TypInfo, Rtti, Lyna.Generics; type CSVColumnAttribute = class(TCustomAttribute) private FColumn: Integer; public constructor Create(AColumn: Integer); property Column: Integer read FColumn; end; TCSVLoader<T: record> = class(TList<T>) public procedure LoadFromFile(const AFileName: string; AEncoding: TEncoding = nil); end; implementation { CSVColumnAttribute } constructor CSVColumnAttribute.Create(AColumn: Integer); begin FColumn := AColumn; end; { TCSVLoader<T> } procedure TCSVLoader<T>.LoadFromFile(const AFileName: string; AEncoding: TEncoding = nil); var ctx: TRttiContext; i, col, row: Integer; fields: TArray<TRttiField>; columns: TArray<Integer>; sl, dl: TFunc<TStringList>; item: T; value: TValue; s: string; begin // 属性をループ毎に取得してたら糞重いのでキャッシュしとく ctx := TRttiContext.Create; fields := ctx.GetType(TypeInfo(T)).GetFields; SetLength(columns, Length(fields)); for i := Low(fields) to High(fields) do begin columns[i] := TFunc<TArray<TCustomAttribute>,Integer>( function(Attributes: TArray<TCustomAttribute>): Integer var attr: TCustomAttribute; begin for attr in Attributes do if attr is CSVColumnAttribute then Exit(CSVColumnAttribute(attr).Column); Result := -1; end)(fields[i].GetAttributes); end; // try-finallyでもいいんだけどここは手抜きで sl := TSmartPointer<TStringList>.Create(TStringList.Create); dl := TSmartPointer<TStringList>.Create(TStringList.Create); dl.StrictDelimiter := True; sl.LoadFromFile(AFileName, AEncoding); for row := 0 to sl.Count-1 do begin if sl[row] = '' then Continue; dl.CommaText := sl[row]; item := Default(T); for col := Low(fields) to High(fields) do begin if columns[col] = -1 then Continue; try s := dl[columns[col]]; case fields[col].FieldType.TypeKind of tkWChar, tkLString, tkWString, tkString, tkChar, tkUString: value := s; tkInteger, tkInt64: value := StrToInt(s); tkFloat: value := StrToFloat(s); tkEnumeration: value := TValue.FromOrdinal(fields[col].FieldType.Handle, GetEnumValue(fields[col].FieldType.Handle, s)); else Continue; // 単純型以外は放置で>< end; fields[col].SetValue(@item, value); except raise Exception.CreateFmt('col:%d row:%d で何かあったよ的な', [col, row]); end; end; Add(item); end; end; end.
使用例
program Project1; {$APPTYPE CONSOLE} uses TypInfo, Lyna.CSVLoader; type THogeFlag = (Kurara, Ga, Tatta); THogeRec = record // 前にも書いたけどxxxxAttributeというカスタム属性はAttribute部分を省ける [CSVColumn(0)] ID: Integer; [CSVColumn(1)] Name: string; [CSVColumn(3)] Flags: THogeFlag; end; var loader: TCSVLoader<THogeRec>; hoge: THogeRec; begin loader := TCSVLoader<THogeRec>.Create; loader.LoadFromFile('hoge.txt'); for hoge in loader do Writeln(hoge.ID, ' ', hoge.Name, ' ', GetEnumName(TypeInfo(THogeFlag), Integer(hoge.Flags))); loader.Free; Readln; end.
結果
// hoge.txtの内容 1,foo,123,Kurara 2,bar,456,Ga 3,baz,789,Tatta // 出力結果 1 foo Kurara 2 bar Ga 3 baz Tatta
で、何がいいの?
実装部分はそこそこ酷い感じになってるけど、使用がとても簡単なところ。recordの定義や変数の宣言を抜いたらたったの3行で読めちゃうし。SaveToFileメソッドを実装*1すれば保存もできるしね。当たり前だけど。
反省など
CSVLoaderなんて名乗るくらいならTArray
むしゃくしゃしてやった。
ところで某ブログはどこまで本気なのか誰か教えて下さい。嘘を嘘と見抜く伝説の掲示板力を持たない僕にはよく分かんないです><
参考
どう見てもオモシロ外人にしか見えない写真が素敵なロブ先生のブログの、
このへんとか
http://robstechcorner.blogspot.com/2009/10/xml-serialization-basic-usage.html
このへん。
http://robstechcorner.blogspot.com/2009/10/xml-serialization-control-via.html
*1:微妙に使い勝手の悪いTValueの仕様から考えると、たぶん読み込みより保存の方が簡単に書ける