delegateキーワードにより宣言したデリゲートやライブラリで提供されるデリゲートは暗黙的にMulticastDelegateクラスを継承していて、デリゲートの基本的な機能はその基底クラスであるDelegateクラスにより提供されます。 ここではDelegateクラスの持ついくつかのメソッド・メンバとデリゲートの機能について見ていきます。
マルチキャストとデリゲートの連結・削除 (Combine, Remove)
イベントのマルチキャストで解説したとおり、デリゲートには一つのデリゲートで複数のメソッドを呼び出すことが出来るようになっています。 これは、一つのデリゲートを別のデリゲートと連結することによりマルチキャストするデリゲートを作成することで実現されます。
デリゲートの連結にはCombineメソッド、デリゲートからのメソッドの削除にはRemoveメソッドもしくはRemoveAllメソッドを使います。 なお、Delegateクラスは不変クラスです。 そのため、これらのメソッドは静的メソッドとして存在し、個々のデリゲートインスタンスへの変更は行われません。 メソッドを呼び出すと、連結・削除を行った結果を含む新たなインスタンスを返すようになっています。 また、これらのメソッドの戻り値の型はSystem.Delegateとなるため、連結・削除を行ったデリゲートの呼び出しを行う場合は適切な型にキャストしなおす必要があります。
以下はCombineメソッドとRemoveメソッドを使ってデリゲートの連結と削除を行う例です。
using System;
class Sample {
static void Main()
{
Action a = Print1;
// デリゲートの連結
a = Delegate.Combine(a, new Action(Print2)) as Action;
a = Delegate.Combine(a, new Action(Print3)) as Action;
// 連結したデリゲートの呼び出し
a();
// デリゲートの削除
a = Delegate.Remove(a, new Action(Print2)) as Action;
a();
}
static void Print1()
{
Console.Write("Hello, ");
}
static void Print2()
{
Console.Write("world!");
}
static void Print3()
{
Console.WriteLine();
}
}
Hello, world! Hello,
なお、C#ではイベントの場合と同様に+=演算子と-=演算子を使ってデリゲートの連結・削除を行うことが出来るようになっています。
using System;
class Sample {
static void Main()
{
Action a = Print1;
// デリゲートの連結
a += Print2;
a += Print3;
// 連結したデリゲートの呼び出し
a();
// デリゲートの削除
a -= Print2;
a();
}
static void Print1()
{
Console.Write("Hello, ");
}
static void Print2()
{
Console.Write("world!");
}
static void Print3()
{
Console.WriteLine();
}
}
連結されたデリゲートの呼び出しリストに同一のメソッドが複数存在する場合、Removeメソッドはそのうちの最後にあるものを削除します。 RemoveAllメソッドでは、一致する全てのメソッド呼び出しを削除します。
using System;
class Sample {
static void Main()
{
Action a = Print1;
// デリゲートを連結して同一のメソッドを複数回呼び出すデリゲートを作成する
a = Delegate.Combine(a, new Action(Print1)) as Action;
a = Delegate.Combine(a, new Action(Print1)) as Action;
a = Delegate.Combine(a, new Action(Print1)) as Action;
a = Delegate.Combine(a, new Action(Print2)) as Action;
a = Delegate.Combine(a, new Action(Print3)) as Action;
a();
// 連結したデリゲートの呼び出しリストにある最後のPrint1メソッドの呼び出しを削除
Console.WriteLine("[Remove]");
a = Delegate.Remove(a, new Action(Print1)) as Action;
a();
// 連結したデリゲートの呼び出しリストにあるすべてのPrint1メソッドの呼び出しを削除
Console.WriteLine("[RemoveAll]");
a = Delegate.RemoveAll(a, new Action(Print1)) as Action;
a();
}
static void Print1()
{
Console.Write("Hello, ");
}
static void Print2()
{
Console.Write("world!");
}
static void Print3()
{
Console.WriteLine();
}
}
Hello, Hello, Hello, Hello, world! [Remove] Hello, Hello, Hello, world! [RemoveAll] world!
Removeメソッド・RemoveAllメソッドによって削除を行った結果としてデリゲートの呼び出しリストが空になる場合、戻り値はnull/Nothingとなります。 従って、Remove・RemoveAllメソッドを使って呼び出しリストが空のデリゲートを作成することは出来ません。 デリゲートは常に1つ以上のメソッドを呼び出しリストに含みます。
using System;
class Sample {
static void Main()
{
Action a = Print1;
a = Delegate.Combine(a, new Action(Print2)) as Action;
a = Delegate.Combine(a, new Action(Print3)) as Action;
// デリゲートの削除
Delegate d = a;
d = Delegate.Remove(d, new Action(Print1));
Console.WriteLine("d == null : {0}", d == null);
d = Delegate.Remove(d, new Action(Print2));
Console.WriteLine("d == null : {0}", d == null);
d = Delegate.Remove(d, new Action(Print3));
Console.WriteLine("d == null : {0}", d == null);
}
static void Print1()
{
Console.Write("Hello, ");
}
static void Print2()
{
Console.Write("world!");
}
static void Print3()
{
Console.WriteLine();
}
}
d == null : False d == null : False d == null : True
呼び出されるメソッド・インスタンスの取得 (Method, Target, GetInvocationList)
デリゲートのMethodプロパティを参照することでデリゲートに指定されているメソッドのMethodInfoを取得することが出来ます。 また、Targetプロパティを参照することで、呼び出される対象のインスタンスを取得することが出来ます。
using System;
class ConsolePrint {
private int id;
private string message;
public ConsolePrint(int id, string message)
{
this.id = id;
this.message = message;
}
public void Print()
{
Console.WriteLine(message);
}
public override string ToString()
{
return string.Format("{0}(id = {1})", GetType().Name, id);
}
}
class Sample {
static void Main()
{
ConsolePrint p1 = new ConsolePrint(1, "Hello,");
ConsolePrint p2 = new ConsolePrint(2, "world!");
Action a1 = p1.Print;
Action a2 = p2.Print;
Console.WriteLine("a1: {0}; {1}", a1.Target, a1.Method);
a1();
Console.WriteLine("a2: {0}; {1}", a2.Target, a2.Method);
a2();
}
}
a1: ConsolePrint(id = 1); Void Print() Hello, a2: ConsolePrint(id = 2); Void Print() world!
なお、静的(共有)メソッドの場合はTargetプロパティがnull(Nothing)になります。 MethodInfo.IsStaticプロパティを参照することでメソッドが静的かどうか調べることが出来ます。
using System;
class Sample {
static void Main()
{
Action a = Print;
Console.WriteLine("a.Target: {0}", (a.Target == null ? "null" : a.Target.ToString()));
Console.WriteLine("a.Method: {0} (IsStatic={1})", a.Method, a.Method.IsStatic);
a();
}
static void Print()
{
Console.WriteLine("Hello, world!");
}
}
a.Target: null a.Method: Void Print() (IsStatic=True) Hello, world!
また、連結されたデリゲートの場合、MethodプロパティとTargetプロパティはデリゲートの呼び出しリストにある一番最後のメソッドとインスタンスを返します。 呼び出しリストに含まれる個々のデリゲートを取得するにはGetInvocationListメソッドを使います。
using System;
class Sample {
static void Main()
{
Action a = Print1;
a += Print2;
a += Print3;
Console.WriteLine("a.Method: {0}", a.Method);
Console.WriteLine("[GetInvocationList]");
foreach (Delegate d in a.GetInvocationList()) {
Console.WriteLine(d.Method);
}
a();
}
static void Print1()
{
Console.Write("Hello, ");
}
static void Print2()
{
Console.Write("world!");
}
static void Print3()
{
Console.WriteLine();
}
}
a.Method: Void Print3() [GetInvocationList] Void Print1() Void Print2() Void Print3() Hello, world!
複製 (Clone)
デリゲートを複製するにはCloneメソッドが使えます。 このメソッドは、簡易コピーを作成します。
using System;
class Sample {
static void Main()
{
Action a1 = Print;
Action a2 = a1.Clone() as Action; // a1の複製を作成
Console.WriteLine("a1.Method: {0}", a1.Method);
Console.WriteLine("a2.Method: {0}", a2.Method);
a1();
a2();
}
static void Print()
{
Console.WriteLine("Hello, world!");
}
}
a1.Method: Void Print() a2.Method: Void Print() Hello, world! Hello, world!
複数のメソッドが指定されているデリゲートも同様に複製されます。
using System;
class Sample {
static void Main()
{
Action a1 = Print1;
a1 += Print2;
Action a2 = a1.Clone() as Action; // a1の複製を作成
Console.WriteLine("[a1.GetInvocationList]");
foreach (Delegate d in a1.GetInvocationList()) {
Console.WriteLine(d.Method);
}
Console.WriteLine("[a2.GetInvocationList]");
foreach (Delegate d in a2.GetInvocationList()) {
Console.WriteLine(d.Method);
}
a1();
a2();
}
static void Print1()
{
Console.Write("Hello, ");
}
static void Print2()
{
Console.WriteLine("world!");
}
}
[a1.GetInvocationList] Void Print1() Void Print2() [a2.GetInvocationList] Void Print1() Void Print2() Hello, world! Hello, world!
等価性の比較 (Equals, ==演算子, !=演算子)
二つのデリゲートの等価性を比較するには、Equalsメソッド、等価演算子==、不等価演算子!=(<>)が使えます。 デリゲート同士の比較では
- 二つのデリゲートの型が同じであり
- かつ、二つのデリゲートの呼び出しリストにある個々のメソッドが、すべて同じインスタンスの同じメソッドである場合
等しいと判断されます。 なお、.NET Framework 1.xではデリゲートの型が異なっていても呼び出しリストの内容が等しい場合、二つのデリゲートは等しいと判断されます。 また、等価演算子・不等価演算子では両辺のデリゲートが同じ型でないと比較出来ませんが、Equalsメソッドでは異なるデリゲート型やデリゲート以外の型でも比較することは出来ます(この場合、戻り値はfalseとなります)。
using System;
class ConsolePrint {
public void Print()
{
Console.WriteLine("Hello, world!");
}
}
delegate void DoPrint(); // Actionと同じシグネチャのデリゲート
class Sample {
static void Main()
{
Console.WriteLine(Environment.Version);
Console.WriteLine();
ConsolePrint p1 = new ConsolePrint();
ConsolePrint p2 = new ConsolePrint();
Action a1 = p1.Print;
Action a2 = p2.Print;
Action a3 = p1.Print;
DoPrint d1 = p1.Print;
Console.WriteLine("a1 == a2 : {0}", a1 == a2);
Console.WriteLine("a1 == a3 : {0}", a1 == a3);
Console.WriteLine("a1.Equals(a2): {0}", a1.Equals(a2));
Console.WriteLine("a1.Equals(a3): {0}", a1.Equals(a3));
Console.WriteLine("a1.Equals(d1) : {0}", a1.Equals(d1));
}
}
2.0.50727.1433 1 == a2 : False a1 == a3 : True a1.Equals(a2): False a1.Equals(a3): True a1.Equals(d1) : False
メソッドの非同期呼び出し (BeginInvoke, EndInvoke)
デリゲートを使ってメソッドを呼び出す場合、特殊なメソッドであるBeginInvokeメソッドとEndInvokeメソッドを使うことで非同期的にメソッドを呼び出すことが出来ます。
BeginInvokeメソッドは、呼び出すと非同期的にメソッドの実行を開始したのち、すぐにIAsyncResultを返します。 このインターフェイスには、非同期的に実行したメソッドの処理が完了しているかどうかを知るためのプロパティIsCompletedや、メソッドの終了を待機するための待機ハンドルを参照するプロパティAsyncWaitHandleなどが用意されています。 BeginInvokeメソッドが返すIAsyncResultを参照することにより、非同期操作の完了を待機することができます。
WaitHandleについてはSystem.Threading.WaitHandleで解説しています。
非同期呼び出しはEndIvokeメソッドを呼び出すことで完了します。 EndInvokeメソッドは引数にIAsyncResultを取るようになっていて、非同期呼び出しの開始時にBeginInvokeメソッドが返すIAsyncResultを渡します。 EndInvokeメソッドによって非同期呼び出しが完了すると、非同期呼び出ししたメソッドの戻り値がEndInvokeメソッドの戻り値として返されます。 EndInvokeメソッドはメソッドの処理が終わっていない場合でも呼び出すことが出来、その場合は処理が完了するまで待機してから結果を返します。
以下は、結果を計算するのに時間がかかる重い処理を行うメソッドComputeを用意し、デリゲートを使って非同期的に呼び出す例です。 また、呼び出し側では処理が完了するまでIAsyncResult.AsyncWaitHandleを使って待機するようにしています。
using System;
using System.Threading;
class Sample {
static void Main()
{
// 一つのstring型の引数を取り、int型の値を返すメソッドのデリゲートを作成
Func<string, int> p = Compute;
// デリゲートを使ってメソッドの非同期呼び出しを開始する
IAsyncResult ar = p.BeginInvoke("answer to life the universe and everything", null, null);
// 最大0.5秒待機して、メソッドの処理が完了していなければピリオドを表示して待機を続ける
while (!ar.AsyncWaitHandle.WaitOne(TimeSpan.FromSeconds(0.5), false)) {
Console.Write(".");
}
// メソッドの処理が完了した場合は、非同期呼び出しを終了し、メソッドの戻り値を取得する
int ret = p.EndInvoke(ar);
// 戻り値を表示
Console.WriteLine(ret);
}
// 「重い」処理を行うメソッド
static int Compute(string question)
{
Thread.Sleep(TimeSpan.FromSeconds(7.5));
return question.Length;
}
}
..............42
比較のために、デリゲートを使った通常の(同期的な)呼び出しを行うコードを記述すると次のようになります。
using System;
using System.Threading;
class Sample {
static void Main()
{
// 一つのstring型の引数を取り、int型の値を返すメソッドのデリゲートを作成
Func<string, int> p = Compute;
// デリゲートを使ってメソッドの同期的な呼び出しを開始する
int ret = p("answer to life the universe and everything");
// 戻り値を表示
Console.WriteLine(ret);
}
// 「重い」処理を行うメソッド
static int Compute(string question)
{
Thread.Sleep(TimeSpan.FromSeconds(7.5));
return question.Length;
}
}
BeginInvokeメソッドは、通常のデリゲート呼び出しの場合に加えてさらに二つの引数を要求します。 例えば、引数の無いActionデリゲートでは、BeginInvokeを呼び出す場合2の引数を指定する必要があります。 同様に引数が1つあるActionデリゲートでは、3つの引数を指定してBeginInvokeを呼び出します。
デリゲートの引数と、対応するBeginInvokeメソッドのシグネチャは次のようになります。
デリゲートのシグネチャ | BeginInvokeのシグネチャ |
---|---|
void Action() | IAsyncResult BeginInvoke(AsyncCallback, object) |
void Action(int) | IAsyncResult BeginInvoke(int, AsyncCallback, object) |
void Action(string, string) | IAsyncResult BeginInvoke(string, string, AsyncCallback, object) |
int Func(int) | IAsyncResult BeginInvoke(int, AsyncCallback, object) |
void EventHandler(object, EventArgs) | IAsyncResult BeginInvoke(object, EventArgs, AsyncCallback, object) |
このように要求される二つの引数は、非同期呼び出しが終了した際に呼び出されるコールバックとパラメータを指定する際に使用されます。 コールバックが不要な場合は、これら二つの引数にnull/Nothingを指定することができます。
EndInvokeメソッドは常に1つの引数IAsyncResultを要求しますが、戻り値はBeginInvokeメソッドと同様にデリゲートのシグネチャによって変わります。
デリゲートのシグネチャ | EndInvokeのシグネチャ |
---|---|
void Action() | void EndInvoke(IAsyncResult) |
void Action(string, string) | void EndInvoke(IAsyncResult) |
int Func() | int EndInvoke(IAsyncResult) |
double Func(int) | double EndInvoke(IAsyncResult) |
非同期呼び出し完了のコールバック
BeginInvokeメソッドの最後の二つの引数には、非同期呼び出しが終了した際に呼び出されるコールバックメソッドを指定するデリゲートAsyncCallbackと、コールバックメソッドに渡す任意の引数を指定出来ます。 この引数は、AsyncCallbackに渡されるIAsyncResultのAsyncStateプロパティで参照出来ます。
次の例は、上記の例をコールバックメソッドを使うように書き換えたものです。 この例では、非同期呼び出しによって処理を開始した後、完了まで待機せずに続けて別の処理を平行して行っています。 また、EndInvokeの呼び出しと戻り値の取得・表示はコールバックメソッドの側で行っています。 この例では、非同期呼び出しを開始したデリゲートを取得するために、コールバックメソッドに渡されたIAsyncResultをAsyncResultクラス (System.Runtime.Remoting.Messaging)にキャストし、AsyncDelegateプロパティを参照しています。
using System;
using System.Runtime.Remoting.Messaging;
using System.Threading;
class Sample {
static void Main()
{
Func<string, int> p = Compute;
// 非同期呼び出しを開始し、完了した際にCallbackメソッドを呼び出すようにする
p.BeginInvoke("answer to life the universe and everything", Callback, null);
// 以降、呼び出し側では他の処理を続ける
for (int i = 0; i < 10; i++) {
Console.Write("process #{0}: ", i);
Thread.Sleep(TimeSpan.FromSeconds(1.0));
Console.WriteLine("done");
}
}
// 非同期呼び出しが完了した際にコールバックされるメソッド
static void Callback(IAsyncResult asyncResult)
{
// IAsyncResultをAsyncResultに変換
AsyncResult ar = asyncResult as AsyncResult;
// 非同期呼び出しを行ったデリゲートを取得
Func<string, int> func = ar.AsyncDelegate as Func<string, int>;
// 非同期呼び出しを完了し、メソッドの戻り値を取得する
int ret = func.EndInvoke(ar);
// 戻り値を表示
Console.WriteLine("result: {0}", ret);
}
// 「重い」処理を行うメソッド
static int Compute(string question)
{
Thread.Sleep(TimeSpan.FromSeconds(7.5));
return question.Length;
}
}
process #0: done process #1: done process #2: done process #3: done process #4: done process #5: done process #6: done process #7: result: 42 done process #8: done process #9: done
非同期処理に関するドキュメント
非同期処理およびBeginInvoke, EndInvoke, IAsyncResultについてより詳しく知るためには、以下のドキュメントを参照してください。
また、.NET Framework 4以降では、System.Threading.Tasks名前空間のクラス群を使うことで非同期処理をより簡単に実装することができるようになっています。 詳しくは.NET Framework の並列プログラミングなどを参照してください。