Archive - 2009.10

AnonymousComparer - lambda compare selector for Linq

class MyClass
{
    public int MyProperty { get; set; }
}
 
static void Main()
{
    // 例として、こんな配列があったとします
    var mc1 = new MyClass { MyProperty = 3 };
    var mc2 = new MyClass { MyProperty = 3 };
    var array = new[] { mc1, mc2 };
    // Distinctは重複を取り除く。でも結果として、これは、2です。
    var result = array.Distinct().Count();
    // 参照の比較なので当然です。では、MyPropertyの値で比較したかったら?
    // DistinctにはIEqualityComparerインスタンスを受け付けるオーバーロードもあります
    // しかしIEqualityComparerはわざわざ実装したクラスを作らないと使えない
 
    // そこで、キー比較のための匿名Comparerを作りました。
    // ラムダ式を渡すことで、その場だけで使うキー比較のIEqualityComparerが作れます。
    array.Distinct(AnonymousComparer.Create((MyClass mc) => mc.MyProperty));
 
    // でも、長いし、型推論が効かないから型を書く必要がある
    // Linqに流れているものが匿名型だったりしたら対応できないよ!
    // というわけで、本来のLinqメソッドのオーバーロードとして、記述出来るようにしました
    // ちゃんと全てのIEqualityComparerを実装しているLinq標準演算子に定義してあります
    array.Distinct(mc => mc.MyProperty);
 
    // 短いし、型推論もちゃんと効くしで素晴らしいー。
    // 匿名型でもいけます(VBの匿名型はC#(全ての値が一致)と違ってKey指定らしいですね)
    var anonymous = new[] 
    {
        new { Foo = "A", Key = 10 },
        new { Foo = "B", Key = 15 }
    };
    // true
    anonymous.Contains(new { Foo = "dummy", Key = 10 }, a => a.Key);
}

と、いう内容のコードをCodePlexで公開しました。LinqのIEqualityComparerって使いにくいよね、を何とかするためのものです。DLLでも何でもなく、ただの100行のコードなのでコピペで使ってくださいな。メソッドはAnonymousComparer.Createしかありません。newを使わせないのは型推論のためです。メソッド経由なら引数の型を書くだけで済み、戻り値の型を書く手間が省けるので……。あとはLinq標準演算子でIEqualityComparerを使うオーバーロードの全てに、キー比較用ラムダ式を受けるオーバーロードが追加されています。使い方、使い道は、まあ、見た通りです。

わざわざzipをダウンロードするのも面倒、という人はCodePlexのソース直接表示でどーぞ。どうせ.txtと.csしか入ってないので。でもダウンロード数とかが増えてると少し嬉しいですね。linq.jsもようやく50超えましたよ、あまりの少なさに笑えない。

以前にも同様のものを書いてた LinqとIEqualityComparerへの疑問 のですが、今回やっと重い腰を上げてまとめてみました。GroupJoinのオーバーロードとか手書きだと死ぬほどダルいですからねえ。と、いっても、やっぱ手書きでやってたら洒落にならないほど面倒くさいので、機械生成でサッと作りました。全然サッとしてないんですけどね。むしろ泥臭い。Linqネタなのでワンライナーで強引に仕上げてみましたよ!

static string data  = @"ここに定義へ移動で出てくるEnumerableのデータを貼り付けてね、と(4000行ぐらい)";
 
static void Main(string[] args)
{
    var result = data
        .Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)
        .Where(s => Regex.IsMatch(s, "public static.+IEqualityComparer"))
        .Select(s => Regex.Replace(s, @"(<.+)(>\(this)", "$1,TCompareKey$2"))
        .Select(s => Regex.Replace(s, @"IEqualityComparer<(.+?)>", "Func<$1,TCompareKey>"))
        .Select(s => Regex.Replace(s, @"comparer", "compareKeySelector"))
        .Select(s => s.Trim(' ', ';'))
        .Select(s => new { Signature = s, Groups = Regex.Match(s, @"^(.+? ){3}(?<method>[^ ]+?)<.+?>\(this (?<args>.+)\)$").Groups })
        .Select(a => new
        {
            a.Signature,
            MethodName = a.Groups["method"].Value,
            Args = a.Groups["args"].Value
                .Split(new[] { ", " }, StringSplitOptions.None)
                .Select(s => s.Split(' ')).Where(ar => ar.Length == 2).Select(ar => ar.Last())
        })
        .Select(a => string.Format("{1} {0} {{ return {2}.{3}({4}{5}); {0}}}{0}{0}",
            Environment.NewLine,
            a.Signature,
            a.Args.First(),
            a.MethodName,
            string.Join(",", a.Args.Skip(1).TakeWhile(s => s != "compareKeySelector").ToArray()),
            (a.Args.Count() == 2 ? "" : ",") + "AnonymousComparer.Create(compareKeySelector)"))
        .Aggregate(new StringBuilder(), (sb, s) => sb.Append(s))
        .ToString();
}

string dataのところにEnumerable.Rangeなんかを右クリックして「定義へ移動」で出てくるメタデータから、のものを全部コピーしてペースト。あとは、それをLinqでゴリゴリ加工すれば出来上がり。です。Select7連打は悪ノリですね。別にRegexの部分は.Replaceを繋げればいいのにね。あと、かなり決めうち成分強めなのと、正規表現が苦手であんまり上手く書けてないところが多かったりとで全く褒められたコードではありません。正規表現は本当に何とかしたいなあ……。

ああ、あと英語が酷い(笑) CodePlexのちょっとしたプロジェクト説明みたいな部分だけですら破綻しまくってる、単語すら繋げられない、これは酷い。

そういえば作ってから気づいたんですが、普通にリフレクションで取得した方が……遙かに……楽!綺麗に……仕上がる! と、気づいてしまったのだけど気づかなかったことにしようそうしよう。

100万

ノーマルALPHA SITEで一人Firefight100万到達しました。途中から疲れてプレイが雑になって、それでも中々死なない状況だったので、100万行ったら自殺する、ということで終えました。オールスカルオンになっても、怖いのはロッドガン持ちのブルートぐらいですかね。そいつのためにロケットランチャーを取っておいて、プラズマガン->ランチャー二発で倒していました。ハンマー持ちはランチャー二発打ち込む前にオーバーシールドが始まってしまうのでコンボは効きません。というわけで、避けて背後殴りがド安定。あ、勿論ハンターは怖いです。超怖いです。ハンターはランチャー利用で倒すしかないですなあ。まあ、そのぐらいです。ウザったい赤いハエはプラズマライフル+マシンガンで。

Bungie.net : Halo 3 ODST Firefight Game : Alpha Site (Solo) : 10.15.2009 9:25 というデータでした。3時間45分とかウンザリですね、はい。そういえば、後半になるとジャッカルの盾の握り手が硬くて、隙間撃ってもちっとも怯まないのはウザかった。ああいうのはウザいだけなので止して欲しいなあ。基本的には硬くなるだけの調整ってつまらないからね。

HALO3:ODSTのFirefightは全方位STG

一人ファイアファイトで20万超えたのが嬉しくてつい動画撮っちゃった記念に、超久しぶりにゲーム関連の記事を。結局Ustreamもやってないしね!配線関連の見直しをしているので、次こそは始めるぞ、と毎回言ってる気がしますが中々定着しません。というわけでHalo3:ODSTなのですが、これはHALO3とは似て非なるものでした。非常にHALO1に近いバランス。原点回帰ですねー。1と2以降は全然違う。HALO1にはとてもはまったけど、2以降はそれほどでもー、と思っていた私にはこのバランスは嬉しいものでした。シンプル明快なハンドガンとグレネードのコンビネーションで敵をなぎ倒していくという内容が好きです、ええ。今後出るHalo:Reachもこの方向で調整して欲しいなあ。二丁持ちなんてシステム的に余計なものに過ぎないし、あと、3はレベルデザインもしくじってたと思う。最初のジャングルステージの、自分の武器(カービン)の射程外に配置されているスナイパー連中をひたすらチマチマと超遠距離から倒していくとか、発狂もの。配置するなら、せめて射程内にして欲しいものです。

そんなODSTは、ぶっちゃけハンドガンとPガンがあれば他に何もいらないよね!というシンプルさ。これが短い本編と、ファイアファイトのアーケードライクな構成に、非常に合ってる。グラントたんは、正面時には大きめのヘッドショット判定で爽快。盾持ちのジャッカルは、盾の継ぎ目に撃って怯ませて、頭を狙うという、いわばヘッドショットを二回連続で決めなければならない、的なところはリズム良い。ブルートはPガンでシールドを剥がすとご丁寧に正面を向いて一定時間棒立ちしてくれるんですねえ。これをもってヌルいと言えば確かにヌルいのですが、サクッとお手頃TUEE感が出ていて好きです。で、まあ、それは別にHALO3でも一緒だったと言えばそーなのですが、標準武器のハンドガンが十分な射程を持っていて(HALO3のハンドガン射程は短すぎ!) パスパス打ち取れるところがいーんですよ。

HALOはドラクエと喩えられたりするけれど、(コンシューマー向けに最適化された上での)シンプルなFPSという意味で、ヘッドショット偏重のエイム主義、近・中距離でのダイナミックな立ち回り、役割が明確な単純な武器。ほーいいじゃないか、こういうのでいいんだよこういうので。塩コショウしただけの、肉!といった好ましさを感じます。

ファイアファイトはHorde(GoW2)だのSurvival(L4D)だの最近の作品では普通に搭載される1マップでの持久戦というわけで、目新しさはないものの、よく研究されているんじゃないかと思います。メリハリをつけるためのスカルシステムとか。そして一番嬉しい点が、一人プレイにも対応しているということ。L4Dは勿論、GoW2もライフが1なので実質一人プレイは不可能だったんですが、Firefightではプレイヤー全員の共有ライフというシステムを入れることで複数人プレイから一人プレイまでを実現。以前紹介したexception conflictもそうですが、複数人プレイを前提に調整されたものを一人でプレイするってのは、萌えるシチュエーションよね。激しい戦闘!これだよこれ、みたいな。そして、全方位から敵が攻めてくるというのは、その性質上どうしても籠もりプレイになりがちで、特にGoW2は籠城プレイ大前提なところにはガックリ来てたんですが(籠城じゃなく、華麗に全方位大立ち回りをしたいんですよ!) HALOは敵がどれだけ硬くなっても、とりあえずヘッドショットしておけば一撃で殺せる。ということになっているので、比較的籠もらなくても済むのも嬉しい感じ。他にもコンボによるスコア上昇や、メダル獲得など、無限に続く敵との殲滅戦を単調にさせない工夫がいっぱい。

そう、無限に続く。一応20万点が目標スコアとなっているけれど、それ以降も終わりなく死ぬまで続く。ライフも1ラウンド終了毎に無限にエクステンドされていく。これは……。Geometry Warsではないか!それも、2ではなく1。無限エクステンドと、無限に見えるかのような難易度上昇、にみえて実は上限のある難易度上昇(Firefightもスカル全ONが最大難易度として打ち止め) Firefightは2D全方位STGのFPSからの解釈なのですよ! FPSなんて3D全方位STGなのさ、という。しかも実に巧妙に出来てる。素晴らしすぎる。これが楽しくないわけないじゃないか、ねえ? 一人プレイ専用のランキングモードをつければ永遠に遊べるゲームになってました、そこが惜しい。ランキングないんだよねえ、残念。

あと、もう少しプレイヤーのスピードが上がって、マップに高低差がついてれば理想的なのだけど、そうなるとコントローラーでエイム出来なくなるので、落としどころとしてはこの辺がベストなのかなあ、と思います。FPSでジオメトリ、という「ぼくのかんがえたさいきょうのげえむ」を今現在最も体現しているのがFirefightですねえ。

あ、ところで動画なんですが、ハンマーブルートが無敵になることとか知らなくて無駄に突撃して死んだり、Pガンで動き止めてランチャーで即殺なのを途中まで気づいてなかったり、そもそも避けて殴り暗殺で一撃だったりするのは知らなかったり間抜け。更にはスナイパーを殺すことに拘泥して周囲の敵を無視したあげく死んだりと、結構ヘボいプレイなのですが、そこは「はぢめての20万点」という初々しさと稚拙さを楽しんでください。通してみると90分と長いんですけど、さすがに90分もプレイしてると一回や二回ぐらいは神テクニックが炸裂してるところがあるので、そこを見てニヤニヤしてたりします。

return IEnumerableとyield return

static void Main(string[] args)
{
    var path = @"C:\test.txt";
    var lines = EnumerateAllLines(path).ToArray();
}
 
static IEnumerable<string> EnumerateAllLines(string filePath)
{
    using (var sr = new StreamReader(filePath))
    {
        return Enumerable.Repeat(sr, int.MaxValue)
            .TakeWhile(s => !s.EndOfStream)
            .Select(s => s.ReadLine());
    }
}

これの実行結果はどうなるでしょうか。答えは、「閉じているTextReaderから読み取ることはできません。」という例外が発生します。当たり前ですか? すみません。Linqばかり触っていると、ついついIEnumerableだから遅延評価だね!と単刀直入に思ってしまっていたりしたのですが、IEnumerableは決して必ずしも遅延評価であるということでは、ない。配列だってIEnumerableなんだよ!という。当然のようなことですが、すっかり頭から抜け落ちていました。反省。

何で例外が発生するかと言えば、EnumerateAllLines(path)の時点でメソッドが呼ばれ、returnで返した時点でusingを抜けてストリームが閉じられてしまう。ので、ToArray()で閉じられたストリームに対して読み込みを始めて、南無。というわけです。ではどうすればいいかというと……

static IEnumerable<string> EnumerateAllLines(string filePath)
{
    using (var streamReader = new StreamReader(filePath))
    {
        var seq = Enumerable.Repeat(streamReader, int.MaxValue)
            .TakeWhile(sr => !sr.EndOfStream)
            .Select(sr => sr.ReadLine());
        foreach (var item in seq) yield return item;
    }
}

yield returnを使ってやれば、コンパイラがイテレータを作るので、遅延実行される。EnumerateAllLines(path)の時点ではメソッド内部は一切通らない。MoveNextが呼ばれて初めてusingを通り、列挙が終わるかDisposeが呼ばれるまではusingを抜けない。という、なって欲しいであろう挙動を取ってくれるわけです。実行ファイルをReflectorで見ると、復元不可能なぐらいグチャグチャなものが出力されていて、あまりの難読化っぷりにビビりますが気にしないことにしませう。

そもそもEnumerable.Repeat(sr, int.MaxValue)のほうを改善してRepeatWithUsing作った方がいい、のではあるのですけど、まあ、それはそれということで。

無限リピート + SQL

上のはただの説明用の例でクソの役にもたたないので、もう少し実用的なものを一つ。

static void Main(string[] args)
{
    var command = new SqlCommand();
    command.CommandText = @"select hogehogehoge";
    var result = command.EnumerateAll(dr => new
    {
        AA = dr.GetString(0),
        BB = dr.GetInt32(1)
    });
}
 
static IEnumerable<T> EnumerateAll<T>(this IDbCommand command, Func<IDataReader, T> selector)
{
    using (var reader = command.ExecuteReader())
    {
        var seq = Enumerable.Repeat(reader, int.MaxValue)
            .TakeWhile(dr => dr.Read())
            .Select(selector);
        foreach (var item in seq) yield return item;
    }
}
 
static T[] ReadAll<T>(this IDbCommand command, Func<IDataReader, T> selector)
{
    return command.EnumerateAll(selector).ToArray();
}

シーケンス無限リピートをSQLの読み込みに応用してみるとかどうでしょう。Linq to Sqlのように、とまでは到底行きませんが、匿名型も使えるし、何となくそれっぽい雰囲気は出てるんじゃないかしらん。EnumerateAllの後段にTakeWhileを付けて条件で途中で止めるとか、Take(10)で10件のみ取得とか、それなりに自由に動かせます。

ラムダ式の引数の名前

Linqもそうですが、Linq意外でもFuncやActionのお陰様でラムダ式を渡しまくりな昨今。そうなるとちょろっとした引数の名前付けが面倒くさかったりします。ので、大抵は一文字で済ませるわけです。一文字ってどうなのよ、よくないんじゃないの?というと、ラムダ式のような極端にスコープが短いものは、しっかりした名前がついているもののほうが逆に見づらかったりするので、一文字でいいと思います。面倒くさいからって全て_だの__だので済ませると見づらいので一文字はなるべく死守します。但し引数を利用しない場合は_を使います。ネストしたり、式じゃなくて文になる場合はしっかりした名前を付けます。

a // AnonymousType
i // int
s // string
c // char
d // datetime
g // IGrouping
t // Type
e(ex) // exception
e(elem) // XElement
e(ev) // event
e(ie) // IEnumerable(あんま使わないですが)
ar // array(IEnumerableの時もこれ使ったりする(適当))
ar // IAsyncResult(うわ、被っとる)
fs // fileSystemとかラクダの頭文字を繋げる
x // 適当なのがない場合とか、x => xとか
xs // arrayやらIEnumerableやらどっちでもいー、とヤケクソな時
_ // 引数を式中で利用しない場合

大抵は型の頭文字一つです。AnonymousTypeは、前はtを使ってたんですが、IntelliSenseで出てくるのが’aだし、世間的にもaが主流だしTypeと紛らわしいので、最近はaで書くようにしています。Aggregateなんかは(a,b)=>って昔は書いていたのですが、aが匿名型なので紛らわしいから、(x,y)=>と書くようにしています。徹底はしてません。適当です。

それにしてもeの被りっぷりは酷い。こうなると、不人気頭文字を使いたい。というわけで、ライブラリからTypeの頭文字を取得してみる。アセンブリの取得部分は2009-08-17 - 当面C#と.NETな記録のコードをお借りしました。

// asmNamesは http://d.hatena.ne.jp/siokoshou/20090817#p1 から
// Groupingをバラしているのは、デバッガで中身を見る時にかったるかったから
var list = asmNames
    .SelectMany(s => Assembly.LoadWithPartialName(s).GetExportedTypes())
    .Select(t => t.Name)
    .GroupBy(s => s.First())
    .OrderBy(g => g.Key)
    .Select(g => new { Key = g.Key, Types = g.ToArray() })
    .ToList();
list.ForEach(a => Console.WriteLine("{0} : {1}", a.Key, a.Types.Length));
 
A : 319
B : 252
C : 773
D : 715
E : 275
F : 263
G : 172
H : 264
I : 887
J : 14
K : 58
L : 249
M : 424
N : 140
O : 189
P : 526
Q : 31
R : 312
S : 1039
T : 644
U : 169
V : 118
W : 272
X : 372
Z : 7
_ : 32

IはInterfaceだから数が多いのはしょうがない。Zは当然としてJ, K, Qは不人気ワードなので狙いどころですね!そしてSは人気あり過ぎ、と。別に考察でも何でもないです、すみません。CamelCaseを短縮結合して、一文字の場合のみを取り上げたら

// SplitCamelWordはasmNamesと同じくsiokoshouさんのコードから
.Select(t => SplitCamelWord(t.Name).Aggregate("", (x, y) => x + y.First(), s => s.ToLower()))
.Where(s => s.Length == 1)

ちゃんと(?)、eが一番人気でした。

Search/Archive

Category

Profile


Yoshifumi Kawai
Microsoft MVP for Visual Studio and Development Technologies(C#)

April 2011
|
July 2018

Twitter:@neuecc
GitHub:neuecc
ils@neue.cc