Linq と Windows Phone 7(Mangoアップデート)
- 2011-04-14
今冬に予定されている大型アップデート(Mango)に向けて、MIX2011で大量に情報出てきました。色々ありましたが、その中でも目を引いたのがデータベース対応とQueryable対応。データベースは恐らくSQL Server Compact 4.0相当で、これはいいものだ。ただ、生SQL文かあ、という思いもあり。他のやり方ないのかなあ。オブジェクトDBとか?などと夢見てしまうけれど、高機能なエンジン(SQLCE4)がある以上は、それをそのまま使ったほうがいいのは自明で。
インピーダンスミスマッチを解消する、そのためのLinqでしょ!と、ああ、そうだね。でもLinq to Sqlはないし、Linq to Entitiesが乗っかるわけもないし、策はなく普通に生SQLの気もするなあ。どうでしょうね。どうなるのでしょうね。で、Queryable対応も発表された、というかデモられていました。
これでDB+Linqがあることが保証されたから安泰。何のQueryProviderと見るべきかしら。この辺、不透明なので、情報入り次第しっかり追っかけます。
Queryableの難点は、見た目はスッキリしてますが、コンパイル後の結果は式木のお化けになる+実行時は式木のVisitというわけで、軽い処理とは言い難いところなのですが、軽いとか重いとか気にしないし!じゃあなくて、MangoではGC改善など性能向上も入るようなので、十分なパフォーマンスは確保できそうです。あと、ユーザーエクスペリエンスを損なうボトルネックは結局そこじゃあないんだよ、的なこともありそうだし。
例えば現行のWP7のListBoxは致命的に重くて、純正アプリは軽いのにユーザー作成アプリではクソ重く、純正だけネイティブでチートかよゴルァ、という勢いだったんですが(Mangoで改善されるそうです!)、これの要因の一つは画像の読み込み周りがアレだったことにあるようで……。
CocktailFlowという素敵な見た目のアプリがあるのですが、これはその辺の問題に対処するためか、事前にLoadingを取って、完全に読み込み終わってからしか表示しないようになってます。でも、それはそれでネットワーク読みに行ってるわけでもないのにLoadingが長くて、微妙だなあ、と。
MIX11でのRx
二つセッションがありました。一つはLinq in Actionの著者(最初期に出たものですが、良い内容でしたね)によるWP7でのRx活用例。
マイクのボリュームが不安定で聴きにくいですー。話はPull(Enumerable)とPush(Observable)の比較から入ってますね。MoveNextして、MoveNextして、MoveNextして、ええ、ああ、メソッド探訪第7回:IEnumerable vs IObservableの辺りで書きましたね~。それのEnumerbaleサイドのリライト版であるLINQの仕組みと遅延評価の基礎知識の続きとして「Rxの仕組み」を書きたいのですが書くと思ってもう3ヶ月も経ってる、うぐぐ……。
話がそれた。んで、話の序盤は結構退屈です。IEnumerator vs IObserverとかつまらないです、って、あ、私もやってたっけ……。話の流れを上手くつながないと何か唐突なのね。さて、デモはサイコロ転がしを完成させていくというもの。どんどんコードがメソッドチェーンのお化けになっていく様はどこかで見たことある(笑)。最初は、重たい処理を、ObserveOn(Scheduler.ThreadPool)とするだけで実行スレッドを切り替えて、CPUバウンドの処理を簡単に同期→非同期に変換してUIをブロックしないというデモ。ちなみにObserveOn(Scheduler.ThreadPool)じゃなくて、これはToObservableの時点でScheduler渡したほうがいいんじゃあないのかなあ。まあ、大して違いはないといえばないですがー。
次は加速センサーをRxに変換して、TimestampをつけてShakeされたことを検出するというもの。生のセンサーイベントをRxで事前に加工して扱いやすくする、という手ですね。私もUtakotohaの再生イベントでそれやったよ!(いちいち対抗心燃やして宣伝しなくてよろし)。ObservableだとMockに置き換えやすいんだよ、という話もね。気づいたらメソッドチェーンのお化けになっているのも。Linq使いの末路はみんなこんなのになってしまうという証明が!
セッションはもう一つ。RxチームのBart De SmetによるRxJSのセッション。MIX2011はIE10と合わせてHTML5推しもありましたしね。
最初の20分は、ObservableとObserverの購読概念などについて簡単な説明。あとRange,FromArray,Timerといった生成子。Pullとの比較がない、つまり"Linq"とは全く言わないのがJavaScript向けですね。喩えもHTML5のGeolocation APIが、などなど新鮮。そして、だからこそ分かりやすいかもしれません。その後はいきなりGeolocation APIをラップする自作Observableを作ろうが始まって難易度が、まあLevel300だものね。実用的な話ではあるし、Observableを作るということは仕組みを理解するという話でもあるので、こういうのも良い流れ。ほか、KeyUpを拾ってネットワークを問い合せてのリアルタイム検索(event+asynchronousの合成例)など。これはハンズオン資料でもお馴染みの例ですが、順に追ってデモしてもらえるとThrottleの意味は凄く分かりやすいですね。Switchも強力だけど分かりづらさを持つので、こうして丁寧に解説してもらえると、いいね。ともかく、概念としてC#もJavaScriptも超えたところにあるので、考え方、コード例はC#/WP7でも使えます。良いセッションだと思いますので見るのお薦め。
そういえばで、Async CTPが更新されていて、一部変更があったので、RxとAsyncとのブリッジライブラリ(Rx-AsyncLinq)が動かなくなったと思われるので、それに合わせて近いうちにRxもアップデートありそうな気がします。RxJSもここずっと更新とは遠かったのですが、こうしてセッションもあったことだし、大型アップデートはあってもおかしくないかな。
Async CTPはufcppさんの記事Async CTP Refreshに詳しく書かれていますが、ライセンス形態がご自由にお使いください(※但し自己責任)へと変更、日本語版でも入るようになった、WP7対応、といった具合に、WP7開発でも今すぐ投下しても大丈夫だ問題ないになったのは大きい。
他言語からWP7開発のために来たので、C#の経験があまりなくてLinqがイマイチ分からない。でも非同期ダルい。という人には、Rxよりも、Asyncのほうがずっと扱いやすく応えてくれるので、なし崩し的に投入してしまっていい気がしますねー。勿論、ある程度は大きな変更に対応していく気は持ってないとダメですが。
ある程度Linqが分かる人なら(別にそんな大仰なスキルを要求するわけじゃなく、SelectしてWhereして、Linq便利だな~、ぐらいでいいです!)Rxを学ぶのもお薦め。基本的にはLinqに沿っているので、学習曲線はそんなにキツくないです。特に、非同期をラップしてWhereしてSelectしてSubscribe、ぐらいならすぐ。他のは、徐々に覚えればいいだけで。
まとめ
WP7はRx標準搭載で、ただでさえLinq濃度が高かったというのに、Queryable搭載でLinq天国に!もはやWP7が好きなのかLinqが好きなのかよくわかりませんが(多分、後者です)、ともかく最高のプラットフォームなのは間違いない。
ところでDay1ではnugetが割とフィーチャーされていたのですが、その背景、よくみると……
右のほうにlinq.jsが!すんごく嬉しい。
Utakotoha - Windows Phone 7用の日本語歌詞表示アプリケーション
- 2011-04-09
マーケットプレイスに通り、公開されました。フリーです(下で述べますがソースも公開しています)。再生中の音楽のアーティスト・タイトルを元に自動で検索して、歌詞を表示する、というものです。海外製の同種アプリとの違いは、対象が日本の曲ということになります。下記バナー、もしくはマーケットプレイス検索"utakotoha"でどうぞ。アプリケーション名は例によってヒネりなしで「歌詞」からそのままとりました。うたのことば。Utakata TextPadと名前が似ていますが偶然の一致です(ほんと)
スクリーンショットでは格好つけてズームインして隠蔽していますが、スクリーンショット用詐欺なだけで、実際はこんな感じです。
ようは、単純にWebBrowserにgoo歌詞を出しているだけです。ダブルタップでエリアに沿って幅一杯の拡大してくれるので、誤魔化し的にはまぁまぁ。WebBrowserを経由する理由は、APIが提供されているわけでもないので、権利関係考えるとこうでないとマズいかな、といったところで。goo歌詞を選んだ理由も直リンが許可されていたからです。
あと、WP7はテーマに黒背景のDarkと白背景のLightがあるわけですが、goo 音楽の背景は強制白で、どうもミスマッチなわけですね。アプリ自体を完全に白背景にしてしまうのもいいかな、とは思ったんですが、ブラウザのCSSを書き換えて黒背景にするという手を(ネタとして)取ってみました。オプションでOFFに出来るというか、デフォルトはOFFです。ズームすれば違和感はないんですが、画面いっぱいに広がってるものでは、微妙度極まりない。
ほか、Twitterに再生中楽曲を投稿する機能があります。
ソースコード
CodePlex上で公開しています。単機能アプリですので、コード規模は小さめです。あまりしっかりとはしてませんが、ユニットテストなども書いてあります。
WP7のReactive Extensions実践例サンプル、のつもりで書いたので、Rxを全面的に使っています。むしろRx縛りと言ってもいいぐらいにRxのみでやるのを無駄に貫いています。意味がなくてただ複雑化しただけの箇所多数。UIと絡めて使うのがイマイチ分からず振り回されてますねー。とはいえ、バッチリはまった部分も勿論あり。というわけで、Windows Phone 7においてReactive Extensionsがどのような場所で使えるのか、というのをコードとともに解説していきます。
コードはWindows Phone 7向けですが、通常のSilverlightでもWPFでも適用できる話なので、WP7関係ないからー、と言わず、Rxに興味ありましたら、眺めてみてください。
Linq to Event
Rxの特徴の一つはイベントのLinq変換です。Linq化して何が嬉しいかというと、柔軟なフィルタリングが可能になることです。WP7では、センサーからのデータ処理やタッチパネルなど、様々な箇所で威力を発揮すると思われます。UtakotohaではMediaPlayerの再生情報の変化に対してRx化とフィルタリングを施しました。
// Model/MediaPlayerStatus.cs
public class MediaPlayerStatus
{
public MediaState MediaState { get; set; }
public ActiveSong ActiveSong { get; set; }
public static MediaPlayerStatus FromCurrent()
{
return new MediaPlayerStatus
{
MediaState = MediaPlayer.State,
ActiveSong = MediaPlayer.Queue.ActiveSong
};
}
public static IObservable<MediaPlayerStatus> ActiveSongChanged()
{
return Observable.FromEvent<EventArgs>(
h => MediaPlayer.ActiveSongChanged += h, h => MediaPlayer.ActiveSongChanged -= h)
.Select(_ => MediaPlayerStatus.FromCurrent());
}
public static IObservable<MediaPlayerStatus> MediaStateChanged()
{
return Observable.FromEvent<EventArgs>(
h => MediaPlayer.MediaStateChanged += h, h => MediaPlayer.MediaStateChanged -= h)
.Select(_ => MediaPlayerStatus.FromCurrent());
}
// (省略)すぐ下に...
}
Rx化は、ただFromEventをかますだけです。また、その際に、IEventではなく、本当に使う情報にだけ絞ったもの(この場合はMediaStateとActiveSong)をSelectで変換しておくと色々とコードが書きやすくなるのでお薦めです。
さて、「柔軟なフィルタリング」とは何か。ただデータを間引くだけなら、イベントの先頭でif(cond) return;を書けばいいだけです。RxではLinq化により、(自分自身を含めた)イベント同士の合成と、時間軸を絡めた処理が可能になります。これにより、従来一手間だった処理がたった一行で、連続した一塊のシーケンスとして処理することが可能になりました。
Utakotohaでも、生のイベントをそのまま扱わず、ある程度間引いて加工したものを渡しています。
/// <summary>raise when ActiveSongChanged and MediaState is Playing</summary>
public static IObservable<Song> PlayingSongChanged(int waitSeconds = 2, IScheduler scheduler = null)
{
return ActiveSongChanged()
.Throttle(TimeSpan.FromSeconds(waitSeconds), scheduler ?? Scheduler.ThreadPool) // wait for seeking
.Where(s => s.MediaState == MediaState.Playing)
.Select(s => new Song(s.ActiveSong.Artist.Name, s.ActiveSong.Name));
}
「再生中かつx秒間(デフォルトは2秒)新しいイベントが発生しなかった最新のものだけを流す」というものです。どういう意味かというと、連続で楽曲をスキップした時。Utakotohaでは再生曲の変更に合わせて、自動で歌詞検索をしますが、連続スキップにたいしても全て裏で検索に走っていたらネットワークの無駄です。なので、2秒間だけ間隔を置いて連続スキップされていない、と判断した曲のみを歌詞検索するようにしています。
こういう処理は、地味だけど必ず入れなければならない、けれど面倒くさい。でも、Rxを使えばなんてことはなく、Throttleで一撃です。Timer動かしたりDateTimeを比較したりなんて、もうしなくていいんだよって。
/// <summary>raise when MediaState Pause/Stopped -> Playing</summary>
public static IObservable<Song> PlayingSongActive()
{
return MediaStateChanged()
.Zip(MediaStateChanged().Skip(1), (prev, curr) => new { prev, curr })
.Where(a => (a.prev.MediaState == MediaState.Paused || a.prev.MediaState == MediaState.Stopped)
&& a.curr.MediaState == MediaState.Playing)
.Select(s => new Song(s.curr.ActiveSong.Artist.Name, s.curr.ActiveSong.Name));
}
こちらは、状態が停止->再生になったことを検知するというもの。停止->再生でも自動検索を走らせたいので。
この source.Zip(source.Skip(1), /* merge */) は、一見奇妙に見えるかもしれませんが、ある種のイディオムです。一つ先(Skip(1))の値と合流するということは、Skip(1)時の値を基準にすると、現在値と一つ前の値で合流させることができる、ということになります。それにより、一つ前の状態を参照して停止->再生を検知しています。
過去の値を参照するには、他に、Scan(Aggregateの列挙版、そう考えると現在値と一つ前の値が使えることのイメージつくでしょうか?)やBufferWithCount(Listでバッファを持つ、第二引数でずらす範囲を指定可能)など、幾つかやり方がありますが、このZip(Skip(1))が最も扱いやすいところ。ただし、通常のものとSkipしたものとで、二つ分Subscribeされるということは留意したほうがいいかもしれません。そのことが問題になるケースもあるので、キャッシュを使う(Pulish(xs=>xs.Zip(xs.Skip(1)))など回避策を頭に入れておくと良いケースもあります。
Linq to Asynchronous
Rxのもう一つの大きな特徴は非同期のLinq変換です。そうすることにより、コールバックの連鎖で扱いにくかった非同期が、一本の流れに統一されます。
Utakotohaでは、歌詞の検索部分が代表的です。歌詞検索の背景ですが、goo歌詞のものを表示しているのだからgoo歌詞の検索を呼んでやるかなあ、と思ったのですが、それは色々マズいので、Bing Apiのサイト内検索を経由して、表示しました。Bing Apiについては若干苦労話もあるので、いつかまた。
// Model/Bing/BingRequest.cs
public IObservable<SearchWebResult> Search(params SearchWord[] keywords)
{
var req = WebRequest.Create(BuildUrl(keywords));
return Observable.Defer(() => req.GetResponseAsObservable())
.Select(res =>
{
var serializer = new DataContractJsonSerializer(typeof(SearchWebStructure));
using (var stream = res.GetResponseStream())
{
return (SearchWebStructure)serializer.ReadObject(stream);
}
})
.SelectMany(x => (x.SearchResponse.Web.Results != null)
? x.SearchResponse.Web.Results
: Enumerable.Empty<SearchWebResult>());
}
割とあっさり。Bing APIならびにJSONに関しては、以前Windows Phone 7でJSONを扱う方法について(+ Bing APIの使い方)として書きました。ほとんどそのままです。SearchResponse.Web.Resultsは、配列なので、SelectManyで分解してやります(nullの場合は空シーケンスを流す)。すると、後続に繋げるのが非常にやりやすくなります。実際に
// Model/Song.cs
public IObservable<SearchWebResult> SearchLyric()
{
return new BingRequest()
.Search(MakeWord(Artist), MakeWord(Title), LyricSite, Location, Language)
.Where(sr => sr.Url.EndsWith("index.html"))
.Do(Clean);
}
といったように、Searchから更に続いて、若干のフィルタリングが入っています。これを使う場面では、勿論、更にチェーンが続きます。といったように、Rxは一つの流れを構築するわけですが、それらを徹底的に分解・分割して適切な場所への配置・組み合わせが可能になっています。もし、通常の非同期処理のようなコールバックの連鎖だったら、組み合わせは大変でしょう(だから、その場だけで処理したくなって、ネストが嵩んでしまう)
Orchestrate and Coordinate
Rx全体の特徴として、また、他の非同期を扱うライブラリと最も異なる、しかし重要な点として、中に流れるデータを区別しません。非同期もイベントもタイマーもオブジェクトシーケンスも、全て同列に扱います。それはどういうことかというと、データの種別を超えて合成処理が可能になるということです。つまり、Rxは、あらゆるデータソースを統合する基盤といえます。上のAsynchronousの例でも、非同期のWebResponseが、SelectMany以降はオブジェクトシーケンスに摩り替わっていました。
Utakotohaでは、歌詞の表示部分で色々なデータを混ぜあわせる処理を行っています。
// View/MainPage.xaml.cs
LyricBrowser.NavigatedAsObservable()
.Where(ev => ev.EventArgs.Uri.AbsoluteUri.Contains(GooLyricUri))
.SelectMany(ev =>
{
// polling when can get attribute
return Observable.Timer(TimeSpan.Zero, TimeSpan.FromSeconds(3))
.ObserveOnDispatcher()
.Select(_ => (ev.Sender as WebBrowser).SaveToString())
.Select(s => Regex.Match(s, @"s.setAttribute\('src', '(.+?)'"))
.Where(m => m.Success)
.Take(1);
})
.Select(m => WebRequest.Create(GooUri + m.Groups[1].Value).DownloadStringAsync())
.Switch()
.Select(s => Regex.Replace(s.Trim(), @"^draw\(|\);$", ""))
.Where(s => !string.IsNullOrEmpty(s))
.ObserveOnDispatcher()
.Subscribe(jsonArray =>
{
// insert json array to html
LyricBrowser.InvokeScript("eval", @"
var array = " + jsonArray + @";
var sb = [];
for(var i = 0; i < array.length; i++) sb.push(array[i]);
document.getElementById('lyric_area').innerHTML = sb.join('<br />')");
// (省略)
}, e => MessageBox.Show("Browser Error"))
.Tap(disposables.Add);
歌詞の表示といっても、WebBrowserに表示しているこということは、歌詞のURLを渡すだけです。それだけで済むはずでした。が、……妙に複雑怪奇です。歌詞のURLが渡されて表示が完了してからが起点として(Navigated)、処理を開始しています。
何故こうなったか。WP7が現在積んでるIEは7。ついでにFlashはまだ未対応で見れません。それとこれとがどう関係あるかというと、歌詞表示に関係あります。日本の歌詞サイトは大抵はコピペ禁止のために右クリック禁止、だけでなく、Flashで表示していたりします。それじゃあ手が出せない。では今回利用しているgoo歌詞は、というと、少し変わっていてHTML5 Canvasに描画しています。勿論Canvasは古いブラウザじゃ動かないので、互換用JavaScriptも挟んでいるよう。uuCanvas.js を使っているようですが、環境貧弱なWP7版のIE7じゃあ、土台動きませんでした。
このまんまじゃあ歌詞が表示出来なくて困ったわけですが、幸いgoo歌詞はJSONPで歌詞データを別途取得してCanvasにデータを流しているようなので、HTMLからJSONPの発行先を割り出して、歌詞データを頂いてしまえば問題ない(この時点で規約的にはグレーな気が)。生の歌詞データが手に入ってしまっ……。こいつをキャッシュするようにしてオフラインでも見れるといった機能を提供してあげられれば幸せになれるのですが、そういうのは利用規約に違反、してますね、明らかに。
じゃあどうするか。手元には歌詞の表示されていないブラウザ上のHTMLと、歌詞データがある。よし、じゃあブラウザにこちらからはめ込んでやればいいんじゃなイカ?
WP7ではWebBrowserのDOMは触れません。DOMを外から触ってappendChildしてサクッと終了、というわけにはいかず。ただ、外部から干渉出来る口が一つだけ用意されています。それがInvokeScript。外から実行関数を指定して、戻り値を受け取れます(DOMは無理なので、Stringで貰うのが無難)。ならば、evalして外から実行関数自体を注入してやれば、何だって出来る。どうにも馬鹿らしい気もしますが、このぐらいしか手がないのでShoganai(なお、予めWebBrowserのプロパティでスクリプト実行を許可しておかないと例外が出ます)。
Timer and Polling
イベント(Navigated)→タイマー(SaveToString)→非同期(DownloadStringAsync)という直列の合成でした。直列の合成を行うメソッドはSelectMany(もしくはSelect+Switch、両者には若干の違いがあるのですが、それに関しては後日説明します)で、Rxの中でも頻繁に使うことになるメソッドです。
ところで、タイマーが唐突なのですが、何故タイマーを仕込んでいるのか。どうもNavigated直後にSaveToString(WebBrowser内のHTMLを文字列化)だと、タイミング次第で上手く抽出できないことが多かったので(JSで色々処理されてる影響かな?)、必要なJSONPの書かれた属性が取れるまでSaveToStringをリトライするようにしました。つまり、ポーリング(定期問い合わせ)です。
ポーリングは普通だと面倒くさいはずなんですが、Rxだと恐ろしく簡単に書ける上に、こうして通常の処理の流れと合成することが可能になっているのが何よりも強力です。.NET Frameworkには幾つものTimerクラスがあって、何を使えばいいのかと戸惑ってしまうところがありますが、答えは出ました。Observable.Timerがベスト。大変扱いやすい。
これで、本来Canvasのあった領域にテキストデータとして歌詞を表示させられました。全く違和感のない、完璧なハメコミ合成。無駄なコダワリです。地味すぎて一手間かけてることなんてさっぱり分からない。だがそれがいい。……いや、ちょっと悲しい。それにしてもでしかし、Canvas, Flash対応になればこんなやり方は不要になるわけで、今年後半のアップデートでのIE9搭載が待ち遠しい。
Unit Testing
Silverlight向けのユニットテスト環境って、全然ない。ブラウザ上のSilverlightで動かす、というタイプは幾つかありますが、Visual Studioと統合された形のでないと、そんなのアタシが許さない。
TDDするわけでも、熱心にテスト書くわけでも、特段カバレッジを気にするわけでもない私ですが、テストが書けることは重要視しています。何故かというと、メソッドの動作確認が最も素早く行えるから。テスト(が出来ること)に何を期待しているかというと、確認したいんです、メソッドの動作を。手軽に、コンソールアプリを書く感覚で、素早く。処理をコンソールアプリにコピペって、もしくは並走してコンソールアプリを立てながら開発していたりなどを以前よくしたのですが、それを単体テストのメソッド部分に書けば、動作確認のついでに、テストまで手に入るので、それは素敵よね?と。
(単体)テストが第一の目的ではない、(動作確認)テストが目的なのだ。だから、テストフレームワークはVisual Studioと完全な統合を果たしていなければならない。ショートカットでIDE内のウィンドウで即座に実行。スピードが大事。また、シームレスなデバッグ実行への移行も。故にMSTestを選択するのである(キリッ
などなどはその辺にしておいて、それはともかくで、素直にフル.NET Frameworkで動くMSTestを使います。一応Silverlightのアセンブリはフル.NETからも参照可能のようなので、普通にテストプロジェクトを立ててDebug.dll を参照してやる(プロジェクト参照は警告出るので)という手も使えなくはなさそうなのですが、完全な互換を持つコアライブラリは全体のごく一部で、それ以外を使っていると普通に実行時例外でコケるなど、正直使えないと私は判断しました。よって、アセンブリ参照でやるのは諦め。プロジェクト参照で警告が出る理由も分かりました、あまり実用的な機能ではない……。
代わりにWP7プロジェクトとテストプロジェクトの間に、.NET4ライブラリプロジェクトを立てて、「追加->既存の項目->リンクとして追加」で、フルフレームワークとWP7間で.csファイルを共有してやります(Viewは勿論共有出来ないので、基本はModelのみ)。
勿論、コードレベルで互換が取れていないと動きません。そんなわけで、少なくともModelに関してはWPF/SL/WP7で共通で使いまわせるように意識して作りたいところです。移植性というだけじゃなく、MSTestの恩恵を受けれるので。ただまあ、無理な部分は無理で諦めちゃってもいいとは思います(SLのほうにだけあるクラスとかもありますから)。非同期のテストなどは、幸いRxを使っていれば非常に簡単なので、バシバシ書いちゃいましょう。
// Utakotoha.Test/SongTest.cs
[TestMethod]
[Timeout(3000)]
public void SearchLyric()
{
var song = new Song("吉幾三", "俺ら東京さ行ぐだ");
var array = song.SearchLyric().ToEnumerable().ToArray();
array.Count().Is(1);
array.First().Title.Is("俺ら東京さ行ぐだ 吉幾三");
array.First().Url.Is("http://music.goo.ne.jp/lyric/LYRUTND1127/index.html");
}
ToEnumerableしてToArrayするだけです!非同期のテストなんて怖くない。
Portable Library Tools CTPという、各環境で互換性の取れるライブラリが作成出来るもの、なども出てきているので、リンクで追加とかいう間抜け(そして面倒)なことじゃなく、プロジェクトごと分離して、Modelは互換で生成、というのが将来的には良いやり方になるかなあ、などと思っています。そういう事情から、私は移植性とは関係ない立場からも、Portable Library Toolsの発展に期待しています。
なお、アサーションはChaining Assertion使っています。ドッグフードドッグフード。というかもう、必需品なので、これないと書けない、書きたくない……。
Mocking Event with Moles and Rx
さて、このやり方の利点として、PexやMolesが使えます(Pexは一応SLをサポートしたものの、Molesは依然としてSL未サポート)。Moles(Microsoft Researchが提供するモックフレームワーク、フリー)の乗っ取り機構は強力なので、テスト可能範囲が大幅に広がります。詳しくはRx + MolesによるC#での次世代非同期モックテスト考察をどうぞ。
今回Linq to Eventで紹介したPlayingSongActiveは、以下のようにテストしています。MediaPlayer周りはXNAなので、Test側の参照DLLとしてXNA Game Studio v4.0のMicrosoft.Xna.Framework.dllを参照。そしてMolesで乗っ取り。
// Utakotoha.Test/MediaPlayerStatusTest.cs
private MediaPlayerStatus CreateStatus(MediaState state, string artist, string name)
{
return new MediaPlayerStatus
{
MediaState = state,
ActiveSong = new Microsoft.Xna.Framework.Media.Moles.MSong
{
NameGet = () => name,
ArtistGet = () => new MArtist
{
NameGet = () => artist
}
}
};
}
[TestMethod, HostType("Moles")]
public void PlayingSongActiveTest()
{
// event invoker
var invoker = new Subject<MediaPlayerStatus>();
MMediaPlayerStatus.MediaStateChanged = () => invoker;
// make target observable
var target = MediaPlayerStatus.PlayingSongActive().Publish();
target.Connect();
// at first, stopped
using (target.VerifyZero())
{
invoker.OnNext(CreateStatus(MediaState.Stopped, "", ""));
}
// next, playing
using (target.VerifyOnce(song => song.Is(s => s.Title == "song" && s.Artist == "artist")))
{
invoker.OnNext(CreateStatus(MediaState.Playing, "artist", "song"));
}
// pause
using (target.VerifyZero())
{
invoker.OnNext(CreateStatus(MediaState.Paused, "", ""));
}
// play again
using (target.VerifyOnce(song => song.Is(s => s.Title == "song2" && s.Artist == "artist2")))
{
invoker.OnNext(CreateStatus(MediaState.Playing, "artist2", "song2"));
}
}
元のコード自体がイベント発火部分はRxで包んであるので、そのイベント発火だけを差し替え。SubjectとはイベントのRxでの表現。OnNextでイベント発火の代用が可能になっています。これで、任意のイベント(今回はMediaPlayerなので、再生停止であったり再生開始であったり)を発行して、その結果の挙動を確認しています。
VerifyなんたらはIObservableへの自前拡張メソッドで、発火されたか/回数の検証です。「イベントは発生したけれどフィルタリングされて値が届かなかった」ことの、フィルタリングが正常に出来たかの確認って、そのままだと難しい。如何せんSubscribeまで届いてくれないということですから。そのため、その辺を面倒みてくれるものを用意しました。
// Utakotoha.Test/Tools/ObservableVerifyExtensions.cs
/// <summary>verify called count when disposed. first argument is called count.</summary>
public static IObservable<T> Verify<T>(this IObservable<T> source, Expression<Func<int, bool>> verify)
{
var count = 0;
return source
.Do(_ => count += 1)
.Finally(() =>
{
var msg = verify.Parameters.First().Name + " = " + count + " => " + verify.Body;
Assert.IsTrue(verify.Compile().Invoke(count), "Verifier " + msg);
});
}
/// <summary>verify called count when disposed. first argument is called count.</summary>
public static IDisposable VerifyAll<T>(this IObservable<T> source, Expression<Func<int, bool>> verify, Action<T> onNext = null)
{
return source.Verify(verify).Subscribe(onNext ?? (_ => { }));
}
/// <summary>verify not called when disposed.</summary>
public static IDisposable VerifyZero<T>(this IObservable<T> source)
{
return source.VerifyAll(i => i == 0);
}
/// <summary>verify called once when disposed.</summary>
public static IDisposable VerifyOnce<T>(this IObservable<T> source, Action<T> onNext = null)
{
return source.VerifyAll(i => i == 1);
}
usingによるスコープを抜けるとFinallyで検証が入ります。RxでイベントをラップするとIDisposableになる、そのことの利点が生きてきます。
今後の改善
Pivotのヘッダーのデザインがどうも間抜け(マージンの取り方が変だし文字サイズも違和感あり)なのが気になってるので、変えたいです。HeaderTemplateの編集の仕方がよくわからずで放置なのですけれど、ゆったり紐解けば出来るでしょう。多分。
レジュームへの配慮が全くなくて、別画面にいくと真っ白になるのがビミョい。WebBrowserが絡むので完全な復元は無理だから、いっかー、とか思ったのが半分はあるのですが、いやまてその理屈はオカシイ。ので、ちょっと何とかさせないとですね。
xaml.csのコードが全体的にマズい。特にOAuth認証の部分はありえない強引さなのでとっとと変更。あと、もう少し適切な分割。MVVMはわからんちん。
Settingsが何か変。IsolatedStorageSettingsというか、その内部のDataContractSerializerの都合というか。これだ!というやり方ないかしら。今のやり方は、非常に間抜け。
ブラウザ画面黒背景はビミョーなので、設定でアプリ全体を白背景に変更するオプションを入れるのもいいかなー。それとアプリ起動時は楽曲を再生中でも自動検索しないのだけど、自動検索してくれたほうが嬉しいかなー。など、細かい点では色々考えること、追加することあります。
まとめ
上手く決まった部分しか解説してないので、実際のコードは残念ながらスパゲッティです:) というか、UI絡みのコードってほとんど書いたことないので、経験の無さが如実に現れていて苦すぃ。WP7で勝手がわからないというもの若干はありますが、それ以前の問題がかなり。サンプルアプリとしても、もう少し良くしたいので、コードは徐々に洗練させていきたいですます。
アプリとしては、まあまあいい出来というか、実用品として悪くないフィーリングだと思うのですが、どうでしょうか?画面周りは、このレイアウトで決まるまで何度も試して投げてを繰り返してこれに落ち着きました。実際に作りながら、試しながらでないとこういうの決められないよね。コードに関してもそうだけど。ともあれ、Pivotいいよねー。やはりWP7といったらPivot。
そんなわけで、RxはWP7開発のお供として欠かせない代物なので、是非使ってみてください。また、WP7で欠かせないということはSilverlightで欠かせないということであり、Silverlightで欠かせないということはWPFでも欠かせないということでも、あったりなかったりするので、Rxによるプログラミングパラダイムの変化を是非とも楽しんでみてください。Linq to Anything!
一記事に収めるため少々駆け足気味だったので、なにか不明な点、質問などありましたら気楽にコメントどうぞ。突っ込みも勿論歓迎です。
Microsoft MVP for Visual C#を受賞しました
- 2011-04-02
Microsoft MVPから、Visual C#カテゴリで受賞しました。for Linqというカテゴリがないので大変厳しいかと思われましたが何とか認められたようです。活動申請には、ほんとうにLinqが~Linqが~ばかりで。今見返すと「活動の源泉は、C#/Linqの素晴らしさを伝えたい!ということです」などとコメント欄に書いちゃっていたり。あと、審査するのは本社の連中だからアメリカンに、大袈裟に書くぐらいがいいんだよ、とアドバイスを受けたので「RxJSによりJavaScriptにおいてもInterective(Enumerable)とReactive(Observable)の美しい融合が果たせることが可能となりました。そのうちのInterective側を支える(linq.js)ことが出来るのは自分しかいない」とかいう恥ずかしいことまで言っちゃってますね、うわあ。
というわけで、公約みたいなものなので、今後もC#/Linqの素晴らしさを伝える道を邁進したいと思います。
私とMVP。MVPアワードの存在を知ったのはXNEWSがマイクロソフトMVPアワードを受賞した時のことで、ただのゲーマーだった頃でした。その頃は何なのか分からず月日は流れ、就職してC#を触りだしてから(2008年)、MVPがどういう存在であるかを知り、以下略。
私が成長できたのは多くの先人の、ネット上のリソースのお陰です。今度は、私が恩返しする番。今まで通り、今まで以上に知識を伝えていけたらと思っています。
Windows Phone 7でJSONを扱う方法について(+ Bing APIの使い方)
- 2011-03-31
C#と親和性の高いデータ形式はXMLです。何と言ってもLinq to Xmlが強力です。また、SOAPも悪くない、というのもVisual Studioの自動生成が効くので何も考えずともホイホイ使えます。ではJSONは、というと、これは割と扱いづらいところがあるのが正直なところ。しかしWindows Phone 7においては、JSONを選択すべきでしょう。なにせ、モバイル機器。ネットワークがとても貧弱。データは小さいに越したことはない。XMLとJSONとでは、雲泥の差です。
WPFではJsonReaderWriterFactory(と、内部にそれを用いたDynamicJson)、SilverlightではSystem.Jsonなどが用意されていますが、WP7には一切ありません。じゃあどうするかといえば、シリアライザを使います。WP7ではDataContractJsonSerializerが標準で用意されている(WPF, SLにもあります)ので、それを使ってデシリアライズしてJSONをオブジェクトに変換するのが基本戦略となります。
外部ライブラリ、Json.NETを使うという手も勿論ありますが。
BingからのJSONの取得
何はともあれ、サンプル題材のJSONを拾ってきましょう。Webからの取得というと、最近はいつもTwitterのPublic Timelineでマンネリ飽き飽きなので、別のものを。WP7なので、Bing APIを使いましょう!Bing APIはIDを取得しないと使えないのでサンプル的にどうよ、というところもありますが、IDの取得は簡単(ほんとワンクリックです)だしWP7と親和性の高いAPIでもあるので、これを機に、試しに取ってみるのも良いのではと思います。画像検索、翻訳など色々種類があるのですが、今回はWeb検索(sources=web)にします。
// 標準WP7テンプレのMainPage.xaml.csにベタ書き
const string AppId = ""; // AppIdは登録してください
Uri CreateQuery(params string[] words)
{
// countなどは変数で置き換えれるようにするといいのではと思います、ここでは固定決め打ちですが
var query =
"?Appid=" + AppId
+ "&query=" + Uri.EscapeUriString(string.Join(" ", words))
+ "&sources=web"
+ "&version=2.0"
+ "&Market=ja-jp"
+ "&web.count=20"
+ "&web.offset=0";
return new Uri("http://api.search.live.net/json.aspx" + query);
}
public MainPage()
{
InitializeComponent();
var wc = new WebClient();
Observable.FromEvent<DownloadStringCompletedEventHandler, DownloadStringCompletedEventArgs>(
h => h.Invoke, h => wc.DownloadStringCompleted += h, h => wc.DownloadStringCompleted -= h)
.ObserveOnDispatcher()
.Subscribe(e =>
{
var json = e.EventArgs.Result; // ダウンロード結果(json文字列)
MessageBox.Show(json);
});
wc.DownloadStringAsync(CreateQuery("地震"));
}
json.aspxにクエリ文字列をつけてGETするだけなので割とお手軽。クエリ文字列がゴチャゴチャして分かりづらいのですが、基本的に弄るのはqueryとweb.countぐらいかな、と思います。BingのReferenceは、生成元のクラス構造がまんま掲示されているだけで、恐ろしく分かりづらいので、適当にサンプルから当たりをつける感じで。
非同期通信の実行はReactive Extensions(Rx)で行います。Windows Phone 7では標準で入っているのでSystem.ObservableとMicrosoft.Phone.Reactiveを参照に加えてください。非同期通信を生でやるなんてありえませんから!Rx利用を推奨します。
得られるJSONは下記のものです。
{
"SearchResponse": {
"Version": "2.0",
"Query": {
"SearchTerms": "地震"
},
"Web": {
"Total": 88,
"Offset": 0,
"Results": [
{
"Title": "地震情報 - Yahoo!天気情報",
"Description": "Yahoo!天気情報は、市区町村の天気予報、世界の天気...",
"Url": "http://typhoon.yahoo.co.jp/weather/jp/earthquake/",
"DisplayUrl": "typhoon.yahoo.co.jp/weather/jp/earthquake",
"DateTime": "2011-03-29T19:11:00Z"
},
{
"Title": "地震情報 :: ウェザーニュース",
"Description": "最新の地震の震度、震源地、震度分布を速報で届けます...",
"Url": "http://weathernews.jp/quake/",
"DisplayUrl": "weathernews.jp/quake",
"DateTime": "2011-03-28T09:10:00Z"
},
// 配列上なので幾つも...
]
}
}
}
JSONに関してはJSON ViewerをVisual StudioのVisualizerに組み込むとかなり快適にプレビュー出来るようになります。が、カスタムVisualizerはWP7では実行出来ないのでテキストで見て、スタンドアロンのものにコピペってのを実行ですね、しょんぼり。
DataContractJsonSerializer
では、JSONをオブジェクトに変換しましょう。基本的には、1:1に対応するクラスを作るだけ。必要に応じて System.Runtime.Serializationの参照を加えDataContract, DataMember属性なども加えればよし。
public class BingWebRoot
{
public SearchResponse SearchResponse { get; set; }
}
public class SearchResponse
{
public string Version { get; set; }
public Query Query { get; set; }
public Web Web { get; set; }
}
public class Query
{
public string SearchTerms { get; set; }
}
public class Web
{
public int Total { get; set; }
public int Offset { get; set; }
public Results[] Results { get; set; }
}
public class Results
{
public string Title { get; set; }
public string Description { get; set; }
public string Url { get; set; }
public string DisplayUrl { get; set; }
public string DateTime { get; set; }
}
JSONは、JavaScriptのオブジェクトとほぼ同一の記述ですが、ようするに{}になっている部分はクラスで、[]になっている部分は配列で、置き換えていけばいい、ということで。難しくはないのですが、面倒くさいには大変面倒くさい。なお、JSONの構造を全部記述する必要はなく、必要なものだけでも構いません、例えばVersionやQueryはいらないから省くとか、全然アリです。
そして、 System.ServiceModel.Web を参照設定に加え、DataContractJsonSerializerを使います。
var wc = new WebClient();
Observable.FromEvent<OpenReadCompletedEventHandler, OpenReadCompletedEventArgs>(
h => h.Invoke, h => wc.OpenReadCompleted += h, h => wc.OpenReadCompleted -= h)
.ObserveOnDispatcher()
.Subscribe(e =>
{
using (var stream = e.EventArgs.Result)
{
var serializer = new DataContractJsonSerializer(typeof(BingWebRoot));
var result = (BingWebRoot)serializer.ReadObject(stream);
MessageBox.Show(result.SearchResponse.Web.Results[0].Title);
}
});
wc.OpenReadAsync(CreateQuery("地震"));
デシリアライズはReadObject、シリアライズはWriteObjectで行います。基本はstreamを渡すだけでオブジェクトの出来上がり。
with Reactive Extensions
ですが、まあ、Resultsが欲しいだけなのにSearchResponse.Web.Resultsは長げーよ、とか、DateTimeがstringでイヤだー、とか色々あります。そういう場合はJSONとのマッピング用のクラスとは別に、アプリケーション側で使うクラスを別に立ててやればいいんぢゃないかしら。
public class SearchResults
{
public string Title { get; set; }
public string Url { get; set; }
public DateTime DateTime { get; set; }
public override string ToString()
{
return DateTime + " : " + Title + " : " + Url;
}
}
TitleとUrlとDateTimeしかいらない!という具合で。これを、今度はWebRequestを使って書くと
var req = WebRequest.Create(CreateQuery("ほむほむ"));
Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse, req.EndGetResponse)()
.Select(r =>
{
using (var stream = r.GetResponseStream())
{
var serializer = new DataContractJsonSerializer(typeof(BingWebRoot));
return (BingWebRoot)serializer.ReadObject(stream);
}
})
.SelectMany(x => x.SearchResponse.Web.Results)
.Select(x => new SearchResults { DateTime = DateTime.Parse(x.DateTime), Title = x.Title, Url = x.Url })
.ObserveOnDispatcher()
.Subscribe(x =>
{
// 加工は全部終わってるのでここで色々自由に処理
Debug.WriteLine(x);
});
となります。最初のSelectは非同期の結果、次のSelectManyではResults[]、つまり普通の配列を平坦化して、以降は普通のLinqのようなコレクション処理をしています。
非同期リクエストとオブジェクトのコレクション処理が、完全にシームレスに溶け込んでいます。これが、RxがLinqとして存ることの真価の一つです。記述が統一され、かつ限りなくシンプルになる。Rxは非同期が、イベントが、時間が、簡単に扱えます。でも、本当の真価は単独で使うというだけでなく、それらが全てPush型シーケンスに乗っていることで、統合することが可能だというところにあります。
でも、むしろ分かりにくい?ふむむ……。慣れの問題、などというと全く説得力がなくてアレですが、しかし、慣れです。記述がシンプルになり、柔軟性と再利用性が増していることには間違いないわけで、後は一度全て忘れてLINQの世界に飛び込んでしまえばいいと思うんだ。
Linqは各処理の単位が細分化されている(Selectは射影、Whereはフィルタ)ことも特徴ですが、これは思考の再利用可能性を促します。非同期->オブジェクト配列=SelectManyなど、単純な定型パターンに落とし込めます。C#はもとより強力なIntelliSenseにより、ブロックを組み立てるかの如きなプログラミングを可能にしていますが、Linqでは、それが更に先鋭化されていると見れます。
まとめ
これも現在製作中のWP7アプリからの一部です。最近Bing API利用に切り替えたので。無駄に汎用化して作りこみつつきりがないので適度なところできりあげつつ。ユニットテスト作ってあったので移行自体は幸いすんなりいった。良かった良かった。テスト大事。
Bing APIの前は諸事情あってGoogleからのスクレイピングでした。スクレイピングはグレーだろうということで代替案をずっと探していて、何とかBingに落ち着きました。最初はどうにも使い物にならない、と思ったのですが、検索パラメータを色々変えて、ある程度望む結果が出るようにはなったかな、と。Bingは結構癖があって、調整大変ですね。その話は後日、WP7アプリが完成したときにでも……。
コード的にはスクレイピングのほうも割と凝ってたんですけどねー、バッサリとゴミ箱行き。復活することは、ないかな。もったいないけどしょうがない。いつかそのうち紹介する日は、来るかも来ないかも。
そんなわけで延々と足踏みしていて実装は相変わらず一歩も進んでませんが(!) 順調に制作は進行中なので乞うご期待。いやほんと。
Widows Phone 7でアプリケーション名やバージョン番号をバインドする方法
- 2011-03-28
アプリケーション名の表示や、about画面に表示したいであろうバージョン番号、どうします?直書きのstringやリソースから?それもいいのですけれど、せっかくプロジェクトのプロパティ(AssemblyInfo.cs)で、アプリケーション名やバージョン番号を設定しているわけだから、そこから利用できたほうがいいですよね。
というわけで、これらの情報はアセンブリから取得しましょう。
public class AssemblyInfoData
{
public static readonly AssemblyInfoData ExecutingAssembly = new AssemblyInfoData(Assembly.GetExecutingAssembly());
public string FileName { get; private set; }
public string Version { get; private set; }
public string FileVersion { get; private set; }
public string Title { get; private set; }
public string Description { get; private set; }
public string Configuration { get; private set; }
public string Company { get; private set; }
public string Product { get; private set; }
public string Copyright { get; private set; }
public string Trademark { get; private set; }
public string Culture { get; private set; }
public AssemblyInfoData(Assembly assembly)
{
var assemblyName = new AssemblyName(assembly.FullName);
FileName = assemblyName.Name;
Version = assemblyName.Version.ToString();
FileVersion = GetAttributeName<AssemblyFileVersionAttribute>(assembly, a => a.Version);
Title = GetAttributeName<AssemblyTitleAttribute>(assembly, a => a.Title);
Description = GetAttributeName<AssemblyDescriptionAttribute>(assembly, a => a.Description);
Configuration = GetAttributeName<AssemblyConfigurationAttribute>(assembly, a => a.Configuration);
Company = GetAttributeName<AssemblyCompanyAttribute>(assembly, a => a.Company);
Product = GetAttributeName<AssemblyProductAttribute>(assembly, a => a.Product);
Copyright = GetAttributeName<AssemblyCopyrightAttribute>(assembly, a => a.Copyright);
Trademark = GetAttributeName<AssemblyTrademarkAttribute>(assembly, a => a.Trademark);
Culture = GetAttributeName<AssemblyCultureAttribute>(assembly, a => a.Culture);
}
private string GetAttributeName<T>(Assembly assembly, Func<T, string> selector) where T : Attribute
{
var attr = assembly.GetCustomAttributes(typeof(T), true).Cast<T>().FirstOrDefault();
return (attr == null) ? "" : selector(attr);
}
}
FileName, Versionはnew AssemblyNameに渡してから(WPFだとGetNameで直に取れるのですが、Silverlightだとセキュリティ違反で例外が飛ぶためこうする必要がある)、それ以外の値はカスタム属性から取得できます。また、任意のAssemblyの情報をコンストラクタに投げて取得出来るようになっていますが、どうせ必要なのは実行アセンブリの情報だけでしょ?ってことで、public staticなフィールドにExecutingAssemblyのデータを公開するようにしています。なので、コードからは、 AssemblyInfoData.ExecutingAssembly.Version とアクセスするだけで、簡単に取得できます。
でも、コードから欲しいということはほとんどなくて、UIに表示するためだけに欲しいのですよね、こういう情報は。バインディングしましょう!まず、こんなクラスを用意します。
public class AssemblyInfoDataBindingHelper
{
public AssemblyInfoData Value { get { return AssemblyInfoData.ExecutingAssembly; } }
}
何故これが必要かというと、WPFの場合は{x:static}でstatic変数もバインド出来るのですが、Silverlight/WP7ではバインド出来ないためです。いやあ、カッコ悪いですね、{x:static}欲しいですね、まあ、ないものはしょうがない。
次にApp.xamlのApplication.Resourcesの中に
<Application.Resources>
<!-- Applicationのところで xmlns:local="ネームスペース" を宣言しておく-->
<local:AssemblyInfoDataBindingHelper x:Key="AssemblyInfoData"/>
</Application.Resources>
と書いてリソースを登録。準備はこれで完了で、あとはバインドするだけ。
<TextBlock x:Name="ApplicationTitle" Text="{Binding Value.Title, Source={StaticResource AssemblyInfoData}}" Style="{StaticResource PhoneTextNormalStyle}"/>
<TextBlock x:Name="PageTitle" Text="{Binding Value.Version, Source={StaticResource AssemblyInfoData}}" Style="{StaticResource PhoneTextTitle1Style}" />
と、以上です。これで下のような感じに
表示されました。ApplicationTitleにTitleは分かりますがPageTitleにVersionは丸っきりイミフ。ボタンは、何となく寂しいから置いただけで意味はないです気にしないで。
AssemblyInfoDataクラスのコードは完全に独立して使い回しが効くので、コピペってどうぞご自由にお使いください。煮るなり焼くなり……、パブリックドメインで。
まとめ
といったのは、一応、今製作中のWP7アプリの一部です。順調に制作は遅れまくり。うむむ。今月中といきたかったのだけど、まーだずれ込みそう。その前は二月中のつもりだったのだけど、Chaining Assertionが思いの外引っ張りすぎて手を付けてる余裕がなかった。とにかく、4月中頃までには、マーケットプレイスで公開したいなあ。あとソースコードも公開します(というか既に製作中のがこっそり公開されてます)。Reactive Extensionsの実践例として、ただたんに非同期で使うというだけじゃなく、こういうケースで使える、コードはこうなる。というサンプルとして役立てればいいな、という思いで書いてますので、適当に待っていてください。
人に見せるためのコード、というのを念頭に置きすぎていて、同じ場所のコードの修正ばかり繰り返していてアプリ全体としては一歩も製作が進まないという超鈍足状態に陥ってますが(コード書きの遅さに定評のある私です(キリッ)、でも、書きなおす度に確実によくなっていく実感はあるので、最終的にそこそこ見せれるコードになるのではないかと思っています。少なくとも、部分的には面白い内容になるはずです。
DynamicAccessor - Chaining Assertion ver.1.4.0.0
- 2011-03-17
テストブームはまだ続いています。さて、テスト可能性の高い設計は良いのですが、本来あるべきである設計を歪めて(単純なところでいえば、virtualである必要でないものをvirtualにするとか、privateであるべきものをprotectedにするとか、無駄なinterfaceとか、不自然な引数の取り方とか)テスト可能性を確保するのは、私は嫌だなー。などと思っていましたが、しかし、モックについてはMolesを使うことで、最高の形で解決しました。
次に何を考えるべきかな、と浮かんだのはprivateのテスト。privateのテストは考えないという流儀もあるし、それは尤もだと思いますが、publicのものをテストするにも、ちょっと確認とりたかったり値を弄ってやりたかったりなど、触れると楽な場合もいっぱいあるので、出来るにこしたことはありません。MSTestにはAccessorの自動生成で完全なタイプセーフとIntelliSenseの保証をしてくれて、それはそれで大変素敵。なのですが、もう少し軽くテスト出来る機構を用意しました。MSTestへの依存はないので、NUnitでも他のテストフレームワークでも使えます。
privateへのアクセスはリフレクションが常套手段ですが、C#4.0ならdynamicがあるよね。というわけで、dynamicで包んだアクセサを用意しました。dynamicにしたからってprivateのものは呼び出せないので、DynamicObjectで包んでリフレクション経由になるようにしています。
AsDynamic()
こんな感じで使えます。
// こんなprivateばっかなクラスがあったとして
public class PrivateMock
{
private string privateField = "homu";
private string PrivateProperty
{
get { return privateField + privateField; }
set { privateField = value; }
}
private string PrivateMethod(int count)
{
return string.Join("", Enumerable.Repeat(privateField, count));
}
}
// AsDynamic()をつけるだけでPrivateプロパティが呼べる
var actual = new PrivateMock().AsDynamic().PrivateProperty;
Assert.AreEqual("homuhomu", actual);
// dynamicは拡張メソッドが呼べないのでIsを使う場合はキャストしてくださいな。
(new PrivateMock().AsDynamic().PrivateMethod(3) as string).Is("homuhomuhomu");
// 勿論setも出来ます(インデクサもいけます。ジェネリックメソッドも若干の制限付きですが呼べます)
var mock = new PrivateMock().AsDynamic();
mock.PrivateProperty = "mogumogu";
(mock.privateField as string).Is("mogumogu");
オブジェクトへの拡張メソッドにより、全てのオブジェクトに対しAsDynamic()が使える状態です。IntelliSense汚染なので通常だとあまり許容できることではないのですが、UnitTestなのでOKだろう、と。AsDynamic()後はDynamicObjectとして、全ての呼び出しがリフレクション経由となり、public/privateのメソッド/プロパティ/フィールド/インデクサに自由にアクセス可能となっています。見た目は普通と全く一緒で大変自然なのがdynamicの利点。
dynamicの状態では拡張メソッドの呼び出しは不可能なので、IsによるAssertionを行う場合は、キャストして型を適用してやる必要があります。メンドクセーという場合はAssert.AreEqualなど、本来用意されているものはobjectが対象なので、そのまんま使えます。どちらでも好き好きでどうぞ。
ダイナミックとジェネリックとメソッド呼び出し
実装内部の話。DynamicObjectでTryInvokeMemberです。んで、最初はすんごく簡単に実装出来ると思ったんですよ!dynamicでリフレクション包むだけね、はいはい、余裕余裕、と。が、実際に書きだすとどうも引っかかる。オーバーロードが。ジェネリックが。型推論が。ふつーに呼んでるとうまくオーバーロードを解決してくれなくて、AmbiguousMatchException(あいまいな一致)を投げてくれます。なので、手動でマッチさせる必要があります。
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
var csharpBinder = binder.GetType().GetInterface("Microsoft.CSharp.RuntimeBinder.ICSharpInvokeOrInvokeMemberBinder");
if (csharpBinder == null) throw new ArgumentException("is not generic csharp code");
var typeArgs = (csharpBinder.GetProperty("TypeArguments").GetValue(binder, null) as IList<Type>).ToArray();
var method = MatchMethod(binder.Name, args, typeArgs);
result = method.Invoke(target, args);
return true;
}
private Type AssignableBoundType(Type left, Type right)
{
return (left == null || right == null) ? null
: left.IsAssignableFrom(right) ? left
: right.IsAssignableFrom(left) ? right
: null;
}
private MethodInfo MatchMethod(string methodName, object[] args, Type[] typeArgs)
{
// name match
var nameMatched = typeof(T).GetMethods(TransparentFlags)
.Where(mi => mi.Name == methodName)
.ToArray();
if (!nameMatched.Any()) throw new ArgumentException(string.Format("\"{0}\" not found : Type <{1}>", methodName, typeof(T).Name));
// type inference
var typedMethods = nameMatched
.Select(mi =>
{
var genericArguments = mi.GetGenericArguments();
if (!typeArgs.Any() && !genericArguments.Any()) // non generic method
{
return new
{
MethodInfo = mi,
TypeParameters = default(Dictionary<Type, Type>)
};
}
else if (!typeArgs.Any())
{
var parameterGenericTypes = mi.GetParameters()
.Select(pi => pi.ParameterType)
.Zip(args.Select(o => o.GetType()), Tuple.Create)
.GroupBy(a => a.Item1, a => a.Item2)
.Where(g => g.Key.IsGenericParameter)
.Select(g => new { g.Key, Type = g.Aggregate(AssignableBoundType) })
.Where(a => a.Type != null);
var typeParams = genericArguments
.GroupJoin(parameterGenericTypes, x => x, x => x.Key, (_, Args) => Args)
.ToArray();
if (!typeParams.All(xs => xs.Any())) return null; // types short
return new
{
MethodInfo = mi,
TypeParameters = typeParams
.Select(xs => xs.First())
.ToDictionary(a => a.Key, a => a.Type)
};
}
else
{
if (genericArguments.Length != typeArgs.Length) return null;
return new
{
MethodInfo = mi,
TypeParameters = genericArguments
.Zip(typeArgs, Tuple.Create)
.ToDictionary(t => t.Item1, t => t.Item2)
};
}
})
.Where(a => a != null)
.Where(a => a.MethodInfo
.GetParameters()
.Select(pi => pi.ParameterType)
.SequenceEqual(args.Select(o => o.GetType()), new EqualsComparer<Type>((x, y) =>
(x.IsGenericParameter)
? a.TypeParameters[x].IsAssignableFrom(y)
: x.Equals(y)))
)
.ToArray();
if (!typedMethods.Any()) throw new ArgumentException(string.Format("\"{0}\" not match arguments : Type <{1}>", methodName, typeof(T).Name));
// nongeneric
var nongeneric = typedMethods.Where(a => a.TypeParameters == null).ToArray();
if (nongeneric.Length == 1) return nongeneric[0].MethodInfo;
// generic--
var lessGeneric = typedMethods
.Where(a => !a.MethodInfo.GetParameters().All(pi => pi.ParameterType.IsGenericParameter))
.ToArray();
// generic
var generic = (typedMethods.Length == 1)
? typedMethods[0]
: (lessGeneric.Length == 1 ? lessGeneric[0] : null);
if (generic != null) return generic.MethodInfo.MakeGenericMethod(generic.TypeParameters.Select(kvp => kvp.Value).ToArray());
// ambiguous
throw new ArgumentException(string.Format("\"{0}\" ambiguous arguments : Type <{1}>", methodName, typeof(T).Name));
}
private class EqualsComparer<TX> : IEqualityComparer<TX>
{
private readonly Func<TX, TX, bool> equals;
public EqualsComparer(Func<TX, TX, bool> equals)
{
this.equals = equals;
}
public bool Equals(TX x, TX y)
{
return equals(x, y);
}
public int GetHashCode(TX obj)
{
return 0;
}
}
泥臭い。LINQ的には普段あまり使わないGroupJoinや、SequenceEqualでのIEqualityComparerとか、ここぞとばかりに色々仕込んで実に楽しげになりました。何とも酷いゴリ押し。速度とかどうなのこれ、ただリフレクションを使っただけじゃないよね、というのは、UnitTestですから。だから、許容される。そうでなければ、やれない。
名前からマッチ->型の当てはめ->実引数からマッチ->非ジェネリックメソッドを優先->引数が全てジェネリックでなければ優先->最終的にメソッドが一つにまで絞り込めなければエラー。コンパイラの行う、正確なオーバーロードの解決法はC#言語仕様書の7.5.3に書いてあります。従っていません。というか、outとかrefとか非対応だし、ジェネリックに関しても持ってる情報が足りなすぎてマッチしたくてもできない。特に痛いのはコード上での型が吹っ飛んでいて、GetTypeによる具象型しか取得できないこと。そのせいで、引数の複数の同一のTは、ほぼ同じ型同士でないとダメになってしまっていて(コード上で宣言しているインターフェイスの情報が取れないのでどうしょもない)。その他、入れ子なジェネリックの場合もコケます(対応させるの面倒くさい)。
でも、ふつーの9割がたなシチュエーションでは動作するはずです。
ちなみに、ジェネリックの型引数は通常、DynamicObjectのBinderでは取得出来ません。Binderの実際の型(の持つインターフェイス)であるICSharpInvokeOrInvokeMemberBinderのTypeArgumentsが持っているのですが、ICSharpInvokeOrInvokeMemberBinderがinternalのため、外側から手出しは出来ないのです。どうするかって、もうここまで来たら何も躊躇うことなくリフレクションです本当にありがとうございました。GetInterface("Microsoft.CSharp.RuntimeBinder.ICSharpInvokeOrInvokeMemberBinder");とか、負けたにも程がある。
そんなわけで、ふつーに作ると存外面倒くさいっぽいです。いや、こんな泥臭くなるのは何かおかしい気はかなりしなくもないんですが、うーん。まあ、泥臭くてもライブラリ側で吸収出来るなら、それはそれでいいかな、と。結果だけを見れば。ライブラリが泥臭さを担保するかわりに、ユーザーはAsDynamic()だけで綺麗に呼び出せる。それはとっても嬉しいなって。
余談
そんなDynamicAccessorのテスト作るのにIsとかAssertEx.Throws、あって良かった。大変助かった。ExpectedExceptionAttributeなんて使ってられない。どっぐふーどどっぐふーど。個人的にはMSTestは凄い好きというか、テストツール選ぶのにあたって優先度が一番高い項目はIDE統合(デバッグ含むというかデバッグ最重要)なので、統合できてないものはその時点でアウトです(どれも、一手間加えれば統合出来るのでしょうけれど)。その上で、Chaining Assertionを使えばMSTestの色々な不満が一気に解消出来て、最高に幸せだなあ、と、自画自賛。
まとめ
当初のIsだけでサクッと軽量なテスト~とかってノリは何処に行ったんでしょうか。ぐぬぬ。それでも、最大限のシンプルさは保ち続けている、と、思いたい。内部がどうあれ、外からはAsDynamic()が足されただけだし、それ自体もシンプルそのものですよね、ね?コンセプトはまだ守れてると、思いたい。
ところで、dynamic使ってます?ぶっちけ全然使ってません。結局のところvarが最高に便利なわけで、dynamicは、例えば以前書いたDynamicJsonであったり、これのようなリフレクションであったりと、通常のC#とは違う場所との糊なわけで、そうそう出番のあるものでもない、ですねん。C#4.0の言語的な追加の最たるものはdynamicなわけですが、普段はそんな使わない代物なわけだと、言語的にはC#4.0はあんま変わらなかったねー、という印象で。LL的な視点から、C#にはdynamicで動的言語でもあるんだって?という意見をたまに見ますが、純C#上ではぶっちゃけほとんど使わないのでそんなでもない。
じゃあなくてもいいか、というと、んー、まあ、このように、たまにある分には便利だし、言語的にもスムースに入り込んでいるので、良いのではないか、むしろ良いのではないか、とは思います。あんま使わないけどたまには思い出してあげると大変可愛い。
そういえば私はメタ構文変数としてhoge, huga, hageの順にhogehogeと使ってるのですが、Twiterでhomuにする。というのを見て、なんかいいな、とか思ってしまったので、当分はhomu, mogu, mamiの順に使おうかと思っている昨今。こーいうのは半年後ぐらいに、あちゃーという気持ちになるのが常なのですがー。
Rx + MolesによるC#での次世代非同期モックテスト考察
- 2011-03-10
最近、妙にテストブームです。Chaining Assertionを作ったからですね。ライブラリドリブンデベロップメント。とりあえずでも何か作って公開すると、その分野への情報収集熱に火がつくよね。そしてテスト厨へ。さて、ユニットテストで次に考えるべきは、モックの活用。C#でモックといえばMoqが評価高い。メソッドチェーンとExpression Treeを活かしたモック生成は、なるほど、良さそうです。読み方も可愛いしね。もっきゅ。もっきゅ。
というわけでスルーして(えー)Molesを使いましょう。Microsoft Research謹製のモックフレームワークです。PexとのセットはMSDN Subscriptionが必要ですが、MolesのみならばFreeです。VS Galleryに置かれているので、VSの拡張機能マネージャーからでも検索に引っかかります。
Moles。Pex and Molesとして、つまりPex(パラメータ自動生成テスト)のオマケですよねー、と考えていたりしたりした私ですが(実際、Pexがこの種のモックシステムを必要とする、という要請があって出来た副産物のよう)、これがオマケだなんてとんでもない!アセンブリ解析+DLL自動生成+ILジャックという、吹っ飛んだ発想による出鱈目すぎる魔法の力でモック生成してしまうMolesは、他のモックフレームワークとは根源的に違いすぎる。
Molesとは何か。既存のクラスの静的/インスタンスメソッドやプロパティの動作を、自由に置き換えるもの。既存のクラスとは、自分の作ったものは勿論、.NET Frameworkのクラスライブラリも例外ではありません。Console.WriteLineやDateTime.Now、File.ReadAllTextなども、そのままに乗っ取ることが可能です。PublicもPrivateも、どちらでも乗っ取れます。
しかも使うのは簡単。往々に強力なものは扱いも難しくなってしまうものですが、常識はずれに強力な魔法が働いている場合は、逆に非常に簡単になります。対象となるメソッドにラムダ式を代入する。それだけ。moqなどよりも遥かに簡単。
Molesを使う
日本語での紹介はMoles - .NETのモック・スタブフレームワーク - Jamzzの日々に、また、MSRのページのDocumentationにLevel分けされた沢山のドキュメントが用意されているので(素晴らしい!Rxも見習うべし!)、そちらに目を通せば大体分かると思われます。
とりあえず使ってみましょう。Molesをインストールしたら、テストプロジェクトを作って、参照設定を右クリックし、Add Moles Assembly for mscorlibを選択。
するとmscorlib.molesというファイル(中身はただのXML)が追加されます。そして、とりあえずビルドするとMicrosoft.Moles.Framework, mscorlib.Behavior, mscorlib.Molesが参照設定に追加されます。つまり、mscorlibが解析され、モッククラスが自動生成されました!mscorlib以外のものも生成したい場合は、参照設定の対象dll上で右クリックし、Add Moles Assemblyを選べば、.molesが追加されます。なお、解析対象が更新されてHoge.Molesも更新したい、という場合はリビルドすれば更新されます(逆に言えばリビルドしないと更新されないため、コンパイルは通るものの実行時エラーになります)。また、もし追加したことによって何かエラーが出る場合(VS2010 SP1で私の環境ではSystem.dllでエラーが発生する)は、.molesの対象アセンブリの属性にReflectionOnly="true"も記載すると回避できることもあります。
では簡単な例を。
// mscorlibに含まれる型の場合のみ、Molesで乗っ取りたい型を定義しておく必要があります
// 定義なしで実行すると、この型定義してね、って例外メッセージが出るので
// それが出たらコピペってAssemblyInfo.csの下にでも書いておけばいいんぢゃないかな
[assembly: MoledType(typeof(System.DateTime))]
[TestClass]
public class UnitTest1
{
// 現在時刻を元に"午前"か"午後"かを返すメソッド
public static string ImaDocchi()
{
return (DateTime.Now.Hour < 12) ? "午前" : "午後";
}
// HostType("Moles")属性を付与する必要がある
[TestMethod, HostType("Moles")]
public void TestMethod1()
{
// ラムダ式で置き換えたいメソッドを定義する
// プリフィックスMが自動生成されているクラス、
// サフィックスGetはプロパティのgetの意味
MDateTime.NowGet = () => new DateTime(2000, 1, 1, 5, 0, 0);
ImaDocchi().Is("午前");
MDateTime.NowGet = () => new DateTime(2000, 1, 1, 15, 0, 0);
ImaDocchi().Is("午後");
}
}
お約束ごと(属性付与)が若干ありますが、エラーメッセージで親切に教えてくれるので、そう手間もなくMoles化出来ます。モック定義自体は何よりも簡単で、見たとおり、デリゲートで置き換えるだけです。非常に直感的。(IsはChaining Assertion利用のものでAssert.AreEqualです、この場合)
システム時刻に依存したメソッドのテストは、単体テストの書き方として、よく例に上がります。そのままじゃテスト出来ないのでリファクタリング対象行き。メソッドの引数に時刻を渡すようにするか、時刻取得を含んだインターフェイスを定義して、それを渡すとか、ともかく、共通するのは、外部から時刻を操れるようにすることでテスト可能性を確保する。ということ。
Molesを使えば、そもそもDateTime.Now自体をジャックして任意の値を返すように定義出来てしまいます。これは単純な例でしかないので、いくら出来てもそんなことやらねーよ、かもですね。はい。それが良い設計かどうかは別としても、Molesの存在を前提とすると、テスト可能にするための設計方法にも、かなりの変化が生じるのは間違いないでしょう。時に、テスト可能性のために歪んだ設計となることも、Molesで乗っ取れるのだと思えば、自然な設計が導出できるはず。
イベントのモック化
続けてイベントの乗っ取りも画策してみましょう。イベントの乗っ取りは、正直なところ少し面倒です。
// こんな非同期でダウンロードして結果を表示するメソッドがあるとして
public static void ShowGoogle()
{
var client = new WebClient();
client.DownloadStringCompleted += (sender, e) =>
{
Console.WriteLine(e.Result);
};
client.DownloadStringAsync(new Uri("http://google.co.jp/"));
}
[TestMethod, HostType("Moles")]
public void WebClientTest()
{
// 外から発火出来るように外部にデリゲートを用意
DownloadStringCompletedEventHandler handler = (s, e) => { };
// AddHandlerとRemoveHandlerを乗っ取って↑のものに差し替えてしまう
MWebClient.AllInstances.DownloadStringCompletedAddDownloadStringCompletedEventHandler =
(wc, h) => handler += h;
MWebClient.AllInstances.DownloadStringCompletedRemoveDownloadStringCompletedEventHandler =
(wc, h) => handler -= h;
// DownloadStringAsyncをトリガに用意したデリゲートを実行
MWebClient.AllInstances.DownloadStringAsyncUri = (wc, uri) =>
{
// DownloadStringCompletedEventArgsはコンストラクタがinternalなので↓じゃダメ
// handler(wc, new DownloadStringCompletedEventArgs("google!modoki"));
// というわけで、モックインスタンス作ってしまってそれを渡せばいいぢゃない
var mockArgs = new MDownloadStringCompletedEventArgs()
{
ResultGet = () => "google!modoki"
};
handler(wc, mockArgs);
};
// 出力はConsole.WriteLineなので、それを乗っ取って、結果にたいしてアサート
MConsole.WriteLineString = s => s.Is("google!modoki");
ShowGoogle(); // 準備が終わったので、実行(本来非同期だけど、全て同期的処理に置き換えられてます)
}
ちょっと複雑です。テストしたい処理はDownloadStringCompletedの中ですが、外からこれを発火する手段は、ない。この例だとAddHandlerだけ乗っ取って、直に発火させてもいいのですが、(非同期だけじゃなく)他のイベントの場合でも応用が効くように、正攻法(?)でいきましょう。イベントの発火を自分でコントロール出来るように、まずはAddとRemoveに対し、外部デリゲートに通すよう差し替えます。なお、インスタンスメソッドを乗っ取る場合は.AllInstancesの下にあるインスタンスメソッドを、静的メソッドと同じようにラムダ式で直に書き換えるだけです。非常に簡単。なお、第一引数は必ず、そのインスタンス自身となっていることには注意。
あとは、トリガとなるメソッドがあればそれを(この場合はDownloadStringAsync)通して、そうでない場合(例えばただのボタンクリックとか)なら直にイベントを乗っ取ったデリゲートを発火してやれば完了。で、ここでEventArgsがコンストラクタがprivateなせいで生成出来なかったりというケースも少なくないのですが、それはモックインスタンスを作って、そいつを渡してやるだけで簡単に回避できます。
少し手順が多いですが、「出来る」ということと、まあ流れ自体は分かるしそれぞれは置き換えるだけで複雑じゃない。ということは分かるのではと思います。でも、それでも面倒くさいですよ。ええ、どう見ても面倒くさいです。しかし、このことはReactive Extensionsを使えば解決出来ます。んが、その前にもう一つ別の例を。
APMのモック化
非同期繋がりで、APM(Asynchronous Programming Model, BeginXxx-EndXxx)のモック化もやってみましょう。
// こんな非同期でダウンロードして結果を表示するメソッドがあるとして
public static void ShowBing()
{
var req = WebRequest.Create("http://bing.co.jp/");
req.BeginGetResponse(ar =>
{
var res = req.EndGetResponse(ar);
using (var stream = res.GetResponseStream())
using (var sr = new StreamReader(stream))
{
var result = sr.ReadToEnd();
Console.WriteLine(result);
}
}, null);
}
[TestMethod, HostType("Moles")]
public void WebRequestTest()
{
// Beginでコールバックを呼ぶ、EndでWebResponseを返す
MHttpWebRequest.AllInstances.BeginGetResponseAsyncCallbackObject =
(req, ac, obj) => { ac(null); return null; };
MHttpWebRequest.AllInstances.EndGetResponseIAsyncResult = (req, ar) =>
{
return new MHttpWebResponse
{
GetResponseStream = () => new MemoryStream(Encoding.UTF8.GetBytes("bing!modoki"))
};
};
MConsole.WriteLineString = s => s.Is("bing!modoki");
ShowBing(); // 実行
}
イベントよりは少し簡単ですが、BeginとEndの絡み具合は混乱してしまいます。また、HttpWebResponseのダミーを作るのも面倒。
Reactive Extensions
見てきたように、イベントもAPMも、モック化は面倒です。そこで出てくるのがReactive Extensions。RxならばIObservableとして一つにまとまるので、その一点をモック化してしまえばそれだけですむ、しかもダミーのIObservableを生成するのは非常に簡単!というわけで、例を見ましょう。モック化、の前に非同期のRx化と、そのテストを。
// こっち本体
public class Tweet
{
public string Name { get; set; }
public string Text { get; set; }
// 実際やるなら静的メソッドじゃなくて、API操作はまとめて別のクラスで、と思いますが、まあとりあえずこれで
public static IObservable<Tweet> FromPublicTL()
{
var req = WebRequest.Create("http://twitter.com/statuses/public_timeline.xml");
return Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse, req.EndGetResponse)()
.Select(r =>
{
// StreamはSilverlightでも同期で書けるので、同期で取得しちゃいます
using (var stream = r.GetResponseStream())
using (var sr = new StreamReader(stream))
{
return sr.ReadToEnd();
}
})
.SelectMany(s => XElement.Parse(s).Elements()) // 配列上のものをバラして
.Select(x => new Tweet // Tweetに変換
{
Text = x.Element("text").Value,
Name = x.Element("user").Element("screen_name").Value
});
}
}
// こっちがTest
[TestMethod]
[Timeout(3000)] // Timeoutはテスト全体のオプションで設定してもいいね
public void FromPublicTL()
{
var tl = Tweet.FromPublicTL().ToEnumerable().ToArray();
// 20件あって、NameとかTextが
// 全部空じゃなければ正常にParse出来てるんじゃないの、的な(適当)
tl.Length.Is(20);
tl.All(t => t.Name != "" && t.Text != "").Is(true);
}
Twitterのpublic_timeline.xml、つまり認証のかかってない世界中のパブリックなツイートが20件(オプション無しの場合)XMLで取れるAPIを叩いています。RxのFromAsyncPatternを使い、リクエストは非同期。非同期のテストは通常難しい、のですが、Rxの場合はFirstやToEnumerableで簡単にブロックして同期的なものに変換出来るため、それで結果を取って、何食わぬ顔でアサートしちゃえます。
Rxは非同期が簡単にテスト出来てメデタシメデタシ。これはこれで良いのですが、ところでパブリックじゃなくて認証入るものを取るときはどうするの?ストリーミングAPI(ツイートだけじゃなくFavoriteなど色々な形式のXMLが届く)を試したいけど、誰かがFavoriteつけるまで待機とか、テストに不定な時間がかかるものはどうするの?などなどで、本物のウェブ上のデータをテストで毎回取ってくるのは大変です。また、誤ったデータが流れてきた/サーバーが応答不能状態な場合などの例外処理のテストは、通常では出来ないですね?
そこで、モック。ウェブからじゃなくてモックがダミーのデータを返せばいいわけだ。そして改めてFromPublicTLメソッドを見ると「データ取得」と「データパース」の二つを行っている。なので、ここはその二つに分けて、後者の「データパース」がモックでテスト出来るようにしてやりましょう。
public class Tweet
{
public string Name { get; set; }
public string Text { get; set; }
private static IObservable<String> GetRawPublicTL()
{
var req = WebRequest.Create("http://twitter.com/statuses/public_timeline.xml");
return Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse, req.EndGetResponse)()
.Select(r =>
{
using (var stream = r.GetResponseStream())
using (var sr = new StreamReader(stream))
{
return sr.ReadToEnd();
}
});
}
public static IObservable<Tweet> FromPublicTL()
{
return GetRawPublicTL()
.SelectMany(s => XElement.Parse(s).Elements())
.Select(x => new Tweet
{
Text = x.Element("text").Value,
Name = x.Element("user").Element("screen_name").Value
});
}
}
リファクタリングというほど大仰なものでもなく、メソッドチェーンのうちのネットワークアクセス部分をprivateメソッドとして切り出しただけです。Rxは、メソッドチェーンの一つ一つが独立しているので、切った貼ったが簡単なのもメリット。では、このprivateメソッドをMolesで差し替えてしまおう!
[TestMethod, HostType("Moles")]
public void FromPublicTLMock()
{
// これは省略した文字列ですが、実際は取得したXMLをファイルに置いて、それを読み込むといいかも
var statuses = @"
<statuses>
<status>
<text>Hello</text>
<user>
<screen_name>neuecc</screen_name>
</user>
</status>
<status>
<text>Moles</text>
<user>
<screen_name>xbox99</screen_name>
</user>
</status>
</statuses>
";
// 本来ネットワーク取得のものを、たった一行でただのシーケンスに置き換える
MTweet.GetRawPublicTL = () => Observable.Return<string>(statuses);
var tl = Tweet.FromPublicTL().ToEnumerable().ToArray();
tl.Length.Is(2);
tl[0].Is(t => t.Name == "neuecc" && t.Text == "Hello");
tl[1].Is(t => t.Name == "xbox99" && t.Text == "Moles");
}
これだけです。データ用意は別として、モックへの差し替えはたった一行書いただけ。既存のコードに一切手を加えず、こんなにも簡単にモックへの置き換えが可能だなんて、わけがわからないよ。
理由として、Rxの持つ非同期もイベントも普通のシーケンスも、全て等しく同じ基盤に乗っている、という性質が生きています。この性質は時に分かりづらさを生むこともありますが、しかしそれ故に絶大な柔軟性も持っていて、その結果、本来非同期処理のものをただのシーケンスに置き換えることを可能にしています。非同期が、イベントがテストしづらいならMolesでただのシーケンスに差し替えてしまえばいい。別段「テストのため」の設計を意識しなくても、Rxで書くということ、それだけで自然にテスト可能な状態になっています。
なんて、さらっと流してしまっているわけですが、この事に気づいた瞬間にこれはヤバい!と悶えました。いや、凄いよ、凄過ぎるよRx + Moles。
Moq vs Moles、あるいは検証のやり方
Molesは非常に強力ですが、ではMoqと、どう使いわけよう?もしくは、全て代替出来てしまう?Molesは純粋な置き換えのみなので、呼び出しの検証はありません。モックとスタブの用語の違い、を言うならば、Molesの提供するものはモックではなくスタブ。自動生成クラスにつくプリフィックスのSは勿論Stubですが、MはMockではなく、Moleを指します(じゃあMoleって何よ、っていうと、何なんでしょうね……)
さて、使い分けとかいうほどのものでもないので、基本はMolesのみでいいんじゃないかなあー。もし呼び出しを保証したければ、こういうふうに書ける。
// IDisposableのDisposeは1回しか呼ばれないとしたい場合を検証する
// インターフェイスの場合はMHogeではなくSHogeなことに注意
var callCount = 0;
var mock = new SIDisposable()
{
Dispose = () => { callCount += 1; }
};
(mock as IDisposable).Dispose(); // mockを使った処理があるとする...
callCount.Is(1); // 1回のみ
フレームワークに用意されていないから一手間なのは事実ですが、Molesの持つシンプルさを失ってまで足したいほどでもなく、好きなようなチェックを自前で書けるのだから、それでいいかな。むしろこのほうが大抵スッキリ。といったようなことは、MolesのマニュアルのComparison to Existing Frameworksに書かれています。Molesの提供するシンプルさが、私は好きです。
フレームワークは最大限のシンプルさを保って、機能は他の機構に回すというのはChaining Assertionも一緒ですよ←比較するとはなんておこがましい
もう一つ、もっと具体的なもので行きましょうか。LinqのCount()はICollectionの場合は全部列挙せず、Countプロパティのほうを使ってくれる(詳細は過去記事:LinqとCountの効率をどうぞ)ことのテスト。
var countCalled = false;
var enumeratorCalled = false;
var mock = new SICollection01<int>
{
CountGet = () => { countCalled = true; return 100; },
GetEnumerator = () => { enumeratorCalled = true; return null; }
};
// 呼んでるのはLinqのCount()のほうね
mock.Count().Is(100);
countCalled.Is(true);
enumeratorCalled.Is(false);
CountGetで100返せば、それだけでいい気もしますが、念のため+意図を表明するということで。
そういえばですが、Chaining AssertionのIsは、散々DisったAssertThatに存外近かったりします。 Assert.That(actual, Is(expected)) と書くものを、 actual.Is(expected) と書けるようになった、ですから(但しこれはAreEqualsの場合であって、Shuold.Be.GreaterThanとかやり始めたらぶん殴る)。
Silverlight? Windows Phone 7?
Silverlightのテスト環境は貧弱です。当然それに連なってWP7のテスト環境も貧弱です。というかMSTestが使えない!というだけじゃなく、Molesも動かせませんし。どうする?そこは、「リンクとして追加」でSilverlight/WP7のファイルをWPFのプロジェクトにでも移して、そのWPFのコードをテストするという手段が取れなくもないです。非同期周りはRxが吸収出来るし、互換性は、元来クラス群が貧弱なSLのほうが第一ターゲットなので、まあまあ大丈夫なはず。ViewModelはともかくとして、Modelのテストなら行けるはずです。
非同期のテストは難しいって?うん、Rxを使えば簡単なんだ。大丈夫。
まとめ
次世代というか、もう現世代なんですよ。今まで理想論に過ぎなかったものを、急速に現実のものとしてくれています。徒手空拳では難しい領域はいっぱいあった。でも、今、手元にはRxとMolesがある。この二つを手に、もう一度領域を見てみたらどうだろう?晴れた景色が広がっているはずです。
それにしてもRxの素晴らしさがMolesで更に輝くことといったらない。
今回はRxと組み合わせた例を中心に説明しましたが、Molesは単体でも文句なく素晴らしい。Moqも悪くないけれど、選ぶならMolesです。とにかく抜群に使いやすい。機能が極まっていることと、APIのシンプルさは両立するんだって。自動生成を活かしきった事例ですねー。VSとのシームレスな一体化といい、文句のつけようがない。ついこないだまで軽視していた私が言うのもアレですが、これがそんなに知られていない(少なくともググッて引っかかる記事はid:jamzzさんの記事だけだ)のは勿体無い話。Moles、是非試してみてください。
C#(.NET)のテストフレームワーク4種の比較雑感
- 2011-03-03
for MSTestをやめて、NUnitとMBUnitとxUnit.NETにも対応しました。MSTestに限定していたのは、単純に他のを入れて試すの面倒くせー、というだけの話であり、そういう態度はいけないよね、と思ったので全部入れました。NUnitはDocumentだけは読んでかなり参考にしてたのですが、他のは全くはぢめて。MSTest以外はみんな野心的に開発進んでるんですね。比べると機能面では一番見劣りするMSTest。
というわけで、対応させるために各種フレームワークを入れる&多少触ったので、それらの紹介/感想などを書きたいと思います。C#上というか.NET上、ですね。の前に、更新事項が幾つかあるのでそれを。まず、CollectionAssertに等値比較するラムダ式を受けるオーバーロードを追加しました。
var lower = new[] { "a", "b", "c" };
var upper = new[] { "A", "B", "C" };
// IEqualityComparer<T>かFunc<T,T,bool>を渡して、コレクションの任意の演算子で等値比較
lower.Is(upper, StringComparer.InvariantCultureIgnoreCase);
lower.Is(upper, (x, y) => x.ToUpper() == y.ToUpper());
// Linq to ObjectsのSequenceEqualを使えば今までも出来なくもなかったのですが!
lower.SequenceEqual(upper, StringComparer.InvariantCultureIgnoreCase).Is(true);
SequenceEqual使えばいいぢゃん(キリッ って思ってました。Isのオーバーロードがかなり嵩んでいて、オーバーロードのIntelliSense汚染が始まってる。IntelliSense = ヘルプ。であるべきなのですが、数が多かったり似たようなオーバーロードが多いと、混乱を招いてしまい良くないわけです。だいたいがして、以前にAllを拒んでいたのにSequenceEqualはアリなのかよ、という。一応弁解すると、CollectionAssert自体に比較子を受けるオーバーロードがあるので、IsがCollectionAssertを示すのなら、それを使える口を用意すること自体は悪くないかな、と。あと、SequenceEqualはIEqualityComparerなのが使いづらい。ラムダ式を渡せる口がないので、こちらに用意して回避したかったというのもあります。AnonymousComparer - lambda compare selector for Linqを使えばラムダ式渡せますが。
それにしても、本来のものはIComparerを受けるんですよ、ジェネリックじゃないほうの。今時ノンジェネリックを強要とか、しかもIComparer<T>とIComparerは別物なのでIComparer<T>として定義すると使えないという。ありえない!そもそも何故にIComparerなのか、という話ではある。大小比較じゃないので、等値として0しか判定に使ってませんもの。それならIEqualityComparer<T>だろ、と。GetHashCodeの定義が(不要なのに)面倒だから、そのせいかなー。 そう考えると分からなくもないので、Func<T, T, bool>を用意しておきました。ラムダ式は最高よね。
それと、今回、コレクションでのIsの動作を変更しました。今までは欠陥があって、例えばlower.Is(upper)とすると、オーバーロードがかち合ってCollectionAssertではなくAreEqualsのほうで動いてしまってました。これは、大変望ましくない。オーバーロードだけだと解決しようがないので、内部で動的にIEnumerbaleかを判定した上でCollectionAssertを使うようにしてやりました。
例外のテスト
他のテストフレームワークを色々見たのですが、例外は属性じゃなくてラムダ式でのテストをサポートするのも少なくない。というかMSTestだけですが、それがないのは。属性だと、範囲が広すぎる(メソッドをテストしたいのに、コンストラクタがテスト範囲に含まれ、コンストラクタが例外を吐いてしまったらメソッドのテストにならない)問題があるので、ラムダ式で書けるほうがいい、という流れのようで。その他に1例外テスト1メソッドが強制されて面倒くさいー、引数のチェックぐらい、1メソッドに全部突っ込んでおきたい、というモノグサな私的にもラムダ式のほうが好み。というわけで、移植移植。
// Throes<T>で発生すべき例外を指定する
AssertEx.Throws<ArgumentNullException>(() => "foo".StartsWith(null));
// 戻り値も取得可能で、その場合は発生した例外が入ってます
var ex = AssertEx.Throws<InvalidOperationException>(() =>
{
throw new InvalidOperationException("foobar operation");
});
ex.Message.Is(s => s.Contains("foobar")); // それにより追加のアサーションが可能
// 例外が起きないことを表明するテスト
AssertEx.DoesNotThrow(() =>
{
// code
});
これを入れるのに伴い、拡張メソッド置き場のクラス名をAssertExに変更しました。それに加え、partial classにしたので、独自に何かメソッド置きたい場合に、AssertExに追加することが可能です。.csファイル一つをポン置きでライブラリと言い張るからこそ出来るやり口……。割とどうでもいい。
テストフレームワーク雑感
Assertちょっと触った程度でしかないので、本当に雑感ですが、一応は各フレームワークを触ったので感想など。
非常に何とかUnitっぽい仕上がり。おお、これこれ、みたいな。ある種のスタンダード。他のテストフレームワークにあるあの機能が欲しいなあ、みたいなものもしっかり取り入れてる感で、全く不足なく。情報も豊富、周辺環境も充実。
不満は、Assert.That。これJavaのJUnit発祥なのかしらね。Hamcrest?まあしかしともかく、酷い。英文のようで読みやすいとか入力補完ですらすらとか、ないない。これが本当に読みやすい?書きやすい?ありえないでしょ……。Is.All.GreaterThanとか、ただのシンタックス遊び。ラムダ式のないJava(or .NET2.0)ならともかく、現在のC#でそれやる意味はどこにもない。
かなり独自な方向に走っている印象。Gallioというプラットフォーム中立なテストシステムを立てて、その上にNUnitやMSTest、そしてGallioの前身でありテストフレームワークのMbUnitなどが乗っかる。という、壮大なお話。IronRubyでRSpec、など独自にテストシステムを立てられるほどに需要がなさそうな、でもあると絶対嬉しいと思う人いるよね、といったものを一手に吸収出来るかもです(実際RSpecは乗っかってる模様)。そんな壮大な話を出すだけあって、テストランナーとしてのGallioの出来はかなり良いように見えます。
MbUnit自体は可もなく不可もなく。属性周りとかは独特なのかなあ、単純なアサート関数しか使っていないので、その辺は分かりません。
ちなみに、今回紹介するテストフレームワークの中で、唯一NuGet非対応。対応に関しては議論されたようですが、どうもGallioのプラグインを中心とする依存関係が、現在のNuGetだと上手く対応させられないそうで。将来のNuGetでも対応するような仕組みへの変更は今のところ考えてない、とNuGet側から返答を貰っているみたいなので、当面はMbUnitのNuGet入りはなさそうです、残念。まあ、若干大掛かりなGallioのインストール込みで考えたほうが嬉しいことも多く、NuGet経由での必要性は薄いから、それはそれでしょうがないかな、といったところ。
非常に独特で、旧来のテストの記述法を徹底的に見直して新しく作り直されています。とっつきは悪いのですが(如何せん、他と比べて全然互換がない)良く考えられていて、素晴らしいと思います。
例えば、CollectionAssertはなく、Assert.Equalが万能に、Objectの場合はEqualsで、Collectionの場合は列挙しての値比較で行ってくれます。つまり、この辺はChaining AssertionのIsと同じ。旧来のしがらみに囚われず、Assert関数はどういう形であることがベストなのか、ということを突き詰めて考えると、そうなる。と思う。
ただ、非常に厳密な比較を行うので、型が違う(IEnumerable<T>とT[]とか)とFailになります。Chaining AssertionのIsは、ゆるふわに列挙後の値比較だけで判定します。どちらが良いのかは、正しいテスト、ということを考えればxUnit.NETのほうなのでしょう。私は、その辺は、とにかく「書き味」優先で振りました。型比較の厳密さは例外テストのThrowsメソッドにも現れていて、MSTestやMbUnitは派生型も含め一致すればSuccessとしますが、xUnit.NETは厳密に一致しないとFailになります。Chaining AssertionのThrowsは厳密一致のほうを採用しました。
正しいテストを書くために、テストフレームワークはかくあるべき、という強い意志でもって開発されている感じ。これは、相当に良いと思います。MSTest以外のものを使うなら、私はこれを選びたい。付属のテストランナーは貧弱ですが、Gallioを使うことで克服出来ます。
- MSTest
唯一TestCaseがない。これがたまらなく不便なんです!かわりに強力なデータソース読み込みが可能になっているようですけれど、強力な分、セットアップに手間がかかってダルいという。他は、いたって普通。ふつーのAssert関数群とふつーの属性でのテスト設定。このノーマルっぷりは標準搭載らしいかもです。最大の欠点はウィザードで自動生成されるテンプレコードがどうしょうもなくクソなこと。あのノイズだらけのゴミは何とかすべし。
最大のメリットは完全なVisual Studio統合。サクッとデバッグ実行。何て素晴らしい。標準搭載で準備一切不要なのも嬉しい。昨今のテストの重要度を考えると、Express EditionにもMSTest入ってるべきですねえ。ちょっと弱いAssert関数群とTestCaseがないことは、Chaining Assertionを使えば補完されるので、全体的に割と問題なくなって素敵テストフレームワークに早変わり。TestCaseに関してはモドきなので若干弱いけれど。
番外編。僕と契約してPexをより強固にしようよ!Code Contracts + Pexは後ろにMicrosoft Researchがいる.NETならではの強烈さだなあ、と。とりあえずVS2010 後の世代のプログラミング (2) Pex « kazuk は null に触れてしまったの素晴らしい導入記事を読んで試せばいいと思うんだ。そういえば、Pex開発者はMbUnitのプロジェクト創設者(学生時代にMbUnit作って、その後MSRに行ったそうで、凄いね...)だそうですよ。
比較しての相違点
他のテストフレームワークや拡張補助ライブラリと根源的に異なるのは、CollectionAssertに対する考え方です。Linq to Objectsに投げればそれが一番でしょ?という。末尾に.Isなのは、それが一番Linq to Objectsを適用して返した場合に書きやすいから。Linqはインフラ。これを活用しないなんて勿体無い。ドキュメントにそれを書くことで、公的に推奨している、ということを押し出している、つもりです。
まとめ
xUnit.NETはかなり素晴らしいんじゃないかと思います。IDE完全統合という点で、私はMSTestを選んでしまいますが、MSTest以外から使うものを選択するのならば、xUnit.NETにしたいところ。周辺環境がまるで整ってない感はありますが、その辺はGallioを使えば吸収出来るっぽいので、セットで、みたいなところかしらん。
テストは別に同一言語である必要はない、ので、CLRの特色を活かせば、IronRubyでRSpecというのは魅力的な道に見えます。今回は試せていないのですが、いつか試してみたい。F#を用いてのテストフレームワークも良さそう。F#は、特にテストのような小さい単位ではF# Interactiveに送りながら書けるのが強烈なアドバンテージになるよね。ユニットテストぐらいの小さな単位なら、デバッガよりもむしろこちらのほうが小回り効いて書きやすそう。
私的にはChaining Assertionはそれらと比べても、全く引けを取らない書きやすさがあると思っています。C#はクラス定義などを除けば、コード本体自体の書き味はライトウェイトなのですよ、ね、ね!
それにしてもAssertThat一族の毒はどうにかならないのかねえ……。AssertThatがない、という点だけでxUnit.NET一択ですよほんと。F#などでも、この形式を踏襲しているのを見ると大変モニョる。こういうのがビヘイビアドリブンなんですかね?ただのシンタックス遊びなだけで、そーいうの違くない?って。でもAssertで詳細なデータが出ない?Expression Treeを解析して詳細なデータを出しゃあいいわけで。どっかの言語の仕組みを持ってこないで、C#の持つ特色、強みであるExpression Treeを活かす方向で動けないものなのか。
別にJavaだからどうとか言うつもりではないです。大切なのはあらゆる言語を見つめて、良いものを取り入れることでしょう?例えば、Groovyの素晴らしいPower AssertをC#でやろうとするプロジェクト expressiontocode など。C#に取り込もうとするなら、こちらのやり方でしょう。時系列を考えればそれは違う、という話もあるでしょうけど、現在の、これからの姿勢として。本当に良いものは何なのかを、考えよう。それを実現するための力がC#には備わっているのだから。
Chaining Assertion ver 1.1.0.1
- 2011-02-28
リリースから、意外にも色々と反響を頂き、ありがとうございました。そんなわけで、意見を考慮した結果、メソッド増えました。当初のスタンスは、シンプルにIsのみでやる、というところだったので、現在結果的に4x2つある、という状況は理想的には望ましくないのですが、現実的にはしょうがないかな、といった感です。以下が、Chaining Assertionの全拡張メソッド。
Is
IsNot
IsNull
IsNotNull
IsInstanceOf
IsNotInstanceOf
IsSameReferenceAs
IsNotSameReferenceAs
IsNotはまんまで、AreNotEqualです。IsSameReferenceAsはAreSameで、参照比較。メソッド名長いね!ただ、私はAreSameという名前は曖昧で何がSameなのか(値がSameとも取れるじゃないか!)よく分からず嫌いだったので、Referenceをメソッド名に入れるのは確定でした。その上で、IsReferenceEqualsにするかで悩みましたが、ここはAreSameであることを想起させるためにも、Sameを名前に含めることにしました。
拡張のジレンマ
最も多く使う部分のみに絞って最少のものを提供する。という思想がありました。だからNotもSameも頻度的には下がるから不要で、それらが使いたい場合はAssert.AreSameといったものを使えばいい、と。従来のものを完全に置き換えるものじゃなく、要点を絞って、処方を出す。
でもまあ、Isのみにこだわりすぎるのって、逆に良くないよね、と。事実妥協してIsNull/IsNotNullは入れていたわけだし、もう少し妥協したっていいのではないかと。と、たがが外れた結果、増量しすぎた……。これだけ増えると、Isだけで済むんだよ!簡単だよ!強力だよ!という文句に説得力に欠けていってしまう。
なので、このまま拡張し続ける気はありません。メソッドが増えると、学習コストの増加とIntelliSense汚染が出てきてしまうので、便利メソッドを増やすのは、ないかな、と。特にStringやCollection周りには、Isのような汎用的なものではなく、もっと特化的なものがあれば「便利」なのは違いないのですが、でも、それ本当に便利かな、って。ラムダ式を使えば十分代替出来る、Linq to Objectsを使えば十分代替出来る。なら、それでいいよね、と。
例えば seq.All(pred).Is(true) が失敗する時、これだとどの値がfalseになったのか分からない。判定をLinq to ObjectsのAllに任せているから。これがもし、 seq.IsAll(pred) といったような、専用メソッドが用意されていれば、細かいエラーメッセージを出すことが出来て便利なのは間違いない。
でもAllだけ?他のは?Allだけ特別扱いする理由もない。じゃあ全部のLinq to Objectsを実装するか、といったらありえない!実装の手間・俺々実装によるバグ混入の可能性、そしてIntelliSense爆発。学習コストの増大(まあ、IsXxxでXxxをLinq to Objectsのものと同一にする、という原則を貫けば増大はしませんが)。
// ソート済みであるかどうかのチェック(NUnitのIsOrderedにあたるもの)を、Linq to Objectsで
var array = new[] { 1, 5, 10, 100 };
array.Is(array.OrderBy(x => x));
Linq to Objectsの汎用性の高さ!既存の仕組みに乗っかれる場合は、全力で乗っかりたい。DynamicJsonもそうで、扱いづらいJsonReaderWriterFactoryにdynamicの皮を被せているだけにすぎない、という。
まとめ
Assert.Hogeよりも圧倒的に書きやすく美しく短くなるというほかに、圧倒的に読みやすくなります。読みやすさはメンテナンス性の向上に繋がる。一生懸命テストを書いたはいいものの、面倒くさくてメンテ放置になって、ゴミの山になってしまった。という経験が、私はある(えー)。そんなわけで、書きやすさは何より大事だし、読みやすさまで手に入るなら僥倖だし、かつ、学習コストも最小限。という売り文句なので、是非試してみてもらえればと思います。
// 今回追加された細々としたもの
// Not Assertion
"foobar".IsNot("fooooooo"); // Assert.AreNotEqual
new[] { "a", "z", "x" }.IsNot("a", "x", "z"); /// CollectionAssert.AreNotEqual
// ReferenceEqual Assertion
var tuple = Tuple.Create("foo");
tuple.IsSameReferenceAs(tuple); // Assert.AreSame
tuple.IsNotSameReferenceAs(Tuple.Create("foo")); // Assert.AreNotSame
// Type Assertion
"foobar".IsInstanceOf<string>(); // Assert.IsInstanceOfType
(999).IsNotInstanceOf<double>(); // Assert.IsNotInstanceOfType
ようするところ、AssertThatを更に凝縮したような感じです。また、 Assert.That(array, Is.All.GreaterThan(0)) なんてのは array.All(x => x >= 0).Is(true) のほうがずっと良くない?って。思うわけです(但し、Assert.Thatはエラーメッセージがずっと親切)。なお、Is.All.GreaterThanは、一見IntelliSenseが効いて書きやすそうに見えるけれど、無駄な連鎖によりイライラさせられるだけで別に全然書きやすくない。連鎖において、一つ一つの処理に重みがないものは、どうかなあ。ただのシンタックス遊びだよ、こんなの(ラムダ式のないJavaでの苦肉の策だというのなら分かる)。
それと、基本的にオブジェクトへの拡張メソッドというのは、影響範囲が広すぎて禁忌に近いわけですが、ユニットテストという範囲が限定されている状況なので、許容できるのではないかな、と思っています。しかし8つはなぁ、多すぎかなあ……。今も、これで良かったのか相当悩んでます。InstanceOfは不要だったかも、typeof書きたくなかったというだけじゃ理由としてアレだし、メソッド増やすほどの価値はあったのか、多分、ないよね、うぐぐ。でも、これでほぼ全てがチェーンで書けるようになりました。それはそれでヨシと思うことにします。
メソッドチェーン形式のテスト記述ライブラリ
- 2011-02-24
昨日の今日で特に更新はないのですが、せっかく画像作ったので再解説。命名は見たとおりに、メソッドチェーンで切らさずアサーションが書ける!ということから付けました。テストを、限りなくシンプルに素早く迷いなく書けるようにしたかった。その答えが、メソッドチェーンで最後に値を取ることです。
// 全てIs一つでメソッドチェーンで流るまま!
Math.Pow(5, 2).Is(25);
"foobar".Is(s => s.StartsWith("foo") && s.EndsWith("bar"));
Enumerable.Range(1, 5).Is(1, 2, 3, 4, 5);
Assert.AreEqualの最大の問題は、どっちがactualでどっちがexpectedだか悩んでしまうこと。一秒でも引っかかったら「気持よくない」のです。テストはリズム。リズムを崩す要素は極限まで潰さなければならない。Chaining Assertionならば、引数として与えるのはexpectedだけなので、全く悩む必要がなくなる。些細なことですが、しかし、大きなことです。
また、この手のメソッドチェーン式のものでよく見られるのが「流れるようなインターフェイス」を名乗って、自然言語のようにチェーンで書けるようにする、などというものがありますが、滑稽です。EqualTo().Within().And()だの.Should().Not.Be.Null()だの、馬鹿げてる。ラムダ式なら一発だし、そちらのほうが遥かに分かりやすい。DSLは分かりやすく書きやすくすることを目指すものであって、形式主義に陥ることじゃない。自然言語風流れるようなインターフェイスは二度とDSLを名乗るべきではない。
もう一つの問題は、無駄に沢山あるアサート関数。覚えるのは面倒。特に、コレクション関連。ぶっちゃけ全然扱いやすくなく、そして、私達にはずっとずっと扱いやすいLinq to Objectsがあるじゃないか。というわけで、コレクションのテストをしたい時は、Linq to Objectsで結果まで絞ってIsで書くという手法を推奨します。
new[]{1, 3, 7, 8}.Contains(8).Is(true);
new[]{1, 3, 7, 8}.Count(i => i % 2 != 0).Is(3);
new[]{1, 3, 7, 8}.Any().Is(true);
new[]{1, 3, 7, 8}.All(i => i < 5).Is(false);
ね、このほうがずっと書きやすいし柔軟に書ける。
非同期のテスト
非同期のテストは難しい。結果が返ってくるのが非同期なのでテストを抜けてしまうので。ではどうするか、答えは、Rx使えば余裕です。例として以下のコードは今こそこそと作ってるWP7アプリ用のテストです。
[TestMethod]
public void Search()
{
// SearchLyricはWebRequestのBeginGetResponseで非同期に問い合わせるもの
// 結果はIObservableを返す非同期なものなので、ToEnumerableして同期的に待機する
var song = new Song { Artist = "吉幾三", Title = "俺ら東京さ行ぐだ" };
var array = song.SearchLyric().ToEnumerable().ToArray();
array.Count().Is(1);
array.First().Title.Is("俺ら東京さ行ぐだ 吉幾三");
array.First().Url.Is("http://music.goo.ne.jp/lyric/LYRUTND1127/index.html");
}
FirstとかToEnumerableで、非同期をサクッとブロックして同期的に待機してしまえば簡単に値を確保できてしまいます。とまあ、そんなわけで非同期処理は全部Rxで行うよう統一すると、こういうところで物凄く楽になるわけですね、素晴らしい。だからもう非同期プログラミングにRx無しとか全方位でありえないわけです。
といっても、Rxなんて使ってないし!という場合は、こんなものが。例は恣意的すぎますが
[TestMethod]
public void SpinUntilTest()
{
int number = 0;
// 非同期処理をしてるとする
ThreadPool.QueueUserWorkItem(_ =>
{
Thread.Sleep(3000); // 重たい処理をしてるとする
number = 1000;
});
// 指定条件が成立するまで待機
SpinWait.SpinUntil(() => number != 0, 10000); // Timeout = 10秒
number.Is(1000);
}
Pxチームの記事 SpinWait.SpinUntil for unit testing で見たのですが、SpinWait.SpinUntilが結構使えそうです。Thread.Sleepでタイムアウトいっぱいまで待つとか、手動でManualResetEventを設定する、などなどに比べると遥かにサクッと書けて良さそう。ていうかSpinWait.SpinUntilなんて初めて知りましたよ、本当にhidden gems!
まとめ
テストのないコードはレガシーコード。と、名著が言ってるので
Chaining Assertionで苦痛を和らげて、素敵なテスト生活を送りましょう。
XboxInfoTwit - ver.2.3.0.3
- 2011-02-24
Xbox.comの認証周りが変わってログイン出来なくなってしまってたのですが、それを修正しました。朝っぱらに急ぎで対応したので、あまりテストしてません。マズいところあったら後で直します。
Chaining Assertion for MSTest
- 2011-02-23
MSTest用の拡張メソッド集をCodePlexと、そしてNuGet(idはChainingAssertionです)にリリースしました。ライブラリといってもたった数百行(うち300行が自動生成)の、csファイル一つです。NuGet経由でも.csが一個配置されるだけという軽量軽快さ。中心となるのはIsというTへの拡張メソッドで、これはAssert.EqualとAssert.IsTrue(pred)とCollectionAsert.AreEqualを、一つのメソッドだけで表現します。単純なものですが、それだけなのに驚くほどテストが書きやすくなります。まずは例を。
// 全てのオブジェクトに.Isという拡張メソッドが追加されていて、3つのオーバーロードがあります
// Assert.AreEqualと等しい
Math.Pow(5, 2).Is(25);
// 条件式によるAssert.IsTrueをラムダ式で
"foobar".Is(s => s.StartsWith("foo") && s.EndsWith("bar"));
// コレクションの等値比較は可変長引数でダイレクトに書ける
Enumerable.Range(1, 5).Is(1, 2, 3, 4, 5);
以前どこかで見たような?はい。ベースはテストを簡単にするほんの少しの拡張メソッドで書いたものです。それを元に若干ブラッシュアップしています。ラムダ式によるAssert時のエラーメッセージが、非常に分かりやすくなりました。
前はFuncだったのですが、Expressionで受けるようにしたので、情報が取れるようになったらからです。それと渡ってくる値に関しても表示。これで、情報が足りなくてイライラってのがなくなって、かなり書きやすくなりました。
ところでnullに関してはIsNullとIsNotNullというメソッドに分けました。あまり増やしたくはないので、入れるか悩んだんですけどね。x => x == null でもいいのだし。
TestCase/Run
以前書いた時もNUnit風のTestCaseの再現を試みていたのですが、今回は抜本的に作り替えてリベンジしました。再現度(それっぽい度)は相当上がっています。といっても、目的は再現度を上げることじゃあないですね、大丈夫です、これにより、遥かに使いやすく実用的になりました。
[TestClass]
public class UnitTest
{
public TestContext TestContext { get; set; }
[TestMethod]
[TestCase(1, 2, 3)]
[TestCase(10, 20, 30)]
[TestCase(100, 200, 300)]
public void TestMethod2()
{
TestContext.Run((int x, int y, int z) =>
{
(x + y).Is(z);
(x + y + z).Is(i => i < 1000);
});
}
}
TestContextへの拡張メソッドにRunというのが仕込んであって、それとTestCase属性が連動して、パラメータ指定のテストが行えるようになっています。TestCaseSource属性もありますよ?
[TestMethod]
[TestCaseSource("toaruSource")]
public void TestTestCaseSource()
{
TestContext.Run((int x, int y, string z) =>
{
string.Concat(x, y).Is(z);
});
}
public static object[] toaruSource = new[]
{
new object[] {1, 1, "11"},
new object[] {5, 3, "53"},
new object[] {9, 4, "94"}
};
何でTestContextへの拡張メソッドという形をとっているかというと、TestContextから実行中クラス名とメソッド名が簡単に取れるから、です。この二つさえ分かれば、あとはリフレクション+LINQでやり放題ですからねー。クラス名が取れるTestContext.FullyQualifiedTestClassNameは、どうやらVS2010からの追加なようで、ナイス追加。VS2008からTest周りは何も進化しなくてNUnitと比べるとアレもコレも、な感じなわけですが、地味に改良はされていたんですね。いや、誰が嬉しいんだよって話ですが、私は大変嬉しかったです。
テストケースとして分けられないので、非常にオマケ的ではあるんですが、大変お手軽なので無いよりは嬉しいかな、と。
まとめ
Assert.HogeHogeの楽しくなさは異常。本当にダルい。かといって、英語的表現を目指した流れるようなインターフェイスは爆笑ものの滑稽さ。ああいうのをDSLと言うのは止めましょうぜってぐらいの。まあ、そんなわけで、これを使うと結構快適に書けます。Isの一つ一つは馬鹿みたいに単純なたった一行のメソッドなんですが、それだけなのに、全然違うんですよね。
というわけでまあ、楽しいテスト生活を!あと、最近は自動化テストなPexや、それと絡むCodeContractsを触っているので、それらについて近いうちに書けたらと思っています。
Rx FromEvent再訪(と、コードスニペット)
- 2011-02-18
最近の私ときたらFromAsyncでキャッキャウフフしすぎだが、Asyncの前にEventを忘れているのではないか、と。というわけで、FromEventについて、「また」見直してみましょう!延々と最初の一歩からひたすら足踏みで前進していないせいな気はその通りで、いい加減飽きた次に進めということだけれど、まあそれはそのうち。私的にはFromEventはとうに既知の話だと思い込んで進めているのであまり話に出していなかっただけなのですが、初期に出したっきりで、特にここ数カ月で(WP7出たりAsync CTP出たり昇格したり)でRxが注目を浴びるのが多くなってはじめましてな場合は、そんな昔の記事なんて知らないよねですよねー(Blogの形式は過去記事へのポインタの無さが辛い)。なわけなので定期再び。
「Rxの原理再解説」や「時間軸という抽象で見るRx」、というネタをやりたいのですが、長くなるのと絵書いたり動画撮ったり色々準備がという感じで中々書き進められていないので、先にFromEvent再訪を。
4オーバーロード
FromEventはイベントをReactive Sequenceに変換するもの。これはオーバーロードが4つあります。
// ただのEventHandlerを登録する場合はハンドラの+-を書くだけ
// 例はWPFのWindowクラスのActivatedイベント
Observable.FromEvent(h => this.Activated += h, h => this.Activated -= h);
使う機会は少ないかな? 実際はEventHandler/EventArgsだけのものなどは少ないわけで。
// WPFのButtonなどり
Observable.FromEvent<RoutedEventArgs>(button1, "Click");
これはサンプルなどで最も目にすることが多いかもで、文字列でイベントを登録するもの。記述は短くなるのですが、動作的にはイベント登録時にリフレクションで取ってくることになるので、あまり推奨はしない。じゃあどうすればいいか、というと
// 第一引数conversionはRoutedEventHandlerに変換するためのもの、とにかく記述量大すぎ!
Observable.FromEvent<RoutedEventHandler, RoutedEventArgs>(
h => h.Invoke, h => button1.Click += h, h => button1.Click -= h);
ハンドラの+-を自前で書くわけですが、EventArgsと一対一の俺々EventHandlerへの変換関数も必要になっています。これはnew RoutedEventHandler() などとしなくても、 Invoke と書くだけで良いようです。最後のオーバーロードは
// EventHandler<T>利用のものって本当に少ないんですよね、こちらを標準にして欲しかった
Observable.FromEvent<TouchEventArgs>(h => button1.TouchDown += h, h => button1.TouchDown -= h);
EventHandler<T>のものはスッキリ書けます。
コードスニペット
conversionが必要なFromEvent面倒くさい。それにしても面倒くさい。WPFのINotifyPropertyChangedほどじゃないけれど、やはり面倒くさい。ジェネリックじゃない俺々EventHandlerどもは爆発しろ!デリゲートはEventHandler<T>とFuncとActionがあれば他は原則不要(ref付きが必要とか、そういう特殊なのが欲しい時に初めて自前定義すればよろし)。と、嘆いてもしょうがない。何とかしなければ。以前はT4でガガガガッと自動生成してしまう方法を紹介しましたが、少し大仰な感があります。もう少しライトウェイトに、今度は、コードスニペットでいきましょう。
// 普通に使うもの
Observable.FromEvent<$EventHandler$, $EventArgs$>(h => h.Invoke, h => $event$ += h, h => $event$ -= h)
// 拡張メソッドとして定義する場合のもの
public static IObservable<IEvent<$EventArgs$>> $eventName$AsObservable(this $TargetType$ target)
{
return Observable.FromEvent<$EventHandler$, $EventArgs$>(
h => h.Invoke, h => target.$eventName$ += h, h => target.$eventName$ -= h);
}
この二つです。二つ目の拡張メソッドのものは、ええと、大体の場合は長ったらしくて面倒なので拡張メソッドに退避させるわけですが、それを書きやすくするためのものです。利用時はこんな形。
class Program
{
static void Main(string[] args)
{
var c = new ObservableCollection<int>();
var obs1 = Observable.FromEvent<NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventArgs>(h => h.Invoke, h => c.CollectionChanged += h, h => c.CollectionChanged -= h);
var obs2 = c.CollectionChangedAsObservable();
}
}
public static class EventExtensions
{
public static IObservable<IEvent<NotifyCollectionChangedEventArgs>> CollectionChangedAsObservable<T>(this ObservableCollection<T> target)
{
return Observable.FromEvent<NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventArgs>(
h => h.Invoke, h => target.CollectionChanged += h, h => target.CollectionChanged -= h);
}
}
rxevent -> TabTab か、 rxeventdef -> TabTab というだけで、この面倒くさい(長い!)定義が簡単に書けます。おっと、スニペットファイルはXMLなのでこれだけじゃ動きませんね。ベタ張りだと長いので、ダウンロードはneuecc / RxSnippet / source – Bitbucketの二つからどうぞ。そうしたら、ツール->コードスニペットマネージャーで追加してやってください。
今回、スニペットはSnippet Designerで作成しました。Visual Studioと統合されているので非常に書きやすくてGood。コンパイルがしっかり通る、万全な雛形をコード上で作ったら右クリックしてExport As Snippet。スニペットエディタ上に移ったら、置換用変数を選択してMake Replacement。それだけで出来上がり。あとはプロパティのDescriptionとShortcutを書くだけ。楽すぎる。もうスニペットを手書きとか馬鹿らしくてやってられません。これだけ楽だと、ちょっとした面倒事を片っ端からスニペット化していけるというものですねん。
アタッチ、デタッチのタイミングを考えよう
Rxで意外と困るのが、いつアタッチされているのか、デタッチされているのか、よくわからなかったりします。慣れると分かってくるのですが、最初は存外厳しい。そういう時は悩まずに、デバッガを使おう!
こういったように、アタッチのラムダ式、デタッチのラムダ式にそれぞれ縄を張れば、いつ呼ばれるのか一目瞭然です。悩むよりも手を動かしたほうがずっと早い。
ところでRxのいいところは、イベントのデタッチが恐ろしく簡単になったことです。そのお陰で今まであまりやらなかった、そもそも考えすらしなかった、アタッチとデタッチを繰り返すようなイベント処理の書き方が発想として自然に浮かび上がるようになった。少し簡単に書けるようになった、という程度じゃあそんな意味がない。Rxのように極限まで簡単に書けるようになると、スタイルが一変して突然変異が発生する。ある意味これもまた、パラダイムシフトです。オブジェクト指向から関数型へ?いえいえ、C#から、Linqへ。
まとめ
2/11にRx v1.0.2856.0 releaseとしてアップデートが来てました。大量更新ですよ大量更新!正式入りしたから更新ペースがゆったりになるかと思いきや、その逆で加速しやがった!ちなみに破壊的変更も例によって平然とかけてきて、Drainというメソッドが消滅しました(笑) 何の躊躇いもないですね、すげー。
代わりに、Christmas Releaseの時に消滅してWP7と互換がなくなった!と騒いだPruneとReplayは復活しました(なんだってー)。というわけで、再びWP7との互換は十分保たれたという形ですね、ヨカッタヨカッタ。そんなわけで、常に見張ってないと分からないエキサイティングさが魅力のRx、是非是非使っていきましょう。
実践 F# 関数型プログラミング入門
- 2011-02-14
共著者の一人であるいげ太さんから献本のお誘いを受け、実践F#を献本頂きました。発売前に頂いたのですが、もう発売日をとっくに過ぎている事実!ど、同時期に書評が並ぶよりもずらしたほうがいいから、分散したんだよ(違います単純に遅れただけです、げふんげふん)
NuGetの辺りでも出しましたが、F#スクリプトは活用し始めています。いいですね、F#。普通に実用に投下できてしまいます、今すぐで、C#とかち合わない領域で。勿論、それだけで留めておくのはモッタイナイところですが、とりあえずの一歩として。実用で使いながら徐々に適用領域を増やせるという、なだらかな学習曲線を描けるって最高ぢゃないですか。
F#を学ぶ動機
最近流行りだから教養的に覚えておきたいとか、イベントがファーストクラスとか非同期ワークフローが良さそうなので知っておきたいとか、私はそんな動機ドリブンのつもりでしたが、そういう動機だと実に弱いんですね!そんな動機から発したもので完走出来たものは今まで一つもありません(おっと、積み本の山が……)。もっと具体的に甘受できる現金なメリットがないと駄目なんだ。そんな情けない人間は私だけではない、はず、はず。
というわけで、実際、動機付けが一番難しいのではないかと思います、「実践」するには。F#の場合「それC#で」という誘惑から逃れるのは難しく、正面から向かわないとでしょう。この図式と対比させられるJava-Scala間では、「それJavaで」とは口が裂けても言えなくて(Java……ダメな子)、学ぶことがそのままJVMの資産を活かしてアプリケーションを書けるというモチベーションに繋がりますが、C#は割とよく出来る子だから。そんなわけかないかですが、本書では、冒頭1章でF#手厚く説明されています。言語の歴史を振り返って、パラダイムを見て、F#とはどういう流れから生まれてきた言語なのか。丁寧だとは思います。
並列計算。マルチパラダイム。うーん、それだけだと請求力に欠けるよね、何故ならF#が関数型ベースのマルチパラダイム言語であるように、C#はオブジェクト指向型ベースのマルチパラダイム言語だから。
などとやらしくgdgdとしててもまあ何も始まらない。いいからコード書こうぜ!といった流れで2章で環境導入の解説(この解説は非常に役立ちでした、F# Interactiveのディレクティブ一覧や、__SOURCE_DIRECTORY__でパスが取れるとか、F#のソースコードの所在とか)で、あとは書く!と。なんとも雑念に満ちたまま読み始めたわけですが、読み始めるとグイグイ引きこまれました。なんというか、学ぶのに楽しい言語なんですよね、F#。
それと、Visual Studioに統合されたF# Interactiveがとんでもなく便利で。こいつは凄い。私、今までREPLって別にどうでもいいものと思っていたのですよ。コマンドプロンプトみたいな画面で一行一行打っていくの。REPLは動作が確認しやすくてイイとかいう話を耳にしては、なにそれ、そもそもメチャクチャ打ちづらいぢゃん、イラネーヨって。でもVSに統合されたF# Interactiveは、IDEのエディタで書くこと(シンタックスハイライト, 補完, リアルタイムエラー通知)とREPLの軽快さが合体していて、最強すぎる。しかもその軽快さで書いたコードはスクリプトとして単独ファイルで実行可能、だと……!F#スクリプト(fsx)素晴らしい。C#で心の底から欲しいと思っていたものが、ここにあったんだ……!
と、読み始めてたら、普通に楽しい言語だし、並列や言語解析といった大変なところに入らなくても実用的だしで、かなりはまってます。F#いいよF#。始める前に考えてた動機だとかなんて幻でしかなく、始めたら自然に心の奥から沸き上がってくるものこそ継続されるものだと、何だか感じ入ってしまったり。
パイプライン演算子
F#といったらパイプライン演算子。パイプラインは文化。と、いうわけかで、実際、私がよく目にしているF#のコードというのは基本|>で繋ぐ、という形であり、それが実にイイなー。などという憧憬はあるのでF#を書くとなったらとりあえずまずはパイプライン演算子の学習から入ったりなどしたりする。
このパイプライン演算子、書くだけならスッと頭に入るけれど、どうしてそう動くのかが今一つしっくりこなかった、こともありました。関数の定義自体は物凄くシンプルでたった一行で。
// |>は中置換なのとinlineなので正確には一緒ではないですが、その辺は本を参照ください!
let pipe x f = f x
おー、すんごくシンプル。シンプルすぎて逆にさっぱり分からない。型も良くわからない。困ったときはじっくり人間型推論に取り組んでみますと、まず、変数名はpipeであり、二引数を持つから関数。
pipe : x? -> f? -> return?
まだ型は分からないので?としておきます。右辺を見るとf x。つまりfは一引数を持つので関数。
x? -> (f_arg? -> f_ret?) -> return?
fの第一引数はxの型であり、fの戻り値が関数全体の戻り値の型となるので
x? -> (x? -> return?) -> return?
これ以上は型を当てはめることが出来ず、また、特に矛盾なくジェネリクスで構成できそうなので
'a -> ('a -> 'b) -> 'b
となる('aがC#でいう<T>みたいなものということで)。なるるほど、あまりに短いスパッとした定義なので面食らいますが、分かってしまえばその短さ故に、これしかないかしらん、という当たり前のものとして頭に入る、といいんですがそこまではいきませんが、まあ使うときは感覚的にこう書けるー、程度でいいので大丈夫だ問題ない。
このパイプライン演算子をC#で定義すると
public static TR Pipe<T, TR>(this T obj, Func<T, TR> func)
{
return func(obj);
}
比較するとちょっと冗長すぎはします。とはいえ、この拡張メソッドは、これはこれでかなり有益で、例えばEncodingのGetBytesなどを流しこんだりがスムーズに出来ます。例えばbyte[]の辺りは変換後に別の関数を実行して更に別の、という形で入れ子になりがちで、かといって変数名を付ける必要性も薄くて今一つ綺麗に書けなくて困るところなのですが、パイプライン演算子(モドき)さえあれば、
// ハッシュ値計算
var md5 = "hogehogehugahuga"
.Pipe(Encoding.UTF8.GetBytes)
.Pipe(MD5.Create().ComputeHash)
.Pipe(BitConverter.ToString);
// B6-06-FC-CF-DC-99-6D-55-95-B8-B6-75-DB-EE-C8-AE
Console.WriteLine(md5); // Pipe(Console.WriteLine)でもいいですね
気持ちよく、また入れ子がないため分かりやすく書けます。そのためC#でも最近は結構使ってます。ただ、Tへの拡張メソッドという影響範囲の大きさは、相当な背徳を背負います。というかまあ、共同作業だと、使えないですね、やり過ぎ度が高くなりすぎてしまって。パイプはC#のカルチャーでは、ない。うぐぐ。F#なら
"hogehogehugahuga"
|> Encoding.UTF8.GetBytes
|> MD5.Create().ComputeHash
|> BitConverter.ToString
|> printfn "%s"
このようになりますね。「|>」という記号選びが実に絶妙。ちゃんと視覚的に意味の通じる記号となっているし、縦に並べた際の見た目が美しいのが素敵。美しいは分かりやすいに繋がる。
"hogehogehugahuga"
|> (Encoding.UTF8.GetBytes >> MD5.Create().ComputeHash >> BitConverter.ToString)
|> printfn "%s"
翌々眺めると、関数が並んでるなら合成(>>演算子)も有りですね!びゅーてぃほー。
LINQ
関数型言語といったら高階関数でもりもりコレクション処理であり、そしてそれはLinq to Objectsであり。F#ではSeq関数群をパイプライン演算子を使って組み上げていきます。
[10; 15; 30; 45;]
|> Seq.filter (fun x -> x % 2 = 0)
|> Seq.map (fun x -> x * x)
|> Seq.iter (printfn "%i")
filterはWhere、mapはSelect、iterは(Linq標準演算子にないけど)ForEachといったところでしょうか。
new[] { 10, 15, 30, 45 }
.Where(x => x % 2 == 0)
.Select(x => x * x)
.ForEach(Console.WriteLine); // 自前で拡張メソッド定義して用意する
比べると、|>はドットのような、メソッドチェーンのような位置付けで対比させられようです。違いは、パイプライン演算子のほうが自由。例えば、拡張メソッドとして事前にかっきりと定義しなくてもチェーン出来る。だから、F#にSeq.iterなどがもしなかったとしても、
[10; 15; 30; 45;]
|> (fun xs -> seq {for x in xs do if x % 2 = 0 then yield x * x })
|> (fun xs -> for x in xs do printfn "%i" x)
その場でサクサクッと書いたものを繋げられたり、yieldもその場で書けたり(まあこれは内包表記に近くC#だと別にクエリ式でもいいし、といった感じで意味はあまりないですが)実に素敵。しかし、自由には代償が必要で。何かといえば、補完に若干弱い。シーケンスの連鎖では、基本的には次に繰り出したいメソッドはSeqなわけで、一々Seq.などと打つまでもなくドットだけでIntelliSenseをポップアップさせて繋げていくほうが楽だし、ラムダ式の記法に関してもfunキーワードが不要な分スッキリする。
この辺は良し悪しではなくカルチャーの違いというか立ち位置の差というか。C#のほうがオブジェクト指向ライクなシンタックスになるし、F#のほうが関数型ライクなシンタックスだという。同じ処理を同じようにマルチパラダイムとして消化していても、優劣じゃない差異ってのがある。これは、私は面白いところだなーと思っていて。どちらのやり方も味わい深い。
最後に
C#単独で見た時よりも.NETの世界が更に広く見えるようになったと思います。あ、こんな世界は広かったんだって。今後は、ぽちぽちとF# Scriptを書きつつ、FParsecにも手を出したいなあといったところですね。
大して書けはしませんが、それなりに書き始めて使い出せている、歩き始められているのは間違いなく本書のお陰です。「こう書くと良いんだよ」という、誘導がうまい感じなのでするっと入れました。どうしてもC#風に考えてしまって、それがF#にそぐわなくてうまくいかなくて躓いたりするわけですが、そこで、ここはこうだよ、って教えてくれるので。ちょっとした疑問、何でオーバーロードで作らないの?とかの答えは、載っています。
それと最後にクドいけれど、Visual Studio統合のF# Interactiveは本当に凄い。C# 5.0のCompiler as a ServiceはこれをC#にも持ってきてくれることになる、のかなあ。
Linq to ObjectsとLinq to Xmlを.NET 2.0環境で使う方法
- 2011-02-09
LinqのないC#なんて信じられない。カレールゥのないカレーライスみたいなものです。しかし.NET Framework 2.0ときたら……。幸いなことに、開発はVisual Studio 2008以降で、ターゲットフレームワークを2.0に、とすることでvarやラムダ式は使うことが可能です。拡張メソッドも小細工をすることで利用可能になります。といったことは、C# 3.0 による .NET 2.0 アプリケーション開発 - XNA で LINQ を使おう - NyaRuRuの日記に書いてありますねん。あと足りないのはLinqを実現するEnumerableクラス。その辺のことはNyaRuRuさんの日記の追記のほうにもありますが、LINQBridgeを使うことで拡張メソッドのための小細工なども含めて、全部面倒見てくれます。つまり、Linq to Objectsを.NET 2.0で使いたければLINQBridge使えばいい。以上。
というだけで終わるのもアレなので、Linq to Objectsが使えるなら、Linq to Xmlも使いたいよね?Linq to Xmlの強力さを一度味わったら二度とXmlDocumentも使いたくないし生のXmlReader/Writerも使いたくないし。でも残念なことにLINQBridgeはto Objectsだけ。となれば自前再実装、は無理なので、そこは.NET Frameworkのオープンソース実装のMonoからソースコードをお借りすればいいんじゃなイカ?
Monoのソースコードはmono/mono - GitHubで管理されています。私はGitじゃなくてMercurial派なのでGitは入れてないのでPullは出来ない、ので、普通にDownloadsからzipで落としました。クラスライブラリはmcs\class以下にあります。まずはEnumerableから行きましょう。
Linq to Objects
新規にクラスライブラリプロジェクトを立てて(プロジェクト名は何が良いでしょうかねえ、私はMono.Linqとしました)、ターゲットフレームワークを2.0に変更。そしてSystem.Core/System.Linqをフォルダごとドラッグアンドドロップでソリューションエクスプローラに突っ込む。そしてコンパイル!
ふむ。華麗に673件のエラーが出てますね。どうやらExpressionsがないそうで。ふーむ、つまりQueryable関連ですねえ。Enumerableだけだと関係ないので削除してもいいんですが、せっかくなのでIQueryableも使えるようにしましょう!System.Core/System.Linq.ExpressionsもEnumerableと同じようにフォルダごとコピー。更にコンパイル!
するとまだまだ346件エラー。FuncがないとかExtensionAttributeがないとか。.NET 3.0で追加された拡張メソッド用の属性とかFuncがないわけですね。というわけで、それらも持ってきてやります。ExtensionAttributeはmcs/class/System.Core/System.Runtime.CompilerServicesにあります。Enumerableだけの場合はExtensionAttributeだけでいいのですが、Queryableも使う場合は他のクラスも必要になるので、ここもフォルダごとコピーしましょう。
もう一つの、FuncとActionはSystem.Core/SystemにFuncs.csとActions.csとして定義されているので、これらも持ってきます。なお、FuncとActionは#ifディレクティブにより.NET4以下の場合は4引数までのものしか使えないようになっていますが、.NET4からの16引数までのものも使いたければ、#ifディレクティブを削除すればOK。
これでコンパイルするとエラーはたった4つになります!ってまだエラーあるんですか、あるんですねえ。HashSetがないとか。HashSetで、ああ、集合演算系が使ってるもんねえ、とティンと来たら話は早い。こいつはSystem.Core/System.Collections.Generic/HashSet.csにあります。みんなSystem.Core下にあるので捜すの楽でいいですね。
コンパイルしたらエラーが増えた!HashSet.csのエラーですね。CollectionDebuggerViewとMonoTODOという属性が無いそうだ。よくは分かりませんが、名前からして大したことはなさそうだしたったの5つなので、削除してしまっても問題なく動きます。ので削除してしまいましょう。と言いたいんですが、せっかくなのでこの二つの属性も拾ってきます。この二つはSystem.Coreにはないし、正直見たこともない属性なので何処にあるのか検討付きません。というわけで、まあ検索すれば一発です。
System.Data.Linq/src/DbLinq/MonoTODOAttribute.cs、って随分変なとこにありますね、とにかくこれと、corlib/System.Collections.Generic/CollectionDebuggerView.csを持ってくる。
これで完成。コンパイル通る。動く。ターゲットフレームワーク2.0でもLinq!何も問題もなくLinq!ラムダもvarも拡張メソッドもある!うー、わっほい!C# 3.0で.NET Framework 2.0という奇妙な感覚が非常に素敵です。
Linq to Xml
ではLinq to Xmlも用意しましょう。といっても、やることは同じようにmonoのコードから拝借するだけです。mcs/class/System.Xml.Linq下にあるSystem.Xml.Linq, System.Xml.Schema, System.Xml.XPathをフォルダごとコピー。そしてコンパイルすると!例によってエラー。
XNameが見つからないそうで。んー、あれ、XNameは普通にLinq to Xmlの一部では?と、いうわけでSystem.Xml.Linq/XName.csを見に行くと、あー、#if NET_2_0で.NET2.0環境下では全部消えてる!しょうがないので、ここではソースコードを直に編集して#ifディレクトブを除去しちゃいます。
コンパイルは通りましたが警告ががが。CLSCompliantがついてないってさ。というわけで、Properties/AssemblyInfo.csにCLSCompliantを付けてやります。
[assembly: System.CLSCompliant(true)]
これで完成。Linq to Xmlが使えるようになりました!マジで!マジで。
ライセンス
ライセンスは大事。FAQ: Licensing - Monoで確認するところ、クラスライブラリはMIT X11 Licenseになるようです。かなり緩めのライセンスなので比較的自由に扱えるのではないかと思いますが、詳細はFAQならびにMIT X11 Licenseの条項を個々人でご確認ください。
まとめ
Linqがあれば.NET 2.0でも大丈夫。もう何も怖くない。まあ、実際.NET 2.0のプロジェクトを今からやるかといえば、これは最終手段であって、まずやることは全力で反対して.NET 4を採用させることでしょう。既存のプロジェクトに対する改修でLinqを突っ込むのは、うーん、そんなこと許されるんですか!許されるなら平気でやります!大抵は許されない気がしますが!
さて、.NET 4の人でもこれを用意する利点はあります。学習用に。シームレスにLinqの中へデバッグ実行で突入出来ます。挙動の理解にこれより最適なものはないでしょう。ソースコードを眺めるもよし、ですしね。それと、これを機にMonoに触れる、機会はWindowsな私だとあまりないのですが、ソースコードに触れてみるのも結構幸せ感です。mono独自のクラス(Mono.Xxx)も色々あって面白そう。
余談ですが、Windows Phone 7やSilverlightであのクラスがない!という状況もMonoの手を借りることで何とかなるケースも。(何とかならないケースは、依存がいっぱいで沢山ソースを持ってこなければならない場合。さすがにそう大量となるとどうかな、と)
.NETコードへデバッグ実行でステップインする方法
デバッグ実行といえば、Microsoftもソースコードを公開しています。.NET Framework Librariesで公開されてます。.NET 4をDownloadすれば、その中にあります。やたら階層が深くて迷ってしまいますが、EnumerableとQueryableは Source.Net\4.0\DEVDIV_TFS\Dev10\Releases\RTMRel\ndp\fx\src\Core\System\Linq にあります。Symbolをモニョッとすれば、Visual Studio内でもステップインでデバッグ出来ますねえ。というわけで、その解説もついでなので。
まず、オプション->デバッグ->全般で「マイコードのみ設定を有効にする」のチェックを外します。そして、「ソースサーバーサポートを有効にする」のチェックを入れます。この二つだけ(多分)。ちなみに、「.NET Frameworkソースのステッピングを有効にする」というなんともそれっぽいオプションは罠なので無視しておきましょう。
あとはデバッグ->シンボルでダウンロードした先のフォルダを指定すればOK。私はZ:\RefSrc4\Symbolsになってます。これで、F11でめくるめく.NET Frameworkの無限世界にステップインで帰ってこれなくなります!やり過ぎると普通に鬱陶しくなるので、その他のオプション類とかで適度に抑制しながらやりませう。