Cutting Edge
くよくよせずに先延ばしにしよう
Dino Esposito
ソフトウェア開発では、遅延 (laziness) という言葉は、怠惰という意味よりもむしろ、コストの高い特定の作業を可能な限り先延ばしにすることを指します。ソフトウェアにおける遅延では、処理が行われることに変わりはありませんが、あらゆる処理は特定の作業を完了するために必要なときにだけ行われます。この点で、遅延はソフトウェア開発において重要なパターンであり、設計と実装を含むさまざまなシナリオにうまく適用することができます。
たとえば、エクストリーム プログラミングの方法論における基本的なコーディング プラクティスの 1 つは、"You Aren't Gonna Need It" (そんなものが必要になることはない) という簡単な言葉にまとめられます。これは、作業を可能な限り先延ばしにして、必要な機能だけを必要なときにだけコードベースに実装するようにという明確な勧めです。
話は変わりますが、クラスの実装中に、アクセスにコストがかかるソースからデータを読み取る場合、遅延が適していることがあります。実際、遅延読み込みパターンは、クラスのメンバーを定義しておくが、その中身が他のクライアント コードから実際に要求されるまでそのメンバーを空にしておくという、一般的に認められた手法を表します。遅延読み込みは、Entity Framework や NHibernate などのオブジェクト リレーショナル マッピング (ORM) ツールに最適です。ORM ツールは、オブジェクト指向の世界とリレーショナル データベースとの間でデータ構造をマップするのに使用されます。この場合、遅延読み込みは、フレームワークで、たとえば、なんらかのコードが Customer クラスの公開されている Orders コレクション プロパティを読み取ろうとしているときにだけ顧客の Orders を読み込むことが可能であることを指します。
ただし、遅延読み込みは、ORM プログラミングなどの特定の実装シナリオにしか適用できないものではありません。遅延読み込みとは、なんらかのデータのインスタンスを実際に使うときまで取得しないということです。つまり、遅延読み込みとは、作成する必要があるものを追跡し、その中身がいずれ要求されたときにそれを暗黙のうちに作成する、特別なファクトリ ロジックを持つということです。
Microsoft .NET Framework では、長い間、私たち開発者は遅延動作をクラス内に手動で実装する必要がありました。この作業に役立つ組み込みのメカニズムはありませんでした。ですが、それは、.NET Framework 4 が登場するまでの話です。.NET Framework 4 では、新しい Lazy<T> クラスを活用できるようになりました。
Lazy<T> クラスの紹介
Lazy<T> は、特定の型 T のオブジェクトを包むラッパーとして使用される特別なファクトリです。Lazy<T> ラッパーは、まだ存在しないクラス インスタンスのライブ プロキシを表します。このような遅延ラッパーを使用する理由はたくさんありますが、最も重要な理由はパフォーマンスの向上です。オブジェクトの遅延初期化を使用すると、厳密には必要でない計算は行われなくなるので、メモリ消費量が削減されます。適切に適用した場合、オブジェクトの遅延インスタンスの作成は、アプリケーションの起動速度を大幅に向上させるための非常に優れたツールにもなります。次のコードは、オブジェクトを遅延初期化する方法を示しています。
var container = new Lazy<DataContainer>();
この例では、DataContainer クラスは、他のオブジェクトの配列を参照する簡単なデータ コンテナー オブジェクトを示します。Lazy<T> インスタンスに対して new 演算子を呼び出した直後に返されるのは、Lazy<T> クラスのライブ インスタンスだけです。指定した型 T のインスタンスが返されることはありません。DataContainer のインスタンスを他のクラスのメンバーに渡す必要がある場合は、次のように、そのメンバーのシグネチャを、Lazy<DataContainer> を使用するように変更する必要があります。
void ProcessData(Lazy<DataContainer> container);
必要なデータをプログラムで処理できるように、DataContainer の実際のインスタンスが作成されるのはいつでしょうか。Lazy<T> クラスのパブリック プログラミング インターフェイスを見てみましょう。パブリック インターフェイスはとても簡略で、Value と IsValueCreated という 2 つのプロパティしか含まれていません。Value プロパティは、Lazy 型に関連付けられたインスタンスの現在の値を返します (存在する場合)。このプロパティは次のように定義されています。
public T Value
{
get { ... }
}
IsValueCreated プロパティは、ブール値を返し、Lazy 型のインスタンスが作成されているかどうかを示します。このプロパティのソース コードからの抜粋を以下に示します。
public bool IsValueCreated
{
get
{
return ((m_boxed != null) && (m_boxed is Boxed<T>));
}
}
m_boxed メンバーは Lazy<T> クラスのプライベートで volatile な内部メンバーで、m_boxed には、T 型の実際のインスタンス (存在する場合) が格納されています。したがって、IsValueCreated は単に T のライブ インスタンスが存在するかどうかをチェックし、ブール値の答えを返します。前述のとおり、m_boxed メンバーはプライベートで volatile です (次のスニペット参照)。
private volatile object m_boxed;
C# では、volatile キーワードは、同時に実行されているスレッドで変更を加えることができるメンバーを示します。volatile キーワードは、マルチスレッド環境で使用できるメンバーでありながら、同時にアクセスしてくる可能性のある同時実行スレッドからの保護を (主にパフォーマンス上の理由で) 備えていないメンバーに対して、使用されます。Lazy<T> の、スレッドに関する側面については、後でまた説明します。今のところは、Lazy<T> のパブリック メンバーとプロテクト メンバーは既定でスレッド セーフである、と言うにとどめておきます。型 T の実際のインスタンスは、コードによる Value メンバーへのアクセスが最初に試みられたときに作成されます。オブジェクト作成の詳細は、Lazy<T> コンストラクターを通じて必要に応じて指定されたスレッド属性によって決まります。スレッド モードの指定が重要な意味を持つのは、ボックス化された値が実際に最初に初期化またはアクセスされるときだけです。
既定では、型 T のインスタンスは、Activator.CreateInstance を呼び出すことにより、リフレクションを通じて取得されます。Lazy<T> 型の一般的な使い方の簡単な例を以下に示します。
var temp = new Lazy<DataContainer>();
Console.WriteLine(temp.IsValueCreated);
Console.WriteLine(temp.Value.SomeValue);
Value を呼び出す前に IsValueCreated をチェックするという処理は、必ずしも必要ではありません。一般に、IsValueCreated の値をチェックするのは、現在、Lazy 型に値が関連付けられているかどうかを (なんらかの理由により) 知る必要がある場合のみです。Value で null 参照の例外が発生するのを避けるために IsValueCreated をチェックする必要はありません。次のコードは問題なく機能します。
var temp = new Lazy<DataContainer>();
Console.WriteLine(temp.Value.SomeValue);
Value プロパティの getter は、ボックス化された値が既に存在するかどうかをチェックします。存在しない場合、ラップされた型のインスタンスを作成してそれを返すロジックがトリガーされます。
インスタンスの作成プロセス
もちろん、Lazy 型 (上記の例では DataContainer) のコンストラクターが例外をスローする場合は、コードでその例外を処理する必要があります。キャプチャされる例外は TargetInvocationException 型です。これは、.NET リフレクションで、ある型のインスタンスを間接的に作成するのに失敗した場合に発生する標準的な例外です。
Lazy<T> ラッパーのロジックは、型 T のインスタンスが作成されることを保証するだけです。T のパブリック メンバーにアクセスしたときに null 参照の例外が発生しないことは保証されません。たとえば、次のコード スニペットについて考えてみましょう。
public class DataContainer
{
public DataContainer()
{
}
public IList<String> SomeValues { get; set; }
}
クライアント プログラムから次のコードを呼び出そうとするとします。
var temp = new Lazy<DataContainer>();
Console.WriteLine(temp.Value.SomeValues.Count);
この場合、DataContainer 自体が null だからではなく、DataContainer オブジェクトの SomeValues プロパティが null であるため、例外が発生します。例外が発生するのは、DataContainer のコンストラクターがすべてのメンバーを適切に初期化しないためです。エラーは、遅延アプローチの実装とは関係ありません。
Lazy<T> の Value プロパティは読み取り専用プロパティです。つまり、Lazy<T> オブジェクトは、いったん初期化されると、常に型 T の同じインスタンス (T が値型の場合は同じ値) を返すということです。インスタンスに変更を加えることはできませんが、インスタンスのすべてのパブリック プロパティにアクセスできます。
アドホック パラメーターを T 型に渡すように Lazy<T> オブジェクトを構成する方法を以下に示します。
temp = new Lazy<DataContainer>(() => new Orders(10));
Lazy<T> コンストラクターの 1 つは、T コンストラクターへの適切な入力データを生成するために必要な処理を指定するのに使用できるデリゲートを受け取ります。デリゲートは、ラップされた T 型の Value プロパティへの初回アクセスが行われるまで実行されません。
スレッド セーフな初期化
既定では、Lazy<T> はスレッド セーフです。つまり、複数のスレッドが 1 つのオブジェクトにアクセスでき、すべてのスレッドは T 型の同じインスタンスを受け取るということです。Lazy オブジェクトへの初回アクセス時にのみ重要となる、スレッドに関する側面を見てみましょう。
Lazy<T> オブジェクトに最初にアクセスしたスレッドは、型 T のインスタンス作成プロセスをトリガーします。その後に Value にアクセスするスレッドはすべて、最初のスレッドによって生成された応答 (それがどのようなものであれ) を受け取ります。つまり、最初のスレッドが型 T のコンストラクターを呼び出したときに例外が発生した場合は、(スレッドに関係なく) その後のすべての呼び出しは、それと同じ例外を受け取ります。
仕様により、それぞれのスレッドが Lazy<T> の同じインスタンスから異なる応答を受け取ることはできません。これが、Lazy<T> の既定のコンストラクターを選択した場合の動作です。
ただし、以下に示すように、Lazy<T> クラスには他にもコンストラクターが用意されています。
public Lazy(bool isThreadSafe)
ブール型の引数は、スレッド セーフにする必要があるかどうかを示します。前述のとおり、既定値は true です。true が指定されている場合、前述のような動作になります。
false を渡した場合、Value プロパティは、たった 1 つのスレッド (Lazy 型を初期化するスレッド) からのみアクセスされます。複数のスレッドが Value プロパティへのアクセスを試みた場合の動作は不確定です。
ブール値を受け取る Lazy<T> コンストラクターは、より一般的なシグネチャ (Lazy<T> コンストラクターに LazyThreadSafetyMode 列挙体の値を渡します) の特殊なケースです。図 1 では、この列挙体のそれぞれの値の役割について説明しています。
図 1 LazyThreadSafetyMode 列挙体
値 | 説明 |
None | Lazy<T> インスタンスは非スレッド セーフで、複数のスレッドからアクセスされた場合の Lazy<T> インスタンスの動作は不確定です。 |
PublicationOnly | 複数のスレッドが同時に Lazy 型の初期化を試みることができます。最初に処理を完了したスレッドが初期化に成功し、他のすべてのスレッドによって生成された結果は破棄されます。 |
ExecutionAndPublication | 1 つのスレッドだけがスレッド セーフな方法で Lazy<T> インスタンスを初期化できるように、ロックが使用されます。 |
次のコンストラクターのいずれかを使用して、PublicationOnly モードを設定することができます。
public Lazy(LazyThreadSafetyMode mode)
public Lazy<T>(Func<T>, LazyThreadSafetyMode mode)
図 1 の値のうち、PublicationOnly 以外のものは、ブール値を受け取るコンストラクターを使用すると暗黙のうちに設定されます (次のコード参照)。
public Lazy(bool isThreadSafe)
このコンストラクターで、引数 isThreadSafe が false の場合、選択されるスレッド モードは None です。引数 isThreadSafe が true に設定されている場合、スレッド モードは ExecutionAndPublication に設定されます。ExecutionAndPublication は、既定のコンストラクターを選択した場合に使用されるモードでもあります。
PublicationOnly モードは、ExecutionAndPublication によって保証される完全なスレッド セーフと None による非スレッド セーフとの中間に位置するモードです。PublicationOnly では、同時実行スレッドが型 T のインスタンスの作成を試みることはできますが、実際にインスタンスを作成できるスレッドは 1 つだけです。その 1 つのスレッドによって作成された T インスタンスは、その後、(各スレッドによって計算されたインスタンスに関係なく) 他のすべてのスレッドで共有されます。
None と PublicationOnly には、初期化中にスローされる可能性のある例外に関して、興味深い違いがあります。PublicationOnly が設定されている場合、初期化中に生成された例外はキャッシュされません。その後、Value の読み取りを試みる各スレッドは、T のインスタンスを使用できない場合、再初期化を行うことができます。PublicationOnly と None のもう 1 つの違いは、T のコンストラクターが Value への再帰的なアクセスを試みた場合、PublicationOnly モードでは例外はスローされないことです。Lazy<T> クラスが None モードまたは ExecutionAndPublication モードで機能している場合、このような状況では InvalidOperation 例外が発生します。
非スレッド セーフにすると生のパフォーマンスは向上しますが、厄介なバグや競合状態が発生しないように注意する必要があります。したがって、LazyThreadSafetyMode.None オプションは、パフォーマンスが非常に重要な場合にのみ使用することをお勧めします。
LazyThreadSafetyMode.None を使用する場合は、ご自身の責任において、Lazy<T> インスタンスが複数のスレッドから初期化されないようにする必要があります。そうしないと、予期しない結果を招くことがあります。初期化中に例外がスローされた場合、その例外はキャッシュされ、同じスレッド内でのその後の Value へのアクセスでは、毎回それと同じ例外が発生するようになります。
ThreadLocal の初期化
Lazy<T> を使用した場合、仕様により、それぞれのスレッドが型 T の独自のインスタンスを管理することはできません。しかし、この動作を可能にする必要がある場合は、別のクラス (ThreadLocal<T> 型) を選択する必要があります。使用方法を次に示します。
var counter = new ThreadLocal<Int32>(() => 1);
コンストラクターはデリゲートを受け取り、それをスレッドローカル変数の初期化に使用します。各スレッドは独自のデータを保持し、他のスレッドはそのデータにはアクセスできません。Lazy<T> と違って、ThreadLocal<T> の Value プロパティは読み書き可能です。したがって、各アクセスはその次のアクセスから独立しており、異なる結果をもたらす可能性があります (例外をスローするかどうかを含む)。ThreadLocal<T> コンストラクターを通じてアクション デリゲートを提供しない場合、埋め込みオブジェクトは型の既定値 (T がクラスの場合は null) を使用して初期化されます。
遅延プロパティを実装する
ほとんどの場合、Lazy<T> は、独自のクラス内のプロパティに使用しますが、具体的にどのクラスのプロパティに使用すればよいのでしょうか。ORM ツールは遅延読み込みを独自に提供するので、こうしたツールを使用している場合、おそらく、アプリケーションのデータ アクセス層にあるクラスは、遅延プロパティの実装先としてふさわしくないでしょう。ORM ツールを使用していない場合、間違いなく、データ アクセス層は遅延プロパティの実装先として適しています。
依存関係の注入が使用されるアプリケーション セグメントも、遅延の実装先として適していることがあります。.NET Framework 4 では、Managed Extensibility Framework (MEF) は Lazy<T> を使用して拡張性および制御の反転を実装します。MEF を直接使用しない場合でも、依存関係の管理には遅延プロパティが非常に適しています。
図 2 からわかるように、クラス内に遅延プロパティを実装するのはまったく難しいことではありません。
図 2 遅延プロパティの例
public class Customer
{
private readonly Lazy<IList<Order>> orders;
public Customer(String id)
{
orders = new Lazy<IList<Order>>( () =>
{
return new List<Order>();
}
);
}
public IList<Order> Orders
{
get
{
// Orders is created on first access
return orders.Value;
}
}
}
穴を埋める
要約すると、遅延読み込みとは、本当に必要なときにだけデータを読み込むことを指す抽象的な概念です。.NET Framework 4 が登場するまでは、開発者は自分で遅延初期化ロジックを作り上げる必要がありました。Lazy<T> クラスは .NET Framework プログラミング ツールキットを拡張します。Lazy<T> クラスは、コストの高いオブジェクトを厳密に必要なときにだけ、なおかつ使用される直前にインスタンスを作成することで、無駄な計算を避ける大きなチャンスを提供します。
Dino Esposito は『Programming ASP.NET MVC』(Microsoft Press) の著者であり、『Microsoft .NET: Architecting Applications for the Enterprise』(Microsoft Press、2008 年) の共著者でもあります。Esposito はイタリアに在住し、世界各国で開催される業界のイベントで頻繁に講演しています。ブログは weblogs.asp.net/despos で読むことができます。
この記事のレビューに協力してくれた技術スタッフの Greg Paperin に心より感謝いたします。