Swanman's Horizon

性欲をもてあましつつなんらかの話をするよ。

カスタム属性と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の仕様から考えると、たぶん読み込みより保存の方が簡単に書ける