クラス参照を使用してオブジェクトのインスタンスを作成し、コンストラクターが確実に実行されるようにするにはどうすればよいですか?
このコード例では、TMyClassのコンストラクターは呼び出されません。
type
TMyClass = class(TObject)
MyStrings: TStrings;
constructor Create; virtual;
end;
constructor TMyClass.Create;
begin
MyStrings := TStringList.Create;
end;
procedure Test;
var
Clazz: TClass;
Instance: TObject;
begin
Clazz := TMyClass;
Instance := Clazz.Create;
end;
これを使って:
type
TMyClass = class(TObject)
MyStrings: TStrings;
constructor Create; virtual;
end;
TMyClassClass = class of TMyClass; // <- add this definition
constructor TMyClass.Create;
begin
MyStrings := TStringList.Create;
end;
procedure Test;
var
Clazz: TMyClassClass; // <- change TClass to TMyClassClass
Instance: TObject;
begin
Clazz := TMyClass; // <- you can use TMyClass or any of its child classes.
Instance := Clazz.Create; // <- virtual constructor will be used
end;
または、(「TMyClassのクラス」の代わりに)TMyClassへの型キャストを使用することもできます。
アレクサンダーの解決策は素晴らしいものですが、特定の状況では十分ではありません。実行時にTClass参照を格納し、後で任意の数のインスタンスを取得できるTClassFactoryクラスを設定するとします。
このようなクラスファクトリは、保持しているクラスの実際のタイプについて何も知らないため、それらを対応するメタクラスにキャストすることはできません。このような場合に正しいコンストラクターを呼び出すには、次のアプローチが機能します。
まず、簡単なデモクラスが必要です(パブリックフィールドは気にしないでください。デモンストレーション用です)。
interface
uses
RTTI;
type
THuman = class(TObject)
public
Name: string;
Age: Integer;
constructor Create(); virtual;
end;
implementation
constructor THuman.Create();
begin
Name:= 'John Doe';
Age:= -1;
end;
ここで、純粋にRTTIによって、正しいコンストラクター呼び出しを使用して、THuman型のオブジェクトをインスタンス化します。
procedure CreateInstance();
var
someclass: TClass;
c: TRttiContext;
t: TRttiType;
v: TValue;
human1, human2, human3: THuman;
begin
someclass:= THuman;
// Invoke RTTI
c:= TRttiContext.Create;
t:= c.GetType(someclass);
// Variant 1a - instantiates a THuman object but calls constructor of TObject
human1:= t.AsInstance.MetaclassType.Create;
// Variant 1b - same result as 1a
human2:= THuman(someclass.Create);
// Variant 2 - works fine
v:= t.GetMethod('Create').Invoke(t.AsInstance.MetaclassType,[]);
human3:= THuman(v.AsObject);
// free RttiContext record (see text below) and the rest
c.Free;
human1.Destroy;
human2.Destroy;
human3.Destroy;
end;
オブジェクト「human1」と「human2」がゼロに初期化されていることがわかります。つまり、Name = ''とAge = 0ですが、これは私たちが望んでいることではありません。代わりに、オブジェクトhuman3は、THumanのコンストラクターで提供されるデフォルト値を保持します。
ただし、このメソッドでは、クラスにパラメーターではないコンストラクターメソッドが必要であることに注意してください。上記のすべては私が考案したものではありませんが、 Rob Love's Tech Corner で見事にそしてはるかに詳細に説明されています(たとえば、c.Freeの部分)。
AfterConstructionのオーバーライドがオプションかどうかを確認してください。
コードが少し変更されました:
type
TMyObject = class(TObject)
MyStrings: TStrings;
constructor Create; virtual;
end;
TMyClass = class of TMyObject;
constructor TMyObject.Create;
begin
inherited Create;
MyStrings := TStringList.Create;
end;
procedure Test;
var
C: TMyClass;
Instance: TObject;
begin
C := TMyObject;
Instance := C.Create;
end;
基本クラスで抽象メソッドを作成し、それをコンストラクターで呼び出し、クラス参照から作成されたときに実行される子クラスでオーバーライドできます。