Modern C# Programming 現代的なC#の書き方、ライブラリの選び方

と、題しまして第1回 業開中心会議 .NET技術の断捨離で話してきました。岩永さんが概念的なものを、私がC#とライブラリにフォーカスして具体的なものを、パネルディスカッションでフレームワーク周りの話を。的な分担だったでしょうか。

Modern C# Programming 現代的なC#の書き方、ライブラリの選び方 from neuecc

Modern、といっても、すんごく最先端で尖った感じ!ではなくて、本来は今そこに当たり前のようにあるやり方を、ちゃんと選択していこう。といったような内容です。対象をどの辺に置こうかなあ、といったところで、やっぱ@ITでいうギョーカイだと、ラムダ禁止とか.NET 2.0を強いられているんだ、とかそういう人も少なくないのだろうと思ったので、その辺りを最低ラインに敷いて中身を組みました。

つまりLINQ使えってことですよ!というか、もう登場から5年も経とうとしているのに、未だにLINQ使うべきだよ!と言って回らなければならない事実が悲しくもあり。ちなみに他の言語にもLINQ的なものはあるのに、それでもなお何故LINQが良いか、という答えは、IntelliSenseにあります。IntelliSenseの気持ちよさをどう最大限に生かすか。それを考えるのが大事で、IntelliSenseにフォーカスしてプログラムを書くのが、真のModern C# Programmingなのです、というのがメッセージです。嘘。

パネルディスカッション補足

DataSetは、もう古臭いし捨てればいいぢゃーん。なわけですが、UIコンポーネントがDataSet依存なんです!という場合は、そりゃしょうがない。アプリケーションを作るにあたって何を優先するか、であり、そのコンポーネントを使うのが最優先な条件なら、回避不能です。案としては、DataSetを必要としないUIコンポーネントを買うべき、といったところです。スライド中にも入れましたけれど、資産は本当に資産なのか?負債じゃあないのか?と。負の遺産に縛られるのは何よりも苦しいことです。

EntityFrameworkの是非については、うーん、そもそも私はORMは幻想だと思っているところがあるので、根源的にYES、って言いづらいのですよね。別に、もう悪くもないと思ってます。ここで言いたかったのは、選択肢があるよ、ということです。EntityFrameworkというかORMを「使わなければならない」ことは全くない。そういうメッセージを伝えたいのです。

データアクセステクノロジとしてLINQ to SQLを採用していない環境で、LINQ to ObjectsとLINQ to SQLが誤爆するということを懸念している、という話が出ました。少しでも知識のある人ならば「絶対にない」ということは分かります。失礼な言い方をすれば、知識のない人が先入観だけでアリかナシかをトップダウンで決めている、そういう現状があることが停滞を生んでるのではないかな。

WebFormsの未来は、あるのかないのか、本当に使い分けなんてあるのか。ね、まあ、ないよね。WebFormsのコンセプトとか、私は結構好きです、今も。でも、Webの進化についていけなかった結果、生のHTMLと生のJavaScriptとコンポーネントが混じり合ってカオスになってる。中途半端な抽象化ならば、ないほうがよっぽどマシで、WebFormsの達成したかった理想は失敗したといってもいい。

MSがちゃんとWebFormsにも開発リソース割いてるよ、安心してよ、といっても、最低限の現状維持ぐらいでは?私はMSが全力で注力してWebFormsを立て直そうとすれば、立て直せたのじゃないかな、と思ってます。でも、それでMVC側に割くリソースをWebFormsに振るとか愚かしいし、だから実際振られてない。つまるところWebFormsの未来はどうなのか、そりゃ暗いよね。

管理画面はWebFormsで、みたいな話も眉唾なんですよね。使用技術が分かれる(しかも一方は暗い未来のもの)のは良いことかといったら、NOでしょう。別に、既にWebFormsに習熟しているし、既存資産(そう!資産ね、いつだってそうだ!)があるならいいでしょう。でも、これからの人は最初からASP.NET MVCで、WebFormsなんて全然知らなくて。といったことになるでしょ。その時どーなの?その資産を残していきたいの?それだったらMVCでGridView的なものをやるための手法を確立するほうが、よほど資産じゃないの?

などなどですが、WinForms→WPFのように、明らかに後継、のような形でない以上は、Microsoftからはお茶濁しなアナウンスしかこないでしょう。そういうのは、しょうがない。なので、あなたが思っている、それが答えとなります。

謎社始まりました

スライドの自己紹介にも書きましたが、謎社改め株式会社グラニはじまりました。去年の9月に設立で、私もほぼほぼ設立期メンバーとして働いていました(ほぼほぼ、なのはシンガポール行ってたりなんだったりだったのでジョインが若干遅れた)。ちょうどセッションの前日に第一弾タイトル、神獄のヴァルハラゲートがGREEでリリースされました!わー、きゃー。ソーシャルゲーム、ではありますが、ゲーマー視点であっても良い内容に仕上がっていると誇れるだけのものは出来たと思っています(私だけじゃなく、チームとして、ね)。ので、試してみてもらえると嬉しいです。

さて、そうしてうまく立ち上がったことで、謎社も人を募集するフェーズに入ります。C#エンジニア、はもとよりサーバー管理とかインフラとかフロントエンドとかデータマイニングとか、まあ色々な職種の人間が全然足りません(ぶっちけ全員アプリケーションエンジニアなので)。というわけで、興味あるという人は、私のほうまでメール ils@neue.cc でもTwitterでも声かけてもらえるといいかな、と思います。採用ページは出すかもしれないし、その前に埋まるかもしれないし、なので。

個人的には、日本を代表するC#の企業にしたいと思っています。そして、それが出来るメンバーが揃っています(代表する、ためには技術だけではなく企業として育つ=良いコンテンツがなければダメなわけで、それが出来るずば抜けた実力のプランナーやイラストレーターが在籍している)。詳しい話はその時に、という感じではありますが、大きく成長出来ると思っていますので、是非是非お待ちしております。

C#でFlash Liteなswfをバイナリ編集して置換する

Flash Liteに限定しませんが、そういうのをどうしてもしたい!というシチュエーションは少なからずごく一部であるようです。どーいうことかというと、ガラケーが積んでるFlash Lite、は、パラメータを受け取って、それをもとにどうこうする、というのが非常に弱い。ほぼほぼ出来ない。でも、違うメッセージを表示したい、画像を変えたい、などという需要があります。特に、ソーシャルゲームはまさにそうで。そこで各社がどういう手段を取っているかというと、.swfを開いてバイナリ編集して、直接、テキストだったり画像だったりを置換しています。

RubyやPHPには有名なライブラリがあって実例豊富だけれど、ドトネトにはない。というのが弱点の一つでした。ん?あれ?gloopsではどうしていたの?というとそこのところは内緒(辞めた人間なのであまり言えません)

で、SWF仕様読みながら自前で解析してやるしかないかなあ、画像置換ぐらいしかやらないからフルセットの再現はしなくていいので、手間でもないだろう、でも手間だなあ、嫌だなあ、と思ったら、ライブラリ、あったじゃないですか!それがSwf2XNA。to XNAということでFlashをXNAで使えるようにする(GreeのLWF、よりもスクリプトも再現するから高級版ですなー)、他にXAMLに書きだしたりとか色々できるよう。中々高機能で良さそう!

といっても、そもそも目的がテキスト/画像置換しかしないので高機能である必要はないのですが、高機能を実現するために、SWFの解析回りはバッチリ。

残念ながらドキュメントはXNA周り中心で(当たり前か)さっぱり、コンパイル済みバイナリも用意されてないでソースからのみ。と、使うには微妙にハードですが、一から仕様読み解いてぽちぽち作るよりも百億倍楽なので、喜んで使わせて頂きます。とはいえ、swfの仕様については、ある程度読んで頭に入れておいたほうがいいです、というかそうでないと、どう操作すればいいのか全くピンと来ないので。

SWFの詳しい話はSWFバイナリ編集のススメが親切丁寧で非常に詳しい、分かりやすい。ので、それと照らし合わせながら進めていきましょう。

SWFをSwfFormatで読み込む

Swf2XNAのソリューションを開くといっぱいあって何が何やら。しかもコンパイル通らないし(XNA周りが未インストールだから)。で、困るのですが、今回はXNA周りは不要でコアのSWF解析さえできればいいので、そのためのプロジェクトはSwfFormat。これはXNAなどなどを入れなくても単体でビルド通るので、ビルドしてDLLを作りましょう。

さて、ビルドが通ったら、まずはSWFバイナリ編集のススメ第一回に従って、orz.swfをサンプルとしていただいて、解析してみましょう。

// SwfReaderはちょっと高級なBinaryReader的なもの
// swfはbit単位での処理しなきゃならない部分があるのでBinaryReaderだけだと不便
var reader = new SwfReader(File.ReadAllBytes("orz.swf"));

// SwfCompilationUnitがSwfの構造を表す
// コンストラクタの時点で生成出来てる(XElement.Loadみたいなもの)
var swf = new SwfCompilationUnit(reader);

メインとなるクラスはSwfCompilationUnitです。これが全て。中身がどうなってるか、というと、Visual Studioで見るのが速いですね。いやほんと、皆IDE使うべきだと思いますよ、ほんと(最近ぺちぱーなので愚痴る、いや、私自身はPHPはPHPStormで書いてるのですが)

ばっちり解析済みのようです。Headerを見ても、問題なく作れてる。

Tagを置き換えてみる

SWFの中身の実態はTagです。↑で見ると、orz.swfにはTagが87個ありますね。どういうのが並んでいるのか、というと、これもVisual Studioで見るのが手っ取り早い。

ふむふむ。なんとなくわかるような感じ。フレームがあってオブジェクトがあって、みたいな。では、SWFバイナリ編集のススメ第二回に進んで、背景色を変更しましょう。背景色は↑で開いている、BackgroundColorTagを弄ります。具体的な作業手順、コードは以下のような感じ。

// 要素はTagの中に詰まってるので、それを探してパラメータを置きかえ
var tagIndex = swf.Tags.FindIndex(x => x.TagType == TagType.BackgroundColor);
var tag = (BackgroundColorTag)swf.Tags[tagIndex];
tag.Color.R = 255; // 背景色を真っ赤に
tag.Color.G = 0;
tag.Color.B = 0;

// Tagはstructなため、代入しないと反映されない
swf.Tags[tagIndex] = tag;

using (var ms = new MemoryStream())
{
    // SwfWriterはMemoryStreamしか受け付けない(Lengthを最後に書き換えたりするから、その必要があるみたい)
    var sw = new SwfWriter(ms);
    swf.ToSwf(sw); // メモリストリームに書きだされた

    // ファイルに置換後のSWFを出力
    File.WriteAllBytes("replaced.swf", ms.ToArray());
}

Tagを置き換えて(classじゃなくてstructなので、実際にListに再代入しないと変更が反映されないことに注意)、あとはSwfCompilationUnitのToSwfを使ってMemoryStreamに吐き出してやれば、それだけでTagの置換は完了です。無事、背景色が真っ赤なswfが生成されました!すっごく簡単だわー。

画像置換

SWFバイナリ編集のススメ第三回 (JPEG)に従って、画像置換もやってみましょう。

var reader = new SwfReader(File.ReadAllBytes("orz.swf"));
var swf = new SwfCompilationUnit(reader);

// DefineBitsTagはCharacterIdを持つので、実際はそれを参照して置換するTagを探すと良い
var tagIndex = swf.Tags.FindIndex(x => x.TagType == TagType.DefineBitsJPEG2);
var tag = (DefineBitsTag)swf.Tags[tagIndex];
tag.JpegData = File.ReadAllBytes("ethnyan.jpg"); // jpegデータを直接置き換え
swf.Tags[tagIndex] = tag;

using (var ms = new MemoryStream())
{
    // Tagの画像を置き換えたことでHeaderのFileLengthも変わらなければなりませんが
    // ↑でTagに代入しただけでは、そこは変わっていないままです
    // が、ToSwfの際に、Headerも再計算された値に置き換えてくれるので、手動で変える必要はなし
    var sw = new SwfWriter(ms);
    swf.ToSwf(sw);

    // ファイルに置換後のSWFを出力
    File.WriteAllBytes("replaced.swf", ms.ToArray());
}

やってることは当然ながら同じで、置き換えたいTagを探す、置き換える、ToSwfで吐き出す。それだけです。簡単~。

置き換えによってFileLengthが変わる、などといったことはSwfCompilationUnitが面倒を見てくれるので、考えなくても大丈夫です。素晴らしい。

まとめ

RubyやPHPにはライブラリあるけれど、ドトネトにはないというのが弱点の一つでした(多分)。これで解決しましたね!さあ、C#で是非とも参入しましょう。

置換にあたって元swfファイルって変わらないから、SwfCompilationUnitをキャッシュすれば、ファイルオープンや解析のコストがなくなり、バイナリ編集のコストが純粋なバイト書き出しだけに抑えられますね。KlabのFlamixerは、初回パース時に構造を変えてMessagePackでシリアライズしておくので、というけれど、それだって読み込みやデシリアライズのコストありますものね。ASP.NETならゼロシリアライゼーションコストでキャッシュ出来るから、それ以上に期待持てそうだし、実際、軽くテストして見た限りだと、相当速くて、かなりイケテルと思いますですね。

というわけで謎社ではC#でほげもげしたい人をそのうち募集しますので暫しお待ちを。

C#でローカル変数からDictionaryを生成する

どうもPHPerです。あ、すぐC#のコード出しますので帰らないで!というわけで、PHPにはcompactというローカル変数からハッシュテーブルを作るという関数があります。割と多用します。その逆のextractという関数もありますが、そちらはカオスなのでスルー。

$name = "hogehoge";
$age = 35;

// {"name":"hogehoge", "age":35}
$dict = compact("name", "age");

へー。いいかもね。これをC#でやるには?もったいぶってもshoganaiので先に答えを出しますが、匿名型を使えばよいです。

var name = "hogehoge";
var age = 35;

// Dictionary<string, object> : {"name":"hogehoge", "age":35}
var dict = Compact(new { name, age });

はい。別にPHPと見比べても面倒くさいことは全然ないです。C#はLLですから(嘘)

匿名型は「メンバー名を指定しなかった場合、コンパイラによって、初期化に使用するプロパティと同じ名前が付けられます。」ので、それを利用すればローカル変数の名前をキャプチャできる、という至極単純な仕組み。

Reflection vs FastMember

Compactメソッドの中身ですが、リフレクションでプロパティなめてDictionaryに吐き出しているだけです。LINQを使えば瞬殺。

static Dictionary<string, object> Compact(object obj)
{
    return obj.GetType().GetProperties()
        .Where(x => x.CanRead)
        .ToDictionary(pi => pi.Name, pi => pi.GetValue(obj));
}

CanReadは、一応、匿名型以外を流し込む時のことも考慮しましょうか、的に。

さて、リフレクションを使うと実行速度がー気になってーカジュアルにー使いたくないー、のが人情というものです。個人的にはそこまで遅くもないので、そう気にしなければカジュアルに使ってもいいと思ってたりしますが、まあ気になるならShoganaiし、気にするのはいいことです。

そこで取り出すはFastMember。超高速シリアライザで有名なprotobuf-netの作者が作った、シンプルなプロパティアクセス高速化ライブラリです。

これを使って書くと

static Dictionary<string, object> Compact(object obj)
{
    var type = FastMember.TypeAccessor.Create(obj.GetType());
    return type.GetMembers().ToDictionary(x => x.Name, x => type[obj, x.Name]);
}

というように、書き方的にはそんなに違いはないですが、生成速度は数倍上昇します。TypeAccessor.Createして、GetMembersでプロパティ情報の列挙(TypeとNameがあるだけ)、PropertyInfoのGetValue的なのはインデクサを使います。FastMemberにはTypeAccessorの他にObjectAccessorがありますが、使い方は似たような感じなので略(インデクサの第一引数に対象オブジェクトを渡す必要がなくなる)。

FastMemberの仕組みですが、初回実行時にはリフレクションでプロパティ舐めています。別に魔法が存在するわけではないので、プロパティ名を取りたければ、リフレクション以外の選択肢はありません。そして取得したデータを基にしてILの動的生成を行いキャッシュし、以降のアクセス時はキャッシュから取得したアクセサ経由となるため、素のリフレクションよりも高速となっています。

よって、初回実行時に限れば、実行時間はむしろかなり遅くなります(IL生成は軽い処理ではない)。単純な平均で考えれば、1万アクセスぐらいないとペイしません(要素数による、多ければ多いほどFastMemberのほうが有利です)。という程度には、リフレクションもそんなに遅くはないです。ただまあ、初回に目をつむって以降の実行速度重視のほうがユーザー体験での満足度は高いケースがほとんどとは思われますので、個人的にはFastMember使って済ませるほうがいいな、とは思います。気分的にもスッキリしますしね。

ちなみに.NETでリフレクションにはTypeDescriptorという手段も標準で用意されていますが、アレはクソがつくほど遅いので、アレだけはやめておきましょう。少なくとも素のリフレクションを避けてあっちを使う理由がない。

名前大事

Compactという名前はPHP臭が激しいしC#的にはイミフなので、ちゃんとした名前をつけたほうがいいでしょう、ToDictionaryとか、ね。

匿名型 as Dictionary

Compact、という例を出すから何だか新しい感じがしなくもない誤魔化しでして、実のところ、ようするに、ただの匿名型→Dictionaryです。ASP.NET MVCではそこら中に見かけるアレです。ソレです。コレです。

その辺のアレコレはややニッチな Anonymous Types の使い方をまとめてみる (C# 3.0) - NyaRuRuが地球にいたころにまとまっているので見ていただくとして、以上終了。

実際問題、Dictionary<string, object>を要求するシチュエーションというのは少なくありません。パラメータ渡すところなんて、そうですよね。一々Dictionaryを使うのは、カッタルイってものです。なので、別にASP.NET MVCに限らず、↑のようなメソッドを作って、objectも受け入れられるようにしてあげるってのは、現代のC#的にはアリだと私は考えています。

// Dictionaryの初期化は割と面倒くさい
var hoge = ToaruMethod(new Dictionary<string, object>
{
    {"screen_name", "hogehoge"},
    {"count", 10},
    {"since_id", 12345}
});

// 書きやすい!素敵!抱いて!
var hoge = ToaruMethod(new
{
    screen_name = "hogehoge",
    count = 10,
    since_id = 12345
});

んね。

そうなるとメソッドの引数にobjectというものが出てしまって、安全性がショボーンになってしまいますので、やたらめったら使うのもまたアレですけれど。

匿名型がIAnonymousTypeとか、何らかのマーカーついてたらなあ、なんて思わなくもなかったりもしなかったりしましたが、こういう用途で使う時って、普通のクラスからも変換したかったりするので、匿名型に限定したほうが不便なんですね。幾ばくかの安全性は増しますが。ともあれともあれ、普通のクラスと匿名型に違いなんてない、と考えると、区別できないことは自然だから別にいいかなあ、なんて、ね、思ってます。where T : classと引数に制限つけるぐらいが丁度良いんではないでしょうか。

まとめ

PHPの良いところってどこなのか非常に悩ましい。その辺のほげもげに関してはいつか特に言いたいことはなくもないけどとくにない(去年の年末に勉強会というか技術交流会というかで、PHPの会社に行ってPHP vs C#なプレゼンはしてきましたが)。

というわけで、C#はLightweightだという話でした。ん?

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

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

Twitter:@neuecc GitHub:neuecc

Archive