linq.js ver.3.0.0-beta, メソッドlowerCamelCase化、など

ずっとやるやる詐欺だったlinq.js ver.3ですが、ようやく始まりました!

トップページのボタンはstableのものになるので、DOWNLOADSタブからver.3.0.0-betaを選んでください。また、NuGetを使っている人はInstall-Package linq.js -Preで入ります。他にlinq.js-jQuery -Pre, linq.js-RxJS -Pre, linq.js-QUnit -Preなどり。

lowerCamelCase化

はい。ようやくJavaScriptらしくなりました。UpperCamelCaseにはC#っぽいとか、キーワードで衝突しなくなるとか、ちょっとした利点はなくもないのですが、そもそも.NETっぽくないほうがいい、というかJavaScriptの世界にちゃんと馴染ませたいという思いのほうが強い。そして、.NETの人以外にも使って欲しくて。

Enumerable.range(1, 10)
    .where(function(x){ return x % 2 == 0})
    .select(function(x){ return x * x});

当然ながら超破壊的変更です。ver.2までのコードは一切動かなくなります。やりすぎですが、しょうがない。痛くてしょうがないけれどしょうがない。さて、ならばとついでにメソッド名の見直しもしました。

Return -> make
CascadeBreadthFirst -> traverseBreadthFirst
CascadeDepthFirst -> traverseDepthFirst
BufferWithCount -> buffer
ToString -> toJoinedString
Do -> doAction
Let -> letBind
MemoizeAll -> memoize
Catch -> catchError
Finally -> finallyAction
ToJSON -> toJSONString

これを機に、というかこういう機会じゃないとやれないですから。toStringやtoJSONは、上書きしてしまうとマズいので別名にしています。toStringは、まあそのままなので分かると思うのですが、toJSONのほうは、JSON.stringifyで特別扱いされるメソッドなので、こっそり注意が必要なんですね、というか実際ハマッて気づくのに時間かかりました。

extendTo

prototype.js以降、prototype拡張は悪、でしたが、最近のJavaScriptはfor inで列挙しない形での拡張(Object.definePropertyでenumerable:falseにする)が可能になっています。それを全面に押し出したSugarといったライブラリもあり、確かに便利なのですよね。

さて、linq.jsでは配列などをLINQで扱うためにEnumerable.fromで包んでやる必要があって面倒でしたが、配列からそのままselectとかwhereとかが生えていれば、便利、ですよね?なので、任意に拡張できるようにしました。

// Arrayを拡張する
Enumerable.Utils.extendTo(Array);
        
[1, 3, 10, 1000].where("$%2==0").select("$*$");

Enumerable.Utils.extendToを一度呼べば、from不要で直接LINQのメソッドを呼ぶことができます。もしブラウザがObject.definePropertyに対応していなければ、その時はprototypeを直接拡張しています。

さて、LINQのメソッド名とネイティブのメソッド名が被っている場合は、ネイティブのメソッド名を優先して、上書きはしません。例えばjoinとか、concatとか。その場合はByLinqがメソッド名の末尾につきます。joinByLinq、など。

// 名称が被るものはByLinqというプリフィックスがつく
[1, 3, 10].reverseByLinq();

// もしくはasEnumerableメソッドを呼んであげればLINQのメソッドのみになります
[1, 10, 100].asEnumerable().forEach(function(x, index){alert(x + ":" + index)});

forEachなどは古いブラウザではそのまま、新しいブラウザではforEachByLinqになる、といったようにブラウザ互換性がなくなるので、個人的にはByLinqの形で呼ぶよりかは、asEnumerableを使うことのほうをお薦めします。

Visual Studio 2012でのIntelliSense超拡張

VS2012でlinq.jsを使うと、ただでさえ充実していたIntelliSenseが更に超補完されます。どのぐらい補完されるか、というと、selector関数でオブジェクトの候補が並んでしまうぐらいに。

もはや完全にC#。あまりの快適さにチビる。勿論、↑の図ではFooは文字列なので、x.Foo.で文字列のメソッド候補がIntelliSenseに並びます。動的言語とは思えない超補完っぷりがヤバい。そして入力補完が最大限に活きるように設計されているLINQなので、組み合わさった時の快適度は半端ない。

Chaining Assertion for QUnit

ユニットテストを書く際に、equal(actual, expected)と書くのが嫌いでした。どちらがactualなのかexpectedなのか一瞬悩むし、そもそも外側から包むのがかったるくて。かといってshouldといった、英語的表現なのも冗長なだけで全く良いとは思っていませんでした。そこでC#ではChaining Assertionといった、actual.Is(expected)でアサートが書けるライブラリを作ったのですが、それをJavaScript用に移植しました。

// 流れるように.isと打ち込むだけ
Math.pow(10, 2).is(100); // strictEqual(Math.pow(10, 2), 100)

// コレクションに対する適用は可変長にカンマ区切りで値を並べるだけ。勿論、配列にも使えます。
Enumerable.rangeTo(10, 15, 2).is(10, 12, 14); // deepEqual(Enumerable.rangeTo(10, 15, 2).toArray(), [10, 12, 14])

// LINQと組み合わさることでコレクション系のチェックが遥かに容易になる!
[1, 5, 10].all("$<12").isTrue(); // collection assertion with linq.js!

といった感じに書けて、超楽ちんです。使うにはlinq.qunit.jsを別途読み込んでください。

その他

createEnumerable, createEnumerator, createLambdaといった、自作Enumerableメソッドを作るための道具を外部公開するようにしました。是非作っちゃってください。

Enumerable.Utils.createLambda
Enumerable.Utils.createEnumerable
Enumerable.Utils.createEnumerator

更に、メソッドも追加されています。

Enumerable.defer
asEnumerable
merge
choose
isEmpty
distinctUntilChanged
weightedSample
log

それらの細かい使い方などは追々書いていきます。また、merge, zip, concatは複数のシーケンスを引数に受け取れるようになりました。

そして、C#では、以前にneue cc - LINQのWhereやSelect連打のパフォーマンス最適化についてという記事を書いた通り、Where連打やSelect連打、それにWhere->Selectといったよくあるパターンに対して最適化が入っているのですが、それをlinq.jsでも再現しました。なので、Where連打などによるパフォーマンス劣化が抑えられています。また、頻出パターンのWhere->Selectで高速化されたのはかなり大きいと思っています。

それに加えてrange, rangeDown, rangeToといったよく使う生成関数の速度を大幅に上げました(以前はtoInfinity().take()で生成していたのを、独自生成に変更したため)。

なので全体的にパフォーマンスも向上しています。

それと最後に、jQueryのプラグインとしてのものは今回からやめました。なんか混乱するし意味ないな、と思ったので、jQueryとの連携はlinq.jquery.jsによるtoEnumerable/tojQueryを追加するファイルのみとなっています。RxJSに関しても最新版のRxJSと連携できるようにしました(linq.rx.js)

今後

VS2012に対するIntelliSenseの充実化がまだ1/5ぐらいしか出来ていないので、それの充実が優先です。あと、リファレンスやサンプルが書けてないので追加。それらが出来たら、いったんver.3として正式公開します。プログラミング生放送勉強会 第17回@品川 #pronama : ATNDで話すつもりなので、その日、8/25までには正式公開を目指します!というわけで是非是非聞きに来てください。

あ、あとnode.js用にnpm公開も、ですね。

Reactive Extensions + asyncによるC#5.0の非同期処理

Reactive Extensions(Rx)の利点ってなんですかー、というと、合成可能なんです!ということです。合成可能って何?というと、LINQが使えるということなんです!です。じゃあ他には、ということで…… 詳しくはこの動画/スライド見るといいです。 Curing Your Event Processing Blues with Reactive Extensions (Rx) | TechEd Europe 2012 | Channel 9。最初のほうの例が非常に分かりやすいので、とりあえずその部分だけ引っ張ってきますと

// sender, argsの型がふわふわ
exchange.StockTick += (sender, args) => // senderの型が消えてる
{
    if (args.Quote.Symbol == "MSFT")
    {
        // 合成できないからイベントの中でベタ書きしかない
    }
};

exchange.StockTick -= /* ラムダ式でイベント登録すると解除不能 */

これが通常のイベントの弱点です。Rxにすると

// <Quote>という型が生きてる
IObservable<Quote> stockQuotes = ...; // 変数に渡せる

// LINQクエリ演算子が使える
var msft = stockQuotes
    .Where(quote => quote.Symbol == "MSFT");

var subscription = msft.Subscribe(quote => /* */);

// イベント解除が容易
subscription.Dispose();

といった感じで、実に素晴らしい!じゃあEventはもうObsoleteでいいぢゃん、というと、まあいいと思うのですがそれはそれとして、C#ネイティブだからこそデザイナがイベントが大前提で考慮されていたり、軽くて実行速度が良かったり、といったところは勿論あります。あとRxだとイベントのIObservable化が面倒だとかもね。この辺は最初から言語サポートの効いてるF#のほうが強いんですよねー。

非同期のリトライ

Visual Studio 2012も、もうRCということで間近感が相当にあります。一方でReactive Extensions v2.0 Release Candidate available now!ということで、こちらも間近感があります。一度2.0使うと1.0には戻れないよ!(NuGetではRx-Main -Preで入れられます)

じゃあRx 2.0の紹介でもしますかー、というと、しません!(ぉぃ)。その前に、asyncとRxの関係性にケリをつけておきましょう。

で、asyncの非同期とRxの非同期はやっぱり使い分けフワフワッという感じ。複数の値が来るときはRxでー、とか言われても、そもそも複数っていうのがそんなにー、とか。あと、それ以外にないの?というと、Rxの合成の強さが非同期にも発揮してRetry処理とか柔軟でー、とか。確かにそれっぽい。けれど、どうもフワッとしてピンと来ないかもしれない。

ので、例を出していきましょう。まず、リトライ処理。リトライ処理を素の非同期で書くと泣きたくなりますが、C# 5.0を使えばasync/awaitで何も悩むことなくスッキリと!

static async Task<string> DownloadStringAsyncWithRetry(string url, int retryCount)
{
    var count = 0;
RETRY:
    try
    {
        count++;
        var req = WebRequest.CreateHttp(url);

        using (var res = await req.GetResponseAsync())
        using (var stream = res.GetResponseStream())
        using (var sr = new StreamReader(stream))
        {
            return await sr.ReadToEndAsync();
        }

    }
    catch
    {
        if (count >= retryCount) throw;
    }
    goto RETRY;
}

static void Main(string[] args)
{
    var google = DownloadStringAsyncWithRetry("http://google.com/404", 3);
    Console.WriteLine(google.Result);
}

簡単です。さて、ではこれをRxで書くと……

static async Task<string> DownloadStringAsyncWithRetry(string url, int retryCount)
{
    var req = WebRequest.CreateHttp(url);

    // retry処理は.Retryで済む
    using (var res = await req.GetResponseAsync().ToObservable().Retry(retryCount))
    using (var stream = res.GetResponseStream())
    using (var sr = new StreamReader(stream))
    {
        return await sr.ReadToEndAsync();
    }
}

はい。別にRxとasyncは排他じゃありません。使って効果のあるところに差し込んで、Mixしてやれば、ただでさえ強力なasyncが更に強力になります。TaskとIObservableは変換可能なので、ToObservableして、あとはRetryメソッドを繋げるだけ。そしてIObservableはawait可能(LastAsyncと同じ効果で、最後の値を取る。非同期処理の場合は値が一つなので問題なし)なので、まんまawaitしてasyncと繋げてやればいい。素敵ですねー。

が、上のコードはちょっと間違ってます。どこが間違っているか分かりますか?

エラーの帰ってくるページ(google/404などは404エラーを返してくれるのでテストに楽←別に500にすれば500を返してくれるわけじゃなくて、ただたんに存在しないページだから404なだけで、別にどこでもいいです)を指定してFiddlerなどで観察すれば分かりますが、一回しかリクエスト飛ばしません。Retry(3)としても一回しか飛んでいません。ちゃんとRetryは3回しているのに。

どういうことかというと、GetResponseAsync()の時点でリクエストに失敗しているからです。失敗済みのリクエストに対しては、何回Retryしても失敗しか返しません。ここは本当にはまりやすくて注意所なので、よく気を付けてください!

解決策は、Retryで生成を毎回やり直すこと。Deferで包めばいいです。

static async Task<string> DownloadStringAsyncWithRetry(string url, int retryCount)
{
    // Retry時に毎回WebRequestを作り直す
    var asyncQuery = Observable.Defer(() => WebRequest.CreateHttp(url).GetResponseAsync().ToObservable())
        .Retry(retryCount);

    // retry処理は.Retryで済む
    using (var res = await asyncQuery)
    using (var stream = res.GetResponseStream())
    using (var sr = new StreamReader(stream))
    {
        return await sr.ReadToEndAsync();
    }
}

ちょっと罠があるしコードも増えてしまったけれど、それでも、まあ、まだ割といいかな、って感じでしょうか?

さて、リトライは即時じゃなくて一定間隔置いた後にリトライして欲しいってことが多いと思います。同期処理だとThread.Sleepで待っちゃうところですが、それはちょっとスレッド勿体ない。C# 5.0からはawait Task.Delayを使いましょう。

static async Task<string> DownloadStringAsyncWithRetry(string url, int retryCount, TimeSpan retryDelay)
{
    var count = 0;
RETRY:
    try
    {
        count++;
        var req = WebRequest.CreateHttp(url);

        using (var res = await req.GetResponseAsync())
        using (var stream = res.GetResponseStream())
        using (var sr = new StreamReader(stream))
        {
            return await sr.ReadToEndAsync();
        }

    }
    catch
    {
        if (count >= retryCount) throw;
    }

    if (retryDelay > TimeSpan.Zero)
    {
        await Task.Delay(retryDelay); // これで待つ
    }

    goto RETRY;
}

以前のものにTask.Delayを足しただけで簡単です。わーお、素晴らしい、なかなか強力強烈です。ではRxは、というと、同じように遅延する演算子を足すだけ。Delay、ではダメでDelaySubscription(Rx 2.0から追加)を使います。

static async Task<string> DownloadStringAsyncWithRetry(string url, int retryCount, TimeSpan retryDelay)
{
    // DelaySubscriptionで遅延させる
    var asyncQuery = Observable.Defer(() => WebRequest.CreateHttp(url).GetResponseAsync().ToObservable())
        .DelaySubscription(retryDelay)
        .Retry(retryCount);

    using (var res = await asyncQuery)
    using (var stream = res.GetResponseStream())
    using (var sr = new StreamReader(stream))
    {
        return await sr.ReadToEndAsync();
    }
}

できました!できました?いや、これだと初回リクエスト時にも遅延されちゃってて、ちょっとイケてない。修正しましょう。

// 外部変数用意するのがダサい
var count = 0;
var asyncQuery = Observable.Defer(() => WebRequest.CreateHttp(url).GetResponseAsync().ToObservable())
    .Let(xs => count++ == 0 ? xs : xs.DelaySubscription(retryDelay))
    .Retry(retryCount);

はい、ダサいし、なんだか何やってるのかさっぱりになってきました、サイテー。LetもRx 1.0にはなくて(それ以前にはあったのですが削られた)2.0から復活になります。Letは、一時変数を置かなくて済むというチェーン病にかかった人がお世話になる処方薬です。んなもん読みにくくさせるだけじゃねーか、という感じですが、もしLetがないと変数を置いて var xs = ToObservable(); xs = () ? xs : xs.Delay..; xs = xs.Retry(); としなければならなくて、非常に面倒くさいのです。だから、使いどころを守って乱用しなければ、割とイケてます。結構大事。

が、しかし、これも間違っています!(えー)。というかLetではなくて変数に展開してみるとオカシイとはっきり分かるのですが、Letの内部はRetryとか関係なく一回しか評価されないので、これだと必ずDelaySubscriptionなしのほうしか通りません。この路線で行くなら、更にやけくそでDeferを追加しましょうか。

// Deferだらけとかダサすぎるにも程がある
var count = 0;
var asyncQuery = Observable.Defer(() => WebRequest.CreateHttp(url).GetResponseAsync().ToObservable())
    .Let(xs => Observable.Defer(() => count++ == 0 ? xs : xs.DelaySubscription(retryDelay)))
    .Retry(retryCount);

ダサすぎて話にならない。Defer連打ダサい。Deferまみれになったら、ちょっと根本から方針を疑いましょうか。ついでに外部変数を使うというのがそもそもダサい。もう少し頑張りましょう!クエリ演算子をこねくり回して、と。

// DelayなしのDeferとDelayありのDeferを連結して、DelayありのみをRetryさせている
var asyncQuery = Observable.Defer(() => WebRequest.CreateHttp(url).GetResponseAsync().ToObservable())
    .Let(xs => xs.Catch(xs.DelaySubscription(retryDelay).Retry(retryCount - 1)));

どうでしょう。他にもやりようは色々とあるかもですが、正直ワケガワカラナイのでこの辺でよしておいたほうがマシです。実際のところ、以下のような拡張メソッドを作るのがベストだと思っています。

// 結局これが一番なのではかという結論に至る
public static async Task<string> DownloadStringAsyncWithRetry(this WebClient client, string url, int retryCount, TimeSpan retryDelay)
{
    var count = 0;
RETRY:
    try
    {
        count++;
        return await client.DownloadStringTaskAsync(url);

    }
    catch
    {
        if (count >= retryCount) throw;
    }

    if (retryDelay > TimeSpan.Zero)
    {
        await Task.Delay(retryDelay);
    }

    goto RETRY;
}

new WebClient().DownloadStringAsyncWithRetry("hogehoge"); だけですからねー。拡張メソッド万歳。Rx最終形のような短さとか魔術っぽさはゼロで面白くも何ともない、というか正直クソつまらないコードなわけですが、そこがC#のC#たる所以ですな、ということで。私はRxのようなクールさも勿論大好きなのですが、こういうイモさもまた、C#らしさであって、現実をより良くするための、目的を忘れない素敵な側面だと思っています。

ちなみにWebRequestの場合はそれ自体の作り直しが必要なので(一度エラーを受けたら何度GetResponseを繰り返してもダメぽ)、拡張メソッドダメです。WebClientはイベントベースなのでTask系と相性がアレで今一つなわけですが、WebRequestはWebRequestで、これベースに拡張メソッドだけで整えるのは無理があるのですね……。

.NET Framework 4.5からはHttpClientというクラスが入るので、それを使うとちょっとだけモダンっぽい雰囲気。

// モダンなドトネト的にはHttpClientかしら
public static async Task<string> GetStringAsyncWithRetry(this HttpClient client, string url, int retryCount, TimeSpan retryDelay)
{
    var count = 0;
RETRY:
    try
    {
        count++;
        return await client.GetStringAsync(url);
    }
    catch
    {
        if (count >= retryCount) throw;
    }

    if (retryDelay > TimeSpan.Zero)
    {
        await Task.Delay(retryDelay);
    }

    goto RETRY;
}

別途System.Net.Httpの参照が必要なのが面倒ですが。

非同期のタイムアウト

Rxが色々できるのは分かったけれど、結局そういう部分って拡張メソッドとかに隔離してアプリケーションコードからは離れるので、やっぱそんなでもないんじゃないの!?というと、あー、まあそうかもねえ、とか思いつつ、複雑になればなるほど効果は加速しますが、そうなるとRxでも(見た目はスッキリしたとしても)やっぱ複雑ですからね。さておき、このままだとアレなのでもう少しまともな例を、一番最初に挙げたCuring Your Event Processing Blues with Reactive Extensions (Rx) | TechEd Europe 2012から引っ張って来ましょうか。

タイムアウトを追加する例です。WebRequestだとTimeout設定すればいいぢゃーん、ではあるものの、そうではないシチュエーションも沢山ありますから、対策を知っていて損はないです。まず、asyncの例を。

static async Task<string> GetHtmlAsync(Uri url)
{
    var client = new WebClient();

    var download = client.DownloadStringTaskAsync(url);
    var timeout = Task.Delay(TimeSpan.FromSeconds(30));

    // これ結構トリッキーですよね
    if (await Task.WhenAny(download, timeout) == timeout)
    {
        throw new TimeoutException();
    }

    var html = await download;
    return html;
}

WhenAnyが中々トリッキーですね。慣用句として覚えてしまえばどうってことないのですが……。asyncもただawaitするだけじゃなくて、ちょっと慣れてきたらTask.WaitAll/Any, Task.WhenAll/Anyを使いこなすと、性能的な意味でも表現力的な意味でもグッと広がりますので、探究するのがお薦め。

さて、それをRxでやると……

static async Task<string> GetHtmlAsync(Uri url)
{
    var client = new WebClient();

    var download = client.DownloadStringTaskAsync(url)
        .ToObservable()
        .Timeout(TimeSpan.FromSeconds(30));
            
    var html = await download;
    return html;
}

ToObservableして、Retryの時のようにTimeoutを足すだけ。非常に直観的で、楽ちん、分かりやすい。演算子が豊富なのはRxの強みです。だからRetryにTimeoutがつけられるオーバーロードが最初から用意されていれば、Letとかで複雑になってしまった例もスッキリ仕上がって、ドヤァと言えたんですけどね(笑)

// こういうのを作っておけば!
static IObservable<T> Retry<T>(this IObservable<T> source, int retryCount, TimeSpan retryDelay)
{
    return source.Catch(source.DelaySubscription(retryDelay).Retry(retryCount - 1));
}

// 神がかってシンプルに!Rx最強!
public static async Task<string> GetStringAsyncWithRetry(this HttpClient client, string url, int retryCount, TimeSpan retryDelay)
{
    return await Observable.Defer(() => client.GetStringAsync(url).ToObservable()).Retry(retryCount, retryDelay);
}

標準で足りない演算子は自分で作ればいいので、また、asyncが出来たことで、今まで自作が大変だった演算子も作るのが大分容易になりました!ので、ガンガン作ってしまうといいです。汎用的に使える演算子が集まれば集まるほど、Rxの合成可能という性質が価値を発揮します。

リトライやタイムアウトをC# 4.0でRxなしで書くと

死ぬほど面倒なので書きません。いや無理でしょ常識的に考えて。

まとめ

というわけで、Rxとasyncは手を取り合って仲良く高みを目指せばいいわけです。使いこなしが必要なのはどっちも変わらない!

さて、@ITの連載、Reactive Extensions(Rx)入門 - @IT の次回は非同期のはずですが(聞こえなーい)、ええと、はい、すみません……。ええと、あと次はRx 2.0の強化事項を、ええと、まあそのうちいつか……。はい、すみません。

諸事情あって今色々詰まってて本気でヤバいんですが、それはそれとして、現在全力で一年以上やるやる詐欺だったlinq.jsの改修を進めていまして、これは本当に本当に絶対近日中にベータを出すのでお楽しみに。相当にイイ出来で、割と革命的に凄い内容になってます。いやほんと。かなり自信ありますよ。

他の積みタスクは、ReactiveOAuth(バグ修正のPull Requestを放置中というサイテーな有様、OAuth 2.0対応しないの?とか)、ReactiveProperty(WinRT対応まだー?)、Utakotoha(現在動いてない模様なので要改修)、DbExecutor(全面再構築まだー?DataSet殺すんでしょー?)とかでしょうか、って結構ありますね、うわぉぅ。というかReactive系は2.0対応とWinRT対応をやらなきゃならないので作業量的に面倒くさくて、ついつい手が遠ざかってしまいですね。はい、でも、やります。

にゃー、という感じでブログも結構アレなWebFormsとDataSetディスもそろそろさようならして、通常営業に戻ってきませう。

DataSetについて

けちょんけちょんに言ってるとか言ってないとかで言えば言ってるので、遅まきながらその理由などをつらつらと。正直なところDataSetなんて現代の観点から使ってみれば、一発でどれだけクソなのか自明だろう、ぐらいに思ってたので別に言うまでもないと思ってたので特に述べてなかったのですが、意外と支持の声も大きいのですね。困惑するぐらいです。

DataSetというと型付きと型無しがありますが、形無しのほうは、もういらないんじゃないかな。カジュアルな用途ならExpandoObjectを使ってくれという感じだし、そうでないなら、C#で型無しのヘヴィな入れ物とか利点を損ねるしかないわけで。せめてdynamicに合わせた作り直しが必要よね。

それでもADO.NETと密接に結びついていて、たとえばSqlBulkCopyはDataTableしか受け取らないなどがある。だから必要か、というと、そうじゃあなくて。そうじゃなくて、それは害悪なんだって。そのせいでストリームで流し込めないし。今時だったらIEnumerableに対応していて欲しいところだというのに(なお、専用のIDataReaderを手作りすればストリームで流し込めます)。腐った現状を肯定するんじゃなくて、どうあるべきなのかを認識しよう。

ちなみにLINQ to DataSetは型無しDataSetのためのキャスト要因でしかないので、ほとんど名前だけでドーデモイイ代物です。型付きDataSetのほうは一応IEnumerable<TRow>なので不要なんですよね。

さて、話の主題のStrongly Typed(笑) DataSetのほうは、死んでほしい。今すぐに跡形もなく消え去ってほしい。なんでそうも恨み言が多いのかと言ったら仕事で割とヘヴィに使い倒しているから、なのですけれど。

Nullableに非対応

分かりやすく最大の馬鹿げた点はここですね。マトモな神経ならどれだけ頭可笑しいのか分かるはずで。作られた年代が年代だからしょうがない?いや、今話しているのは現代のことで、そんなNullable非対応のまま更新されず、大昔に見捨てられた代物なんてどんな選ぶ理由あって?

なお、型付きDataSetを知らない人に説明すると、nullが入る可能性のある列に対してはIsHogeNullというメソッドが生成されているので、そちらで事前チェックすればいい、というシステムになっています。if(row.IsHogeNull()) row.Hoge; といった感じ。もしnullの状態でrow.Hogeにアクセスすると実行時例外。

これ、すごく気持ち良くないんですよね。型付き言語の良さって型がドキュメントなことであり、C#の良さってそれがIntelliSenseでコードを書いている最中からリアルタイムに立ち上がって教えてくれるところであって。なんでコード書いてIntelliSenseも出ているのに、それがnullが混じる可能性があるのかないのか分からないの?Hogeの型がNullableならば、そこから伝わるのに。こういうC#の利点を損なうような代物は全力で許さない。

Enumに半分非対応

データベースの数値とC#上のEnumを関連付けることは割とあるシチュエーションだと思うわけですが(EntityFrameworkでもずっと要望に上がっていて最近やっとようやく対応しましたね……)DataTableもプロパティに関してはDBの型ではなくEnumに変更できます。ただし、TableAdapterによるメソッドの引数のほうは変えられないんですねー、あははぁ、intだー、intだぁー、凄いね、キャストだね。クソが。

ただたんにキャストすればいいだけだから大したことないぢゃん、と思うかもですが、これは非常に大きなことなのです。タイプセーフ、というだけじゃなくて、引数の型がEnumだと、それに沿うようIntelliSenseの第一候補として優先的に表れてくれて、こういう些細な気配りがC#の気持ちのよいプログラミングを支えているのです。

これでどこがTypedなのか。ありえないレベル。

使えないデザイナ

デザイナ、重いんだよね、普通に。激しくストレスなぐらいに。そして位置の設定はすぐに吹き飛んで横一列に並びきった整列へ。重い状態でセーブするとDesginer1.cs, Designer2.csと数字が延々とインクリメント。そして、長大奇怪なXMLに保存されるのでコンフリクトが発生したらマージ不能。OK、DataSetは古の悪名高きVisual SourceShredder(ロック方式なのでコンフリクトは原理上一応発生しない)とセットで使うべきものなんだな、それならばしかたがない。つまり、現代で使うべきものではない。

そして、基本的にクエリはこのデザイナから書かせるものなのですが、うまくSQLを解釈してくれない。ちょっと凝ったクエリを書くだけで、機能しなくなる。例えばSQL Serverの共通テーブル式とかうまく作れない。生SQLを書かせるのに、シンプルなSQLしか書けない。whereのin句にパラメータを並べるとかもできない。それなら逐語的文字列で書かせてもらったほうが百億倍マシだわ。というか書かせろという感じですが。(できなくもないですけれどね、ただもうそれならそもそもDataSet使わなくていいぢゃん、ほかの余計な制約もあるのだから、といったところで)。

お節介DataRowView

私の今の主戦場はWebFormsなのですが、RepeaterにDataTableをバインドすると、あら不思議、DataRowがDataRowViewに化ける!わー、嬉しいー、死ね。余計なおせっかいとしか言いようがない。これ、まあ現代的なC#erならばDataTableをLINQ使って加工したのをバインドしたりもするわけで、IEnumerable<DataRow>の場合は、そのままのDataRowが来る。ええ、同じはずの型が、違う型でやってくるなんて、悪夢すぎる。狂ってる。

文字列クエリ

Selectメソッド!紛らわしいですが、DataTableのSelectはLINQにおけるWhereにあたります。「文字列」でクエリ書かせるものが存在します。おお、文字列、タイプセーフじゃあないねえ……。型付きDataTableであっても戻り値は型無しDataTable、なんだねえ……。すごい、すごいすごい。いらないね。現代的に強化するならExpressionに対応させてタイプセーフなクエリを発行するとか、やりようはあるはずですが2005年で更新止まってるのでそんな高尚な機能が追加されることは未来永劫ないでしょう。

おまけに型付きのDataTableはLINQ to Objectsで扱えるので、素直にLINQ to Objectsにまかせてしまったほうが遥かに良い。LINQ以前は、DataTableってインメモリDBとしてある程度のクエリが簡単に実装できる、というところがあったのですが、LINQ以後の世界では純粋なC#コードとして簡単にソートも射影もフィルタリングも可能、それどころか備え付きのクエリとは比較にならないほど柔軟で強力なクエリ能力を手にしているので、もはや中途半端なインメモリDBは不要で、純粋なコレクションだけで構わないぐらいなのですよね。

モック作るのが面倒くさい

専用のヘルパでも作りこまない限りは絶望的。

じゃあどうするの?

そうですね、ここの回答がない限りはDataSetから抜けられないのですしね。私としてはLINQ to SQLでいいぢゃん(EntityFrameworkじゃなくてね)、と思うのですけれど。MSのコンサルタント連中が2009年末にもなっていま使うべき、学ぶべき.NETテクノロジはどれ?という講演で「まずはデータセットやテーブルアダプタを活用できることが大事、とか」「更新系が弱い」とか言い続けているのが絶望的。なんでDataSetが基礎知識なんだよ、馬鹿じゃねーの。

オールドテクノロジーで縛り付けたいのかしらね。求められるのは、ある程度の弱さを知覚した上でのPOCO+DataContextでの使いこなしかたの説明が求めるわけで。まさか、2012年の現在でもEntityFrameworkは更新に弱くてDataSetがまずは基本ですね、とか言っていやあしないですよね、知らないけど。

何でも得手不得手があって使い分けが大事、とかいうのはすごく簡単な逃げ口上ですが、何にでもメリットデメリット、そして未来の潮流を踏まえたうえでの学習の投資で天秤にかけなければならない。DataSetに未来はどこにあるの?腐臭を放ってる資産の保守ぐらいでしょ。こういう影響力ある人らがどうしょうもないことを言うのには、猛烈に腹が立っていてずっと不信感しか持てない。今のところ最後の赤間本であるLINQ本も急いで作った感バリバリでとてもひどいしね(そのことが前説にも書いてあるしね!影響力があるのは分かっているのでしょうから、もう少し丁寧に書けなかったものなのか)。

まあ、WebFormsやWinFormsにはDataSetを前提においたコントロール資産が山のようにあるから……。というのは移れない理由にはなるでしょうね。その場合はプラットフォームごとサヨウナラするしかないんじゃないの?そこまでは知りませんよ。で、その完全に縛られたポトペタ成果物って、魅力的なの?公に出たときに競争力あるの?年々、競争力を失っていっていると思うんですよね。それが許される賞味期限はとうに過ぎていて、残っているのはガラクタだけ。

そして、これからは定型的な業務アプリへならLightSwitchも出てきましたしね(VS2012からは標準搭載で、出力先もSilverlightだけじゃなくHTML5が選べるので実用性高くなったと思う)

じゃあEF使えばいいの?

LINQ to SQLは更新されていなくて、今のMSが推してるデータアクセステクノロジはEntityFrameworkだからEF使おう、というと、うーん、私はEntityFrameworkあんま好きくないので、そんなに薦めないかなあ、とか言っちゃったりして。EntityFrameworkの思想に一ミリも魅力を感じないので。LINQ to SQLのほうがまだずっといいよ!更新されてないぢゃん、というならDataSetだって一緒だしさ!なんというか、DataSetといいEFといい、ADO.NETチームってとってもセンス悪いんじゃないか、と思ったり。(ちなみにLINQ to SQLはC#チーム側からの実装だそうで、さもありなん)。あと同じくセンス悪いなーって思うのはEnterprise Libraryとかですね!

ORMは信用ならねえがDataSetはクソだから、もはや生ADO.NETで、つまりDbConnectionからDbCommandでDbReaderで、というので手作業しかねえ!というのはあると思いますが、うーん、手作りはナシね、ナシ。生を生のまま扱うのはアレなので、ちょっとしたユーティリティ、独自マッパーっぽいものは作ると思うのですが、これがねえ。私は以前に、センスのない独自マッパーを使わされていたことがあったのですが、使いにくくて結構な不幸でした。

単純にマッピングする薄い代物だとはいえ、作るにはそれなりなセンスと技量が必要なのです。で、そういうのをMicro-ORMと称しています。生ADO.NETのちょこっとだけ上層にあって主にクエリ結果のマッピングを効率よく行う、程度な代物なので、実質的には生ADO.NETを扱ってると考えてもいいです。現在だと代表的なものにdapperとか、色々と良いものがあるので、それらを選べばいいんじゃないですか。

フルORMにしたって、Microsoft純正以外にもLightSpeedとか良い選択肢がありますよ。NHibernateはどうかと思いますが。

ORMについてはLightSpeedの作者の語るORMのパフォーマンス最適化という記事が良いと思います。特にDataSetからの移行を意識するのならば。LINQ to SQLの成り立ちについてはThe Origin of LINQ to SQL を訳してみた - NyaRuRuの日記を。

Micro-ORMによるデータコンテキスト

Micro-ORMは、当然ながらDataSetやORMが持つ作業単位の保持はないので、そういうのが必要だったら、ある程度は手作業で作りこむ必要はあります。

少し実例を挙げると私が作っているというか作ったものは、データ保存先がDBだけじゃなくてMemcached(キャッシュとしてだけじゃなくデータ保持にも使う)だったりRedis(ListやHashなどのデータ構造を持つKVS)だったりが、それぞれのパフォーマンス的に適していると思える箇所に挟み込まれたデータコンテキストをなしていて、一個のDBだけの世界を構築するORM系はどれも不適切でして。

かといってDapperなどの既存のMicro-ORMも若干、某弊社の事情に合わないところがあるので、自分で作ろうかなあ(上で作るな、と言ってたのに!)とはずっと思ってるところですね。ベースになるMicro-ORMは既にある→DbExecutor - Simple and Lightweight Database Executorのと、拡張のアイディアは沢山あるので、あとは実装時間ががが。

まとめ

DataSetは単純に言って古い。言語機能が年々強化されていく中で、2005年の時点でストップしている(しかも2005の時点の言語機能(Nullable)にすら非対応)なものを使うのは、プログラミングにおいて足枷でしかない。7年前ですよ、7年前。あんまり一般化して言うのもアレですが、ドトネトの人って浦島太郎な雰囲気ありますよ、キャッチアップが遅すぎるというか枯れてるのが、とかかんとかって。エンタープライズだとどうだとか業務アプリだとどうだとかメンバーのレベルがどうだとか、そんな言い訳ばかりで、すごく格好悪い。

すっごくクールじゃないわけですよ。そんな言説が目立つところに、魅力を感じるのは難しい。せっかくC#や.NETは魅力的なのに。というわけで、私としては、資産がどうだとかこうだとかって言説は吐きたくないし、もっと活力ある感じになればいいな、って思います。2009年の現実に最も使える.NETのバージョンはどれ?→.NET 2.0が現時点でベスト、とか凄く絶望的じゃないですか。まあ、えんたーぷらいずの世界ではそれだししょうがないというのならそうなのでしょうが、なるべくそうじゃない世界を作りたい。

そのためにも、新しく、負の遺産を作るのだけはナシです。DataSetに別れを。

控えめなViewStateによるハイパフォーマンスASP.NET Web Forms開発

今どきのウェブ開発はMVCだよねー、な昨今を皆様どうお過ごしでしょうか。そんな中であっても、Web Formsでモバイル向けにハイパフォーマンスサイトを作らなきゃいけない時だってあるんです。さて、そんなWeb Fromsですが、とりあえずの敵はViewStateです。ViewStateをどのように活かし、どのように殺害するか、そこに全てがかかっています。幾つかの典型的なシチュエーションを取り出して、ViewStateを抹消していきましょう。

ViewStateMode = "Disabled"

下準備として、ViewStateModeをDisabledにします。ViewStateModeは.NET Framework 4から入った新機能で、「ようやく」ViewStateのオン・オフをルート階層から切り替えることが出来るようになりました。それまではEnableViewStateのみで、falseにすると、その階層の下のViewStateが全てオフになってしまうという使いにくいものでした。全部OFFで済ませられるほど世の中は甘くなく、Web Formsでは部分的にONにする必要性があります。ViewStateModeとEnableViewStateの両方が記述されているとEnableViewStateのほうが優先されて害悪となります。というわけで、.NET Framework 4以降ならばViewStateModeのみを使いましょう。

<%@ Master Language="C#" AutoEventWireup="true" CodeBehind="Site1.master.cs" Inherits="WebApplication3.Site1" %>

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>ViewState殺害教</title>
</head>
<body>
    <asp:ContentPlaceHolder ID="BodyPlaceHolder" runat="server" ViewStateMode="Disabled">
    </asp:ContentPlaceHolder>
</body>
</html>

まずは、マスターページのContentPlaceHolderに対して、ViewStateModeをDisabledにしましょう。こうすることで全ページが強制的にデフォでViewStateオフが働きます。また、ページ単位でDisabledにしてしまうと、各Pageの this.ViewState["hogehoge"] も無効になってしまって不便なので(PageのViewStateは便利なinput hiddenみたいなものですし、闇雲にすべてをオフにせず、便利なものは便利に使うのが大事です)、マスターページのContentPlaceHolderに仕込むのが最良だと私は考えています。

テキストボックスやドロップダウンリストから値を取り出す

そんなわけで、ViewStateを丸ごとオフにした状態からデータを取り出してみませう。

<%@ Page Title="" Language="C#" MasterPageFile="~/Site1.Master" AutoEventWireup="true"
    CodeBehind="WebForm1.aspx.cs" Inherits="WebApplication3.WebForm1" %>

<asp:Content ContentPlaceHolderID="BodyPlaceHolder" runat="server">
    <form runat="server">
        <asp:TextBox runat="server" ID="ToaruTextBox" />
        <asp:DropDownList runat="server" ID="ToaruDropDownList" />
        <asp:Button runat="server" Text="ただのボタン" OnClick="Button_Click" />
    </form>
</asp:Content>

こんなどうでもいい画面があるとして、コードビハインド側は

public partial class WebForm1 : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (IsPostBack) return;

        ToaruTextBox.ToolTip = "ほげほげ";
        var items = Enumerable.Range(1, 10)
            .Select(x => new ListItem
            {
                Text = x + "点",
                Value = x.ToString(),
                Selected = x == 5
            })
            .ToArray();
        ToaruDropDownList.Items.AddRange(items);
    }

    protected void Button_Click(object sender, EventArgs e)
    {
        try
        {
            Response.Write("TextBox.Text:" + ToaruTextBox.Text + "<br />");
            Response.Write("TextBox.ToolTip:" + ToaruTextBox.ToolTip + "<br />");
            Response.Write("DropDownList:" + ToaruDropDownList.SelectedValue);
            Response.End();
        }
        catch (System.Threading.ThreadAbortException) { }
    }
}

こんな感じとします。ところでどーでもいーんですが、DropDownListに値を突っ込むときはLINQでListItemの配列を作って、それをAddRangeで流し込むほうがDataSourceに入れてDataBindするよりも楽です。というのも、DataBindだとSelectedを指定するのが非常に難しいというか不可能に近いようなので。こういう色々と中途半端なとこがWeb Formsは嫌ですね。

さて、このボタンを押した実行結果は、「TextBox.Text:あいうえお」「TextBox.ToolTip:」「DropDownList:」になります。TextBox.Textは取り出せてるけど、ToolTipは取り出せてない。DropDownListも全滅。つまるところ、ViewStateをオフにしていると、復元できる(イベントで取り出せる)プロパティと、そうでないプロパティがあります。挙動としては、Web Formsが復元出来るものは自動で復元してくれて、復元できないものは復元してくれない、といった感じです。そして、それだと困るわけです。ToolTipはどうでもいいのですが、DropDownListの値とか取れないと困る。ViewStateがオンなら、全部取得できているのに!やっぱりViewStateはオンにしよう!ではなくて、何とかしましょう。

「復元出来るものは自動で復元してくれる」というけれど、その情報はどこにあるのでしょう。これは、別にWeb Formsだからって特殊なわけでもなんでもなく、Request.Formにあります。

public static class NameValueCollectionExtensions
{
    public static IEnumerable<KeyValuePair<string, string>> AsEnumerable(this NameValueCollection collection)
    {
        return collection.Keys.Cast<string>().Select(x => new KeyValuePair<string, string>(x, collection[x]));
    }
}

Button_Clickのところでブレークポイント張って、Request.Formの中身を覗きましょう。AsEnumerableは独自拡張メソッドです。Request.FormはNameValueCollectionというゴミに格納されていて、Keysは見れるけどValuesが見れないというクソ仕様なので、そこはLINQで何とかしましょうというか、これは多用するのでNameValueCollectionへの拡張メソッドとして定義しておくと捗りますというか、ないと死ぬレベル。

そんなわけで、Formに普通に格納されていることが分かりました。そうそう、ViewStateをオフにしてるはずなのに__VIEWSTATEに値が入ってるぞ!とお怒りかもですが、ほんの少し入ってくるのは仕様なので、そこは我慢しましょう。大した量じゃないので。

では、どうすれば値を取得できるのか、というと

protected void Button_Click(object sender, EventArgs e)
{
    try
    {
        Response.Write("TextBox.Text:" + Request.Form[ToaruTextBox.UniqueID] + "<br />");
        Response.Write("DropDownList:" + Request.Form[ToaruDropDownList.UniqueID]);
        Response.End();
    }
    catch (System.Threading.ThreadAbortException) { }
}

こうです。コントロールのUniqueIDをFormに渡せば良いわけですね。ToolTipとかいうどうでもいいものは取れないので、そういう、HTMLのinputに存在しないものに関しては、PageのViewStateに保存しておけば良いでしょう。私としては、Web Forms使うならinput hiddenよりもthis.ViewState["hoge"]を使うべきだと思います。せっかくある道具ならば、嫌々ながらも有効活用したほうが良いでしょう。

リピーターとチェックボックス

お次は、繰り返しなシチュエーションを考えてみましょう。繰り返しといったらRepeater一択です。それ以外は存在しません。BulletedListすら存在しません。書き出すHTMLを完全にコントロールできるものはRepeater以外存在しません。Web Formsとはなんだったのか。ともかく、Repeaterです。

<asp:Content ContentPlaceHolderID="BodyPlaceHolder" runat="server">
    <form runat="server">
        <asp:Repeater runat="server" ID="ToaruRepeater">
            <ItemTemplate>
                <input runat="server" id="ToaruCheckBox" type="checkbox" value="<%# Container.DataItem %>" />
                チェック 値:<%# Container.DataItem %>
                <br />
            </ItemTemplate>
        </asp:Repeater>
        <asp:Button runat="server" OnClick="Button_Click" Text="ぼたん" />
    </form>
</asp:Content>
public partial class WebForm1 : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (IsPostBack) return;

        ToaruRepeater.DataSource = Enumerable.Range(1, 10);
        DataBind();
    }

    protected void Button_Click(object sender, EventArgs e)
    {
        try
        {
            // CheckされたCheckBoxの値をどう取り出す?
            Response.End();
        }
        catch (System.Threading.ThreadAbortException) { }
    }
}

こんな、まあ簡単な画面があるとします。チェックされたCheckBoxの値をどうやって取り出しましょうか?そうそう、ちなみにですがasp:CheckBoxはvalueが指定できないというゴミ仕様なのでやめておきましょう(Web Formsってそんなのばっか、もうやだよ……。ちなみにコード側のInputAttributesで渡すことは一応できます、一応)。

例によってRequest.Formから取り出すことになるので、まずはデバッガで値を見てやります。

チェックしたチェックボックスの値、ToaruCheckBox,3とToaruCheckBox,8が確認できます。では、どうやって取り出してやろうか。Repeaterの中のコントロールなのでUniqueIDを使うことはできません。ただ、コントロール階層順に$で連結されてる、という法則は見えるわけなので、単純に$でSplitして文字列一致で見てやりましょうか。

protected void Button_Click(object sender, EventArgs e)
{
    try
    {
        var checkedValues = Request.Form.AsEnumerable()
            .Where(x => x.Key.Split('$').Last() == "ToaruCheckBox")
            .Select(x => x.Value);

        // Checked:3, Checked:8
        foreach (var item in checkedValues)
        {
            Response.Write("Checked:" + item + "<br />");
        }
        Response.End();
    }
    catch (System.Threading.ThreadAbortException) { }
}

はい、これで完璧です!なお、ASP.NETのID生成ルールはそれなりに変更が聞くので、詳しくはプログラミングMicrosoft ASP.NET 4 (マイクロソフト公式解説書)でも読めばいいでしょう。

ボタンとCommandArgument

ボタンはOnClickではなくOnCommandを使うと、CommandArgument(とCommandName)を渡せて便利です。簡単な例としては

<asp:Content ContentPlaceHolderID="BodyPlaceHolder" runat="server">
    <form runat="server">
        <asp:Button runat="server" OnCommand="Button_Command" CommandArgument="<%# (int)Fruit.Grape %>" Text="ぶどう!" />
        <asp:Button runat="server" OnCommand="Button_Command" CommandArgument="<%# (int)Fruit.Apple %>" Text="りんご!" />
        <asp:Button runat="server" OnCommand="Button_Command" CommandArgument="<%# (int)Fruit.Orange %>" Text="みかん!" />
    </form>
</asp:Content>
public partial class WebForm1 : System.Web.UI.Page
{
    public enum Fruit
    {
        Grape,
        Apple,
        Orange
    }
    protected void Page_Load(object sender, EventArgs e)
    {
        if (IsPostBack) return;

        DataBind();
    }

    protected void Button_Command(object sender, CommandEventArgs e)
    {
        Response.Write("Clicked:" + (Fruit)int.Parse((string)e.CommandArgument));
    }
}

注意しなければならないのは、aspx上でCommandArgumentに渡すと文字列になるので、enumを渡すときはintにキャストしておくことと、イベント側のCommandEventArgsに渡ってくるときは文字列なのでintにParseしなければならないこと、です。クソ面倒くさいですね、もう少し気が利いてもいいと思うんですが、まあWeb Formsなのでしょうがないと思っておきましょう。

さて、ViewStateがオンならばこれでいいのですが、オフの場合はe.CommandArgumentは常に空文字列になってしまいます。何故か、というと、CommandArgumentはViewStateに乗ってやってくるからです。さて、どうしましょう、というと、解決策は部分的にオンにすることです。

<asp:Content ContentPlaceHolderID="BodyPlaceHolder" runat="server">
    <form runat="server">
        <asp:Button runat="server" ViewStateMode="Enabled" OnCommand="Button_Command" CommandArgument="<%# (int)Fruit.Grape %>" Text="ぶどう!" />
        <asp:Button runat="server" ViewStateMode="Enabled" OnCommand="Button_Command" CommandArgument="<%# (int)Fruit.Apple %>" Text="りんご!" />
        <asp:Button runat="server" ViewStateMode="Enabled" OnCommand="Button_Command" CommandArgument="<%# (int)Fruit.Orange %>" Text="みかん!" />
    </form>
</asp:Content>

ButtonのViewStateModeだけを"Enabled"にすることで、最小限のViewStateで最大限の成果を発揮することができます。ViewStateを嫌うならば、そもそもCommandなんて使わない!になるでしょうけれど、そこまでやってしまうと勿体ない。縁あってWeb Formsを使うわけなのですから、「控えめに」「隠し味」として、ViewStateを有効活用していきましょう。

リピーターとボタン

最後に、リピーターの中にボタンが仕込まれているパターンを見てみましょう。

<asp:Content ContentPlaceHolderID="BodyPlaceHolder" runat="server">
    <form runat="server">
        <asp:Repeater runat="server" ID="ToaruRepeater" ViewStateMode="Enabled">
            <ItemTemplate>
                <%# Container.DataItem %>:<asp:Button runat="server" OnClick="Button_Click" Text="ぼたん!" /><br />
            </ItemTemplate>
        </asp:Repeater>
    </form>
</asp:Content>
public partial class WebForm1 : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (IsPostBack) return;

        ToaruRepeater.DataSource = Enumerable.Range(1, 10).Select(x => new string(Enumerable.Repeat('a', 10000).ToArray()));
        DataBind();
    }

    protected void Button_Click(object sender, EventArgs e)
    {
        try
        {
            Response.Write("Clicked!");
            Response.End();
        }
        catch (System.Threading.ThreadAbortException) { }
    }
}

a(x100000):ボタン という表示結果が得られます。ボタンをクリックするとClicked!と実行されて欲しいわけですが、ViewStateがオフだとうんともすんとも言いません。何故か、というと、イベントの選択もまたViewStateに乗ってくるからです。解決策はRepeaterのViewStateModeをEnabledにすること、です。

しかし、単純にRepeaterのViewStateModeをEnabledにしただけだと、それ以下の全てのViewStateがオンになってしまいます。どういうことかというと、ViewStateを見てみると、この場合の結果は「133656文字」もあります!どれだけデカいんだよ!なぜかというとaaaaaa...(x10000) x 10がViewStateに乗っかってしまったからです。ただたんにボタンクリックできればいいだけなのに!じゃあ、どうするか、というと、ViewStateのオンオフを入れ子にしてRepeaterだけをオンにします。

<asp:Content ContentPlaceHolderID="BodyPlaceHolder" runat="server">
    <form runat="server">
    <asp:Repeater runat="server" ID="ToaruRepeater" ViewStateMode="Enabled">
        <ItemTemplate>
            <asp:PlaceHolder runat="server" ViewStateMode="Disabled">
                <%# Container.DataItem %>:<asp:Button runat="server" OnClick="Button_Click" Text="ぼたん!" /><br />
            </asp:PlaceHolder>
        </ItemTemplate>
    </asp:Repeater>
    </form>
</asp:Content>

これで解決。クソ面倒くさくて回りっくどくてイライラしますが、"そういうもの"だと思うしかないです。

Web Formsの良さをスポイルしてまでWeb Forms使いたい?

使いたくないです。それでもやらなきゃいけない時はあるんですDataSet死ね。

(ところで関係なく)async event

Visual Studio 2012 RC出ました!neue cc - Visual Studio 11の非同期(”C#, ASP.NET, Web Forms, MVC”)で非同期系について特集しましたが、Web FormsではRegisterAsyncTask(new PageAsyncTask)しなきゃならなくて面倒くさい死ねという感じでしたが、ようやくイベントにasyncをつけるだけでよくなりました!

protected async void Button_Click(object sender, EventArgs e)
{
    await Task.Delay(TimeSpan.FromSeconds(3));
    try
    {
        Response.Write("hoge");
        Response.End();
    }
    catch (System.Threading.ThreadAbortException) { }
}

正式リリース前に対応してくれて本当に良かった。これでWeb Formsでももう少し戦える……、戦いたくないけど。

まとめ

世の中にはUnobtrusive JavaScriptという言葉がありますが、そのように、私としてもUnobtrusive ViewStateを唱えたい。控えめに。とにかく控えめに。ほとんどないも同然なぐらいに。隠し味として使うのが、一番良いわけです。今までのWeb Formsはデフォルトオンで化学調味料をドバドバと投げ込んでいました。そんなものは食えたものじゃあありません。化学調味料を使うなら、超絶控えめに、ほんの少しでいいんです。そうすれば、革命的に美味しくなるのですから。それが正しい化学調味料の使い方。

そして、理想を言えば化学調味料はゼロがいいんですけれどね。ゼロにしたければWeb Formsはやめましょう。それ以外の答えはない。Web Formsを使う以上は正しく向き合うことが大事。

ViewStateをドバドバ使うことがWeb Formsの良さだというのも半分は事実ですが、半分はNOですね。ていうか馬鹿でしょ。そういう発想に未来はないし脳みそイカれてると思いますよ。DataSetを褒め称えていた狂った時代の発想なので、腐った部位はとっとと切り落としましょう。

さて、そうして控えめにしたWeb Formsですけれど、これはこれでそれなりに良いとは思います。最小限にしたとしても、依然としてパーツ配置してOnClickでほいほい、という楽ちんさは健在ですし、カスタムコントロールによるモジュール化というのは悪くない。絶賛はしませんが、悪くないとは思います、そこまで悪しざまにいうものでもない。でも、まあ、もはや時代じゃあないでしょうね。もう、さようなら。

と、まあ割とキツめにWeb Forms(やDataSet)にあたるのは日々苦しめられているからなので、罵詈雑言多いのは勘弁してね!別に現状がダメでも未来に良くなるとかならいいんですが、明らかに未来がないので、なんというかもうねえ、という。Web Formsも、これはこれで面白い仕組みだし、まっとうに進む道もあったとは思うんですが、舵取りにしくったと思います。結果的にこの閉塞感漂う現状と、明らかにリソース割かれてない感があるので、未来はないですね、残念ながら。しかし、最後の徒花としてやれるだけはやるのさぁ。

配列とGetEnumetorのお話

LINQ書いていますか?LINQでデータベース活用とか見出しにありますが、データベース活用といいつつ、その内実は100% LINQ to Objectsです。じゃあデータベースに何で問い合わせやってるの?というと(禁則事項です)。さて、そんなわけで毎日LINQ書いているわけですが、それとは全く関係なく、配列が結構困ったちゃん。例えば以下のような、配列を包んだコレクションを提供したいとします。

public class WrappedCollection<T>
{
    T[] source;

    public WrappedCollection(T[] innerSource)
    {
        this.source = innerSource;
    }
}

で、T[]なsourceを元にメソッドを幾つか提供する、と。それ自体はないこともないと思われます。さて、Collectionを名乗っているので、IEnumerable<T>であってほしいですよね?

なぜだ、って、配列のGetEnumeratorの戻り値はIEnumeratorなのです。IEnumerator<T>ではなくて。マジで!マジで。さて、どうしようかしら、と。foreach(var item in source) yield return item; をすれば解決ですが、そんなダサいことはやりたくない。正解は、AsEnumerableです。

public class WrappedCollection<T> : IEnumerable<T>
{
    T[] source;

    public WrappedCollection(T[] innerSource)
    {
        this.source = innerSource;
    }

    public IEnumerator<T> GetEnumerator()
    {
        return source.AsEnumerable().GetEnumerator();
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

これだけで解決。やったね、AsEnumerableは偉大で奥深いなあ。さて、以前にDeep Dive AsEnumerableで書きましたが、AsEnumerableの実態はただのキャストなので

return ((IEnumerable<T>)source).GetEnumerator();

でもOKです。

さて、それらの中身ですが

var source = Enumerable.Range(1, 10).ToArray();

var e1 = source.GetEnumerator();
var e2 = source.AsEnumerable().GetEnumerator();

// System.Array+SZArrayEnumerator
Console.WriteLine(e1.GetType());
// System.SZArrayHelper+SZGenericArrayEnumerator`1[System.Int32]
Console.WriteLine(e2.GetType());

といった具合に、型が違うと渡ってくるEnumeratorも違うようですね。これ自体は別にスペシャルな機能ではなく明示的なインターフェイスの実装をした時の挙動、ではありますが、まあ配列周りはそもそもに色々とややこしいですからね。私みたいなゆとりなんて、SZって何だよクソが(single-dimension zero-baseの意味だそうで)とか思ってしまいます。

まとめ

IEnumerable<T>じゃないコレクションは逝ってよし。つまりMatchCollectionは何で.NET 4.5になっても手を加えてくれないんだよぅううううぅぅぅ。

XboxInfoTwit - ver.2.4.0.3

Xbox.comが内部的にちょっと変わっていて動かなくなっていたので、それに対応しました。前々から報告されている不具合などに関する修正などは一切入ってません、すみません。最近割と普通に忙しくて全く手をかけられない状態でして……。落ち着いたらその時には必ず。

MetroStyleDeveloper #03の告知

最近は告知ばかりで、まともにブログ記事書いてなくてすみませんすみません。Windows Developer Daysでのセッションはお陰様で立ち見が出るほどに大盛況で、うれしかったです。資料に関しては、公開できるように調整中です(いちおー会社身分で出てるので、公開するときは会社サイトのほうになるかしら、と思っています)。中々面白い感じに仕上がっているので、なるべく早いうちに出したいところです。

さて、WDDのキーノートで 4月25日に開催されたMicrosoft Windows Developer Days にて、Windows8ベースで開発したグループスの試作アプリが発表されました ということで、シアトルクエストという試作アプリが公開されました。その詳細を、5/12に開催されるMetroStyleDeveloper #03で発表します!(予定)。会場、弊社ですお。

というわけで、気になった方はそちらも是非是非来ていただけたらと思います。

Windows Developer Daysで登壇します

4/24-25に開催されるMicrosoft Windows Developer Daysで登壇します。セッション一覧より、Day 2の11:30-13:00、Room Fでタイトルは「LINQ to Everything : データソース・言語を超える LINQ の未来」です。

これなのですが、私の勤め先のgloopsがWDDのプラチナスポンサーで、そのスポンサー用のセッション枠を私が頂いた、みたいな形です。スポンサー枠なのに完全に趣味のLINQの話しかしない気満々とか非常にアレ。そんなわけなので是非来てください。ランチセッションなのでお昼も出るので、ご飯食べながらゆるゆると聞いてもらえればと思ってます。

弊社からは、他にSQL Azure MVPの大和屋さんがgloops提供人気ソーシャルゲームでトラフィック限界に挑戦!SQL Azure Federation検証結果公開というタイトルで4/24に出ますので、そちらも是非是非どうぞ。

Microsoft MVP for Visual C#を再受賞しました

去年の報告から一年、再受賞することができました。活動はあまり変わらずC#とLINQについてブログに書いて、小さめのライブラリを作って公開して、それと今回は少し、外での活動が増えましたね。つい一昨日もSilverlightを囲む会でReactive Extensions v2.0としてお話しました。

日本の海外に比べて弱いところ、先端的な技術の紹介であったりディープに踏み込んだ使い方であったり、何かを作って公開することであったり。それらを埋めていきたいな、と思っています。そういったことを通してC#の良さを伝えられていければ何よりです。

近況

さて、しかし色々何かと停滞中です……。ライブラリは更新していない、ツールもバグ報告スルー中、Pull Requestすら放置、原稿……、などなどなどなど。はい、どうみてもダメ人間です。やりたいこと、はいっぱいあって、やらなければならないこと、もそれなりにあって、それらがかちあってどっちも出来ず仕舞いで時間を浪費してばかり。今年はその辺もしっかり整理して、より多くの何かを届けていきたいところです。実際のとこダラダラTwitterやってる時間が一番長くてマズいのは確定的に明らか。Twitterやってる時間をSkyrimに回したほうがよっぽど有意義ってものですよ!しかもなんと超絶楽しみにしていたSkyrimは未だ未開封という、もうアレ。悲しいですね。

ソーシャルゲームとわたくし

最近のWeb業界の風潮にならって、ちゃんとロクロ回しました!

猫も杓子もみんなソーシャルゲーム業界に行くんだなあ、とかポケーッと眺めていましたが、まさか私も行くとは全然思ってもいませんでした。というわけで、1月から転職したと書きましたけれど、株式会社gloopsで働いています。転職理由とかは、インタビューの通りです。C#を世間(といっても、技術系の人に、ですね)に強くアピールするにはどうすればいいのか、といったら、一番大事なのはそれで書かれたメジャーなアプリケーションなのです。PerlではてなやMixi、livedoorなどを思い浮かべる、RubyでCookpadを、ScalaでFoursquareやTwitterを、そういった憧れにも似た気持ちを浮かべさせるようなアプリケーションがなければいけなくて。

Stack OverflowはC#, ASP.NET MVCだし、TIOBEのプログラミング言語ランキングでは三位など、海外でのC#の地位は十分に高い。のですが、国内ではそれと比べれば全くもってない。日本で誰もが知る会社の誰もが知るアプリケーション、それがC#で書かれている。そういう状態にならなければ、日本で強く普及は無理だな、と。

ギョーム系アプリでは十分に強いじゃん、とかそうじゃなくて、私としてはもっと学生とかも憧れてガンガン使う感じであって欲しいんですよ。Visual Studioは無料版ありますし、学生向けの支援(DreamSparkなど)もやってはいますが、あまりリーチできてないのではないかなあ、って。そういうのって内からやりたいって気持ちが湧いてきて初めて支援があって嬉しい!になるわけで。

まあその領域だったら、やっぱゲームですよゲーム!なんというか、Unityブームのほうがずっと請求してるかもなのですかねー。というわけで第77回codeseek勉強会&第17回日本C#ユーザー会 勉強会 : C#とゲームでは、C#とゲーム全般を取り扱って、そのなかで弊社CTOの池田もソーシャルゲーム枠でセッション持ちますので、残席まだありますので是非是非どうぞ(宣伝)

ゲームもいいのですが、デスクトップアプリケーションもいいし(最近はあんまし流行らないですって?そうかもですがー)、モバイルアプリもいいし(Windows Phone 7よりも、MonoTouchやMono for Androidのほうが受けますな)、そして、ウェブアプリもいい。C#の持つ強みや範囲というのは、本当に、もっともっと知られて欲しいなって。

そんなようなことは入社初日にも言ったりなどしていて、今も当然変わってませんし、3ヶ月働いてきて、実現できる会社であると実感しています。2012 グループス MLB開幕戦の冠協賛や、最近はCMも増えてきたりなど、露出も増えてきて、勢いありますね。その勢いを止めない、加速させるためにも、まだまだ人が必要というわけで、求人バナーもかなり見かけてるんじゃないかと思います。というわけで弊社ではエンジニアを絶賛募集中なのでC#で書きたい!という人は是非是非お願いします(宣伝)

あと、私自身の目的としてはもう一つあって、日本ローカルだけじゃなく世界にも通用する技術力を掲示したいという欲求があります。その面でも、世界に向けても前進している(gloops International CEO冨田由紀治氏インタビュー - GAME Watch)のは、マッチしていました。とはいえ、まずは日本、です。そもそも全然半人前だと痛感する毎日で寝言は寝てから言え状態なので、日々鍛錬ですね。

ソーシャルゲーム

このサイトの前身はゲーム攻略サイト(Xbox系)で、2002年からです(このサイトのアレゲな配色はその頃から全く変わってないから、というのが大きな理由です)。また、特に好きなGeometry Warsなんて世界ランキングでもかなり上位だったりニコ動にプレイ動画を投稿したりする程度には、一応そこそこハードなゲーマーを自称していたのですが、最近はめっきりゲームとは遠ざかってしまいました。あうあう。完全にプログラミング系サイトですしねー。

というわけで、元ゲーマーとでもしませうか。で、ソーシャルゲーム。ゲーマーといったらソーシャルゲームは割と忌み嫌うという感じの!ふむ。まあでも、ほら、最近はアイドルマスター シンデレラガールズが(色々な意味で)話題だったり、それはそれで独特に面白さってのはあるんですね。いや、モゲマス面白いですよ、ガチャ地獄とか除いても普通に。必ず人と繋がり合う(それが衣装泥棒であっても)、それも緩やかに非同期に(MMORPGや対戦系は同期的ですから)というのは、独特なものがあると思います。

正直いって、まだ完全に面白さには繋がってないとは思いますが(特にゲーマー向けには)、このシンプルで非常に限られた中からゲーム性(と○○)を引き出すチャンスは転がっている、かもしれません。ずっとガラケー向けに貧相な画面とボタン連打だけで変わらない、わけでは、ない。スマートフォンの時代は来ているし、HTML5の流れだってあるし、それはソーシャルゲームだって同じなのです。

フロントエンドはモバイル向けというのを考慮しながらもリッチしなければならないし、バックエンドはハイパフォーマンスに耐えなければならない。課題は大量にありますが、だからこそ技術者としては挑戦しがいのある面白いテーマが山のように転がっています。例えばC# 5.0が非同期でハイパフォーマンスといったって、別にそんなのそこまで必要じゃないしなー、で終わってしまう状況もいっぱいあると思うのです。でも、弊社では、今すぐにでも必要なのだというぐらいに切羽詰ってる。C#の能力をフルに使いきることが求められる。これは楽しいですね?はい、楽しいです。

会社員ですからー

インタビュー記事が求人記事なので、会社員として宣伝しました!なので基本的にイイ事しか言いませんが、勿論イクナイ面もそれなりにあります。例えば最近の私の記事を見ると某テクノロジーをやたらDisってますが、なんでなんでしょうかねー、ふふり。とはいえ、不満に思うなら自分でぶち壊して再構築すればいいし、それが許される(勿論ちゃんと能力を証明したうえで)環境だとは思います。スキルある人が何の制約もなく存分に腕をふるえるのなら、素敵な話ではないでしょうか。

ちなみに私のスキルはC#/LINQに偏っていてウェブとかASP.NETの知識は並なので、毎日勉強です、人はそれを付け焼刃とも言う。

Visual Studio 11の非同期("C#, ASP.NET, Web Forms, MVC")

世の中ひどぅーきひどぅーきと騒ぐばかりで、猫も杓子もNode.js。でもですね、【デブサミ2012】16-A-5 レポート ソーシャルアプリケーションにおけるNode.jsの活かし方(1/2):CodeZineなんかを見ても、そこで独自に作りこんでる例外処理だの非同期フロー管理だのは、そりゃあ必要ですよね、まずはそこから始めるのは当然ですよね、と思いつつC#は最初から備えているんですよね。むしろ色々とC#のほうが、とか思ったりするわけですが(勿論Node.jsのほうがGoodなものもありますが)、こんなところで嘆いていても始まらないのでC#流の非同期の活かし方を見ていきましょうか。

HttpTaskAsyncHandler

ASP.NETの非同期ハンドラはIHttpAsyncHandlerなわけですが、VS11ではそれをTask(つまりC# 5.0 async/await)で扱いやすくした基底クラス、HttpTaskAsyncHandlerが用意されています。例えばTwitterの検索を叩いて返すだけどのものは以下のようになります。

public class TwitterSearchHandler : HttpTaskAsyncHandler
{
    public async override Task ProcessRequestAsync(HttpContext context)
    {
        var term = context.Request.QueryString["q"];
        var json = await new HttpClient().GetStringAsync("http://search.twitter.com/search.json?q=" + term);

        context.Response.ContentType = "application/json";
        context.Response.Write(json);
    }
}

普通と違うのはasyncとawaitだけなので、特に混乱もなく同期→非同期に乗り換えられると思います。非常に簡単。

HttpClientも.NET 4.5からの新顔で、WebClientの後継的な位置付けでしょうか。細かいコントロールも可能で、かつ、WebRequestよりも簡単で、非同期にもきっちりマッチしている。というかHttpClientには同期的なメソッドは用意されていません。これからの非同期世代に完全準拠した新しいクラスということですね。

そして、テスト用のサーバー立てるのも非常に簡単で。Visual Studioで新規で空のASP.NETサイトプロジェクトを作って、↑のハンドラ足して、Ctrl + F5すればIIS Expressが立ち上がって、もうそれだけでOKなわけですよ。超簡単なわけですよ、マジでマジで。

こないだ、RIA アーキテクチャー研究会 第3回でのセッションではそうして作ったHttpTaskAsyncHandlerで、 context.Response.StatusCode = 404 にしてエラーを返した状態を再現したりしながらデモしていました。

今回はTaskを中心にしましたが、Rxを中心にしたものをSilverlightを囲む会in東京#6で3/31に話す予定なので、まだ募集中なので是非来て下さい。また、Rx v2.0に関してはReactive Extensions v2.0 Beta available now! - Reactive Extensions Team Blog - Site Home - MSDN Blogsで超詳細に書かれていますね。私もちょいちょいと書きたいことは溜まってるのですが中々にぐぬぬぬ。

非同期ページ

今更Web Formsとか超どうでもいいって感じが世界全体に漂ってるし真面目に色々と腐ってると本気で思うしDataSetとWeb Formsは今となっては.NET三大汚点の筆頭かなとか思ったり思わなかったり適当に言ったり呪詛を吐いたり、もう色々アレなのですが、それでも現実とは戦わなければならないのです!

というわけでVS11のASP.NET Web Formsの非同期の強化でも見てみましょう。C# 5.0でasync/awaitが入るのでASP.NET MVCのほうは非同期コントローラーでヒャッホイなのですがWeb Formsも一応対応してきました、一応ね、一応。

// Web.config
<appSettings>
  <add key="aspnet:UseTaskFriendlySynchronizationContext" value="true" />
</appSettings> 
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="~/WebForm1.aspx.cs" Inherits="WebApplication8.WebForm1"
    Async="true" ViewStateMode="Disabled" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Async Test</title>
</head>
<body>
    <form id="form1" runat="server">
    <asp:TextBox ID="WordTextBox" runat="server" />
    <asp:Button ID="SearchButton" runat="server" Text="Button" OnClick="SearchButton_Click" />
    <asp:Repeater runat="server" ID="TwitterStatuses" ItemType="dynamic">
        <ItemTemplate>
            <p>
                <asp:Label runat="server" Text="<%#: Item.from_user %>" /><br />
                <asp:Label runat="server" Text="<%#: Item.text %>" />
            </p>
        </ItemTemplate>
    </asp:Repeater>
    </form>
</body>
</html>
// namespace WebApplication8
public partial class WebForm1 : System.Web.UI.Page
{
    protected void SearchButton_Click(object sender, EventArgs e)
    {
        var task = new PageAsyncTask(async () =>
        {
            var word = WordTextBox.Text;
            using (var stream = await new HttpClient().GetStreamAsync("http://search.twitter.com/search.json?q=" + word))
            {
                var json = System.Json.JsonObject.Load(stream);
                TwitterStatuses.DataSource = json["results"];
            }
            DataBind();
        });

        RegisterAsyncTask(task);
    }
}

非同期ページの利用には Async="true" 属性をつける必要があります。.NET 4.0まではつけていない場合は、同期的に動作するようになっていたのですが、.NET 4.5からはエラーになるように挙動が変更されています。また、PageAsyncTaskを利用する場合はWeb.configにUseTaskFriendlySynchronizationContext = true する必要もあるっぽいです。

これ自体はテキストボックスに検索語を入れてボタンを押すとひどぅーきでTwitter検索して表示する、というだけのDoudemoii代物です。PageAsyncTaskが引数にTaskを受け入れるようになったので、そこでasyncなラムダ式を突っ込んでやればいい、というわけで、まあまあ簡単と言えなくもなく仕上がっています。理想的には/直感的にはasync void SearchButton_Clickと書けるようになるべきなのですが、そうはいかないようです、残念。

JSONは.NET 4.5からお目見えのSystem.Jsonを使いました。これ、AsDynamic()とするとdynamicで扱えるのでサクサクッと使えて便利です。また、そのdynamicとして使える性質を活かして、dynamicのままバインドしてみました(AsDynamicはコード上dynamicにキャストするというだけで、JsonValueはそのもの自身がdynamic = IDynamicMetaObjectProviderなのです)。System.JsonはNuGet - System.Jsonにもあるので、.NET 4ではそれを使えばいいでしょう。DynamicJsonはお払い箱で。

それとRepeaterのItemType="dynamic"。これでItem.from_userといったように、dynamicに使えるようになっています。匿名型をバインドしたい時なんかも、同じようにItemType="dynamic"にしてしまうといいかな、と思ったんですが、それは出来ませんでした。あともう一歩、気を利かせてくれても良かったですねえ。

まあ、VS11からは、念願のバインディング式の中でIntelliSenseが効くようになっていて、それはRepeaterのItemTypeも例外ではないので、ちゃんと型作ってあげるのも良いとは思います。あと%:でHtmlEncodeもしてくれますのも良いところ。

ViewStateMode="Disabled"で無駄なViewStateは生成しないようにするのも大事。これは.NET 4.0からですね。EnableViewStateとは別物という紛らわしさが残っているのも、まあなんともかんとも。ところでPageのViewStateModeをDisableにしてしまうと、this.ViewState[]が使えなくなってしまうので、マスターページからの、asp:Contentにしかけたほうがいいかもです。

EventHandlerTaskAsyncHelper

ASP.NETの非同期関連はMSDNマガジンのWickedCode: ASP.NET の非同期プログラミングを使ったスケール変換可能なアプリケーションにまとまっていますが、そこにあるとおり非同期ページの実現方法にはもうひとつ、AddOnPreRenderCompleteAsyncを使う方法があります。それにもTask用のやり方がありますので、見てみましょう。

var helper = new EventHandlerTaskAsyncHelper(async (_, __) =>
{
    var word = WordTextBox.Text;
    using (var stream = await new HttpClient().GetStreamAsync("http://search.twitter.com/search.json?q=" + word))
    {
        var json = System.Json.JsonObject.Load(stream);
        TwitterStatuses.DataSource = json["results"];
    }
    DataBind();
});

AddOnPreRenderCompleteAsync(helper.BeginEventHandler, helper.EndEventHandler);

EventHandlerTaskAsyncHelperを作り、それのBeginとEndをAddOnPreRenderCompleteAsyncに渡してあげます。ちょっとPageAsyncTaskより面倒ですね。まあ、でも、どちらでもいいでしょう。大した違いはありません。二つやり方があるとどちらにすればいいのかと迷ってしまうのが良くないところなんですよねえ、しかもどちらも似たようなものだと……。

非同期モジュール

Moduleについても見てみましょう。感覚的にはAddOnPreRenderCompleteAsyncと一緒で、EventHandlerTaskAsyncHelperを作り、追加したいイベントにBeginとEndを渡します。

public class MyModule : IHttpModule
{
    public void Init(HttpApplication application)
    {
        var helper = new EventHandlerTaskAsyncHelper(async (sender, e) =>
        {
            var app = (HttpApplication)sender;
            var path = app.Server.MapPath("~/log.txt");

            using (var fs = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.Read, 4096, useAsync: true))
            using (var sw = new StreamWriter(fs, Encoding.UTF8))
            {
                await sw.WriteLineAsync("Request:" + DateTime.Now);
            }
        });

        application.AddOnBeginRequestAsync(helper.BeginEventHandler, helper.EndEventHandler);
    }

    public void Dispose() { }
}

AddOnXxxAsyncは沢山あるので、追加したいイベントを選べばいいでしょう。また、非同期でファイルを扱いたい時は、useAsync: trueにするのが大事です。デフォルトはfalseになっているので、Begin-Endをしても非同期にならない(というかスレッドプールを使った挙動になってしまう)そうです(と、プログラミング.NET Frameworkに書いてあった)。

非同期コントローラー

一応ASP.NET MVCでも見てみましょうか。TwitterのPublicTimelineを表示するだけのものを(テキストボックスすら作るのが面倒になってきた)

public class PublicTimelineController : AsyncController
{
    public async Task<ActionResult> Index()
    {
        using (var stream = await new HttpClient().GetStreamAsync("https://twitter.com/statuses/public_timeline.json"))
        {
            var json = System.Json.JsonObject.Load(stream);
            return View(json);
        }
    }
}
<!DOCTYPE html>
<html>
<head></head>
<body>
    @foreach (var item in Model)
    {
        <p>
            @item.user.screen_name
            <br />
            @item.text
        </p>
    }
</body>
</html>

AsyncControllerの自然さと、きゃーRazor最高ー抱いてー。

まとめ

HttpTaskAsyncHandlerにせよEventHandlerTaskAsyncHelperにせよ、中身は割とシンプルにTaskでラップしただけなので、それを自前で用意すればTask自体は.NET 4.0に存在するので、async/awaitは使えませんがそれなりに簡単に書けるようにはなります。とりあえず私はWeb Forms用のものを仕事で使うために用意しました。コードは会社で書いたものなので上げられませんが!というほど大したものでもないので上げちゃってもいいんですが上げません!Web Formsにはとっととお亡くなりになってもらいたいので。延命措置禁止。

Web Formsだって悪くないものだ、全力で頑張ればほら、こんなに出来るじゃないか、ということは容易い、ことはまったくなく全力なわけですが、しかし可能ではあるんですね、モバイル対応だろうがハイパフォーマンスサイトだろうが。きっとたぶん。でもね、なんかもうIE6にも対応しつつHTML5サイトです、とかやるぐらいに不毛感漂ってるし、その労力は別のとこに向けたいですよね、っていうか別のとこに向けばどれだけ幸せになれるだろうか、と思ってしまうのです。

考えてみると、こうもうぇぶけーな話を書くのも初めてな気がする。近頃はお仕事がそっち方面なので、出せる範囲でちょいちょい出してこうかと思います。とにかく結論としてはWeb Formsちゃんは、もう沢山頑張ったと思うのでそろそろ逝ってもらって構いません。

LINQのWhereやSelect連打のパフォーマンス最適化について

Where連打していますか?それともパフォーマンスの悪化を心配して&&連結にしていますか?LINQの仕組み&遅延評価の正しい基礎知識 - @ITではWhere+Selectに対して

「WhereSelectEnumerableIterator」となっていて、名前のとおり、WhereとSelectが統合されていることです。これは、「Where」->「Select」が頻出パターンなので、それらを統合することでパフォーマンスを向上させるためでしょう。

と書きましたが、では連打の場合はどうなっているでしょうか。見てみましょう。

var seq1 = Enumerable.Range(1, 10)
    .Where(x => x % 2 == 0)
    .Where(x => x % 3 == 0);

// どうでもいいんですが、これはVisual Studio 11 Betaです。VS11最高ですよ!

@ITの記事では、sourceに格納されて内包した形の連鎖となっている、と書きました。しかしseq1のsourceはRangeIteratorで、Where連打のはずなのに、すぐ上の階層が元ソースとなっています。そして、predicateの名前がCombinePredicates。はい、その通りで、2つの条件式が連結されています。確認してみましょう。

var pred = (Func<int, bool>)seq1.GetType().GetField("predicate", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(seq1);

Console.WriteLine(pred(2)); // false
Console.WriteLine(pred(3)); // false
Console.WriteLine(pred(6)); // true

というわけで、Where連打はpredicateが連結されて一つのWhereに最適化されることが確認できました。LinqとCountの効率でICollectionやIListの場合の特別扱いなケースがあることを紹介しましたが、Whereに関しても同様な特別扱いが発生するというわけです。

Selectの場合

Whereの他にSelectの場合も、同じような最適化を行ってくれます。

var seq2 = Enumerable.Range(1, 10)
    .Select(x => x * 2)
    .Select(x => x + 10);

var selector = (Func<int, int>)seq2.GetType().GetField("selector", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(seq2);

Console.WriteLine(selector(2)); // 2 * 2 + 10 = 14
Console.WriteLine(selector(5)); // 5 * 2 + 10 = 20

sourceのすぐ上がRangeIteratorで、selectorにCombineSelectorsとして格納されていました。なお、型名であるWhereSelectEnumerableIteratorのとおり、現在はpredicateはnullですが、前段にWhereを書けばpredicateに格納されて、やはりWhere+Selectの最適化となります。では、後段にWhereを書いた場合は……?

最適化されない場合

Where+SelectとSelect+Whereは異なるものです。見てみましょう。

var whereSelect = Enumerable.Range(1, 10)
    .Where(x => x % 2 == 0)
    .Select(x => x * 2);

var selectWhere = Enumerable.Range(1, 10)
    .Select(x => x * 2)
    .Where(x => x % 2 == 0);

Where+SelectはWhereSelectEnumerableIteratorのpredicateとselectorにそれぞれデリゲートが格納され、ひとまとめに最適化されていますが、Select+WhereはsourceがRangeIteratorではなくWhereSelectEnumerableIteratorであるように、普通に階層の内包構造となっています。Selectの後にWhereは最適化されません。まあ、そりゃ値が変形されているのだからpredicateがひとまとまりになるわけがなく、当たり前ではあります。

次にインデックスが使えるオーバーロードのケースを見てみましょう。

var whereIndex = Enumerable.Range(1, 10)
    .Where(x => x % 2 == 0)
    .Where((x, i) => i % 2 == 0);

var selectIndex = Enumerable.Range(1, 10)
    .Select(x => x * 2)
    .Select((x, i) => i * 2);

// GetEnumeratorしないとpredicate/selectorとsourceがnullです
// これはyield returnによる生成なためです
whereIndex.GetEnumerator();
selectIndex.GetEnumerator();

これもひとまとめにしようにも、しようがないので、当然といえば当然ですね。

IQueryableの場合

IQueryableだとどうなのか、というと……

// LINQ to SQL - AdventureWorks sample
var ctx = new AdventureWorksDataContext();
var query = from model in ctx.ProductModel
            where model.Name == "hoge"
            where model.ProductModelID == 100
            select model.Instructions;

Console.WriteLine(query);
// 結果
SELECT [t0].[Instructions]
FROM [Production].[ProductModel] AS [t0]
WHERE ([t0].[ProductModelID] = @p0) AND ([t0].[Name] = @p1)

というわけで、LINQ to SQLはand連結されますね。ここで注意なのが、どういう挙動を取るのかは全てクエリプロバイダの解釈次第です。例えばLINQ to Twitterはwhere連打ではダメで、&&で連結しなければなりません。

Reactive Extensionsの場合

Reactive Extensionsの場合も見てみましょうか。Rx-Main(1.0.11226)では、というと

var rx = Observable.Range(1, 10)
    .Where(x => x % 2 == 0)
    .Where(x => x % 3 == 0);

さっぱりワケワカメですが、とりあえずひとまとめになってないのでは感でしょうか。それにしても本当にワケワカメ。次にRx_Experimental-Main(1.1.11111)は、というと

var pred = (Func<int, bool>)rx.GetType().GetField("_predicate", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(rx);

Console.WriteLine(pred(2)); // false
Console.WriteLine(pred(3)); // false
Console.WriteLine(pred(6)); // true

_predicate発見!Experimental版では挙動が改善され、ひとまとめにされているようです。IDisposable<Generate>は、Rangeの生成がGenerateメソッドによってなされているからですね。しかし、やはり読み取りにくい。

Rx v2

3/5にReactive Extensions (Rx) v2.0 Betaの配布がスタートしています。NuGetではInstall-Package Rx-Main -Preで配布されていますね。改善内容は後日詳しくということでまだ詳しくは出てないのですが、v2というだけあって中身はガラッと変わっています。とりあえず、見てみましょう。

もちろん、predicateはひとまとめにされているのですが、それだけじゃなくて、とにかく見やすい、分かりやすい。しかし、ところどころ変ですね、Observαble(aがアルファ)だのΩだの。v2はソースのキチガイ度が跳ね上がっているのでILSpyとかで覗いちゃえる人は一度見ちゃうといいと思います、頭おかしい。あと、C#でのプログラミング的な小技も効いてたりして、テクニックの学習にもとても良い。

スタックトレースへの影響

このコードのクリアさはスタックトレースにも良い影響を与えています。まず、Rx v1で見てみると

try
{
    Observable.Range(1, 10)
        .Where(x => x % 2 == 0)
        .Take(10)
        .Timestamp()
        .Subscribe(_ => { throw new Exception(); });
}
catch (Exception ex)
{
    Console.WriteLine(ex.StackTrace);
}
場所 ConsoleApplication9.Program.<Main>b__1(Timestamped`1 _) 場所 c:\Users\ne
場所 System.Reactive.AnonymousObserver`1.Next(T value)
場所 System.Reactive.AbstractObserver`1.OnNext(T value)
場所 System.Reactive.AnonymousObservable`1.AutoDetachObserver.Next(T value)
場所 System.Reactive.AbstractObserver`1.OnNext(T value)
場所 System.Reactive.Linq.Observable.<>c__DisplayClass408`2.<>c__DisplayClass40a.<Select>b__407(TSource x)
場所 System.Reactive.AnonymousObserver`1.Next(T value)
場所 System.Reactive.AbstractObserver`1.OnNext(T value)
場所 System.Reactive.AnonymousObservable`1.AutoDetachObserver.Next(T value)
場所 System.Reactive.AbstractObserver`1.OnNext(T value)
場所 System.Reactive.Linq.Observable.<>c__DisplayClass43e`1.<>c__DisplayClass440.<Take_>b__43d(TSource x)
// 以下略

これは酷い。こんなの見ても何一つ分かりはしません。では、Rx v2で試してみると

場所 ConsoleApplication10.Program.<Main>b__1(Timestamped`1 _) 場所 c:\Users\n
場所 System.Reactive.AnonymousObserver`1.Next(T value)
場所 System.Reactive.ObserverBase`1.OnNext(T value)
場所 System.Reactive.Linq.Observαble.Timestamp`1._.OnNext(TSource value)
場所 System.Reactive.Linq.Observαble.Take`1._.OnNext(TSource value)
場所 System.Reactive.Linq.Observαble.Where`1._.OnNext(TSource value)
場所 System.Reactive.Linq.Observαble.Range._.LoopRec(Int32 i, Action`1 recurse)
場所 System.Reactive.Concurrency.Scheduler.<>c__DisplayClass3a`1.<InvokeRec1>b__37(TState state1)
// 以下略

めっちゃよく分かる。Timestamp->Take->Where->Rangeという遡りがしっかり見える。何て素晴らしいんだ!

匿名 vs 有名

さて、どういうことかというと、これ、neue cc - Rx(Reactive Extensions)を自前簡易再実装するで紹介したような、ラムダ式をぶん投げてその場で匿名のクラスを作るAnonymousパターンをやめたんですね。で、代わりに名前付きのクラスを立ててる。だから分かりやすい。

これ、uupaaさんが仰ってるナビ子記法←ググッた先の本人のスライドが、Handsoutがサービス終了で見れないので、紹介のある記事にリンクします-などにも近いところがあるかなあ、と。

ただやっぱ書くのにはコスト高というか匿名で書けることの良さを殺してしまうので、ライブラリサイドだったら検討する、アプリケーションサイドだったらやらない、になってしまうかなあ。ライブラリサイドであってもかなり手間なので、よほど余裕あるとかでないとやらない、かなあ。JavaScriptならともかくC#では、特に……。

Rx v2についてもう少し

詳しい話は詳細が出てから、と思いますが(と言いながらRxJSの話も結局書いてないので、あうあう)、とりあえずObservableへの拡張メソッド郡はExperimentalから変化は特にありません。ただ、Experimentalは既にStableとはかなり違っているので、Stableしか追っかけてない人は、かなり目新しいものを見かけることができると思います。

内部実装は見たとおりガラッと変わって、スタックトレースも見やすくなった、などなどなわけですが、それとあわせてパフォーマンスも相当上がっています。v1で基本的な部分を固めたので、v2ではそういった周辺部分に本気で取り組みだした、ということですね。

まとめ

LINQは細かいところまで配慮が行き届いていて本当に素晴らしいですね。というわけで平然とWhereの連打かましましょう。私もつい昨日にWhere6連打かましたりしてました。

linq.js - LINQ for JavaScriptはさすがにここまではやってないんですが、いずれかはやりたいですね。その前にやらなきゃならないことがありすぎて当面はないですけれど。うーん、なんかもう色々やることありすぎて、かつそれなりに忙しくて、頭が爆発しそうです。はっきしいってヤバい。で、こうしてヤバくなると、硬直しちゃって余計に何もできなくなったり、唐突にこうして息抜き記事を書き出したり、うみみぅ。まあともかく、がんばろふ。

とあるRoslynではないC# Parser、NRefactoryの紹介

ロッズリーン、はっじまらないよ~。というわけでMicrosoft “Roslyn” CTP、Compiler as a Service。NuGet - Roslynでも手に入るので、サンプル類やC# Interactiveとかはなしで、とりあえずScriptingやCompilerを触ってみたい、ということなら、お手軽です。しかし、まあ未実装も少なくなく、まだまだ先は長そうな雰囲気ではある。今すぐ欲しいのに!切実にC# Parserが!というわけで、今日はその良き代替となる(かもしれない)NRefactoryを紹介します。

NRefactoryはNuGet - NRefactoryからも入ります。verは5.0.0.4、「This is an alpha release. Expect bugs and breaking changes in the future.」とのことで、こちらもまだまだこれからのよう(MonoDevelopの新しいC#エディタで使われる予定、だそうです)。とりあえずNuGetで参照してみませう。

参照するとMono.Cecilが入ったり名前空間にMono.CSharpがあったりと、全体的にMonoのCSharp Compilerやその周辺が使われているふいんき。Mono.CSharpは単体でもついこないだNuGet - Mono.CSharpで入れられるようになりましたが、そのまんまだと、なんというかどう使っていいか分からないというか、はいEvalできた、さて、はて?みたいになってしまって。そのへん、NRefactoryはゆるふわで、結構すぐに使い方分かります。とりあえず使ってみませう。

using System;
using System.IO;
using System.Linq;
using ICSharpCode.NRefactory.CSharp;

class Program
{
    static void Main(string[] args)
    {
        var code = File.ReadAllText(@"../../Program.cs");
        var parser = new CSharpParser();

        var root = parser.Parse(code, "");

        var program = root.Descendants.OfType<ICSharpCode.NRefactory.CSharp.TypeDeclaration>().First();
        program.Name = "Hogegram";

        Console.WriteLine(root.ToSourceString());
    }
}

public static class CompilationUnitExtensions
{
    static readonly CSharpFormattingOptions DefaultOptions = new CSharpFormattingOptions()
    {
        // TODO:130のboolを自分の気にいるようなOption(というかVSのデフォ)に近づける
    };

    public static string ToSourceString(this CompilationUnit compilationUnit, int indentation = 4, string indentationString = " ")
    {
        return ToSourceString(compilationUnit, DefaultOptions, indentation, indentationString);
    }

    public static string ToSourceString(this CompilationUnit compilationUnit, CSharpFormattingOptions options, int indentation = 4, string indentationString = " ")
    {
        using (var sw = new StringWriter())
        {
            var formatter = new TextWriterOutputFormatter(sw)
            {
                Indentation = indentation,
                IndentationString = indentationString
            };

            var visitor = new CSharpOutputVisitor(formatter, options);
            compilationUnit.AcceptVisitor(visitor);
            return sw.ToString();
        }
    }
}

Program.csを読み込んで、クラス名をHogegramに変更したのを出力する、というだけのものです。今のところ整形した文字列化にはCSharpOutputVisitorを作って、AcceptVisitorしなきゃならないようで面倒ぃので、とりあえず拡張メソッドにしました。デフォルトの整形オプションが気に食わないのですが、フォーマット設定が超細かくて100個以上のboolをON/OFFしなきゃいけないのでとりあえず放置。

構文木の取得自体は超単純で、new CSharpParser()してParse。そしてツリーを辿るのはLINQ to Xmlと同じ感覚でDescendantsやAncestors、Childrenなどなどが用意されているので、一発で分かりますね!そしてOfTypeで自分の欲しいのにフィルタリング、と。この辺は超簡単。そして名前を変えたければ、プロパティに代入するだけ。非常に楽ちんです、素晴らしい……!

vs Roslyn

さて、じゃあRoslynでも同じことをやってみましょう。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Roslyn.Compilers.CSharp;

class Program
{
    static void Main(string[] args)
    {
        var code = File.ReadAllText(@"../../Program.cs");
        var tree = SyntaxTree.ParseCompilationUnit(code);

        var program = tree.Root.DescendentNodes().OfType<ClassDeclarationSyntax>().First();

        var newNode = new ClassNameRewriter(new Dictionary<ClassDeclarationSyntax, string> { { program, "Hogegram" } })
            .Visit(tree.Root);

        Console.WriteLine(newNode.ToString());
    }
}

public class ClassNameRewriter : SyntaxRewriter
{
    readonly IDictionary<ClassDeclarationSyntax, string> replaceNames;

    public ClassNameRewriter(IDictionary<ClassDeclarationSyntax, string> replaceNames)
    {
        this.replaceNames = replaceNames;
    }

    protected override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node)
    {
        var oldIdentifierToken = node.Identifier;

        if (replaceNames.ContainsKey(node))
        {
            var newNode = node.Update(node.Attributes,
               node.Modifiers, node.Keyword,
               Syntax.Identifier(
                   oldIdentifierToken.LeadingTrivia,
                   replaceNames[node], // ここだけ!
                   oldIdentifierToken.TrailingTrivia),
               node.TypeParameterListOpt, node.BaseListOpt,
               node.ConstraintClauses, node.OpenBraceToken,
               node.Members, node.CloseBraceToken,
               node.SemicolonTokenOpt);

            return newNode;
        }
        else
        {
            return base.VisitClassDeclaration(node);
        }
    }
}

構文木の取得はこちらも簡単です、SyntaxTree.ParseCompilationUnitだけ。ツリーの辿り方も似ていて、DescendentNodes、ChildNodes、Ancestors、と、戸惑うことなく使える感じです。OfTypeでフィルタして絞り込みも同じ。書き換えたコードの出力は、こちらはToStringだけでOK、しかもフォーマットルールは何の設定もいらずVS標準と同じ状態になっているので楽ちん。

が、しかし、こちらはクラス名の書き換えが面倒。Expression Treeと同じく基本的にイミュータブルになっているので、書き換えはプロパティに代入するだけ、とはいかず、大掛かりな仕掛けが必要です。というかこの程度のためだけにVisitorとか……。一応ReplaceNodeとかUpdateとかもあるんですが、うーん、まあ、何というか、よくわかってないので深く突っ込まれると窮します:)

まとめ

提供する機能は同じでも、使い心地とかは、両者、結構違くなるのではという印象。どちらも発展途上なのですが、ライセンス的にRoslynは今は使えないのに比べると、NRefactoryはMITライセンスで、アルファ版なのを留意しておけば使えるのではというのが、私的には大きいかなあ。割と切迫してC# Parserが必要なところなので、ここはちょっとNRefactoryを使い込んでみたいなあ、なんて思っています。いや、そこまでDeepに使い倒すというより簡単なコード解析と置換程度なので、しっかり使ってバグ報告、とかの貢献は出来なさそうですんがー。

ImplicitQueryString - 暗黙的変換を活用したC#用のクエリストリング変換ライブラリ

QueryStringかったるいですね、変換するのが。intに。boolに。それ以外に。そのままじゃどうしようもなくストレスフル。そこで、以下のように書けるライブラリを作りました。勿論NuGetでのインストールも可能です。あと、ライブラリといっても例によって.csファイル一個だけなので、導入は超お手軽。

以下はASP.NETの例ですが、NameValueCollectionを使っているものは全て対象になります。QueryStringだけじゃなくてRequest.Formなどにも使えますね。

// using Codeplex.Web;

int count;
string query;
bool? isOrdered; // support nullable

protected void Page_Load(object sender, EventArgs e)
{
    count = Request.QueryString.ParseValue("c");
    query = Request.QueryString.ParseValue("q");
    isOrdered = Request.QueryString.ParseValue("ord");
}

NameValueCollectionへの拡張メソッドとして実装しているので、using Codeplex.Webを忘れないように。ポイントは「型の明示が不要」というところです。クエリストリングを解析したい時って、通常はフィールドで既に変数自体は作っているのではないかと思います。なら、代入時に左から型を推論させてしまえばいいわけです、モダンなC#はわざわざ明示的に型なんて書かない、書きたくない。なのでこのライブラリ、ImplicitQueryStringではParseValueだけで全ての基本型(int, long, bool, string, DateTimeなど)へと型指定不要で代入可能となっています。代入先の型がNullableである場合は、キーが見つからなかったりパースに失敗した場合はnullにすることもサポートしてます。

また、よくあるクエリストリングはUrlEncodeされているのでデコードしたかったりするケースにも簡単に対応しています、UrlDecodeを渡すだけ!他にもEnumへの変換も出来ますし、キーが見つからなかったり変換不可能だったりした場合は指定したデフォルト値を返すParseValueOrDefault/ParseEnumOrDefaultも用意してあります。キーがあるかチェックするContainsKeyも。

enum Sex
{
    Unknown = 0, Male = 1, Female = 2
}

enum BloodType
{
    Unknown, A, B, AB, O
}

int age;
string name;
DateTime? requestTime;  // nullableやDateTimeもサポート
bool hasChild;
Sex sex;               // enumもいけます
BloodType bloodType;

protected void Page_Load(object sender, EventArgs e)
{
     // こんなQueryStringがあるとして
    // a=20&n=John%3dJohn+Ab&s=1&bt=AB

    // ageは左から推論してintを返します
    age = Request.QueryString.ParseValue("a"); // 20

    // UrlDecodeしたstringが欲しい時は第二引数にメソッドそのものを渡すだけ
    name = Request.QueryString.ParseValue("n", HttpUtility.UrlDecode); // John=John Ab

    // 代入先の型がnullableの場合は、もしキーが見つからなかったりパースに失敗したらnullにしてくれます
    requestTime = Request.QueryString.ParseValue("t", HttpUtility.UrlDecode); // null

    // キーが見つからなかったりパースに失敗したら指定した値を返してくれます
    hasChild = Request.QueryString.ParseValueOrDefault("cld", false); // false
    
    // Enumの変換は数字の場合でも文字列の場合でも、どちらでも変換可能です
    sex = Request.QueryString.ParseEnum<Sex>("s"); // Sex.Male
    bloodType = Request.QueryString.ParseEnumOrDefault<BloodType>("bt", BloodType.Unknown); // BloodType.AB

    // ContainsKeyはキーの有無をチェックします
    var hasFlag = qs.ContainsKey("flg"); // false
}

これで、あらゆるケースでサクッと変換することが可能なのではかと思います。ちなみに、ParseValue/ParseEnumでキーが見つからなかった場合はKeyNotFoundExceptionを返します。ParseValueで失敗したらFormatException、ParseEnumで失敗したらArgumentExceptionです。この二つが分かれているのは、int.Parseとかに渡しているだけなので、そちらの都合です。

仕組み

ImplicitQueryStringという名前がネタバレなのですが、単純に暗黙的変換をしているだけです。左から推論とか、ただのハッタリです。neue cc - dynamicとQueryString、或いは無限に不確定なオプション引数についてを書いたときに、わざわざdynamicを持ち出さなくても、暗黙的変換で十分じゃないの?そのほうが利便性も上じゃないの?と思ったのでやってみたら、やはりドンピシャでした。

暗黙的変換は、あまり使う機能じゃあないと思いますし、実際、乱用すべきでない機能であることには違いないのですが、たまに活用する分には中々刺激的で創造性に満ちています。大昔からあるけれど普段使わない機能だなんて、ロストテクノロジーっぽくて浪漫に溢れています。

さて、ただし活用するにはコード中に型を並べなければならないので、汎用的というわけではないのもそうなのですが、人力で書くのもシンドイところ。勿論人力でやる気はしないのでT4 Templateを使いました。

<#@ template language="C#" #>
<#@ output extension="cs" #>
<#@ assembly Name="System.Core.dll" #>
<#@ import namespace="System" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ import namespace="System.Reflection" #>
<#
    var methods = typeof(Convert).GetMethods(BindingFlags.Static | BindingFlags.Public);

    var converters = methods.Where(x => x.Name.StartsWith("To"))
        .Select(x => Regex.Replace(x.Name, "^To", ""))
        .Where(x => !x.StartsWith("Base64") && x != "String")
        .Distinct()
        .ToArray();
#>
using System;
using System.Collections.Generic;
using System.Collections.Specialized;

namespace Codeplex.Web
{
    public static class NameValueCollectionExtensions
    {
        public static ConvertableString ParseValue(this NameValueCollection source, string key)
        {
            return ParseValue(source, key, null);
        }

        public static ConvertableString ParseValue(this NameValueCollection source, string key, Func<string, string> converter)
        {
            var values = source.GetValues(key);
            if (values == null) return new ConvertableString(null);

            var value = values[0];
            return new ConvertableString(converter == null ? value : converter(value));
        }
        
        // 中略
    }

    public struct ConvertableString
    {
        public readonly string Value;

        public ConvertableString(string value)
        {
            this.Value = value;
        }

<# foreach (var converter in converters) { #>
        public static implicit operator <#= converter #>(ConvertableString self)
        {
            if (self.Value == null) throw new KeyNotFoundException();
            return <#= converter #>.Parse(self.Value);
        }

        public static implicit operator <#= converter #>?(ConvertableString self)
        {
            <#= converter #> value;
            return (self.Value != null && <#= converter #>.TryParse(self.Value, out value))
                ? new Nullable<<#= converter #>>(value)
                : null;
        }

<# } #>
        public static implicit operator String(ConvertableString self)
        {
            return self.Value;
        }

        public override string ToString()
        {
            return Value;
        }
    }
}

以上のコードから、T4生成部分を取り出すと

public static implicit operator Boolean(ConvertableString self)
{
    if (self.Value == null) throw new KeyNotFoundException();
    return Boolean.Parse(self.Value);
}

public static implicit operator Boolean?(ConvertableString self)
{
    Boolean value;
    return (self.Value != null && Boolean.TryParse(self.Value, out value))
        ? new Nullable<Boolean>(value)
        : null;
}

public static implicit operator Char(ConvertableString self)
{
    if (self.Value == null) throw new KeyNotFoundException();
    return Char.Parse(self.Value);
}

public static implicit operator Char?(ConvertableString self)
{
    Char value;
    return (self.Value != null && Char.TryParse(self.Value, out value))
        ? new Nullable<Char>(value)
        : null;
}

// 以下SByte, Byte, Int16, Int32, Int64, ..., DateTimeと繰り返し

といったコードがジェネレートされます。清々しいまでのゴリ押しっぷり。一周回ってむしろエレガント。ConvertableStringのほうでKeyNotFoundExceptionを出すのがイケてないのですが、まあそこはShoganaiかなー、ということで。さて、それはそれとして、やっぱVisual Studioに組み込みで、こういった自動生成のテンプレートエンジンが用意されているというのは非常に嬉しいところです。

まとめ

特にASP.NETを使っている方々にどうぞ。ASP.NET MVCの人はしらにゃい。なのでいつもの私だったら.NET 4.0専用にするところが、今回は.NET 3.5でも大丈夫!Visual Studio 2008でも動作確認取りました!

それにしても実際ASP.NETを使いこなしてる人はクエリストリングの取得はどんな風にやっていたものなのでしょうかー。何かしらの手法がないとシンドすぎやしませんか?そう思って検索してみたんですが、どうにも見つからず。さすがに ParseValueAsInt() とかって拡張メソッドぐらい作ってやり繰りは私も大昔していたし、それでintに対応しておけばほぼほぼOKではありますが(拡張メソッドがなかった時代はさすがに想像したくない!)。そう考えると、ちょっとこれはやりすぎ感も若干。でも、一度できあがればずっと使えますしね。というわけでImplicitQueryString、是非是非使ってみてください。本当に楽になれると思います。

dynamicとQueryString、或いは無限に不確定なオプション引数について

どうもこんばんわ、30時間も寝てないわー。まあ、お陰で真昼間の就業中にうつらうつらしてましたが!ちなみにGRAVITY DAZEにはまって延々とプレイしてたから寝てないだけです。PS陣営にくだるなんて!さて、それはそれとしてBlog更新頻度の低下っぷりはお察し下さい。そんなこんなで最近の仕事(仕事ですって!仕事してるんですって!?)はASP.NETなんですが、色々アレですね。ふふふ。ソウルジェムは真っ黒と真っ白を行ったり来たりしてて楽しいです。まあ、ようするに楽しいです。楽しいのはいいとしてBlogが停滞するのはいくないので、電車でGRAVITY DAZEやりながらQueryStringうぜえええええ、とか悶々としたので帰り道に殺害する算段を整えていました。

QueryStringって、qとかnumとかiとかsとか、それそのものだけじゃ何を指してるかイミフなので、C#的にはちゃんと名前をつけてやりたいよね。だから、ただシンプルに触れるというだけじゃダメで。あと、コンバートもしたいよね。数字はintにしたいしboolは当然boolなわけで。さて、そんな時に私たちが持つツールと言ったら、dynamicです。C#とスキーマレスな世界を繋ぐ素敵な道具。今日も活躍してもらうことにしましょう。たまにしか出番ないですからね。

private int id;
private int? count;
private string keyword;
private bool isOrdered;

protected void Page_Load(object sender, EventArgs e)
{
    // id=10&c=100&q=hogehoge&od=true というクエリストリングが来る場合

    // dynamicに変換!
    var query = this.Request.QueryString.AsDynamic();

    id = query.id; // キャストは不要、書くの楽ですね!(キーが存在しないと例外)
    count = query.c; // 型がnullableならば、存在しなければnull
    keyword = query.q; // stringの場合も存在しない場合はnull
    isOrdered = query.od(false); // メソッドのように値を渡すと、キーが存在しない場合のデフォルト値になる
}

ASP.NETの例ですが、どうでしょう、まぁまぁ良い感じじゃあなくて?ちなみに最初はnull合体演算子(??)を使いたかったのですが、DynamicObjectでラップした時点で、そもそも存在がnullじゃないということで、何をどうやってもうまく活用できなくて泣いた。しょうがないのでメソッド形式でデフォルト値を渡すことでそれっぽいような雰囲気に誤魔化すことにしました、とほほほ。

dynamicとオプション引数

クエリストリングを生成することも出来ます。

// 戻り値はdynamicで空のDynamicQueryStringを生成
var query = DynamicQueryString.Create();

// オプション引数の形式で書くと……?
query(id: 100, c: 100, q: "hogehoge", od: true);

Console.WriteLine(query.ToString()); // id=10&c=100&q=hogehoge&od=true

といった感じで、面白いのがオプション引数の使い方。匿名型渡しのように、スマートにKey:Valueを渡すことを実現しています。dynamicなので引数名は完全自由。個数も完全自由。非常にC#らしくなくC#らしいところが最高にCOOL。匿名型とどちらがいいの?というと何とも言えないところですが、面白いには違いないし、面白いは正義。C#も工夫次第でまだまだ色々なやり方が模索できるんですよー、ってところです。

実装など

眠いのでコードの解説はしません(ぉ。DynamicObjectの実装方法はneue cc - C# DynamicObjectの基本と細かい部分についてでどーぞ。overrideしていくだけなので、別に難しくもなんともないので是非是非やってみてください。ちなみに無限のオプション引数を実現している箇所は binder.CallInfo.ArgumentNames.Zip(args, (key, value) => new { key, value }) です。

public static class NameValueCollectionExtensions
{
    public static dynamic AsDynamic(this NameValueCollection queryString)
    {
        return new DynamicQueryString(queryString);
    }
}

public class DynamicQueryString : DynamicObject
{
    NameValueCollection source;

    public DynamicQueryString()
    {
        this.source = new NameValueCollection();
    }

    public DynamicQueryString(NameValueCollection queryString)
    {
        this.source = queryString;
    }

    public static dynamic Create()
    {
        return new DynamicQueryString();
    }

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        var value = source[binder.Name];
        result = new StringMember((value == null) ? value : value.Split(',').FirstOrDefault());
        return true;
    }

    public override bool TryInvoke(InvokeBinder binder, object[] args, out object result)
    {
        foreach (var item in binder.CallInfo.ArgumentNames.Zip(args, (key, value) => new { key, value }))
        {
            source.Add(item.key, item.value.ToString());
        }

        result = this.ToString();
        return true;
    }

    public override bool TryConvert(ConvertBinder binder, out object result)
    {
        if (binder.Type != typeof(string))
        {
            result = null;
            return false;
        }
        else
        {
            result = this.ToString();
            return true;
        }
    }

    public override string ToString()
    {
        return string.Join("&", source.Cast<string>().Select(key => key + "=" + source[key]));
    }

    public override IEnumerable<string> GetDynamicMemberNames()
    {
        return source.Cast<string>();
    }

    class StringMember : DynamicObject
    {
        readonly string value;

        public StringMember(string value)
        {
            this.value = value;
        }

        public override bool TryInvoke(InvokeBinder binder, object[] args, out object result)
        {
            var defaultValue = args.First();

            try
            {
                result = (value == null)
                    ? defaultValue
                    : Convert.ChangeType(value, defaultValue.GetType());
            }
            catch (FormatException) // 真面目にやるならType.GetTypeCodeでTypeを分けて、例外キャッチじゃなくてTryParseのほうがいいかな?
            {
                result = defaultValue;
            }

            return true;
        }

        public override bool TryConvert(ConvertBinder binder, out object result)
        {
            try
            {
                var type = (binder.Type.IsGenericType && binder.Type.GetGenericTypeDefinition() == typeof(Nullable<>))
                    ? binder.Type.GetGenericArguments().First()
                    : binder.Type;

                result = (value == null)
                    ? null
                    : Convert.ChangeType(value, binder.Type);
            }
            catch (FormatException)
            {
                result = null;
            }

            return true;
        }

        public override string ToString()
        {
            return value ?? "";
        }
    }
}

コンバートに対応させるために、要素一つだけのDynamicObjectを中で用意しちゃってます。そこがポイント、といえばポイント、かしら。GetDynamicMemberNamesはデバッガの「動的ビュー」に表示されるので、Visual Studioに優しいコードを書くなら必須ですね。

まとめ

dynamicはC#と外の世界を繋ぐためのもの。今日もまた一つ繋いでしまった。それはともかくとして、一番最初、DynamicJsonを実装した頃にも言ったのですが、dynamicはDSL的な側面もあって、普通に楽しめますので、ちょっと頭をひねって活用してみると、また一つ、素敵な世界が待っています。

今回はコード書くのに2時間、この記事を書くのに1時間、でしたー。

Prev | | Next

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(.NET)
April 2011
|
July 2026

X:@neuecc GitHub:neuecc

Archive