- PR -

イベントハンドラーの中でさらにイベントを起こすと、再帰的に呼び出されてしまう

投稿者投稿内容
unibon
ぬし
会議室デビュー日: 2002/08/22
投稿数: 1532
お住まい・勤務地: 美人谷        良回答(20pt)
投稿日時: 2008-04-07 23:04
イベントハンドラーの中でイベントを起こすと、イベントハンドラーが再帰的に呼ばれてしまい、コールスタック(呼び出し履歴)がどんどん深くなっていきます。
これを避けたいのですがどうするのが巧みなコーディングなのでしょうか?

たとえば、つぎのようなコードを Console.WriteLine の行にブレークポイントをしかけてデバッガーで実行しボタン(button1)を押す、ブレークポイントを通るたびにコールスタックが深くなっているのが分かります。

コード:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

namespace HogeApp
{
    public partial class Form1 : Form
    {
        private Hoge hoge = new Hoge();

        public Form1()
        {
            InitializeComponent();
            hoge.HogeHandler += new HogeEventHandler(hoge_HogeHandler);
        }

        private void hoge_HogeHandler(object sender, HogeEventArgs e)
        {
            Console.WriteLine("hoge_HogeHandler");
            hoge.foo();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            hoge.foo();
        }
    }

    class HogeEventArgs : EventArgs
    {
        public HogeEventArgs()
        {
        }
    }

    delegate void HogeEventHandler(object sender, HogeEventArgs e);

    class Hoge
    {
        public event HogeEventHandler HogeHandler;

        public Hoge()
        {
        }

        public void foo()
        {
            if (HogeHandler != null)
            {
                HogeHandler(this, new HogeEventArgs());
            }
        }
    }
}


この問題について私は、C# や VB(VB6.0やそれ未満) や Java など、どの言語でも、何年も前から悩んでいます。たいていは 0秒後に発火するようなタイマーを使うとうまく回避できるのですが、もっとスマートなやりかたはないものでしょうか?
それともタイマーを使うのが良いソリューションだと考えて良いのでしょうか?
otf
ベテラン
会議室デビュー日: 2006/08/04
投稿数: 91
投稿日時: 2008-04-07 23:24
どういうときにそういう設計になってしまうんですか?
具体的な例がほしいです。
れい
ぬし
会議室デビュー日: 2005/11/01
投稿数: 346
投稿日時: 2008-04-08 01:56
引用:

これを避けたいのですがどうするのが巧みなコーディングなのでしょうか?


引用:

どの言語でも、何年も前から悩んでいます。



とりあえず、「イベントドリブン」という概念について、
今一度考えてみることをオススメします。

利点・欠点・典型的な実装などを整理するといいでしょう。

大抵の場合は「イベントドリブン」をきちんと理解していない、
うまく使えていないことが原因であろうと思います。

それほど頻度は多くないですが、
場合によってはスタックがどんどん深くなるような実装にならざるを得ない状況もあることはあります。
#他人のコードをいじるときなど。
そういう場合、その処理は「イベントドリブン」というパラダイムと相性がよくない、ということになります。

どのような処理をしたいのかによって解決策は変わりますが、
別スレッドで処理させるとうまくいったり、
独自のタスクキューイングを実装したり…
Timerを使うのもよい方法であろうと思います。
.NetならApplication.Idleを使える場合もあります。
いずれにせよ泥臭い方法です。

引用:

もっとスマートなやりかたはないものでしょうか?


イベントドリブンという概念に合うように、
自分の頭にあるアルゴリズムを修正できればスマートでしょう。
unibon
ぬし
会議室デビュー日: 2002/08/22
投稿数: 1532
お住まい・勤務地: 美人谷        良回答(20pt)
投稿日時: 2008-04-08 02:39
引用:

otfさんの書き込み (2008-04-07 23:24) より:
どういうときにそういう設計になってしまうんですか?
具体的な例がほしいです。


たとえば、Observer パターンを使うような場合、Observer 自身がたまたま Observable でもある、というような場合があります。そういうときに、自分がたまたま Observable であっただけで、このような再帰問題に悩まされてしまいます。

具体例としては、DBMS のテーブルのように複数のレコードを保持するようなクラスを考えます。レコードAに変更が加えられたら、それを監視する Observer が、レコードBを変更したいような場合があります。それがレコードBではなく、たまたまレコードAを更新したかった場合など、が今回のケースにあたります。
たしかに再帰であることは違いないのですが、フラットに分解できる再帰(「末尾再帰(tail recursion)」みたいなもの?)ですので、コールスタックが深くなってほしくないのです。

引用:

れいさんの書き込み (2008-04-08 01:56) より:
どのような処理をしたいのかによって解決策は変わりますが、
別スレッドで処理させるとうまくいったり、
独自のタスクキューイングを実装したり…
Timerを使うのもよい方法であろうと思います。
.NetならApplication.Idleを使える場合もあります。
いずれにせよ泥臭い方法です。


私が、もっとも疑問に思うのは、タイマーやスレッドのようなものを持ち出さないと、再帰が解決できないのだろうか?ということです。
換言すれば、私が悩んでいる再帰の問題は、言語レベルでは解決できず、タイマーやスレッドといった環境がないと解決できないものなのだろうか?ということです。
Jitta
ぬし
会議室デビュー日: 2002/07/05
投稿数: 6267
お住まい・勤務地: 兵庫県・海手
投稿日時: 2008-04-08 07:11
イベント ハンドラの中で、同じイベントを発生させる、という状況が理解できません。
「阿波おどり」というイベントを実行中に「阿波おどり」というイベントを新たに実行することはできないと思います。すでに実行中なので。
オブザーバーは、互いに監視しあっているのですか?それなら再起もありえるけど、相互監視が必要な状況って?
masa
大ベテラン
会議室デビュー日: 2004/10/28
投稿数: 161
投稿日時: 2008-04-08 09:19
相互に値を変更しあうことが避けられないとして、
それぞれの値変更時に「値が等しかったら何もしない」というような制御は行っていますか?
同じプロパティ(フィールド?)を無限に変更し続けることはなくなると思います。

よくあるプロパティ実装
public int Value {
  get { return m_Value; }
  set {
    -- 値が等しければ何もしない
    if ( m_Value == value ) { return; }
    m_Value = value;
    -- 値変更イベント
    OnValueChanged( EventArgs.Empty );
  }
}
otf
ベテラン
会議室デビュー日: 2006/08/04
投稿数: 91
投稿日時: 2008-04-08 09:33
引用:

unibonさんの書き込み (2008-04-08 02:39) より:
具体例としては、DBMS のテーブルのように複数のレコードを保持するようなクラスを考えます。レコードAに変更が加えられたら、それを監視する Observer が、レコードBを変更したいような場合があります。それがレコードBではなく、たまたまレコードAを更新したかった場合など、が今回のケースにあたります。


レコードAがイベント発生を知っているわけですからなんで自分自身を変更するために
Observerを用いる必要があるのですか?
まず一番最初にあげられた例ではObserverを用いる必要がないのは確かですよね。
Hogeが自分を変更するときはHoge.foo()に定義すればいいわけですから
逆転させたい依存関係は観察ではなく実装のほうではないですか?

根本的な解決ではないと思いますが、
私はイベントの発生を抑止するときは下記のように書きます。
コード:
        private void hoge_HogeHandler(object sender, HogeEventArgs e)
        {
            hoge.HogeHandler -= hoge_HogeHandler;
            try
            {   
                Console.WriteLine("hoge_HogeHandler");
                hoge.foo();
            }
            finally
            {
                hoge.HogeHandler += hoge_HogeHandler;
            }
        }

unibon
ぬし
会議室デビュー日: 2002/08/22
投稿数: 1532
お住まい・勤務地: 美人谷        良回答(20pt)
投稿日時: 2008-04-08 10:09
引用:

Jittaさんの書き込み (2008-04-08 07:11) より:
イベント ハンドラの中で、同じイベントを発生させる、という状況が理解できません。
「阿波おどり」というイベントを実行中に「阿波おどり」というイベントを新たに実行することはできないと思います。すでに実行中なので。


私は、今、WebBrowser を制御して、いわゆる巡回アプリケーションを作るようなことをしているのですが、たとえば、つぎのようなことはできています。
コード:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Threading;

namespace TestApp
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            webBrowser1.Navigate("http://www.example.com");
        }

        private void webBrowser1_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
        {
            webBrowser1.Document.Links[0].InvokeMember("click");
        }
    }
}


すなわち、DocumentCompleted イベントハンドラーの中でページの最初のハイパーリンクに対して InvokeMember("click") をして、これは結局は新たに DocumentCompleted イベントを発生させることになります。これにより、ページがどんどん遷移します。
しかし、DocumentCompleted の中でブレークポイントで一時停止しても、コールスタックは深くなっていません。
WebBrowser のような挙動を、自分で作ったクラスでもやりたいのです。

スキルアップ/キャリアアップ(JOB@IT)