++C++; // 未確認飛行 C ブログ

http://ufcpp.net/

イベントのメンバー名

leave a comment »

背景: イベント使いにくい

.NET 4が出て以来、「イベントを最初に1回だけ受け取りたい」みたいな要件では、Taskクラスを返すようにしているわけですが。

ボタンを1回押したらページ遷移しちゃうから、1回きりしか受け取る必要がない。Viewの債務としては「遷移用のボタンが押されるまで待つTaskを返す」(状態遷移のトリガーになる)だけにして、実際のページ遷移(状態遷移の管理)はnavigatorみたいなやつが外に別にいて、そっちに任せたい、みたいな使い方をしていまして。

Rxを使えば割と簡単には書けたりします。

public event EventHandler<Point> Click;

public Task<EventPattern<Point>> AwaitClick()

{

    return Observable.FromEventPattern<Point>(this, “Click”).FirstAsync().ToTask();

}

といっても、”Click” の部分が文字列リテラルになっちゃってるのを見て察しがつく通り、これは内部的にはリフレクションを使ってやっています。もちろん効率悪い。

eventを(add/removeメソッドを取り出せる形で)引数として使えればこんな面倒起きないんですけどね。C#に、以下のような構文でも入ってくれれば。

public static Task<TEventArg> FirstAsync<TEventArg>(event EventHandler<TEventArg> e)

{

     var tsc = new TaskCompletionSource<TEventArg>();

    EventHandler<TEventArg> handler = null;

    handler = (sender, arg) =>

    {

         e.Remove(handler);

         tsc.SetResult(arg);

     };

     e.Add(handler);

     return tsc.Task;

}

まあ、あんまり実現しなさそうな機能要望ですが。

てことで、C#のevent構文使うのいい加減やめて(Rxが出たころから言われてはいるものの、今考えてもやっぱりやめたい)、最初からIObservable<T>使う方がいいんじゃないかと思っていたり。

次の問題: メンバーが3つに

で、せっかくRxを使って書くんなら、以下のようにしたい。

// Subscribe(イベントとして見るなら、要はAdd/Remove) public にしたい(ハンドラーの登録口は当然 public)

public IObservable<Point> Click { get { return _click; } }

// OnNext(イベントとして見るなら、要はInvoke) protected にしたい(実際にイベントを起こすのは派生クラスでやりたい)

protected IObserver<Point> _Click { get { return _click; } }

// 実体は private にしたい。初期化だけでいいんだけども。

private readonly Subject<Point> _click = new Subject<Point>();

Click, _Click, _clickとかいう名前が並んでるのとか、自分で書いてても思うけども、かなり邪悪。とはいえ、この3つって、同じ1つのイベントを、異なる3つの側面から見てるだけなので、本質的には1つにしたいんですよね。

event構文の場合、

public event EventHandler<Point> Click;

って書いたら、

  • 外側から見えるイベント(ハンドラー登録口)としてのClick (public)
  • 内側から呼べるデリゲートとしてのClick (private)

の2つが、同じ名前Eで作られるわけで (同じ名前で2つの側面があることで、それはそれで初心者への説明に困る機能だったりはしますが)、これと同じノリの同名別側面が使いたい。

C# vNextで多少マシになるとして

C# vNextで、get-only auto-propertyが入るので、多少はマシにできそうな感じはするんですが…

// 1個だけなら、C# vNext get-only auto-property とその初期化子で解決できるんだけども。

public IObservable<Point> Click { get; } = new Subject<Point>();

「初期化子を使ってreadonlyに初期化がしたいだけ」なのであれば、これで解決します。問題は、public IObservableなClickの1個だけじゃないこと。

// この場合、問題は、2個、別インターフェイスで受けたいということ。

public IObservable<Point> Click { get; } = new Subject<Point>();

// ↓これじゃだめ。と同じ Subject インスタンスを参照したい。

protected IObserver<Point> _Click { get; } = new Subject<Point>();

あるいは、ダウンキャストでごまかせなくもないんですが…

public IObservable<Point> Click { get; } = new Subject<Point>();

// こう書けなくはないけども、ダウンキャストが結構みっともない

protected IObserver<Point> _Click => Click as IObserver<Point>;

そもそも、これでも、3つが2つに減っただけで、同じものの別側面に対する名前付けで悩むのは一緒。

結局どうしよう

で、今回僕はあきらめて、最初に挙げた邪悪なClick, _Click, _clickを使うことにしてしまったわけですが。

他だと、publicにInvokeされてしまうことを認める方向、つまり、同様にRxを使うなら、

public Subject<Point> Click { get; } = new Subject<Point>();

こうしてしまう(Rxを使わない場合でも、Subscribe, OnNext両方相当のものを持ったクラスをpublicにしてしまう)方向で妥協する人なんかも多い(Unityゲームエンジンがまさにそれ。UnityEventとかいう型。あと、F#のイベントもそんなノリ)。外からInvokeできるのは、イベントの偽装(テストとか自動化)がしやすい反面、悪用・誤用(単一スレッド動作しか保証されないUIイベントなんかを別スレッドから起こせたり)し放題なので、微妙は微妙。

という感じで悩んでいたりします。というか、こんな話が(C#もリリースされてから10年以上経っても)今になってまた悩む程度には、ずっと悩ましいんですが。どうしよう。

Written by ufcpp

2014年10月27日 @ 23:20

カテゴリー: C#

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中

%d人のブロガーが「いいね」をつけました。