DbExecutor ver.2 - C#での生SQL書き補助ライブラリ
- 2011-04-29
データベース用のどこにでも転がっていそうなシンプルなユーティリティ、Part2。全面的につくり直しました(Part1もありました、一年前に公開してます、が、正直イマイチだった!)。
何でこの時期に?というと、睨んでいるのはWP7にSQLCEが乗るという話、です。まあ、Linq to Hogeが積まれるようなので、イラネーだろという話はあるのですが!あるのですが、それでも生SQLを使わざるを得ないシチュエーションは出てくるはずで、そのために、今のうちに作っておく/作りなおしておこうかと。まだWP7でどういう形で載るのか分からないので、今は普通に.NET 4 Client Profile用です。WP7へはSDKが出次第、すぐに対応させるつもり。
さて、どんな場合がターゲットかというと、生SQLを発行したい場合向け。大抵は軽くラップしたの作ってると思うんですが、そういう軽めのユーティリティとしてはベストなものを提供したいな、と考えました。この手のもので一番なのはEnterprise LibraryのDataなのでしょうか。確かに立派なんですが、見た感じ高尚すぎてお口に合いません(個人的にはかなり嫌いな雰囲気……)。古いものがベースのまま拡張している感がありありなところも見えるので、余分な贅肉をバッサリ切り落として極限までライトウェイトにしました。
ExecuteReader
基本的にはADO.NETのシンプルなラッパーです。生SQLを書いて実行を、少しだけ楽にサポートするという、それだけのものです。単純明快にIDbConnectionからインターフェイスだけで生やしているので、Sql Server, Sql Server Compactはもとより、Entity SQLでも動きます。SQL Azureもいけるかな。依存は極力廃したので、MySqlやOrcale、SQLiteでも(多分)動きます。但しプレースホルダは名前付きでないとダメなので(順序依存のものは動作を保証しません)、Accessとかはきっとダメ。
とりあえず、何もかぶせてないものとの比較で例を。DBは、例なので何でもいいんですが、Productsテーブルに、ProductNameとQuantityPerUnitとSupplierIDとUnitPriceというカラムがある。といったような代物です。ようは、Northwindですが。
// こんなデータ格納クラスがあるとして
public class Product
{
public string ProductName { get; set; }
public string QuantityPerUnit { get; set; }
}
//
var connStr = @"Data Source=NORTHWIND"; // Northwindサンプルから...
// 何もかぶせてない素の状態だと結果セットを取得するためのListを予め作ってAdd
// コマンドの準備も面倒、結果セットを回すのも定型句なのに行数沢山取ってシンドイ
var products1 = new List<Product>();
using (var conn = new SqlConnection(connStr))
using (var cmd = conn.CreateCommand())
{
conn.Open();
cmd.CommandText = @"
select ProductName, QuantityPerUnit from Products
where SupplierID = @SupplierID and UnitPrice > @UnitPrice";
cmd.Parameters.Add(new SqlParameter("SupplierID", 1));
cmd.Parameters.Add(new SqlParameter("UnitPrice", 10));
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
var p = new Product
{
ProductName = (string)reader["ProductName"],
QuantityPerUnit = (string)reader["QuantityPerUnit"]
};
products1.Add(p);
}
}
}
// DbExecutorを使うとパラメータは匿名型で生成
// 結果はIEnumerable<IDataRecord>からLinq to Objectsでさらさら書ける
var products2 = DbExecutor.ExecuteReader(new SqlConnection(connStr), @"
select ProductName, QuantityPerUnit from Products
where SupplierID = @SupplierID and UnitPrice > @UnitPrice
", new { SupplierID = 1, UnitPrice = 10 })
.Select(dr => new Product
{
ProductName = (string)dr["ProductName"],
QuantityPerUnit = (string)dr["QuantityPerUnit"]
})
.ToArray();
そのまんまだと、ただ結果取りたいだけなのに、物凄く行数を使うんですね。コマンドパラメータの追加なども大変面倒くさいし、結果セットの受け取りも大変。DbExecutorでは、この二つに対処するため、コマンドパラメータは匿名型で渡せるように、結果はIEnumerable<IDataRecord>の形で受け取ることができます。そのため、Selectした後は、ToArrayするなり、Linq to Objectsの操作にそのまま流れることが可能です。
この手のユーティリティでたまに見かける、SqlDataReaderだけを返すものは、あまり意味ないのではかな。やることなんて99%、グルッと回して行の値を取ることなので、それなら、そこまで面倒見てあげよう。IEnumerable<T>を返されても扱いようがなかった石器時代と違って、今はLinqがあるので、yield returnで返す。Stream的なものは全てIEnumerable<T>に変換する。そして全部Linqで処理する。それが現代の常識(キリッ
なお、基本的には静的メソッドで(DbConnection, SQL文字列, パラメータ(匿名型))という形でメソッドを呼びます。DbConnectionは実行完了時にDisposeするため、usingで囲う必要はありません。
ExecuteReaderDynamic
C#3.0の鉄則がストリームっぽいものはyield returnで返す、ならば、C#4.0の鉄則は、動的っぽいものは全部dynamicで返す。ことです。実に色々と楽になります。
// ExecuteReaderDynamicはIDataRecordをdynamicで包んだものを列挙する
// dynamicであることにより、カラム名のアクセスが自然に、また、キャストが不要になる
var products3 = DbExecutor.ExecuteReaderDynamic(new SqlConnection(connStr), @"
select ProductName, QuantityPerUnit from Products
where SupplierID = @SupplierID and UnitPrice > @UnitPrice
", new { SupplierID = 1, UnitPrice = 10 })
.Select(d => new Product
{
ProductName = d.ProductName,
QuantityPerUnit = d.QuantityPerUnit
})
.ToArray();
ExecuteReaderのものとの違いはSelectの箇所だけです。ExecuteReaderDynamicはIEnumerable<dynamic>を返し、そのdynamicの中身はIDataRecordをDynamicObjectで包んだものです。このことにより、見た目が更に自然に、また、煩わしい型変換をdynamicが自動でやってくれるので、キャストが不要になり、書くのがとても楽になります。
更に嬉しい特典はデバッガでの表示。
列挙中にブレークポイントを張って観察すると、動的ビューでカラム名と値、型が見えるようになります。(おっと、これはカラム名にスペースが入っているので動かないというツッコミが、いやまあ、はは……)
簡単な出力ぐらいならSelectを通す必要すらないです。
var query = DbExecutor.ExecuteReaderDynamic(new SqlConnection(connStr), @"
select * from Products where UnitPrice < @UnitPrice
", new { UnitPrice = 20.0 });
foreach (var item in query)
{
Console.WriteLine(item.ProductName + ":" + item.UnitPrice);
}
大変シンプルに書けますね。C#はLLですから!
ExecuteNonQuery/ExecuteScalar
ExecuteNonQueryやExecuteScalarも同じノリで行けます。
// パラメータはプロパティから取得するので、別に匿名型でなく普通のクラスでも可(insertやupdateで便利)
DbExecutor.ExecuteNonQuery(new SqlConnection(connStr), @"
insert into Products(ProductName, QuantityPerUnit)
values (@ProductName, @QuantityPerUnit)
", new Product { ProductName = "何か", QuantityPerUnit = "QUQNQUN" });
// パラメータが不要な場合は省略可
var serverTime = DbExecutor.ExecuteScalar<DateTime>(new SqlConnection(connStr), @"
select GetDate()");
パラメータは匿名型でなくても、普通のクラスでも可なので、UpdateやInsertの際に便利に使えるかと思います。クエリ文の@に書かれてないパラメータは無視されるので、クラス側に余計なパラメータがある分には問題ありません。
Select/SelectDynamic/Insert/Update/Delete
今までの4つは、IDataReaderの基本的な操作をラップしただけのものでしたが、他に、少し手の入ったメソッドを5つ用意してあります。メソッド名通り、Select/Insert/Update/Deleteをシンプルに行うためのものです。
- Select
select文の結果をIDataRecordを触ることなく、指定した型に移します。
// IEnumerable<T>を返し、カラム名とプロパティ名を自動でマッピングする
var products4 = DbExecutor.Select<Product>(new SqlConnection(connStr), @"
select ProductName, QuantityPerUnit from Products
where SupplierID = @SupplierID and UnitPrice > @UnitPrice
", new { SupplierID = 1, UnitPrice = 10 })
.ToArray();
O/Rマッパーというには烏滸がましいというか別にそんな大仰なものではなく、単純にselect文をオブジェクトに転写するという、それだけ。それだけなんですが、それだけのシチュエーションって結構多いですよね?これだけで、生SQLの苦痛が随分と癒される。そう、生SQLを書くのが嫌なんじゃなくて、最後にオブジェクトに手作業で対応付けるのが嫌だったんだよ、と思える程度には。
パフォーマンスもほとんど問題ありません。一回目の実行時にデリゲートの動的生成+キャッシュを行い、全てリフレクション経由ではなくデリゲート経由のアクセスを行うため、十分高速です。基本はneue cc - Expression Treeのこね方・入門編 - 動的にデリゲートを生成してリフレクションを高速化で書いたものですが、若干これ向けに修正してあります。
そうそう、今までのパラメータの匿名型渡しも同様にデリゲート生成していますので、匿名型渡しであることによる速度低下は全くありません。
- SelectDynamic
select文の結果をExpandoObjectに移します。
// IEnumerable<dynamic>で、dynamicの中身はExpandoObject
var products5 = DbExecutor.SelectDynamic(new SqlConnection(connStr), @"
select ProductName, QuantityPerUnit from Products
where SupplierID = @SupplierID and UnitPrice > @UnitPrice
", new { SupplierID = 1, UnitPrice = 10 })
.ToArray();
SelectDynamicもExecuteReaderDynamicも、共にIEnumerable<dynamic>なのですが、ExecuteReaderDynamicのdynamicは、あくまでIDataRecord、というよりもIDataReaderをdynamicでラップしたに過ぎないため、Selectで何かに射影するか、そうでなければシーケンシャルにしか値が取れません。SelectDynamicは、結果の一行一行をExpandoObjectに予め射影しているので、それだけで永続化されます。
使い勝手的にはDataTableが近い。実際、例えばASP.NETでは (Container.DataItem as dynamic).PropName とすることでデータバインドも行けます。また、ExpandoObjectは何気にINotifyPropertyChangedが実装されていたりするので、存外使い勝手は良いかもですね。
DataTableと違ってデバッガに非常に優しいのも嬉しい。動的ビューで中身が簡単に確認できます。
- Insert
Insertは指定したオブジェクトを元にInsertするというもの。
// テーブル名と対象オブジェクト(匿名型でも可)を渡すだけでInsert
DbExecutor.Insert(new SqlConnection(connStr), "Products",
new Product { ProductName = "何か2", QuantityPerUnit = "QOQOQUN" });
Insertって書くの面倒。table名(列名)values(@列名)。クソ単純なのに……。というわけで、テーブル名とオブジェクトを指定するだけで極々シンプルなinsert into valuesに変換されます(これはExecuteNonQueryの例で出したSQL文と同じものになります)。やりたいことがシンプルなとき、シンプルに書ける。そういうのがいいなって思っていて。
- Update
Insertと同じようなコンセプトです。
// where条件(複数の場合はand連結)とupdate対象を渡します
DbExecutor.Update(new SqlConnection(connStr), "Products",
new { ProductName = "何か!!!" }), // update対象
new { ProductName = "何か!", SupplierID = 100 }); // where条件
// 以下のようなSQLが発行されます
update Products set ProductName = @ProductName
where ProductName = @__extra__ProductName
and SupplierID = @__extra__SupplierID
第三引数にupdateする値を、第四引数にwhereの条件を、。whereの条件は必須なのと、また、複数の場合はandで連結されます。発行されるSQLに__extra__というのが付くのはupdate対象とプロパティ名が被っても大丈夫なようにするため、なので、特に気にしなくてもいいです。比較的そのまんまなSQLに変換される、とだけ分かってもらえれば。
- Delete
Updateと同じような(以下略)
// delete条件を渡します(複数の場合は例によってand連結)
DbExecutor.Delete(new SqlConnection(connStr), "Products",
new { ProductName = "何か2!" });
// 以下のSQLが発行される
delete from Products where ProductName = @ProductName
ちなみに、nullだけを削除といったようなことは出来ません。いや、isnullがコマンド渡しで書けないからね…… そういうのは普通にExecuteNonQueryで書いてくださいな。そういえばでそれと、Insert, Update, Deleteは内部的にはExecuteNonQueryを実行しているので、戻り値は影響された行の個数が返ってきます。
これらは、あくまで補助的なものとして用意したので、生SQL文を完全に代替することは最初から意識していません。それが当てはまるシンプルなシチュエーションで、シンプルに書けること。それが目的です。
接続を維持しての複数クエリ/トランザクション
今までの例は全て静的メソッドの、一接続一実行の例だけでしたが、接続をつなぎっぱなしにしたりトランザクションをかけたりも出来ます。usingで囲んでnewでインスタンス化すればOK。
// 静的メソッドではなくnewすればDisposeまで接続をCloseしないモード
using (var exec = new DbExecutor(new SqlConnection(connStr)))
{
// 今まで第一引数に渡していたコネクションが(当然)不要になる
var count = exec.ExecuteScalar<int>("select count(*) from Products");
// なお、ストアドプロシージャの実行は第三引数でCommandTypeを変更すればOK
var twoyears = exec.SelectDynamic("Sales by Year",
new { Beginning_Date = "1996-1-1", Ending_Date = "1997-12-31" },
CommandType.StoredProcedure)
.ToArray();
}
// 第二引数にIsolationLevelを渡すとTransactionがかかります
using (var exec = new DbExecutor(new SqlConnection(connStr), IsolationLevel.ReadCommitted))
{
// こんな露骨でなくても、配列上に沢山オブジェクトがあって
var products = Enumerable.Range(1, 10)
.Select(i => new
{
ProductName = "Test!",
SupplierID = i
});
// サクッと一気にInsertするとか、あったりなかったり
foreach (var product in products)
{
exec.Insert("Products", product);
}
exec.TransactionComplete(); // usingを抜ける前にこれを呼び出せばCommit、呼び出さなければRollback
}
new DbExecutorの際にIsolationLevelを渡すとトランザクションがかかります。TransactionScopeのように、確定させる際は最後にTransactionCompleteを。TransactionCompleteが実行されなかった場合はRollbackされます。なお、別に普通にTransactionScopeを使っても問題ありません。
まとめ
生SQLなんて、好きじゃない!どうやったって、異物だもの。生SQL文じゃないストアドプロシージャならいいかといえば勿論そんなわけはなく、呼び出し時のパラメータと値の受け取りが……。むしろ、実態のSQL文がコードと相当離れたところに置かれ、見通しが低下するわけで、それなら逐語的文字列リテラルでC#コード中に埋めたほうがいいよ。逐語的文字列リテラルのない言語だったら、悪夢すぎて考えたくないけれど。逐語的文字列リテラルの何がいいかって、コピペでSQL Server Management Studioに移せるところなんだよね。そして逆も然りで。XMLか何かに外出しも当然イマイチで、そんなことやるぐらいなら文字列埋め込みのほうがずっといい。んー、でもS2Daoの2Way SQLというのはいいですね。パラメータの修正などもせずManagement Studioでそのまま実行可能、という。Linq to Sql/EntitiesもLinq Padを使うことでそれらしいことは出来るかな?
と、まあ、なにはともあれで、SQLをどれだけ嫌ったところで、現実問題付き合っていかなければならない。ことはなくLinq to Hogeを使いたい。けど、無理なら、それならせめて軽やかに扱いたいよね、とは皆思うはずで、皆それぞれの俺々ユーティリティは用意されていると思いますが、私も作ってみました(一年ぶりに再チャレンジで)。
特徴はIEnumerableベース(Linq to Objectsに乗っかる基盤)であることと、匿名型を多用したパラメータの受け渡し、ExpressionTreeを用いた動的デリゲート生成による高速化、dynamicによるシンプルなアクセサ。C#3, 4の機能を満遍なく使って、軽快に書けるようにしたつもりです。生SQLを扱うわりには、かなりLL的な軽さは出せてるのではないかとー。とにかく簡素なAPIになるよう気を使いました。ついでに、今回はCode Contractsも全面的に導入しています。
とりあえず、SQLCEも4.0になってDLLのみでよくなって、更にはNuGetでサクッと用意できてと(EF CodeFirstも用意できる)、C#でもデータベースがもんのすごく身近に扱えるようになりました。とてもいい事です!というわけで、生SQLのお供に是非どうぞ。Linq to SqlやLinq to Entities使っていても普通に共存出来ますので~。
まあ私は生SQLよりもCodeFirstにしたいですが!生SQLなんてどうでもいいのでLinq to Entitiesでキャッキャウフフしたいです。そんなわけでDbExecutorのテストに使ったDBは、SQLCE4+EF CodeFirstで組んであったりして。Code FirstでDB組んだのにLinq to Entitiesではなく生SQLでアクセスするとか大変モニョる。
4/30追記
ver.2.0.0.1に。ExecuteScalarの契約で事後条件を!=nullとしていたのですが、大間違いで普通にnullりるので。DbNullが返ってくるものと勘違いしていてAssumeを足してわざわざ抑制してたんですが、全くもってダメダメな対応だった……。わざわざ(静的チェッカが)警告してくれたのを、深く考えずAssumeで消すとは、愚かすぎる。そして、1日で差し替えたのにStableリリースと言い張ったことを深く反省します。なお、リリースバイナリから事後条件は削除されているので、誤った契約による問題は、静的チェッカが正しく動作しない(nullではないとマークされる)ことだけになります。その程度の軽い障害なのだから良いかといえば、勿論全然よくはなく、本当にすみませんでした。
流れるようなインターフェイス vs 生SQL
で、思い出した。基本的には私は「流れるようなインターフェイス」自体が大嫌いというのもありますが、Seasar2 - S2JDBCのような仕掛けは全く無意味だと思いますね。Linqの式木のような深い解析が出来なければ、こういうのはただのファッションで、別に書きやすくも何ともないと思っていて。それなら生SQLのほうが遥かにマシだと。だから私はDbExecutorでは前段は生SQL、後段にLinq to Objectsという形体を取っています。「生SQLは避けられないもの」という認識が大前提のうえで、それを如何にサポートするかが主眼です(まあ、なので単純に比較しても意味のないところですが)。また、C#にはLinq to Entitesもあることですし、欠けてる部分を上手く補完出来ればというのが願うところです。
XboxInfoTwit - ver.2.3.0.4
- 2011-04-29
Twitterの認証周りが変わって、新規認証が出来なくなってしまってたので、それを修正しました。
Tester-DoerパターンとCode Contracts
- 2011-04-26
僕と契約して安全性の高いソフトウェアを作ってよ!というだけじゃ、何か、弱い。動機付けに足りない。という、分かったような分からないようなCode Contracts。困ったところは、で、何が嬉しいの?にたいする積極的具体的な動機付けを提供しにくいということ。契約をしっかり行うことで、強固なソフトウェアが設計出来ます。うーん、理念は分かりますけど実用的に便利ー?if hoge==null throw に毛が生えた程度のものだったら、ちょっとよくわからない。
// こういうコード見るともう目も当てられなくて、画面の半分が引数チェックで埋まってるよ!
public void Hoge(string arg1, string arg2, string arg3)
{
if (arg1 == null)
{
throw new ArgumentNullException("arg1");
}
if (arg1.Length == 0)
{
throw new ArgumentException("arg1");
}
if (arg2 == null)
{
throw new ArgumentNullException("arg2");
}
if (arg2.Length == 0)
{
throw new ArgumentException("arg2");
}
if (arg3 == null)
{
throw new ArgumentNullException("arg3");
}
if (arg3.Length == 0)
{
throw new ArgumentException("arg3");
}
// やっとメソッドの本体...
}
うん、これは、イヤ。ifは必ず{}をつけなければ、とやると行数が嵩んで最悪の視認性に。個人的には、明らかに一行な処理はifの真横に書いてもいいと思う。
// これぐらいなら許す(えらそう)
public void Hoge(string arg1, string arg2, string arg3)
{
if (string.IsNullOrEmpty(arg1)) throw new ArgumentException("arg1");
if (string.IsNullOrEmpty(arg2)) throw new ArgumentException("arg2");
if (string.IsNullOrEmpty(arg3)) throw new ArgumentException("arg3");
// ↑もしくはGuard.NotNull(arg1, "arg1"); とか用意するなど、ね。
}
そんなわけかで、この手のnullチェックが好きでなくて、必要性だって、どうせ次の行のその引数使うところで死ぬんだからどうでもよくね?と思う場合があまりにも多いともにょもにょもにょ。書くけど書かないけど。
その延長線上でContractsも面倒くさいしなー、と思っていた時もありました。しかしCode Contractsは、あらゆる方向から契約を積極的に行うための動機付けを提供してくれています。Premiumにしか提供されていない静的チェックが最も強力なのは確かですが、Standardのユーザーのためにも、ドキュメント生成、IntelliSense表示サポート、Pex自動テスト生成サポート、引数名を文字列で書かなくていい。などなど。
そこまであの手この手で、契約するといいよ、と迫ってこられれば納得です。理屈上素晴らしいから、というだけじゃなくて、何だかんだで面倒くさいものを、現実的にこんなにメリットがあるから契約しようよ!という。そういう姿勢がいいよね、普及させるために全方位から攻めるというの。それだけ並べられれば、そりゃ書くってものですよ?
特にIntelliSense厨の私は、IntelliSenseへの契約表示に感動しまして。Enumerable.Rangeで条件が表示されてるよ、きゃー!って。そして、BCLが契約を表示してくれるなら、自前のクラス群も契約表示させてあげたい。と、いうのが一番のCode Contractsやろう!という動機付けになりましたね。頻繁にクラッシュするんですが、その辺は多めに見てあげます。
Tester-DoerパターンとCode Contracts
Code Contracts自体は第54回CLR/H勉強会発表資料を公開します。 - Bug Catharsisの「オブジェクト指向と契約による設計」と「Code Contracts入門」という資料が素晴らしいので、今すぐそちらを見たほうがいいです!
というわけでCode Contracts自体には全く触れないで、例でも。
// Addのないstring, string辞書(これはひどい)
public class StringDictionary
{
Dictionary<string, string> dict = new Dictionary<string, string>();
public bool ContainsKey(string key)
{
return dict.ContainsKey(key);
}
public string Get(string key)
{
return dict[key];
}
}
static void Main(string[] args)
{
var dict = new StringDictionary();
// 存在しないキーをGetすると例外
dict.Get("hogehoge");
// チェックしてから取得する(Tester-Doerパターン)
if (dict.ContainsKey("hogehoge"))
{
dict.Get("hogehoge");
}
}
取得時にダメな可能性があるものは、先にチェックしてから取りに行く。といったことは、.NETのクラスライブラリ設計に書いてあるので読もう~。とてもお薦め本。
問題は、キーの存在チェックをすることがほぼ必須なのに、それを強制出来ないんですね。あくまで任意でしかなく、別にチェックしなくてもコンパイラ通るし、そうして書かないでいるとうっかりな例外発生の可能性を常に抱えてしまう。こういう問題は例外自体にも言えますが。ほぅ、では検査例外が必要か…… いえ、あれはいりません。
まあともかくで、ここでCode Contractsを使うとどうなるか、というと……
public class StringDictionary
{
Dictionary<string, string> dict = new Dictionary<string, string>();
[Pure] // Pureじゃないと怒られるのでPureってことにしておこう(善意の申告制です)
public bool ContainsKey(string key)
{
return dict.ContainsKey(key);
}
public string Get(string key)
{
Contract.Requires(ContainsKey(key)); // 事前条件 ContainsKeyがtrueでなければダメ
return dict[key];
}
}
static void Main(string[] args)
{
var dict = new StringDictionary();
// ContainsKeyの前にGetしようとすると...
dict.Get("hogehoge");
}
静的チェッカーが警告を出してくれます。いいねいいね。そんなわけで、Code Contractsを使うと、比較的安全にTester-Doerパターンが適用できるのでした。こういう、コードだけでは表現できない約束事を表現でき、実行時ではなくコンパイル時に検出出来るようになる、っていうのは、魅力的な話なのではと思います。
まとめ
Code Contractsは一部で無理矢理感が否めません。そもそもバイナリリライター必須なうえに、文法的にもContract.Requiresぐらいならまあいいとしても、Ensures(Contracts.Result)やInvariantやContractClassForはC#的に不自然さを残す介入の仕方で、些か残念と言わざるをえない。
不自然さ漂う記述方法がC#に統合される(それSpec#)日は来るのだろうか(絶対来ないよね)。Spec#は軽く仕様とTutorialを眺めた感じだと、やはり言語統合されてると、洗練されてるし、書きやすさも段違いになるよねえ、などと思いました。全ては必要ないけれど、一部はC#に入って欲しい、のですが文法と衝突しなくても、既存のコードとルールが衝突してしまったりするので難しいかなあ。
とはいえ、Code Contracts全体のエコシステムがもたらすメリットも多大だし、mscorlibすらContractsが書き足されている(.NET4から)ぐらいなので、今のうちにそれに従って流行りものに乗っかるのもいいと思います。流行ってるか謎ですが。単純なnullチェックぐらいならサクッと書けますが、少し凝った契約をしようとすると途端にワケワカランし正道がサッパリ。という敷居の高さはありますが……。
実際ヨクワカラナイデス。少しよーし、張り切って書いちゃうぞー、とやると、それダメそれダメ、とリライターに言われてしまったりでnullチェックに毛の生えた程度しか使いこなせない昨今です。それだけでも有益といえば有益なんですが、あちこちでrequires not null, ensures not nullを書いていると、もうデフォルトを非nullにしてくれよ!と叫ばずにはいられない。何ともいえない不毛感がちょっと、かなり、嫌。
あと、静的チェッカが上手く機能するように書くには静的チェッカが必要なのも。当たり前?うーん、静的チェッカなしで、ただ普通にContractsを書いているだけじゃあダメなのかな?契約自体は成立していてリライターは通るけど静的チェッカ使うと警告だらけ。みたいな形になりがちで。Standardで書いてPremiumでチェックしたら涙目の落差が激しすぎて、じゃあ結局は静的チェッカ必須なの?でもそれPremium以上じゃん、というのが残念で。そうなるとStandardにも欲しいねえ、静的チェッカー。
でも、ちょっと気の利いたGuard句として、事前条件だけで画面半分が埋まるような事態が緩和されるなら、それはとっても嬉しいなって。まずは、その程度から始めよう。順を追ってステップアップすればいいのだから。
Expression Treeのこね方・入門編 - 動的にデリゲートを生成してリフレクションを高速化
- 2011-04-20
Expression Treeは、IQueryableの中心、Code as Dataなわけですが、それ以外にも用途は色々あります。ただたんに名前を取り出すだけ(考えてみると贅沢な使い方よね)とか、デリゲートを生成したりとか。varはLinqのために導入されたものだからそれ以外に無闇に使うのは良くない(キリッ とか言う人は、式木も同じ考えなんですかね、匿名型へも同じ態度で?導入された、そして発展させたのはLinqだとしても、別にそれ以外に使ってもいいんだよって。縛られた考えイクナイ。
というわけで、今更に、初歩からの式木再入門。特に.NET 4から大幅に拡張されて式だけじゃなく文までいけるようになって、何でも表現出来るようになりました。式木の用途は多岐に渡るわけですが、今回はリフレクションの高速化をお題にしたいと思います。プロパティ名の文字列からPropertyInfoを取ってGetValueといったように、動的に値のGet/Setをするわけですが、それを動的コード生成で高速化しよう!
方針としては、プロパティアクセスのデリゲートを生成します。GetだったらFunc<object, object>を作って、引数が対象インスタンス、戻り値が取得結果の値、といった具合です。まんまPropertyInfoのGetValueがデリゲートになったもの、といった具合。
では、実際に書きながら見ていきます。というわけで、私の書き方などりを順を追って。まず、何をやりたいかを明確にするため、実際のコードで、具体的なラムダ式を書いてコンパイル通します。
// 適当なクラス
class MyClass
{
public int MyProperty { get; set; }
}
static void Main(string[] args)
{
// これで書くと(.NET 4以降の)代入やループはサポートされてないので、Funcでも全然いいです
// ただ、デバッガで生成された式の結果が見えるので、Expressionでコンパイル通せたほうが楽かな
Expression<Func<object, object>> expr =
target => ((MyClass)target).MyProperty;
}
コンパイル通るということは、それで書けるということ。机上で、頭の中だけで考えてもいいことありません。ささっとコンパイラ使いませう。そして一旦デバッガでexprを覗いてみますと
// ToString()
// target => Convert(Convert(target).MyProperty)
// DebugView
.Lambda #Lambda1<System.Func`2[System.Object,System.Object]>(System.Object $target) {
(System.Object)((Program+MyClass)$target).MyProperty
}
Expressionで宣言して書くというのは、コンパイラがコンパイル時に式木を生成するということです。見た目はFuncに毛が生えた程度なのに、コンパイル結果は大違い。なんて恐ろしい!そして、なんて素晴らしい!ともあれ、結果から逆算してくのが手っ取り早い。大枠は機械生成に任せてしまって、微調整だけを手動でやればいいわけで。無から作るのは大変ですが、枠組みが出来ているなら簡単だもの。このToStringとDebugViewは大変便利です。なお、リフレクタで生成結果を見るという夢のない方法もあります。
上記結果から、具体的な成分に置き換えた、何を書くのかの式をイメージ。
// (object target) => (object)((T)target).PropertyName
もう、あとは機械的に置き換えていくだけ!というわけで、具体的にこねこねしていきますが、まずパラメータとラムダ本体を用意します。この二つは、最終的にデリゲートの生成を目指した式木の作成では定型句みたいなものなので何も考えず用意。
// まず、引数のパラメータとLambda本体を書く
var target = Expression.Parameter(typeof(object), "target");
var lambda = Expression.Lambda<Func<object, object>>(
/* body */
, target);
基本的に、埋めやすいところから埋めていくのがいいのではないかなー。そして、Expressionは最後の引数が一番埋めやすいので、外から内に向かって書いていくことになります。最初のLambdaは、引数パラメータを最初に置いてしまって、bodyは後回しにする。というわけで、これで左辺は終了しました。次は右辺。外周から構築されるので、まずobjectへのキャスト。これはExpression.Convertです。
var lambda = Expression.Lambda<Func<object, object>>(
Expression.Convert(
/* body */
, typeof(object))
, target);
当然ですが、ちゃんとインデントつけたほうがいいです。カンマ前置は気持ち悪いですが、こうして後ろから埋めていく時は、こっちのほうが書きやすいかなー。気持ち悪ければ最後にまとめて直せばいいのではないかと。
次はTへのキャスト、ではなくプロパティ呼び出し。実行の順番はTにキャスト→プロパティ呼び出し→objectにキャストですからね。プロパティ呼び出しはExpression.Property。ですが、フィールドも似たようなものだし、幸いExpressionには両者を区別しないPropertyOrFieldがあるので、そちらを使いましょう。名前はstringで渡しますが、とりあえず"PropertyName"で。
var lambda = Expression.Lambda<Func<object, object>>(
Expression.Convert(
Expression.PropertyOrField(
/* body */
, "PropertyName")
, typeof(object))
, target);
最後は((T)target)。(T)は後で置き換えるとして、とりあえずMyClassにしておきますか。targetは、最初に作った右辺のパラメータです。
var target = Expression.Parameter(typeof(object), "target");
var lambda = Expression.Lambda<Func<object, object>>(
Expression.Convert(
Expression.PropertyOrField(
Expression.Convert(
target
, typeof(MyClass))
, "PropertyName")
, typeof(object))
, target);
埋まった!埋まったらとりあえずまずコンパイル。するとPropertyNameはMyClassにないよ、と例外出て終了。ふむふむ。ところで、ここでチェック入るんですね、へー。それでは、別関数に分けることを意識して、(ようやく)変数用意しますか。
// あとで関数の引数にするとして
var type = typeof(MyClass);
var propertyName = "MyProperty";
var target = Expression.Parameter(typeof(object), "target");
var lambda = Expression.Lambda<Func<object, object>>(
Expression.Convert(
Expression.PropertyOrField(
Expression.Convert(
target
, type)
, propertyName)
, typeof(object))
, target);
コンパイル通ったー。そしたら、とりあえずデバッガでlambda変数を観察。ToString結果とDebugViewプロパティを見るといいでしょう。
// lambda.ToString()
target => Convert(Convert(target).MyProperty)
// lambda.DebugView
.Lambda #Lambda1<System.Func`2[System.Object,System.Object]>(System.Object $target) {
(System.Object)((Program+MyClass)$target).MyProperty
}
問題なさそうですね!この二つは作るときに非常に便利なので、大きめのを書くときは断片を書いてこれでチェック、みたいにするといいかも。では、最後にデリゲート生成(Compile)を。
// デリゲート生成!
var func = lambda.Compile();
// てすと
var test = new MyClass { MyProperty = 200 };
var result = func(test);
Console.WriteLine(result);
というわけでした。DynamicMethodでILもにゃもにゃ(敷居高すぎ!)とか、Delegate.CreateDelegateだのでもにゃもにゃ(面倒くさい!)に比べると、随分素直に書けて素敵。.NET 4.0からはブロックやループなど、式だけではなく全ての表現が可能になったので、動的コード生成が身近になりました。
Setのほうも同様な感じに書けます。
// (object target, object value) => ((T)target).memberName = (U)value
static Action<object, object> CreateSetDelegate(Type type, string memberName)
{
var target = Expression.Parameter(typeof(object), "target");
var value = Expression.Parameter(typeof(object), "value");
var left =
Expression.PropertyOrField(
Expression.Convert(target, type), memberName);
var right = Expression.Convert(value, left.Type);
var lambda = Expression.Lambda<Action<object, object>>(
Expression.Assign(left, right),
target, value);
return lambda.Compile();
}
// Test
static void Main(string[] args)
{
var target = new MyClass { MyProperty = 200 };
var accessor = CreateSetDelegate(typeof(MyClass), "MyProperty");
accessor(target, 1000); // set
Console.WriteLine(target.MyProperty); // 1000
}
Expression.Assignが代入なのと、objectで渡されるvalueは、プロパティに代入する際にプロパティの型へキャストする必要があるので、left.Typeで取り出しています。これ、Lambdaの中に一気に書いてしまうと値が取れないので、外で書く必要があるのが少々面倒かしらん。とりあえず、コメントで生成後の式を書いておいてあげると見る人に(少しだけ)優しい。
今回のSet/Getは微妙に汎用的なものにするため全てobjectで扱っていますが、ジェネリクスにすれば、余計なExpression.Convertがなくてスッキリ記述+パフォーマンスも向上、が狙えそうですねん。
何で突然?
思うところあって、じゃなくて、WP7にSQLCE搭載の報を受けて、以前書いたデータベース用ユーティリティを書き直そうと思いまして。そして、コマンドパラメータの受け渡しには匿名型を使おうかな、と。そうすると、書くのがとても簡単になるんですね。凄く軽快で。これは良い。のですけど、実行の度にPropertyInfoを取ってきてNameとGetValueでの値取り出しはどうかと思ったわけです。そりゃねーよ、と。そこで、じゃあ、キャッシュしよう。キャッシュするならPropertyInfoをキャッシュしたってそんな速くはない、やるならデリゲート生成までやろう。と、紆余曲折あってそうなりました。
パラメータだけではなくて、簡易マッパー(selectの各カラムの名前とプロパティ名からインスタンス生成)も用意しているのですが、それもデリゲートのキャッシュで高速化効いてくるかなー、と。
で、速いのかというと、うーん、生成のコストが結構高いので、平均取ると、PropertyInfoのキャッシュと比べると、数千回実行しないとコスト回収出来ないかも。PropertyInfoのGetValueも遅い遅いというほどにそんな遅くないのかなあ、いや、デリゲートと比べると十数倍ぐらいは違うんですが、しかし。マッパー的に使って、一回の実行に100行取ってくる、とかだったら余裕ですぐ回収出来ますが、コマンド程度だとどうだろうなー。まあ、ASP.NET MVCなんかはTypeDescriptor(正直遅い)経由でやってるみたいだし、それと比べれば悪くないかもはしれない。でもWP7を見ると、アプリケーションのキャッシュ生存期間を考えると、ペイ出来そうな気がしない。
とはいえ、初回に少し重くてあとは高速、のほうがユーザーエクスペリエンス的にはいいかな、と思うので(あと、どちらにせよたかが知れてる!)、式木デリゲートキャッシュは採用の方向で。スッキリしてて、C#らしい美しさなところも好き。で、まあ一応、速度を気にしてのことなので、ベンチマークを取ったりするわけですが、生成時間が気になる……。エクストリームにハイパフォーマンスなシリアライザを作る!とかってわけじゃないので、あんまキチキチに気にしてもしょうがないのですが、でもちょっと気になる。Boxingが~、とかも少し、でもそれは放置として。
リフレクションは遅いから情報をキャッシュするとかのお話 - Usa*Usa日記 [雑記] 動的コード生成のパフォーマンス (C# によるプログラミング入門) 動的プロキシなViewModelの実装とパフォーマンスの比較(MVVMパターン) - the sea of fertility 効率の良い実行時バインディングとインターフェイス指向プログラミングでの boxing の回避テクニック - NyaRuRuの日記 パフォーマンスのための Delegate, LCG, LINQ, DLR (を後で書く) - NyaRuRuの日記 インライン・メソッド・キャッシュによる動的ディスパッチ高速化 - @IT ByRef parameter and C# - 猫とC#について書くmatarilloの日記 c# - How does protobuf-net achieve respectable performance? - Stack Overflow HyperDescriptor: Accelerated dynamic property access - CodeProject Making reflection fly and exploring delegates - Jon Skeet: Coding Blog patterns & practices – Enterprise Library(Data) MetaAccessor クラス (System.Data.Linq.Mapping) TypeDescriptor クラス (System.ComponentModel) AutoMapper csharp/msgpack at master from kazuki/msgpack - GitHub cli/src/MsgPack at master from yfakariya/msgpack - GitHub
ネタ元。先人が百億光年前に辿ってきた話だ、的な何か。コードは、シリアライザやマッパーはこの辺の仕組み載せてるよね、という当たりをつけて、ソースコードをPropertyInfoやDictionaryで検索してヒットした周辺を眺めるなど。で、まあ、分かったような分からないような。キャッシュの仕組みと含めて、上手くまとまったらDynamicJsonにも載せようと思っているんですが。一週間ぐらい延々と弄ってるんですが、どうも固まらなくて。この辺、コード書きの遅さに定評のある私です(キリッ。
ちなみにWP7には今のところExpression.Compileはないんですけどね!(Betaの頃はあったけど削られたよう)。SQLCE搭載でLinq to Hogeも搭載するはずなので、それと一緒に復活するはずと信じています。あとSL4相当じゃないとExpression.AssignやUnboxが使えなくてどちらにせよ困るので、MangoではSL4相当にグレードアップしてくれないと。もし一切変わらなかったら、IL生成はもとから出来ないし将来も搭載されないと思うので、適当に何かでお茶濁しますか。
Linq と Windows Phone 7(Mangoアップデート)
- 2011-04-14
今冬に予定されている大型アップデート(Mango)に向けて、MIX2011で大量に情報出てきました。色々ありましたが、その中でも目を引いたのがデータベース対応とQueryable対応。データベースは恐らくSQL Server Compact 4.0相当で、これはいいものだ。ただ、生SQL文かあ、という思いもあり。他のやり方ないのかなあ。オブジェクトDBとか?などと夢見てしまうけれど、高機能なエンジン(SQLCE4)がある以上は、それをそのまま使ったほうがいいのは自明で。
インピーダンスミスマッチを解消する、そのためのLinqでしょ!と、ああ、そうだね。でもLinq to Sqlはないし、Linq to Entitiesが乗っかるわけもないし、策はなく普通に生SQLの気もするなあ。どうでしょうね。どうなるのでしょうね。で、Queryable対応も発表された、というかデモられていました。
これでDB+Linqがあることが保証されたから安泰。何のQueryProviderと見るべきかしら。この辺、不透明なので、情報入り次第しっかり追っかけます。
Queryableの難点は、見た目はスッキリしてますが、コンパイル後の結果は式木のお化けになる+実行時は式木のVisitというわけで、軽い処理とは言い難いところなのですが、軽いとか重いとか気にしないし!じゃあなくて、MangoではGC改善など性能向上も入るようなので、十分なパフォーマンスは確保できそうです。あと、ユーザーエクスペリエンスを損なうボトルネックは結局そこじゃあないんだよ、的なこともありそうだし。
例えば現行のWP7のListBoxは致命的に重くて、純正アプリは軽いのにユーザー作成アプリではクソ重く、純正だけネイティブでチートかよゴルァ、という勢いだったんですが(Mangoで改善されるそうです!)、これの要因の一つは画像の読み込み周りがアレだったことにあるようで……。
CocktailFlowという素敵な見た目のアプリがあるのですが、これはその辺の問題に対処するためか、事前にLoadingを取って、完全に読み込み終わってからしか表示しないようになってます。でも、それはそれでネットワーク読みに行ってるわけでもないのにLoadingが長くて、微妙だなあ、と。
MIX11でのRx
二つセッションがありました。一つはLinq in Actionの著者(最初期に出たものですが、良い内容でしたね)によるWP7でのRx活用例。
マイクのボリュームが不安定で聴きにくいですー。話はPull(Enumerable)とPush(Observable)の比較から入ってますね。MoveNextして、MoveNextして、MoveNextして、ええ、ああ、メソッド探訪第7回:IEnumerable vs IObservableの辺りで書きましたね~。それのEnumerbaleサイドのリライト版であるLINQの仕組みと遅延評価の基礎知識の続きとして「Rxの仕組み」を書きたいのですが書くと思ってもう3ヶ月も経ってる、うぐぐ……。
話がそれた。んで、話の序盤は結構退屈です。IEnumerator vs IObserverとかつまらないです、って、あ、私もやってたっけ……。話の流れを上手くつながないと何か唐突なのね。さて、デモはサイコロ転がしを完成させていくというもの。どんどんコードがメソッドチェーンのお化けになっていく様はどこかで見たことある(笑)。最初は、重たい処理を、ObserveOn(Scheduler.ThreadPool)とするだけで実行スレッドを切り替えて、CPUバウンドの処理を簡単に同期→非同期に変換してUIをブロックしないというデモ。ちなみにObserveOn(Scheduler.ThreadPool)じゃなくて、これはToObservableの時点でScheduler渡したほうがいいんじゃあないのかなあ。まあ、大して違いはないといえばないですがー。
次は加速センサーをRxに変換して、TimestampをつけてShakeされたことを検出するというもの。生のセンサーイベントをRxで事前に加工して扱いやすくする、という手ですね。私もUtakotohaの再生イベントでそれやったよ!(いちいち対抗心燃やして宣伝しなくてよろし)。ObservableだとMockに置き換えやすいんだよ、という話もね。気づいたらメソッドチェーンのお化けになっているのも。Linq使いの末路はみんなこんなのになってしまうという証明が!
セッションはもう一つ。RxチームのBart De SmetによるRxJSのセッション。MIX2011はIE10と合わせてHTML5推しもありましたしね。
最初の20分は、ObservableとObserverの購読概念などについて簡単な説明。あとRange,FromArray,Timerといった生成子。Pullとの比較がない、つまり"Linq"とは全く言わないのがJavaScript向けですね。喩えもHTML5のGeolocation APIが、などなど新鮮。そして、だからこそ分かりやすいかもしれません。その後はいきなりGeolocation APIをラップする自作Observableを作ろうが始まって難易度が、まあLevel300だものね。実用的な話ではあるし、Observableを作るということは仕組みを理解するという話でもあるので、こういうのも良い流れ。ほか、KeyUpを拾ってネットワークを問い合せてのリアルタイム検索(event+asynchronousの合成例)など。これはハンズオン資料でもお馴染みの例ですが、順に追ってデモしてもらえるとThrottleの意味は凄く分かりやすいですね。Switchも強力だけど分かりづらさを持つので、こうして丁寧に解説してもらえると、いいね。ともかく、概念としてC#もJavaScriptも超えたところにあるので、考え方、コード例はC#/WP7でも使えます。良いセッションだと思いますので見るのお薦め。
そういえばで、Async CTPが更新されていて、一部変更があったので、RxとAsyncとのブリッジライブラリ(Rx-AsyncLinq)が動かなくなったと思われるので、それに合わせて近いうちにRxもアップデートありそうな気がします。RxJSもここずっと更新とは遠かったのですが、こうしてセッションもあったことだし、大型アップデートはあってもおかしくないかな。
Async CTPはufcppさんの記事Async CTP Refreshに詳しく書かれていますが、ライセンス形態がご自由にお使いください(※但し自己責任)へと変更、日本語版でも入るようになった、WP7対応、といった具合に、WP7開発でも今すぐ投下しても大丈夫だ問題ないになったのは大きい。
他言語からWP7開発のために来たので、C#の経験があまりなくてLinqがイマイチ分からない。でも非同期ダルい。という人には、Rxよりも、Asyncのほうがずっと扱いやすく応えてくれるので、なし崩し的に投入してしまっていい気がしますねー。勿論、ある程度は大きな変更に対応していく気は持ってないとダメですが。
ある程度Linqが分かる人なら(別にそんな大仰なスキルを要求するわけじゃなく、SelectしてWhereして、Linq便利だな~、ぐらいでいいです!)Rxを学ぶのもお薦め。基本的にはLinqに沿っているので、学習曲線はそんなにキツくないです。特に、非同期をラップしてWhereしてSelectしてSubscribe、ぐらいならすぐ。他のは、徐々に覚えればいいだけで。
まとめ
WP7はRx標準搭載で、ただでさえLinq濃度が高かったというのに、Queryable搭載でLinq天国に!もはやWP7が好きなのかLinqが好きなのかよくわかりませんが(多分、後者です)、ともかく最高のプラットフォームなのは間違いない。
ところでDay1ではnugetが割とフィーチャーされていたのですが、その背景、よくみると……
右のほうにlinq.jsが!すんごく嬉しい。
Utakotoha - Windows Phone 7用の日本語歌詞表示アプリケーション
- 2011-04-09
マーケットプレイスに通り、公開されました。フリーです(下で述べますがソースも公開しています)。再生中の音楽のアーティスト・タイトルを元に自動で検索して、歌詞を表示する、というものです。海外製の同種アプリとの違いは、対象が日本の曲ということになります。下記バナー、もしくはマーケットプレイス検索"utakotoha"でどうぞ。アプリケーション名は例によってヒネりなしで「歌詞」からそのままとりました。うたのことば。Utakata TextPadと名前が似ていますが偶然の一致です(ほんと)
スクリーンショットでは格好つけてズームインして隠蔽していますが、スクリーンショット用詐欺なだけで、実際はこんな感じです。
ようは、単純にWebBrowserにgoo歌詞を出しているだけです。ダブルタップでエリアに沿って幅一杯の拡大してくれるので、誤魔化し的にはまぁまぁ。WebBrowserを経由する理由は、APIが提供されているわけでもないので、権利関係考えるとこうでないとマズいかな、といったところで。goo歌詞を選んだ理由も直リンが許可されていたからです。
あと、WP7はテーマに黒背景のDarkと白背景のLightがあるわけですが、goo 音楽の背景は強制白で、どうもミスマッチなわけですね。アプリ自体を完全に白背景にしてしまうのもいいかな、とは思ったんですが、ブラウザのCSSを書き換えて黒背景にするという手を(ネタとして)取ってみました。オプションでOFFに出来るというか、デフォルトはOFFです。ズームすれば違和感はないんですが、画面いっぱいに広がってるものでは、微妙度極まりない。
ほか、Twitterに再生中楽曲を投稿する機能があります。
ソースコード
CodePlex上で公開しています。単機能アプリですので、コード規模は小さめです。あまりしっかりとはしてませんが、ユニットテストなども書いてあります。
WP7のReactive Extensions実践例サンプル、のつもりで書いたので、Rxを全面的に使っています。むしろRx縛りと言ってもいいぐらいにRxのみでやるのを無駄に貫いています。意味がなくてただ複雑化しただけの箇所多数。UIと絡めて使うのがイマイチ分からず振り回されてますねー。とはいえ、バッチリはまった部分も勿論あり。というわけで、Windows Phone 7においてReactive Extensionsがどのような場所で使えるのか、というのをコードとともに解説していきます。
コードはWindows Phone 7向けですが、通常のSilverlightでもWPFでも適用できる話なので、WP7関係ないからー、と言わず、Rxに興味ありましたら、眺めてみてください。
Linq to Event
Rxの特徴の一つはイベントのLinq変換です。Linq化して何が嬉しいかというと、柔軟なフィルタリングが可能になることです。WP7では、センサーからのデータ処理やタッチパネルなど、様々な箇所で威力を発揮すると思われます。UtakotohaではMediaPlayerの再生情報の変化に対してRx化とフィルタリングを施しました。
// Model/MediaPlayerStatus.cs
public class MediaPlayerStatus
{
public MediaState MediaState { get; set; }
public ActiveSong ActiveSong { get; set; }
public static MediaPlayerStatus FromCurrent()
{
return new MediaPlayerStatus
{
MediaState = MediaPlayer.State,
ActiveSong = MediaPlayer.Queue.ActiveSong
};
}
public static IObservable<MediaPlayerStatus> ActiveSongChanged()
{
return Observable.FromEvent<EventArgs>(
h => MediaPlayer.ActiveSongChanged += h, h => MediaPlayer.ActiveSongChanged -= h)
.Select(_ => MediaPlayerStatus.FromCurrent());
}
public static IObservable<MediaPlayerStatus> MediaStateChanged()
{
return Observable.FromEvent<EventArgs>(
h => MediaPlayer.MediaStateChanged += h, h => MediaPlayer.MediaStateChanged -= h)
.Select(_ => MediaPlayerStatus.FromCurrent());
}
// (省略)すぐ下に...
}
Rx化は、ただFromEventをかますだけです。また、その際に、IEventではなく、本当に使う情報にだけ絞ったもの(この場合はMediaStateとActiveSong)をSelectで変換しておくと色々とコードが書きやすくなるのでお薦めです。
さて、「柔軟なフィルタリング」とは何か。ただデータを間引くだけなら、イベントの先頭でif(cond) return;を書けばいいだけです。RxではLinq化により、(自分自身を含めた)イベント同士の合成と、時間軸を絡めた処理が可能になります。これにより、従来一手間だった処理がたった一行で、連続した一塊のシーケンスとして処理することが可能になりました。
Utakotohaでも、生のイベントをそのまま扱わず、ある程度間引いて加工したものを渡しています。
/// <summary>raise when ActiveSongChanged and MediaState is Playing</summary>
public static IObservable<Song> PlayingSongChanged(int waitSeconds = 2, IScheduler scheduler = null)
{
return ActiveSongChanged()
.Throttle(TimeSpan.FromSeconds(waitSeconds), scheduler ?? Scheduler.ThreadPool) // wait for seeking
.Where(s => s.MediaState == MediaState.Playing)
.Select(s => new Song(s.ActiveSong.Artist.Name, s.ActiveSong.Name));
}
「再生中かつx秒間(デフォルトは2秒)新しいイベントが発生しなかった最新のものだけを流す」というものです。どういう意味かというと、連続で楽曲をスキップした時。Utakotohaでは再生曲の変更に合わせて、自動で歌詞検索をしますが、連続スキップにたいしても全て裏で検索に走っていたらネットワークの無駄です。なので、2秒間だけ間隔を置いて連続スキップされていない、と判断した曲のみを歌詞検索するようにしています。
こういう処理は、地味だけど必ず入れなければならない、けれど面倒くさい。でも、Rxを使えばなんてことはなく、Throttleで一撃です。Timer動かしたりDateTimeを比較したりなんて、もうしなくていいんだよって。
/// <summary>raise when MediaState Pause/Stopped -> Playing</summary>
public static IObservable<Song> PlayingSongActive()
{
return MediaStateChanged()
.Zip(MediaStateChanged().Skip(1), (prev, curr) => new { prev, curr })
.Where(a => (a.prev.MediaState == MediaState.Paused || a.prev.MediaState == MediaState.Stopped)
&& a.curr.MediaState == MediaState.Playing)
.Select(s => new Song(s.curr.ActiveSong.Artist.Name, s.curr.ActiveSong.Name));
}
こちらは、状態が停止->再生になったことを検知するというもの。停止->再生でも自動検索を走らせたいので。
この source.Zip(source.Skip(1), /* merge */) は、一見奇妙に見えるかもしれませんが、ある種のイディオムです。一つ先(Skip(1))の値と合流するということは、Skip(1)時の値を基準にすると、現在値と一つ前の値で合流させることができる、ということになります。それにより、一つ前の状態を参照して停止->再生を検知しています。
過去の値を参照するには、他に、Scan(Aggregateの列挙版、そう考えると現在値と一つ前の値が使えることのイメージつくでしょうか?)やBufferWithCount(Listでバッファを持つ、第二引数でずらす範囲を指定可能)など、幾つかやり方がありますが、このZip(Skip(1))が最も扱いやすいところ。ただし、通常のものとSkipしたものとで、二つ分Subscribeされるということは留意したほうがいいかもしれません。そのことが問題になるケースもあるので、キャッシュを使う(Pulish(xs=>xs.Zip(xs.Skip(1)))など回避策を頭に入れておくと良いケースもあります。
Linq to Asynchronous
Rxのもう一つの大きな特徴は非同期のLinq変換です。そうすることにより、コールバックの連鎖で扱いにくかった非同期が、一本の流れに統一されます。
Utakotohaでは、歌詞の検索部分が代表的です。歌詞検索の背景ですが、goo歌詞のものを表示しているのだからgoo歌詞の検索を呼んでやるかなあ、と思ったのですが、それは色々マズいので、Bing Apiのサイト内検索を経由して、表示しました。Bing Apiについては若干苦労話もあるので、いつかまた。
// Model/Bing/BingRequest.cs
public IObservable<SearchWebResult> Search(params SearchWord[] keywords)
{
var req = WebRequest.Create(BuildUrl(keywords));
return Observable.Defer(() => req.GetResponseAsObservable())
.Select(res =>
{
var serializer = new DataContractJsonSerializer(typeof(SearchWebStructure));
using (var stream = res.GetResponseStream())
{
return (SearchWebStructure)serializer.ReadObject(stream);
}
})
.SelectMany(x => (x.SearchResponse.Web.Results != null)
? x.SearchResponse.Web.Results
: Enumerable.Empty<SearchWebResult>());
}
割とあっさり。Bing APIならびにJSONに関しては、以前Windows Phone 7でJSONを扱う方法について(+ Bing APIの使い方)として書きました。ほとんどそのままです。SearchResponse.Web.Resultsは、配列なので、SelectManyで分解してやります(nullの場合は空シーケンスを流す)。すると、後続に繋げるのが非常にやりやすくなります。実際に
// Model/Song.cs
public IObservable<SearchWebResult> SearchLyric()
{
return new BingRequest()
.Search(MakeWord(Artist), MakeWord(Title), LyricSite, Location, Language)
.Where(sr => sr.Url.EndsWith("index.html"))
.Do(Clean);
}
といったように、Searchから更に続いて、若干のフィルタリングが入っています。これを使う場面では、勿論、更にチェーンが続きます。といったように、Rxは一つの流れを構築するわけですが、それらを徹底的に分解・分割して適切な場所への配置・組み合わせが可能になっています。もし、通常の非同期処理のようなコールバックの連鎖だったら、組み合わせは大変でしょう(だから、その場だけで処理したくなって、ネストが嵩んでしまう)
Orchestrate and Coordinate
Rx全体の特徴として、また、他の非同期を扱うライブラリと最も異なる、しかし重要な点として、中に流れるデータを区別しません。非同期もイベントもタイマーもオブジェクトシーケンスも、全て同列に扱います。それはどういうことかというと、データの種別を超えて合成処理が可能になるということです。つまり、Rxは、あらゆるデータソースを統合する基盤といえます。上のAsynchronousの例でも、非同期のWebResponseが、SelectMany以降はオブジェクトシーケンスに摩り替わっていました。
Utakotohaでは、歌詞の表示部分で色々なデータを混ぜあわせる処理を行っています。
// View/MainPage.xaml.cs
LyricBrowser.NavigatedAsObservable()
.Where(ev => ev.EventArgs.Uri.AbsoluteUri.Contains(GooLyricUri))
.SelectMany(ev =>
{
// polling when can get attribute
return Observable.Timer(TimeSpan.Zero, TimeSpan.FromSeconds(3))
.ObserveOnDispatcher()
.Select(_ => (ev.Sender as WebBrowser).SaveToString())
.Select(s => Regex.Match(s, @"s.setAttribute\('src', '(.+?)'"))
.Where(m => m.Success)
.Take(1);
})
.Select(m => WebRequest.Create(GooUri + m.Groups[1].Value).DownloadStringAsync())
.Switch()
.Select(s => Regex.Replace(s.Trim(), @"^draw\(|\);$", ""))
.Where(s => !string.IsNullOrEmpty(s))
.ObserveOnDispatcher()
.Subscribe(jsonArray =>
{
// insert json array to html
LyricBrowser.InvokeScript("eval", @"
var array = " + jsonArray + @";
var sb = [];
for(var i = 0; i < array.length; i++) sb.push(array[i]);
document.getElementById('lyric_area').innerHTML = sb.join('<br />')");
// (省略)
}, e => MessageBox.Show("Browser Error"))
.Tap(disposables.Add);
歌詞の表示といっても、WebBrowserに表示しているこということは、歌詞のURLを渡すだけです。それだけで済むはずでした。が、……妙に複雑怪奇です。歌詞のURLが渡されて表示が完了してからが起点として(Navigated)、処理を開始しています。
何故こうなったか。WP7が現在積んでるIEは7。ついでにFlashはまだ未対応で見れません。それとこれとがどう関係あるかというと、歌詞表示に関係あります。日本の歌詞サイトは大抵はコピペ禁止のために右クリック禁止、だけでなく、Flashで表示していたりします。それじゃあ手が出せない。では今回利用しているgoo歌詞は、というと、少し変わっていてHTML5 Canvasに描画しています。勿論Canvasは古いブラウザじゃ動かないので、互換用JavaScriptも挟んでいるよう。uuCanvas.js を使っているようですが、環境貧弱なWP7版のIE7じゃあ、土台動きませんでした。
このまんまじゃあ歌詞が表示出来なくて困ったわけですが、幸いgoo歌詞はJSONPで歌詞データを別途取得してCanvasにデータを流しているようなので、HTMLからJSONPの発行先を割り出して、歌詞データを頂いてしまえば問題ない(この時点で規約的にはグレーな気が)。生の歌詞データが手に入ってしまっ……。こいつをキャッシュするようにしてオフラインでも見れるといった機能を提供してあげられれば幸せになれるのですが、そういうのは利用規約に違反、してますね、明らかに。
じゃあどうするか。手元には歌詞の表示されていないブラウザ上のHTMLと、歌詞データがある。よし、じゃあブラウザにこちらからはめ込んでやればいいんじゃなイカ?
WP7ではWebBrowserのDOMは触れません。DOMを外から触ってappendChildしてサクッと終了、というわけにはいかず。ただ、外部から干渉出来る口が一つだけ用意されています。それがInvokeScript。外から実行関数を指定して、戻り値を受け取れます(DOMは無理なので、Stringで貰うのが無難)。ならば、evalして外から実行関数自体を注入してやれば、何だって出来る。どうにも馬鹿らしい気もしますが、このぐらいしか手がないのでShoganai(なお、予めWebBrowserのプロパティでスクリプト実行を許可しておかないと例外が出ます)。
Timer and Polling
イベント(Navigated)→タイマー(SaveToString)→非同期(DownloadStringAsync)という直列の合成でした。直列の合成を行うメソッドはSelectMany(もしくはSelect+Switch、両者には若干の違いがあるのですが、それに関しては後日説明します)で、Rxの中でも頻繁に使うことになるメソッドです。
ところで、タイマーが唐突なのですが、何故タイマーを仕込んでいるのか。どうもNavigated直後にSaveToString(WebBrowser内のHTMLを文字列化)だと、タイミング次第で上手く抽出できないことが多かったので(JSで色々処理されてる影響かな?)、必要なJSONPの書かれた属性が取れるまでSaveToStringをリトライするようにしました。つまり、ポーリング(定期問い合わせ)です。
ポーリングは普通だと面倒くさいはずなんですが、Rxだと恐ろしく簡単に書ける上に、こうして通常の処理の流れと合成することが可能になっているのが何よりも強力です。.NET Frameworkには幾つものTimerクラスがあって、何を使えばいいのかと戸惑ってしまうところがありますが、答えは出ました。Observable.Timerがベスト。大変扱いやすい。
これで、本来Canvasのあった領域にテキストデータとして歌詞を表示させられました。全く違和感のない、完璧なハメコミ合成。無駄なコダワリです。地味すぎて一手間かけてることなんてさっぱり分からない。だがそれがいい。……いや、ちょっと悲しい。それにしてもでしかし、Canvas, Flash対応になればこんなやり方は不要になるわけで、今年後半のアップデートでのIE9搭載が待ち遠しい。
Unit Testing
Silverlight向けのユニットテスト環境って、全然ない。ブラウザ上のSilverlightで動かす、というタイプは幾つかありますが、Visual Studioと統合された形のでないと、そんなのアタシが許さない。
TDDするわけでも、熱心にテスト書くわけでも、特段カバレッジを気にするわけでもない私ですが、テストが書けることは重要視しています。何故かというと、メソッドの動作確認が最も素早く行えるから。テスト(が出来ること)に何を期待しているかというと、確認したいんです、メソッドの動作を。手軽に、コンソールアプリを書く感覚で、素早く。処理をコンソールアプリにコピペって、もしくは並走してコンソールアプリを立てながら開発していたりなどを以前よくしたのですが、それを単体テストのメソッド部分に書けば、動作確認のついでに、テストまで手に入るので、それは素敵よね?と。
(単体)テストが第一の目的ではない、(動作確認)テストが目的なのだ。だから、テストフレームワークはVisual Studioと完全な統合を果たしていなければならない。ショートカットでIDE内のウィンドウで即座に実行。スピードが大事。また、シームレスなデバッグ実行への移行も。故にMSTestを選択するのである(キリッ
などなどはその辺にしておいて、それはともかくで、素直にフル.NET Frameworkで動くMSTestを使います。一応Silverlightのアセンブリはフル.NETからも参照可能のようなので、普通にテストプロジェクトを立ててDebug.dll を参照してやる(プロジェクト参照は警告出るので)という手も使えなくはなさそうなのですが、完全な互換を持つコアライブラリは全体のごく一部で、それ以外を使っていると普通に実行時例外でコケるなど、正直使えないと私は判断しました。よって、アセンブリ参照でやるのは諦め。プロジェクト参照で警告が出る理由も分かりました、あまり実用的な機能ではない……。
代わりにWP7プロジェクトとテストプロジェクトの間に、.NET4ライブラリプロジェクトを立てて、「追加->既存の項目->リンクとして追加」で、フルフレームワークとWP7間で.csファイルを共有してやります(Viewは勿論共有出来ないので、基本はModelのみ)。
勿論、コードレベルで互換が取れていないと動きません。そんなわけで、少なくともModelに関してはWPF/SL/WP7で共通で使いまわせるように意識して作りたいところです。移植性というだけじゃなく、MSTestの恩恵を受けれるので。ただまあ、無理な部分は無理で諦めちゃってもいいとは思います(SLのほうにだけあるクラスとかもありますから)。非同期のテストなどは、幸いRxを使っていれば非常に簡単なので、バシバシ書いちゃいましょう。
// Utakotoha.Test/SongTest.cs
[TestMethod]
[Timeout(3000)]
public void SearchLyric()
{
var song = new Song("吉幾三", "俺ら東京さ行ぐだ");
var array = song.SearchLyric().ToEnumerable().ToArray();
array.Count().Is(1);
array.First().Title.Is("俺ら東京さ行ぐだ 吉幾三");
array.First().Url.Is("http://music.goo.ne.jp/lyric/LYRUTND1127/index.html");
}
ToEnumerableしてToArrayするだけです!非同期のテストなんて怖くない。
Portable Library Tools CTPという、各環境で互換性の取れるライブラリが作成出来るもの、なども出てきているので、リンクで追加とかいう間抜け(そして面倒)なことじゃなく、プロジェクトごと分離して、Modelは互換で生成、というのが将来的には良いやり方になるかなあ、などと思っています。そういう事情から、私は移植性とは関係ない立場からも、Portable Library Toolsの発展に期待しています。
なお、アサーションはChaining Assertion使っています。ドッグフードドッグフード。というかもう、必需品なので、これないと書けない、書きたくない……。
Mocking Event with Moles and Rx
さて、このやり方の利点として、PexやMolesが使えます(Pexは一応SLをサポートしたものの、Molesは依然としてSL未サポート)。Moles(Microsoft Researchが提供するモックフレームワーク、フリー)の乗っ取り機構は強力なので、テスト可能範囲が大幅に広がります。詳しくはRx + MolesによるC#での次世代非同期モックテスト考察をどうぞ。
今回Linq to Eventで紹介したPlayingSongActiveは、以下のようにテストしています。MediaPlayer周りはXNAなので、Test側の参照DLLとしてXNA Game Studio v4.0のMicrosoft.Xna.Framework.dllを参照。そしてMolesで乗っ取り。
// Utakotoha.Test/MediaPlayerStatusTest.cs
private MediaPlayerStatus CreateStatus(MediaState state, string artist, string name)
{
return new MediaPlayerStatus
{
MediaState = state,
ActiveSong = new Microsoft.Xna.Framework.Media.Moles.MSong
{
NameGet = () => name,
ArtistGet = () => new MArtist
{
NameGet = () => artist
}
}
};
}
[TestMethod, HostType("Moles")]
public void PlayingSongActiveTest()
{
// event invoker
var invoker = new Subject<MediaPlayerStatus>();
MMediaPlayerStatus.MediaStateChanged = () => invoker;
// make target observable
var target = MediaPlayerStatus.PlayingSongActive().Publish();
target.Connect();
// at first, stopped
using (target.VerifyZero())
{
invoker.OnNext(CreateStatus(MediaState.Stopped, "", ""));
}
// next, playing
using (target.VerifyOnce(song => song.Is(s => s.Title == "song" && s.Artist == "artist")))
{
invoker.OnNext(CreateStatus(MediaState.Playing, "artist", "song"));
}
// pause
using (target.VerifyZero())
{
invoker.OnNext(CreateStatus(MediaState.Paused, "", ""));
}
// play again
using (target.VerifyOnce(song => song.Is(s => s.Title == "song2" && s.Artist == "artist2")))
{
invoker.OnNext(CreateStatus(MediaState.Playing, "artist2", "song2"));
}
}
元のコード自体がイベント発火部分はRxで包んであるので、そのイベント発火だけを差し替え。SubjectとはイベントのRxでの表現。OnNextでイベント発火の代用が可能になっています。これで、任意のイベント(今回はMediaPlayerなので、再生停止であったり再生開始であったり)を発行して、その結果の挙動を確認しています。
VerifyなんたらはIObservableへの自前拡張メソッドで、発火されたか/回数の検証です。「イベントは発生したけれどフィルタリングされて値が届かなかった」ことの、フィルタリングが正常に出来たかの確認って、そのままだと難しい。如何せんSubscribeまで届いてくれないということですから。そのため、その辺を面倒みてくれるものを用意しました。
// Utakotoha.Test/Tools/ObservableVerifyExtensions.cs
/// <summary>verify called count when disposed. first argument is called count.</summary>
public static IObservable<T> Verify<T>(this IObservable<T> source, Expression<Func<int, bool>> verify)
{
var count = 0;
return source
.Do(_ => count += 1)
.Finally(() =>
{
var msg = verify.Parameters.First().Name + " = " + count + " => " + verify.Body;
Assert.IsTrue(verify.Compile().Invoke(count), "Verifier " + msg);
});
}
/// <summary>verify called count when disposed. first argument is called count.</summary>
public static IDisposable VerifyAll<T>(this IObservable<T> source, Expression<Func<int, bool>> verify, Action<T> onNext = null)
{
return source.Verify(verify).Subscribe(onNext ?? (_ => { }));
}
/// <summary>verify not called when disposed.</summary>
public static IDisposable VerifyZero<T>(this IObservable<T> source)
{
return source.VerifyAll(i => i == 0);
}
/// <summary>verify called once when disposed.</summary>
public static IDisposable VerifyOnce<T>(this IObservable<T> source, Action<T> onNext = null)
{
return source.VerifyAll(i => i == 1);
}
usingによるスコープを抜けるとFinallyで検証が入ります。RxでイベントをラップするとIDisposableになる、そのことの利点が生きてきます。
今後の改善
Pivotのヘッダーのデザインがどうも間抜け(マージンの取り方が変だし文字サイズも違和感あり)なのが気になってるので、変えたいです。HeaderTemplateの編集の仕方がよくわからずで放置なのですけれど、ゆったり紐解けば出来るでしょう。多分。
レジュームへの配慮が全くなくて、別画面にいくと真っ白になるのがビミョい。WebBrowserが絡むので完全な復元は無理だから、いっかー、とか思ったのが半分はあるのですが、いやまてその理屈はオカシイ。ので、ちょっと何とかさせないとですね。
xaml.csのコードが全体的にマズい。特にOAuth認証の部分はありえない強引さなのでとっとと変更。あと、もう少し適切な分割。MVVMはわからんちん。
Settingsが何か変。IsolatedStorageSettingsというか、その内部のDataContractSerializerの都合というか。これだ!というやり方ないかしら。今のやり方は、非常に間抜け。
ブラウザ画面黒背景はビミョーなので、設定でアプリ全体を白背景に変更するオプションを入れるのもいいかなー。それとアプリ起動時は楽曲を再生中でも自動検索しないのだけど、自動検索してくれたほうが嬉しいかなー。など、細かい点では色々考えること、追加することあります。
まとめ
上手く決まった部分しか解説してないので、実際のコードは残念ながらスパゲッティです:) というか、UI絡みのコードってほとんど書いたことないので、経験の無さが如実に現れていて苦すぃ。WP7で勝手がわからないというもの若干はありますが、それ以前の問題がかなり。サンプルアプリとしても、もう少し良くしたいので、コードは徐々に洗練させていきたいですます。
アプリとしては、まあまあいい出来というか、実用品として悪くないフィーリングだと思うのですが、どうでしょうか?画面周りは、このレイアウトで決まるまで何度も試して投げてを繰り返してこれに落ち着きました。実際に作りながら、試しながらでないとこういうの決められないよね。コードに関してもそうだけど。ともあれ、Pivotいいよねー。やはりWP7といったらPivot。
そんなわけで、RxはWP7開発のお供として欠かせない代物なので、是非使ってみてください。また、WP7で欠かせないということはSilverlightで欠かせないということであり、Silverlightで欠かせないということはWPFでも欠かせないということでも、あったりなかったりするので、Rxによるプログラミングパラダイムの変化を是非とも楽しんでみてください。Linq to Anything!
一記事に収めるため少々駆け足気味だったので、なにか不明な点、質問などありましたら気楽にコメントどうぞ。突っ込みも勿論歓迎です。
Microsoft MVP for Visual C#を受賞しました
- 2011-04-02
Microsoft MVPから、Visual C#カテゴリで受賞しました。for Linqというカテゴリがないので大変厳しいかと思われましたが何とか認められたようです。活動申請には、ほんとうにLinqが~Linqが~ばかりで。今見返すと「活動の源泉は、C#/Linqの素晴らしさを伝えたい!ということです」などとコメント欄に書いちゃっていたり。あと、審査するのは本社の連中だからアメリカンに、大袈裟に書くぐらいがいいんだよ、とアドバイスを受けたので「RxJSによりJavaScriptにおいてもInterective(Enumerable)とReactive(Observable)の美しい融合が果たせることが可能となりました。そのうちのInterective側を支える(linq.js)ことが出来るのは自分しかいない」とかいう恥ずかしいことまで言っちゃってますね、うわあ。
というわけで、公約みたいなものなので、今後もC#/Linqの素晴らしさを伝える道を邁進したいと思います。
私とMVP。MVPアワードの存在を知ったのはXNEWSがマイクロソフトMVPアワードを受賞した時のことで、ただのゲーマーだった頃でした。その頃は何なのか分からず月日は流れ、就職してC#を触りだしてから(2008年)、MVPがどういう存在であるかを知り、以下略。
私が成長できたのは多くの先人の、ネット上のリソースのお陰です。今度は、私が恩返しする番。今まで通り、今まで以上に知識を伝えていけたらと思っています。