Delphi モバイル コンパイラでの自動参照カウント

提供: RAD Studio
移動先: 案内検索

Delphi におけるマルチデバイス アプリケーションについての考慮事項 への移動


Delphi ユーザーにとって、自動参照カウント(ARC)の考え方は、もうかなり前からおなじみのものです。これまで、Delphi デスクトップ コンパイラ(DCC32、DCC64、DCCOSX)では、インターフェイス、動的配列、文字列の ARC をサポートしてきました(インターフェイスの ARC は Delphi 3 で導入され、文字列については AnsiStringDelphi 2 で導入)。現在、Delphi モバイル コンパイラには、クラスの自動参照カウントが導入されています。それに合わせて、クラスの演算子オーバーロードの機能と共に、"循環" を管理するために弱い参照の考え方も導入されつつあります。

自動参照カウントの詳細については、ARC のリリース ノート(Apple 社)を参照してください。

自動参照カウント

自動参照カウントは、オブジェクトが不要になったときにそのオブジェクトを解放せずにその存続時間を管理する手法です。このいい例はローカル変数で、スコープの外へ行くオブジェクトを参照しています。スコープから出たら、オブジェクトは自動的に破棄されます。 上記のとおり、Delphi では、インターフェイス型変数を通じて参照される文字列とオブジェクトの参照カウントを既にサポートしています。 ただし、Delphi モバイル コンパイラでの ARC では、インターフェイス型変数では扱いにくい循環参照などの問題を解決するために、少し柔軟性があります。

新しい Delphi モバイル コンパイラに導入されたもう 1 つの考え方は、弱い参照です。これにより、循環参照の問題がいくつか解決されます。

iOS デバイスおよび iOS シミュレータ向けにコンパイルされたアプリケーションの場合、メモリ管理のしくみは同じです。

  • DCCIOSARM(32 ビット iOS デバイス向けの Delphi コンパイラ)には、デフォルトで自動参照カウント機能が備わっています。
  • DCCIOSARM64(64 ビット iOS デバイス向けの Delphi コンパイラ)には、デフォルトで自動参照カウント機能が備わっています。
  • DCCIOS32(iOS シミュレータ向けの Delphi コンパイラ)は従来のコンパイラ アーキテクチャに基づいてはいますが、ARC が有効になっています。
メモ: これらの新しい機能が RAD Studio ソース コードに表われるのは、RTL(たとえば System.pas など)内の {$AUTOREFCOUNT} 条件ブロックの中だけです。

コーディング スタイルの変更点

新しいモバイル コンパイラでは自動参照カウントをサポートしているため、メソッド内で一時オブジェクトを参照する必要がある場合、コードを大幅に簡略化できます。以下のように、メモリ管理は完全に無視されます。

class procedure TMySimpleClass.CreateOnly;
var
  MyObj: TMySimpleClass;
begin
  MyObj := TMySimpleClass.Create;
  MyObj.DoSomething;
end;

上記の例では、プログラムの実行が end 文に達したとき、つまり MyObj 変数がスコープから出るときに、オブジェクトのデストラクタが呼び出されます。

また、メソッドが終了しないうちにオブジェクトを使用しなくなる可能性もあります。そのような場合には、以下のように変数を nil に設定します。

class procedure TMySimpleClass.SetNil;
var
  MyObj: TMySimpleClass;
begin
  MyObj := TMySimpleClass.Create;
  MyObj.DoSomething (False); // True => raise
  MyObj := nil;	
  // do something else
end;

このような場合、メソッドが終了する前でも、変数を nil に設定した時点でオブジェクトは破棄されます。DoSomething 手続きで例外が発生した場合は、nil 代入文がスキップされます。結局、メソッドが終了したときに、やはりオブジェクトが破棄されます。

オブジェクトの存続時間はプログラム フローに従うので、以下のような新しい RefCount プロパティを使って、オブジェクトの参照カウントを問い合わせることができます(参照カウント機能が用意されているプラットフォームのみ)。

public
    property RefCount: Integer read FRefCount;
メモ: オブジェクトの参照カウントを問い合わせることはお勧めしません。一般に、そのような問い合わせは使用すべきではありません。

インターロックされた操作の速度

オブジェクトの参照カウントのインクリメント操作およびデクリメント操作は、参照カウントを表すインスタンス メンバがスレッドセーフになるように、スレッドセーフな操作を使って実行されます。つまり、すべてのスレッドで変更が直ちに認識され、古い値を変更することが絶対にないように、参照カウントを表すインスタンス変数にはメモリ フェンスがきちんと適用されているのです。

メモ: 自動参照カウント メカニズムでは、競合状態やデッドロックを防げません。

ARC と {$AUTOREFCOUNT} 指令

ARC を利用できる場合でも従来の場合でも最適なコードにするために、{$IFDEF AUTOREFCOUNT} 指令を使用することを検討するとしましょう。AUTOREFCOUNT は、Delphi モバイル コンパイラ向けのコードなど、自動参照カウント機能を使用するコードであることを明示するものです。これは重要な指令で、{$IFDEF NEXTGEN}(モバイル コンパイラの新しい言語機能であることを明示する指令)とは異なります。AUTOREFCOUNT は、今後 ARC が Delphi デスクトップ コンパイラ上にも実装される場合には役に立つ可能性があります。

ARC の下での Free メソッドおよび DisposeOf メソッド

クラスについては、Delphi 開発者は、try-finally ブロックで保護した Free メソッドの呼び出しをベースにした別のコーディング パターンに慣れています。

例:

class procedure TMySimpleClass.TryFinally;
var
  MyObj: TMySimpleClass;
begin
  MyObj := TMySimpleClass.Create;
  try
    MyObj.DoSomething;
  finally
    MyObj.Free;
  end;
end;

Delphi デスクトップ コンパイラの場合、FreeTObject のメソッドで、現在の参照が nil でないかどうかを確かめ、該当する場合は Destroy デストラクタを呼び出します。その結果、適切なデストラクタ コードの実行後、オブジェクトがメモリから削除されます。

新しい Delphi モバイル コンパイラの場合は、そうではなく、Free の呼び出しの代わりに、変数への nil の代入が使用されます。これがオブジェクトへの最後の参照である場合、そのデストラクタの呼び出し後、やはりこれがメモリから削除されます。他に参照が残っている場合は、(参照カウントの減少以外は)何も起こりません。

FreeAndNil (MyObj);

同様に、上記のような呼び出しでは、オブジェクトを nil に設定し、そのオブジェクトを参照している変数が他にない場合に限り、やはりオブジェクトの破棄をトリガします。ほとんどの場合はこれで間違いはありません。プログラムの別の部分で使用されているオブジェクトを破棄しようとはしないからです。一方、オブジェクトへの保留中の参照が他にあっても、デストラクタ コードをすぐに実行しなければならない場合もあります。開発者が(実際のオブジェクトをメモリから解放せずに)デストラクタを強制的に実行できるようにするため、新しいコンパイラには、以下のような破棄パターンが導入されています。

MyObject.DisposeOf;

これを呼び出すと、保留中の参照が残っていても、デストラクタ コードが強制的に実行されます。破棄操作がさらに実行される場合や参照カウントがゼロに達しメモリが実際に解放される場合にデストラクタが再度呼び出されないように、この時点でオブジェクトは特別な状態に置かれます。この破棄状態(またはゾンビ状態)は非常に重要であり、オブジェクトがその状態にあるかどうかを、Disposed プロパティを使って問い合わせることができます。

先に述べたように、Delphi デスクトップ コンパイラ向けのコードで使用される従来の try-finally ブロックは、たとえ必要なくても、新しいコンパイラでも問題なく動作します。 ただし、多くの場合には、(Delphi の以前のバージョン向けにコードを再コンパイルするのでない限り)代わりに破棄パターンを使用するとよいでしょう。

以下の例では、DisposeOf で Free が呼び出されるので、効果は従来のコンパイラの場合と変わりません。ARC が有効な場合は、そうではなく、以下のコードでは予想どおりに(従来のコンパイラの場合と同じタイミング。ただし、メモリは ARC メカニズムで管理)、デストラクタが実行されます。

var
  MyObj: TMySimpleClass;
begin
  MyObj := TMySimpleClass.Create;
  try
    MyObj.DoSomething;
  finally
    MyObj.DisposeOf;
  end;
end;
メモ: オブジェクトの参照が最大 230 個までであることを考慮して、Disposed フラグの記憶域は FRefCount フィールド内の 1 ビットです。

ARC の下での TObject 生成メソッドおよび破棄メソッド

TObject クラス内部のメソッドのうち、生成と破棄に関係するものを以下にまとめます。

FreeDisposeOf の違いを調べる方法の 1 つは、意図を考えることです。Free を使用する場合、意図は、ユーザーがその特定の参照をインスタンスから引き離す必要があるということです。いかなる種類の破棄やメモリの割り当て解除も意味していません。これに対して、DisposeOf は、プログラマがインスタンスに "自分自身をクリーンアップする" 必要があることを明示的に伝える手段です。メモリからの解放は必ずしも意味していません。DisposeOf はインスタンスの明示的な "事前クリーンアップ" を行い、次にそれが通常の参照カウント セマンティクスに基づいて、最終的にインスタンスをメモリから解放するのです。

弱い参照

ARC のもう 1 つの重要な概念は、弱い参照というロールです。これは、[weak] 属性でタグ付けすることにより作成できます。2 つのオブジェクトがフィールドを使って互いを参照し合っていて、外部変数が第 1 のオブジェクトを参照しているとしましょう。第 1 オブジェクトの参照カウントは 2(外部変数と第 2 オブジェクト)になるのに対して、第 2 オブジェクトの参照カウントは 1 です。さて、外部変数がスコープから出ると、2 つのオブジェクトの参照カウントは 1 のままで、それらはいつまでもメモリに残ります。

この種の状況やそれに似た多くのシナリオを解決する方法は、弱い参照を使用して、最後の外部参照がスコープから出たときに循環参照を解除することです。

弱い参照は、参照カウントを増やさないオブジェクト参照です。弱い参照を宣言するには、[weak] 属性を使用します。この属性は Delphi モバイル コンパイラでサポートされています。

前のシナリオを考えると、第 2 オブジェクトから第 1 オブジェクトへの参照が弱い参照であれば、外部変数がスコープから出たとき、両方のオブジェクトは破棄されます。このような状況の例を以下に示します。

type
  TMyComplexClass = class;

  TMySimpleClass = class
  private
    [Weak] FOwnedBy: TMyComplexClass;
  public
    constructor Create();
    destructor Destroy (); override;
    procedure DoSomething(bRaise: Boolean = False);
  end;

  TMyComplexClass = class
  private
    fSimple: TMySimpleClass;
  public
    constructor Create();
    destructor Destroy (); override;
    class procedure CreateOnly;
  end;

以下のコード断片では、複合クラスのコンストラクタでもう一方のクラスのオブジェクトが作成されます。

constructor TMyComplexClass.Create;
begin
  inherited Create;
  FSimple := TMySimpleClass.Create;
  FSimple.FOwnedBy := self;
end;

FOwnedBy フィールドは弱い参照であることを思い出してください。そのため、参照先のオブジェクト(この例では現在のオブジェクト self)の参照カウントは増えません。

このクラス構造を前提として、以下のようにコーディングできます。

class procedure TMyComplexClass.CreateOnly;
var
  MyComplex: TMyComplexClass;
begin
  MyComplex := TMyComplexClass.Create;
  MyComplex.fSimple.DoSomething;
end;

弱い参照が適切に使用されていれば、この結果、メモリ リークは発生しません。

弱い参照のさらなる使用例として、Delphi RTL に含まれている以下のコード断片に注目してください。これは TComponent クラス宣言の一部です。

type
  TComponent = class(TPersistent, IInterface,
    IInterfaceComponentReference)
  private
    [Weak] FOwner: TComponent;

Delphi デスクトップ コンパイラのいずれか 1 つでコンパイルされるコードで weak 属性を使用する場合、その属性は無視されます。DCC32 を使用する場合は、必ず、"所有する側" のオブジェクトのデストラクタに適切なコードを追加して "所有される側" のオブジェクトも解放する必要があります。Free を呼び出すことは可能ですが、その効果は Delphi モバイル コンパイラの場合はまた異なります。ほとんどの状況では、どちらの場合も正しく動作します。

メモ: インスタンスのメモリが解放されると、アクティブな [weak] 参照はすべて nil に設定されます。強い(通常の)参照と同様に、[weak] 変数も nil になるか有効なインスタンスを参照するかのどちらかしかありません。弱い参照を、nil かどうか検査して逆参照する前に強い参照に代入しなければならない主な理由はこれです。強い参照に代入することで、インスタンスが早まって解放されることはなくなります。
var
  O: TComponent;// a strong reference
begin
  O := FOwner;  // assigning a weak reference to a strong reference
  if O <> nil then
    O.<method>;// safe to dereference
end;
メモ: [Weak] 変数は、やはり [Weak] と指定されている var パラメータまたは out パラメータにのみ渡すことができます。通常の強い参照を [Weak] 指定の var パラメータや out パラメータに渡すことはできません。

Unsafe 属性

unsafe 属性を関数に設定するには、以下の構文を使用します。

[Result: Unsafe] function ReturnUnsafe: TObject;
警告: [Unsafe] は変数(メンバ)およびパラメータにも適用できます。非常にまれな状況では、これを System ユニットの外部でのみ使用しなければなりません。これは危険と見られており、参照カウントに関連するコードが生成されないので、それを使用することはお勧めできません。
メモ: [Unsafe] 変数は、やはり [Unsafe] と指定されている var パラメータまたは out パラメータにのみ渡すことができます。通常の強い参照を [Unsafe] 指定の var パラメータや out パラメータに渡すことはできません。

クラスの演算子オーバーロード

ARC メモリ管理を使用する場合、関数から返される一時オブジェクトの存続時間をコンパイラで処理できるという非常に興味深い副作用があります。一時オブジェクトが演算子から返されるケースは、その具体例の 1 つです。実際に、新しい Delphi コンパイラの最新の機能は、Delphi 2006 以降レコードに対して使用可能になっているのと同じ構文およびモデルでクラスの演算子を定義できることです。

メモ: 以下のコード例は Delphi モバイル(iOS)コンパイラでは動作しますが、Delphi デスクトップ コンパイラではコンパイルできません。

一例として、以下の簡単なクラスについて考えてみましょう。

type
  TNumber = class
  private
    FValue: Integer;
    procedure SetValue(const Value: Integer);
  public
    property Value: Integer read FValue write SetValue;
    constructor Create(Value: Integer);
    function ToString: string; override;
    class operator Add (a, b: TNumber): TNumber;
    class operator Implicit (n: TNumber): Integer;
    class operator Implicit(n: Integer): TNumber;
  end;

constructor TNumber.Create(Value: Integer); begin
  inherited Create;
  Self.Value := Value;
end;

class operator TNumber.Add(a, b: TNumber): TNumber; begin
  Result := TNumber.Create(a.Value + b.Value); end;

class operator TNumber.Implicit (n: TNumber): Integer; begin
  Result := n.Value;
end;

class operator TNumber.Implicit (n: Integer): TNumber; begin
  Result := TNumber.Create(n);
end;

procedure TNumber.SetValue(const Value: Integer); begin
  FValue := Value;
end;

function TNumber.ToString: string;
begin
  Result := IntToStr(Value);
end;

procedure Test;
var
  a, b, c: TNumber;
begin
  a := 10;
  b := 20;
  c := a + b;
  Writeln(c.ToString);
end;

関連項目

コード例