サンプルとして以下の機能を持つオブジェクトを設計します
「1桁の数字配列」を内部に持つシンプルなオブジェクトを扱います。
例:123 → 内部データは [1,2,3]
このオブジェクトに以下のAPIを持たせます。
■ API
-
Add(D: Integer)
数字を追加する -
Reverse
中身を反転する -
ToString
配列を文字列として取得する -
Concat(const Other: TDigitClass): TDigitClass
2つのインスタンスを結合した新しいインスタンスを返す
まずは普通にクラスで書くとこうなります
type
TDigitClass = class
private
FData: TArray<Integer>;
public
constructor Create(Value: Integer);
procedure Add(D: Integer);
procedure Reverse;
function ToString: string;
function Concat(const Other: TDigitClass): TDigitClass;
end;
constructor TDigitClass.Create(Value: Integer);
var
s: string;
i: Integer;
begin
s := Value.ToString;
SetLength(FData, Length(s));
for i := 1 to Length(s) do
FData[i - 1] := Ord(s[i]) - Ord('0');
end;
procedure TDigitClass.Add(D: Integer);
var
Len: Integer;
begin
Len := Length(FData);
SetLength(FData, Len + 1);
FData[Len] := D;
end;
procedure TDigitClass.Reverse;
var
i, Len: Integer;
NewData: TArray<Integer>;
begin
Len := Length(FData);
SetLength(NewData, Len);
for i := 0 to Len - 1 do
NewData[i] := FData[Len - 1 - i];
FData := NewData;
end;
function TDigitClass.ToString: string;
var
i: Integer;
begin
Result := '';
for i := 0 to High(FData) do
Result := Result + FData[i].ToString;
end;
function TDigitClass.Concat(const Other: TDigitClass): TDigitClass;
begin
Result := TDigitClass.Create(0);
Result.FData := Self.FData + Other.FData;
end;
使用例👇
A := TDigitClass.Create(123);
B := TDigitClass.Create(678);
A.Add(4);
A.Reverse;
A.Add(5);
Writeln(A.ToString); // 出力: 43215
C := A.Concat(B);
Writeln(C.ToString); // 出力: 43215678
A.Free;
B.Free;
C.Free;
不満
やりたいこと
A := TDigitClass.Create(123);
B := TDigitClass.Create(678);
Writeln(A.Add(4).Reverse.Add(5).ToString); // 出力: 43215
Writeln((A + B).ToString); // 出力: 43215678
👉しかも Free を書きたくない
発想:スマートポインタ風にする
Delphiにはインターフェースの参照カウントがあります。
👉 最後の参照が消えたら自動でDestroyされる
これを使います。
核となる設計
record型を実装して以下のフィールドを持たせる
外部インターフェースのAPIをすべて実装し、TDigitClass をラップする
FCore: TDigitClass ; // 本体
FRef : IInterface; // 寿命管理(参照カウント)
👉 同じインスタンスを2つで持つ
- FCore → 普通に使う
- FRef → 生存管理
なぜInterfaceではなくrecordなのか
- 演算子オーバーロードが使える
- 値型っぽく扱える
- 外部APIのインターフェースをいちいち宣言する必要なし
👉 クラスではできない
今回のテクニックを使った実装コード
program SmartRecordDemo;
{$APPTYPE CONSOLE}
uses
System.SysUtils;
type
//元のクラスそのものにインターフェースを実装する
TDigitClass = class(TInterfacedObject)
private
FData: TArray<Integer>;
public
constructor Create(Value: Integer);
procedure Add(D: Integer);
procedure Reverse;
function ToString: string;
function Concat(const Other: TDigitClass): TDigitClass;
// ログ取得用に追加
destructor Destroy; override;
end;
// スマートポインタ風のレコード
TDigit = record
private
FCore: TDigitClass;
FRef : IInterface;
function Core: TDigitClass;
public
constructor Create(Value: Integer);
function Add(D: Integer): TDigit;
function Reverse: TDigit;
function ToString: string;
// 演算子オーバーロードが使えるので実装する
class operator Add(const A, B: TDigit): TDigit;
end;
//////////////////////////
// TDigitClass
//////////////////////////
constructor TDigitClass.Create(Value: Integer);
var
s: string;
i: Integer;
begin
s := Value.ToString;
SetLength(FData, Length(s));
for i := 1 to Length(s) do
FData[i - 1] := Ord(s[i]) - Ord('0');
end;
procedure TDigitClass.Add(D: Integer);
var
Len: Integer;
begin
Len := Length(FData);
SetLength(FData, Len + 1);
FData[Len] := D;
end;
procedure TDigitClass.Reverse;
var
i, Len: Integer;
NewData: TArray<Integer>;
begin
Len := Length(FData);
SetLength(NewData, Len);
for i := 0 to Len - 1 do
NewData[i] := FData[Len - 1 - i];
FData := NewData;
end;
function TDigitClass.ToString: string;
var
i: Integer;
begin
Result := '';
for i := 0 to High(FData) do
Result := Result + FData[i].ToString;
end;
function TDigitClass.Concat(const Other: TDigitClass): TDigitClass;
begin
Result := TDigitClass.Create(0);
Result.FData := Self.FData + Other.FData;
end;
// ログの出力用に追加
destructor TDigitClass.Destroy;
begin
Writeln('>> Destroy TDigitClass');
inherited;
end;
//////////////////////////
// TDigit
//////////////////////////
constructor TDigit.Create(Value: Integer);
begin
FCore := TDigitClass.Create(Value);
FRef := FCore;
end;
function TDigit.Core: TDigitClass;
begin
if FRef = nil then
raise Exception.Create('TDigit is not initialized');
Result := FCore;
end;
function TDigit.Add(D: Integer): TDigit;
begin
Core.Add(D);
Result := Self;
end;
function TDigit.Reverse: TDigit;
begin
Core.Reverse;
Result := Self;
end;
function TDigit.ToString: string;
begin
Result := Core.ToString;
end;
class operator TDigit.Add(const A, B: TDigit): TDigit;
var
Inst: TDigitClass;
begin
Inst := A.Core.Concat(B.Core);
Result.FCore := Inst;
Result.FRef := Inst;
end;
// デモ実行
procedure RunDemo;
var
A, B: TDigit;
begin
Writeln('--- デモ ---');
A := TDigit.Create(123);
A.Add(4).Reverse.Add(5);
Writeln('A = ', A.ToString);
B := TDigit.Create(678).Reverse.Add(9);
Writeln('B = ', B.ToString);
Writeln('A + B = ', (A + B).ToString);
Writeln('--- 終了 ---');
Readln;
end;
begin
RunDemo;
Readln;
end.
デモ結果
--- デモ ---
A = 43215
B = 8769
A + B = 432158769
--- 終了 ---
>> Destroy TDigitClass
>> Destroy TDigitClass
>> Destroy TDigitClass
何が起きているか
- recordはコピーされる
- でも中身は同じTDigitClassオブジェクトを共有
- interfaceが寿命を管理
👉 最後の参照が消えたらDestroy
この手法のメリット
- メソッドチェーンできる
👉 通常メソッドチェーンは非破壊操作1で実現されるが、 今回は破壊系2でありながら Self を返すことで成立させているのがポイント - Free不要
- 演算子も使える
- Interface型の外部APIのインターフェースをいちいち宣言する必要なし