Archive - Programming
RxとRedisを用いたリモートPub/Sub通信
今日から始まったBuild Insiderで、RedisとBookSleeveの記事を書きました - C#のRedisライブラリ「BookSleeve」の利用法。Redis、面白いし、Windowsで試すのも想像以上に簡単なので、是非是非試してみて欲しいです。そして何よりもBookSleeve!使うと、強制的に全てがasyncになるので、C# 5.0でのコーディングの仕方のトレーニングになる(笑)。にちじょー的にasync/awaitを使い倒すと、そこから色々アイディアが沸き上がっきます。ただたんにsyncがasyncになった、などというだけじゃなく、アプリケーションの造りが変わります。そういう意味では、Taskは結構過小評価されてるのかもしれないな、なんて最近は思っています。
さて、RedisにはPub/Sub機能がついているわけですが、Pub/Sub→オブザーバーパターン→Rx!これはティンと来た!と、いうわけで、これ、Rxに乗せられました、とても自然に。というわけで、□□を○○と見做すシリーズ、なCloudStructuresにRx対応を載せました。
追加したクラスはRedisSubject<T>。これはSubjectやAsyncSubjectなどと同じく、ISubject<T>になっています。IObservableであり、IObserverでもある代物。とりあえず、コード例を見てください。
var settings = new RedisSettings("127.0.0.1"); var subject = new RedisSubject<string>(settings, "PubSubTest"); // SubscribeはIObservable<T>なのでRxなLINQで書ける var a = subject .Select(x => DateTime.Now.Ticks + " " + x) .Subscribe(x => Console.WriteLine(x)); var b = subject .Where(x => !x.StartsWith("A")) .Subscribe(x => Console.WriteLine(x), () => Console.WriteLine("completed!")); // IObserverなのでOnNext/OnError/OnCompletedでメッセージ配信 subject.OnNext("ABCDEFGHIJKLM"); subject.OnNext("あいうえお"); subject.OnNext("なにぬねの"); Thread.Sleep(200); // 結果表示を待つ... a.Dispose(); // UnsubscribeはDisposeで subject.OnCompleted(); // OnCompletedを受信したSubscriberもUnsubscribeされる
はい、別になんてこともない極々フツーのRxのSubjectです。が、しかし、これはネットワークを通って、Redisを通して、全てのSubscriberへとメッセージを配信しています。おお~。いやまあ、コードの見た目からじゃあそういうの分からないので何も感動するところもないのですが、とにかく、本当に極々自然に普通に、しかし、ネットワークを超えます。この、見た目何も変わらずに、というところがいいところなわけです。
Subscribeする側はIObservableなので別にフツーに合成していけますし、Publishする側もIObserverなので、Subscribeにしれっと潜り込ませたりしても構わない。もう、全然、普通にインメモリなRxでやる時と同じことが、できます。
オワリ。地味だ。
う、うーん。ほ、ほらほら!!
// ネットワーク経由 var settings = new RedisSettings("xx.xxx.xxx.xxx"); var subject = new RedisSubject<DateTime>(settings, "PubSubTest"); // publisherはこちらのコード while (true) { Console.ReadLine(); var now = DateTime.Now; Console.WriteLine(now.Ticks); subject.OnNext(now); } // subscriberはこちらのコードを動かす subject.Subscribe(x => { var now = DateTime.Now; Console.Write(x.Ticks + " => " + now.Ticks); Console.WriteLine(" | " + (now - x)); });
というわけで、実際にネットワーク経由(AWS上に立てたRedisサーバーを通してる)で動かしてみた結果がこんな感じです。ネットワークを超えたことで用法は幾らでもある!夢膨らみまくり!
で、↑のコードは遅延時間のチェックを兼ねてるのですが、概ね、0.03秒ぐらい。たまにひっかかって0.5秒超えてるのがあって、ぐぬぬですが。実際のとこRedis/Linuxの設定で結構変わってくるところがあるので、その辺は煮詰めてくださいといったところでしょうか。
ともあれチャットとかなら全然問題なし、ゲームでもアクションとかタイミングにシビアじゃないものなら余裕ですね、ボードゲームぐらいなら全く問題ない。ちょっとしたMMOぐらいならいけるかも。これからはネットワーク対戦はRedisで、Rxで!!!
余談
CloudStructuresですが、.configからの設定読み込み機能も地味につけました。
<configSections> <section name="cloudStructures" type="CloudStructures.Redis.CloudStructuresConfigurationSection, CloudStructures" /> </configSections> <cloudStructures> <redis> <group name="cache"> <add host="127.0.0.1" /> <add host="127.0.0.2" port="1000" /> </group> <group name="session"> <add host="127.0.0.1" db="2" valueConverter="CloudStructures.Redis.ProtoBufRedisValueConverter, CloudStructures" /> </group> </redis> </cloudStructures>
// これで設定を読み込める var groups = CloudStructuresConfigurationSection.GetSection().ToRedisGroups();
色々使いやすくなってきて良い感じじゃあないでしょーか。
コールバック撲滅
そうそう、最後に実装の話を。元々BookSleeveのPub/Sub購読はコールバック形式です。「public Task Subscribe(string key, Action<string, byte[]> handler)」handlerはstringがキー, byte[]が送られてくるオブジェクトを指します。コールバック is ダサい。コールバック is 扱いにくい。ので、コールバックを見かけたらObservableかTaskに変換することを考えましょう!それがC# 5.0世代の常識です!
というわけで、以下のようにして変換しました。
public IDisposable Subscribe(IObserver<T> observer) { var channel = Connection.GetOpenSubscriberChannel(); var disposable = System.Reactive.Disposables.Disposable.Create(() => { channel.Unsubscribe(Key).Wait(); }); // ここが元からあるコールバック channel.Subscribe(Key, (_, xs) => { using (var ms = new MemoryStream(xs)) { var value = RemotableNotification<T>.ReadFrom(ms, valueConverter); value.Accept(observer); // この中でobserverのOnNext/OnError/OnCompletedが叩かれる if (value.Kind == NotificationKind.OnError || value.Kind == NotificationKind.OnCompleted) { disposable.Dispose(); // ErrorかCompletedでもUnsubscribeしますん } } }).Wait(); return disposable; // もしDisposableが呼ばれたらUnsubscribeしますん }
こんなふぅーにRxで包むことで、相当使いやすさがアップします。感動した。
CloudStructures - ローカルとクラウドのデータ構造を透過的に表現するC# + Redisライブラリ
- C# - 13.04/05
というものを作りました。インストールはNuGetから。
何を言ってるのかヨクワカラナイので、まずはコード例を。
// こんなクラスがあるとして public class Person { public string Name { get; private set; } public List<Person> Friends { get; private set; } public Person(string name) { Name = name; Friends = new List<Person>(); } } // こんなのがいるとして var sato = new Person("さとう"); // 人を足す sato.Friends.Add(new Person("やまだ")); sato.Friends.Add(new Person("いとう")); // 件数数える var friendCount = sato.Friends.Count;
これは普通にローカルで表現する場合です。実に普通です。では、次。
// RedisServerの設定の表現 public static class RedisServer { public static readonly RedisSettings Default = new RedisSettings("127.0.0.1"); } // こんなクラスがあるとして public class Person { public string Name { get; private set; } public RedisList<Person> Friends { get; private set; } public Person(string name) { Name = name; Friends = new RedisList<Person>(RedisServer.Default, "Person-" + Name); } } // こんなのがいるとして var sato = new Person("さとう"); // 人を足す await sato.Friends.AddLast(new Person("やまだ")); await sato.Friends.AddLast(new Person("いとう")); // 件数数える var friendCount = await sato.Friends.GetLength();
この場合、Redisを通してサーバー上にデータは保存されています。ですが、操作感覚はローカルにあるものとほぼほぼ同じです。違いは全ての操作が非同期なので、awaitするぐらい。
IAsyncList
これは、Actor Framework for Windows AzureのDistributed Collectionsに影響を受けています。ActorFxのそれは、SOURCE CODEを落としてdocsフォルダの Distributed Collections using the ActorFx.docx に色々書いてあって面白いので必読です。
そして、ActorFxではSystem.Cloud.Collectionsとして(System名前空間!)、現状、以下のようなインターフェイスが定義されています(まだ変更の可能性大いにあり)。
namespace System.Cloud.Collections { public interface IAsyncCollection<T> : IObservable<T> { Task<int> CountAsync { get; } Task<bool> IsReadOnlyAsync { get; } Task AddAsync(T item); Task ClearAsync(); Task<bool> ContainsAsync(T item); Task CopyToAsync(T[] array, int arrayIndex); Task<bool> RemoveAsync(T item); } public interface IAsyncList<T> : IAsyncCollection<T> { Task<T> GetItemAsync(int index); Task SetItemAsync(int index, T value); Task<int> IndexOfAsync(T item); Task InsertAsync(int index, T item); Task RemoveAtAsync(int index); // Less chatty versions Task AddAsync(IEnumerable<T> items); Task RemoveRangeAsync(int index, int count); } public interface IAsyncDictionary<TKey, TValue> : IAsyncCollection<KeyValuePair<TKey, TValue>> { Task<TValue> GetValueAsync(TKey key); Task SetValueAsync(TKey key, TValue value); Task<Tuple<bool, TValue>> TryGetValueAsync(TKey key); // No AddAsync - use SetValueAsync instead. We have no atomic operation to add iff a value is not in the dictionary. Task<bool> ContainsKeyAsync(TKey key); Task<bool> RemoveAsync(TKey key); // Bulk operations Task<ICollection<TValue>> GetValuesAsync(IEnumerable<TKey> keys); Task SetValuesAsync(IEnumerable<TKey> keys, IEnumerable<TValue> values); Task RemoveAsync(IEnumerable<TKey> keys); ICollection<TKey> Keys { get; } ICollection<TValue> Values { get; } } }
わくわくしてきません?私はこの定義を見た瞬間に衝撃を受けました。RxのIObservable<T>を見た時と同程度の衝撃かもわからない。Ax(ActorFx)の実装としてはCloudList, CloudDictionary, CloudStringDictionaryがありますが(基盤としてAzure Table)、見てすぐにRedisと結びついた。Redisの持つデータ構造、List, Hash, Set, SortedSetってこれじゃないか!って。こういう風に表現されたらどれだけ素敵な見た目になるか……!
Strings, Set, SortedSet, List, Hash, その他
というわけで、最初の例ではRedisListだけ出しましたが、StringsもSetもSortedSetもHashもあります。また、HashClassやMemoizedRedisStringといった特殊なものも幾つか用意してあります。
// フィールドに持たなくても、ふつーにRedisClient的に使ってもいいよ var client = new RedisString<string>(RedisServer.Default, "toaru-key"); await client.Set("あいうえお!", expirySeconds: TimeSpan.FromMinutes(60).TotalSeconds); // RedisClassはRedisのHash構造をクラスにマッピングするもの var hito = new RedisClass<Hito>(RedisServer.Default, "hito-1"); await hito.SetField("Name", "やまもと"); await hito.Increment("Money", 100); var localHito = await hito.GetValue(); // Cloud -> Localに落とす、的ないめーぢ
実際色々あるので見て回ってください!
ConnectionManagement
基盤的な機能として、BookSleeveの接続管理を兼ねています。
// Redisの設定を表す var settings = new RedisSettings(host: "127.0.0.1", port: 6379, db: 0); // BookSleeveはスレッドセーフで単一のコネクションを扱う // コネクションを一つに保ったり切断されていた場合の再接続などをしてくれる var conn = settings.GetConnection(); // 複数接続はRedisGroupで管理できる var group = new RedisGroup(groupName: "Cache", settings: new[] { new RedisSettings(host: "100.0.0.1", port: 6379, db: 0), new RedisSettings(host: "105.0.0.1", port: 6379, db: 0), }); // keyを元に分散先のサーバーを決める(デフォルトはMD5をサーバー台数で割って決めるだけの単純な分散) var conn = group.GetSettings("hogehoge-100").GetConnection(); // シリアライザはデフォルトではJSONとProtoBufを用意(未指定の場合はJSON) new RedisSettings("127.0.0.1", converter: new JsonRedisValueConverter()); new RedisSettings("127.0.0.1", converter: new ProtoBufRedisValueConverter());
って、ここまでBookSleeveの説明がなかった!BookSleeveはRedisのライブラリで、非同期の操作のみを提供しています。CloudStructuresのRedis操作はBookSleeveに全部委ねてます。というかぶっちゃけ、かなり単純なラップがほとんどだったりします(!)。見せ方を変えただけ、です、よーするところ。
んで、BookSleeveは斬新で非常に良いライブラリなのですけれど、操作が本当にプリミティブなものしかないので(全てのGetとSetがstringとbyte[]しかない、とかね)、ある程度、自分で作りこんでやらないと全く使えません。なので、この部分だけでも、結構使えるかなって思います。
Next
個人的にはすっごく面白いと思ってます。見せ方の違いでしかないわけですが、しかし、その見せ方の違いというのが非常に大事なのです。直感的、ですが、ある種奇抜なデザインなので、戸惑うとは思います。異色度合いで言ったら、以前に私の作ったReactivePropertyと同程度に異色かな、と。だからこそ、凄く大きな可能性を感じませんか?
ちなみに、これは(いつものように)コンセプト止まりじゃなくて、実際に使う予定アリなので、しっかり育ててく気満々です。是非、試してみてもらえると嬉しいですね。
C#のラムダ式でyieldっぽい何かをawaitで代用する方法
- C# - 13.03/27
C#がインラインでyield書けないならawait使えばいいじゃない。と、偉い人は言いました。というわけで、こそこそっと開発がされているIxに、面白い機能が入りました(開発リポジトリ上だけなのでNuGetからダウンロードしても、まだ入ってません)。こんなのです。
var hoge = "あいうえお"; var seq = EnumerableEx.Create<int>(async Yield => { await Yield.Return(10); await Yield.Return(100); hoge = "ふがふが"; // インラインで書けるのでお外への副作用が可能 await Yield.Return(1000); }); foreach (var item in seq) { Console.WriteLine(item); // 10, 100, 1000 } Console.WriteLine(hoge); // ふがふが
そう、yield return(っぽい何か)がラムダ式で、メソッド外部に出すことなく書けてしまうのです!これは素敵ですね?い、いや、なんか何やってるのか分からなすぎて黒魔術怖いって雰囲気も漂ってますね!しかし面白いものは面白いので、実装見ましょう。
add types iyielder, iawaitable, and iawait; add support for creating ienumerable from action
public static class EnumerableEx { public static IEnumerable<T> Create<T>(Action<IYielder<T>> create) { if (create == null) throw new ArgumentNullException("create"); foreach (var x in new Yielder<T>(create)) { yield return x; } } } public interface IYielder<in T> { IAwaitable Return(T value); IAwaitable Break(); } public interface IAwaitable { IAwaiter GetAwaiter(); } public interface IAwaiter : ICriticalNotifyCompletion { bool IsCompleted { get; } void GetResult(); } public class Yielder<T> : IYielder<T>, IAwaitable, IAwaiter, ICriticalNotifyCompletion { private readonly Action<Yielder<T>> _create; private bool _running; private bool _hasValue; private T _value; private bool _stopped; private Action _continuation; public Yielder(Action<Yielder<T>> create) { _create = create; } public IAwaitable Return(T value) { _hasValue = true; _value = value; return this; } public IAwaitable Break() { _stopped = true; return this; } public Yielder<T> GetEnumerator() { return this; } public bool MoveNext() { if (!_running) { _running = true; _create(this); } else { _hasValue = false; _continuation(); } return !_stopped && _hasValue; } public T Current { get { return _value; } } public IAwaiter GetAwaiter() { return this; } public bool IsCompleted { get { return false; } } public void GetResult() { } public void OnCompleted(Action continuation) { _continuation = continuation; } public void UnsafeOnCompleted(Action continuation) { _continuation = continuation; } }
ほぅ、わけわからん?若干トリッキーなので、順を追っていきますか。asyncについて考える前に、まず、基本的なforeachのルール。実はIEnumerableを実装している必要はなくて、GetEnumeratorという名前のメソッドがあればいい。同様にMoveNextとCurrentというメソッドがあればIEnumerator扱いされる。なので、foreach (var x in new Yielder
あと、インターフェイスが、IAwaitableとかいっぱい再定義されてて、ワケワカランのですけれど、そこまで意味あるわけじゃないです。これはラムダ式にYielderを渡すわけですが、そこで内部の諸々が呼べちゃうのはイクナイので隠ぺいする、程度の意味合いでしかないので、これを実装するのにインターフェイスの再定義が必要!というわけは全然ないです。
で、コアになるのはMoveNext。
public bool MoveNext() { if (!_running) { _running = true; _create(this); } else { _hasValue = false; _continuation(); } return !_stopped && _hasValue; }
そもそもyield returnで生成されたメソッドが最初に実行されるのは、GetEnumeratorのタイミングではなく、GetEnumeratorされて最初のMoveNextが走った時、なので、ここが本体になっているのはセマンティクス的に問題なし。
!_runnningは初回実行時の意味で、ここで_create(this)、によってラムダ式で書いた本体が走ります。
var seq = EnumerableEx.Create<int>(async Yield => { await Yield.Return(10); // ↑のとこがまず実行され始める await Yield.Return(100); await Yield.Return(1000); }); public IAwaitable Return(T value) { _hasValue = true; _value = value; return this; }
まずはメソッド実行なのでReturn。これは値をセットして回っているだけ。そしてIAwaitableを返し、await。ここで流れは別のところに行きます。
public bool IsCompleted { get { return false; } } public void GetResult() { } public void OnCompleted(Action continuation) { _continuation = continuation; } public void UnsafeOnCompleted(Action continuation) { _continuation = continuation; }
まず完了しているかどうかの確認(IsCompleted)が走りますが、この場合は常にfalseで(そうしないと終了ということになってラムダ式のほうに戻ってこなくなっちゃう)。これによってUnsafeOnCompleted(ICriticalNotifyCompletionが実装されている場合はこっちが走る)でcontinuation(メソッド本体)が走る。で、「次回用」に変数保存して、MoveNext(create(this)したとこの位置)に戻ってくる。あとはMoveNextがtrueを返すのでCurrentで値取得して、それがyield returnされる。
二度目のMoveNextでは
public bool MoveNext() { if (!_running) { _running = true; _create(this); } else { _hasValue = false; _continuation(); // ここが呼び出されて } return !_stopped && _hasValue; } var seq = EnumerableEx.Create<int>(async Yield => { await Yield.Return(10); // ここから再度走り出す await Yield.Return(100); await Yield.Return(1000); });
といった感じになって、以下繰り返し。良く出来てますね!ていうか、asyncなのに非同期全く関係ないのが素敵。そう、asyncは別に非同期関係なく使えちゃうわけです。ここ大事なので繰り返しましょう。asyncは別に非同期関係なく使うことができます。
まとめ
async、フツーに使うのもそろそろ飽きてき頃だと思うので、弄って遊ぶのは大正義。実際に投下しだすかどうかは判断次第。あと、↑のはまだ大事な要素ができていないので絶対使いませんけれど。大事な要素はIDisposableであること。foreachで大事だと思ってるのはDisposeしてくれるとこ!だとも思っているので、それが実現できてないのはナイナー、と。
そういえばAsyncについてですが、3/30の土曜にRoom metro #15でHttpClient(非同期の塊!)について話すので、まだ残席ありますので良ければお越しくだしあー。
並列実行とSqlConnection
- C# - 13.03/09
どうも、ParallelやThreadな処理が苦痛度100なペチパーです。嘘です。空前のThreadLocalブームが来てたり来てなかったりする昨今です。あ、謎社の宣伝しますとグリーとグラニ、「GREE」におけるソーシャルゲームの提供などについて戦略的業務提携に合意というわけで、ぐりとぐら、としかいいようがない昨今でもあります。その日に開催されていたGREEプラットフォームカンファレンスでは、謎社はC#企業になる!と大宣言したので、ちゃんと実現させていきたいところです、いや、むしろそのためにフル回転しています。
そんな宣伝はおいておいて本題なのですけれど、SQL。データベース。大量にクエリ発行したい時など、パラレル実行したいの!インサートだったら当然BulkInsertが一番早いんですが、Updateとかね。シンドイんだよね。あとUpsert(Merge/ON DUPLICATE KEY UPDATE)とかも使っちゃったりしてね。そんなわけで、お手軽お気楽な手法としてはParallelがありますねー、.NETはこの辺本当に楽なんだよねー、ぴーHPはシラネ。
で、実際パラレールにこんな感じに書くと……
using (var connection = new SqlConnection("接続文字列")) { connection.Open(); Parallel.For(1, 1000, x => { var _ = connection.Query<DateTime>("select current_timestamp").First(); // Dapper }); }
落ちます。理由は単純明快でSqlConnectionはスレッドセーフじゃないから。というわけで、やるなら
Parallel.For(1, 1000, x => { using (var connection = new SqlConnection("接続文字列")) { connection.Open(); var _ = connection.Query<DateTime>("select current_timestamp").First(); // Dapper } });
となります、これなら絶対安全。でも、スレッドって基本的にコアの数とちょびっとしか立てられないわけだし、連続的に実行しているのだから、たとえコネクションプール行きだとかなんだりであっても、一々コネクションを開いて閉じてをするよりも、開きっぱで行きたいよね。
ようするにSqlConnectionがスレッドセーフじゃないからいけない。これはどこかで聞いたような話です。先日C#とランダムで出したThreadLocalの出番ではないでしょうか!
ThreadLocal
というわけでスレッドセーフなSqlConnectionを作りましょう、ThreadLocalを使って。
using (var connection = new ThreadLocal<SqlConnection>(() => { var conn = new SqlConnection("接続文字列"); conn.Open(); return conn; })) { Parallel.For(1, 1000, x => { var _ = connection.Value.Query<DateTime>("select current_timestamp").First(); // Dapper }); }
new SqlConnectionがThreadLocalに変わっただけのお手軽さ。これで、安全なんですって!本当に?本当に。で、実際こうして速度はどうなのかというと、私の環境で実行したところ、シングルスレッドで16秒、毎回new SqlConnectionするParallelで5秒、ThreadLocalなParallelで2秒でした。これは圧勝。幸せになれそう。
Disposeを忘れない、或いは忘れた
でも↑のコードはダメです。ダメな理由は、コネクションをDisposeしてないからです。ThreadLocalのDisposeは、あくまでThreadLocalのDisposeなのであって、中身のDisposeはしてくれてないのです。ここ忘れると悲劇が待ってます。でもFactoryで作ってる上にThreadで一意なValue、どうやってまとめてDisposeすればいいの!というと、trackAllValuesというオプションを有効にすると簡単に実現できます。
using (var connection = new ThreadLocal<SqlConnection>(() => { var conn = new SqlConnection("接続文字列"); conn.Open(); return conn; } , trackAllValues: true)) // ThreadLocalの.Valuesプロパティの参照を有効化する { Parallel.For(1, 1000, x => { var _ = connection.Value.Query<DateTime>("select current_timestamp").First(); // Dapper }); // 生成された全てのConnectionを一括Dispose foreach (var item in connection.Values.OfType<IDisposable>()) item.Dispose(); }
このtrackAllValuesが可能なThreadLocalは.NET 4.5からです。それ以前の人は、残念でした……。謎社は遠慮なく.NET 4.5を使いますので全然問題ありません(
もう一つまとめて
とはいえ、なんか面倒くさいので、ちょっとラップしませう、以下のようなクラスを用意します。
public static class DisposableThreadLocal { public static DisposableThreadLocal<T> Create<T>(Func<T> valueFactory) where T : IDisposable { return new DisposableThreadLocal<T>(valueFactory); } } public class DisposableThreadLocal<T> : ThreadLocal<T> where T : IDisposable { public DisposableThreadLocal(Func<T> valueFactory) : base(valueFactory, trackAllValues: true) { } protected override void Dispose(bool disposing) { var exceptions = new List<Exception>(); foreach (var item in this.Values.OfType<IDisposable>()) { try { item.Dispose(); } catch (Exception e) { exceptions.Add(e); } } base.Dispose(disposing); if (exceptions.Any()) throw new AggregateException(exceptions); } }
これを使うと
using (var connection = DisposableThreadLocal.Create(() => { var conn = new SqlConnection("接続文字列"); conn.Open(); return conn; })) { Parallel.For(1, 1000, x => { var _ = connection.Value.Query<DateTime>("select current_timestamp").First(); // Dapper }); }
といったように、超シンプルに書けます。うん。いいね。
それAsync?
Asyncでドバッと発行してTask.WhenAll的なやり方も、接続が非スレッドセーフなのは変わらなくて、結構やりづらいんですよ……。それで、なんか色々細かくawaitしまくりで逆に遅くなったら意味ないし。それならドストレートに行ったほうがいいのでは感が若干ある。どうせThreadなんてそこそこ余ってるんだから(←そうか?)局所的にParallelってもいいぢゃないと思いたい、とかなんとかかんとか。
非.NET 4.5の場合
Parallel.For, ForEachに関しては、localInit, localFinallyというタスク内で一意になる変数を利用したオーバーロードを利用して、似たような雰囲気で書けます。正確には同じ挙動ではないですが、まぁまぁ悪くない結果が得られます。
Parallel.For(1, 1000, () => { // local init var conn = new SqlConnection("接続文字列"); conn.Open(); return conn; }, (x, state, connection) => { var _ = connection.Query<DateTime>("select current_timestamp").First(); // Dapper return connection; }, (connection) => { // local finally connection.Dispose(); });
オーバーロードが結構地獄でシンドイですね!ここも簡単にラップしたものを作りましょう。
public static class ParallelEx { public static ParallelLoopResult DisposableFor<TDisposable>(long fromInclusive, long toExclusive, Func<TDisposable> resourceFactory, Action<long, ParallelLoopState, TDisposable> body) where TDisposable : IDisposable { return Parallel.For(fromInclusive, toExclusive, resourceFactory, (item, state, resource) => { body(item, state, resource); return resource; }, disp => disp.Dispose()); } public static ParallelLoopResult DisposableForEach<T, TDisposable>(IEnumerable<T> source, Func<TDisposable> resourceFactory, Action<T, ParallelLoopState, TDisposable> body) where TDisposable : IDisposable { return Parallel.ForEach(source, resourceFactory, (item, state, resource) => { body(item, state, resource); return resource; }, disp => disp.Dispose()); } }
こうしたものを作れば、
ParallelEx.DisposableFor(1, 1000, () => { var conn = new var conn = new SqlConnection("接続文字列"); conn.Open(); return conn; }, (x, state, connection) => { var _ = connection.Query<DateTime>("select current_timestamp").First(); // Dapper });
まぁまぁ許せる、かな?
C#とランダム
- C# - 13.03/06
古くて新しいわけはない昔ながらのSystem.Randomのお話。Randomのコンストラクタは二種類あって、seed引数アリの場合は必ず同じ順序で数値を返すようになります。
// 何度実行しても同じ結果 var rand = new Random(0); Console.WriteLine(rand.Next()); // 1559595546 Console.WriteLine(rand.Next()); // 1755192844 Console.WriteLine(rand.Next()); // 1649316166
例えばゲームのリプレイなどは、ランダムだけど同一の結果が得られることを期待したいわけなので、大事大事ですね。(とはいえ、Windows-CLIとLinux-monoでは結果が違ったりするので、マルチプラットフォームでの共有などという場合は、別策を取ったほうがよさそうです)。何も渡さない場合はseedとしてEnvironment.TickCountが渡されます。精度はミリ秒。ということは、ですね、例えばループの中でRandomをnewするとですよ、
for (int i = 0; i < 100; i++) { var rand = new Random(); Console.WriteLine(rand.Next()); }
マシンスペックにもよりますが、私の環境では30個ぐらい同じ数値が出た後に、別の、また30個ぐらい同じ数値が続き……となりました。何故か、というと、seedがEnvironment.TickCountだからで、ループ内といったようなミリ秒を超える超高速の状態で生成されている時は、seed値が同じとなってしまうから。なので、正しくは
var rand = new Random(); for (int i = 0; i < 100; i++) { Console.WriteLine(rand.Next()); }
といったように、ループの外に出す必要性があります。
ランダムなランダム
では、ランダムなランダムが欲しい場合は。例えばマルチスレッド。そうでなくても、例えばループの外に出す(直接的でなくてもメソッドの中身がそうなっていて、意図せず使われてしまう可能性がある)のを忘れてしまうのを強制的に避ける場合。もしくは、別にマルチスレッドは気を付けるよー、といっても、ASP.NETとか複数リクエストが同時に走るわけで、同タイミングでのRandom生成になってしまう可能性は十分にある。そういう時は、RandomNumberGeneratorを使います。
using (var rng = new RNGCryptoServiceProvider()) { // 厳密にランダムなInt32を作る var buffer = new byte[sizeof(int)]; rng.GetBytes(buffer); var seed = BitConverter.ToInt32(buffer, 0); // そのseedを基にRandomを作る var rand = new Random(seed); }
これでマルチスレッドでも安全安心だ!勿論、RNGCryptoServiceProviderはちょっとコスト高。でも、全然我慢できる範囲ではある。お終い。
ThreadLocal
でも、これって別にスレッドセーフなランダムが欲しいってだけなわけだよね、それなのにちょっとした、とはいえ、コスト高を背負うのって馬鹿げてない?そこで出てくるのがThreadLocal<T>、.NET 4.0以降ですが、スレッド単位で一意な変数を宣言できます。それを使った、Jon Skeet氏(ゆーめーじん)の実装は
public static class RandomProvider { private static int seed = Environment.TickCount; private static ThreadLocal<Random> randomWrapper = new ThreadLocal<Random>(() => new Random(Interlocked.Increment(ref seed)) ); public static Random GetThreadRandom() { return randomWrapper.Value; } }
なるほどねー!これなら軽量だし、とってもセーフで安心できるしイイね!もし複数スレッドで同時タイミングで初期化が走った時のために、Interlocked.Incrementで、必ず違う値がseedになるようになってるので、これなら色々大丈夫。
マルチスレッド→マルチサーバー
けれど、大丈夫なのは、一台のコンピューターで完結する時だけの時の話。クラウドでしょ!サーバー山盛りでしょ!な時代では、サーバーをまたいで同時タイミングなEnvironment.TickCountで初期化されてしまう可能性が微レ存。というわけで、Environment.TickCountに頼るのは完全に安全ではない。じゃあ、そう、合わせ技で行けばいいじゃない、seedは完全ランダムで行きましょう。
public static class RandomProvider { private static ThreadLocal<Random> randomWrapper = new ThreadLocal<Random>(() => { using (var rng = new RNGCryptoServiceProvider()) { var buffer = new byte[sizeof(int)]; rng.GetBytes(buffer); var seed = BitConverter.ToInt32(buffer, 0); return new Random(seed); } }); public static Random GetThreadRandom() { return randomWrapper.Value; } }
これで、軽量かつ安全安泰なRandomが手に入りました。めでたしめでたし。
AsyncOAuth - C#用の全プラットフォーム対応の非同期OAuthライブラリ
- C# - 13.02/27
待ち望まれていたHttpClientがPortable Class Library化しました、まだBetaだけどね!というわけで、早速PCL版のHttpClientをベースにしたOAuthライブラリを仕上げてみました。ポータブルクラスライブラリなので、.NET 4.5は勿論、Windows Phone 7.5, 8, Windows Store Apps, Silverlight, それと.NET 4.0にも対応です。
前身のReactiveOAuthがTwitterでしかロクにテストしてなくてHatenaでズタボロだったことを反省し、今回はSampleにTwitterとHatenaを入れておきました&どっちでもちゃんと正常に動きます。なお、完全に上位互換なので、ReactiveOAuthはObsoleteです。それと、ライブラリのインストールはNuGet経由でのみの提供です。
PM> Install-Package AsyncOAuth -Pre
もしくはPreReleaseを表示に含めてGUIから検索してください。
AsyncOAuth is not a new library
AsyncOAuthの実態はOAuthMessageHandlerというDelegatingHandlerです。
var client = new HttpClient(new OAuthMessageHandler("consumerKey", "consumerSecret", new AccessToken("accessToken", "accessTokenSecret"))); // 上のだとnewの入れ子が面倒なので短縮形、戻り値は上のと同じ var client = OAuthUtility.CreateOAuthClient("consumerKey", "consumerSecret", new AccessToken("accessToken", "accessTokenSecret"));
こうなっていると何がいいか、というと、全ての操作がHttpClient標準通りなのです。
// Get var json = await client.GetStringAsync("http://api.twitter.com/1.1/statuses/home_timeline.json?count=" + count + "&page=" + page); // Post var content = new FormUrlEncodedContent(new[] { new KeyValuePair<string, string>("status", status) }); var response = await client.PostAsync("http://api.twitter.com/1.1/statuses/update.json", content); var json = await response.Content.ReadAsStringAsync(); // Multi Post var content = new MultipartFormDataContent(); content.Add(new StringContent(status), "\"status\""); content.Add(new ByteArrayContent(media), "media[]", "\"" + fileName + "\""); var response = await client.PostAsync("https://upload.twitter.com/1/statuses/update_with_media.json", content); var json = await response.Content.ReadAsStringAsync();
もうおれおれクライアントのAPIを覚える必要はありません。これからの標準クライアントであるHttpClientの操作だけを覚えればいいのです。
コンセプトはHttpClientチームから掲示されているサンプルコードExtending HttpClient with OAuth to Access Twitterどおりですが、このサンプルコードは本当にただのコンセプトレベルなサンプルで、そのまんまじゃ使えないので、ちゃんと実用的なOAuthライブラリとして叩き直したのがAsyncOAuthになります。DelegatingHandlerというのは、リクエストを投げる直前をフックするものなので、そこでOAuth用の認証を作っているわけです。
イニシャライズ
使う場合は、必ず最初にHMAC-SHA1の計算関数をセットしなければなりません。何故か、というと、ポータブルクラスライブラリには現状、暗号系のライブラリが含まれていなくて、その部分は含むことができないからです。外部から差し込んでもらうことでしか対処できない、という。ご不便おかけしますが、的な何か。そのうち含まれてくれるといいなあ、って感じですねえ。それまでは、以下のコードをApp.xaml.csとかApplication_Startとか、初回の本当に最初の最初に呼ばれるところに、コピペってください。
// WinRT以外(Silverlight, Windows Phone, Consoleなどなど) OAuthUtility.ComputeHash = (key, buffer) => { using (var hmac = new HMACSHA1(key)) { return hmac.ComputeHash(buffer); } }; // Windows Store App(めんどうくせえええええ) AsyncOAuth.OAuthUtility.ComputeHash = (key, buffer) => { var crypt = Windows.Security.Cryptography.Core.MacAlgorithmProvider.OpenAlgorithm("HMAC_SHA1"); var keyBuffer = Windows.Security.Cryptography.CryptographicBuffer.CreateFromByteArray(key); var cryptKey = crypt.CreateKey(keyBuffer); var dataBuffer = Windows.Security.Cryptography.CryptographicBuffer.CreateFromByteArray(buffer); var signBuffer = Windows.Security.Cryptography.Core.CryptographicEngine.Sign(cryptKey, dataBuffer); byte[] value; Windows.Security.Cryptography.CryptographicBuffer.CopyToByteArray(signBuffer, out value); return value; };
また、使いかたの詳しいサンプルは、GitHub上のソースコードからAsyncOAuth.ConsoleAppの中にTwitter.csとHatena.csがあるので、それを見てもらえればと思います。AccessToken取得までの、認証系の説明はここには書きませんが(OAuthAuthorizerという特別に用意してあるものを使う)、その具体的な書き方が乗っています。特にHatenaの認証はTwitterに比べるとかなりメンドーくさいので、メンドーくさい系のOAuthが対象の場合は参考になるかと思います。
ストリーミング、Single vs Multiple、或いはRxの再来
勿論、TwitterのストリーミングAPIにも対応できます。以下のようなコードを書けばOK。
public async Task GetStream(Action<string> fetchAction) { var client = OAuthUtility.CreateOAuthClient(consumerKey, consumerSecret, accessToken); client.Timeout = System.Threading.Timeout.InfiniteTimeSpan; // ストリーミングなのでTimeoutで切られないよう設定しておくこと using (var stream = await client.GetStreamAsync("https://userstream.twitter.com/1.1/user.json")) using (var sr = new StreamReader(stream)) { while (!sr.EndOfStream) { var s = await sr.ReadLineAsync(); fetchAction(s); } } }
ほぅ、Actionですか、コールバックですか……。ダサい。使い勝手悪い。最悪。しかし、じゃあ何返せばいいんだよ!ということになる。Taskは一つしか返せない、でもストリーミングは複数。うーん、うーん、と、そこでIObservable<T>の出番です。Reactive Extensionsを参照して、以下のように書き換えましょう。
public IObservable<string> GetStream() { return Observable.Create<string>(async (observer, ct) => { try { var client = OAuthUtility.CreateOAuthClient(consumerKey, consumerSecret, accessToken); client.Timeout = System.Threading.Timeout.InfiniteTimeSpan; // ストリーミングなのでTimeoutで切られないよう設定しておくこと using (var stream = await client.GetStreamAsync("https://userstream.twitter.com/1.1/user.json")) using (var sr = new StreamReader(stream)) { while (!sr.EndOfStream && !ct.IsCancellationRequested) { var s = await sr.ReadLineAsync(); observer.OnNext(s); } } } catch (Exception ex) { observer.OnError(ex); return; } if (!ct.IsCancellationRequested) { observer.OnCompleted(); } }); }
var client = new TwitterClient(consumerKey, consumerSecret, new AccessToken(accessTokenKey, accessTokenSecret)); // subscribe async stream var cancel = client.GetStream() .Skip(1) .Subscribe(x => Console.WriteLine(x)); Console.ReadLine(); cancel.Dispose(); // キャンセルはDisposeで行う
といったように、自然にRxと繋げられます。コールバックのObservable化はObservable.Createで、そんなに難しくはない(ただしOnNext以外にちゃんとOnError, OnCompletedも記述してあげること)です。キャンセル対応に関しては、ちゃんとCancelleationToken付きのオーバーロードで行いましょう。そうしないと、Subscribeの解除はされていても、内部ではループが延々と動いている、といったような状態になってしまいますので。
ともあれ、asyncやCancellationTokenとRxがスムースに結合されていることは良くわかるかと思います。完璧!
こういった、単発の非同期はTaskで、複数の非同期はIObservable<T>で行う、というガイドはTPLチームからも示されています。先日のpfxteamからのスライドから引用すると(ちなみにこのスライドはTask系の落とし穴などが超丁寧に書かれているので必読!)
といった感じです。んねー。
まとめ
ReactiveOAuthはオワコン。HttpClient始まってる。Reactive Extensions自体は終わってない、むしろ始まってる。というわけで、色々と使いこなしていきましょう。
追記:リリースから一晩開けて、POST周りを中心にバグが発見されていてお恥ずかしい限りです。あらかた修正したとは思うのですが(NuGetのバージョンは随時上げています)、怪しい挙動見つけたら報告下さると嬉しいです。勿論、GitHubなのでPull Requestでも!
C#でぬるぽを回避するどうでもいい方法
- C# - 13.02/19
どうもペチパーです。嘘です逃げないで。まあ、どうでもいいPHPの例をまずは出しませう。あ、逃げないで、PHPの話はすぐやめるんで。
// ネストしてる配列 $hoge["huga"]["hage"]["tako"] = "なのなの"; // なのなの $v = isset($hoge["huga"]["hage"]["tako"]) ? $hoge["huga"]["hage"]["tako"] : "ない"; // 途中で欠けてる配列 $hoge["huga"] = "なのなの"; // ない $v = isset($hoge["huga"]["hage"]["tako"]) ? $hoge["huga"]["hage"]["tako"] : "ない";
全体的にキモいんですが、まあ無視してもらって、何が言いたいか、と言うとisset。これはネストしてる部分も一気に評価してくれるのです。フツーの関数だと常識的に考えて評価は先に内側で行うので配列の境界外で死ぬんですが、issetは関数みたいな見た目だけど実は言語構文なのだ!キモチワルイ。ともあれ、そんなわけでネストしてるものの有無を一気にチェックできるのです。
で、PHPのこと書いてるとサイト違うので、C#の話をしませう。
C#でネストネスト
PHPは何でも連想配列なのですが、C#だったらクラスのプロパティでしょうか。以下のようなシチュエーション。
// こういうドカドカした構造があるとして class Hoge { public Huga Prop1 { get; set; } } class Huga { public Hage Prop2 { get; set; } } class Hage { public string Prop3 { get; set; } } // こっちプログラム本体 var hoge = new Hoge(); // とちゅーでヌルぽが発生すると死んじゃうんの回避が醜悪! var prop3 = (hoge != null && hoge.Prop1 != null && hoge.Prop1.Prop2 != null && hoge.Prop1.Prop2.Prop3 != null) ? hoge.Prop1.Prop2.Prop3 : null;
!=nullの連鎖が面倒くさいですぅー。なんとかしてくださいぃー。ぴーHPに負けてるんじゃないですかぁー?とか言われてないですが言われてるってことにするので、しょうがないからエレガントな解決策を探してあげました、誰にも頼まれてませんが!
Love ExpressionTree
こーいう風に書ければいいんでしょ!下のhoge.GetValueOrDefaultってとこです。
// こんなHogeがあるとして var hoge = new Hoge(); // すっきり! var value = hoge.GetValueOrDefault(x => x.Prop1.Prop2.Prop3); Console.WriteLine(value == null); // true // 中身が詰まってたら hoge = new Hoge { Prop1 = new Huga { Prop2 = new Hage { Prop3 = "ほげ!" } } }; var value2 = hoge.GetValueOrDefault(x => x.Prop1.Prop2.Prop3); Console.WriteLine(value2); // ほげ!
すっごくスッキリしますね!イイね!
で、どーやってるかというと、ExpressionTreeでグルグルですよ。
public static class MonyaMonyaExtensions { public static TR GetValueOrDefault<T, TR>(this T value, Expression<Func<T, TR>> memberSelector) where T : class { var expression = memberSelector.Body; var memberNames = new List<string>(); while (!(expression is ParameterExpression)) { if ((expression is UnaryExpression) && (expression.NodeType == ExpressionType.Convert)) { expression = ((UnaryExpression)expression).Operand; continue; } var memberExpression = (MemberExpression)expression; memberNames.Add(memberExpression.Member.Name); expression = memberExpression.Expression; } object value2 = value; for (int i = memberNames.Count - 1; i >= 0; i--) { if (value2 == null) return default(TR); var memberName = memberNames[i]; dynamic info = value2.GetType().GetMember(memberName)[0]; value2 = info.GetValue(value2); } return (TR)value2; } }
はい。というわけで、一つ言えるのは、これ、あんま速くないんで実用には使わないでくださいね、あくまでネタです、ネタ。
もにゃど
それもにゃど、という人はLINQでMaybeモナドでも検索しませう。既出なので私は書きません。
連打対策などりの同時アクセス禁止機構
- C# - 13.02/08
ゆるふわ連打対策のお時間です。連打されて無限にあーーーーーーー!という悲鳴を上げたり上げなかったりするとかしないとしても、何らかの対策したいよね!ということで、ASP.NETのお話。Application.Lock使ってSessionに、というのは複数台数あったら死ぬのでナシね(Application.Lockは当然、一台単位でのロックなので複数台数でロックは共有されてない)。そんなわけで、カジュアルな一手を打ちます。先に利用例から。
static void StandardUsage(string token) { // 複数サーバーで共有されるロックもどきの取得 using (var rock = DistributedLock.Acquire("StandardUsage-Lock-Token-" + token)) { rock.ThrowIfLockAlreadyExists(); // 二重に取得された場合は即座に例外! // 以下、本体を書けばいい } }
こんなふーに書けると、楽ですね。tokenは、まあ好きな単位で。ユーザー一人の単位だったら、認証済みなら何らかのIDを。非認証状態なら、POSTのHiddenにGUIDでも仕込んでおけばいい、と。ただの連打対策ってわけじゃなく、複数ユーザー間で同時処理されるのを抑えたければ、何らかのキーを、例えばソーシャルゲームだとチーム単位で、チームIDでかけたりとかします。
ロックもどきには↑の例ではMemcachedを使いました。単純に、Memcachedに指定キーでAddしにいく→Keyが既に存在していると上書きしないで追加に失敗→二重実行時は必ず失敗したという結果を受け取れる(bool:falseで)→Disposeで追加出来たときのみキーを必ず削除する(&保険でexpireもつけておく)
usingの部分は割と定型なので、毎回コントローラーを丸ごと囲むとかなら、属性作って、属性ペタッと貼るだけでOKみたいな形にするといいと思われます!
ド単純ですが、普通に機能して、結構幸せになれるかな?Memcachedならカジュアルに叩いても、相当耐えきれますから。あ、勿論、固定の台にリクエストが飛ぶの前提なのでノードがぐいぐい動的に追加削除されまくるよーな状況ではダメですよ、はい。あんまないでしょうが(Memcachedはクライアントサイドの分散で、複数台あってもキーが同一の場合は基本的に同じ台に飛ぶ)。
public class DistributedLockAlreadyExistsException : Exception { public DistributedLockAlreadyExistsException(string key) : base("LockKey:" + key) { } } public class DistributedLock : IDisposable { static MemcachedClient client = new MemcachedClient(); static readonly TimeSpan DefaultExpire = TimeSpan.FromSeconds(5); public bool IsAcquiredLock { get; private set; } string key; bool disposed; private DistributedLock(string key, TimeSpan expire) { this.key = key; this.IsAcquiredLock = client.Store(StoreMode.Add, key, DateTime.Now.Ticks, expire); } public static DistributedLock Acquire(string key) { return Acquire(key, DefaultExpire); } public static DistributedLock Acquire(string key, TimeSpan expire) { return new DistributedLock(key, expire); } public async Task<bool> WaitAndRetry(int retryCount, TimeSpan waitTime) { var count = 0; while (count++ < retryCount && !IsAcquiredLock) { await Task.Delay(waitTime); IsAcquiredLock = client.Store(StoreMode.Add, key, DateTime.Now.Ticks, DefaultExpire); } return IsAcquiredLock; } public void ThrowIfLockAlreadyExists() { if (!IsAcquiredLock) { throw new DistributedLockAlreadyExistsException(key); } } public void Dispose() { if (!disposed && IsAcquiredLock) { disposed = true; var removeSuccess = client.Remove(key); } GC.SuppressFinalize(this); } ~DistributedLock() { Dispose(); } }
MemcachedのライブラリはEnyimMemcachedです。
Asyncとリトライ
取得に失敗したら、間隔おいてリトライぐらいはしたいですよね、いや、連打対策なら不要ですが、そうでないように使う場合は。でも、ベタにThread.Sleepでまったりしたくないよねえ、という、そこでasyncですよ!async!
async static Task TaskUsage(string token) { using (var rock = DistributedLock.Acquire("TaskUsage-Lock-Token-" + token)) { if (!rock.IsAcquiredLock) { // 200ミリ秒感覚で3回取得に挑戦する await rock.WaitAndRetry(3, TimeSpan.FromMilliseconds(200)); rock.ThrowIfLockAlreadyExists(); // それでもダメなら例外投げるん } // 以下、本体を書けばいい! } }
WaitAndRetryメソッドではawait Task.Delay(waitTime)によって待機させています。少し前だとまんどくせ、と思って書く気のしない処理も、C# 5.0のお陰でカジュアルに書けるようになっていいですね。
Memcachedを立てないサーバー一台の場合
サーバー一台の場合は、わざわざMemcached立てるのも馬鹿らしいので、インメモリなキャッシュを代替として使えばいいと思われます。HttpRuntime.Cacheでも、System.Runtime.Caching.MemoryCacheでも、なんでもを、client.Storeのとこに差し替えてもらえれば。ただ、MemoryCacheは何かちょっと今回試すためにもぞもぞ弄ってたんですが、Addまわりの挙動がすんごく怪しくて信用ならない気がするので私は使うのパス。大丈夫なのかなあ。
まとめ
うーん、まんま、かつ、ゆるふわ単純な話なので特にまとめる話はないかしらん。
ので、We’re Hiringということで謎社のほめぱげが少しだけリニューアル、ただしリクルートページが諸事情でまだ工事中!メールフォーム入れるつもりなので、↑のような感じにC# 5.0をすぐに振り回すような最先端な環境のC#でウェブな開発がやりたい方は、是非応募してください。相当本気で人が欲しいところですねー。現状ですけれど、リリース2週間で早くもランキング3位を獲得などと、あまり細かくは言えないのですけれど、まあ非常に好調ですので、安心して&是非とも一緒に加速させましょう。
Razorで空テンプレートとセパレータテンプレート
- C# - 13.02/02
Razorに限らずT4でもなんでもいいんですが、テンプレートで素のforeachだと、セパレータだったり空の時の代替テンプレートだったりを、どういう風に表現すればいいのかなあ、と悩ましいのです、どうなっているのでしょう実際世の中的に。
WebFormsのRepeaterだとSeparatorTemplateタグと、拡張すればEmptyTemplateなども作れますね。Smarty(PHPのテンプレート、最近ペチパーなので)には{foreachelse}で配列が空の時のテンプレートが吐かれます。カスタムの構文を定義すれば、勿論なんだってありです。
RepeaterにせよSmartyにせよ、よーするところ独自のテンプレート構文だから好き放題できますが、俺々構文って、それ自体の覚える手間もあり、あんまスッキリしないんですよねえ。RazorのIs not a new language、だからEasy to Learn。は大事。また、そういった独自拡張がないからこそ、Compact, Expressive, and Fluidが実現できる(開き@だけで閉じタグレスはやっぱ偉大)し、フルにIntelliSenseなどエディタサポートも効くわけだし。
やりたいことって、コード上のノイズが限りなく少なく、かつ、HTMLという”テキスト”を最大限コントロールの効く形で吐くこと。なわけで、その辺を損なっちゃあ、見失っちゃあ、いけないね。
で、しかしようするところ、やりたいのはforeachを拡張したい。foreachする時に空の時の出力とセパレータの時の出力を足したい。あと、どうせならインデックスも欲しい。あと、最初の値か、とか最後の値か、とかも欲しい(最初はともかく「最後」はindexがないものを列挙すると大変)
そのうえで、Razorの良さである素のC#構文(と、ほぼほぼ同じものとして扱える)というのを生かしたうえで、書きやすくするには(例えばHtmlヘルパーに拡張メソッド定義して、引数でテンプレートやラムダ渡したり、というのは閉じカッコが増えたり空ラムダが出たりして書きづらいしグチャグチャしてしまいクリーンさが消える)、と思って、考えたのが、foreachで回すアイテム自体に情報載せればいいな、と。
<table> @foreach (var item in source.ToLoopItem(withEmpty: true, withSeparator: true)) { // empty template if (item.IsEmpty) { <tr> <td colspan="2">中身が空だよ!</td> </tr> } // separator if (item.IsSeparator) { <tr> <td colspan="2">------------</td> </tr> } // body if (item.IsElement) { <tr style="@(item.IsLast ? "background-color:red" : null)"> <td>@item.Index</td> <td>@item.Item</td> </tr> } } </table>
何も足さない何も引かない。とはいえどっかに何か足さなきゃならない。C#として崩さないで足すんなら、単独の要素の一つ上に包んで情報を付与してやりゃあいいんだね、と。foreachで回す時にToLoopItem拡張メソッドを呼べば、情報を足してくれます。
IsEmptyは全体が空の時、IsSeparatorは要素の間の時、IsElementが本体の要素の列挙の時、を指します。Elementの時は、更にIsFirst, IsLast, Indexが取れる。item.Itemはちょっと間抜けか。ともあれ、実際にRazorで書いてみた感触としても悪くなく収まってる。
Emptyだけならばループの外で@if(!source.Any()) /* 空の時のテンプレート */ としてやればいいし、そのほうが綺麗感はある。けれど、それだとsourceがIEnumerableの時キモチワルイ(二度列挙開始が走る)とかもあるし、コレクションに関わるものはforeachのスコープ内に全部収まったほうがスッキリ感も、なくもない。
IndexとIsLastだけが欲しいなら、空テンプレートとセパレータはオプションだから、withEmpty, withSeparatorを共にfalseにすれば、全部Elementなので、if(item.IsElement)は不要になる。
それにしてもRazor V2で属性にnull渡すと属性自体を吐かないでくれる機能は素敵ですなあ。クリーンは正義!
実装はこんな感じ。
public struct LoopItem<T> { public readonly bool IsEmpty; public readonly bool IsSeparator; public readonly bool IsElement; public readonly bool IsFirst; public readonly bool IsLast; public readonly int Index; public readonly T Item; public LoopItem(bool isEmpty = false, bool isSeparator = false, bool isElement = false, bool isFirst = false, bool isLast = false, int index = 0, T item = default(T)) { this.IsEmpty = isEmpty; this.IsSeparator = isSeparator; this.IsElement = isElement; this.IsFirst = isFirst; this.IsLast = isLast; this.Index = index; this.Item = item; } public override string ToString() { return (IsEmpty) ? "Empty" : (IsSeparator) ? "Separator" : Index + ":" + Item.ToString(); } } public static class LoopItemEnumerableExtensions { public static IEnumerable<LoopItem<T>> ToLoopItem<T>(this IEnumerable<T> source, bool withEmpty = false, bool withSeparator = false) { if (source == null) source = Enumerable.Empty<T>(); var index = 0; using (var e = source.GetEnumerator()) { var hasNext = e.MoveNext(); if (hasNext) { while (true) { var item = e.Current; hasNext = e.MoveNext(); if (hasNext) { yield return new LoopItem<T>(index: index, isElement: true, isFirst: (index == 0), item: item); } else { yield return new LoopItem<T>(index: index, isElement: true, isFirst: (index == 0), isLast: true, item: item); break; } if (withSeparator) yield return new LoopItem<T>(index: index, isSeparator: true); index++; } } else { if (withEmpty) { yield return new LoopItem<T>(isEmpty: true); } } } } }
大事なのは、IEnumerable<T>へのループは必ず一回にすること、ね。よくあるAny()で調べてから、ループ本体を廻すと、二度列挙実行が走る(Anyは最初を調べるだけですが、もしIEnumerable<T>が遅延実行の場合、そのコストは読めない)というのは、精神衛生上非常に良くない。
あとIsLastを取るために、一手先を取得してからyield returnをしなければならないので、少しゴチャついてしまいましたが、まあ、こういうのがViewの表面上に現れる苦難を思えば!
最近、イミュータブルな入れ物を作りたい時はコンストラクタにずらずら引数並べるでファイナルアンサー。と思うようになりました、一周回って。名前付き引数で書かせれば、数が多くても可読性落ちたりとかないですし、これでいいでしょう。名前付きで書かせることを強制したいけれど、それは無理なので適度に諦めるとして。
最後にユニットテストを置いておきます。例によってMSTest + Chaining Assertionで。
[TestClass] public class LoopItemTest { [TestMethod] public void Empty() { Enumerable.Empty<int>().ToLoopItem(withEmpty: false).Any().IsFalse(); Enumerable.Empty<int>().ToLoopItem(withEmpty: true).Is(new LoopItem<int>(isEmpty: true)); ((IEnumerable<int>)null).ToLoopItem(withEmpty: false).Any().IsFalse(); ((IEnumerable<int>)null).ToLoopItem(withEmpty: true).Is(new LoopItem<int>(isEmpty: true)); } [TestMethod] public void Separator() { Enumerable.Range(1, 3).ToLoopItem(withSeparator: false).Is( new LoopItem<int>(index: 0, item: 1, isFirst: true, isElement: true), new LoopItem<int>(index: 1, item: 2, isElement: true), new LoopItem<int>(index: 2, item: 3, isLast: true, isElement: true) ); Enumerable.Range(1, 1).ToLoopItem(withSeparator: true).Is( new LoopItem<int>(index: 0, item: 1, isFirst: true, isLast: true, isElement: true) ); Enumerable.Range(1, 3).ToLoopItem(withSeparator: true).Is( new LoopItem<int>(index: 0, item: 1, isFirst: true, isElement: true), new LoopItem<int>(index: 0, isSeparator: true), new LoopItem<int>(index: 1, item: 2, isElement: true), new LoopItem<int>(index: 1, isSeparator: true), new LoopItem<int>(index: 2, item: 3, isLast: true, isElement: true) ); Enumerable.Range(1, 4).ToLoopItem(withSeparator: true).Is( new LoopItem<int>(index: 0, item: 1, isFirst: true, isElement: true), new LoopItem<int>(index: 0, isSeparator: true), new LoopItem<int>(index: 1, item: 2, isElement: true), new LoopItem<int>(index: 1, isSeparator: true), new LoopItem<int>(index: 2, item: 3, isLast: false, isElement: true), new LoopItem<int>(index: 2, isSeparator: true), new LoopItem<int>(index: 3, item: 4, isLast: true, isElement: true) ); } }
structだと同値比較のために何もしなくていいのが楽ですね、けれどChaining AssertionならIsStructuralEqualがあるので、もしclassでも、やっぱり楽です!
まとめ
RazorだけじゃなくT4でコレクション回す時なんかにも使えます。なにかと毎度毎度、悩みの種なんですよねー。他に、こういうやり方もいいんでないー?とかあったら教えてください。
Modern C# Programming 現代的なC#の書き方、ライブラリの選び方
- C# - 13.01/29
と、題しまして第1回 業開中心会議 .NET技術の断捨離で話してきました。岩永さんが概念的なものを、私がC#とライブラリにフォーカスして具体的なものを、パネルディスカッションでフレームワーク周りの話を。的な分担だったでしょうか。
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をバイナリ編集して置換する
- C# - 13.01/10
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を生成する
- C# - 13.01/05
どうも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だという話でした。ん?
Micro-ORMとC#(とDapperカスタマイズ)
- C# - 12.12/11
C#に続き、ASP.NET Advent Calendar 2012です。前日は84zumeさんのWebFormっぽいコントロールベスト3でした。私はC#ではMemcachedTranscoder - C#のMemcached用シリアライザライブラリを書きまして、ああ!これこそむしろASP.NETじゃねえか!と悶絶したりなどして、日付逆にすれば良かったよー、困ったよー。しかもあんまし手持ちの札にASP.NETネタがない!というわけで、ASP.NETなのかビミョーですが押し通せば大丈夫だろう、ということでMicro-ORMについて。
Micro-ORM?
最近タイムリーなことに、またORM論争が起こっていて。で、O/R Mapperですが、私としては割と否定派だったりして。C#にはLINQ(to SQL/Entities)があります!はい、色々な言語のORMを見ても、LINQ(to SQL/Entities)の完成度はかなり高いほうに入ると思われます。それもこれもC#の言語機能(Expression Tree, 匿名型, その他その他)のお陰です。言語は実現できる機能にあんま関係ないとかいう人が割とたまにじゃばにいますが、んなことは、ないでしょ。
で、ORMと一口に言うとややこしいので、分解しよう、分解。一つはクエリビルダ。SQL文を組み立てるところです。ORMといったら、まず浮かぶのはここでしょう、そして実際、ここの部分の色々のもやもやを振り払うために、世の中のORMは色々腐心しているのではかと思います。
残りは、クエリを発行してDBに投げつける実行部分。コネクション作ってコマンド作ってパラメータ作って、とかがお仕事。最後に、結果セットをマッピングするところ。この2つは地味ですね、ORMという時に、特に意識されることはないでしょう。
で、Micro-ORMはクエリビルダはないです。あるのは実行とマッピングだけです。生SQL書いてオブジェクトにマッピングされたのが返ってくる。つまり、ORMと言ったときにまず浮かべる部分が欠けてます。だからORMって、RelationalとはMappingしてないんならもうDataMapperとかTableMapperとか言ったほうがいいのでは、感もありますが、つまるところそういうわけでMicro-ORMはORMじゃないですね。
ORM or その他、といった時に、ORM(DataSet, NHibernate, LINQ to SQL, Entity Framework)を使わない、となると、その次が生ADO.NETに吹っ飛ぶんですよね、選択肢。それ、えっ?って。生ADO.NETとか人間が直に触るものじゃあない、けど、まあ昔からちょっとしたお手製ヘルパぐらいは存在していたけれど、それだけというのもなんだかなー。という隙間に登場したのがMicro-ORMです。
Not ORM
つまりORMじゃあない。LINQという素敵な完成系があるのに、違うのを選びたくなる。何故?LINQという素敵なもので夢を見させてくれた、それでなお、ダメかもね、という結論に至ってしまう。じゃあもうORMって無理じゃない?
SQLは全然肯定できません。30年前のしょっぱい構文、の上にダラダラ足されていく独自拡張。じゃあ標準万歳かといえば、全然そんなことはないのでにっちもさっちもいかずだし、そもそもその標準の時点で相当しょっぱいっつーの。でも、それでも、ORMにまつわる面倒ごとであったり制限を押しのけてまで欲しいかい?と言われると、いらない。になる。
結局、データベースはデータベースであり、オブジェクトはオブジェクトであり。
EF CodeFirstって凄く滑稽。オブジェクトをそのまんまDBに投げ込むのなんて幻想で。だからデータベースを意識させて、クラスじゃないクラスを作る。リレーションを手でコードで張っていく、そんな、おかしいよ!まともなクラスじゃないクラスを手で書かされるぐらいなら、SQL Server Management Studioでペトペト作って、DBからクラス生成するほうがずっといい(勿論EFはそれできます)。
オブジェクト入れたいならさ、Redisとかも検討できる、そっちのほうがずっと素直に入る。勿論、データベースをやめよう、じゃないよ。ただ、データベースはデータベースである、というだけなんだ。
SQLだってすごく進化しているのに(書きやすさは置いておいてね)、ORMの抽象はそれらに完璧に対応できない。だって、データベース毎に、違うんだものね、同じ機能なかったりするものね。RDBMSは同じだ、というのが、まず、違うんじゃないかな、って。
良い面がいっぱいあるのは分かるよ!where句を文字列で捏ね捏ねするよりもオブジェクト合成したいし、LINQのタイプセーフなところは凄く魅力的なんだ!それでもね、厄介な挙動と複雑な学習コスト、パフォーマンスの問題、その他諸々。それらとは付き合わない、という選択もね、あっていいよね。
Dapper
具体例としてDapperを扱います。もっともポピュラーだから。速いしね。で、チマッとした具体例は、出してもつまらないので省略。それは↑の公式サイトで見ればいいでしょ。
拡張しよう
基本的にマッピングはプロパティ名とDBのカラム名が一致してないとダメです。ダメ絶対。しかし、世の中往々にして一致してるとは限らないケースが少なくもない。例えばDBのカラム名はsnake_caseでつけられていたりね。勿論、その場合C#のプロパティ名もsnake_caseにすりゃあいんですが、きんもーっ。嫌なんだよね、それ。
というわけでDapperには救済策が用意されていて、マッピングルールを型毎に設定することが可能です。この辺はリリース時にはなかったんですが後から追加されてます。そしてドキュメントが一向に更新されないため、何が追加されてるのとか、はためにはさっぱり分かりません。何気に初期リリースから地味に随分と機能が強化されていたりなかったりするんですんが、この辺は定期的にSourceとTest見れってとこですねー、shoganai。
方法としてはCustomPropertyTypeMapを作って、SqlMapper.SetTypeMapに渡してやればOK。CustomPropertyTypeMapではTypeとDBのカラム名が引数にくるので、そこからPropertyInfoを返してやればOK。一度定義されたマッピングファイルは初回のクエリ実行時にIL生成&キャッシュされ、二度呼ばれることはないので高速に動作します。
例えばsnake_caseをPascalCaseにマッピングさせてやるには
// こーいう関数を用意してやると static void SetSnakeToPascal<T>() { var mapper = new CustomPropertyTypeMap(typeof(T), (type, columnName) => { //snake_caseをPascalCaseに変換 var propName = Regex.Replace(columnName, @"^(.)|_(\w)", x => x.Groups[1].Value.ToUpper() + x.Groups[2].Value.ToUpper()); return type.GetProperty(propName); }); SqlMapper.SetTypeMap(typeof(T), mapper); } // こんなクラスがあるとして public class Person { // DBではid, first_name, last_name, created_at public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public DateTime CreatedAt { get; set; } } static void Main() { // MyClassをカラム名snake_cake → プロパティ名PascalCaseにマッピングするようセット SetSnakeToPascal<Person>(); using (var conn = new MySqlConnection("せつぞくもじれつ")) { conn.Open(); var result = conn.Query<Person>("select * from people"); // 無事マッピングできてる } }
といった感じ。SqlMapper.SetTypeMapをどこで呼ばせるか&管理面倒くせー、という問題は無きにしも非ずですが、まあその辺はやりようは幾らでもあるので(例えばクラスに専用の属性でも貼り付けておいてApplication_Startでリフレクションで全部舐めて登録してしまうとか)、大した問題ではないでしょう。
Dapperは何もかもの面倒は見てくれません。必要なものは随時、自分で足す。作る。でも、それでいいんじゃない?どうせ、出来あいの代物が自分達の要求に100%合致するなんてことはなくて、大なり小なり、自分達で足回り部分は作るでしょう。なら、そのついでです。大したことじゃあない。むしろ余計な面倒がなくていい。
ところでちなみに何でMySqlConnectionなのかというと、手元にあるDBが諸事情でMySQLだからです。諸事情!そこはdoudemoiiとして、DapperならMySQLでも繋げやすいという利点がありますね。DB選びません。C#はSQL Server専用みたいなものでしょ?なんてことはないのです。
Query Builder for Dapper
Dapperは純粋な実行とマッピングのみとなるように作られています、というのが設計やIssueの返信などからも見て取れます。生ADO.NETの一つ上の層として存在する、混じり気なしの代物にするのが目標だ、と。つまり、Dapper自身に、ちょっとしたPKで取ってくるだけのもの、Findとかよく言われるようなヘルパメソッドが乗ったりすることはありません。が、欲しいですよね、それ、そういうの。生で使ってもいいんですが、もう一枚、ほんの少しだけ、薄いの、被せたい。
そんなわけで、そのDapperの更に一枚上にのった、ちょっとしたCRUDヘルパーがDapperExtensionsやDapper.Rainbowなのですけれど、ビミョー。しょーじきビミョー。なので、作りましょう。自分で。例えばこういうのよくなくないですか?
// これで↓のクエリに変換される // select * from Person p where p.FirstName = @0 && p.CreatedAt <= @1 var sato = conn.Find<Person>(x => x.FirstName == "佐藤" && x.CreatedAt <= new DateTime(2012, 10, 10)); // updateもこんな感じで update .... values ... が生成、実行される conn.Update(sato, x => x.Id == 10)
Expression Treeから、タイプセーフなクエリ生成をする。select-where程度の、PKで取ってきたり、ちょっとした条件程度のものならさくっと書ける。InsertやUpdateも、そんまんまぶん投げて条件入れるだけなので単純明快。ところで、このまま拡張していくと、事前のマッピングクラス生成が不要な即席Queryable、LINQ to DBみたいなものができなくない?たとえばconn.AsQueryable().Where().OrderBy().Select() といったように。
結論を言えば、できる。が、やらないほうがいいと思ってます。一つは、どこかでQueryableのクエリ抽象の限界に突き当たること。生SQLで書いたほうがいいのか、Queryableで頑張ればいいのか。もしくは、これはQueryableでちゃんとサポートしているのか。そういう悩み、無駄だし意味ないし。select-whereならヘルパある、それ以外は生SQL書け。それぐらい単純明快なルールが敷けたほうが、シンプルでいいんじゃないかな。どうでもいい悩みを減らすためにやっているのに、また変な悩みを増やすようじゃやってられない。
もう一つは、Queryableを重ねれば重ねるほどパフォーマンスロスが無視できなくなっていくこと。たった一つの、↑のFindみたいなExpression Treeの生成/解析なんてたかがしれていて、無視できる範囲に収まっています。あ、これはちゃんと検証して言ってますよん。遅くなるといえば遅くなってますが、Entity Frameworkのクエリ自動コンパイルは勿論、手動コンパイルよりも速いです、逐次解析であっても。
Queryableを重ねれば重ねるほど遅くなるので、手動コンパイル(&キャッシュ)させなければならなくて、しかし手動コンパイルはかなり手間で滑稽なのでやりたくない。EFの自動コンパイルは悪くない!のですが、やっぱ相応に、そこまで速くはなくて、ね……。
実際に実装すると、こんな風になります。
// Expression Treeをなめなめする下準備 public static class ExpressionHelper { // Visitorで舐めてx => x.Hoge == xxという形式のExpression Treeから値と演算子のペアを取り出す public static PredicatePair[] GetPredicatePairs<T>(Expression<Func<T, bool>> predicate) { return PredicateExtractVisitor.VisitAndGetPairs(predicate); } class PredicateExtractVisitor : ExpressionVisitor { readonly ParameterExpression parameterExpression; // x => ...のxなのかを比較判定するため保持 List<PredicatePair> result = new List<PredicatePair>(); // 抽出結果保持 public static PredicatePair[] VisitAndGetPairs<T>(Expression<Func<T, bool>> predicate) { var visitor = new PredicateExtractVisitor(predicate.Parameters[0]); // x => ... の"x" visitor.Visit(predicate); return visitor.result.ToArray(); } public PredicateExtractVisitor(ParameterExpression parameterExpression) { this.parameterExpression = parameterExpression; } // Visitぐるぐるの入り口 protected override Expression VisitBinary(BinaryExpression node) { // && と || はスルー、 <, <=, >, >=, !=, == なら左右の解析 PredicatePair pair; switch (node.NodeType) { case ExpressionType.AndAlso: pair = null; break; case ExpressionType.OrElse: pair = null; break; case ExpressionType.LessThan: pair = ExtractBinary(node, PredicateOperator.LessThan); break; case ExpressionType.LessThanOrEqual: pair = ExtractBinary(node, PredicateOperator.LessThanOrEqual); break; case ExpressionType.GreaterThan: pair = ExtractBinary(node, PredicateOperator.GreaterThan); break; case ExpressionType.GreaterThanOrEqual: pair = ExtractBinary(node, PredicateOperator.GreaterThanOrEqual); break; case ExpressionType.Equal: pair = ExtractBinary(node, PredicateOperator.Equal); break; case ExpressionType.NotEqual: pair = ExtractBinary(node, PredicateOperator.NotEqual); break; default: throw new InvalidOperationException(); } if (pair != null) result.Add(pair); return base.VisitBinary(node); } // 左右ノードから抽出 PredicatePair ExtractBinary(BinaryExpression node, PredicateOperator predicateOperator) { // x.hoge == xx形式なら左がメンバ名 var memberName = ExtractMemberName(node.Left); if (memberName != null) { var value = GetValue(node.Right); return new PredicatePair(memberName, value, predicateOperator); } // xx == x.hoge形式なら右がメンバ名 memberName = ExtractMemberName(node.Right); if (memberName != null) { var value = GetValue(node.Left); return new PredicatePair(memberName, value, predicateOperator.Flip()); // >, >= と <, <= を統一して扱うため演算子は左右反転 } throw new InvalidOperationException(); } string ExtractMemberName(Expression expression) { var member = expression as MemberExpression; // ストレートにMemberExpressionじゃないとUnaryExpressionの可能性あり if (member == null) { var unary = (expression as UnaryExpression); if (unary != null && unary.NodeType == ExpressionType.Convert) { member = unary.Operand as MemberExpression; } } // x => xのxと一致してるかチェック if (member != null && member.Expression == parameterExpression) { var memberName = member.Member.Name; return memberName; } return null; } // 式から値取り出すほげもげ色々、階層が深いと面倒なのね対応 static object GetValue(Expression expression) { if (expression is ConstantExpression) return ((ConstantExpression)expression).Value; if (expression is NewExpression) { var expr = (NewExpression)expression; var parameters = expr.Arguments.Select(x => GetValue(x)).ToArray(); return expr.Constructor.Invoke(parameters); // newしてるけどアクセサ生成で高速云々 } var memberNames = new List<string>(); while (!(expression is ConstantExpression)) { if ((expression is UnaryExpression) && (expression.NodeType == ExpressionType.Convert)) { expression = ((UnaryExpression)expression).Operand; continue; } var memberExpression = (MemberExpression)expression; memberNames.Add(memberExpression.Member.Name); expression = memberExpression.Expression; } var value = ((ConstantExpression)expression).Value; for (int i = memberNames.Count - 1; i >= 0; i--) { var memberName = memberNames[i]; // とりまリフレクションだけど、ここはアクセサを生成してキャッシュして高速可しよー dynamic info = value.GetType().GetMember(memberName)[0]; value = info.GetValue(value); } return value; } } } // ExpressionTypeだと範囲広すぎなので縮めたものを public enum PredicateOperator { Equal, NotEqual, LessThan, LessThanOrEqual, GreaterThan, GreaterThanOrEqual } // x.Hoge == 10 みたいなのの左と右のペアを保持 public class PredicatePair { public PredicateOperator Operator { get; private set; } public string MemberName { get; private set; } public object Value { get; private set; } public PredicatePair(string name, object value, PredicateOperator predicateOperator) { this.MemberName = name; this.Value = value; this.Operator = predicateOperator; } } public static class PredicatePairsExtensions { // SQL文作るー、のでValueのほうは無視気味。 public static string ToSqlString(this PredicatePair[] pairs, string parameterPrefix) { var sb = new StringBuilder(); var isFirst = true; foreach (var pair in pairs) { if (isFirst) isFirst = false; else sb.Append(" && "); // 今は&&連結だけ。||対応は面倒なのよ。。。 sb.Append(pair.MemberName); switch (pair.Operator) { case PredicateOperator.Equal: if (pair.Value == null) { sb.Append(" is null "); continue; } sb.Append(" = ").Append(parameterPrefix + pair.MemberName); break; case PredicateOperator.NotEqual: if (pair.Value == null) { sb.Append(" is not null "); continue; } sb.Append(" <> ").Append(parameterPrefix + pair.MemberName); break; case PredicateOperator.LessThan: if (pair.Value == null) throw new InvalidOperationException(); sb.Append(" < ").Append(parameterPrefix + pair.MemberName); break; case PredicateOperator.LessThanOrEqual: if (pair.Value == null) throw new InvalidOperationException(); sb.Append(" <= ").Append(parameterPrefix + pair.MemberName); break; case PredicateOperator.GreaterThan: if (pair.Value == null) throw new InvalidOperationException(); sb.Append(" > ").Append(parameterPrefix + pair.MemberName); break; case PredicateOperator.GreaterThanOrEqual: if (pair.Value == null) throw new InvalidOperationException(); sb.Append(" >= ").Append(parameterPrefix + pair.MemberName); break; default: throw new InvalidOperationException(); } } return sb.ToString(); } } public static class PredicateOperatorExtensions { // 演算子を反転させる、 <= と >= の違いを吸収するため public static PredicateOperator Flip(this PredicateOperator predicateOperator) { switch (predicateOperator) { case PredicateOperator.LessThan: return PredicateOperator.GreaterThan; case PredicateOperator.LessThanOrEqual: return PredicateOperator.GreaterThanOrEqual; case PredicateOperator.GreaterThan: return PredicateOperator.LessThan; case PredicateOperator.GreaterThanOrEqual: return PredicateOperator.LessThanOrEqual; default: return predicateOperator; } } }
public static T Find<T>(this IDbConnection conn, Expression<Func<T, bool>> predicate) { var pairs = ExpressionHelper.GetPredicatePairs(predicate); // とりあえずテーブル名はクラス名で var className = typeof(T).Name; var condition = pairs.ToSqlString("@"); // とりま@に決めうってるけどDBによっては違いますなー var query = string.Format("select * from {0} where {1}", className, condition); // 匿名型でなく動的にパラメータ作る時はDynamicParameterを使う var parameter = new DynamicParameters(); foreach (var pair in pairs) { parameter.Add(pair.MemberName, pair.Value); } // Dapperで実行. 勿論、FirstではないFindAllも別途用意するとヨシ。 return conn.Query<T>(sql: query, param: parameter, buffered: false).First(); } static void Main(string[] args) { using (var conn = new MySqlConnection("せつぞくもじれつ")) { conn.Open(); // ↓のようなクエリ文になる // select * from Person where FirstName = @FirstName && CreatedAt <= @CreatedAt var sato = conn.Find<Person>(x => x.FirstName == "佐藤" && x.CreatedAt <= new DateTime(2012, 10, 10)); }
といった、Expression TreeベースのタイプセーフなMicro Query Builderを中心にしたMicro-ORMが、DbExecutor ver.3で、実際に作っていました。水面下で。そしてお蔵入りしました!お蔵入りした理由は色々お察し下さい。まぁまぁ悪くないセンは行ってたかなー、とは思うのでお蔵入りはMottainai感が若干あるものの、全体的には今一つだったなあ、というのが正直なところで、”今”だったら違う感じになったかな、と思っちゃったりだから、あんまし後悔はなく没でいいかな。某g社の方々へは申し訳ありません、と思ってます。
そんなわけでMicro Query Builderというコンセプトを継いで、マッピング部分はDapperを使うDapper拡張として作り直したものは、近日中にお目見え!はしません。しませんけれど(タスクが山積みすぎてヤバい)、そのうちに出したいというか、絶対に出しますので、乞うご期待。謎社の今後にも乞うご期待。
まとめ
あんましFull ORM使わなきゃー、とか悩む必要はないです。XXが便利で使いたいんだ!というなら使えばいいですし、逆にXXがあってちょっと嫌なんだよなー、というならば、使わない、が選択肢に入っていいです。.NETだって選択の自由はあるんですよ?そこ勘違いしちゃダメですよ?自由度を決めるのは、Microsoftでもコミュニティーの空気でもなく、自分達ですから。
さて、ASP.NET Advent Calendar 2012、次はMicrosoft MVP for Windows Azureの割と普通さんです。AzureとWeb Sitesについて聞けるようですよ!wktk!
MemcachedTranscoder - C#のMemcached用シリアライザライブラリ
- C# - 12.12/03
今年もAdvent Calendarの季節がやってきました。というわけで、この記事はC# Advent Calendar 2012用の話となります。去年はModern C# Programming Style Guideという記事を書きまして、結構好評でした。また、去年は他Silverlight Advent Calendar 2011で.NETの標準シリアライザ(XML/JSON)の使い分けまとめというシリアライザの話をしました。今年も路線は引き続きで、モダンなシリアライザの話をしましょう。
MemcachedTranscoder
そんなわけで、表題のものを作りました。dllのインストールはNuGet経由でお願いします。
Memcachedは言わずと知れた分散キャッシュ。C#で最もメジャーなMemcachedのライブラリはEnyim.Memcachedです。これを使って、オブジェクトをGet、Setするわけだー。さて、オブジェクトをSetするというのは、最終的にbyte[]に落とす必要があります。ただたんにポーンとオブジェクト投げたらSetできたー、にはなりませんですのよ。では、どうやってbyte[]に変換しているの?というと、シリアライザが内部で動いてます。
シリアライザについては以前に.NET(C#)におけるシリアライザのパフォーマンス比較という記事も書いたりしていて、結構うるさいんで割と気にするほうです。さて、そんなEnyim.Memcachedのシリアライザは、デフォルトではBinaryFormatterです。はい、これは、あまり速くないしファイルサイズも結構かさんでゲンニョリ系シリアライザ。
ただしEnyim.MemcachedはそれらをTranscoderと呼んでいて、自由に差し替えが可能になっています。つまりBinaryFormatterがゲンニョリならば自分で差し替えればいいじゃない!ちなみに純正オプションとしてNetDataContractSerializerも用意されているのですが、これは……話にならないぐらいサイズがデカくなるので、ないわー。
そんなわけで.NET最速シリアライザのProtobuf-netと、やっぱ時代はJSONよねということで、.NETで最もスタンダードなJSONライブラリであるJSON.NETと、新進気鋭のMsgPack-Cliの3種のTranscoderを作りました。
使い方
app.configかweb.configのMemcachedのTranscoderの設定行に、それぞれ使いたいTranscoderのものを指定して、dllを実行ファイルと同ディレクトリにでも置いてください。
<?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <sectionGroup name="enyim.com"> <section name="memcached" type="Enyim.Caching.Configuration.MemcachedClientSection, Enyim.Caching" /> </sectionGroup> </configSections> <enyim.com> <memcached protocol="Binary"> <servers> <add address="127.0.0.1" port="11211"/> </servers> <transcoder type="MemcachedTranscoder.ProtoTranscoder, ProtoTranscoder" /> </memcached> </enyim.com> </configuration>
Transcoderのバリエーションは以下の感じ。
<transcoder type="MemcachedTranscoder.ProtoTranscoder, ProtoTranscoder" /> <transcoder type="MemcachedTranscoder.JsonTranscoder, JsonTranscoder" /> <transcoder type="MemcachedTranscoder.MessagePackTranscoder, MessagePackTranscoder" /> <transcoder type="MemcachedTranscoder.MessagePackMapTranscoder, MessagePackMapTranscoder" />
ProtoTranscoderはProtocol Buffers、JsonTranscoderはJSON、MessagePackTranscoderはMsgPackをArrayモードで、MessagePackMapTranscoderはMsgPackをMapモードでオブジェクトを変換します。
型とデシリアライズ
使い方を説明して終わり、というのもつまらないので、もっと深く見ていきましょう。Enyim.MemcachedはGetもSetもobjectでしかできません。ジェネリックなのもあるように見せかけて、最終的にはobjectに落ちます。ITranscoderのところには型が伝達されないのです。以下のがITranscoderインターフェイスね。
public interface ITranscoder { object Deserialize(CacheItem item); CacheItem Serialize(object value); }
何が困るって?シリアライザは型が必要なんですよ!デシリアライズの時に!DataContractSerialize作るのにtypeofで型を渡しているでしょう?Protobuf.Serialize<T>でしょう?MessagePackSerializer.Create<T>でしょう?(JsonConvert.DeserializeObjectは、一見デシリアライズ可能にみえて、それJObjectが帰ってくるから意味ないです)
例えばMyClassクラスというint MyProperty{get;set;}だけがある、なんてことのないクラスがあるとして、ふつーにJSONにシリアライズした結果は
{"MyProperty":100}
こんな感じになります。が、これだとこれがMyClassという情報は一切ありません。HogeClassかもしれないしHugaClassかもしれない。つまりデシリアライズ不能です。よって、外から型を与える必要があります。Deserialize<MyClass>、といったように。これがもし
{ "Type" : "MyClass", "Properties" : [ {"MyProperty":100} ] }
このように、値が型情報も持っていれば、型がMyClassだと分かるので、型を渡すのは不要になります。BinaryFormatterやNetDataContractSeiralizerが型不要でSerialize/Deserializeできているのは何故か、というと、シリアライズした後の形に型が付与されているからなのです。そして、なぜEnyim.Memcachedが標準でBinaryFormatterとNetDataContractSerializerを用意しているのか、あるいは何故他のものが用意できないのか、というと、型情報が必要だからです。
じゃあ型入れとけばいいじゃーん、といったところですが、こうすると型情報の分だけファイルサイズが嵩んでしまいます。また、.NET固有の型を埋め込むというのは、他の言語と通信するのにあたっては、かなりビミョウです。
だから、理想的には型は外から与えられるといいな、って思うのです。とはいえ、実際問題、Transcoderは型の渡せないインターフェイスなので、どうにかしなきゃあいけません。
型を埋める
そんなわけで、解法は、手動で型を埋める、になります。(他には全てのAPIを型付きにラップしてそれ経由でしかアクセスさせないで、Serializeを呼ぶときはbyte[]に崩してから呼ぶとかいう方法もあるですかしらん)。どういうこっちゃ、というと、伝わりやすいであろうJSON版のTranscoderで見てみましょうか。
protected override ArraySegment<byte> SerializeObject(object value) { var type = value.GetType(); var typeName = writeCache.GetOrAdd(type, TypeHelper.BuildTypeName); // Get type or Register type using (var ms = new MemoryStream()) using (var tw = new StreamWriter(ms)) using (var jw = new Newtonsoft.Json.JsonTextWriter(tw)) { jw.WriteStartArray(); // [ jw.WriteValue(typeName); // "type", jsonSerializer.Serialize(jw, value); // obj jw.WriteEndArray(); // ] jw.Flush(); return new ArraySegment<byte>(ms.ToArray(), 0, (int)ms.Length); } }
[”型名”, {objectのシリアライズ結果}]といった風に埋めてます。長さ2の配列で決め打ち!0番目は型名の文字列!1番目が実態!これなら、まあ他の言語で触るのも問題ないし(多少は不恰好ですけどね)、ファイルサイズ増大もほぼほぼ型名だけで抑えられています。MessagePack用のTranscoderも同じような実装です。このアイディアはMsgPack-Cli作者の @yfakariyaさんから頂きました。
JSON, MsgPackはそうなのですけれど、Protocol Buffers版は……違います。
ProtoTranscoder
Enyim.Memcached用のProtocol BuffersなTranscoderは、もともとprotobuf-net作者のMarc Gravell氏が作成し公開しています。Distributed caching with protobuf-net。
しかし、幾つかの理由により、このコードを使用することはお薦めしません、というかやめたほうがいいです。
- 1.対応しているProtobufやEnyim.Memcachedが古いので若干手直しが必要
- 2.配列や辞書など、効果の高いコレクション系に対してシリアライズしてくれない(BinaryFormatterが使われる)
- 3.そもそもバグっていて、ジェネリックなクラスを突っ込むと壊れる
1はそのまま。2は、そういうif文が入っているからです。別にコレクションだけ避けるようになっている、というわけじゃなくて、ある種の保険でそういう条件分岐があるのですが、結果としてコレクションが避けられることになってしまっていて、効果が薄くなってしまうな、と。そして3ですが、これは致命的です。どこがバグってるかというと、以下のところ。
string typeName = type.AssemblyQualifiedName; int i = typeName.IndexOf(','); // first split if (i >= 0) { i = typeName.IndexOf(',', i + 1); } // second split if (i >= 0) { typeName = typeName.Substring(0, i); } // extract type/assembly only
型情報を埋め込む、つまりは型から型情報の文字列を取ってこなければなりません。それ自体はAssemblyQualifiedNameを呼ぶだけの、造作もないことなのですけれど
// System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Console.WriteLine(typeof(int).AssemblyQualifiedName);
Versionとか、Cultureとか、PublicKeyTokenとか、いらないね。型名とアセンブリ名、それだけ分かればそれでいい、それがいい。なので、それら無駄な情報を除去しようとしているのが↑↑のコードです。
実際うまくいきます。ジェネリックを含まなければ。
var type = typeof(List<int>); string typeName = type.AssemblyQualifiedName; int i = typeName.IndexOf(','); // first split if (i >= 0) { i = typeName.IndexOf(',', i + 1); } // second split if (i >= 0) { typeName = typeName.Substring(0, i); } // extract type/assembly only // ↓のtypeNameは壊れてる // System.Collections.Generic.List`1[[System.Int32, mscorlib Console.WriteLine(typeName);
見事に欠落してしまいます。AssemblyQualifiedNameが、ジェネリックを含むクラスだと形が若干変わるので、この決め打ちSubstringでは対応しきれてません。
でもバグってるから使えない、というだけじゃ勿体ない!.NET最速シリアライザが使えないとか!というわけかで、私の作成したProtoTranscoder半分は氏のコードをベースにしています。また、型情報を埋め込むといったことの元ネタもこのコードからです。
んで、このバグッてた型情報を削るところですが、AssemblyQualifiedNameが実際どういう形を取るのか、もしくはどういう形が読み込めるものなのか、というのはMSDNのType.GetTypeメソッド解説に例付きで詳しく書いてあります。非常に複雑で正面からきっちりパースしようとすると苦戦します。なので、正規表現でサクッと削ることにしました。
internal static class TypeHelper { static readonly Regex SubtractFullNameRegex = new Regex(@", Version=\d+.\d+.\d+.\d+, Culture=\w+, PublicKeyToken=\w+", RegexOptions.Compiled); internal static string BuildTypeName(Type type) { return SubtractFullNameRegex.Replace(type.AssemblyQualifiedName, ""); } }
一応テストは書いてありまして、TypeHelperTest.cs、色々並べたてた限り問題ないようなので、問題ないと思われます。
あと、型情報の埋め込みですが、JsonTranscoderは配列にして型情報を入れていましたが、ProtoTranscoderはbyte[]の先頭に直接埋め込んでいます。先頭4バイトが型情報の長さを表し(int)、その後に続く長さの分だけ型情報の文字列(UTF8)があり、その後ろが実体。配列がどうこうとかないので、サイズ的にも処理的にも有利です。ただ、Memcachedに格納された値自体は不正なProtocol Buffersの値となるわけで、相互運用性には難ありといったところ(他のデシリアライズするもの側でもストリーム先頭の型情報部分をスキップするようにすれば、回避できるといえばできます)。最初から相互運用性ゼロのBinaryFormatter(他の言語ではこれでシリアライズされた後の形を解釈できない)よりは遥かにマシ、ではありますね。
Memcached is dead. Long live Redis!
バグってるとか、いーのかよー、という感じですが、そもそも、使われてないんですよね。Stackoverflowのキャッシュ層はRedisですので。完全にノーメンテ。(StackoverflowのアーキテクチャはStack Overflow Architecture Update - Now At 95 Million Page Views A Monthで。これも2011/3のものなので、今は更に進化してるんだろうねえ。StackoverflowはかなりRedis好きみたいで、Memcached is dead. Long live Redis!ってStackoverflowのエンジニア(Marc氏ではない)が言ってた。
私もRedis好きですね。超好き。アレは超良いものだ……。ちなみにRedisのライブラリはBookSleeveとServiceStack.Redisがありまして、この辺に関して詳しくは、そのうち書きましょう。いや、ほんとRedis良いしC#との相性もいいし、たまらんです。
そんなわけで放置されていたんですが、昨日の今日で、新しいのがリリースされました。protobuf-net.Enyim。そして、バグはそのままでした……。というわけで、そのことはTwitterで伝えたので、そのうち直るでしょう(Twitterは連絡手段として非常に気楽でいいですなあ)。でも、プリミティブ型の配列などにProtobufが使われない、とかTypeCacheからのTypeの取得部分がforeachぐるぐるるーぷ、などはそのままなので、私の作ったもののほうが良いです。多分ね。
パフォーマンス
性能ですが、まず、シリアライザはシリアライズする対象によって速度は変わります。だから、一概にどれが速いとか遅いとか言いにくいところはあります。そのうえで、以下のクラスと、それの配列(長さ10)を用意しました。
[ProtoContract] [Serializable] public class TestClass { [ProtoMember(1)] [MessagePackMember(0)] public string MyProperty1 { get; set; } [ProtoMember(2)] [MessagePackMember(1)] public int MyProperty2 { get; set; } [ProtoMember(3)] [MessagePackMember(2)] public DateTime MyProperty3 { get; set; } [ProtoMember(4)] [MessagePackMember(3)] public bool MyProperty4 { get; set; } } // シンプルなPOCOとしての対象 var obj = new TestClass { MyProperty1 = "hoge", MyProperty2 = 1, MyProperty3 = new DateTime(1999, 12, 11), MyProperty4 = true }; // オブジェクト配列としての対象 var array = Enumerable.Range(1, 10) .Select(i => new TestClass { MyProperty1 = "hoge" + i, MyProperty2 = i, MyProperty3 = new DateTime(1999, 12, 11).AddDays(i), MyProperty4 = i % 2 == 0 }) .ToArray();
これを100000回シリアライズ/デシリアライズした速度と、一個のファイルサイズの検証結果が以下になります。あと、これはTranscoderを介した速度検証であって、決してシリアライザ単体での速度測定ではないことには留意してください。
Simple POCO************************ S DefaultTranscoder:735 D DefaultTranscoder:750 Size:305 S DataContractTranscoder:775 D DataContractTranscoder:1642 Size:746 S ProtoTranscoder:99 D ProtoTranscoder:142 Size:88 S JsonTranscoder:772 D JsonTranscoder:892 Size:167 S MessagePackTranscoder:256 D MessagePackTranscoder:535 Size:89 S MessagePackMapTranscoder:327 D MessagePackMapTranscoder:783 Size:137 Array****************************** S DefaultTranscoder:4234 D DefaultTranscoder:4186 Size:712 S DataContractTranscoder:3874 D DataContractTranscoder:9532 Size:4525 S ProtoTranscoder:2189 D ProtoTranscoder:3040 Size:255 S JsonTranscoder:5618 D JsonTranscoder:6275 Size:1043 S MessagePackTranscoder:752 D MessagePackTranscoder:2696 Size:256 S MessagePackMapTranscoder:1453 D MessagePackMapTranscoder:5088 Size:736
単体ではProtobufが最速。これは予想通り。配列にすると、MsgPack-Cliが爆速。ほええー。理由は分かりません!また、BinaryFormatterが決して悪くないのね。速度もそうだし、サイズも、特に配列にしたときにそんなにサイズが膨れないのは偉い、結果的にJSONより小さくなってるしね。これは、JSONは律儀に全部の配列の値に対してプロパティ名を入れますが、BinaryFormatterは先頭に型情報を一つ定義し、あとはその定義への参照という形で廻しているから、でしょうね。BinaryFormatterのデータ構造の仕様は.NET Remoting: Binary Format Data Structureにありますが、別に読まなくてもいいと思いますん。
私はバイナリアンじゃないのでバイナリと睨めっこはあんましたくないですね、前々職でTrueType Fontの仕様と睨めっこしてバイナリほじほじした時は、それはそれで楽しくはあったけれど、好んでやりたくない感はあったり。ゆるふわゆとり世代ですものー。
Azure Caching
Windows Azure CachingもMemcachedプロトコルをサポートということなので、今回の話はまんま使えますね!まあ、既存のものの移し替え、とかでなければ、Enyim… よりもAzure Cachingのライブラリ使ったほうがいいとは思いますが。「Enyim cache client API で入れたデータを Windows Azure caching API (Client Api) で取得すると、例外が発生します。(その逆も同様です。)」というのは、書いてある通りにシリアライザが違うからですねー。デフォルトはNetDataContractSerializerということで、まあ、アレですね、悲しいですね、Azure Caching使うならCustom Serializer作ったほうがいいんじゃないですかね(これがEnyim…のTranscoderにあたる)。まあ、Memcached ProtocolにしてEnyim… を使ってもいいでしょうけれど、Enyim…もビミョいといえばビミョいので、その辺は何とも。
まあ、私はAzureは知らないので、きっとAzureの誰かが言ってくれるでせう。あ、 Azure Cachingのシリアライズコストが発生しない云々は ローカルキャッシュのみの話で、外側に行くなら原理的にシリアライズ/デシリアライズが発生するのは当たり前です、というのは一応。
まとめ
NetDataContractSerializerは論外として、BinaryFormatterは決して悪くはないので、エクストリームなパフォーマンスを求めないなら、そのまんまでいい気がしました。求めるんなら、やっぱProtobufに安定感ありますねえ。しかしMsgPackも良いんですね。可搬性ならJSONにしちゃうのも良いかなー。結局、アレだ、好きなもの選ぶのがいいと思いますですよ、と。
ところで、これはもともと、前職のgloopsで使うつもりで用意していたのですが、辞めちゃったとかあったので、投入するところまでは行きませんでした。というわけで今のところ利用実績はないです!まあ、多分大丈夫だと思うんですがその辺は投下してみてもらわないと何とも言えません。要は勇気が自己責任。ともあれ、コードの公開を許可してくれたgloopsに感謝します。
そんなこんなで、謎社でもC#でエクストリームな性能を求めたい方を求めております。パブリックに詳しく言えるのは予定は未定なので、そういったことをやりたいという方は、こっそり私のほうに聞いてくれると嬉しいですね。あ、これは割とマジな話ですよ。それとAzureの営業かけるなら今のうちなのでそれも私のほうまで(謎)
Chaining Assertion 1.7.0.1 - 値比較の追加
- C# - 12.11/28
EqualsやGetHashCodeをオーバーライドするかと言ったら、そういう目的があるなら当然するし、そうでないならしない。という極当たり前なところに落ち着いたりはする。目的ってどういう時かといったら、LINQでDistinctの対象にしたい時とかですかね、まあ、よーするに値で比較したい!時です、まんまですね。
なので、逆にそれ以外の用途であえてこれらをオーバーライドすることはないです。特にテストのためにオーバーライドというのは、はっきしいって、良くないって思ってます。Equalsというのはクラスとして非常に重要な意味のあるところなので、そこにテスト都合が混じりこむのはNGです。
でも、テスト都合で値で比較したかったりは割とあるんですよね。じゃあどうするかって、まあ、ふつーにアサート側で構造比較したほうがいいでしょ、テスト都合なんだから。QUnitなんてdeepEqualが基本で、それが存外使いイイんですよ。
と、いうよくわからない前振りですが、つまるところChaining Assertionに値で比較できるIsStructuralEqualを足しました。
いやあ、一年ぶりの更新です!というか、もう一年前ですかー、早いものだ。もうAssert.ThatをDisるのも忘れてたぐらいに昔の話ですねー、あ、今も当然Assert.ThatとかFluent何ちゃらは嫌いですよ、と、それはさておき。かなり前の話なのでChaining Assertionについておさらい。メソッドチェーンな感じにさらさらっとAssertを書けるテスト補助ライブラリです。詳しくはメソッドチェーン形式のテスト記述ライブラリという1.0出した時の説明をどうぞー。主にMSTestやNUnitに対応しています。勿論NuGetでも入りますのでChainingAssertionで検索を。
今回追加したのはIsStructuralEqual(もしくはIsNotStructuralEqual)で、構造を再帰的に辿って値としての一致で比較します。
// こんなクラスがあるとして class MyClass { public int IntProperty { get; set; } public string StrField; public int[] IntArray { get; set; } public SubMyClass Sub { get; set; } } class SubMyClass { public DateTime Date { get; set; } } [TestMethod] public void TestMethod1() { var mc1 = new MyClass { IntProperty = 100, StrField = "hoge", IntArray = new[] { 1, 2, 3, 4, 5 }, Sub = new SubMyClass { Date = new DateTime(1999, 12, 31) } }; var mc2 = new MyClass { IntProperty = 100, StrField = "hoge", IntArray = new[] { 1, 2, 3, 4, 5 }, Sub = new SubMyClass { Date = new DateTime(1999, 12, 31) } }; mc1.IsNot(mc2); // mc1とmc2は全て同じ値ですが、参照比較では当然違います mc1.IsStructuralEqual(mc2); // IsStructuralEqualでは全てのプロパティを再帰的に辿って比較します }
なんのかんので便利で使っちゃいますね、多用しちゃいますね、きっと。ちなみに、ただたんに値比較したいだけなら、JSONにでもDumpして、文字列一致取ればいいだけなんですけれど。自前で辿ることによって、誤った箇所へのメッセージは割と親切かな、と思います。あとIEquatableの扱いとか型の扱いとか、ただのDumpとは色々若干と違うので、まあ、こちらのほうが望ましい具合な結果が得られると思います。
var mc1 = new MyClass { IntProperty = 100, StrField = "hoge", IntArray = new[] { 1, 2, 3, 4, 5 }, Sub = new SubMyClass { Date = new DateTime(1999, 12, 31) } }; // 間違い探し! var mc2 = new MyClass { IntProperty = 100, StrField = "hoge", IntArray = new[] { 1, 2, 3, 100, 5 }, // 4番目が違う Sub = new SubMyClass { Date = new DateTime(1999, 12, 31) } }; // 以下のようなエラーメッセージが出ます // is not structural equal, failed at MyClass.IntArray.[3], actual = 4 expected = 5 mc1.IsStructuralEqual(mc2); // こんどはStrFieldが違う var mc3 = new MyClass { IntProperty = 100, StrField = "hage", IntArray = new[] { 1, 2, 3, 4, 5 }, Sub = new SubMyClass { Date = new DateTime(1999, 12, 31) } }; // 以下のようなエラーメッセージが出ます // is not structural equal, failed at MyClass.StrField, actual = hoge expected = hage mc1.IsStructuralEqual(mc3);
割と十分、分かりやすい。かな?
コードは結構好き勝手感です。PropertyInfoとFieldInfoは共にMemberInfoを継承してるが、GetValueは同じメソッドシグネチャだけど、MemberInfoに定義されてるわけじゃなくてPropertyInfo, FieldInfoにそれぞれあるから共通でまとめられないよー→dynamicで受ければGetValue使えて大解決。とか、まあふつーのコードではやらないようなことも、UnitTest用だから若干の効率低下は無視でなんでもありで行くよ!というのが割と楽しいですね。
その他
ついでにjsakamotoさんからPull Request来ていたIsInstanceOfでメソッドチェーンできるようになったり(Pull Requestの放置に定評のある私です!というか初めてacceptしたわー)、IsTrueとIsFalseを足した(いやあ、Is(true)ってやっぱ面倒くさかったよ、あはは)りなどしました。
さて、お次はWinRT対応とWindows Phone 8対応を、といったところなのですが、それはそのうち近いうちに!WinRT対応はねえ、リフレクション回りがドサッと変わってるので面倒といえば面倒なんですよねえ。まあ、勉強のためのちょうどいい題材ではあるので、手を付けたいとは思ってるのですけれど。
あとね、正直NUnitはいいとしてもMbUnitやxUnit、SLやWP7に対応させるの超面倒くさい。やりすぎた。これのせいでちょっとした修正ですら大仕事なわけですよ。このメンテコスト最悪すぎる状態がどうにもねえ、それでいてNUnitはともかく、その他なんてほとんど使われてないですからねえ。分かってはいたのですが、こうシンドイと結構限界。というわけで、WP7とSilverlightは削除しました。この二つはもういらないぢゃん?さようなら……。
ああ、あとFakes FrameworkのためのVerifierも入れたいしねえ、やりたいことは割と多いんですが、ニートもこれはこれでいて忙しくて手が回らないのですよー。
