Swanman's Horizon

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

カスタム属性でクラス定数っぽいものの振る舞いを継承先で変えたりしてみる。

事の発端

http://pc12.2ch.net/test/read.cgi/tech/1259667697/312-315n
312 :デフォルトの名無しさん:2010/02/26(金) 13:16:25
    TBase = class
    public
      class const Value: Integer; abstract;
    end;
    TBaseClass = class of TBase;

    TExtend1 = class(TBase)
    public
      class const Value: Integer = 1;
    end;

    TExtend2 = class(TBase)
    public
      class const Value: Integer = 2;
    end;

    var
      cls: TBaseClass;

    cls := TExtend1;
    a := cls.Value; //1が入って欲しい


    もちろんこんなコードは書けないんですけど、
    上記のようなクラス定数(もしくはクラス変数)の継承みたいなことって何かで代用できますかね

カスタム属性を使ってみる

type
  UserValueAttribute = class(TCustomAttribute) // ちなみにカスタム属性は××Attributeという名前にしておくと、
  public
    constructor Create(AValue: Integer);
    var Value: Integer; // 手抜き
  end;

  [UserValue(0)] // このようにAttributeを抜いた省略形が使える。C#っぽい
  TBase = class
  public
    class function Value: Integer; virtual;
  end;
  TBaseClass = class of TBase;

  [UserValue(1)]
  TExtend1 = class(TBase);

  [UserValue(2)]
  TExtend2 = class(TBase);

  TExtend3 = class(TBase);

constructor UserValueAttribute.Create(AValue: Integer);
begin
  Value := AValue;
end;

function GetCustomAttribute(BaseCls, AttrCls: TClass): TCustomAttribute;
var
  c: TRttiContext;
  t: TRttiType;
  a: TCustomAttribute;
begin
  while BaseCls <> nil do
  begin
    c := TRttiContext.Create;
    t := c.GetType(BaseCls);
    for a in t.GetAttributes do
      if a is AttrCls then Exit(a);
    BaseCls := BaseCls.ClassParent; // 属性が定義されてない時は親クラスの属性を調べることで属性の継承を実現
  end;
  Result := nil;
end;

class function TBase.Value: Integer;
begin
  Result := UserValueAttribute(GetCustomAttribute(Self, UserValueAttribute)).Value; // ここを含めエラー処理は全然してない
end;

var
  base: TBaseClass;
begin
  Writeln(TBase.Value);
  Writeln(TExtend1.Value);
  Writeln(TExtend2.Value);
  Writeln(TExtend3.Value);
  base := TExtend1;
  Writeln(base.Value);
  base := TExtend2;
  Writeln(base.Value);
  base := TExtend3;
  Writeln(base.Value);
end.

結果

0
1
2
0
1
2
0

カスタム属性を使う利点

  • 基底のクラスに実装を書いとけば、あとは継承先で属性定義するだけで振る舞いが変えられる
  • クラス参照型から参照してもちゃんと異なる値が返ってくる

とか?

カスタム属性って意外と使える

最近ちょうど似たようなことをやっていたので書いてみたけど、カスタム属性って最初知った時はシリアライズの際にどうの、みたいな漠然とした使い道しか知らなかったんだよね。プログラミングC#読んでも「使い方はこうだよ」みたいなことしか書いてなかったし。でもよくよく考えるとこうやって色んな属性を持たせられるというのはとても便利だなーと思った。

追記(10/03/01)

よく考えたらカスタム属性のインスタンスはTRttiContextが持ってるから、GetCustomAttributeを抜けた時点で解放されちゃってた…。となると、GetCustomAttributeに外部からTRttiContextを渡すか、無名メソッド渡して値を返してもらうかしないとなぁ。

// TRttiContextを渡すバージョン
function GetCustomAttrinute(const C: TRttiContext; BaseCls, AttrCls: TClass): TCustomAttribute;
var
  t: TRttiType;
  a: TCustomAttribute;
begin
  while BaseCls <> nil do
  begin
    t := c.GetType(BaseCls);
    for a in t.GetAttributes do
      if a is AttrCls then Exit(a);
    BaseCls := BaseCls.ClassParent;
  end;
  Result := nil;
end;

class function TBase.Value: Integer;
begin
  // 使うとき
  Result := UserValueAttribute(GetCustomAttribute(TRttiContext.Create, Self, UserValueAttribute)).Value;
end;

// 無名メソッドを渡すバージョン
type
  TAttributeProc = reference to procedure(Attr: TCustomAttribute);
procedure GetCustomAttrinute(BaseCls, AttrCls: TClass; const Proc: TAttributeProc);
var
  c: TRttiContext;
  t: TRttiType;
  a: TCustomAttribute;
begin
  c := TRttiContext.Create;
  while BaseCls <> nil do
  begin
    t := c.GetType(BaseCls);
    for a in t.GetAttributes do
      if a is AttrCls then
      begin
        Proc(a);
        Exit;
      end;
    BaseCls := BaseCls.ClassParent;
  end;
end;

class function TBase.Value: Integer;
begin
  // 使うとき
  GetCustomAttribute(Self, UserValueAttribute, 
    procedure(Attr: TCustomAttribute)
    begin
      Result := UserValueAttribute(Attr).Value;
    end);
end;

うーん、間に合わせで書いたせいか、どうも改善の余地しかない感じに…。後者は叙述関数渡せばカスタム属性のクラス参照いらねーな…まぁいいか。