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時間、でしたー。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

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

Twitter:@neuecc GitHub:neuecc

Archive