Archive - Silverlight

.NETの標準シリアライザ(XML/JSON)の使い分けまとめ

今年もAdvent Calendarの季節がやってきましたね。去年は私はC#とJavaScriptで書きましたが、今年はC#とSilverlightでやります。というわけで、この記事はSilverlight Advent Calendar 2011用のエントリです。前日は@posauneさんのSilverlightのListBoxでつくるいんちきHorizontalTextBlock でした。

今回の記事中のサンプルはSilverlight 4で書いています。が、Silverlight用という体裁を持つためにDebug.WriteLineで書いているというだけで、Silverlightらしさは皆無です!えー。.NET 4でもWindows Phone 7でも関係なく通じる話ですねん。

シリアライザを使う場面

概ね3つではないでしょうか。外部で公開されているデータ(APIをネット経由で叩くとか)をクラスに変換する。これは 自分の管理外→プログラム での片方向です。内部で持っているデータ(クラスのインスタンス)を保存用・復元用に相互変換する。これは プログラム←→自分の管理内 での双方向です。最後に、内部で持っているデータを公開用に変換する。これは プログラム→外部 での片方向。

目的に応じてベストな選択は変わってきます。こっから延々と長ったらしいので、まず先に結論のほうを。

  • 外部APIを叩く→XML/XmlSerializer, JSON/DataContractJsonSerializer
  • オブジェクトの保存・復元用→DataContractSerializer
  • 外部公開→さあ?

外部公開のは、Silverlightの話じゃないので今回はスルーだ!XStreamingElementで組み上げてもいいし、何でもいいよ!WCFのテンプレにでも従えばいいんぢゃないでしょーか。

XmlSerializer

古くからあるので、シリアライザといったらこれ!という印象な方も多いのではないでしょうか。その名の通り、素直にXMLの相互変換をしてくれます。

// こんなクラスがあるとして
// (以降、断り書きなくPersonが出てきたらこいつを使ってると思ってください)
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}
// データ準備
var data = new Person { Name = "山本山", Age = 99 };
 
var serializer = new XmlSerializer(typeof(Person));
using (var ms = new MemoryStream())
{
    serializer.Serialize(ms, data); // シリアライズ
 
    // 結果確認出力
    var xml = Encoding.UTF8.GetString(ms.ToArray(), 0, (int)ms.Length);
    Debug.WriteLine(xml);
 
    ms.Position = 0; // 巻き戻して……
    var value = (Person)serializer.Deserialize(ms); // デシリアライズ
    Debug.WriteLine(value.Name + ":" + value.Age); // 山本山:99
}
// 出力結果のXML
<?xml version="1.0" encoding="utf-8"?>
<Person xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Name>山本山</Name>
  <Age>99</Age>
</Person>

素直な使い勝手、素直な出力。いいですね。さて、しかし特に外部APIを叩いて手に入るXMLは名前PascalCaseじゃねーよ、とか属性の場合どうすんだよ、という場合も多いでしょう。細かい制御にはXmlAttributeを使います。

[XmlRoot("people")]
public class People
{
    [XmlElement("count")]
    public int Count { get; set; }
    [XmlArray("persons")]
    [XmlArrayItem("person")]
    public Person[] Persons { get; set; }
}
 
[XmlRoot("person")]
public class Person
{
    [XmlElement("name")]
    public string Name { get; set; }
    [XmlAttribute("age")]
    public int Age { get; set; }
}
// データ準備
var data = new People
{
    Count = 2,
    Persons = new[]
{
    new Person { Name = "山本山", Age = 99 },
    new Person { Name = "トマト", Age = 19 }
}
};
var xml = @"
    <people>
        <count>2</count>
        <persons>
            <person age=""14"">
                <name>ほむ</name>
            </person>
            <person age=""999"">
                <name>いか</name>
            </person>
        </persons>
    </people>";
 
var serializer = new XmlSerializer(typeof(People));
 
// シリアライズ
using (var ms = new MemoryStream())
{
    serializer.Serialize(ms, data);
    Debug.WriteLine(Encoding.UTF8.GetString(ms.ToArray(), 0, (int)ms.Length));
}
 
// デシリアライズ
using (var sr = new StringReader(xml))
{
    var value = (People)serializer.Deserialize(sr);
    foreach (var item in value.Persons)
    {
        Debug.WriteLine(item.Name + ":" + item.Age);
    }
}
 
// 出力結果のXMLは↑に書いたXMLと同じようなものなので割愛

ちょっと属性制御が面倒ですが、それなりに分かりやすく書けます。他によく使うのは無視して欲しいプロパティを指定するXmlIgnoreかしら。さて、そんな便利なXmlSerializerですが、XML化するクラスに制限があります。有名所ではDictionaryがシリアライズできねえええええ!とか。小細工して回避することは一応可能ですが、そんな無理するぐらいなら使うのやめたほうがいいでしょう、シリアライザは別にXmlSerializerだけじゃないのだから。

というわけで、XmlSerializerの利用シーンのお薦めは、ネットワークから外部APIを叩いて手に入るXMLをクラスにマッピングするところです。柔軟な属性制御により、マッピングできないケースは(多分)ないでしょう。いや、分かりませんが。まあ、ほとんどのケースでは大丈夫でしょう!しかし、LINQ to XMLの登場により、手書きで変換するのも十分お手軽なってしまったので、こうして分かりにくい属性制御するぐらいならXElement使うよ、というケースのほうが多いかもしれません。結局、XML構造をそのまま映すことしかできないので、より細かく変換できたほうが良い場合もずっとあって。

実際、私はもう長いことXmlSerializer使ってない感じ。LINQ to XMLは偉大。

DataContractSerializer

割と新顔ですが、もう十分古株と言ってよいでしょう(どっちだよ)。XmlSerializerと同じくオブジェクトをXMLに変換するのですが、その機能はずっと強力です。Dictionaryだってなんだってシリアライズできますよ、というわけで、現在では.NETの標準シリアライザはこいつです。

// データ準備
var data = new Person { Name = "山本山", Age = 99 };
 
var serializer = new DataContractSerializer(typeof(Person));
using (var ms = new MemoryStream())
{
    serializer.WriteObject(ms, data); // シリアライズ
 
    // 結果確認出力
    var xml = Encoding.UTF8.GetString(ms.ToArray(), 0, (int)ms.Length);
    Debug.WriteLine(xml);
 
    ms.Position = 0; // 巻き戻して……
    var value = (Person)serializer.ReadObject(ms); // デシリアライズ
    Debug.WriteLine(value.Name + ":" + value.Age); // 山本山:99
}
<Person xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/SilverlightApplication34"><Age>99</Age><Name>山本山</Name></Person>

とまあ、使い勝手はXmlSerializerと似たようなものです。おお、出力されるXMLは整形されていません。整形して出力したい場合は

// 出力を整形したい場合はXmlWriter/XmlWriterSettingsを挟む
using (var ms = new MemoryStream())
using (var xw = XmlWriter.Create(ms, new XmlWriterSettings { Indent = true }))
{
    serializer.WriteObject(xw, data);
 
    xw.Flush();
    var xml = Encoding.UTF8.GetString(ms.ToArray(), 0, (int)ms.Length);
    Debug.WriteLine(xml);
}
<?xml version="1.0" encoding="utf-8"?>
<Person xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/SilverlightApplication34">
  <Age>99</Age>
  <Name>山本山</Name>
</Person>

さて、結果をXmlSerializerと見比べてみるとどうでしょう。名前空間が違います。SilverlightApplication34ってありますね。これは、私がこのXMLを出力するのに使ったSilverlightプロジェクトの名前空間です。ワシのConsoleApplicationは221まであるぞ(整理しろ)。さて、ではこのXMLをデシリアライズするのに、別のアプリケーション・別のクラスで使ってみるとどうでしょう?

namespace TestSilverlightApp
{
    public class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }
 
    public partial class MainPage : UserControl
    {
        public MainPage()
        {
            InitializeComponent();
 
            var xml = @"<?xml version=""1.0"" encoding=""utf-8""?>
            <Person xmlns:i=""http://www.w3.org/2001/XMLSchema-instance"" xmlns=""http://schemas.datacontract.org/2004/07/SilverlightApplication34"">
                <Age>99</Age>
                <Name>山本山</Name>
            </Person>";
 
            var serializer = new DataContractSerializer(typeof(Person));
            using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(xml)))
            {
                // System.Runtime.Serialization.SerializationExceptionが起こってデシリアライズできない
                // 名前空間 'http://schemas.datacontract.org/2004/07/TestSilverlightApp' の要素 'Person' が必要です。
                // 名前が 'Person' で名前空間が 'http://schemas.datacontract.org/2004/07/SilverlightApplication34' の 'Element' が検出されました。
                var value = (Person)serializer.ReadObject(ms);
            }
        }
    }
}

デシリアライズ出来ません。対象オブジェクトが名前空間によって厳密に区別されるからです。じゃあどうするのよ!というと、属性で名前空間を空、という指示を与えます。

// DataContract属性をクラスにつけた場合は
// そのクラス内のDataMember属性をつけていないプロパティは無視される
[DataContract(Namespace = "", Name = "person")]
public class Person
{
    [DataMember(Name = "name")]
    public string Name { get; set; }
    [DataMember(Name = "age")]
    public int Age { get; set; }
}
// こんなプレーンなXMLも読み込める
var xml = @"
    <person>
        <age>99</age>
        <name>山本山</name>
    </person>";
 
var serializer = new DataContractSerializer(typeof(Person));
using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(xml)))
{
    var value = (Person)serializer.ReadObject(ms);
    Debug.WriteLine(value.Name + ":" + value.Age);
}

属性面倒くせー、ですけれど、まあしょうがない。そうすれば外部からのXMLも読み込めるし、と思っていた時もありました。以下のようなケースではどうなるでしょうか?Personクラスは↑のものを使うとして。

// こんなさっきと少しだけ違うXMLがあるとして
var xml = @"
    <person>
        <name>山本山</name>
        <age>99</age>
    </person>";
 
var serializer = new DataContractSerializer(typeof(Person));
using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(xml)))
{
    var value = (Person)serializer.ReadObject(ms);
    Debug.WriteLine(value.Name + ":" + value.Age); // 結果は???
}

これは出力結果は「山本山:0」になります。Ageが0、つまり復元されませんでした。なぜかというと、XMLを見てください。nameが先で、ageが、後。DataContractSerializerは規程された順序に強く従います。DataMember属性のOrderプロパティで順序を与えるか、与えない場合はアルファベット順(つまりAgeが先でNameが後)となります。この辺はデータ メンバーの順序に書かれています。

と、いうような事情から、DataContractSerializerを外部XMLからの受け取りに使うのはお薦めしません。XmlSerializerなら順序無視なので大丈夫です。いや、普通は順序が変わったりなどしないだろう!と思わなくもなくもないけれど、意外とデタラメなのじゃないか、基本的にはお外からのデータが何もかも信用できるわけなどないのだ、とうがってしまい(TwitterのAPIとか胡散臭さいのを日常的に触っていると!)、厳しいかなって、思ってしまうのです。

しかし、オブジェクトの保存・復元用にはDataContractSerializerは無類の強さを発揮します。例えば設定用のクラスを丸ごとシリアライズ・デシリアライズとかね。iniにして、じゃなくてフツーはXMLにすると思いますが、それです、それ。Dictionaryだってシリアライズできるし、引数なしコントラクタがないクラスだってシリアライズできちゃうんですよ?

// とある引数なしコンストラクタがないクラス
[DataContract]
public class ToaruClass
{
    [DataMember]
    public string Name { get; set; }
 
    public ToaruClass(string name)
    {
        Name = name;
    }
}
var toaru = new ToaruClass("たこやき");
 
var serializer = new DataContractSerializer(typeof(ToaruClass));
using (var ms = new MemoryStream())
{
    serializer.WriteObject(ms, toaru); // シリアライズできるし
 
    ms.Position = 0;
    var value = (ToaruClass)serializer.ReadObject(ms); // デシリアライズできる
 
    Debug.WriteLine(value.Name); // たこやき
}

ただし、対象クラスにDataContract属性をつけてあげる必要はあります。つけてないとシリアライズもデシリアライズもできません。

ちなみに何でコンストラクタがないのにインスタンス化出来るんだよ!というと、System.Runtime.Serialization.FormatterServices.GetUninitializedObjectを使ってインスタンス化しているからです(Silverlightの場合はアクセス不可能)。こいつはコンストラクタをスルーしてオブジェクトを生成する反則スレスレな存在です、というか反則です。チートであるがゆえに、対象クラスにはDataContract属性をつける必要があります。コンストラクタ無視してもいいよ、ということを保証してあげないとおっかない、というわけです。(GetUninitializedObjectメソッド自体は別に属性は不要で何でもインスタンス化できます、typeof(void)ですらインスタンス化できます、無茶苦茶である)

なお、このGetUninitializedObjectが使われるのはDataContract属性がついているクラスのみです。DataContract属性がついていなければ、普通のコンストラクタが呼ばれるし、逆にDataContract属性がついていると、例え引数をうけないコンストラクタがあったとしても、GetUninitializedObject経由となりコンストラクタは無視されます。DataContract属性を付ける時はコンストラクタ内でシリアライズで復元できない副作用のある処理をすべきではない。ということに注意してください。

また、.NET 4版ではprivateプロパティの値も復元できるのですが、Silverlightの場合は無理のようです。ということでフル.NETなら不変オブジェクトでもサクサク大勝利、と思ってたのですが、Silverlightでの不変オブジェクトのシリアライズ・デシリアライズは不可能のようです。保存したいなら、保存専用の代理のオブジェクトを立ててやるしかない感じでしょうかね。

そんなわけで微妙な点も若干残りはしますが、オブジェクトを保存するのにはDataContractSerializerがお薦めです。

DataContractとSerializable

シリアライズ可能なクラス、の意味でDataContract属性をつけているわけですが、じゃあSerializable属性は?というと、えーと、SerializableはSilverlightでは入っていなかったりするとおり、過去の遺物ですね。なかったということで気にしないようにしましょう。

DataContractJsonSerializer

今時の言語はJSONが簡単に扱えなきゃダメです。XMLだけ扱えればいい、なんて時代は過ぎ去りました。しかしC#は悲しいことに標準では……。いや、いや、SilverlightにはSystem.Jsonがありますね。しかし.NET 4にはありません(.NET 4.5とWinRTには入ります)。いや、しかし.NET 4にはDynamicJsonがあります(それ出していいならJSON.NETがあるよ、で終わりなんですけどね)。が、Windows Phone 7には何もありません。ああ……。

とはいえ、シリアライザならば用意されています。DataContractJsonSerializerです。

// データ準備
var data = new Person { Name = "山本山", Age = 99 };
 
var serializer = new DataContractJsonSerializer(typeof(Person));
using (var ms = new MemoryStream())
{
    serializer.WriteObject(ms, data); // シリアライズ
 
    // 結果確認出力
    var xml = Encoding.UTF8.GetString(ms.ToArray(), 0, (int)ms.Length);
    Debug.WriteLine(xml); // {"Age":99,"Name":"山本山"}
 
    ms.Position = 0; // 巻き戻して……
    var value = (Person)serializer.ReadObject(ms); // デシリアライズ
    Debug.WriteLine(value.Name + ":" + value.Age); // 山本山:99
}

使い勝手はDataContractSerializerと完全に一緒です。ただし、違う点が幾つか。名前空間が(そもそもJSONで表現不可能なので)なくなったのと、順序も関係なく復元可能です。

var json1 = @"{""Name"":""山本山"",""Age"":99}";
var json2 = @"{""Age"":99,""Name"":""山本山""}";
 
var serializer = new DataContractJsonSerializer(typeof(Person));
using (var ms1 = new MemoryStream(Encoding.UTF8.GetBytes(json1)))
using (var ms2 = new MemoryStream(Encoding.UTF8.GetBytes(json2)))
{
    var value1 = (Person)serializer.ReadObject(ms1);
    var value2 = (Person)serializer.ReadObject(ms2);
 
    Debug.WriteLine(value1.Name + ":" + value2.Age);
    Debug.WriteLine(value2.Name + ":" + value2.Age);
}

というわけで、随分とDataContractSerializerよりも使い勝手が良い模様。いい話だなー。さて、難点は出力されるJSONの整形が不可能です。DataContractSerializerではXmlWriterSettingsで行えましたが、DataContractJsonSerializerではそれに相当するものがありません。というわけでヒューマンリーダブルな形で出力、とはならず、一行にドバーっとまとめて吐かれるのでかなり苦しい。

もう一つ、これは本当に大したことない差なのでどうでもいいのですが、DataContractSerializerのほうが速いです。理由は単純でDataContractSerializerに一枚被せる形でDataContractJsonSerializerが実装されているから。その辺の絡みで.NET 4にはJsonReaderWriterFactoryなどがあって、これを直に触ってJSON→XML変換をするとLINQ to XMLを通したJSONの直接操作が標準ライブラリのみで可能なのですが、Silverlight/Windows Phone 7では残念なことに触ることができません。

外部APIを叩いて変換する際に、シリアライズはお手軽で便利であると同時に、完全に同一の形のオブジェクトを用意しなければならなくて、かったるい側面もあります。LINQ to XML慣れしていると特に。そういった形でJSONを扱いたい場合、WP7ではJson.NETを使う、しかありません。使えばいいんぢゃないかな、どうせNuGetでサクッと入れられるのだし。

とはいえまあ、そう言うほど使いづらいわけでもないので、標準のみでJSONを扱いたいという場合は、DataContractJsonSerializerが第一にして唯一の選択肢になります。

JavaScriptSerializer

.NET Framework 4.0 Client Profileでは使えないのですが、FullならばSystem.Web.Extensionを参照することでJavaScriptSerializerが使えます。もはや完全にSilverlightと関係ないのでアレですが、少し見てみましょう。

var serializer = new JavaScriptSerializer();
 
var target = new { Name = "ほむほむ", Age = 14 };
var json = serializer.Serialize(target); // stringを返す

Serializeで文字列としてのJSONを返す、というのがポイントです。それと、シリアライザ作成時にtypeを指定しません。また、匿名型もJSON化することが可能です(これはDataContractSerializerでは絶対無理)。ただし、コンストラクタのないクラスのデシリアライズは不可能です。

中々使い勝手がいいですね!で、これは、リフレクションベースの非常に素朴な実装です。だから匿名型でもOKなんですねー。ちょっとした用途には非常に楽なのですが、Client Profileでは使えないこともありますし(ASP.NETで使うために用意されてる)、あまり積極的に使うべきものではないと思います。ちなみに、一時期ではObsoleteになっていてDataContractJsonSerializer使え、と出ていたのですが、またObsoleteが外され普通に使えるようになりました。やはり標準シリアライザとしてはDataContractJsonSerializerだけだと重すぎる、ということでしょうか。

バイナリとか

別にシリアライズってXMLやJSONだけじゃあないのですね。サードパーティ製に目を向ければ、色々なものがあります。特に私がお薦めなのはprotobuf-net。これはGoogleが公開しているProtocol Buffersという仕様を.NETで実装したものなのですが、とにかく速い。めちゃくちゃ速い。稀代のILマスターが書いているだけある恐ろしい出来栄えです。SilverlightやWP7版もあるので、Protocol Buffersの本来の用途というだけなく、幅広く使えるのではかとも思います。

もう一つは国内だと最近目にすることの多いMessagePack。以前に.NET(C#)におけるシリアライザのパフォーマンス比較を書いたときは振るわないスコアでしたが、最近別のC#実装が公開されまして、それは作者によるベンチMessagePack for .NET (C#) を書いたによると、protobuf-netよりも速いそうです。

Next

というわけでSilverlight枠でいいのか怪しかったですが、シリアライザの話でした。次は@ugaya40さんのWeakEventの話です。引き続きチェックを。あ、あと、Silverlight Advent Calendarはまだ埋まってない(!)ので、是非是非参加して、埋めてやってください。申し込みはSilverlight Advent Calendar 2011から。皆さんのエントリ、待ってます。どうやらちょうど今日Silverlight 5がリリースされたようなので、SL5の新機能ネタとかいいんじゃないでしょうか。

SL/WP7のSilverlight Unit Test Frameworkについて少し深く

の、前に少し。DynamicJsonAnonymousComparerをNuGetに登録しました。どちらも.csファイル一個のお手軽クラスですが、NuGetからインストール可能になったことで、より気楽に使えるのではかと思います。機能説明は省略。

そして、昨日の今日ですがChaining AssertionSilverlight Unit Test Frameworkに対応させました。リリースのバージョンは1.6.0.1ということで。NuGetではChainingAssertion-SLChainingAssertion-WP7になります。

Silverlight Unit Test Framework

Silverlightで使う場合は(WP7じゃなくてね、という意味です)、一応Silverlight Toolkitに同梱という話ではあるのですが、テンプレートなどの用意が面倒くさいので、NuGet経由で入れるのが最も楽のようです。Install-Package Silverlight.UnitTestで。

まず、Silverlightアプリケーションを新規作成。Webサイトでのホストはなしでいいです。それとブラウザで実行させる必要もないので、プロジェクトのプロパティからOut of Browserに変更してしまいましょう。次に、NuGetからInstall-Package Silverlight.UnitTest。これでライブラリの参照と、ApplicationExtensions.cs(イニシャライズ用拡張メソッド)、UnitTest.cs(テスト用テンプレ)が追加されているはずです。次にApp.xaml.csのStartupを以下のように書き換えます。

private void Application_Startup(object sender, StartupEventArgs e)
{
    // this.StartTestRunnerDelayed();
    this.StartTestRunnerImmediate();
}

StartTestRunnerDelayedはテストランナー起動時に実行オプション(指定属性のもののみ実行するなど)を選択可能にするもの、Immediateはすぐに全テストを実行する、というものです。どちらかを選択すればOK。それで、とりあえず実行(Ctrl+F5)してみれば、テストランナーが立ち上がって、デフォテンプレに含まれるUnitTest.csのものが実行されているんじゃないかしらん。あとは、それを適宜書き換えていけばよし。なお、テンプレのテストクラスはSilverlightTestを継承していますが、これは必ずしも継承する必要はありません。後述しますが、Asynchronousのテストを行いたいときは必須ですが、そうでないならば、普通にMSTestでの場合と同じように、[TestClass]と[TestMethod]属性がついているものがテスト対象になっています。

なお、MainPage.xaml/.xaml.csは不要なので削除してしまってOK。StartTestRunnerによって、参照DLLのほうに含まれるxamlが呼ばれているためです。

WP7の場合。

一応NuGetにも用意されてるっぽい(silverlight.unittest.wp7)んですが、動きませんでした。ので、今のところ手動で色々用意する必要があります。詳しくはWindows Phone 7用の単体テストツール? その2「使ってみた」 - かずきのBlog@Hatenaに全部書いてあるのでそちらを参照のことということで。参照するためのDLLを拾ってくる→App.xaml.cs、ではなくてMainPage.xaml.csを書き換える、という、Silverlight版とやることは一緒なのですけどね。こういう状況なのはMangoのSDKがベータだったからとかなんとかのせいだとは思うので、近いうちに解決するのではかと、楽観視したいところです。

Chaining Assertionを使ってみる

Chaining Assertion ver 1.6.0.0の解説で紹介した失敗結果が丁寧に表示されるよー、をチェックしてみませう。

// こんなクラスがあるとして
public class Person
{
    public int Age { get; set; }
    public string FamilyName { get; set; }
    public string GivenName { get; set; }
}
 
[TestClass]
public class ToaruTest
{
    [TestMethod]
    public void PersonTest()
    {
        // こんなPersonがあるとすると
        var person = new Person { Age = 50, FamilyName = "Yamamoto", GivenName = "Tasuke" };
        // こんな風にメソッドチェーンで書ける(10歳以下でYamadaTarouであることをチェックしてます)    
        // 実際の値は50歳でYamamotoTasukeなので、このアサーションは失敗するでしょう
        person.Is(p => p.Age <= 10 && p.FamilyName == "Yamada" && p.GivenName == "Tarou");
    }
}

はい、ちゃんと表示されます。Chaining Assertionを使うと、メソッドチェーンスタイルで、実際の値.Is(期待値の条件)というように、 簡潔な記述でテストを書くことが出来るのがうりです。また、失敗時には、この場合personの値を詳細に出力してくれるので、何故失敗したのかが大変分かりやすい。もし、普通に書くと以下のようになりますが、

// もし普通に書く場合
var person = new Person { Age = 50, FamilyName = "Yamamoto", GivenName = "Tasuke" };
Assert.IsTrue(person.Age <= 10);
Assert.AreEqual("Yamada", person.FamilyName);
Assert.AreEqual("Tarou", person.GivenName);

まず、Assert.IsTrueでは失敗時にperson.Ageの値を出してくれないので、確認が面倒です。また、この場合、Personが正しいかをチェックしたいわけなので、FamilyNameやGivenNameも同時に判定して欲しいところですが、Ageを判定した時点で失敗のため、そこでテストは終了してしまうため、FamilyNameやGivienNameの実際の値を知ることは出来ません。

などなどの利点があるので、Chaining Assertionはお薦めです!この記事はSilverlight Unit Test Frameworkの紹介の体をとっていますが、実態はChaining Assertionの宣伝記事ですからね(キリッ

非同期テストをしてみる

Silverlightといったら非同期は避けて通れない。というわけで、Silverlight Unit Test Frameworkには非同期をテストできる機構が備わっています。[Asynchronous]というように、Asynchronous属性をつければそれだけでOK。と、思っていた時もありました。実際に試してみると全然違って、独特なシステムのうえにのっかっていて、かなり面倒くさかった……。

準備。まず、非同期テストをしたいクラスはSilverlightTestクラスを継承します。そしてAsynchronous属性をつけます。すると、そのテストメソッドはTestCompleteが呼ばれるか例外を検知するまでは、終了しなくなります。というわけで、こんな感じ。

[TestClass]
public class ToaruTest : SilverlightTest
{
    [TestMethod]
    [Asynchronous]
    public void AsyncTest()
    {
        var req = WebRequest.Create("http://www.google.co.jp/");
        req.BeginGetResponse(ar =>
        {
            try
            {
                req.EndGetResponse(ar)
                    .ResponseUri.ToString()
                    .Is("http://www.google.co.jp/");
            }
            catch (Exception ex)
            {
                EnqueueCallback(() => { throw ex; }); // 例外はテスト用スレッドに投げる必要がある
                return;
            }
 
            // ↓は定型句なので、EnqueueTestComplete(); という単純化されたのが用意されている
            EnqueueCallback(() => TestComplete()); // 何事もなければ終了でマーク
        }, null);
    }
}

このUnitTestの非同期は、独自のスレッドモデル(のようなもの)で動いていて、Dispatcherのようなキューにたいしてアクションを放り投げてあげる必要があります。別スレッドからUIスレッドは触れないように、「成功(TestComplete)」か「失敗(例外発生)」を伝えるには、EnqueueCallbackを経由しなければなりません。この辺はDispatcher.BeginInvokeするようなもの、と考えるといいかもしれません。

上のは少し原理に忠実にやりすぎた。まるごとEnqueueCallbackしてしまえばスレッドを意識する必要性は少しだけ減ります。

[TestMethod, Asynchronous]
public void AsyncTest()
{
    var req = WebRequest.Create("http://www.google.co.jp/404"); //404なので例外出してくれる
    req.BeginGetResponse(ar =>
    {
        EnqueueCallback(() => req.EndGetResponse(ar)
            .ResponseUri.ToString()
            .Is("http://www.google.co.jp/"));
 
        EnqueueTestComplete();
    }, null);
}

といっても、これは非常に単純なケースなだけであって、複雑なケースを書くとどんどん泣きたくなっていくでしょう……。一応、Enqueueには他にEnqueueConditionalという、条件式がtrueになるまで待機し続けるというものが用意されているので、若干制御はできなくもないんですが、あんまりできるとは言い難い仕組みがあります。詳しくは述べませんというか、別に使いやすいシステムじゃないのでどうでもいいです。

Rxを使ってみる

結果・もしくは例外を別のスレッドシステムに投げる。どこかで聞いたことあるような。ここでティンと来るのはReactive ExtensionsのObserveOnDispatcherです。Dispatcher.BeginInvokeのかわりにEnqueueCallback。丸っきりそっくり。なので、ObserveOnTestQueueのようなメソッドが作れれば、非常に使い勝手がいいんじゃないか。と思い浮かぶわけです。

と、浮かんだ人は実に素敵な発想力を持っていますね。浮かんだのは私じゃなくて海外の人です。はい。Writing asynchronous unit tests with Rx and the Silverlight Unit Testing Framework | Richard Szalayに、実装が書かれています。

そのRxによるScheduler実装を使うと(WP7版なのでSystem.ObservableとMicrosoft.Phone.Reactiveも参照してください)

[TestMethod, Asynchronous]
public void AsyncTest()
{
    var req = WebRequest.Create("http://www.google.co.jp/");
    Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse,req.EndGetResponse)()
        .ObserveOnTest(this)
        .Subscribe(r => 
            r.ResponseUri.ToString().Is("http://www.google.co.jp/"),
            () => TestComplete());
}

EnqueueCallbackの管理がなくなり、非常に簡単に記述できました。Rxのスケジューラのシステムの柔軟さの賜物ですね。これはRxの素晴らしい応用例だと本当に感動しました。Richard Szalayさんに乾杯。それと、私がこの記事を知ったのはInfoQ: Rx と Silverlight で非同期テストを記述するからなので、紹介したInfoQと、そして翻訳した勇 大地さんにも大変感謝します。

Silverlightの場合

Richard SzalayさんのコードはWP7のMicrosoft.Phone.Reactiveのためのものなので、Silverlight用Rxの場合はそのままでは動きません。はい。残念ながら、WP7版RxとDataCenter版Rxとでは、互換性がかなり崩壊しているので、そのまま動くことなんてないんです。悲しいですねえ……。これに関しては銀の光と藍い空: 「Rx と Silverlight で非同期テストを記述する」をWeb版にも使えるようにしたい!に書かれていますが、Silverlight用に移植してあげればよいようです。

既に、上記記事で田中さんが移植されているのですが、二番煎じに書いてみました(と、※欄で書いたものを流用です、毎回、流用させてもらっていてすみません……)

public static class TestHarnessSchedulerObservableExtensions
{
    public static IObservable<T> ObserveOnTestHarness<T>(this IObservable<T> source, WorkItemTest workItemTest)
    {
        return source.ObserveOn(new TestHarnessScheduler(workItemTest));
    }
 
    public static IDisposable RunAsyncTest<T>(this IObservable<T> source, WorkItemTest workItemTest, Action<T> assertion)
    {
        return source.ObserveOnTestHarness(workItemTest).Subscribe(assertion, () => workItemTest.TestComplete());
    }
}
 
public class TestHarnessScheduler : IScheduler, IDisposable
{
    readonly WorkItemTest workItemTest;
    readonly CompositeDisposable subscriptions;
 
    public TestHarnessScheduler(WorkItemTest workItemTest)
    {
        var completionSubscription =
            Observable.FromEventPattern<TestMethodCompletedEventArgs>(
                h => workItemTest.UnitTestHarness.TestMethodCompleted += h,
                h => workItemTest.UnitTestHarness.TestMethodCompleted -= h)
            .Take(1)
            .Subscribe(_ => Dispose());
 
        this.subscriptions = new CompositeDisposable(completionSubscription);
        this.workItemTest = workItemTest;
    }
 
    public void Dispose()
    {
        subscriptions.Dispose();
    }
 
    public DateTimeOffset Now
    {
        get { return DateTimeOffset.Now; }
    }
 
    public IDisposable Schedule<TState>(TState state, DateTimeOffset dueTime, Func<IScheduler, TState, IDisposable> action)
    {
        return Schedule(state, dueTime - Now, action);
    }
 
    public IDisposable Schedule<TState>(TState state, TimeSpan dueTime, Func<IScheduler, TState, IDisposable> action)
    {
        if (subscriptions.IsDisposed) return Disposable.Empty;
 
        workItemTest.EnqueueDelay(dueTime);
        return Schedule(state, action);
    }
 
    public IDisposable Schedule<TState>(TState state, Func<IScheduler, TState, IDisposable> action)
    {
        if (subscriptions.IsDisposed) return Disposable.Empty;
 
        var cancelToken = new BooleanDisposable();
 
        workItemTest.EnqueueCallback(() =>
        {
            if (!cancelToken.IsDisposed) action(this, state);
        });
 
        subscriptions.Add(cancelToken);
        return Disposable.Create(() => subscriptions.Remove(cancelToken));
    }
}

Richard Szalayさんのコードが非常に素晴らしく、あらゆるケースへのキャンセルに対して完全に考慮されているという感じなので、そのまま持ってきました。実際のところ、テスト用なので「例外発生/TestCompleteが呼ばれる」で実行自体が終了してしまうわけなので、こうもギチギチに考えなくてもいいのではかなー、とか緩いことを思ってしまいますが、まあ、よく出来ているならよく出来ているままに使わさせてもらいます。

メソッド名は、ObserveOnTestHarnessに変更しました。ObserveOnTestだけだと何かイマイチかなー、と思いまして。それと、時間のスケジューリングは、NotSupportedではなくて、EnqueueDelayというのものがあるので、それを使うことにしてみました。それと、ObserveOn -> Subscribe -> onCompletedにTestCompleteが定形文句なので、それらをひとまとめにしたRunAsyncTestを追加。こんな風に書けます。

var req = WebRequest.Create("http://www.google.co.jp/444");
Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse, req.EndGetResponse)()
    .RunAsyncTest(this, res => 
        res.ResponseUri.ToString().Is("http://www.google.co.jp/"));

定形文句が減る、つまりうっかりミスで書き忘れて死亡というのがなくなる、というのはいいことです。

通常のMSTestの場合

ところで、もしSilverlight/WP7固有の機能は使っていなくて、WPFでも利用出来るようなコードならば、コードをリンク共有の形でWPF側に持っていってしまって、そこでテスト実行してしまうと非常に楽です。まず第一に、MSTestやNUnitなどの通常のテストフレームワークが使えるため、Visual Studio統合やCIが簡単に行えます。第二に、非同期のテストが(Rxを使った場合)更に簡単になります。

[TestMethod]
public void AsyncTest()
{
    var req = WebRequest.Create("http://www.google.co.jp/");
    var result = Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse, req.EndGetResponse)()
        .First(); // First()で同期的に待機して値が取れる。複数の場合はToEnumerable().ToArray()で。
 
    result.ResponseUri.ToString().Is("http://www.google.co.jp/");
}

FirstやToEnumerable.ToArrayにより、同期的に待機することが出来るので、簡単にテストすることができます。通常のコードは同期的待機はすべきではないのですが、こうしたユニットテストの場合は便利に使えます。

じゃあSilverlightのユニットテストでも待機できるのはないか?というと、それはできません。理由はWindows Phone 7で同期APIを実現するたった つの冴えないやり方で書いたのですが、WebRequestなどのネットワーク問い合わせは、一度Dispatcherに積まれて、現在のメソッドを抜けた後に実行開始されるので、テスト実行スレッドで同期的に待って値を取り出すことは不可能なのです。

こういった細部の違いもあるので、コード共有してMSTestでチェックするのは楽でいいのですが、やはりSilverlight/WP7の実際の環境で動かされるユニットテストのほうも必要不可欠かなー、と。どこまでやるか、にもよりますが。

まとめ

Chaining Assertionは便利なので是非試してみてね!

なお、Rxを使うとTestScheduler(時間を好きなように進められる)やITestableObserver(通知の時間と値を記録できる)といった、イベント/非同期のテストを強力に支援する仕組みが備わっているので、それらと併用することで、より簡単に、もしくは今までは不可能だったことを記述できるようになります。それはまた後日そのうち。

SL/WP7のテストは、本当はIDE統合されてるといいんですけどねー。まあ、エミュレータ動かさなければならないので、しょうがないかな、というところもありますけれど。その辺も次期VisualStudioでは改善されるのかされないのか、怪しいところです。現在DeveloperPreviewで出ているVS11は、特に何も手をつけられてる感じがしないので、そのままな可能性はなきにしもあらず。どうなるかしらん。async/awaitが入ることだし、色々変わってくるとは思うんですけれど。

イベントベースの非同期処理をReactive Extensionsで扱う方法

Reactive Extensionsを使用して複数のサービス非同期コールを連続して呼べるか試してみた。その2 - y_maeyamaの日記 という記事にて、Rxを使ってWCFを綺麗に呼んでました。なるほど!Silverlightで!WCFで!非同期コールを連続して呼ぶ!とっても実践的なテーマ。XboxInfoほげほげがどうのこうの、とかやってたどこかの私と違います、げふんげふん。とても素晴らしいテーマとコードだと思ったので、拝借して私も少し書いてみました。

// 実行部分(実行後10秒後ぐらいにWCFサービスの連鎖を経てメッセージボックスに結果を表示)
var client = new ServiceReference1.Service1Client();
 
var asyncQuery = from a in Observable.Defer(() => client.GetAAsObservable("てすと"))
                 from b in client.GetBAsObservable(a.EventArgs.Result)
                 from c in client.GetCAsObservable(b.EventArgs.Result)
                 select c.EventArgs.Result;
 
asyncQuery.Subscribe(s => MessageBox.Show("チェーン終了 - " + s));

「前に呼んだサービスの引数にアクセス」に関しては、SelectManyで匿名型を作る、が解だと思います。で、幾つも連鎖する時はクエリ構文を使うのが大変お手軽。ただしクエリ構文は独自定義のメソッドと繋がりが悪くなるという欠点があります。RxではDoとかObserveOnとか結構使いますから、クエリ構文、便利な用で使いどころに困る代物。その辺は実際に書く内容によって判断つけるところですねー。

XxxAsObservable

突然出てきているGetAAsObservableって何だ、といったら、勿論、拡張メソッドです。FromEventやFromAsyncPatternなどはコード本体に書いてあると鬱陶しいので、基本は拡張メソッドで隔離してしまいましょう。命名規則に特に決まりはありませんが、私はXxxAsObservableという規則で書いています。今回はWCFのサービス参照で自動生成されたイベントベースの非同期処理のラップなので、FromEventをベースに、ただし少し小細工を。

public static class IObservableExtensions
{
    // イベントベースの非同期はRxと今一つ相性が悪いので変換する
    public static IObservable<IEvent<T>> ToAsynchronousObservable<T>(this IObservable<IEvent<T>> source) where T : EventArgs
    {
        var connectable = source
            .Take(1) // 実行は一回のみ(これしとかないとイベントハンドラの解除がされないし、そもそもPruneが動かなくなる)
            .Prune(); // AsyncSubjectに変換
        var detacher = connectable.Connect(); // コネクト(イベント登録実行)
        return connectable.Finally(() => detacher.Dispose()); // 任意のDispose時にイベントのデタッチ(なくても構いません)
    }
}
 
public static class Service1Extensions
{
    // 量産なので多いようならT4テンプレートで生成してしまうと良いでしょう
    public static IObservable<IEvent<GetACompletedEventArgs>> GetAAsObservable(this Service1Client client, string key)
    {
        var o = Observable.FromEvent<GetACompletedEventArgs>(
                h => client.GetACompleted += h, h => client.GetACompleted -= h)
            .ToAsynchronousObservable();
        client.GetAAsync(key); // 非同期実行開始
        return o;
    }
 
    // あと二つほど
}

ふつーにFromEventをラップして終わり、ではなくて少々細工を仕込んでいます。理由は、イベントベースの非同期処理はそのままだとRxでは扱いづらいから。

イベントベース非同期処理とReactive Extensions

.NET Frameworkには基本的に2つの非同期処理の方法があります。一つはBeginXxx-EndXxxによるAPM(非同期プログラミングモデル)。もう一つはXxxAsyncとXxxCompletedによるイベントベースの非同期処理。イベントベースは素で扱う分にはAPMよりもずっと書きやすかった。だからWebClient、BackgroundWorkerなど、手軽に使える系のものに採用された(というのが理由かは知りませんが)。そしてサービス参照の自動生成のものもイベントベース。

しかし、ラッピングして使う場合はとにかく使いづらい!一度の登録で何度も何度も実行されてしまうことは合成時に都合が悪い。また、ラップしたメソッドと、引数を渡す処理の実行箇所が離れてしまうことは(FromEvent.Subscribe で登録して client.GetAsync で実行)書くのが二度手間になり面倒、という他に、Subscribeよりも前に非同期実行して、更にSubscribeよりも前に非同期実行が完了してしまった場合は何も起こらなくなる。といった実行タイミングの面倒くさい問題まで絡んでくる。

というわけで、イベントベースの非同期処理をReactive Extensionsに載せる場合は、ただたんにFromEventで包むだけではなく、一工夫することをお薦めします。

それが上で定義したToAsynchronousObservable拡張メソッド。これはRxを使って非同期プログラミングを簡単にの時に少し出しました。 source.Take(1).Prune() ですって! ふむ、よくわからん。一行ずつ見ていくと、sourceは、この場合はFromEvent後のものを想定しています(なので拡張メソッドの対象はIObservable<IEvent>)。それをTake(1)。これは、ふつーの非同期処理では実行完了は1回だけだから、それを模しています。イベントベースのラップなので何もしないと無限回になっていますから。

続けてPrune。これは内部処理をAsyncSubjectに変換します。AsyncSubjectに関してはReactive Extensionsの非同期周りの解説と自前実装で簡易実装しましたが、処理が完了(OnCompleted)まで待機して、完了後はキャッシュした値を流すという、非同期処理で不都合(実行タイミングの問題など)が起こらなくするようにしたもの。対になるのはSubjectでイベント処理を模したもの。FromEventを使うと内部ではSubjectが使われることになるので、それを非同期で都合が良い形であるAsyncSubjectに変換する、ためにPruneを使いました。そしてここでConnectすることで即座にイベントにアタッチします(実行タイミングの前後の問題をなくすため)。

awaitは救いの手?

Reactive Extensionsは準備さえ済んでしまえば、非同期が恐ろしく簡単に書ける、のですが、如何せん準備が決して簡単とは言いません。さて、それがC# 5.0のasync/awaitは魔法のように解決してくれるのか、というと、そんなことはありません。現在のAsync CTPではAsyncCtpLibrary.dllにAsyncCtpExtensionsというクラスが用意されていて、その中には既存クラスに対して涙ぐましいまでにAsyncで使うのに最適なようにとXxxAsync拡張メソッドが大量に定義されています。例えばWebClientのDownloadStringにはDownloadStringTaskAsyncが。少しその中身を見てみれば、上で書いたものとやっていることは同じようなもの。イベントを登録して、完了時にはイベントハンドラを解除して。

既存クラスは用意してもらったのがあるからいいけど、サービス参照のような自動生成されるクラスにたいしてはどうすれば?答えは勿論、自分で書こうね!orz。まだまだ、そんな受難な時代は続きそうです。でも、Rxは一度分かってしまえばTake(1).Prune()で済むし、T4で自動生成もそんな難しくない雰囲気なので、まあ悪くはないんじゃないでしょーか。悪いのはむしろイベントベースの非同期処理なのでは、という気がしてきた昨今。

まとめ

今月はまだまだ非同期周りの記事を書くよ!いっぱい書くよ!

そういえばで、今回のソースをBitbucketに上げました。今後も何か書くときは合わせてBitbucketに上げていきたいと思っています。

neuecc / EventToAsync / overview – Bitbucket

RSSなどのアイコンが並んでいる部分の一番右のget sourceからzipで落とせます(Bitbucketはナビゲーションが少しわかりづらいのよねえ、Mercurialベースで非常に快適なソースホスティングサービスだとは思うのですが)。また、RxのライブラリはNuGetを使って参照しています(なのでRx本体のインストールがしてなくても試せます、かわりにNuGetが必要?不必要?その辺まだ仕組みがよく分かってないのですが……)。以前はNuPackっていう名前でしたが、名前が被るとかで変更されたそうです。VS2010の拡張機能マネージャのオンラインギャラリーからもVS拡張がインストール出来るので、とりあえずお薦め。色々なライブラリが簡単にインストール出来ます。

私もlinq.js登録しちゃおうかな、かな……。まあ、Fixしなきゃいけないことがかなり溜まってるので、まずはそれやれよって話なのですががが。

Windows Phone 7で同期APIを実現するたった つの冴えないやり方

Windows Phone 7が発表されました。中々に素晴らしい仕上がりに見えます。米国では来月発売と非常に順調そうですが、日本では…… ローカライズが非常に難しそうに見えました。発売されること自体は全然疑っていませんが、問題は、米国で達成出来ているクオリティをどこまで落とさず持ってこれるか。日本語フォントや日本語入力、今一つなBing Map、但し日本は除くなZune Pass。本体だけではなく、周辺サービスも持ってきて初めてWindows Phone 7の世界が完成する。ということを考えると、大変難しそう。

その辺はMicrosoft株式会社に頑張ってもらうとして、一開発者的には淡々とアプリ作るだけでする。というわけで、標題のお話。WP7というかSilverlightと、そして例によっていつもの通り、Rxの話です。

問題です。以下のコードの出力結果(Debug.WriteLineの順序)はどうなるでしょうか。

// 何も変哲もないボタンをクリックしたとする
void Button_Click(object sender, RoutedEventArgs e)
{
    Debug.WriteLine("start");
 
    // 10秒以内にレスポンスが来るとする
    var req = WebRequest.Create("http://bing.com/");
    req.BeginGetResponse(ar => Debug.WriteLine("async"), null);
 
    Thread.Sleep(10000); // 10秒待機
    Debug.WriteLine("end");
}

答えは後で。

Dispatcher.BeginInvokeとPriority

Dispatcherとは何ぞやか。について説明するには余白が狭すぎる。ので軽くスルーしてコードを。Dispatcher.BeginInvokeは通常は別スレッドから単発呼び出しが多いですが、UIスレッド上でDispatcher.BeginInvokeを呼ぶとどうなるでしょう?

// (WPF)何も変哲もないボタンをクリックしたとする
void Button_Click(object sender, RoutedEventArgs e)
{
    Debug.WriteLine("start");
 
    Dispatcher.BeginInvoke(new Action(() => Debug.WriteLine("normal1")), DispatcherPriority.Normal);
    Dispatcher.BeginInvoke(new Action(() => Debug.WriteLine("background")), DispatcherPriority.Background);
    Dispatcher.BeginInvoke(new Action(() => Debug.WriteLine("normal2")), DispatcherPriority.Normal);
 
    Debug.WriteLine("end");
}

結果は、start->end->normal1->normal2->backgroundです。なおDispatcherPriorityはWPFでは設定可能ですが、Silverlightでは設定不可で、内部的には全てBackgroundになります。挙動は以下の図のようになっています。

一番上のブロックが現在実行中メソッド。下のがDispatcher。BeginInvokeで実行キューに優先度付きで突っ込まれて、現在実行中のメソッドが終了したら、キューの中のメソッドが順次、優先度順に実行されます。といったイメージ。

問題の答え

冒頭の問題の答えは、WPFではstart->async->endの順。Silverlight(WP7も含む)ではstart->end->asyncの順になります。ええ。WPFとSilverlightで挙動が違うのです!今更何をっていう識者も多そうですが(Silverlightももう4だしねえ)私ははぢめて知りました。はまった。BeginGetResponseはWPFでは(というか普通の.NET環境では)そのまま別スレッド送りで実行されますが、Silverlightでは一旦Dispatcherに突っ込まれた後に実行されるのですねー、といったような雰囲気(なので一つ前でDispatcher.BeginInvokeがどうのという話を挟みました)。

Silverlightでは、BeginGetResponseはすぐには実行されない。それを踏まえて次へ。

非同期 to 同期

非同期を同期に変換してみましょう。Reactive Extensions for .NET (Rx)で。

void Button_Click(object sender, RoutedEventArgs e)
{
    // 非同期を同期に変換!
    var req = WebRequest.Create("http://bing.com/");
    var response = Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse, req.EndGetResponse).Invoke()
        .First();
    Debug.WriteLine(response.ResponseUri);
}

Rxで非同期を包むと長さ1のReactiveシーケンスとなるので、Firstを使うと同期的に値を取り出せる、という話を前回の記事 Rxを使って非同期プログラミングを簡単に でしました。そして実際、上のコードはWPFでは上手く動きます。きっちりブロックして値を取り出せる。勿論、それならGetResponseを使えよという話ではありますが。

では、Silverlight(勿論WP7でも)では、というと…… 永久フリーズします。理由は、BeginGetResponseはDispatcherに積まれた状態なので、現在実行中のメソッドを抜けない限りは動き出さない。Firstは非同期実行が完了するまでは現在実行中のメソッドで待機し続けるので、結果として、待機しているので実行が始まらない=実行完了は来ない→永遠に待機。になります。

結論としては、UIスレッド上で同期的に待つことは不可能です。代替案としてはThreadPoolで丸々包んでしまうということもなくはない。

void Button_Click(object sender, RoutedEventArgs e)
{
    ThreadPool.QueueUserWorkItem(_ =>
    {
        var req = WebRequest.Create("http://bing.com/");
        var response = Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse, req.EndGetResponse)
            .Invoke()
            .First();
        Debug.WriteLine(response.ResponseUri);
    });
}

こうすれば、BeginGetResponseが発動するので問題なく待機して値を取り出せます。でも、これじゃあ全然嬉しくもない話で全く意味がない。Rxで包んでいる状態ならば、.Subscribeでいいぢゃん。ということだし。

// 非同期を同期に、そんなことは幻想なのでこう書くのがベストプラクティス
void Button_Click(object sender, RoutedEventArgs e)
{
    var req = WebRequest.Create("http://bing.com/");
    Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse, req.EndGetResponse)
        .Invoke()
        .Subscribe(res => Debug.WriteLine(res.ResponseUri));
}

素直に、普通にReactive Extensionsを使うのが、一番簡単に書けます。息を吸うように、ごく自然にそこにあるものとしてRxを使おう。

Delegate.BeginInvokeのこと

相違点はまだあります。普通の.NET環境ではDelegateのBeginInvokeで非同期実行できますが、Silverlightにはありません。やってみるとNotSupportedExceptionが出ます。じゃあFuncにラップしてみるとどうだろう?

void Button_Click(object sender, RoutedEventArgs e)
{
    Debug.WriteLine("start");
 
    Action action = () => Debug.WriteLine("action");
    Func<AsyncCallback, object, IAsyncResult> wrappedBeginInvoke = action.BeginInvoke;
    wrappedBeginInvoke.Invoke(ar => Debug.WriteLine("async"), null);
 
    Debug.WriteLine("end");
}

WPFではstart->end->action->async。Silverlightではstart->NotSupportedExceptionの例外。ここまではいいんです。Windows Phone 7でこのコードを試すと、例外出ません。何故か実行出来ます。SilverlightではBeginInvokeは出来ないはずなのに!そして、その実行結果はstart->action->end。つまり、非同期じゃない。BeginInvokeじゃない。Invokeとして実行されてる。意味がさっぱりわかりません。

不思議!不思議すぎたので、MSDNのWindows Phone 7 Forumで聞いてみましたが、良い返答は貰えず。とりあえず、怪しい挙動をしているのは間違いないので、これはやらないほうが無難です。勿論、通常こんなこと書きはしないと思うのですが、RxのFromAsyncPatternをDelegateに対して使おうとするとこうなりますので注意。Delegateの非同期実行したい場合はFromAsyncPetternじゃなくてToAsyncを使いましょう。

void Button_Click(object sender, RoutedEventArgs e)
{
    Debug.WriteLine("start");
 
    // Observable.ToAsync(()=>{})でもいいし、すぐにInvokeするならObservable.Start(()=>{})も有用
    Action action = () => Debug.WriteLine("action");
    action.ToAsync().Invoke().Subscribe(_ => Debug.WriteLine("async"));
 
    Debug.WriteLine("end");
}

こうすることで、Rxは内部でBeginInvokeではなくThreadPoolを使うので、問題は起こらずWPFと同じ結果が得られます。

まとめ

同期的に書くほうが分かりやすいには違いないし、また、非同期が苦痛なのもその通り。でも、非同期を同期に、なんて考えない方がいい。AutoResetEventなどを駆使して擬似的に再現出来たとしても、やっぱ無理ありますし、非同期のメリットを犠牲にしてまでやるものではない。確かに非同期をそのまま扱うのは苦痛だけれど、Rxを使えば緩和される。むしろ慣れれば同期的に書くよりも利点が見えてくるぐらい。無理に同期に変換しようとしないでRxを覚えよう。が、結論です。

でもドキュメント全然ないし日本語の話なんて皆無で難しいって? そうですねえ、そうかもですねえ……。このブログも全然順序立ってなくて、思い立ったところから書いてるだけで分かりづらいことこの上ないし。うむむ……。でも、Rxの機能のうち非同期周りの解説に関してはほとんど出せているはずなので、読みにくい文章ですが、目を通してもらえればと思います。

もしつまづくところがあれば、Twitterで「Reactive Extensions」を投稿文に含めてくれれば、Twitter検索経由で見つけて反応します。(「Rx」だと検索結果が膨大になるので反応出来ません……)。検索を見てるワードとしては、他に「Linq」なども高確率で反応しにいきます←逆に怖いって?すみませんすみません。

「C#」が検索キーワードに使えたらいいんですけどねえ。「Scala」とか「JavaScript」は常時見てるんですが、かなり活況に流れているんですよ。そういうの見てると、Twitter上のC#な話も漏らさず見たい・参加したいと思ってしまうわけで。

Windows Phone 7 + Reactive ExtensionsによるXml取得

Windows Phone 7にはReactive Extensionsが標準搭載されていたりするのだよ!なんだってー!と、いうわけで、Real World Rx。じゃないですけれど、Rxを使って非同期処理をゴニョゴニョとしてみましょう。ネットワークからデータ取って何とかする、というと一昔前はRSSリーダーがサンプルの主役でしたが、最近だとTwitterリーダーなのでしょうね。というわけで、Twitterリーダーにします。といっても、ぶっちゃけただたんにデータ取ってリストボックスにバインドするだけです。そしてGUI部分はSilverlightを使用してWindows Phone 7でTwitterアプリケーションを構築 - @ITのものを丸ごと使います。手抜き!というわけで、差分としてはRxを使うか否かといったところしかありません。

なお、別に全然Windows Phone 7ならでは!なことはやらないので、WPFでもSilverlightでも同じように書けます。ちょっとしたRxのサンプルとしてどうぞ。今回は出たばかりのWindows Phone Developer Tools Betaを使います。Windows Phone用のBlendがついていたりと盛り沢山。

Xmlを読み込む

とりあえずLinq to XmlなのでXElement.Load(string uri)ですね。違います。そのオーバーロードはSilverlightでは使えないのであった。えー。なんでー。とはまあ、つまり、同期系APIの搭載はほとんどなくて、全部非同期系で操作するよう強要されているわけです。XElement.Loadは同期でネットワークからXMLを引っ張ってくる→ダウンロード時間中はUI固まる→許すまじ!ということのようで。みんな大好きBackgroundWorkerたん使えばいいぢゃない、みたいなのは通用しないそうだ。

MSDNにお聞きすれば方法 : LINQ to XML で任意の URI の場所から XML ファイルを読み込むとあります。ネットワークからデータを取ってくるときはWebClient/HttpWebRequest使えというお話。

では、とりあえず、MainPage.xamlにペタペタと書いて、MessageBox.Showで確認していくという原始人な手段を取っていきましょう。XElementの利用にはSystem.Xml.Linqの参照が別途必要です。

public MainPage()
{
    InitializeComponent();
 
    var wc = new WebClient();
    wc.OpenReadCompleted += (sender, e) =>
    {
        var elem = XElement.Load(e.Result); // e.ResultにStreamが入ってる
        MessageBox.Show(elem.ToString()); // 確認
    };
    wc.OpenReadAsync(new Uri("http://twitter.com/statuses/public_timeline.xml")); // 非同期読み込み呼び出し開始
}

別に難しいこともなくすんなりと表示されました。簡単なことが簡単に書けるって素晴らしい。で、WebClientのプロパティをマジマジと見ているとAllowReadStreamBufferingなんてものが。trueの場合はメモリにバッファリングされる。うーん、せっかくなので完全ストリーミングでやりたいなあ。これfalseならバッファリングなしってことですよね?じゃあ、バッファリング無しにしてみますか。

var wc = new WebClient();
wc.AllowReadStreamBuffering = false; // デフォはtrueでバッファリングあり、今回はfalseに変更
wc.OpenReadCompleted += (sender, e) =>
{
    try
    {
        var elem = XElement.Load(e.Result); // ここで例外出るよ!
    }
    catch (Exception ex)
    {
        // Read is not supporeted on the main thread when buffering is disabled.
        MessageBox.Show(ex.ToString());
    }
};

例外で死にました。徹底して同期的にネットワーク絡みの処理が入るのは許しません、というわけですね、なるほど。じゃあ別スレッドでやるよ、ということでとりあえずThreadPoolに突っ込んでみた。

wc.OpenReadCompleted += (sender, e) =>
{
    ThreadPool.QueueUserWorkItem(_ =>
    {
        try
        {
            var elem = XElement.Load(e.Result);
            MessageBox.Show(elem.ToString()); // 今度はここで例外!
        }
        catch(Exception ex)
        {
            // Invalid cross-thread access.
            Debug.WriteLine(ex.ToString());
        }
    });
};

読み込みは出来たけど、今度はMessageBox.Showのところで、Invalid Cross Thread Accessで死んだ。そっか、MessageBoxもUIスレッドなのか。うーむ、世の中難しいね!というわけで、とりあえずDispatcher.BeginInvokeしますか。

wc.OpenReadCompleted += (sender, e) =>
{
    ThreadPool.QueueUserWorkItem(_ =>
    {
        var elem = XElement.Load(e.Result);
        Dispatcher.BeginInvoke(() => MessageBox.Show(elem.ToString()));
    });
};

これで完全なストリームで非同期呼び出しでのXmlロードが出来たわけですね。これは面倒くさいし、Invoke系の入れ子が酷いことになってますよ、うわぁぁ。

Rxを使う

というわけで、非Rxでやると大変なのがよく分かりました。そこでRxの出番です。標準搭載されているので、参照設定を開きMicrosoft.Phone.ReactiveとSystem.Observableを加えるだけで準備完了。

var wc = new WebClient { AllowReadStreamBuffering = false };
 
Observable.FromEvent<OpenReadCompletedEventArgs>(wc, "OpenReadCompleted")
    .ObserveOn(Scheduler.ThreadPool) // ThreadPoolで動かすようにする
    .Select(e => XElement.Load(e.EventArgs.Result))
    .ObserveOnDispatcher() // UIスレッドに戻す
    .Subscribe(x => MessageBox.Show(x.ToString()));
 
wc.OpenReadAsync(new Uri("http://twitter.com/statuses/public_timeline.xml"));

非常にすっきり。Rxについて説明は、必要か否か若干悩むところですが説明しますと、イベントをLinq化します。今回はOpenReadCompletedイベントをLinqにしました。Linq化すると何が嬉しいって、ネストがなくなることです。非常に見やすい。更にRxの豊富なメソッド群を使えば普通ではやりにくいことがいとも簡単に出来ます。今回はObserveOnを使って、どのスレッドで実行するかを設定しました。驚くほど簡単に、分かりやすく。メソッドの流れそのままです。

FromAsyncPattern

WebClientだけじゃなく、ついでなのでHttpWebRequestでもやってみましょう。(HttpWebRequest)WebRequest.Create()死ね、といつも言ってる私ですが、SilverlightにはWebRequest.CreateHttpでHttpWebRequestが作れるじゃありませんか。何ともホッコリとします。微妙にこの辺、破綻した気がしますがむしろ見なかったことにしよう。

var req = WebRequest.CreateHttp("http://twitter.com/statuses/public_timeline.xml");
req.AllowReadStreamBuffering = false;
req.BeginGetResponse(ar =>
{
    using (var res = req.EndGetResponse(ar))
    using (var stream = res.GetResponseStream())
    {
        var x = XElement.Load(res.GetResponseStream());
        Dispatcher.BeginInvoke(() => MessageBox.Show(x.ToString()));
    }
}, null);

非同期しかないのでBeginXxx-EndXxxを使うのですが、まあ、結構面倒くさい。そこで、ここでもまたRxの出番。BeginXxx-EndXxx、つまりAPM(Asynchronus Programming Model:非同期プログラミングモデル)の形式の非同期メソッドをラップするFromAsyncPatternが使えます。

var req = HttpWebRequest.CreateHttp("http://twitter.com/statuses/public_timeline.xml");
req.AllowReadStreamBuffering = false;
 
Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse, req.EndGetResponse)
    .Invoke() // 非同期実行開始(Invoke()じゃなくて()でもOKです、ただのDelegateなので)
    .Select(res => XElement.Load(res.GetResponseStream()))
    .ObserveOnDispatcher()
    .Subscribe(x => MessageBox.Show(x.ToString()));

ラップは簡単で型として戻り値を指定してBeginXxxとEndXxxを渡すだけ。あとはそのまま流れるように書けてしまいます。普通だと面倒くさいはずのHttpWebRequestのほうがWebClientよりも素直に書けてしまう不思議!FromAsyncPatter、恐ろしい子。WebClient+FromEventは先にイベントを設定してURLで発動でしたが、こちらはURLを指定してから実行開始という、より「同期的」と同じように書ける感じがあって好き。WebClient使うのやめて、みんなHttpWebRequest使おうぜ!(ふつーのアプリのほうでは逆のこと言ってるのですががが)

ところで、非同期処理の実行開始タイミングはInvokeした瞬間であって、Subscribeした時ではありません。どーなってるかというと、ぶっちゃけRxは実行結果をキャッシュしてます。細かい話はまた後日ちゃんと紹介するときにでも。

バインドする

GUIはScottGu氏のサンプルを丸々頂いてしまいます。リロードボタンを押したらPublicTLを呼ぶだけ、みたいなのに簡略化してしまいました。

<Grid x:Name="LayoutRoot" Background="Transparent">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
 
    <Button Grid.Row="0" Height="72" Width="200" Content="Reload" Name="Reload"></Button>
    <ListBox Grid.Row="1" Name="TweetList" DataContext="{Binding}">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal">
                    <Image Source="{Binding Image}" Height="73" Width="73" VerticalAlignment="Top" />
                    <StackPanel Width="350">
                        <TextBlock Text="{Binding Name}" Foreground="Red" />
                        <TextBlock Text="{Binding Text}" TextWrapping="Wrap" />
                    </StackPanel>
                </StackPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</Grid>

あとは、ボタンへのイベント設定と、Twitterのクラスを作る必要があります。

public class TwitterStatus
{
    public long Id { get; set; }
    public string Text { get; set; }
    public string Name { get; set; }
    public string Image { get; set; }
 
    public TwitterStatus(XElement element)
    {
        Id = (long)element.Element("id");
        Text = (string)element.Element("text");
        Name = (string)element.Element("user").Element("screen_name");
        Image = (string)element.Element("user").Element("profile_image_url");
    }
}
 
public partial class MainPage : PhoneApplicationPage
{
    public MainPage()
    {
        InitializeComponent();
        Reload.Click += new RoutedEventHandler(Reload_Click); // XAMLに書いてもいいんですけど。
    }
 
    void Reload_Click(object sender, RoutedEventArgs e)
    {
        var req = HttpWebRequest.CreateHttp("http://twitter.com/statuses/public_timeline.xml");
        req.AllowReadStreamBuffering = false;
 
        Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse, req.EndGetResponse)
            .Invoke()
            .Select(res => XElement.Load(res.GetResponseStream()))
            .Select(x => x.Descendants("status").Select(xe => new TwitterStatus(xe)))
            .ObserveOnDispatcher()
            .Subscribe(ts => TweetList.ItemsSource = ts);
    }
}

実行するとこんな具合に表示されます。簡単ですねー。ただ、これだとリロードで20件しか表示されないので、リロードしたら継ぎ足されるように変更しましょう。

イベントを合成する

継ぎ足しの改善、のついでに、一定時間毎に更新も加えよう。基本は一定時間毎に更新だけど、リロードボタンしたら任意のタイミングでリロード。きっとよくあるパターン。Reload.Click+=でハンドラ足すのはやめて、その部分もFromEventでObservable化してしまいましょう。そして一定時間毎のイベント発動はObservable.Timerで。

// 30秒毎もしくはリロードボタンクリックでPublicTimeLineを更新
Observable.Merge(
        Observable.Timer(TimeSpan.Zero, TimeSpan.FromSeconds(30), Scheduler.NewThread).Select(_ => (object)_),
        Observable.FromEvent<RoutedEventArgs>(Reload, "Click").Select(_ => (object)_))
    .SelectMany(_ =>
    {
        var req = HttpWebRequest.CreateHttp("http://twitter.com/statuses/public_timeline.xml");
        req.AllowReadStreamBuffering = false;
        return Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse, req.EndGetResponse)();
    })
    .Select(res => XStreamingReader.Load(res.GetResponseStream()))
    .SelectMany(x => x
        .Descendants("status")
        .Select(xe => new TwitterStatus(xe))
        .Reverse()) // 古い順にする
    .Scan((before, current) => before.Id > current.Id ? before : current) // 最後に通した記事よりも古ければ通さない(で、同じ記事を返す)
    .DistinctUntilChanged(t => t.Id) // 同じ記事が連続して来た場合は何もしないでreturn
    .ObserveOnDispatcher()
    .Subscribe(t => TweetList.Items.Insert(0, t)); // Insertだって...

流れるようにメソッド足しまくるの楽しい!楽しすぎて色々足しすぎて悪ノリしている感が否めません、とほほ。解説しますと、まず一行目のMerge。これは複数本のイベントを一本に統一します。統一するためには型が同じでなければならないのですが、今回はTimer(long)と、Click(RoutedEventArgs)なのでそのままでは合成出来ません。どちらも発火タイミングが必要なだけでlongもRoutedEventArgsも不必要なため、Objectにキャストしてやって合流させました。

こういう場合、Linq to Objectsなら.Cast<object>()なんですよね。Castないんですか?というと、一応あるにはあるんですが、実質無いようなもので。というわけで、今のところキャストしたければ.Select(=>(object))を使うしかありません。多分。もっとマシなやり方がある場合は教えてください。

続いてSelectMany。TimerもしくはClickは発火のタイミングだけで、後ろに流すのはFromAsyncPatternのデータ。こういった、最初のイベントは発火タイミングにだけ使って、実際に流すものは他のイベントに摩り替える(例えばマウスクリックで発動させて、あとはマウスムーブを使うとか)というのは定型文に近い感じでよく使うことになるんじゃないかと思います。SelectMany大事。

XMLの読み込み部は、せっかくなので、こないだ作ったバッファに貯めこむことなくXmlを読み込めるXStreamingReaderを使います。こんな風に、XMLを読み取ってクラスに変換する程度ならXElement.Loadで丸々全体のツリーを作るのも勿体無い。XStreamingReaderなら完全ストリーミングでクラスに変換出来ますよー。という実例。

その下は更にもう一個SelectMany。こっちはLinq to Objectsのものと同じ意味で、IEnumerableを平たくしています。で、ScanしたDistinctUntilChangedして(解説が面倒になってきた)先頭にInsert(ちょっとダサい)。これで古いものから上に足される = 新しい順番に表示される、という形になりました。XAML側のListBoxを直に触ってInsertとか、明らかにダサい感じなのですが、まあ今回はただのサンプルなので見逃してください。

RxのMergeに関しては、後日他のイベント合流系メソッド(CombineLatest, Zip, And/Then/Plan/Join)と一緒に紹介したいと思っています。合流系大事。

まとめ

驚くほどSilverlightで開発簡単。っぽいような印象。C#書ける人ならすぐにとっかかれますねー。素晴らしい開発環境だと思います。そして私は同時に、Silverlight全然分かってないや、という現実を改めて突きつけられて参ってます。XAMLあんま書けない。Blend使えない。MVVM分からない。モバイル開発云々の前に、基本的な技量が全然欠けているということが良く分かったし、それはそれで良い収穫でした。この秋なのか冬なのかの発売までには、ある程度は技術を身につけておきたいところです。

そしてそれよりなにより開発機欲すぃです。エミュレータの起動も速いし悪くないのですが、やっぱ実機ですよ、実機!配ってくれぇー。

.NET Reactive Framework メソッド探訪第五回:Scan

Microsoft Silverlight を入手

var random = new Random();
Func<byte> nextByte = () => (byte)random.Next(0, byte.MaxValue + 1);
 
// CenterEllipseは真ん中の円のこと。
// 1000秒以内に3回クリックするとランダムで色が変わる
// GetMultiClickScan関数が今回の主題で、解説は後で。
CenterEllipse.GetMultiClickScan(3, 1000).Subscribe(() =>
    CenterEllipse.Fill = new SolidColorBrush(Color.FromArgb(255, nextByte(), nextByte(), nextByte())));

円を1秒以内にトリプルクリックすると色が変わります。右側のログ表示はミリセカンド単位でトリプルクリック時の経過時間を表しています。つまり、これが1000以内ならば色が変わり、1000より上ならば色は変わりません。このサンプルのテーマは、トリプルクリックの検出です。トリプルだけではなく、クアドラプルでもクインタプルでも、1秒以内じゃなくて5秒でも10秒でも応用できる形で書くとしたら、どう書く? グローバル変数に、クリックイベントを格納する配列でも置くかしらん。それがきっと簡単で分かりやすくて、でも……。

今回はRx Frameworkの関数から、Scanを取り上げます。この例題は私が考えたわけではなく、Ramblings of a Lazy Coder : My solutions to the Reactive Framework Tripple-click puzzleという問題からです。URLリンク先は解答編、というわけで、この解答を解説します。

考え方としては、クリックイベントに「クリックされた時間」という情報を付加。更に、クリック回数分の「前のデータ」を参照して指定時間内で連続クリックかどうかを確認する。時間情報の付加は匿名型を作るだけなので簡単ですが、「前のデータ」を参照するのが厄介。Linqは前にしか進まないし、送られてくるデータは過去のものなど知らない。このことはReactive Frameworkだけの話ではなく、以前にもLINQで左外部自己結合Scan?という記事で書きましたが、そこで出てくる解決策がScan。そうそう、ScanはAchiralにあるので(ということでlinq.jsにもあります)、動作は分かってます。ようするにAggregateの計算過程吐きだし版です。とりあえずlinq.js ReferenceでE.Range(1,10).Scan("x,y=>x+y")と打ってみてください(宣伝宣伝)

Scanならば「一個前」の情報が手に入る。しかし「複数個前」の情報はどうすれば? 答えは配列使えばいいぢゃない。だそうです。過程のデータをとりあえず配列に入れて、次に送り出してやれば、そりゃ簡単に取り出せますね。何だか邪道な気がしますが、気にしない気にしない。URL先の解答例では生の配列を使って、インデックスをゴニャゴニャとしていて非常に正しいとは思いますが、あまり生の配列のインデックスは扱いたくないので、Queueを使ってみました。別にQueueのClear()のコストなんてたかが知れてるっしょ(中ではArray.Clearを呼んでいて、Array.Clearは……以下略)という割り切りで。

public static IObservable<MouseButtonEventArgs> GetMultiClickScan
    (this UIElement element, int count, int multiClickSpeedInMilliSeconds)
{
    return Observable.FromEvent<MouseButtonEventArgs>(element, "MouseLeftButtonDown")
        .Select(e => e.EventArgs)
        .Scan(new
        {
            Times = new Queue<DateTime>(count),
            Hit = false,
            Event = (MouseButtonEventArgs)null // ダミーなのでnullをキャストするのが楽
        }, (a, e) =>
        {
            var isHit = false;
            var now = DateTime.Now;
            a.Times.Enqueue(now);
            if (a.Times.Count == count)
            {
                var first = a.Times.Dequeue();
                Debug.WriteLine((now - first).TotalMilliseconds); // Debug
                if ((now - first).TotalMilliseconds <= multiClickSpeedInMilliSeconds)
                {
                    isHit = true;
                    a.Times.Clear();
                }
            }
 
            return new
            {
                Times = a.Times,
                Hit = isHit,
                Event = e
            };
        })
        .Where(a => a.Hit)
        .Select(a => a.Event);
}

ScanはAggregateと全く同じで、accumlatorのみの実行の他に、第一引数でseedを渡すこともできます。Scanのseedで、変数と判定を行うためのフラグを保持するクラスを作り、accumlatorで判定と変数の持ち越しを行い、Whereで判定をフィルタリング。このコンボは非常に強力で、幾らでも応用が効きそう。LinqでのAggregateはほとんど使われませんが、RxにおけるScanはよく見かけることになるのではないかと思います。

で、理屈は分かったけれど、何だかゴチャゴチャとしてない? という感想は否めない。ただ、グローバル領域に変数を置く必要なくフラグを閉じ込められていること、そして、応用の効きそうな柔らかさが見えたり見えなかったりしませんか? 応用的なものも、追々考えていきたいです。

次元の狭間へ

Scanが出たので、Aggregateもついでにおさらい。これはLinq to ObjectsのAggregateと変わりません。ちなみにRx Frameworkでの内部実装はScan().Last()だったりするので、Scanとほんとーに丸っきり変わりません。じゃあ、無限リピートのFromEventにたいしてAggregateって、実行するとどうなるの?というのは気になるところですが、答えは次元の狭間に入ってしまって、その行からコードが一切進まなくなります。AggregateだけじゃなくCountやLast、ToEnumerableなど全てを列挙してから答えを返す系のメソッドは全て同じ結果になります。コンソールアプリの簡単なコードで試してみると、こうなる。

class MyClass
{
    public event EventHandler<EventArgs> Ev;
 
    public void Fire()
    {
        Ev(this, new EventArgs());
    }
}
 
static void Main(string[] args)
{
    var mc = new MyClass();
    var count = Observable.FromEvent<EventArgs>(mc, "Ev")
        .Count(); // ここで次元の狭間にダイブする
 
    mc.Fire(); // ここに到達することは未来永劫無い
}

恐ろしや。ただ、前段階でTakeやTakeWhileを挟めば、無限リストは有限リストとなるので、面白い感じに制限が出来ます。この辺も応用例として、そのうち紹介していければと思います。

カウンター

もう一度Scanを見ます。トリプルクリックの例題は捻りすぎな感が否めないので、もっとストレートに、Scanの「前の値を保持し続けることが出来る」という点を見せる例題を一つ。クロージャの例なんかでも定番のカウンターで。

Microsoft Silverlight を入手

Observable.FromEvent<RoutedEventArgs>(CounterButton, "Click")
    .Scan(0, (x, y) => ++x)
    .Subscribe(i => CounterButton.Content = i + "Clicked");

とても簡潔で、変数が全て閉じ込められていて、綺麗……。

ログ吐き骨組み

Microsoft Silverlight を入手

デモ大事。Subscribeの時にConsole.WriteLine並べて、実行結果想像つきますよね、というのがいまひとつすぎたので、出力が見える骨組みを作りました。今後のReactive Frameworkの紹介時にソースコード上のDebug.WriteLineは、こーいうことなんですねー、と思ってください。毎回これ乗っけてると長ったらしいので、暗黙の、ということで。

<UserControl x:Class="SilverlightApplication4.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
    mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition Width="200" />
        </Grid.ColumnDefinitions>
 
        <StackPanel Grid.Column="0">
            <Button Name="ExecuteButton" Content="Execute" />
            <Button Name="ErrorButton" Content="Error" />
            <Button Name="ObservableButton" Content="Observable" />
            <Button Name="EnumerableButton" Content="Enumerable" />
        </StackPanel>
        <ScrollViewer Grid.Column="1">
            <TextBlock Name="LogBrowseTextBlock"></TextBlock>
        </ScrollViewer>
    </Grid>
</UserControl>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
using System.Windows.Controls.Primitives;
using System.Reflection;
 
namespace SilverlightApplication4
{
    public partial class MainPage : UserControl
    {
        public MainPage()
        {
            InitializeComponent();
            Debug.Set(LogBrowseTextBlock, Dispatcher);
 
            ExecuteButton.GetClick().Subscribe(() =>
                Observable.Range(1, 10).Subscribe(
                    i => Debug.WriteLine(i),
                    e => Debug.WriteLine(e),
                    () => Debug.WriteLine("completed")
                )
            );
 
            ErrorButton.GetClick().Subscribe(() =>
                Observable.Range(1, 10)
                    .Do(i => { if (i == 5) throw new Exception(); })
                    .Subscribe(
                        i => Debug.WriteLine(i),
                        e => Debug.WriteLine("onError"),
                        () => Debug.WriteLine("onCompleted")
                    )
            );
 
            ObservableButton.GetClick().Subscribe(() =>
                GetMethodNames(typeof(Observable)).ToList().ForEach(s => Debug.WriteLine(s))
            );
 
            EnumerableButton.GetClick().Subscribe(() =>
                  GetMethodNames(typeof(Enumerable)).ToList().ForEach(s => Debug.WriteLine(s))
            );
        }
 
        IEnumerable<string> GetMethodNames(Type type)
        {
            return type.GetMethods(BindingFlags.Static | BindingFlags.Public)
                .Select(mi => mi.Name)
                .OrderBy(s => s)
                .Distinct();
        }
    }
 
    public static class Debug
    {
        private static TextBlock textBlock;
        private static Dispatcher dispatcher;
 
        public static void Set(TextBlock textBlock, Dispatcher dispatcher)
        {
            Debug.textBlock = textBlock;
            Debug.dispatcher = dispatcher;
        }
 
        public static void WriteLine(object message)
        {
            if (textBlock != null)
            {
                dispatcher.BeginInvoke(() =>
                    textBlock.Text = message.ToString() + Environment.NewLine + textBlock.Text);
            }
        }
    }
 
    public static class ControlExtensions
    {
        public static IObservable<Event<RoutedEventArgs>> GetClick(this ButtonBase button)
        {
            return Observable.FromEvent<RoutedEventArgs>(button, "Click");
        }
    }
}

幾ら簡易的なものだから、という言い訳がましさを考えても、かなり微妙なコードな気がする。原型はWPFでTraceListenerにTextBlockに書きだすものを追加して、Trace.WriteLineで処理していたもの。SilverlightにはTraceがなかったので、そのまま静的クラス・メソッドに置き換えて、Silverlightにも本来あるDebugを塗り替えちゃうという……。ようするにそのままコピペしても動くよね!的な感じでやりたいな、というところなわけです。ダメ?

ついでにSubscribeの中でObservableって、汚い、ように見えるかも。でも実際これはなんてことなくて、ようはJavaScriptっぽいんですよね、ほんと。DOMContentLoadedにイベント登録のaddEventListener並べるのと一緒で。じゃあ実際こうしてグチャグチャ並べるかというとそうではないようで、実際は拡張メソッドへ記述する、という形で分散していくようですが、まだまだ分からず。Microsoft側の実例やドキュメントが整ってくれないと何とも言えない感じ。

作業環境

画像クリックで原寸サイズ。最近思うところあってVisual Studioの配置をごにょごにょと弄っています。今は、こんな感じに落ち着きました。左にエラー一覧・検索など。右にソリューションエクスプローラー・クラスビュー・スタートページなど。そして左右にそれぞれコードウィンドウを分割。原則的にメインウィンドウは左。コード定義ウィンドウを右ウィンドウに開いて常時表示。もしくはXAML編集と並列したり。といったところです。コード定義ウィンドウはデカい画面で常時表示で初めて効果を発揮しますね、素晴らしく便利。

30インチ 2560×1600の無駄遣いが火を吹く!というわけですが、やっぱ広いって便利、エディタウィンドウ2面同時表示って便利、です。30インチでなくても、横2560は19インチ1280×1024のデュアルで行けます。ただ、実際はこれに加えてデバッグ時のプログラム本体なりブラウザなりを置いておく場所が欲しいので、その場合はデュアルじゃ足りないですね……。グラフィックボードが一枚でトリプルをサポートしてくれれば、というか、するべき、ですよね。ATIのEyefinityにはとても期待してます。

.NET Reactive Framework メソッド探訪第一回:FromEvent

まず、リアクティブフレームワークとは何ぞや、ということなのですが今のところInfoQ: .NETリアクティブフレームワーク(Rx)がLINQ to Eventsを可能にするの記事ぐらいしか情報はありません。.NET 4.0に含まれる(かもしれない)ということ、現在のところSilverlight Toolkitの単体テストのところにこっそりと配置されていること。それだけです。紹介も、記事中にもリンクされていますがunfold: Introducing Rx (Linq to Events)の一連の記事ぐらいしかありません。これの前文が中々に素敵です。

Buried deep in the bin folder of the Silverlight Toolkit Unit Tests is a hidden gem: The Rx Framework (System.Reactive.dll). If you glanced quickly you’d miss it altogether but it’s one of the most exciting additions to the .NET framework since Linq.

今のところ微妙にパッとしない(Parallelは簡単に使えるがゆえにインパクトが足らない)4.0の隠し玉はコレですね、間違いない。軽く触ってみたのですが、中々に感動的。Linq to Objects好きならば間違いなく琴線に触れます。C#3.0がコレクションの操作をforeachからLinqに変えてしまったように、.NET4.0はイベントもLinqに変わる。まさにLinq to Everywhere! Functional Reactive Programming!

How to use

Silverlight ToolkitをダウンロードしてSource/Binaries/System.Reactive.dllを頂けば完了。ただし、これはそのままだとSilverlightのプロジェクトでしか動作しないので、その他ので利用したい場合はここの記事に示されているように、githubに公開されているコードを実行(Cecilのdllが必要、記事文中にリンクされています)して変換する必要があります。今回はとりあえず、Silverlightで試してみたいと思います。こちらはこちらで、Silverlight 3 Toolsのダウンロードが必要ですけれど。

実例

Microsoft Silverlight を入手

マウスの動きに円が追随する、という単純なものをSilverlightで作ってみました。移動は完全に追随するのではなく、座標が15で割り切れる位置の場合のみ移動としました。スナップすることをイメージしたつもりなのですが、動きがガクガクです。これは、マウス移動に完全に追随して全ての座標でイベントが発生するわけではない = 15で割り切れる座標を通過してもイベントが発生しない場合がある = 動きがガクガク。というわけで、スナップしたい場合はWhereで間引くのではなく、Selectで近傍座標に寄せるべきです、が、いやまあ、例なので……。

// XAMLではなく全部コード上に書いたのは両方を張るのが面倒だから……
// 内容はCanvasとEllipseを配置するというもので、本筋とは関係ありません
InitializeComponent();
var canvas = new Canvas { Background = new SolidColorBrush(new Color {A=255, R = 100, G = 100, B = 100 }) };
var ellipse = new Ellipse { Height = 30, Width = 30, Fill = new SolidColorBrush(Colors.Orange) };
canvas.Children.Add(ellipse);
this.Content = canvas;
 
// FromEventはイベント発火がトリガとなってLinq発動
// 後段に送られるのはEvent<T>というもので、
// SenderとEventArgsという読み取り専用プロパティを持つクラス
var canvasMove = Observable.FromEvent<MouseEventArgs>(canvas, "MouseMove")
    .Select(e => e.EventArgs.GetPosition(canvas))
    .Where(p => (p.X % 15 == 0) || (p.Y % 15 == 0))
    .Subscribe(p =>
    {
        ellipse.SetValue(Canvas.LeftProperty, p.X - ellipse.Width / 2);
        ellipse.SetValue(Canvas.TopProperty, p.Y - ellipse.Height / 2);
    });
 
// Subscribeの戻り値の型はIDisposable
// Disposeを呼ぶと登録したイベントをデタッチすることが出来る
// デタッチしないなら取得する必要は特にはない
// canvasMove.Dispose();

MouseMoveでイベントが発火する度にLinqを通る。なるほど、イベントがリストに、見える。イベントを無限リスト生成として捉えることで、イベントに対してLinq操作が可能になった。ObserverパターンとIteratorパターンは同じだったんだよ!なんだってー!みたいなノリがある。もう少し丁寧に見ると、Observable.FromEventでイベントをPush型の無限リストに変換。戻り値はIObservable<T>。イベント発火時に後段に流れてくるのはEvent<T>。これは通常のイベント登録時に使うsenderとeventArgsをラップしただけの単純なもの。あとはIObservableに用意されているメソッド(Select, Where, TakeWhileなどお馴染みのものから、Delay, WaitUntilなどイベント用の目新しいメソッドなど多数)を繋げて、最後にSubscribe。このSubscribeは、つまり通常のイベント登録時のメソッド本文の役割を果たす。Linqで言ったらForEachのようなもの。Subscribeのオーバーロードも幾つかあるのですが、それはまた後日。

// つまるところ、以下のコードと同じだったりはする
// ただ、IObservable<T>は通常のイベント登録では無理な複雑な操作が簡単、
// そして何よりも、このような単純なコードでもそんなに複雑になっていない!
canvas.MouseMove += (sender, e) =>
{
    var pos = e.GetPosition(canvas); // Select
    if (!(pos.X % 15 == 0 || pos.Y % 15 == 0)) return; // Where
    ellipse.SetValue(Canvas.LeftProperty, pos.X - ellipse.Width / 2);
    ellipse.SetValue(Canvas.TopProperty, pos.Y - ellipse.Height / 2);
};

通常のイベント登録と対比してみると分かりやすいかしらん。FromEventではMouseEventArgsという型を明示する必要があるのがカッタルイ。推論は偉大。が、しかし、IObservableが複雑な操作が可能なのに対し、イベントに追加では直球なものしか書けない。また、複雑な操作が可能なわりには、FromEventは驚くほどシンプルに書ける。シンプルな操作でも(記述するのに)重たくない、というのは特筆すべきことじゃあないでしょうか。

// stringを避けたこういう登録方法もあるけれど、面倒なうえに警告出る
Observable.FromEvent((EventHandler<MouseEventArgs> h) => new MouseEventHandler(h),
        h => canvas.MouseMove += h, // addHandler
        h => canvas.MouseMove -= h) // removeHandler
    .Subscribe(e => Debug.WriteLine(e.EventArgs.GetPosition(canvas)));

ところで、イベント名をstringで書くのはどうよ、ていうかJavaScriptのaddEventHandlerみたいで嫌だよね?ね?リファクタリング効かないわ、IntelliSenseも動かないわでロクなことがない。というわけで、FromEventのオーバーロードを見ると、ちゃんと普通に登録する方法も用意されてはいる。一応、用意、されては、いる。が、しかし、あんまりだー。あんまりすぎるー。流れてくるEventhandler<MouseEventArgs>をMouseMoveが受け取ってくれないので、第一引数でMouseEventHandlerに変換する(ところで警告が消せないのですが、警告無しで処理する方法ってあるのかしらん)。あとは、addとremoveの登録。長ったらすぎてこれはダメぽ。確かに、こんなんなら、stringでいいです……。

Microsoft Silverlight を入手

// 普段あまり書かないMouseEventArgsとかいう型定義は書きにくいし
// メソッド名もstringで書くのはミスが出がち、ということで
// 拡張メソッドでイベント取り出し用のメソッドを予め作っておくと良い
public static IObservable<Event<MouseEventArgs>> GetMouseMove(this UIElement elem)
{
    return Observable.FromEvent<MouseEventArgs>(elem, "MouseMove");
}
 
// マウスの軌跡を1秒後に描画します
canvas.GetMouseMove()
    .Select(e => e.EventArgs.GetPosition(canvas))
    .Delay(1000)
    .Subscribe(p =>Dispatcher.BeginInvoke(()=>
    {
        ellipse.SetValue(Canvas.LeftProperty, p.X - ellipse.Width / 2);
        ellipse.SetValue(Canvas.TopProperty, p.Y - ellipse.Height / 2);
    }));

汚い部分は隔離!ということで、拡張メソッドに退避してやると、美しく書ける。いやまあ、この辺は全部unfold: The Joy of Rx: Extension Eventsに書いてあることなのですけど。んで、デモ的にもう少し面白げがあったほうがいいかな、と思ったのでDelayを足してみました。1秒後にマウス移動の軌跡を描画します。グルグルーっとマウス動かして止めてみてください。スムーズ、とは言い難いですね、しょんぼり。記述も、Delayを足すだけ。と言いたかったんですがBeginInvokeかあ、これどーにかなる方法ないかなあ。

次回

全10回ぐらいで、全部のメソッドを紹介するつもりです。私が理解できればの話ですが。ちょこちょこと実例的なものも交えていきたいと思います。私が使いこなせればの話ですが。というわけで、次回はSubscribeのオーバーロードの紹介にしたいと思います。Reactive Frameworkならではの魅力、に関してはもう少し先になってしまいそう。少し飛ばして、非同期連結の話なんかを先に持ってきた方が良いかなあ。

Search/Archive

Category

Profile


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

April 2011
|
July 2018

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