2018年5月19日土曜日

TokyoでDictionaryを要素にもつJson形式のファイルの読み書きをしてみた。

前回、LIST構造を持つJSON形式のファイルを読み書きしたので、今回はDictionary構造を持つファイルを読み書きしてみた。
(プロジェクトは、https://bitbucket.org/OldTPFun/delphitest/src/master/JsonTest/Proj2/ に配置してあります。)

今回読み書きするのは以下のようなファイル。

{"TownName":"木組みの家と石畳の街",
    "Shops":{
        "ラビットハウス":
            {"ShopName":"ラビットハウス",
                "Clerks":[
                    {"Name":"ココア","Age":17},
                    {"Name":"チノ","Age":15},
                    {"Name":"リゼ","Age":17}
                ]      
            },
        "甘兎庵":
            {"ShopName":"甘兎庵",
                "Clerks":[
                    {"Name":"チヤ","Age":17}
                ]
            }
    }
}


先ずは書き込みから。

今回は、Keyが文字列で、ValueがTCoffeeShop型のTDictionaryをシリアライズ・デシリアライズすればよいので、前回にならって、TDictionary用のコンバータ

  1. TCoffieShopDicConverter = class(TJsonDictionaryConverter<string, TCoffeeShop>);  

を作成して、上記のJSON文字列にシリアライズするためのクラスを作成して、TDictionary型の変数にTCoffieShopDicConverter属性を設定

  1. [JsonSerialize(TJsonMemberSerialization.&Public)]  
  2. TCofeeShopList = class  
  3. private  
  4.   FTownName: String;  
  5.   FShops: TObjectDictionary<string, TCoffeeShop>;  
  6.   procedure SetTownName(const Value: String);  
  7.   procedure SetShops(const Value: TObjectDictionary<string, TCoffeeShop>);  
  8.   public  
  9.     property TownName : String read FTownName write SetTownName;  
  10.     [JsonConverter(TCoffieShopDicConverter)]  
  11.     property Shops : TObjectDictionary<string, TCoffeeShop> read FShops write SetShops;  
  12.     constructor Create;  
  13. end;  

で、シリアライズするためのコード

  1. procedure TForm1.Button1Click(Sender: TObject);  
  2. var  
  3.   CoffeeShop1,CoffeeShop2 : TCoffeeShop;  
  4.   serializer: TJsonSerializer;  
  5.   CofeeShopList : TCofeeShopList;  
  6.   s : string;  
  7. begin  
  8.   CofeeShopList := TCofeeShopList.Create;  
  9.   CoffeeShop1 := TCoffeeShop.Create;  
  10.   CoffeeShop2 := TCoffeeShop.Create;  
  11.   try  
  12.     CoffeeShop1.ShopName := 'ラビットハウス';  
  13.     CoffeeShop1.Clerks.Add(TClerk.Create('ココア',17));  
  14.     CoffeeShop1.Clerks.Add(TClerk.Create('チノ',15));  
  15.     CoffeeShop1.Clerks.Add(TClerk.Create('リゼ',17));  
  16.   
  17.     CoffeeShop2.ShopName := '甘兎庵';  
  18.     CoffeeShop2.Clerks.Add(TClerk.Create('チヤ',17));  
  19.   
  20.     CofeeShopList.TownName := '木組みの家と石畳の街';  
  21.     CofeeShopList.Shops.Add(CoffeeShop1.ShopName,CoffeeShop1);  
  22.     CofeeShopList.Shops.Add(CoffeeShop2.ShopName,CoffeeShop2);  
  23.   
  24.     serializer := TJsonSerializer.Create;  
  25.     try  
  26.   
  27.       s := serializer.Serialize(CofeeShopList);  
  28.       Memo1.Text := s;  
  29.   
  30.       TFile.WriteAllText('CoffeeShop.Json',s);  
  31.   
  32.     finally  
  33.       serializer.Free;  
  34.     end;  
  35.   
  36.   finally  
  37.     CofeeShopList.Shops.Clear;  
  38.     CofeeShopList.Free;  
  39.   end;  
  40. end;  

を書いて実行すると、見事に成功と思いきや・・・・

例外クラスEAbstractError (メッセージ'抽象エラー')を送出しました。

と例外が発生。ありゃりゃ。

デバッグ実行した結果、PropertyToKeyの呼び出し時に例外が発生していることが分かったので、System.JSON.Converters.pasのTJsonDictionaryConverter<k,v>の定義を調べてみると、
  1. TJsonDictionaryConverter<k,v> = class(TJsonConverter)  
  2.   protected  
  3.     function CreateDictionary: TDictionary<k,v>; virtual;  
  4.     function PropertyToKey(const APropertyName: string): K; virtual; abstract;  
  5.     function KeyToProperty(const AKey: K): string; virtual; abstract;  
  6.     function ReadKey(const AReader: TJsonReader; const ASerializer: TJsonSerializer): string;  
  7.     function ReadValue(const AReader: TJsonReader; const ASerializer: TJsonSerializer): V; virtual;  
  8.     procedure WriteValue(const AWriter: TJsonWriter; const AValue: V; const ASerializer: TJsonSerializer); virtual;  
  9.   public  
  10.     procedure WriteJson(const AWriter: TJsonWriter; const AValue: TValue; const ASerializer: TJsonSerializer); override;  
  11.     function ReadJson(const AReader: TJsonReader; ATypeInf: PTypeInfo; const AExistingValue: TValue;  
  12.       const ASerializer: TJsonSerializer): TValue; override;  
  13.     function CanConvert(ATypeInf: PTypeInfo): Boolean; override;  
  14.   end;  

と、PropertyToKey,とKeyToPropertyにabstractがついているので、この2つは、継承先のコンバーターで実装しないといけなかったのね。

System.JSON.Converters.pasに、TJsonDictionaryConverterを継承した、キーが文字列のTJsonStringDictionaryConverter<V>の定義が あるので、中身をみてみると、

  1. TJsonStringDictionaryConverter<V> = class(TJsonDictionaryConverter<string, V>)  
  2. protected  
  3.   function PropertyToKey(const APropertyName: string): string; override;  
  4.   function KeyToProperty(const AKey: string): string; override;  
  5. end;  

とあって、実装が

  1. function TJsonStringDictionaryConverter<V>.KeyToProperty(const AKey: string): string;  
  2. begin  
  3.   Result := AKey;  
  4. end;  
  5.   
  6. function TJsonStringDictionaryConverter<V>.PropertyToKey(const APropertyName: string): string;  
  7. begin  
  8.   Result := APropertyName;  
  9. end;  

となっているので、Dictionaryのコンバーターを作成する時は、TJsonDictionaryConverterから、キーの型を決めた派生型を作成し、KeyToPropertyにはキーから文字列に変換、PropertyToKeyには文字列からキーの型のインスタンスへの変換を自前で実装すれば、良いわけですね。
例えば、キーが整数型のDictionaryのコンバータは

  1.   TJsonIntegerDictionaryConverter<V> = class(TJsonDictionaryConverter<Integer, V>)  
  2.   protected  
  3.     function PropertyToKey(const APropertyName: string): Integer; override;  
  4.     function KeyToProperty(const AKey: Integer): string; override;  
  5.   end;  
  6.   
  7. function TJsonStringDictionaryConverter<V>.KeyToProperty(const AKey: Integer): string;  
  8. begin  
  9.   Result := AKey.ToString;  
  10. end;  
  11.   
  12. function TJsonStringDictionaryConverter<V>.PropertyToKey(const APropertyName: string): Integer;  
  13. begin  
  14.   Result := APropertyName.ToInteger;  
  15. end;  

とすれば、良いわけだ。

今回は、定義済みの、TJsonStringDictionaryConverterを使用して先ほどの

  1. TCoffieShopDicConverter = class(TJsonDictionaryConverter<string, TCoffeeShop>);  


  1. TCoffieShopDicConverter = class(TJsonStringDictionaryConverter<TCoffeeShop>);  

に修正し実行すれば、目的のJSON形式の文字列のファイルが作成できます。

ファイルの読み込みは前回と同様、次のコードになります。(確認用に読み込んだものメモに列挙しております。)

  1. procedure TForm1.Button2Click(Sender: TObject);  
  2. var  
  3.   CoffeeShop : TCoffeeShop;  
  4.   CofeeShopList : TCofeeShopList;  
  5.   serializer: TJsonSerializer;  
  6.   s : string;  
  7.   Clerk : TClerk;  
  8. begin  
  9.   
  10.   s := TFile.ReadAllText('CoffeeShop.Json',TEncoding.UTF8);  
  11.   
  12.   serializer := TJsonSerializer.Create;  
  13.   try  
  14.     CofeeShopList := serializer.Deserialize<tcofeeshoplist>(s);  
  15.     try  
  16.       Memo1.Clear;  
  17.       Memo1.Lines.Add(CofeeShopList.TownName);  
  18.       for CoffeeShop in CofeeShopList.Shops.Values do  
  19.       begin  
  20.         Memo1.Lines.Add(CoffeeShop.ShopName);  
  21.         for Clerk in CoffeeShop.Clerks do  
  22.           begin  
  23.           Memo1.Lines.Add(Clerk.Name + '(' + Clerk.Age.ToString() + ')');  
  24.         end;  
  25.       end;  
  26.   
  27.     finally  
  28.       CoffeeShop.Clerks.Clear();  
  29.       CoffeeShop.Free;  
  30.     end;  
  31.   finally  
  32.     serializer.Free  
  33.   end;  

2018年5月3日木曜日

TokyoでJson形式のファイルの読み書きをして見た。

唐突ですが、Json.NET良いですよね。高度なこともできますが、Json形式の文字列のシリアル化/デシリアライズ化がだけで良いな簡単にできますしね。
Delphi(Tokyo)でJson形式のファイルを読み込む必要が出てきたので、Json.NETのような書き方ができなか調べていたところもう1年くらい前の記事になりますが、

@lynatan さんのTJsonSerializerの記事

TJsonSerializerの使い方。
TJsonSerializerの実用例

エンバカデロさんのブログ

TJsonSerializerでJSONに変換する[JAPAN]

DelphiでもTokyoになって、Json.NETのような書き方ができるようになっているとのことでしたので、試して見ました。

読み書きするのは、以下のような内容のファイル

{"ShopName":"ラビットハウス",
  "Clerks":[
    {"Name":"ココア","Age":17},
    {"Name":"チノ","Age":15},
    {"Name":"リゼ","Age":17}
  ]
}

上記の構造にマップできるクラスを作成します。
先ずは、従業員(Clerk)クラス。
パブリックメンバーをシリアライズ対象のするのでクラスに[JsonSerialize(TJsonMemberSerialization.&Public)]
属性を付加しています。
(余談ですが、予約語、指令と被るワードを使う場合、その前に"&"が必要です。)

  1. unit Unit1;  
  2.   //パブリックメンバーをシリアライズ対象にする属性  
  3.   [JsonSerialize(TJsonMemberSerialization.&Public)]  
  4.   TClerk = class  
  5.     private  
  6.       FName: string;  
  7.       FAge: integer;  
  8.   
  9.       procedure SetAge(const Value: integer);  
  10.       procedure SetName(const Value: string);  
  11.     public  
  12.       property Name : string read FName write SetName;  
  13.       property Age : integer read FAge write SetAge;  
  14.       constructor Create; overload;  
  15.       constructor Create(const vName : stringconst vAge : integer); overload;  
  16.   end;  

次に喫茶店(CoffeeShop)クラスの定義
  1. [JsonSerialize(TJsonMemberSerialization.&Public)]  
  2. TCoffeeShop = class  
  3.   private  
  4.   FClerks: TList<TClerk>;  
  5.   FShopName: string;  
  6.   procedure SetClerks(const Value: TList<TClerk>);  
  7.   procedure SetShopName(const Value: string);  
  8.   public  
  9.     property ShopName : string read FShopName write SetShopName;  
  10.       
  11.     //TClerkクラスのジェネリックリスト用のコンバーターを登録  
  12.     [JsonConverter(TJsonClerkListConverter)]  
  13.     property Clerks : TList<TClerk> read FClerks write SetClerks;  
  14.   
  15.     public constructor Create;  
  16. end;  

メンバーは、喫茶店名と、従業員のリスト(ジェネリックのリスト)です。
こちらも、パブリックメンバーをシリアル化の対象とします。
ジェネリックのリストは、そのままではシリアライズできないので、Json.Converterユニットに定義済みの TJsonListConverterからTClerk型用の派生クラス

  1. //TClerk型のリスト用のコンバーター  
  2. TJsonClerkListConverter = class(TJsonListConverter<TClerk>);  

を作成し、TClerk型のジェネリックリスト型のメンバーClerks用のコンバーター属性を付加しています。

クラス定義の全体は、以下とおりです。
  1. unit Unit2;  
  2.   
  3. interface  
  4. uses  
  5.   //Json.SerializersとConverterを使用する。  
  6.     System.JSON.Serializers  
  7.   , System.JSON.Converters  
  8.   //TList<t>を使用する  
  9.   , System.Generics.Collections  
  10.   ;  
  11.   
  12. type  
  13.   
  14.   //パブリックメンバーをシリアライズ対象にする属性  
  15.   [JsonSerialize(TJsonMemberSerialization.&Public)]  
  16.   TClerk = class  
  17.     private  
  18.       FName: string;  
  19.       FAge: integer;  
  20.   
  21.       procedure SetAge(const Value: integer);  
  22.       procedure SetName(const Value: string);  
  23.     public  
  24.       property Name : string read FName write SetName;  
  25.       property Age : integer read FAge write SetAge;  
  26.       constructor Create; overload;  
  27.       constructor Create(const vName : stringconst vAge : integer); overload;  
  28.   end;  
  29.   
  30.   //TClerk型のリスト用のコンバーター  
  31.   TJsonClerkListConverter = class(TJsonListConverter<TClerk>;);  
  32.   
  33.   [JsonSerialize(TJsonMemberSerialization.&Public)]  
  34.   TCoffeeShop = class  
  35.     private  
  36.     FClerks: TList<TClerk>;  
  37.     FShopName: string;  
  38.     procedure SetClerks(const Value: TList<TClerk>);  
  39.     procedure SetShopName(const Value: string);  
  40.     public  
  41.       property ShopName : string read FShopName write SetShopName;  
  42.         
  43.       //TClerkクラスのジェネリックリスト用のコンバーターを登録  
  44.       [JsonConverter(TJsonClerkListConverter)]  
  45.       property Clerks : TList<TClerk> read FClerks write SetClerks;  
  46.   
  47.       public constructor Create;  
  48.   end;  
  49. implementation  
  50.   
  51. { TClerk }  
  52.   
  53. constructor TClerk.Create(const vName: stringconst vAge: integer);  
  54. begin  
  55.    FName := vName;  
  56.    FAge := vAge;  
  57. end;  
  58.   
  59. constructor TClerk.Create;  
  60. begin  
  61.   
  62. end;  
  63.   
  64. procedure TClerk.SetAge(const Value: integer);  
  65. begin  
  66.   FAge := Value;  
  67. end;  
  68.   
  69. procedure TClerk.SetName(const Value: string);  
  70. begin  
  71.   FName := Value;  
  72. end;  
  73.   
  74. { TCoffeeShop }  
  75.   
  76. constructor TCoffeeShop.Create;  
  77. begin  
  78.   FClerks := TList<TClerk>.Create;  
  79. end;  
  80.   
  81. procedure TCoffeeShop.SetClerks(const Value: TList<TClerk>);  
  82. begin  
  83.   FClerks := Value;  
  84. end;  
  85.   
  86. procedure TCoffeeShop.SetShopName(const Value: string);  
  87. begin  
  88.   FShopName := Value;  
  89. end;  
  90.   
  91. end.  

上記で定義したクラスに対で、冒頭で示した内容のJson形式の定義ファイルCoffeeShop.Jsonを読み込み、デシリアライズする処理は、以下のとおりとなります。

  1. procedure TForm1.Button2Click(Sender: TObject);  
  2. var  
  3.   CoffeeShop : TCoffeeShop;  
  4.   serializer: TJsonSerializer;  
  5.   s : string;  
  6.   Clerk : TClerk;  
  7. begin  
  8.   
  9.   s := TFile.ReadAllText('CoffeeShop.Json',TEncoding.UTF8);  
  10.   
  11.   serializer := TJsonSerializer.Create;  
  12.   try  
  13.     CoffeeShop := serializer.Deserialize<TCoffeeShop>(s);  
  14.     try  
  15.       Memo1.Clear;  
  16.       Memo1.Lines.Add(CoffeeShop.ShopName);  
  17.       for Clerk in CoffeeShop.Clerks do  
  18.       begin  
  19.         Memo1.Lines.Add(Clerk.Name + '(' + Clerk.Age.ToString() + ')');  
  20.       end;  
  21.     finally  
  22.       CoffeeShop.Clerks.Clear();  
  23.       CoffeeShop.Free;  
  24.     end;  
  25.   finally  
  26.     serializer.Free  
  27.   end;  

ファイルを読み込み、シリアライザーを生成し、読み込んだJson文字列をデシリアライズして、ショップ名と、店員の情報をメモに表示しています。 (余談ですが、クラス定義で属性を使用しないで、TJsonSerializerインスタンスのConverterリストにTJsonClerkListConverterのインスタンスを登録してもデシリアライズできます。)

冒頭で示した内容のJson形式の定義ファイルを作成する場合は、以下の処理でできます。

  1. procedure TForm1.Button1Click(Sender: TObject);  
  2. var  
  3.   CoffeeShop : TCoffeeShop;  
  4.   serializer: TJsonSerializer;  
  5.   s : string;  
  6.   
  7.   
  8. begin  
  9.   CoffeeShop := TCoffeeShop.Create;  
  10.   try  
  11.     CoffeeShop.ShopName := 'ラビットハウス';  
  12.     CoffeeShop.Clerks.Add(TClerk.Create('ココア',17));  
  13.     CoffeeShop.Clerks.Add(TClerk.Create('チノ',15));  
  14.     CoffeeShop.Clerks.Add(TClerk.Create('リゼ',17));  
  15.   
  16.     serializer := TJsonSerializer.Create;  
  17.     try  
  18.   
  19.       s := serializer.Serialize(CoffeeShop);  
  20.       Memo1.Text := s;  
  21.   
  22.       TFile.WriteAllText('CoffeeShop.Json',s);  
  23.   
  24.     finally  
  25.       serializer.Free;  
  26.     end;  
  27.   
  28.   finally  
  29.     CoffeeShop.Clerks.Clear();  
  30.     CoffeeShop.Free;  
  31.   end;  
  32. end;  

TCoffeeShop型のインスタンスを生成し、メンバーを設定後、シリアライザーを生成しシリアライズ後、ファイルに保存しています。
(確認の為、画面上のメモにも表示しています。)


プロジェクト一式は、https://bitbucket.org/OldTPFun/delphitest/src/master/JsonTest/Proj1/
に配置しております。

2014年1月25日土曜日

GExpertsのMove to Matching Delimiter 機能

数日前、@_fm2さんが、ツイッターで

"DelphiのIDEで  begin 対する end を検索する、ショートカットなんぞはありませんか?"

とのつぶやかれてまた。

これ、IDEの標準機能では、出来なさそうだけど、GExperts のEditor Exports機能で
できます。

GExperts自体は、有名なツールなので、ご存知の方も多いと思います。

先ずは、GExpertsのインストール。

XE4までは、GExperts のサイトにインストーラがあるので、インストラーを使って
セットアップできます。

XE5は、残念ながら、インストーラがないので手動インストールが必要です。
インストール方法は、以下のとおりです。(Delphi XE5が必要です。)

1) SourceForgeのリポジトリからソースを入手します。

2) XE5用のプロジェクトを開きビルドします。
    ビルドすると、GExpertsRSXE5.dllができます。

3) レジストリ登録を行います。
 レジストリエディターで、

 HKEY_CURRENT_USER\Software\Embarcadero\BDS\12.0\Experts\

 に、文字列キーGExpertsを作成し、値にGExpertsRSXE5.dllをフルパスで
 設定します。

インストールに成功すれば、Delphi ( RadStudio )を起動するとメニューに
GExpertsが表示されます。

この状態で、Delphiのソースコードを開き"begin"のところにカーソルをあてて
[CTRL] + [ALT] + [右矢印キー]を押すと、対応する"end"に移動します。
この位置で、もう一回[CTRL] + [ALT] + [右矢印キー]を押すと、"begin"に
戻ります。(このショートカットキーは変更可能です。)

GExpertsのメニューから、Configurationを選択し、設定用のダイアログを開き、
Editor Expertsタブを選択し、Move to Matching Delimiterのところにカーソルを
あてると、機能の説明が確認できます。
説明には、

"  This expert enables you to quickly move to a matching beginning/ending delimiter for the following Delphi tokens: begin, try, case, repeat, for, with, while, asm, do, if, then, else, class, record, array, interface, implementation, uses, private, protected, public, published, until, end, try, finally, except, (/), and [/], .
  It also supports the following C++ tokens: {/}, (/), and [/]
The following steps are taken to match delimiters:
 - Beginning at the current cursor position, the expert looks to the left on the current line for a delimiter token such as "begin".
 - If a delimiter token is found, the expert scans for the matching token, such as "end" in the above instance.
 - If no delimiter is found to the left, as in the case "if|(i=0)" (where '|' represents the cursor), the expert attempts to find a delimiter token to the right of the current cursor position. In the mentioned example, this will identify the opening parenthesis, and an attempt is made to locate the closing parenthesis. "

とありますので、指定したトークンとペアになるトークンがあれば、その位置に
移動するようです。

試しに、try 〜 finally 〜 end で試した場合、finallyの位置でこの機能を使用した
場合の移動先は状況に依存するようでした。
(tryからはfinallyに、endからはfinallyにそれぞれ移動します。)

ソースも公開されてますので、GX_eFindDelimiter.pas等を書き換えれば
動きのカスタマイズも可能かと思います。










2012年12月13日木曜日

publishedなメソッドを隠す(ネストした型宣言使用編)

前回の続きです。

さて、最近のDelphiでは、ネストした型宣言が可能で、クラス宣言の中にクラスの宣言ができます。

この機能を使って、クラスの使用者からpublishedなメソッドを隠すことを試してみました。

以下、ソースです。

先ず、 【関数名でメソッドが呼び出されるクラスのサンプル】

  1. unit Unit2;  
  2.   
  3. interface  
  4.   
  5.   type TMyCallCalc = class  
  6.     public  
  7.       function CallCalc(CalcName : string; a, b: double) : double;  
  8.     private  
  9.       type TMyCalc = class  
  10.         published  
  11.           function Add(a,b : double) : double;  
  12.           function Subtract(a,b : double) : double;  
  13.       end;  
  14.   
  15.   end;  
  16.   
  17.   
  18. implementation  
  19. { TMyCalc }  
  20.   
  21. function TMyCallCalc.TMyCalc.Add(a, b: double): double;  
  22. begin  
  23.   Result := a + b;  
  24. end;  
  25.   
  26. function TMyCallCalc.TMyCalc.Subtract(a, b: double): double;  
  27. begin  
  28.   Result := a - b;  
  29. end;  
  30.   
  31.   
  32. type TMyCalcFunc = function(a,b : double) : double of object;  
  33.   
  34. function TMyCallCalc.CallCalc(CalcName : string; a, b: double) : double;  
  35. var  
  36.   MyCalc     : TMyCalc;  
  37.   MyCalcFunc : TMyCalcFunc;  
  38.   MethodVar : TMethod;  
  39. begin  
  40.   
  41.   MyCalc := TMyCalc.Create;  
  42.   try  
  43.     MethodVar.Data := MyCalc;  
  44.     MethodVar.Code := MyCalc.MethodAddress(CalcName);  
  45.   
  46.   
  47.     if Assigned(MethodVar.Code) then  
  48.     begin  
  49.       MyCalcFunc := TMyCalcFunc(MethodVar);  
  50.       Result := MyCalcFunc(a,b);  
  51.     end;  
  52.   finally  
  53.     MyCalc.Free;  
  54.   end;  
  55.   
  56. end;  
  57.   
  58. end.  

親クラスのprivateセクションにpublishedなメソッドを持つ子クラスを宣言しています。
これで、ユニットの使用者からは、子クラスのメソッドの宣言が見えなくなり、直接呼び出す
ことができなくなります。
ユニットの使用者には、publicセクションにメソッドを宣言することで、間接的に目的の
メソッドが呼び出せるようにします。

次に上記のクラスを使用するコード

  1. unit Unit1;  
  2.   
  3. interface  
  4.   
  5. uses  
  6.   Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,  
  7.   Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls;  
  8.   
  9. type  
  10.   TForm1 = class(TForm)  
  11.     Button1: TButton;  
  12.     Button2: TButton;  
  13.     LabeledEdit1: TLabeledEdit;  
  14.     LabeledEdit2: TLabeledEdit;  
  15.     StaticText1: TStaticText;  
  16.     StaticText2: TStaticText;  
  17.     procedure OnCalcBtnClick(Sender: TObject);  
  18.   private  
  19.     { Private 宣言 }  
  20.   public  
  21.     { Public 宣言 }  
  22.   end;  
  23.   
  24. var  
  25.   Form1: TForm1;  
  26.   
  27. implementation  
  28.   
  29. {$R *.dfm}  
  30.   
  31. uses Unit2;  
  32.   
  33. procedure TForm1.OnCalcBtnClick(Sender: TObject);  
  34. var  
  35.   CallCalc : TMyCallCalc;  
  36. begin  
  37.   CallCalc := TMyCallCalc.Create;  
  38.   try  
  39.     StaticText2.Caption := FloatToStr(CallCalc.CallCalc(  
  40.                                     TButton(Sender).Caption,  
  41.                                     StrToFloat(LabeledEdit1.Text),  
  42.                                     StrToFloat(LabeledEdit2.Text)));  
  43.   finally  
  44.     CallCalc.Free;  
  45.   end;  
  46.   
  47. end;  
  48.   
  49. end.  

ユニットを使用する側からは子クラスが見えませんので、親クラスのpublicなメソッドのみが
使用できます。



publishedなメソッドを隠す

前回の続きです。

TObjectのMethodAddressメソッドを使用すれば関数名(文字列)でメソッドをコールできます。

しかし、 MethodAddressメソッドでメソッドのアドレスを取得するには、可視性をpublishedに
する必要があり、 publishedにした瞬間にメソッドの存在が丸わかりになってしまいます。

前回の例のような単純な演算であればまあ良いかと思うのですが、いわゆるFactoryメソッド
とかだと、使用者にその存在を隠したい場合があり、このままではちょっと不完全です。

さて、どうしようか?というのが今回のテーマになります。(2010以降の拡張Rttiを使えば良い
というのはあるのですが、今回は別の方法を考えます。)

ところで、DelphiのUnitのimplementationセクションで定義した型(クラスを含む)は別のユニット
から参照できない仕様となっています。

したがって、クラスの定義を  implementation で行えば、外部から処理の存在を隠したうえで
 MethodAddressメソッド が使用可能なクラスができそうです。

で、実際に、作ってみました。

先ずは、【メソッドポインタ経由で呼び出されるクラスのサンプル】
  1. unit Unit2;  
  2.   
  3. interface  
  4.   
  5.   function CallCalc(CalcName : string; a, b: double) : double;  
  6.   
  7. implementation  
  8.   
  9.   type TMyCalc = class  
  10.   published  
  11.     function Add(a,b : double) : double;  
  12.     function Subtract(a,b : double) : double;  
  13.   end;  
  14.   
  15.   type TMyCalcFunc = function(a,b : double) : double of object;  
  16.   
  17. { TMyCalc }  
  18.   
  19. function TMyCalc.Add(a, b: double): double;  
  20. begin  
  21.   Result := a + b;  
  22. end;  
  23.   
  24. function TMyCalc.Subtract(a, b: double): double;  
  25. begin  
  26.   Result := a - b;  
  27. end;  
  28.   
  29.   
  30. function CallCalc(CalcName : string; a, b: double) : double;  
  31. var  
  32.   MyCalc     : TMyCalc;  
  33.   MyCalcFunc : TMyCalcFunc;  
  34.   MethodVar : TMethod;  
  35. begin  
  36.   
  37.   MyCalc := TMyCalc.Create;  
  38.   try  
  39.     MethodVar.Data := MyCalc;  
  40.     MethodVar.Code := MyCalc.MethodAddress(CalcName);  
  41.   
  42.   
  43.     if Assigned(MethodVar.Code) then  
  44.     begin  
  45.       MyCalcFunc := TMyCalcFunc(MethodVar);  
  46.       Result := MyCalcFunc(a,b);  
  47.     end;  
  48.   finally  
  49.     MyCalc.Free;  
  50.   end;  
  51.   
  52. end;  
  53.   
  54. end.  

publishedの可視性を持つクラスをimplementationセクションに定義しています。
但し、そのままでは、外部のユニットからメソッドをコールできませんので、
外部からのアクセスようにラッパー関数を定義しています。

次に、【関数名を文字列で指定して処理を呼び出すサンプル】

  1. interface  
  2.   
  3. uses  
  4.   Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,  
  5.   Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls;  
  6.   
  7. type  
  8.   TForm1 = class(TForm)  
  9.     Button1: TButton;  
  10.     Button2: TButton;  
  11.     LabeledEdit1: TLabeledEdit;  
  12.     LabeledEdit2: TLabeledEdit;  
  13.     StaticText1: TStaticText;  
  14.     StaticText2: TStaticText;  
  15.     procedure OnCalcBtnClick(Sender: TObject);  
  16.   private  
  17.     { Private 宣言 }  
  18.   public  
  19.     { Public 宣言 }  
  20.   end;  
  21.   
  22. var  
  23.   Form1: TForm1;  
  24.   
  25. implementation  
  26.   
  27. {$R *.dfm}  
  28.   
  29. uses Unit2;  
  30.   
  31. procedure TForm1.OnCalcBtnClick(Sender: TObject);  
  32. begin  
  33.   StaticText2.Caption := FloatToStr(CallCalc(  
  34.                                     TButton(Sender).Caption,  
  35.                                     StrToFloat(LabeledEdit1.Text),  
  36.                                     StrToFloat(LabeledEdit2.Text)));  
  37. end;  
  38.   
  39. end.  

unit2のinterfaceセクションに定義したラッパーを使って処理を呼び出しています。


implementationセクションにクラスを定義することで、外部からその存在を隠しつつ
メソッド名を文字列で指定してメソッドをコールすることが可能です。

但し、この方法ですとクラスそのものも隠れてしまいすのでもう少しなんとかしたい
ところです。

ネストした型宣言が可能なバージョンのDelphiであれば、何とかできそうな気が
しますが、その検証はまた後日・・・

2012年12月12日水曜日

メソッドを名前で呼び出す

前回の続きです。

関数(function)のメソッドポインタが定義できれば、TObjectのMethodAddressを使って
関数名を(文字で)指定してコールすることが可能です。

以下、サンプルソース。

先ずは、【メソッドポインタ経由で呼び出されるクラスのサンプル】

  1. unit Unit2;  
  2.   
  3. interface  
  4.   
  5.   type TMyCalc = class  
  6.   published  
  7.     function Add(a,b : double) : double;  
  8.     function Subtract(a,b : double) : double;  
  9.   end;  
  10.   
  11. implementation  
  12.   
  13. { TMyCalc }  
  14.   
  15. function TMyCalc.Add(a, b: double): double;  
  16. begin  
  17.   Result := a + b;  
  18. end;  
  19.   
  20. function TMyCalc.Subtract(a, b: double): double;  
  21. begin  
  22.   Result := a - b;  
  23. end;  
  24.   
  25. end.  

MethodAddressを使用するためにpublishedを指定してることに注意願います。

次に、【関数名を文字列で指定して処理を呼び出すサンプル】

  1. unit Unit1;  
  2.   
  3. interface  
  4.   
  5. uses  
  6.   Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,  
  7.   Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls;  
  8.   
  9. type  
  10.   TForm1 = class(TForm)  
  11.     Button1: TButton;  
  12.     Button2: TButton;  
  13.     LabeledEdit1: TLabeledEdit;  
  14.     LabeledEdit2: TLabeledEdit;  
  15.     StaticText1: TStaticText;  
  16.     StaticText2: TStaticText;  
  17.     procedure OnCalcBtnClick(Sender: TObject);  
  18.   private  
  19.     { Private 宣言 }  
  20.   public  
  21.     { Public 宣言 }  
  22.   end;  
  23.   
  24. var  
  25.   Form1: TForm1;  
  26.   
  27. implementation  
  28.   
  29. {$R *.dfm}  
  30.   
  31. uses Unit2;  
  32.   
  33. type TMyCalcFunc = function(a,b : double) : double of object;  
  34.   
  35. procedure TForm1.OnCalcBtnClick(Sender: TObject);  
  36. var  
  37.   MyCalc     : TMyCalc;  
  38.   MyCalcFunc : TMyCalcFunc;  
  39.   MethodVar : TMethod;  
  40. begin  
  41.   
  42.   MyCalc := TMyCalc.Create;  
  43.   try  
  44.     MethodVar.Data := MyCalc;  
  45.     MethodVar.Code := MyCalc.MethodAddress(TButton(Sender).Caption);  
  46.   
  47.   
  48.     if Assigned(MethodVar.Code) then  
  49.     begin  
  50.       MyCalcFunc := TMyCalcFunc(MethodVar);  
  51.       StaticText2.Caption := FloatToStr(MyCalcFunc(StrToFloat(LabeledEdit1.Text),StrToFloat(LabeledEdit2.Text)));  
  52.     end;  
  53.   finally  
  54.     MyCalc.Free;  
  55.   end;  
  56.   
  57. end;  
  58.   
  59. end.  

ボタンのキャプション名が名前のメソッドが定義されているという前提で、ボタンのキャプションを
使って関数をコールして、結果を求めています。


関数のメソッドポインタ

メソッドポインタでfunctionを使った例のサンプルです。
あまり見当たらなかったので、備忘録がわりに公開しときます。


【メソッドポインタ経由でfunctionを呼び出すサンプル】

  1. unit Unit1;  
  2.   
  3. interface  
  4.   
  5. uses  
  6.   Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,  
  7.   Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls;  
  8.   
  9. type  
  10.   TForm1 = class(TForm)  
  11.     Button1: TButton;  
  12.     Button2: TButton;  
  13.     LabeledEdit1: TLabeledEdit;  
  14.     LabeledEdit2: TLabeledEdit;  
  15.     StaticText1: TStaticText;  
  16.     StaticText2: TStaticText;  
  17.     procedure OnCalcBtnClick(Sender: TObject);  
  18.   private  
  19.     { Private 宣言 }  
  20.   public  
  21.     { Public 宣言 }  
  22.   end;  
  23.   
  24. var  
  25.   Form1: TForm1;  
  26.   
  27. implementation  
  28.   
  29. {$R *.dfm}  
  30.   
  31. uses Unit2;  
  32.   
  33. type TMyCalcFunc = function(a,b : double) : double of object;  
  34.   
  35. procedure TForm1.OnCalcBtnClick(Sender: TObject);  
  36. var  
  37.   MyCalc     : TMyCalc;  
  38.   MyCalcFunc : TMyCalcFunc;  
  39.   Method: TMethod;  
  40. begin  
  41.   
  42.   MyCalc := TMyCalc.Create;  
  43.   try  
  44.     //Method.Data := MyCalc;  
  45.     if TButton(Sender).Caption = 'Add' then  
  46.     begin  
  47.       MyCalcFunc := MyCalc.Add;  
  48.     end  
  49.     else  
  50.     begin  
  51.       MyCalcFunc := MyCalc.Subtract;  
  52.     end;  
  53.     StaticText2.Caption := FloatToStr(MyCalcFunc(StrToFloat(LabeledEdit1.Text),StrToFloat(LabeledEdit2.Text)));  
  54.   finally  
  55.     MyCalc.Free;  
  56.   end;  
  57.   
  58. end;  
  59.   
  60. end.  


【メソッドポインタ経由で呼び出されるクラスのサンプル】

  1. unit Unit2;  
  2.   
  3. interface  
  4.   
  5.   type TMyCalc = class  
  6.     function Add(a,b : double) : double;  
  7.     function Subtract(a,b : double) : double;  
  8.   end;  
  9.   
  10. implementation  
  11.   
  12. { TMyCalc }  
  13.   
  14. function TMyCalc.Add(a, b: double): double;  
  15. begin  
  16.   Result := a + b;  
  17. end;  
  18.   
  19. function TMyCalc.Subtract(a, b: double): double;  
  20. begin  
  21.   Result := a - b;  
  22. end;  
  23.   
  24. end.  

メソッドポインタはヘルプにもあるように、

Type 型名 = "メソッドの定義" of object

のように宣言します。

ここで、"メソッドの定義"は、通常の手続き(関数)の名前を抜いたものになるので、
例えば、TObject型のSenderを引数に持ち、戻り値がない"メソッドの定義"は、

procedure(Sender : TObject)

同様に、Double型のaとbを引数に持ち、Double型の戻り値がある"メソッドの定義"は

function(a,b : Double) : Double

となります。

上記のサンプル1では、ボタンのキャプションに応じてif分で関数を切り替えています。

この例では、関数が2つだけなので、メソッドポインタ経由ではなく、直接目的の関数を
コールしたほうが良いのですが、
同じ型の引数を持つ関数が多数ある場合、メソッドポインタと、TObject.MethodAddressを
組み合わるとコード量を減らせる可能性があります。(上の例だとボタンのCaptionと関数名を
合わせおくことによりif文(あるいは、Case文)を無くすことが可能です。)

そのへんの話はまた後日・・・