yield文でイベントを待機する
概要
現在のプログラム言語にあるyield文の性質を使うことで、コードの実行を途中で待機するwait文の機能を擬似的に模擬する、といったアイディアです。別ページ「wait文を導入してコードを止める」の中で記載したwait文のアイディアをC#で擬似的に実現したものです。これによって、上記ページに記載したようないろいろな場面で、コードが格段に書きやすくなります。
背景
何とかwait文を模擬するには
別ページ「wait文を導入してコードを止める」の中で、プログラム言語に構文「wait イベント名」を新しく導入して、イベントが発生するまでwait文を書いた行でコードの実行を一時停止して待機するアイディアについて記載しました。この構文「wait イベント名」はアイディアの1つで、まだ実現されていないので、実際に現在のプログラム言語では利用できません。
しかし、現在のプログラム言語でも何とかwait文のような機能が使えれば、いろいろな場面でコードが格段に書きやすくなります。そこで、wait文の機能を現在の言語で擬似的に実現するにはどうすればよいかを考えました。すると、既にいくつかの言語に備わっているyield文の性質を使えばwait文の機能が模擬できる、といったアイディアに至りました。
どんなもの?
yield文でwait文を模擬する
現在のプログラム言語にあるyield文の性質を使うことで、コードの実行を途中で一時停止して待機するためのwait文の機能を擬似的に模擬する、といったアイディアです。wait文はまだ現在の言語では利用できないので、本記事ではyield文の性質を使って模擬します。yield文は関数の途中で実行を止める性質があるので、これを利用してwait文の機能を実現します。
はじめにwait文とyield文
まずはじめにwait文とyield文について、簡単に紹介します。その後、この2つを見比べながら、wait文の機能をyield文で模擬するにはyield文をどのように使えばよいかを考えます。
wait文とは、別ページ「wait文を導入してコードを止める」の中で提案したとおり、wait文を書いた行でコードの実行を一時停止して待機する、といったアイディアです。例えば、次のようにコードの中で「wait event;」とwait文を書くと、このコードの動作は、処理Aを実行してから、「wait event;」の部分でeventが発生するまで待機し、発生すれば次の処理Bを実行します。
...処理A...
wait event;
...処理B...
|
wait文を使ってコードの実行を一時停止して待機する |
次にyield文は、必ず関数の中で使います。例えば、次のように[X]の関数の中でyield returnと書きます。このコードの動作は、[X]の関数を呼び出すと、まず処理Aが実行されます。次に[Y]のyield文に来ると、関数の実行が終了します。yield returnは通常のreturnと同じように、この行で実行が終了します。次に、もう一度[X]の関数を呼び出すと、今度は[Y]の次の行から実行が再開します。そのため処理Bが実行されて、関数の実行が完了します。
function ... // ←[X]
...処理A...
yield return 戻り値; // ←[Y]
...処理B...
|
yield文を使ったコードの例 |
このようにyield文は、実行の流れだけに注目すると、yield文の行で実行を止めて、次回その続きから実行を再開する動作をします。このようなyield文の実行を一旦止める動作をうまく利用して、wait文の実行を一時停止して待機する動作を模擬的に実現できないかについて以降で考えます。
yield文をどのように使えばよいか
もっと具体的にコードを考えるために、前述の例をボタンのクリックイベントを待つ例に書き直します。次のように、ボタンbutton1のClickイベントを待機するC#のコードに書き換えて、関数の中に入れます。
void TestFunc() {
...処理A...
wait button1.Click;
...処理B...
}
|
wait文を使ってボタンのクリックイベントを待機する |
通常、イベントの発生を待つには、イベントにイベントハンドラを登録して、そのハンドラが呼ばれるのを待ちます。上の例でも、wait文のところでbutton1.Clickにイベントハンドラを登録して、そこで一旦TestFuncの関数を終了し、登録したハンドラが呼ばれたら、TestFunc関数の先ほど終了した途中の行から実行を再開すれば、wait文でイベントを待機することが出来ます。
このように考えると、wait文のところで関数の実行を一旦止めたいので、wait文をyield文に書き換えて、途中で実行を止めるyield文の性質を利用します。コードをyield文に書き換えると、C#ではyield文を含む関数はIEnumerable型を返す必要があるので、コードは次のような形になります。まだ決めていない部分は[未定]と書いています。
IEnumerable TestFunc([未定]) { // ←[X]
...処理A...
yield return [未定]; // ←[Y]
...処理B...
}
|
wait文の部分をyield文に置き換える |
次に、yield文のところでbutton1.Clickにイベントハンドラを登録するコードを[Z]のように追加します。ハンドラの中身はまだ未定です。
IEnumerable TestFunc([未定]) { // ←[X]
...処理A...
button1.Click += (_sender, _e) => { [未定] }; // ←[Z]
yield return [未定]; // ←[Y]
...処理B...
}
|
yield文のところでイベントハンドラを登録する |
ここでwait文に対応するコードが[Y]と[Z]の2行に渡ってしまうので、これらを1つにまとめて次の[A]のように関数の中に入れます。この関数はwait文に対応するということで、Waitと名付けます。Waitの引数にはイベントハンドラの登録に必要なbutton1とClickを含む情報を渡して、Waitの中でハンドラを登録します。これによって、イベントを待機したい行で[Y]のように「yield return Wait(...)」と1行を書けば、簡単にイベントが待機できるようになり、コードが書きやすくなります。
IEnumerable TestFunc([未定]) { // ←[X]
...処理A...
yield return Wait([button1.Clickを含む情報]); // ←[Y]
...処理B...
}
public static [未定] Wait([button1.Clickを含む情報]) { // ←[A]
button1.Click += (_sender, _e) => { [未定] }; // ←[B]
}
|
Wait関数で1つにまとめる |
Waitの引数にはイベントハンドラの登録に必要なbutton1とClickを含む情報が必要ですが、C#ではbutton1.Clickそのものを変数として渡すことが出来ません。そのため、[B]にある「button1.Click += ...」とは別の方法でイベントハンドラを登録しなくてはいけません。そこで、ここではReflectionの機能を利用します。Reflectionの機能を利用してイベントハンドラを登録するには、[B]のコードが次のような形になります。
EventHandler handler = (_sender, _e) => { [未定] }; // ←[C]
button1.GetType().GetEvent("Click").GetAddMethod()
.Invoke(button1, new[] { handler }); // ←[D]
|
Reflectionの機能を利用してイベントハンドラを登録する |
[C]でイベントハンドラとするラムダ式を一度handlerに代入します。[D]でClickのイベント登録関数GetAddMethodを取得してInvokeにより実行し、button1のClickにhandlerを登録します。Reflectionの機能を利用してイベントハンドラを登録すると、button1の変数と"Click"文字列が必要になります。そのため、Waitの引数はobject型のinstanceと文字列型のevent_nameとします。すると、コードは次のようになります。
IEnumerable TestFunc([未定]) { // ←[X]
...処理A...
yield return Wait(button1, "Click"); // ←[Y]
...処理B...
}
public static [未定] Wait(object instance, string event_name) { // ←[A]
EventHandler handler = (_sender, _e) => { [未定] }; // ←[C]
instance.GetType().GetEvent(event_name).GetAddMethod()
.Invoke(instance, new[] { handler }); // ←[D]
}
|
Reflectionの機能を利用したコード |
このコードの動作は、[X]でTestFuncが呼ばれると、処理Aを実行して[Y]に到達し、[A]のWaitが呼ばれて、[C]と[D]でイベントハンドラに登録し、[Y]に戻ってyield returnでTestFuncが一旦終了します。この後、button1.Clickイベントが発生したら、TestFuncを再度呼んで、[Y]の次の行から実行を再開したいので、 [C]のイベントハンドラの中身にTestFuncを呼び出すコードを追加します。
TestFunc関数は中でyield文を使ったので、通常の関数のように呼び出せません。TestFuncを呼び出すには、次のように[E]でTestFuncのEnumeratorを生成して、[F]のMoveNext()で呼び出します。
var etor = TestFunc([未定]).GetEnumerator(); // ←[E]
etor.MoveNext(); // ←[F]
|
Enumeratorを取得してTestFuncを呼び出す |
上のコードを[C]のイベントハンドラの中身に追加します。
IEnumerable TestFunc([未定]) { // ←[X]
...処理A...
yield return Wait(button1, "Click"); // ←[Y]
...処理B...
}
public static [未定] Wait(object instance, string event_name) { // ←[A]
EventHandler handler = (_sender, _e) => { // ←[C]
var etor = TestFunc([未定]).GetEnumerator(); // ←[E]
etor.MoveNext(); // ←[F]
};
instance.GetType().GetEvent(event_name).GetAddMethod()
.Invoke(instance, new[] { handler }); // ←[D]
}
|
TestFuncを呼び出すコードをイベントハンドラに追加 |
ここでetor変数が出てきました。etorはTestFunc関数のEnumeratorで、TestFuncの実行中は同じEnumeratorを使う必要があります。そのため、一度GetEnumerator()で生成したEnumeratorをどこかに保持しておき、次回からはこのEnumeratorを参照して使うようにします。ここでEnumeratorを保持するために、新しいクラスを定義して[A]のWait関数をクラスの中に入れ、クラスのフィールド変数にEnumeratorを保持します。Waitをクラスに入れると、コードは次のようになります。
IEnumerable TestFunc([未定]) { // ←[X]
...処理A...
yield return [未定: EventWaiter型の変数].Wait(button1, "Click"); // ←[Y]
...処理B...
}
public class EventWaiter { // ←[G]
IEnumerator _Etor; // ←[H]
public EventWaiter() { // ←[I]
_Etor = TestFunc([未定]).GetEnumerator(); // ←[E]
}
public [未定] Wait(object instance, string event_name) { // ←[A]
EventHandler handler = (_sender, _e) => { // ←[C]
_Etor.MoveNext(); // ←[F]
};
instance.GetType().GetEvent(event_name).GetAddMethod()
.Invoke(instance, new[] { handler }); // ←[D]
}
}
|
Wait関数をクラスの中に入れる |
[G]でEventWaiterクラスを新しく定義して、[H]で_Etorフィールド変数にEnumeratorを保持します。[E]のEnumeratorを生成するコードは、このクラスを使う一番始めに一度だけ実行したいので、[I]のコンストラクタの中に入れてクラスを生成するときに実行します。
完成したコードの全体
これで、yield文を使ってイベントを待機する大枠のコードが出来ました。あとは、うまく動作するように細かいところを修正します。修正したコードは次のようになります。コードの中で修正した部分は青くしてあります。
IEnumerable TestFunc(EventWaiter waiter) { // ←[X]
...処理A...
yield return waiter.Wait(button1, "Click"); // ←[Y]
...処理B...
}
public class EventWaiter { // ←[G]
IEnumerator _Etor; // ←[H]
public EventWaiter(Func<EventWaiter, IEnumerable> func) { // ←[I]
_Etor = func(this).GetEnumerator(); // ←[E]
_Etor.MoveNext(); // ←[J]
}
public object Wait(object instance, string event_name) { // ←[A]
EventHandler handler = null;
(_sender, _e) => { // ←[C]
instance.GetType().GetEvent(event_name).GetRemoveMethod()
.Invoke(instance, new[] { handler }); // ←[K]
_Etor.MoveNext(); // ←[F]
};
instance.GetType().GetEvent(event_name).GetAddMethod()
.Invoke(instance, new[] { handler }); // ←[D]
return null; // ←[L]
}
}
void Form_Load(object sender, EventArgs e) { // ←[M]
new EventWaiter(TestFunc); // ←[N]
}
|
うまく動作するように細かいところを修正、完成したコード |
このコードの中で修正した部分は次の6つです。
1. |
まず、新しく定義したEventWaiterクラスを生成するために、[N]でクラスをnewします。一番始めにnewしたいので、[M]にあるFormのLoadイベントの中に[N]のコードを書きます。 |
2. |
[Y]でEventWaiterのWait関数を呼ぶために、[X]の引数をEventWaiter型のwaiterにして、[Y]でwaiterのWait関数を呼び出すようにします。そのため、[E]のEnumerator生成で引数に自身のクラスを表すthisを渡します。 |
3. |
[E]でTestFuncのみでなく任意の関数を指定できるように、[I]にあるコンストラクタの引数をFunc<EventWaiter, IEnumerable>型(EventWaiterを引数にしてIEnumerableを返す関数型)のfuncにして、[E]でfuncのEnumeratorを生成するようにします。そのため、[N]のクラス生成で引数にTestFuncを渡します。 |
4. |
[X]のTestFuncの実行を開始するために、EventWaiterクラスが実行される一番始めの部分[J]でTestFuncを呼び出して処理を開始します。TestFuncの呼び出しは[F]のときと同じようにMoveNext()で呼び出します。 |
5. |
[D]で登録したイベントハンドラは、登録したままだとイベントが発生する度に何度もハンドラが呼ばれてしまうので、[K]でイベントが発生した後すぐにイベントハンドラを削除して、ハンドラが1回だけ呼ばれるようにします。 |
6. |
[A]のWait関数は特に呼び出し側へ戻す情報がないので、[L]でnullを返すようにします。 |
このコードの動作を追うと、まずFormがロードされて[M]のForm_Loadイベントハンドラが呼ばれ、[N]でEventWaiterクラスをnewします。クラスをnewしたので[I]のコンストラクタが呼ばれ、[E]でTestFunc関数のEnumeratorを作成します。[J]でTestFuncを呼び出し、実行は[X]に飛びます。
処理Aを実行して、[Y]でWait関数を呼びます。Waitの中に入って、[C]でイベントハンドラの中身を定義し、[D]でハンドラをbutton1.Clickイベントに登録します。Wait関数が終了すると、[Y]に戻ってyield returnでTestFunc関数を一旦終了します。これでコードの実行は終了し、次にbutton1がクリックされるまで何も実行されず、待機の状態になります。
button1がクリックされると、button1.Clickイベントに登録した[C]のハンドラが呼ばれて、[K]で登録したハンドラを削除し、[F]でTestFuncを呼び出します。すると、yield文の性質から[Y]で一旦終了した次の行から実行が開始され、実行は[Y]の下の処理Bに飛びます。処理Bを実行して、TestFunc関数は終了し、すべてのコードの実行は終了します。
■流れ: wait文の最も簡単な例→yield文の簡単な説明→2つの性質の簡単な比較→ボタンの例に書き直し→waitをyieldに書き直し理想形を描く→それを満たすようにyield周りのコードを作ると...→汎化なしの全体コード完成形を記載
■yield文で関数の実行を一旦止めておいて、wait文のイベントが発生するのを待つ処理=イベントハンドラがコールバックで呼ばれる処理を別で行い、呼ばれたらyield文の関数を呼んで、止めておいた途中から実行を再開させるような仕組みを考える
ソースコードダウンロード
前述のコードにFormやbutton1を配置した完全なソースコードを次のリンクからダウンロードできます。Visual C# 2008のソリューション(*.sln)になります。不足のない完全なソースコードで、ビルドしてすぐ実行できます。
UseYieldForWait.zip [29KB]
上のzipファイルを展開すると複数のファイルが出来ます。その中でTestForm.csに前述のコードが含まれています。他のcsファイルは主要な動作には関係しません。ExceptionForm.csは例外を表示するウィンドウのためのコード、TextboxConsole.csはコンソール出力を表示するコントロールのためのコードです。
どこでも手軽にwait文を利用できる
このようにして、yield文を使ってwait文の機能を模擬できます。上のコードのEventWaiterクラスを再利用すれば、次のようなコードを書くだけで、どこでもwait文の機能を利用できます。
IEnumerable TestFunc(EventWaiter waiter) { // ←[X]
...処理A...
yield return waiter.Wait(button1, "Click"); // ←[Y]
...処理B...
}
void Form_Load(object sender, EventArgs e) { // ←[M]
new EventWaiter(TestFunc); // ←[N]
}
|
EventWaiterクラスを再利用してwait文の機能を実現 |
[X]のTestFunc関数の中身を自由に書き換えて、イベントを待機したい行で単純に「yield return waiter.Wait([変数名], [待つイベント名]);」と書けば、簡単にイベントを待機できます。
例えば、EventWaiterクラスを利用して、別ページ「wait文を導入してコードを止める」の中で挙げた「A.1. Timerを使ってグラデーションする点滅表示」の例を実装すると、次のようになります。上記リンクにあるコードの中のwait文を、単純に「yield return waiter.Wait([変数名], [待つイベント名]);」で置き換えただけです。
IEnumerable TestFunc(EventWaiter waiter) {
while (true) { // ←[B]
for (var bright = 0; bright < 255; bright += 10) { // ←[C]
label1.BackColor = Color.FromArgb( // ←[D]
bright, bright, bright);
yield return waiter.Wait(timer1, "Tick"); // ←[E]
}
for (var bright = 250; bright > 0; bright -= 10) { // ←[F]
label1.BackColor = Color.FromArgb( // ←[G]
bright, bright, bright);
yield return waiter.Wait(timer1, "Tick"); // ←[H]
}
}
}
void Form_Load(object sender, EventArgs e) {
new EventWaiter(TestFunc);
}
|
EventWaiterクラスを利用してグラデーションする点滅表示の例を実装 |
ソースコードダウンロード
前述の点滅表示のコードにlabel1やtimer1を配置した完全なソースコードを次のリンクからダウンロードできます。Visual C# 2008のソリューション(*.sln)になります。不足のない完全なソースコードで、ビルドしてすぐ実行できます。
LabelBlinking.zip [29KB]
機能の拡張
前述のEventWaiterクラスから、yield文を使えば現在のプログラム言語でもwait文の機能が実現できることが分かりました。ここではさらに、前述のEventWaiterクラスをベースに便利な機能を色々付け加えて拡張します。拡張した点をリストにまとめると、次のようになります。以降でそれぞれの項目について説明します。
- And/Orで結合して複数イベントを待機できるようにする
- Notの条件も結合できるようにする
- 指定回数発生したイベントを待機できるようにする
- 普通の条件文も結合できるようにする
- ユーザーが独自の条件を作成できるようにする
- 待機をキャンセルできるようにする
これらの拡張をすべて実装したソースコードをダウンロードできます。
1. And/Orで結合して複数イベントを待機できるようにする
前述のEventWaiterクラスでは、次のように1つのイベントしか待機できませんでした。
...中略...
yield return waiter.Wait(button1, "Click");
...中略...
|
前述のEventWaiterクラスで1つのイベントを待機する |
ここでは、And/Orの考えを新しく導入して、複数のイベントを待機できるように拡張します。例えば、次のように2つのイベントをAndで結合して、両方のボタンがクリックされるまで待機できます。
...中略...
yield return waiter.Wait(
new And(new Event(button1, "Click"), new Event(button2, "Click")));
...中略...
|
Andで結合して2つのイベントを待機する |
And結合ではイベントの発生順序に関係はありません。上のコードは、button1がクリックされてからbutton2がクリックされても、反対にbutton2がクリックされてからbutton1がクリックされても、両方とも待機が完了します。
上のコードは理想的にはwait文を使って「wait button1.Click and button2.Click;」と書きたいところですが、C#の言語でこれを実現するために、EventクラスとAndクラスを新しく導入し、Andクラスの中にEventクラスを2つ入れて、イベントをAnd結合します。Eventクラスは待機するイベントの情報を1つにまとめます。Andクラスは2つ以上のイベントをAnd結合して、結合したすべてのイベントが発生するまで待機します。
Or結合も同じように、Orクラスを導入して実現します。次のように2つのイベントをOrで結合して、どちらか片方のイベントが発生するまで待機できます。
...中略...
yield return waiter.Wait(
new Or(new Event(button1, "Click"), new Event(button2, "Click")));
...中略...
|
Orで結合して2つのイベントを待機する |
ここで導入したAnd/Or結合の拡張によって、次のようにさまざまなバリエーションの結合が可能になります。
...中略...
yield return waiter.Wait( // ←[A]
new And(new Event(button1, "Click"), new Event(button2, "Click"),
new Evant(button3, "Click")));
...中略...
yield return waiter.Wait( // ←[B]
new And(new Event(button1, "Click"),
new Or(new Event(button2, "Click"), new Evant(button3, "Click"))));
...中略...
|
さまざまなバリエーションの結合 |
[A]は「wait button1.Click and button2.Click and button3.Click」を表し、3つのイベントをAnd結合しています。このようにいくつでも続けて結合することが出来ます。[B]は「wait button1.Click and (button2.Click or button3.Click)」を表し、Or結合した条件をさらにAnd結合しています。このようにAnd/Orを複合させて結合することが出来ます。
2. Notの条件も結合できるようにする
ここでは、Notの考えを新しく導入して、イベントにNotを付けて待機できるように拡張します。例えば、次のようにイベントをNotの中に入れて、イベントが発生していない状態を表現できます。
...中略...
yield return waiter.Wait(new And(
new Not(new Event(button1, "Click")), new Event(button2, "Click")));
...中略...
|
イベントにNotを付けて待機する |
上のコードは、button1がクリックされずに、button2がクリックされた場合に待機が完了します。言い換えると、button1がクリックされていない状態で、且つ、button2がクリックされた状態になった場合、待機を終了してコードの実行を再開します。
上のコードは理想的にはwait文を使って「wait (not button1.Click) and button2.Click;」と書きたいところですが、C#の言語でこれを実現するために、Notクラスを新しく導入し、Notクラスの中にEventクラスを入れて、イベントにNotを付加します。
NotクラスをAnd/Orクラスと組み合わせて使って、複雑な条件を作ることが出来ます。次のようにいろいろなバリエーションが考えられます。
...中略...
yield return waiter.Wait(new Not(new Event(button1, "Click"))); // ←[A]
...中略...
yield return waiter.Wait(new And( // ←[B]
new Not(new Event(button1, "Click")), new Event(button1, "Click")));
...中略...
|
Notクラスを使ったいろいろなバリエーション |
[A]は「wait (not button1.Click)」を表し、button1がクリックされていない場合に待機が完了します。これはつまり、待機を開始した瞬間はまだ何もクリックされていないので、[A]ではすぐに待機が完了します。[A]は何も待機しない意味のないコードになります。
[B]は「wait (not button1.Click) and button1.Click」を表し、button1がクリックされずに、button1がクリックされた場合に待機が完了します。button1をクリックせずにbutton1をクリックすることは出来ないので、永遠に条件が合わず、[A]では無限に待機することになります。
3. 指定回数発生したイベントを待機できるようにする
前述のEventWaiterクラスでは、イベントが一度発生する場合しか待機できませんでした。ここでは、イベントを複数回待機できるように拡張します。例えば、次のようにボタンが3回クリックされるまで待機できます。
...中略...
yield return waiter.Wait(new Count(new Event(button1, "Click"), 3));
...中略...
|
ボタンが3回クリックされるまで待機する |
イベントの発生を複数回待機するCountクラスを新しく導入して、「new Count(イベント, 回数)」の形で待機するイベントと回数を指定します。
CountクラスをAnd/Or/Notクラスと組み合わせて使って、次のように複雑な条件を作ることが出来ます。
...中略...
yield return waiter.Wait(new Count(
new Or(new Event(button1, "Click"), new Event(button2, "Click")), 3));
...中略...
|
Countクラスを使ったいろいろなバリエーション |
上のコードは「wait (button1.Click or button2.Click).Count(3)」を表し、button1とbutton2のどちらかが合わせて3回クリックされた場合に待機が完了します。
4. 普通の条件文も結合できるようにする
ここではイベント以外に、通常のコードで書くようなtrue/falseの条件文も、他のイベントと合わせて待機できるように拡張します。例えば、次のようにbutton1.Clickイベントを待機すると共に、button1.Textが"button1"であるかどうかをチェックできます。
...中略...
yield return waiter.Wait(
new Eval(new Event(button1, "Click"), () => button1.Text == "button1"));
...中略...
|
普通の条件文も他のイベントと結合して待機する |
上のコードは、button1がクリックされたときに、button1.Textの内容が"button1"であれば待機が完了します。button1.Text=="button1"のような普通の条件文を評価するために、Evalクラスを新しく導入して、Evalクラスの中に、合わせて待機するイベントと、条件文としてbool値を返すラムダ式を入れます。
5. ユーザーが独自の条件を作成できるようにする
これまでAnd/OrクラスやNotクラスなど、新しくクラスを導入していろいろな機能を拡張してきました。ここでは、ユーザーも独自のクラスを導入して、ユーザーが機能を追加できるように拡張します。独自の機能を追加するには、次のようにConditionクラスを継承した新しいクラスを作り、その中で必要な部分をオーバーライドして実装します。
public class UserExtention : Condition { // ←[A]
public UserExtention(...[任意]...) : base(...[任意]...) {
// todo: インスタンスを生成したときの動作を実装 // ←[B]
}
protected override void OnReset() {
// todo: リセットするときの動作を実装 // ←[C]
}
protected override bool OnUpdateEvaluation(ActionWrapper on_end_update) {
// todo: 評価値を更新するときの動作を実装 // ←[D]
}
}
|
Conditionクラスを継承して独自の機能を追加する |
[A]でクラスを作り、Conditionクラスを継承します。[B]でクラスのコンストラクタを定義して、インスタンスを生成したときの動作を実装します。[C]と[D]で継承元クラスの関数をオーバーライドして、必要な機能を実装します。
これまでのAnd/OrクラスやNotクラスなども、このConditionクラスを継承して実装しています。下のコードはAndクラスの実際のソースコードです。
public class And : Condition {
public EventContainer[] Containers { get { return _Containers; } }
public And(params EventContainer[] containers) : base(containers) { }
protected override bool OnUpdateEvaluation(ActionWrapper on_end_update) {
var is_raised = true;
this.Containers // ←[E]
.ForEach(c => is_raised &= c.UpdateEvaluation(on_end_update));
return is_raised;
}
}
|
Conditionクラスを使ったAndクラスの実装 |
Andクラスのメイン動作は[E]の部分になります。[E]のForEachで子階層の評価値をそれぞれ更新して(UpdateEvaluation)、すべての結果を「&=」の論理積(AND)で演算します。これによってイベントのAndを取ります。Andクラスの実装はたったこの10行だけです。
その他のクラスの実装の詳細は、ソースコードをダウンロードして参照してください。
6. 待機をキャンセルできるようにする
イベントを待機すると、しばしばイベントが発生せずにずっと待機したままになることがあります。そこで、任意のタイミングで待機をキャンセルできるように拡張します。例えば、次のように[B]でbutton1がクリックされるのを待機している間にbutton2をクリックすると、[E]のコードでこの待機をキャンセルできます。
IEnumerable<bool> TestFunc(EventWaiter waiter) { // ←[A]
...中略A...
yield return waiter.Wait(button1, "Click"); // ←[B]
...中略B...
}
EventWaiter _Waiter; // ←[C]
void Form_Load(object sender, EventArgs e) { // ←[D]
_Waiter = new EventWaiter(TestFunc); // ←[E]
}
void button2_Click(object sender, EventArgs e) { // ←[F]
_Waiter.CancelWait(); // ←[G]
}
|
任意のタイミングで待機をキャンセルする |
上のコードは、実行を開始するとフォームがロードされて[D]が呼ばれ、[E]でEventWaiterクラスをnewして、イベントの待機を開始します。このときのnewしたインスタンスは後で待機をキャンセルするために[C]の変数に保持しておきます。待機が開始するので[A]が呼ばれ、[B]に来てbutton1がクリックされるのを待機します。
ここでbutton1を押さずにbutton2をクリックすると、[F]が呼ばれ[G]で待機をキャンセルします。キャンセルすると、[B]で中断していた処理が再開されて、「...中略B...」のコードが実行されます。
ここでは待機をキャンセルするために、EventWaiterクラスに新しくCancelWait関数を追加して、待機の途中でこれを呼び出せば、待機をキャンセルすることが出来ます。
ソースコードダウンロード
前述の拡張をすべて実装したコードを次のリンクからダウンロードできます。Visual C# 2008のソリューション(*.sln)になります。不足のない完全なソースコードで、ビルドしてすぐ実行できます。
EventWaitingCore.zip [51KB]
上のzipファイルを展開すると複数のファイルが出来ます。その中でTestForm.csに、前述で拡張した機能のすべてのテストコードが含まれています。下にこのソースコードを実行したときのスクリーンショットを示します。
|
ソースコードを実行したときのスクリーンショット [拡大] |
テストアプリケーションには0から6までのテストボタンがあり、それぞれのボタンを押すとテストが実行されて右側に随時結果が表示されます。この0から6までのテストによって、前述で拡張したすべての機能をテストできます。
ソースコードの中のWaitCore.csにWait機能の実装がすべて含まれています。このWait機能を自身のコードで利用するには、このWaitCore.csをコピー&ペーストして自身のプロジェクトに追加し、Wait機能の名前空間WaitCoreを次のようにusingで定義します。
using SimonPG.WaitCore;
public partial class Form1 : Form {
...実装など...
}
|
Wait機能を自身のコードで利用する |
本ソースコードにあるテストコードを参考にして、色々な場面でWait機能をご活用ください。もし、アイディアやコメントなどがございましたら、メールにてお知らせお願いいたします。
|
「詳細テスト」 [全体] |
本テストアプリケーションにはその他に、Wait機能の持つすべての機能をもっと詳しくテストできる詳細テストが付いています。2つあるタブのうち「詳細テスト」のタブをクリックすると、右の図のようにテスト項目の一覧が表示されます。
詳細テストは、Wait機能を作った際に、動作を確認するため一緒に作ったテストです。テストでは、Wait機能がより複雑に使用された場合でも、機能が正しく動作するかを確認しています。この詳細テストのソースコードを見れば、Wait機能のより複雑な使用方法がご覧いただけます。
世の中の似た機能
yield文は関数の途中で実行を止めるものと考えて、いろいろなことに応用されています。Webを検索すると、JavaScriptのyield文を使って、繰り返し発生するタイマイベントをコードの途中で待機する例があります。一方、本記事ではyield文の性質を使って、すべてのイベントを統一的にコードの途中で待機できるようにします。
2007年11月にJeffrey Richter氏が、yield文を使って非同期プログラミングを簡単化する方法を提案しています。この中で同氏は、AsyncEnumeratorクラスを独自に作り、他のスレッドの終了を待機する例を説明しています。終了を待機するときにyield文を使うことで、コードの実行を一時停止して待機するものです。次に参照ページを示します。
参照ページ:
コードの実行を途中で止めるためにyield文を使う点において、同氏の提案する考え方は、本記事のアイディアとほぼ同じものになります。一方、同氏の提案では、他のスレッドの終了を待機するような非同期プログラミングに関する待機を対象にするのに対して、本記事のアイディアは、イベントの待機を対象にする点が大きく違います。
C# 5.0から新しくasyncとawaitの2つのキーワードが追加されます。このうち、await文の動作は、コードの実行を途中で止めるという点において、本記事のアイディアとほぼ同じものになります。一方、await文は非同期プログラミングに関する待機を対象にするのに対して、本記事のアイディアは、イベントの待機を対象にする点が大きく違います。
■yield文で関数の途中で実行が止まることを利用してタイマイベントに関するコード例が他にもWebにある
更新履歴 |
2010/04/26 |
|
2010/05/27 v1 |
|
2010/06/16 v2 |
|
2011/06/22 v3 |
- 「世の中の似た機能」にJeffrey Richter氏の提案するAsyncEnumeratorクラスとの比較を追加
- 「世の中の似た機能」にC# 5.0から新しく追加されるasyncとawaitキーワードとの比較を追加
|
|
※ご意見、ご感想、改善点、その他の情報などがありましたら、メールにてお知らせ願います。
|