可変の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に拡張メソッド埋めときますか。

ver 1.3.0.2 色々バグ修正

バグフィックス祭り! まずバグ報告があってそれを直して、つまりは普段と少し違う状態の時の処理が全く入ってなかったので、洗い出して処理を入れました。別に全く考えていなかったわけじゃないのですが、想定からの漏れが幾つか、というかたっぷりありまして……。具体的には「申請待ちのフレンドがいるとエラーになる」「ステータスが「退席中」の際にデータが正しく取得出来ない」「ステータスが「取り込み中」の際にエラーになる」です。不具合くせえええ、と思ったら遠慮無く言っていただけるととっても助かりますので、お願いします。

ver 1.3.0.1(XboxInfoTwit) / exception(アーケード)

全てのゲームタイトルで、オンラインマッチ時にステータスが途中参加可能な状態で実績取得が出来ず、タイトル名がおかしくなる件を修正しました。Xbox.comから文字列決めうちで切り出しているので、どんな文言が来るのかのパターンに漏れがありましたというか知らんかった……。(ちなみに現在、待機中でも同様の問題があったりするので、直します、次回に、他にもないかちゃんと調べてから)。こうしてバグ発覚な上に(しかもこの問題は以前からあったっぽいです、全然気づいてなかった)、自信満々に互換性向上!とか言っておきながら、動かなかったって人がいるみたいでごめんなさい。しかも以前のバージョンでは動いていたとなると、もう本当にごめんなさい……。

//

ところで話かわって、exceptionのアーケード版がロケテされてるらしい、新宿南口ワールドで7/9~15だそうだ。というわけで行ってきましたは二日目。実は初日にも行ったんですが、お上りさんなので間違えて東口のタイトーステーションに行って、置いてないなあ、と思ってた。いや、「タイトーステーション 新宿南口ゲームワールド店」自体には何度か行ったことはあるんですよ!でも、タイトーって印象がないというか真面目にセガだと思ってた(笑) 別に西口のセガのと思い違いしてたわけでもなく素で。それで、東口のほうにはインベーダー(=タイトー)の印象があったので、ほぅほぅ、あんなところも南口に含めるのか、とか思ってたとか思ってたとか。悲しい。ゲーセンの店名って意識したことないのよね、あまり行かない人なので。

アーケード版のゲーム内容ですが、普通にexceptionでした。ステージは若干アレンジされていたり、オリジナルステージがあったりなどですが、内容は全くそのままPC版のexception。処理落ちなどもなく、ヌルヌルと動いておりました。ただし破片量はノーマルか、それより少ないか、といった感じで若干の寂しさは否めない。

気になる操作方法はジョイスティックとボタン3つ。全方位STGでスティック一本ということで、ボタン一つが方向 転換用の固定ボタン(PC版のディレクション)に割り当てられていて、これがぶっちゃけ操作しづらい。残りのボタンはレーザーとレイ、二つを(一度離して)同時押しでカタパルト。このカタパルト動作が筋力貧弱な私には存外辛くて、終わり頃には腕が疲れちゃってた。カタパルト→カタパルト→カタパルトと連打するわけなので、ワンボタンで出したいなあ、と思いつつも、まあボタンあまってないししょうがないね。

そんなこんなだから、というわけじゃないのですが、あえなくPC版3面ボス(アーケードでは5面、だったかな?)で死亡してスコアは5位でした。ランキング一位は、PC版作者のi-saint氏のよう。i-saint氏が初見で?クリアしたようなので、なら私も!とか意気込んでいたのですがズタボロ。いてれーたんに普通にいいように弄ばれるという微笑ましさを見せつけました。

ステージ構成はPC 版->アーケードオリジナル->PC版……と交互のようです。PC版は若干短く簡素にアレンジ。アーケードオリジナルのほうは破片の少なさを補うためにギミック中心の構造になっていて新鮮で面白い。ボスも同様に飛び道具系、というか変形する。アーケードオリジナルボス2体目の倒し方が分からなかくてライフが半分減るまで無意味なことやってたり。新鮮なのはよいことです。

全体的には、昔のアーケード→ファミコン移植のような印象。おお、頑張ってる!という。破片量はしょうがないとしても、操作し辛いのはどうしたものかなー。今回私は3ボタン使う上級者向け操作を選んだので、2ボタン操作の初心者向け操作だと印象違うかもしれません。そういえば、エフェクトも弱めなのでカタパルトの爽快感が薄かったような気がする。回りを破片で囲まれてピンチをカタパルトで一気に突破!的なのが、画面が真っ白になって抜け出せたのか何なのかわけわからないというか自機が本当に白に埋まって見失う、のはどうなのだろう、慣れかな?ジオメトリとかも見失うって言われるわけだし。

とはいえ普通にexceptionだったので普通に面白いと思います。オリジナル面も良い感じだし。PC版に比べてどうよ?と言われると完全にノーコメント(すみません)ですけど、ジョイスティックでやるのも新鮮、ということで遊びに行ける人は遊びに行くと良いやも。とりあえず私は途中で死亡したのが悔しいので、明日も行きます、というかロケテ中にはクリアしたい。(それにしても音楽も効果音も全く聞こえなかった、周囲の音うるさすぎ、私はひなびた田舎のゲーセンが好きですよ(笑))

あと、せっかくなので明後日、7/12の日曜日の22:00頃からUstreamでexceptionとexception conflictを実況プレイしようと思うので良ければ見てやってください。

ver 1.3.0.0 ロジック変更/OAuth対応

公開から半年以上経って、何故かここ数週間で利用者が増えてきたようで同時に動かない報告も目にするようになって嬉しくも悲しい。なので、今回ロジック部分を完全に書き換えました。これで色々な環境でも動作するようになった、はず、です。以前に試して動かなかったという方は、再度試していただけると嬉しいです。ついでに取得処理も軽量化しました。真面目に10倍ぐらいは速くなってます(今までが酷過ぎただけなんだけどね!)。そんなこんなで手を入れた部分が多いので、(ただでさえ不安定との定評があったのに)安定性はむしろ下がった、かも……。

それと、認証がパスワード方式からOAuth方式に変更になりました。ユーザーにとってOAuth対応によるメリットは、クライアントアプリではぶっちゃけ別にないどころかむしろ色々な不都合のほうが多いのですが私がやりたかった、ということで勘弁してください。投稿にfrom XboxInfoTwitってクライアント名が表示されるようになって嬉しいのです、にひひ。自動アップデート経由の方は、認証を一度済ませなければならないので手間です、すみません。

あ、で、ver1.2からそうなのですが、現在はAPIは一切利用していませんので遅延はありません。Xbox.comのステータス反映速度(1分以下で追随しているようです)で処理していますので、原理的には他の同種のアプリと同じです。が、多機能風味なため、巡回ページ数が多いのでチンタラ通信してたりする時があって、そのせいで反映が遅いという印象を持たれるかもしれません。今回の軽量化で若干印象は変わると思いますがそんなに変わらないかもしれません。その他、投稿の法則やFAQは同梱の説明書きを見てください。

↑通信内容はこんな感じです(Fiddlerで観測)。30秒かかりました。結構長いけれど、Xbox.comが重いサイトだからねえ……。まずフレンドリストの取得、そこからフレンドのフレンドに飛んで自分のステータスを取得(取得できない場合は取得出来るまで次のフレンドに飛ぶ)、トップページに飛んでプレイ中ゲームのURLを取得、プレイ中ゲームのページから実績を取得。という流れです。これがステータス取得の最短パスじゃないかと思っています。自分のステータスがFriendsOfFriend経由でしか見れないというのが意味不明で困るところ。

そういえばでどうでもいいんですが、タスクバー右クリックに「ゲイツポイントを買う」が追加されてます。アフィです、アフィ。んーと、ノーコメント。

Linqで文字のバイト単位でグループ分け

var input = "e4bba5e4b88be381aee69687e5ad97e58897e381af5554462d38e38292e69687e5ad97e382a8e383b3e382b3e383bce38387e382a3e383b3e382b0e5bda2e5bc8fe381a8e38199e3828b3136e980b2e695b0e381aee38390e382a4e38388e58897e381a7e38182e3828be38082";
var count = 0; // 外部にカウント用フラグ変数を置く
var words = Regex.Matches(input, "..")
    .Cast<Match>()
    .Select(m => Convert.ToByte(m.Value, 16))
    .Select(b => Convert.ToString(b, 2).PadLeft(8, '0'))
    .ToLookup(s => (s.StartsWith("10")) ? count : ++count);
Console.WriteLine(words.Count);

前回(たった10時間前ですが)の続き。16進数バイト列で表現されたUTF-8の文章の文字数を数えろ、というお話でした。せっかくなのでビット見てうんたらかんたら、のほうでも。律義に2進数に変換しながら分類。例えばwords[0]は[11100100,10111011,10100101]になる。こんな感じで41個分グルーピングされています。.Keyで何文字目かが取得出来るところが地味に素敵でいて微妙に混乱を招く(配列の何番目、ではない)。 で、まあしかし、場外にフラグ変数を置くことで何でもこなせるけれど、それってアリなの?という疑問はある。Linq連鎖の場外に変数を置くなんてグローバル変数的な害悪です。かどうかは分からないけれど、イマイチだとは思う。

場外に置かないで分類するとしたら、どうすればよいかなー。うーん。思い浮かばない。

var text = words.Select(g => g.Select(s => Convert.ToByte(s, 2)).ToArray())
    .Select(bytes => Encoding.UTF8.GetString(bytes))
    .Aggregate(new StringBuilder(), (sb, s) => sb.Append(s))
    .ToString();
Console.WriteLine(text);

こっちはどうでもいいんですが、文章への復元はこんな感じで。面倒くさー。何個Select使えば気が済むんだ、みたいなみたいな。でもLinq使わないとそれこそもっと面倒くさい気はする。とりあえず=>かわいい。=>のかわいさは異常。=>がかわいいすぎて萌え死ぬ。

Cast

天下一プログラマーコンテスト、ほうほう、例題とな?

var input = "e4bba5e4b88be381aee69687e5ad97e58897e381af5554462d38e38292e69687e5ad97e382a8e383b3e382b3e383bce38387e382a3e383b3e382b0e5bda2e5bc8fe381a8e38199e3828b3136e980b2e695b0e381aee38390e382a4e38388e58897e381a7e38182e3828be38082";
var bytes = Regex.Matches(input, "..").Cast<Match>().Select(m => Convert.ToByte(m.Value, 16)).ToArray();
var result = Encoding.UTF8.GetString(bytes);
Console.WriteLine(result); // 以下の文字列はUTF-8を文字エンコーディング形式とする16進数のバイト列である。
Console.WriteLine(result.Length); // 41

題意に全然沿わずフレームワーク丸投げ。それにしても、古いクラス(MatchCollectionとか)はIEnumerable<T>じゃないのでCast<T>が必要で面倒くさい。Castは、その段階で「型を意識」しなければならないのが思考の流れを阻害する。「Select(m=>m.」と打つと、ああ、送られてくるのはMatchなのかあ、って分かる。それ以上には考えさせないで欲しい。型推論万歳。型推論は、JavaScriptを始めとしたゆるふわ加減の心地良さと、型キッチリでコンパイルしてエラーが出て弾かれて万歳の気持ちよさがちょうどよく混ざり合ってて、イイなーって思う。だからというわけじゃないけど、なんでもvarで済ませてます、私は。var hoge = 3とか平気で書く。

そういえば、AchiralはMatchとMatchCollectionに、Selectのみ拡張メソッドで足してあるので、Castを書く必要なくスムーズに繋げられる。この面倒なMatchCollectionをどうしたものか、という解答として丁度良い妥協バランスで、なるほどなるほどでした。Achiralは読んでて本当にためになります。

追記:続き→neue cc - Linqで文字のバイト単位でグループ分け

IEnumerableに文字列連結

C#と諸々 10分でコーディングから。ネタ元、のネタ元の問題文の解読しづらさは異常。例も酷かった。というのはともかく、Linqならグルーピングのお話だよねー

class Cards
{
    public string[] Deal(int numPlayers, string deck)
    {
        var count = deck.Length / numPlayers;

        var result = deck
            .Select((c, i) => new { c, i })
            .GroupBy(t => (t.i % numPlayers), t => t.c)
            .Select(g => new String(g.Select(c => c).ToArray()))
            .Select(s => (s.Length > count) ? s.Substring(0, count) : s)
            .ToArray();

        return (result.Length < numPlayers)
            ? Enumerable.Repeat("", numPlayers).ToArray()
            : result;
    }
}

あれ、スッキリどころか意外と汚い……。んー、最初はGroupByのところがToLookupでそのままreturnしてすっきり終了。だったのですが、「求められるのはIGroupingじゃなくて文字列」なのでLookupじゃなくGroupByにして後段のSelectで結果を文字列化。「余ったカードは配られず捨てられる」ので更に後段のSelectで廃棄処分。「カードが足りなかった場合は人数分の空文字列配列を返す」のでreturnを別に分けてRepeatでそれを生成。後付けでごにゃごにゃ足していった結果、汚くなってしまった、残念。

まあでも、何が嫌かってnew Stringの部分ですね。中身がcharじゃなくて文字列の時はstring.Joinを使いますが、どちらもToArrayがウザくなってイヤー。IEnumerableにToStringは文字列で連結して欲しい。いや、Aggregateで代替出来るのは知ってますが定型処理のくせに必要なタイプ数多くてnew Stringとあんま変わらないし、セパレータも付けたいとなるといよいよ面倒くさい。というわけで拡張メソッドの出番。

public static class Ext
{
    public static string ToJoinedString<T>(this IEnumerable<T> source)
    {
        return source.ToJoinedString("");
    }

    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();
    }
}

残念ながら拡張メソッドは元からあるメソッド名を上書きできないのでToStringではなくToJoinedString。関数名が微妙? そうですね……。 セパレータを使いたい場合にAggregateでサクッと書けなくて昔悩んだのですが、カウント用の変数を場外に用意すればいいんだ!と気付いたのでサクッと書けるようになりました。Linqだけで1行で済ませたい場合はSelectでindex作ってやればいいですねー。

var result = Enumerable.Repeat("hoge", 10)
    .Select((s, i) => new { s, i })
    .Aggregate(new StringBuilder(), (sb, t) => (t.i == 0)
        ? sb.Append(t.s)
        : sb.AppendFormat("{0}{1}", "|", t.s));

悪夢だ。実行効率的にもアレゲ。いくらAggregateで何でも出来るといっても、この辺は標準搭載して欲しいものですねー、Sumがあるなら文字列連結があってくれてもいいじゃない、みたいな。

Prev | | Next

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

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

X:@neuecc GitHub:neuecc

Archive