.NETの標準シリアライザ(XML/JSON)の使い分けまとめ
- 2011-12-10
今年も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の新機能ネタとかいいんじゃないでしょうか。