Observable.FromEventで引数を受け取りたい場合

木内智史之介(シャッチョー)
ミンカさんけっこんしてくださいおねがいします(ズザー SEGAさん、DIVAの筐体ください(ズザー

Observable.FromEventについて

Observable.FromEvent は、eventをObservableに変換するためのメソッドです。
これを使うことで、ありとあらゆるeventをObservableとして扱う事が可能になっています。

たとえば、最もシンプルな例としては、こんな感じかなと思います。

FromEventの最もシンプルな例

public class SomeBehaviour : MonoBehaviour
{
public event Action onSomething;
void Start()
{
// onSometingをObservableに変換
var observable = Observable.FromEvent(h => onSomething += h, h => onSomething -= h);
observable.Subscribe(x => {
Debug.Log("subscribed from event");
Debug.Log(x);
});
// onSomethingを発火
// このタイミングでObservableにストリームが流れ、購読される
onSomething();
}
}

こんな感じで、eventをobservableに変換して、特定の条件下ではevent通知を握りつぶしたりなどの記述を、 LINQの文法に則って記述する事ができるようになります。

Observable.FromEventで引数を受け取る

さて、先ほどの例では、eventはなんの引数も受け取らない、とてもシンプルなeventでした。
では、引数を期待するようなeventでは、どのようにObservableに変換すればいいのでしょうか?

FromEventで引数を期待する例

public class SomeBehaviour : MonoBehaviour
{
public event Action<int> onSomethingWithArg;
void Start()
{
// onSometingをObservableに変換
// Genericで型指定を行うことで、引数を受け取れるようになります
var observable = Observable.FromEvent<int>(h => onSomethingWithArg += h, h => onSomethingWithArg -= h);
observable.Subscribe(x => {
// xはeventに渡された引数が入る
Debug.Log("subscribed from event");
Debug.Log(x);
});
// onSomethingを発火
onSomethingWithArg(12);
}
}

うん、すごく簡単ですね。

Observable.FromEvent<int>のように、ジェネリックで型指定を行う事で、eventから引数を受け取る事が可能になります。

ただ、ただですね。

複数の引数を受け取りたい場合」ちょっとした工夫が必要になりました。
Observable.FromEvent<int,string>的に対応できるなんて甘い話ではなかったですねw)

FromEventで複数の引数を受け取りたい場合

結論から言うと、「FromEventで複数の引数を受けとることは不可能」です。
(僕の勉強不足で本当はできるのかもですが…)

じゃあどうすればいいのかっていうと、複数の引数を丸め込んで一つの引数にしてしまうことです。

public class SomeBehaviour : MonoBehaviour
{
public event Action<OnSomethingEventArgs> onSomethingWithArgs;
public class OnSomethingEventArgs : EventArgs
{
public int myInt;
public string myString;
}
void Start()
{
var observable = Observable.FromEvent<OnSomethingEventArgs>(h => onSomethingWithArgs += h, h => onSomethingWithArgs -= h);
observable.Subscribe(x => {
// xはOnSomethingEventArgs
Debug.Log("subscribed from event");
Debug.Log(x.myInt);
Debug.Log(x.myString);
});
// onSomethingを発火
var args = new OnSomethingEventArgs() { myInt = 10, myString = "hoge" };
onSomethingWithArgs(args);
}
}

これで、実質は二つの引数を受け取る事と同じ事になるのかなと思います。

けど、このパターンしかないとなると、既に引数が二つある形で動いているeventをObservable化するには、EventArgs式に変更せざるを得ないということになります。

それは少し面倒だなー。。。

そもそもの話

EventHandler デリゲート
https://msdn.microsoft.com/ja-jp/library/system.eventhandler(v=vs.90).aspx

このあたりのマニュアルを見る限り、こういうEventHandler系の実装は、 delegateの型にはEventHandlerを指定しとけよ、 EventHandlerの引数はEventArgsだぞ、ぶれるなよ、 という声が聞こえてきそうです。

他にもっといい方法や、自分が勘違いしている場所などあれば、みなさん是非教えてください!

追記 [2016-05-11 10:25]

と、こんな感じの記事をtweetしたところ、まさかの本家の方からコメントをいただけました。

Tupleかあ…なるほど!
普段Tuple使わないから完全に抜け落ちていました。。

指摘を受けた上で試してみたコード

public class SomeBehaviour : MonoBehaviour
{
public event Action<int, string> onSomethingWithIntAndString;
void Start()
{
var observable = Observable.FromEvent<Action<int, string>, Tuple<int, string>>(
h => (x,y) => h(Tuple.Create(x,y)),
h => onSomethingWithIntAndString += h,
h => onSomethingWithIntAndString -= h
);
observable.Subscribe(x => {
Debug.Log("subscribed from event");
Debug.Log(x.Item1);
Debug.Log(x.Item2);
});
onSomethingWithIntAndString(10, "foo");
}
}

うん、たしかにいけてる。

ただ、これだと、購読者内の処理での変数参照が「Item1」「Item2」のようになってしまって分かりづらい、という問題がありそうですね。

この場合だと、こんな合わせ技にするのがベストプラクティスのような気がします!

EventArgsとの合わせ技一本!

public class SomeBehaviour : MonoBehaviour
{
public event Action<int, string> onSomethingWithIntAndString;
public class OnSomethingEventArgs : EventArgs
{
public int myInt;
public string myString;
}
void Start()
{
var observable = Observable.FromEvent<Action<int, string>, OnSomethingEventArgs>>(
h => (x,y) => h(new OnSomethingEventArgs() { myInt = x, myString = y }),
h => onSomethingWithIntAndString += h,
h => onSomethingWithIntAndString -= h
);
observable.Subscribe(x => {
Debug.Log("subscribed from event");
Debug.Log(x.myInt);
Debug.Log(x.myString);
});
onSomethingWithIntAndString(10, "foo");
}
}

これで、既存のどのようなeventもObservableに変換する事ができますね!
素晴らしい!

@neueccさん!ありがとうございます!