WSH用にCOMのIntelliSenseを自動生成する

本題、の前にJavaScript用のIntelliSenseの作成方法について。以前、linq.jsにIntelliSense用ファイルを追加した時に利用法を書きましたが、今回はvsdocの作成方法を、ざっと書きます。まずVS2008 SP1とパッチを適用する。hoge.jsに対しhoge-vsdoc.jsを用意するとhoge.jsのかわりにhoge-vsdoc.jsがIntelliSense用に読み込まれる。IntelliSenseでVSが見ているのはメソッド名と引数だけなので、コードの中身はなくてもいい。例えば本来は存在するがprivateメソッドのつもりのものは、vsdoc.js側に書かなければIntelliSenseには表示されない。C#と同様に関数にはXMLドキュメントコメントを記述することが出来る。記述箇所はC#とは異なり開始の波括弧の直下。ドキュメントコメントのタグで現在機能しているものはsummary, params, returnsのみ。特に重要なのはreturns typeで、これにprototypeが存在する関数(ようするにクラスですなー)を指定することで戻り値の型をVSに認識させ、IntelliSenseを働かせることが出来る。ちなみに、ただのオブジェクトだと認識してくれない。

function Hoge(aaa, bbb)
{
    /// <summary>hogehogehogehoge</summary>
    /// <param name="aaa" type="String">a!a!a!a!a!</param>
    /// <param name="bbb" type="Optional:Boolean">b?b?b?b?b?</param>
    /// <returns type="Number"></returns>
}

基本はこんな感じ。引数の省略や、型が目で見て分かるので大分書きやすくなります。 で、いつぞやかに作成中とか言っていた通りにlinq.jsをWSH対応させようとしているわけですが、IntelliSense書きの量が思ったよりも膨大なわけです。実は最初は普通に手書きしてました。一応勉強も兼ねて書写みたいなノリで。ですが、あまりにも手間かかりすぎるので自動生成することにしました。こんなの人力でやるもんじゃないですよ。微調整はどちらにせよ必要なのですか、大枠だけでも書きだされていると物凄く楽になる。

static void Main(string[] args)
{
    // add reference and replace dll path
    var assembly = Assembly.LoadFrom(@"Interop.IWshRuntimeLibrary.dll");
    var types = assembly.GetTypes();

    var enums = types
        .Where(t => t.IsEnum)
        .OrderBy(t => t.Name)
        .Select(t => string.Format("{0}: \r\n{{\r\n{1}\r\n}}",
            t.Name,
            Enum.GetNames(t).Select(s => string.Format("\t{0}: {1}", s, (int)Enum.Parse(t, s))).Join(",\r\n")));

    var classes = types
        .Where(t => t.IsClass)
        .Select(type =>
        {
            var bindingFlag = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly;

            var properties = type.GetProperties(bindingFlag)
                .OrderBy(pi => pi.Name)
                // .Select(pi => string.Format("{0}: {1}", pi.Name, pi.PropertyType.Name
                .Select(pi => string.Format("{0}: null", pi.Name));

            var methods = type.GetMethods(bindingFlag)
                .Where(mi => !mi.IsSpecialName && mi.Name != "GetEnumerator")
                .Select(mi => new { mi.Name, Parameters = mi.GetParameters(), ReturnType = mi.ReturnType.Name })
                .OrderBy(t => t.Name)
                .Select(t => string.Format("{0}: function({1})\r\n{{\r\n{2}\t/// <returns type=\"{3}\"></returns>\r\n}}",
                    t.Name,
                    t.Parameters.Select(pi => pi.Name).Join(", "),
                    t.Parameters.Select(pi => string.Format("\t/// <param name=\"{0}\" type=\"{1}{2}\"></param>\r\n",
                            pi.Name,
                            (pi.IsOptional) ? "Optional:" : "",
                            pi.ParameterType.Name.Replace("Void", "void").Replace("Int32", "Number")))
                        .Join(""),
                    t.ReturnType.Replace("Void", "void").Replace("Int32", "Number")));

            var result = properties.Concat(methods).Join(",\r\n");
            return string.Format("{0} = function() {{ }}\r\n{0}.prototype =\r\n{{\r\n{1}\r\n}}",
                Regex.Replace(type.Name, "Class$", ""),
                result.Split(new string[] { "\r\n" }, StringSplitOptions.None).Select(s => "\t" + s).Join("\r\n"));
        });

    var name = assembly.GetName().Name.Split('.').Last();
    File.WriteAllText(name + "_enum.js", string.Format("{0}Enum =\r\n{{\r\n{1}\r\n}}", name,
        enums.Join(",\r\n").Split(new string[] { "\r\n" }, StringSplitOptions.None).Select(s => "\t" + s).Join("\r\n")), Encoding.UTF8);
    File.WriteAllText(name + "_class.js", classes.Join("\r\n\r\n"), Encoding.UTF8);
}

static string Join<T>(this IEnumerable<T> source, string separator)
{
    var index = 0;
    return source.Aggregate(new StringBuilder(),
            (sb, o) => (index++ == 0) ? sb.Append(o) : sb.AppendFormat("{0}{1}", separator, o))
        .ToString();
}

参照設定で対象のCOMを読み込んで、一旦ビルド。生成されてるInterop.hoge.dllを読み込んで解析、という流れをとってみました。もっとマシなやり方があれば教えてください。コードは、んーと、string.Formatがクドくて見難いですね! 最初はVSのフォーマッタに後で手動でかければいいや、と思ってたのを、出力状態でちゃんとフォーマットされてるようにと継ぎ接ぎしてたら酷いことに。一回文字列にしたものを改行でバラして再生成とか笑えない。こういうの、HTML/XMLが対象なら何も考えなくてもLinq to Xmlで綺麗に書けるのになあ……。 それ以外はまぁ、こんなものかしらんって感じでしょうか? リフレクションが絡むとLinq大活躍。Selectがネストしまくるのでクエリ構文の出番はない。リフレクションは掘り進める都合上、ネストが深くなるのでLinqがなかったら、と思うとゾッとします。最近はforの二重ループですらウエエ、とか思ってしまうので。

IWshRuntimeLibraryEnum =
{
    IOMode:
    {
        ForReading: 1,
        ForWriting: 2,
        ForAppending: 8
    }
}

Folder = function() { }
Folder.prototype =
{
    Name: null,
    ParentFolder: null,
    Drive: null,
    CreateTextFile: function(FileName, Overwrite, Unicode)
    {
        /// <param name="FileName" type="String"></param>
        /// <param name="Overwrite" type="Optional:Boolean"></param>
        /// <param name="Unicode" type="Optional:Boolean"></param>
        /// <returns type="TextStream"></returns>
    }
}

一部抜粋ですが、こんなようなデータが出力されます。プロパティが全部nullなのは、ここに未指定のものが記述されているとIntelliSenseがエラー起こしてしまうから。例えばDrive: new Drive()で戻り値の型指定が可能といえば可能なのですが、出現位置の上下が問題になってくるので結構面倒くさい。ようはFolderよりも上にDriveがあれば関数が存在するので大丈夫だけど、下にあれば存在しないのでエラー。当たり前といえば当たり前なのですが、ダミーでのIntelliSense作りとしては面倒な問題でして。 この辺は素直に諦めて全部nullで型連鎖を止めてしまうのがお気楽といえばお気楽。あと、Dateはnew Date()にしてもDate.prototypeにしても型指定出来なかったりする問題もある。これは今のところ対処不能。

そんなわけで、linq.js + WSHはわりと順調に作成中なので期待しないでも待っててください。IntelliSenseというだけじゃなく、JScriptではバイト配列が扱いにくいので、扱いやすく出来るような補助メソッドを用意したりとか色々やってたらいつになっても終わらないー。例としてWSHでMD5を作る、とか。.NET Frameworkを通しているのでSHA1でも何でもいけます。これでWSHでwsse認証通してAtomAPIで投稿、とか出来ますね。まあ、もうC#でいいぢゃん、って気がかなりしなくもないですけど、うーん。C#4.0からdynamicが追加されるからCOM連携も楽になるしねえ。ただサイドバーガジェットに使うとか、JavaScriptの用途はまだまだあるもん!(これもSilverlightでやれば?って話がなくもないので、うーん)

簡易文字列置換

static void Main(string[] args)
{
    // {}で括った部分を(ラムダ式/匿名型)を使って置換する
    var input = "食べ物は{tabemono}で飲み物は{nomimono}";
    var r1 = input.Replace(new { tabemono = "たこ焼き", nomimono = "コーラ" });
    var r2 = input.Replace(tabemono => "たこ焼き", nomimono => "コーラ");
}

前も書いたというかJavaScriptでやりましたが、それのC#移植。簡易テンプレート的な文字列置換。ラムダ式版と匿名型版の二つでやってみました。ラムダ式だと、見た目が少し短くて何となく格好良いのですが、変数名の入力時にIntelliSenseが動いてしまうので結構鬱陶しかったり。匿名型のほうはObjectを受け取る関数なので、危なっかしいのが嫌ですね……。

static class Extensions
{
    // 正規表現の|でキーを連結して辞書から置換
    private static string Replace(string input, Dictionary<string, string> dict)
    {
        var pattern = string.Format("{{({0})}}", dict.Select(kvp => Regex.Escape(kvp.Key)).ToJoinedString("|"));
        return Regex.Replace(input, pattern, m => dict[m.Groups[1].Value]);
    }

    /// <param name="anonymousType">{pattern = "replacement"}</param>
    public static string Replace(this string input, Object anonymousType)
    {
        var dict = anonymousType.GetType()
            .GetProperties()
            .ToDictionary(pi => pi.Name, pi => pi.GetValue(anonymousType, null).ToString());
        return Replace(input, dict);
    }

    /// <param name="exprs">pattern => "replacement"</param>
    public static string Replace(this string input, params Expression<Func<Object, string>>[] exprs)
    {
        var dict = exprs.ToDictionary(e => e.Parameters[0].Name, e => e.Compile().Invoke(null));
        return Replace(input, dict);
    }

    // 文字列連結補助メソッド(これないとシンドイので)
    public static string ToJoinedString<T>(this IEnumerable<T> source, string separator)
    {
        var index = 0;
        return source.Aggregate(new StringBuilder(),
                (sb, o) => (index++ == 0) ? sb.Append(o) : sb.AppendFormat("{0}{1}", separator, o))
            .ToString();
    }
}

速度は、匿名型版のほうが圧倒的に速いです。100000回の繰り返しが匿名型だと2秒なのに対してラムダ式版は30秒かかった。Compile().Invokeがアレなんですかねえ。ちなみに、string.Formatでふつーにやった場合は0.03秒でした。まー、でも、メール雛型の置換に、とかちょっとしたことにそこそこ便利に使えるかなー、とは思いつつもっと効率考えてちゃんと組んだ方がいい気もする。

ラムダ式から取り出すのはC# 3.0 における疑似 Map 生成リテラル - NyaRuRuの日記から、匿名型はprototype.jsとかPHPのstrtrとか色々。あとややニッチな Anonymous Types の使い方をまとめてみる (C# 3.0) - NyaRuRuの日記です。

ver 0.0.0.3

拡張子が大文字だとアップロード出来なかったので直しました。XboxInfoTwitの時も同じのやってたのにまたかよって感じです。拡張子判定部分は、ちゃんとIgnoreCaseにしたしあれえ?と思ってたんですがContentType作るところで漏れがあって、ウッカリ。てへ。

まあ、そんなこんなでちゃんと一眼レフも買いました。わざわざこのためだけに!neuecc's fotolife 。それで、しかし撮るものが悲しいほど無いんですよね、やっぱり。とはいえ、自分で撮って自分で上げてかないと、何をどうすれば良くなるのか分からないので、室内写真で栄える何かを探し中です。現在は多肉植物でも育てようかな、と思ってるんですがどうでしょうかねえ。

ちなみに現在までのDL数は余裕で一桁。べ、別に悲しくないもん! そういえばこの半自動ってぶっちゃけ機能的にいらなくね?むしろフォルダ監視で自動化したほうがよくね?とも思ってきたので、まあ、そのうち。そのうち。

ネストした三項演算子の書き方

javaプログラムの組み方がわかりませんorz 無理な課題を学校で出されました(´・_・... - Yahoo!知恵袋

「小学生向けの四則演算の計算練習のプログラムを考える」……? 回答のJavaコードが長くてよくわからにゃい。ぽりもふぃずむって何?

これでおk。最近、三項演算子の:は後置が普通だけど、前置にすると[F#のパターンマッチ](http://msdn.microsoft.com/en-us/library/dd233242(VS.100).aspx)みたいで格好良くね?とか思うので、開き直って前置にしたいです。これはコーディング規約に載せるべきですね(笑) あと三項演算子の、うにゃ、条件演算子の条件は必ずカッコで括る派。

LINQ to XMLのNamespaceと書き出し時のEncodingについて

ver 0.0.0.2に更新。アップロードするフォルダが指定出来るようになりました。アップロードツール名(FotolifeUploader)が利用されるようになりました。フォルダ指定は再設定が必要なので、前のバージョンを使っている方はsettings.xmlを削除して、再度設定し直してください。あとは間抜けだったUploadToFotolifeメソッドを手直ししたり。

私自身が、そもそもフォトライフのヘビーユーザーではないので、細かいところに気が利いてないかもですね……。そういうのは、よくない。というわけで、当分はFotolifeをちゃんと利用しようキャンペーンを張ることにします。なので、デジタル一眼を買う。と言いたいのだけど、何か微妙なのよねん。いや、そもそも引き籠って家から出ないので撮影するものがないので。かといって熱帯魚や食虫植物とかフィギュアとか、撮影に適した趣味があるわけでもなく。困った困った。まあ、考えます。食虫植物を育てる方向で(?) 部屋が殺風景なので何かは入れたいのだけど、手間はかけたくない。ううむ、難しい。

LINQ to XML

アップロードにはAtomAPIを利用しているので、XMLです。つまりLINQ to XMLの出番です。出力結果がこんな感じなので、そこから逆に考えると……

<entry xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns="http://purl.org/atom/n
s#">
  <title>タイトル</title>
  <content mode="base64" type="image/jpeg">画像BASE64</content>
  <generator>FotolifeUploader</generator>
  <dc:subject>フォルダ名</dc:subject>
</entry>

XElementは、Namespaceの利用が少しややこしいんですよね。最初引っかかりました。「XNamespace.Xmlns + "接頭辞"」で登録できます。

XNamespace ns = "http://purl.org/atom/ns#";
XNamespace dc = "http://purl.org/dc/elements/1.1/";
var xml =
    new XDocument(new XDeclaration("1.0", "UTF-8", null),
    new XElement(ns + "entry", new XAttribute(XNamespace.Xmlns + "dc", dc),
        new XElement(ns + "title", "タイトル"),
        new XElement(ns + "content", new XAttribute("mode", "base64"), new XAttribute("type", "image/jpeg"), "画像BASE64"),
        new XElement(ns + "generator", "FotolifeUploader"),
        new XElement(dc + "subject", "フォルダ名")
    ));
Console.WriteLine(xml); // 出力確認、DeclarationはToStringでは出力されない

少し独特ですが、ほとんど1:1で対応させられるので慣れるとサクサク書けます。非常に快適。個人的にはXMLリテラル的なものよりも好き。Linqがあってほんと良かった……。で、Declarationを出力したい場合の話に続く。(hatena (diary ’Nobuhisa))にもあるように、ToStringでは出力されないのでSaveを使う、と……

var sb = new StringBuilder();
var sw = new StringWriter(sb);
xml.Save(sw);
Console.WriteLine(sb); // UTF-16になる

これでencodingがUTF-16になるのは、Saveメソッド呼ぶとDeclarationは作りなおしているから。.Save("string fileName")ではXDeclarationのエンコーディングを見て、それで保存するけれど、それ以外の場合はXDeclaration無視で再構築される。XDocumentというかXmlWriterのほうの話でしょうか。実際にファイル出力してみると分かる。

var fs = new FileStream(@"C:\text.xml", FileMode.Create);
var sw = new StreamWriter(fs, Encoding.GetEncoding("x-mac-turkish"));
xml.Save(sw);

出力先のエンコードに合わせてくれる、のを便利と見るか、むしろ気が利かない、Writer部分もC#3.0に合わせて作りなおせ、なのかは不明。まあ、嘘エンコード宣言は許しませんよってことですかね。じゃあどうするか、って言ったら

// これで別に何も問題ないと思います、文字列として吐くんだからToStringでいいと思ふ
var xmlString = string.Format("{0}{1}{2}",
    xml.Declaration, Environment.NewLine, xml);
Console.WriteLine(xmlString);

// ToStringがどうしても嫌ならMemoryStream経由で、とか?
string result;
var encoding = Encoding.UTF8;
using (var ms = new MemoryStream())
using (var sw = new StreamWriter(ms, encoding))
{
    xml.Save(sw);
    result = encoding.GetString(ms.ToArray());
}
Console.WriteLine(result); // 望み通りのUTF-8で出力されてます

結論は、普通にToStringでいいんじゃないかな、と。ToStringメソッドだけではXmlWriterSettingsで言うところのOmitXmlDeclarationを設定出来ないから、デフォルトでは付加しないようにしてる。削除は無理だけど、追加なら簡単だから。XmlDeclarationを付加したい時は別途、自分でくっつければいい。というだけのお話かなー? ToStringで一発で終わらせられないからStringBuilder使って組み立てるってのは、何でそうなるの?と、とても思った。ついでにもう一つ。

// こんなXElementがあるとして
var xElement = XElement.Parse("<hoge>3</hoge>");
// intとして値を取り出す時は
var num1 = int.Parse(xElement.Value); // これダメ。
var num2 = (int)xElement; // こう書こう。

です。LINQ to XMLは既存のものを上手く使ってシンプルに書けるように作られてる。気がする。このキャストもそうだし、ToStringもそう。Parseは頻繁に行うから汚くなるよね→キャストでよくね? 文字列化はよくやるけどSaveもXmlWriterSettingsも面倒くさいよね→ToStringでよくね? といった感じ。関数型構築もそうだけど、今までのもの(XmlDocument)を踏まえて、よく練り直されているなー、と思います。

半自動はてなフォトライフアップローダー ver 0.0.0.1

はてなフォトライフに画像をワンクリックでアップロードするプログラムです。ワンクリックの手間があるので、半自動。主な機能は、実行すると設定したフォルダの最新の更新画像一枚をアップロード。利用例としてデジカメ接続時やメモリーカード内の画像フォルダを指定することを想定しています。写真撮る→PCに繋げる→プログラムを実行する→アップロード完了。みたいな流れです。Twitterに載せるための写真とか最新一枚で十分でしょう? Blogに載せる場合でも、一枚で済む場合って結構多いよね。そんな感じに、サクサクッと写真と付き合えたらいいな、と。

設定

まさかのCUI設定画面(笑) 初回起動時にこの画面になります。設定し直したい時は、生成されるsettings.xmlを削除してください。レトロでアナログで半自動を貫く感じがいいかなー、と思ったんですが、どうでしょう。

最新画像一枚のアップロード

設定終了後にexeファイルを実行すると、設定時に指定したフォルダの中の、拡張子が「jpg/jpeg/gif/png/bmp」で更新日時が最も新しいもの一枚をアップロードします。設定によってはアップロード後にブラウザでフォトライフのURLが開きます。なので、そこからそのままTwitterにURLをポストするなりBlog書くなりがシームレスに行えるわけです。キリッ。ちなみにリサイズ等はこちら側では一切しません、そのまま丸投げ。リサイズ処理もはてな任せ。

任意画像複数枚のアップロード

フォルダ/画像をまとめてexeファイル(本体じゃなくてショートカットでもOKです)にドラッグアンドドロップすると、そのファイルをアップロードします。フォルダはサブディレクトリを含めて全てのファイルをアップロードします。拡張子が「jpg/jpeg/gif/png/bmp」以外のものはちゃんと無視しますので、多少適当でも大丈夫。また、いわゆる「送る」にショートカットを登録することで、このドラッグアンドドロップと同様の結果になります。Vistaの場合はエクスプローラー上で「%AppData%\Microsoft\Windows\SendTo」と入力するとSendToのフォルダに飛べますので、ここにショートカットを登録してみてください。

今回コンソールアプリにしたのは、実行にかかる手間を最小にしたかった、というのがあります。普通のアップロードアプリだと、「アプリを起動→画像フォルダを開く→ドラッグアンドドロップで画像を乗っける→アップロードボタンを押す→アプリを閉じる→Fotolifeにアップロードされた画像を確認しにいく」 これじゃあ工程多すぎであまりにも面倒くさい。というわけで、最新画像一枚ならば、アプリ起動だけで完了。複数毎でも画像フォルダ→ドラッグアンドドロップだけで完了という、考え得る限りの最短を目指しています。

ソースコード

ソースコードも同梱してあります。csファイル一つだけの、200行ちょいのちっぽいコンソールアプリです。好きに改変とか突っ込みとかディスとかしてください。しいていえば、Linqだらけです。個人的には

.SelectMany(s => (Directory.Exists(s))
  ? Directory.GetFiles(s, "*", SearchOption.AllDirectories)
  : Enumerable.Repeat(s, 1))
.Select(s => new FileInfo(s))
.Where(fi => fi.Exists && FotolifeExtensionPattern.IsMatch(fi.Extension))

この部分が気に入ってます。ドラッグアンドドロップで来る文字列配列からファイル抜き出しの部分。SelectManyでディレクトリをファイル名配列に、ディレクトリじゃない場合はEnumerable.Repeatで繰り返し回数が1回のファイル名配列にする。あとはまあ普通に、SelectしてWhereしてToArray。Linqがあって良かったーと本当に思う。逆にAtomPub APIでアップロードする部分はLinqでやる意味がなかったというか、当初予定と変わってあれ追加これ追加で肥大化してしまった結果でして……。

LLの人はこの手のちょっとしたスクリプトをほいほい公開しているわけだから、C#もコンソールアプリぐらいほいほい公開出来ないといかんのぅ、と思いつつもページ用意して云々かんぬんは面倒くさくて、そうホイホイってわけにもいかない感じ。もちっと軽くやれる環境作らないとね……。まあ、でも、このちょっとした重苦しさも悪くはないんだ。だってほら、Rubyでスクリプトがホイッって転がってても、普通の人は動かせもしないわけですよ。だから、少し面倒くさいなー、と思いつつ設定画面つけてexeの形式にして、それだけで幸せになれないかな、どうだろう。

私はプログラム書き始めたのがほんとつい最近で、利用するだけ人間の歴が何年も何年もあるので、その辺は極力優しくやりたいなあ、と思ってます。

可変のLookup

一対多の Dictionary が欲しい - present

今だと、やっぱLookupがあるので、ILookupの実装という形で作った方が統一感取れていいのかな……? 通常のLookupはToLookupでしか作成できず不変なので、追加したり削除したり出来るILookupの実装を作りました。MultiDictionaryとかMultiMapとか言われているもの、ですが名前をimmutableの反対なのでMutableLookupとしてみました。かなり、微妙。名前ってむつかしい。んで、中身はDictionary<TKey, List<TValue>>のゆるふわラッピング。コンストラクタでIEqualityComparerを突っ込めるので、大文字小文字無視とかも出来ます。

// こんな風に使います
var mutableLookup = new MutableLookup<string, string>();
mutableLookup.Add("食べ物", "たこやき");
mutableLookup.AddRange("食べ物", new[] { "いかやき", "さかなやき" });
mutableLookup.Add("飲み物", "ぽかり");

IEnumerable<string> tabemono = mutableLookup["食べ物"]; // インデクサでアクセス
foreach (var item in mutableLookup) // 列挙するとIGroupingが出てくる
{
    var key = item.Key; // キー(食べ物,飲み物)
    var array = item.ToArray(); // 配列
}
public class MutableLookup<TKey, TValue> : ILookup<TKey, TValue>
{
    private readonly Dictionary<TKey, List<TValue>> dictionary;

    // Constructor

    public MutableLookup()
    {
        dictionary = new Dictionary<TKey, List<TValue>>();
    }

    public MutableLookup(IEqualityComparer<TKey> keyComparer)
    {
        dictionary = new Dictionary<TKey, List<TValue>>(keyComparer);
    }

    // Property

    public IEnumerable<TKey> Keys
    {
        get { return dictionary.Select(kvp => kvp.Key); }
    }

    public IEnumerable<TValue> Values
    {
        get { return dictionary.SelectMany(kvp => kvp.Value); }
    }

    // Methods

    public ILookup<TKey, TValue> AsReadOnly()
    {
        return dictionary.SelectMany(kvp => kvp.Value, (kvp, Value) => new { kvp.Key, Value })
            .ToLookup(t => t.Key, t => t.Value);
    }

    public void Add(TKey key, TValue value)
    {
        if (dictionary.ContainsKey(key)) dictionary[key].Add(value);
        else dictionary.Add(key, new List<TValue> { value });
    }

    public void AddRange(TKey key, IEnumerable<TValue> values)
    {
        if (dictionary.ContainsKey(key)) dictionary[key].AddRange(values);
        else dictionary.Add(key, new List<TValue>(values));
    }

    public void RemoveKey(TKey key)
    {
        dictionary.Remove(key);
    }

    public void RemoveValue(TKey key, TValue value)
    {
        if (!dictionary.ContainsKey(key)) return;

        var list = dictionary[key];
        list.Remove(value);
        if (!list.Any()) dictionary.Remove(key);

    }

    public void RemoveWhere(TKey key, Func<TValue, bool> predicate)
    {
        if (!dictionary.ContainsKey(key)) return;

        var list = dictionary[key];
        list.RemoveAll(new Predicate<TValue>(predicate));
        if (!list.Any()) dictionary.Remove(key);
    }

    public void Clear()
    {
        dictionary.Clear();
    }

    public bool Contains(TKey key, TValue value)
    {
        if (dictionary.ContainsKey(key))
        {
            return dictionary[key].Contains(value);
        }
        return false;
    }

    #region ILookup<TKey,TValue>

    public bool Contains(TKey key)
    {
        return dictionary.ContainsKey(key);
    }

    public int Count
    {
        get { return dictionary.Count; }
    }

    public IEnumerable<TValue> this[TKey key]
    {
        get
        {
            return (dictionary.ContainsKey(key))
                ? dictionary[key].AsEnumerable()
                : Enumerable.Empty<TValue>();
        }
    }

    #endregion

    #region IEnumerable<IGrouping<TKey,TValue>>

    public IEnumerator<IGrouping<TKey, TValue>> GetEnumerator()
    {
        return dictionary
            .Select(kvp => new Grouping(kvp.Key, kvp.Value))
            .Cast<IGrouping<TKey, TValue>>()
            .GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }

    #endregion

    // Nested Types

    private class Grouping : IGrouping<TKey, TValue>
    {
        private TKey key;
        private List<TValue> list;

        public Grouping(TKey key, List<TValue> list)
        {
            this.key = key;
            this.list = list;
        }

        #region IGrouping<TKey,TValue>

        public TKey Key
        {
            get { return key; }
        }

        #endregion

        #region IEnumerable<TValue>

        public IEnumerator<TValue> GetEnumerator()
        {
            return list.GetEnumerator();
        }

        #endregion

        #region IEnumerable

        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        #endregion
    }
}

利用はご自由にどうぞ。パブリックドメイン、でいいのかな? あー、AsReadOnlyは新しいのに作り替えちゃってるのでAsじゃないですねえ。まあ、放置。あと、new Predicate()が果てしなくダサい。これ何とかならないのかな。delegateはFuncとAction以外は消滅しちゃえばいいのに。ジェネリクス以前のコレクションクラスと同じで、3.0以前の負の遺産だと何となく思ってる。

LinqとIEqualityComparerへの疑問

Distinctの引数はラムダ式でのselectorを受け付けてくれない。IEqualityComparerだけなので、抽出のためにわざわざ外部にIEqualityComparerを実装したクラスを作る必要がある。それって、面倒くさいし分かり辛いし、何でここだけ古くさいような仕様なのだろう。C#3.0っぽくない。しょうがないので、単純ですけど汎用的に使えるようなものを作ってみた。

// IEqualityComparer<T>の実装が面倒なのでセレクタ的なものはこれで賄う
public class CompareSelector<T, TKey> : IEqualityComparer<T>
{
    private Func<T, TKey> selector;

    public CompareSelector(Func<T, TKey> selector)
    {
        this.selector = selector;
    }

    public bool Equals(T x, T y)
    {
        return selector(x).Equals(selector(y));
    }

    public int GetHashCode(T obj)
    {
        return selector(obj).GetHashCode();
    }
}

class MyClass
{
    public int MyProperty { get; set; }
}

static void Main(string[] args)
{
    // このクラスのMyPropertyで重複除去したい
    var mc1 = new MyClass { MyProperty = 3 };
    var mc2 = new MyClass { MyProperty = 3 };
    var array = new[] { mc1, mc2 };

    var r1 = array.Distinct().Count();
    Console.WriteLine(r1); // 勿論2です
    // 比較用のIEqualityComparer<T>インスタンスを渡す
    var r2 = array
        .Distinct(new CompareSelector<MyClass, int>(mc => mc.MyProperty))
        .Count();
    Console.WriteLine(r2); // 1です
}

newするから、型を書かなければいけなくてね、記述量が多くて嫌だ。重たい重たい。C#3.0ってのは、もっとライトウェイトじゃなきゃダメなんだ。推論!型推論!しょうがないので、Distinctそのものに拡張メソッドを定義すれば……

public static class ExtensionMethods
{
    public static IEnumerable<T> Distinct<T, TKey>(this IEnumerable<T> source, Func<T, TKey> selector)
    {
        return source.Distinct(new CompareSelector<T, TKey>(selector));
    }
}

var r3 = array.Distinct(mc => mc.MyProperty).Count();
Console.WriteLine(r3); // 1になる

ラムダ式で書けるようになる。この調子でIEqualityComparerを使ってるメソッドの全てに拡張メソッドを定義すれば問題なし。しかし準備が面倒。このことは、ForEachが搭載されないことと並ぶLinq最大の謎だと私は思っているのですけど、どうなんでしょうか。何か理由があるのかなあ。とても気になるのだけど……。

どう書く。ドキュメントコメント。

オブジェクト倶楽部、コーディング規約の会の「C# コーディング標準」の駄目なところ - 予定は未定Blog版

全く持ってその通りで、このコーディング規約はねえ、害悪を撒き散らしてるだけだよね……。さて、ところでドキュメントコメント。私は、つけます。(プロパティにはつけないかも、メソッドにはつける)。IntelliSense大好きっ子なので。但しほとんどの場合summaryのみを一行。

/// <summary>何たらメソッドです</summary>
public int Method(string input)
{
    // 色々処理ってことで。
    return 0;
}

こんな感じ。何で一行にするかといったら、上にぐちゃぐちゃXMLで書かれてるのって汚いし読みづらいぢゃん、ということもあるんですが主な理由はアウトラインに折りたたんでも解説が見えるから。

ね?///で出てくるものをご丁寧に全部埋めると、アウトラインに畳んだ時に内容が読めなくて困るのです。自分のソースの時は構わないんですが、人のソースを読むときはアウトラインを閉じたり開いたりを多用するので、かなり困ります。というわけで一行summaryお薦め。問題があるとすれば、手書きが面倒(///は一発で出てくるので)ということですが、この程度はキーマクロで記録してショートカットに割り振れば一発なので是非。あと、///で同時に出てくるparamsやreturnsて、大抵は面倒くさいだけであまり必要ないよね。「そういうの、書かなくていいんだよ、summaryだけなら面倒くさくないでしょ?」という心理的な安心感を得られるのも良いです。

ついでに、もう一つ、コメントの飾り枠についても。

// こういうコメントの入れ方は嫌!
class MyClass
{
    // -------------------------
    // Property
    // -------------------------

    public int MyProperty { get; set; }
}

// これでいいでしょ?
class MyClass
{
    // Property

    public int MyProperty { get; set; }
}

領域を目立たせるために囲むんでしょうけど、アウトラインで折りたたむとコメントがばっさり畳まれて見えなくなるんです。無意味な飾りはほんとーに、止めて欲しい。こういうのは一行でいいんです。別に機械に頼ればいいじゃない、機械があるんだから、機械には頼るべき、だからこそソースは機械に優しく書いて欲しいと私は思います。

そういえば#regionの話も出てましたけど、私は#regionはあんまし使いません。かなり好きくない。フィールドとか短いモノを畳まれると、逆にいちいち展開しなきゃならなくて面倒くさいんですよねー。というわけで、よほど長ったらしい状態で任意のグループ分けがしたい時には使いますが、フィールド、プロパティ、メソッド、みたいな分け方のために#regionを使いたくはない。

まあしかし、つまりはアウトラインってあまり使わないのかなあ。「アウトラインの中止」と「定義に折りたたむ」の二つでバサバサと畳んだり開いたりを連発するなんて、しないのかなあ。しないのかも……。周りの人にキモ!このソースキモ!一行summaryキモ!と思われないためにも、むしろ一行summaryをスタンダードにしようの会。コーディング規約ってつまりそういうことでしょうか、なんて独善的な!

勿論、Sandcastleとか絡むんなら別のお話です。

linq.js :: Next

音沙汰のないlinq.jsなんですが、現在はWindows Script Host対応を進めています。対応といっても数カ所書き換えるだけなんですけど、それに合わせてWSH用のラッパーライブラリを書いているので、それに時間を取られてる感じです。基本的にはほんと薄いラッパーで、列挙処理がlinq.jsに渡せるのでそっちで好きに処理してね、という方向なので機能面での補助は一切なく別にすぐ出来上がるというかもうほとんど出来てるんですが、IntelliSenseを聞かせるためのvs-doc書きに物凄く時間喰われています。とにかくIntelliSense命な私は、IntelliSenseが動かないものなんて書きたくない!なければ自分でIntelliSenseを書く!という意味不明な方向で頑張ってます。

画像の一枚目は一週間前のものなので、現在はW.ToLinqは廃止して、E.Fromで動くようになってます。何のこっちゃ。

このご時世、WSHなんて下火、これからはPowerShellだよねー、って感じですが、それでも私はWSHで頑張る! WSHでLinq書けるのかゴルァ、を合い言葉に。JavaScript好きだし。まあ、素のJScriptだとEnumeratorを被せなきゃいけなくて列挙処理がゴミで使う気になれないのは確かなのですが、そこをlinq.jsがあれば何とか出来るわけなので、全然WSHは現役で行ける、Windows7時代でも全然行ける、と思います、思いたいです。まあ、あとWindowsサイドバーガジェット(デスクトップガジェット)にも使えるので、もうちっと踏ん張っていきたいな、というところ。Web系で頑張るのは無意味なのでニッチを狙いだしたとかそういうことではありま、す。

ver 1.3.0.5 バグ修正とか動かない人用とか色々

動かない-、という人にログを出して貰ったお陰で、幾つか問題を潰せました。本当にありがとうございます。自分の環境で再現出来るエラーを潰せるのは当たり前。再現しないエラーを潰せないのは三流。というわけで三流な私はさっぱり分かりませんでした。分かってみれば、ああ、確かに問題だなーって感じなんですけどねえ。

変更内容は「オフライン→オンライン時投稿のチェックを外していた場合、同期に失敗する不具合を修正」だそうです。えー、こんな初歩ミス埋め込んでたのー、っていうと、そうです、はい、埋め込んでました、はい。げふんげふん。これは酷い。そう、「実績解除」だけ利用できればいいや、って人が利用出来なかったのです、なんだってー。あともう一つ、「GamerTagの入力を大文字小文字を区別しないように変更」です。今まで区別していたので、例えばnEUEcCとか入力すると、同期出来てませんでした。これはいかんですね。いかんので、区別しないようにしました。

ていうか、自分の環境で再現しない問題、じゃなくてただたんに例外ケースの見積もりが甘すぎなだけですなあ。もうちっと気を引き締めて書かないとダメですね。

追記

ver 1.3.0.6になりました。1.3.0.4以降は「+記号」が使えなかったっぽいので、それを直しました。ダメダメすぎて涙。

MinBy

        var minBy = list.Aggregate((a, b) => a.Age < b.Age ? a : b);
        Console.WriteLine(minBy);

お題はGroupBy、OrderByを使ってみる - waりとnaはてな日記のもの。ただの例題だと思うので(例だとGroupByを使う意味がないし)突っ込むとかそういうつもりはなく、そういえばRubyでいうmin_byってないなあ、と思ったので。Aggregateはとても素敵メソッドだと思うけど、いざ使おうと思うとあまり使うシーンがなくてとても悲しい。

ver 1.3.0.4 動かなくなったのでちょっと書き換えた

昨日の朝だか昼だかから動かなくなってたっぽいので、それを直しました。何で動かなかったのかよく分かってないんですが、ダメになった箇所だけは分かったので別のアプローチで、ということで。応急処置もいいとこで、まー情けないのですが、動けばいっか、ってことで。眠いし。ダメですかダメですねすみません。

今後だと、一応ダッシュボードのアップデートが8月に控えているらしいので、それでも引っかかってダメになるかもしれませんが、ダメっぽくなったのを捕捉次第、速やかに直したいとは思っているのでダラダラとお待ちください。

LinqとCountの効率

IEnumerableを受け取ってのReverse(), Count(), ElementAt(), Last()は普通に考えると先頭から舐めるので効率がよろしくない。じゃあLinqの実装はどうなってるかというとSystem.Core.dllを眺めると

// Reverse
ICollection<TElement> is2 = source as ICollection<TElement>;
// ElementAt
IList<TSource> list = source as IList<TSource>;
// Count
ICollection<TSource> is2 = source as ICollection<TSource>;
// Last
IList<TSource> list = source as IList<TSource>;

というわけで、IListを実装しているものなら、ちゃんと変換してくれているので大丈夫。Count()なんかは普通にLength/Countを使うから別にどうでもいいって話なのですが、Last()は[hoge.Length - 1]って書くのは嫌なので、こうして安心して使えると嬉しい話。まあ、こんなことは過去何回も話題に上っているのですが、一応自分で確認しておかないとね、ということで。

MSDNのCountの説明にはICollectionを実装してるとそれ使う、って書いてあるけど、Lastのページには何も書いていなくて困ります。内部でisだのasだのやってゴニョゴニョしてるのなら、そのことも全部記載して欲しいなあ。

といったことを何故突然というと、C#にもほしい ~rubyのeach_with_index~ - SEの勉強日記という記事を見かけたので。Count()のうえにElementAt()のラッシュというのは、IListだけならいいんですけどIEnumerableで拡張メソッドを作っているので、少しよろしくない。

public static class Extension
{
    public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
    {
        foreach (var item in source)
        {
            action(item);
        }
    }

    public static void ForEach<T>(this IEnumerable<T> source, Action<T, int> action)
    {
        var index = 0;
        foreach (var item in source)
        {
            action(item, index++);
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        Enumerable.Range(5, 6).ForEach((item, index) =>
            Console.WriteLine("item={0} index={1}", item, index));
    }
}

という風にしたほうがいいと思われます。まあ、あと、new List()なんてやるぐらいなら.ToList()かしらん。

ver 1.3.0.3 / ちょっとした拡張メソッド群

Gears of War 2やCall of Duty : World at Warでステータスが取得出来なかった件を修正しました。GoW2は修正を確認出来ましたがCoD:WaWは持ってないので分からない。多分直ってると思うけど。あと、この修正の影響で他がダメになる可能性がなくもないです。ダメっぽいのを発見したら即直す、という方向で暴走さえしなければある程度はいっかー、的にゆるふわ気分でいるのですがダメでしょうかダメですね。

ところで、int.Parseが多くなって結構面倒くさいので文字列に拡張メソッド。

public static int ToInt(this string value)
{
    return int.Parse(value);
}

public static int ToIntOrDefault(this string value)
{
    int result;
    int.TryParse(value, out result);
    return result;
}

public static string Remove(this string input, string pattern)
{
    return Regex.Replace(input, pattern, "");
}

public static Match Match(this string input, string pattern)
{
    return Regex.Match(input, pattern);
}

public static string Replace(this string input, string pattern, MatchEvaluator evaluator)
{
    return Regex.Replace(input, pattern, evaluator);
}

RemoveとReplaceは既存のメソッド名のものに足してるので、別の名前のほうが良いかしらん。あと、Replaceは引数が被るので、ただの文字列置換は用意できなかった。まあ、_ => "hogehoge" といった具合に「_ => 」が増えるだけならそう手間でもないような十二分に手間のような。

totalGamerScore = document
    .Descendants("div")
    .First(e => e.GetAttribute("className") == "XbcProfileSubHead")
    .Descendants("strong")
    .Last()
    .InnerText
    .Match(@":\s*(\d+)/")
    .Groups[1]
    .Value
    .ToInt();

こんな風に書けます。これでスクレイピングも快適ー。ドット繋げまくれてバンザイしちゃう。ただまあ、デバッグはし辛いですね。ログ吐くメソッドを仕込んだりもいいんですが、もっと単純に、ラムダ式挟めばデバッガで止められるので

public static class Ext
{
    public static TResult Tap<T, TResult>(this T self, Func<T, TResult> func)
    {
        return func(self);
    }

    public static T Tap<T>(this T self, Action<T> action)
    {
        action(self);
        return self;
    }
}

RubyのTapみたいな、ということで。

ダミー置いて、その場で止められるようになる。それで作って、出来あがったらTapの行をCtrl+Xでゴミ箱ぽい。IEnumerableの場合は、普通にダミーのSelect置けばいいだけでしたね! 今、そのことに気付いた。うーん、あと.Groups.Cast<Group>().Skip(1).Select(g => g.Value) も、頻繁にあるあるなので、Matchに拡張メソッド埋めときますか。

Prev | | Next

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

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

X:@neuecc GitHub:neuecc

Archive