Reactive ExtensionsのFromEventをT4 Templateで自動生成する

Rxで面倒くさいのが、毎回書かなければならないFromEvent。F#ならイベントがファーストクラスで、そのままストリーム処理に流せるという素敵仕様なのですが、残念ながらC#のeventはかなり雁字搦めな感があります。しかし、そこは豊富な周辺環境で何とか出来てしまうのがC#というものです。F#では form.MouseMove |> Event.filter と書けますが、 form.MouseMoveAsObservable().Where と書けるならば、似たようなものですよね?

というわけで、T4です。FromEventを自動生成しましょう!と、いうネタは散々既出で海外のサイトにも幾つかあるし、日本にもid:kettlerさんがFromEventが面倒なので自動生成させてみた2として既に書かれているのですが、私も書いてみました。書くにあたってid:kettlerさんのコードを大変参考にさせていただきました、ありがとうございます。

私の書いたもののメリットですが、リフレクションを使用しないFromEventで生成しているため、実行コストが最小に抑えられています。リフレクションを使わないFromEventは書くのが面倒でダルいのですが、その辺自動生成の威力発揮ということで。それと、命名規則をGetEventではなくEventAsObservableという形にしています。これは、サフィックスのほうがIntelliSenseに優しいため。

んね?この命名規則は、RxJSのほうで公式に採用されているものなので(例えばrx.jQuery.jsのanimateAsObservable)、俺々規則というわけじゃないので普通に従っていいと思われます。

以下コード。利用改変その他ご自由にどうぞ、パブリックドメインで。

<#@ assembly Name="System.Core.dll" #>
<#@ assembly Name="System.Windows.Forms.dll" #>
<#@ assembly Name="C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\Profile\Client\System.Xaml.dll" #>
<#@ assembly Name="C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\Profile\Client\PresentationCore.dll" #>
<#@ assembly Name="C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\Profile\Client\PresentationFramework.dll" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ import namespace="System.Reflection" #>
<#
    // 設定:ここに生成したいクラス(のTypeをFullNameで)を足してください(以下の4つは例)
    // クラスによってはassemblyの増減が必要です、WPF/Silverlightなどはフルパス直書きしてください
    var types = new[] {
        typeof(System.Collections.ObjectModel.ObservableCollection<>),
        typeof(System.Windows.Forms.Button),
        typeof(System.Windows.Controls.Primitives.TextBoxBase),
        typeof(System.Windows.Controls.Primitives.ButtonBase)
    };
#>
using System.Linq;
using System.Collections.Generic;

<# foreach(var x in GenerateTemplates(types)) {#>

namespace <#= x.Namespace #>
{
    <# foreach(var ct in x.ClassTemplates) {#>

    internal static class <#= ct.Classname #>EventExtensions
    {
        <# foreach(var ev in ct.EventTemplates) {#>
		
        public static IObservable<IEvent<<#= ev.Args #>>> <#= ev.Name #>AsObservable<#= ct.GenericArgs #>(this <#= ct.Classname #><#= ct.GenericArgs #> source)
        {
            return Observable.FromEvent<<#= ev.Handler + (ev.IsGeneric ? "<" + ev.Args + ">" : "") #>, <#= ev.Args #>>(
                h => <#= ev.IsGeneric ? "h" : "new " + ev.Handler + "(h)" #>,
                h => source.<#= ev.Name #> += h,
                h => source.<#= ev.Name #> -= h);
        }
        <# } #>
    }
    <# }#>
}
<# }#>
<#+
    IEnumerable<T> TraverseNode<T>(T root, Func<T, T> selector)
    {
        var current = root;
        while (current != null)
        {
            yield return current;
            current = selector(current);
        }
    }

    IEnumerable<ObservableTemplate> GenerateTemplates(Type[] types)
    {
        return types.SelectMany(t => TraverseNode(t, x => x.BaseType))
            .Distinct()
            .GroupBy(t => t.Namespace)
            .Select(g => new ObservableTemplate
            {
                Namespace = g.Key,
                ClassTemplates = g.Select(t => new ClassTemplate(t))
                    .Where(t => t.EventTemplates.Any())
                    .ToArray()
            })
            .Where(a => a.ClassTemplates.Any())
            .OrderBy(a => a.Namespace);
    }

    class ObservableTemplate
    {
        public string Namespace;
        public ClassTemplate[] ClassTemplates;
    }

    class ClassTemplate
    {
        public string Classname, GenericArgs;
        public EventTemplate[] EventTemplates;

        public ClassTemplate(Type type)
        {
            Classname = Regex.Replace(type.Name, "`.*$", "");
            GenericArgs = type.IsGenericType
                ? "<" + string.Join(",", type.GetGenericArguments().Select((_, i) => "T" + (i + 1))) + ">"
                : "";
            EventTemplates = type.GetEvents(BindingFlags.Public | BindingFlags.InvokeMethod | BindingFlags.DeclaredOnly | BindingFlags.Instance)
                .Select(ei => new { EventInfo = ei, Args = ei.EventHandlerType.GetMethod("Invoke").GetParameters().Last().ParameterType })
                .Where(a => a.Args == typeof(EventArgs) || a.Args.IsSubclassOf(typeof(EventArgs)))
                .Select(a => new EventTemplate
                {
                    Name = a.EventInfo.Name,
                    Handler = Regex.Replace(a.EventInfo.EventHandlerType.FullName, "`.*$", ""),
                    Args = a.Args.FullName,
                    IsGeneric = a.EventInfo.EventHandlerType.IsGenericType
                })
                .ToArray();
        }
    }

    class EventTemplate
    {
        public string Name, Args, Handler;
        public bool IsGeneric;
    }
#>
// こんなのが生成されます

namespace System.Collections.ObjectModel
{
    internal static class ObservableCollectionEventExtensions
    {
        public static IObservable<IEvent<System.Collections.Specialized.NotifyCollectionChangedEventArgs>> CollectionChangedAsObservable<T1>(this ObservableCollection<T1> source)
        {
            return Observable.FromEvent<System.Collections.Specialized.NotifyCollectionChangedEventHandler, System.Collections.Specialized.NotifyCollectionChangedEventArgs>(
                h => new System.Collections.Specialized.NotifyCollectionChangedEventHandler(h),
                h => source.CollectionChanged += h,
                h => source.CollectionChanged -= h);
        }
    }
}

namespace System.ComponentModel
{
    internal static class ComponentEventExtensions
    {
        public static IObservable<IEvent<System.EventArgs>> DisposedAsObservable(this Component source)
        {
            return Observable.FromEvent<System.EventHandler, System.EventArgs>(
                h => new System.EventHandler(h),
                h => source.Disposed += h,
                h => source.Disposed -= h);
        }
    }

    // 以下略

使い方ですが、RxGenerator.ttとか、名前はなんでもいいのですがコピペって、上の方のvar typesに設定したい型を並べてください。一緒に並べたものの場合は、全て継承関係を見て重複を省くようになっています。WPFとかSilverlightのクラスから生成する場合は、assembly Nameに直にDLLのパスを書いてやってくださいな。コード的には、例によってLinq大活躍というかLinqなかったら死ぬというか。リフレクションxLINQxT4は鉄板すぎる。

一つ難点があって、名前空間をそのクラスの属している空間にきっちりと分けたせいで、例えばWPFのbutton.ClickAsObservableはSystem.Windows.Controls.Primitivesをusingしないと出てこないという、微妙に分かりづらいことになっちゃっています……。これ普通にHogeHogeExtensionsとかいう任意の名前空間にフラットに配置したほうが良かったのかなあ。ちょっと悩ましいところ。

T4の書き方

漠然と書いてると汚いんですよね、T4。読みにくくてダメだし読みにくいということは書きにくいということでダメだ。というわけで、今回からは書き方を変えました。ASP.NETのRepeater的というかデータバインド的にというかで、入れ物クラスを作って、パブリックフィールド(自動プロパティじゃないのって?そんな大袈裟なものは要りません)を参照させるという形にしました。foreachや閉じカッコ("}")は一行にする。<% %>で囲まれる範囲を最小限に抑えることで、ある程度の可読性が確保出来ているんじゃないかと思います。

といったようなアイディアは

よく訓練されたT4使いは 「何を元に作るか」 「何を作るか」 だけを考える。
何を元に作るかはきっと from ... select になるでしょう。 何を作るかの中では <#=o.Property#> で値を出力する事ができます。
csproj.user を作るための T4 テンプレート

からです。「何を元に作るか」 「何を作るか」 。聞いてみれば当たり前のようだけれど、本当にコロンブスの卵というか(前も同じこと書いた気がする)、脳みそガツーンと叩かれた感じで、うぉぉぉぉぉ、と叫んで納得でした。はい。それと、T4は書きやすいと言っても書きにくい(?)ので、囲む範囲を最小にするってことは、普通のコードでじっくり書いてからT4に移植しやすいってことでもあるんですね。

まとめ

最近F#勉強中なのです。Expert F# 2.0買ったので。と思ったらプログラミングF#が翻訳されて発売されるだとー!もうすぐ。あと一週間後。くぉ、英語にひいこらしながら読んでいるというのにー。

F#すげーなー、と知れば知るほど確かに思うわけですが、しかし何故か同時に、C#への期待感もまた高まっていきます。必ずや「良さ」を吟味して取り込んでくれるという信頼感があります、C#には。そしてまた、ライブラリレベルで強烈に何とか出来る地力がある、例えばイベントをストリームに見立てた処理には、Reactive Extensionsが登場してC#でも実現出来ちゃったり。Scalaと対比され緩やかに死んでいくJavaと比べると、F#と対比しても元気に対抗していくC#の頼もしさといったらない。

といっても、F#も全然まだ表面ぐらいしか見えてないし、突っつけば突っつくほど応えてくれる奥の深い言語な感じなので、今の程度の知識で比較してどうこうってのはないです。Java7のクロージャにたいし、Javaにそんなものはいらない、とか頑な態度を取っている人を見るとみっともないな、と思うわけですが、いつか私もC#に拘泥してC#にそんなものはいらない、的なことを言い出すようだと嫌だなー、とかってのは思ってます。進化を受け入れられなくなったら、終わり。

マルチパラダイム言語の勝利→C++/CLI大勝利ですか?→いやそれは多分違う。的なこともあるので何もかもを受け入れろ、ひたすら取り込んで鈍重な恐竜になれ(最後に絶滅する)、とは言いません。この辺のバランス感覚が、きっと言語設計にとって難しいことであり、そして今のC#は外から見れば恐竜のようにラムダ式だのdynamicだのを取り入れてるように見えるでしょうが、決してそうではなく、素晴らしいバランスに立っています。機能の追加が恐竜への道になっていない。むしろ追加によって過去の機能を互換性を保ちつつ捨てているんですよね、例えば、もうdelegateというキーワードは書くどころか目にすることもほとんどない←なのでC#を学習する場合、C#1.0->2.0->3.0->4.0という順番を辿るのは良くなくて、最新のものから降りていったほうがいい。

何が言いたいかっていったらC#愛してるってことですな。うはは。5.0にも当然期待していますし、Anders Hejlsbergの手腕には絶対的に信頼を寄せています。4.0は言語的な飛躍はあまりなかっただけに、5.0は凄いことになるに違いない。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(C#)
April 2011
|
July 2024

Twitter:@neuecc GitHub:neuecc

Archive