次の方法で共有



November 2016

Volume 31 Number 11

.NET Framework - 隠れた破棄可能な型

Artak Mkrtchyan | November 2016

破棄可能な型は便利です。決定的な方法でリソースを解放することができるからです。しかし、開発者が気付かないうちに破棄可能な型を使用している場合があります。創造的な設計パターンの使用は、破棄可能な型が使用されていることがわかりにくい状況の一例です。このような状況では、オブジェクトが破棄されない可能性があります。この記事では、この問題の対処法を説明します。まず、創造的な設計パターンをいくつか見ていきます。

創造的な設計パターン

創造的な設計パターンの大きなメリットは、パターンが実際の実装から抽象化されており、インターフェイスの言語で利用できる点です。このようなデザイン パターンは、オブジェクトの作成メカニズムに対応し、ソリューションに適したオブジェクトを作成します。基本的なオブジェクト作成と比較すると、創造的な設計パターンは、オブジェクト作成プロセスのいくつかの側面を向上します。創造的な設計パターンの、よく知られているメリットは次の 2 つです。

  • 抽象化: 作成されるオブジェクトの型を抽象化します。そのため、呼び出し元は実際に返されるオブジェクトを認識しません。呼び出し元が認識するのは、インターフェイスのみです。
  • 内部の作成: 特定の型のインスタンス作成に関する知識をカプセル化します。

次に、2 つのよく知られた創造的な設計パターンの概要を簡単に紹介します。

Factory Method (ファクトリ メソッド) 設計パターン: Factory Method 設計パターンは私のお気に入りの 1 つで、日常の作業でたくさん使用しています。このパターンではファクトリ メソッドを使用し、オブジェクト作成の問題に、作成するオブジェクトの具体的なクラスを指定することなく対処します。クラス コンストラクターを直接呼び出すのではなく、ファクトリ メソッドを呼び出してオブジェクトを作成します。このファクトリ メソッドは、抽象化 (インターフェイスまたは基底クラス) を返します。子クラスがこの抽象化を実装します。図 1 に、このパターンの統一モデリング言語 (UML) の図を示します。

図 1 では、ConcreteProduct が IProduct 抽象化/インターフェイスの具体的な型です。同様に、ConcreteCreator は ICreator インターフェイスの具体的な実装です。

Factory Method 設計パターン
図 1 Factory Method 設計パターン

このパターンのクライアントは、ICreator インスタンスを使用します。また、返された実際の製品を認識することなく、Create メソッドを呼び出して IProduct の新しいインスタンスを取得します。

Abstract Factory (抽象型ファクトリ) 設計パターン: Abstract Factory 設計パターンの目的は、具体的な実装を指定せずに関連または依存関係のあるオブジェクトのファミリを作成するためのインターフェイスを提供することです。

このパターンは、クライアントがファクトリ オブジェクトに、望ましい抽象型のオブジェクトを作成し、抽象ポインターをクライアントに返されるオブジェクトに返すように要求することで、クライアント コードをオブジェクト作成の労力から保護します。特にこれは、クライアント コードが具象型を認識しないことを意味します。クライアント コードでは、抽象型のみを扱います。

新しい具象型のサポートを追加するには、新しいファクトリ型を作成し、必要に応じてクライアント コードを変更して異なるファクトリ型を使用することで対処します。ほとんどの場合、コードを 1 行変更するだけで対処できます。クライアント コードは新しいファクトリ型に対応するように変更する必要がないため、変更の処理が目に見えて簡単になります。図 2 に、Abstract Factory 設計パターンの UML 図を示します。

Abstract Factory 設計パターン
図 2 Abstract Factory 設計パターン

クライアントにとっては、Abstract Factory の使用方法は、以下のようなコードで表されます。

IAbstractFactory factory = new ConcreteFactory1();
IProductA product = factory.CreateProductA();

クライアントでは、実際のファクトリの実装を自由に変更し、背後で作成される製品の型を制御できます。これによりコードに影響が及ぶことはまったくありません。

このコードはサンプルにすぎません。適切に構造化されたコードでは、おそらく、例のようにファクトリ メソッド パターンを使用してファクトリのインスタンス作成自体が抽象化されます。

問題点

設計パターンの例の両方で、ファクトリが関与していました。ファクトリは、実際のメソッド/プロシージャであり、クライアントからの呼び出しに対する応答として、抽象化を通じて構築された型の参照を返します。

技術的には、図 3 に示すように、抽象化が存在する場合は、ファクトリを使用してオブジェクトを作成できます。

簡単な抽象化およびその使用方法の例
図 3 簡単な抽象化およびその使用方法の例

ファクトリは、関連する要因に基づいて、さまざまな利用可能な実装の選択を処理します。

依存関係の逆転の原則によれば、以下のことが言えます。

  • 上位モジュールが下位モジュールに依存してはなりません。上位モジュールと下位モジュールの両方が抽象化に依存するようにする必要があります。
  • 抽象化が詳細に依存してはなりません。詳細が抽象化に依存するようにする必要があります。

これは、技術的には、依存関係チェーンのすべてのレベルで、依存関係が抽象化に置き換えられる必要があることを意味します。さらにこれらの抽象化の作成は、ファクトリを利用して処理できます (多くの場合はファクトリを使用する必要があります)。

これはすべて、日常のコーディングにおいてファクトリがどれほど重要であるかを強調しています。しかし、ファクトリには、破棄可能な型という問題が潜んでいます。その詳細について説明する前に、まず IDisposable インターフェイスと Dispose (破棄) 設計パターンについて説明します。

Dispose 設計パターン

すべてのプログラムは、実行中にメモリ、ファイル ハンドル、データベース接続などのリソースを取得します。こうしたリソースは取得して使用した後に解放する必要があるため、開発者は、使用するときに注意が必要です。

共通言語ランタイム (CLR) は、ガベージ コレクター (GC) を通じた自動メモリ管理のサポートを提供します。マネージ メモリを明示的にクリーンアップする必要はありません。GC によって自動的に行われるからです。残念なことに、(アンマネージ リソースと呼ばれる) その他の種類のリソースは、明示的に解放する必要があります。GC はこうした種類のリソースを扱うことを目的としていないので、その他の種類のリソースを解放するのは、開発者の役目になります。

ただ、CLR は、開発者がアンマネージ リソースを処理する際に役立ちます。System.Object 型は、Finalize と呼ばれるパブリック仮想メソッドを定義します。このメソッドは、オブジェクトのメモリが解放される前に GC によって呼び出されます。Finalize メソッドは通常、ファイナライザーと呼ばれます。このメソッドをオーバーライドして、オブジェクトで使用された追加のアンマネージ リソースをクリーンアップするようにできます。

しかし、このメカニズムには、GC の実行における特定の側面により、いくつかの欠点があります。

オブジェクトがコレクションの対象であることが GC で検出されると、ファイナライザーが呼び出されます。この呼び出しは、オブジェクトがもう必要なくなった後の不確定な期間に発生します。

ファイナライザーを呼び出す必要がある場合、GC では実際のメモリ コレクションを次のラウンドのガベージ コレクションに先送りする必要があります。これにより、オブジェクトのメモリ コレクションがさらに後に先送りされます。ここで、System.IDisposable インターフェイスが登場します。Microsoft .NET Framework には、アンマネージ リソースを手動で解放するメカニズムを開発者に提供するために実装する必要のある IDisposable インターフェイスが用意されています。このインターフェイスを実装する型は、破棄可能な型と呼ばれます。IDisposable インターフェイスは、Dispose と呼ばれる、パラメーターなしのメソッド 1 つだけを定義します。オブジェクトが不要になったらすぐに Dispose を呼び出して、参照先のアンマネージ リソースを即座に解放する必要があります。

「結局は GC が処理してくれるのに、なぜ自分で Dispose を呼び出さなければならないのか」と疑問に思うかもしれません。 これに答えるには、GC の実行がパフォーマンスに与える影響についての側面にも触れながら、記事をあらためて説明する必要があります。この質問はこの記事で取り扱う範囲を超えているので触れません。話を次に進めましょう。

ある型が破棄可能であるかどうかを判断する場合に従う決まった規則があります。大まかな規則はこうです。 特定の型のオブジェクトがアンマネージ リソースまたはその他の破棄可能なオブジェクトを参照する場合、その型も破棄可能です。

Dispose パターンは、IDisposable インターフェイスの具体的な実装を定義します。このパターンでは、2 つの Dispose メソッドを実装する必要があります。1 つは、(IDisposable インターフェイスによって定義される) パラメーターなしのパブリック メソッドで、もう 1 つは、ブール値のパラメーターを 1 つ持つ保護された仮想メソッドです。当然ながら、型がシールされる場合は、保護された仮想メソッドはプライベートに置き換えられる必要があります。

図 4 Dispose デザイン パターンの実装

public class DisposableType : IDisposable {
  ~DisposableType() {
    this.Dispose(false);
  }
  public void Dispose() {
    this.Dispose(true);
    GC.SuppressFinalize(this);
  }
  protected virtual void Dispose(bool disposing) {
    if (disposing) {
      // Dispose of all the managed resources here
    }
    // Dispose of all the unmanaged resources here
  }
}

ブール値のパラメーターは、Dispose メソッドが呼び出される方法を示します。パブリック メソッドでは、パラメーター値 “true” を指定して、保護された仮想メソッドを呼び出します。 同様に、クラス階層内の Dispose(bool) メソッドのオーバーロードでは、base.Dispose(true) を呼び出す必要があります。

Dispose パターンの実装でもまた、Finalize メソッドのオーバーロードが必要になります。これは、オブジェクトがもう必要なくなった後に、開発者が Dispose メソッドを呼び出すのを忘れた場合を想定して行います。ファイナライザーは GC によって呼び出されるため、参照されるマネージ リソースは既にクリーンアップされているか、クリーンアップされます。そのため、開発者は、Dispose(bool) メソッドがファイナライザーから呼び出される場合にのみアンマネージ リソースの解放を処理する必要があります。

本題に戻りましょう。問題が発生するのは、創造的な設計パターンで使用された破棄可能なオブジェクトを処理するときです。

抽象化を実装する具象型の 1 つが IDisposable インターフェイスも実装するシナリオを考えてみましょう。私の例では、その具象型を ConcreteImplementation2 とします (図 5 参照)。

抽象化と IDisposable の実装
図 5 抽象化と IDisposable の実装

IAbstraction インターフェイス自体は、IDisposable から継承しないことに注意してください。

次に、クライアント コードを見てみましょう。このコードでは、抽象化が使用される予定です。IAbstraction インターフェイスは変更されていないので、クライアントが背後で行われた可能性のある変更を認識することはありません。当然、クライアントでは、オブジェクトを与えられてそのオブジェクトを破棄する責任を負っているとは想定しません。実際には、IDisposable インスタンスがあるとは想定されません。多くの場合、これらのオブジェクトがクライアント コードによって明示的に破棄されることは絶対にありません。

期待するのは、Concreate­Implementation2 の実際の実装で Dispose 設計パターンを実装することです。ただ、常にそうなるとは限りません。

返された IAbstraction インスタンスが IDisposable インターフェイスも実装するケースを処理する簡単なメカニズムによって、以下に示すような、クライアント コードの明示的なチェックを導入することになるであろうことは明らかです。

IAbstraction abstraction = factory.Create();
try {
  // Operations with abstraction go here
}
finally {
  if (abstraction is IDisposable)
    (abstraction as IDisposable).Dispose();
}

しかし、これはすぐに面倒な手順になります。

残念ながら、using ブロックは IDisposable を明示的に拡張しないため、IAbstraciton で使用することはできません。そこで、finally ブロックのロジックをラップし、using ブロックを使用できるようにするヘルパー クラスを思い付きました。図 6 は、このクラスのコード全体および使用方法のサンプルを示します。

図 6 PotentialDisposable 型とその使用方法

public sealed class PotentialDisposable<T> : IDisposable where T : class {
  private readonly T instance;
  public T Instance { get { return this.instance; } }
  public PotentialDisposable(T instance) {
    if (instance == null) {
      throw new ArgumentNullException("instance");
    }
    this.instance = instance;
  }
  public void Dispose() {
    IDisposable disposableInstance = this.Instance as IDisposable;
    if (disposableInstance != null) {
      disposableInstance.Dispose();
    }
  }
}
The client code:
IAbstraction abstraction = factory.Create();
using (PotentialDisposable<IAbstraction> wrappedInstance =
  new PotentialDisposable<IAbstraction>(abstraction)) {
    // Operations with abstraction wrapedInstance.Instance go here
}

図 6 の "クライアント コード" 部分をご覧になればおわかりのとおり、PotentialDisposable<T> クラスを使用すると、using ブロックを使用したクライアント コードがたったの数行になります。

IAbstraction インターフェイスだけを更新して IDisposable にすることも検討できます。これは、ある状況ではお勧めのソリューションですが、別の状況ではお勧めできません。

IAbstraction インターフェイスがあり、IAbstraction にとって IDisposable を拡張するのが適切である場合、このソリューションを利用することをお勧めします。実は、この好例が System.IO.Stream 抽象クラスです。このクラスは実際に IDisposable インターフェイスを実装しますが、定義された実際のロジックはありません。その理由は、クラスのライターが、ほとんどの子クラスがなんらかの種類の破棄可能メンバーを持つことを認識しているからです。

別の状況: IAbstraction インターフェイスがあるが、IAbstraction インターフェイスの実装のほとんどが破棄不可能のため、IDisposable を拡張するのが適切でない場合があります。例として、ICustomCollection インターフェイスについて考えてみましょう。いくつかのインメモリ実装があるとします。そこで急に、データベースを利用するなんらかの実装を追加する必要が生まれたとします。追加の実装は、唯一の破棄可能な実装になります。

最終的には、IAbstraction インターフェイスがない状態になります。そのため、インターフェイスを管理できません。データベースを利用する ICollection の例を考えてみてください。

まとめ

取得する抽象化がファクトリ メソッドを利用したものであるかどうかに関係なく、コードを作成する際は破棄可能な型について意識することが大切です。破棄可能なオブジェクトを処理する際に、コードを可能な限り効率的にするには、この簡単なヘルパー クラスを使用するのが 1 つの方法です。


Artak Mkrtchyan は、ワシントン州レドモンド在住のシニア ソフトウェア エンジニアです。彼は、釣りと同じくらいコーディングが好きです。

この記事のレビューに協力してくれたマイクロソフト技術スタッフの Paul Brambilla に心より感謝いたします。
Paul Brambilla は Microsoft の上級ソフトウェア開発者で、クラウド サービスおよび基本インフラストラクチャを専門としています。