XStreamingReader - Linq to Xml形式でのストリーミングXml読み込み
- 2010-07-16
CodePlex : XStreamingReader - Streaming for Linq to Xml
1クラスライブラリシリーズ。もしくはストリーミングをIEnumerableに変換していこうシリーズ。またはシンプルだけど小粒でピリッと隙間にぴったりはまるシリーズ(を、目指したい)。といったわけで、100行程度ではあるのですが、表題の機能を持つコードをCodePlexに公開しました。それとおまけとして、XMLファイルからC#クラス自動生成T4 Templateも同梱。
Linq to Xml風にXmlを読み込めるけれど、ツリーを構築せずストリームで、完全遅延評価で実行します。Linq to Xmlには、書き込み用にXStreamingElementというものがあるため、それと対比した読み込み用という位置付けのつもりです。メモリの厳しいモバイル機器や、巨大なXMLを読み込む際に使えるんじゃないかと思っています。
利用例
ぶっちゃけまるっきりXElementと同じです。例としてYahoo!天気情報のRSSから京都と東京を取り出し。
// XElement
var kyoto = XElement.Load(@"http://rss.weather.yahoo.co.jp/rss/days/6100.xml")
.Descendants("item")
.Select(x => new
{
Title = (string)x.Element("title"),
Description = (string)x.Element("description"),
PubDate = (DateTime)x.Element("pubDate")
})
.Where(a => !a.Title.StartsWith("[PR]")) // itemが広告の場合は除外
.ToArray();
// XStreamingReader
var tokyo = XStreamingReader.Load(@"http://rss.weather.yahoo.co.jp/rss/days/4410.xml")
.Descendants("item")
.Select(x => new
{
Title = (string)x.Element("title"),
Description = (string)x.Element("description"),
PubDate = (DateTime)x.Element("pubDate")
})
.Where(a => !a.Title.StartsWith("[PR]")) // itemが広告の場合は除外
.ToArray();
Load/Parseで生成し、ElementsやDescendantsで抽出。あとは、IEnumerable<XElement>となっているので、SelectしたりWhereしたり。完全にLinq to Xmlと同じAPIです。同じすぎてこれだけだと利点がさっぱり見えませんが、100%遅延評価+ストリーミング読み込みで逐次生成という違いがあります。詳しくは次のセクションで。
バックグラウンド
Androidでは性能のためにDOMじゃなくてSAXでXML扱うんだ。という話を良く聞いて、確かにただデータ取るためだけにDOM構築ってのは嫌だし、そりゃ避けたい。対象がDOMなら素直にそう思いますが、しかし、もしそれがLinq to Xmlならどうだろう?Windows Phone 7だったらLinq to Xml使うに決まってるよ、と言いたいのですが、これってDOMと同じく、すぐに(LoadなりParseなりした直後)ツリーを構築しています。Elements()なりDescendants()なりの戻り値がIEnumerableなため、遅延評価かと思ってしまうわけですが、遅延評価されるのはツリーの探索が、というだけであって、構築自体は即時でされています。
DOMに比べて軽量(という謳い文句)であることと、非常に軽々と書けるため抵抗感がないわけですが、考えてみれば Load.Descendants.Select みたいな、API叩いて何らかのクラスなり匿名型なりに変換するという程度の、しかしよくある定型作業は、わざわざツリー作る必要はなくストリーミングで取れるし、それならばストリーミングで取るべきではある。しかし、今時XmlReaderを直で触るなんて、時代への逆行のようなことはやりたくない。
ストリームはIEnumerableに変換するのがLinq以降のC#の常識。というのを日々連呼しているので、今回はXmlReaderをIEnumerable<T>に変換しなければなりません。しかし、困ったのが、<T>のTを何にすればいいのか、ということ。ファイル読み込みなら一行のString。データベースなら、IDataRecord(DbExecutorというライブラリとしてCodePlexに公開しています)を用いましたが、XmlReaderだと適当なのが見当たらない。XmlReaderを直接渡すのは危なっかしいし、そもそも渡したところで面倒くさいことにかわりなくてダメだ。何か適切なコンテナが……。
と、考えたり考えなかったりで、Twitterでもにょもにょと言っていたら
@neuecc Linq to Xml を使うにしても XmlReaderからReadSubtreeで切り出した断片に対してかなー、XML全体をオンメモリさせる必然性がなければStreamから読んで処理した端からGCに捨てて貰えるようにしておきたいだけだけど
http://twitter.com/kazuk/status/18193188205
うぉ!うぉぉぉぉぉ!なるほど、断片をXElementに変換してそれを渡せば、操作しやすいし感覚的にもXElement.Loadなどと変わらないしでベストだ!言われてみればそりゃそうだよねー、ですが全然頭になかった、まさにコロンブスの卵。こういうことがサラッと出てくることこそが、凄さだよね。
と、感嘆しつつ、それそのまま頂き、というわけで、TをXElementにするという形で解決しました。
public IEnumerable<XElement> Descendants(XName name)
{
using (var reader = readerFactory())
{
while (reader.ReadToFollowing(name.LocalName, name.NamespaceName))
{
yield return XElement.Load(reader.ReadSubtree());
}
}
}
Descendantsの実装はこんな感じで、断片から都度XElement生成しているという、それだけの単純明快な代物です。そのため挙動はXElement.Load.Descendantsと完全同一というわけじゃありません。例えばサブツリー中に同名の要素がある場合、XElementでDescendantsの場合はサブツリー中の要素も列挙しますが、XStreamingReaderではトップ要素のものだけが拾われます。
他に注意点としては、それぞれのXElementは完全に独立しているため、ParentやNextNodeなどは全てnullです。よってAncestorsで先祖と比較しながらの複雑な抽出、などといったことも出来ません。TwitterのAPIのような、ウェブサービスとして用意されているXMLなら素直な構造なので問題はありませんが、SGMLReaderでLinq to HTMLなどといった場合は、結構複雑なクエリで抽出することになるため使えないでしょう。その場合は素直にXElement.Loadを使うのが良いと思います。
おまけ(Xml→自動クラス生成)
Xmlから人力でClass作るのって定型作業で面倒だよねー。ということで、自動生成するT4 Templateも同梱しました。プロパティ定義だけではなく(ちゃんとPascalCaseに直します)、コンストラクタにXElementを投げるとマッピングもしてくれます。つまりは、XStreamingReaderの仕様に合わせたものです。
.ttの上の方にある3つの項目を適当に書き換えると
string XmlString = new WebClient().DownloadString("http://twitter.com/statuses/public_timeline.xml");
const string DescendantsName = "status"; // select class root
const string Namespace = "Twitter"; // namespace
namespace Twitter
{
public class Status
{
public string CreatedAt { get; set; }
public string Id { get; set; }
// snip...
public User User { get; set; }
public string Geo { get; set; }
public Status(XElement element)
{
this.CreatedAt = (string)element.Element("created_at");
this.Id = (string)element.Element("id");
this.User = new User(element.Element("user"));
this.Geo = (string)element.Element("geo");
}
}
public class User
{
public string Id { get; set; }
public string Name { get; set; }
public string ScreenName { get; set; }
// snip...
public string FollowRequestSent { get; set; }
public User(XElement element)
{
this.Id = (string)element.Element("id");
this.Name = (string)element.Element("name");
this.ScreenName = (string)element.Element("screen_name");
this.FollowRequestSent = (string)element.Element("follow_request_sent");
}
}
}
こんなのが生成されます。型は全部stringになるので、手動で直してください。半自動生成。T4で生成→新しいクラスファイル作って生成結果をコピペ→型を直す。みたいな使い方をイメージしています。完全自動生成じゃないと変更に対する自動追随ってのが出来ないので、自動生成する意味が半減。しかし、型かあ、スキーマないと無理ですな。まあ、ウェブサービスのAPIなどは基本的には固定で変化がないでしょうから、ある程度は手間を省けるんじゃないかと思われます。
まとめ
断片とはいえ、XElement作るのは無駄じゃないの?というと、無駄ではあります。抽出したらすぐ用済みでポイなわけなので、純粋にパフォーマンスの観点から言えばXmlReaderを直で触ったほうが良いに決まっています。しかし、さすがにそこまで来ると無視して良いと思うわけです。例えばLinqで一時的な匿名型は使わないって?ああ、むしろLinqなんてやめて全部forループにでもします?言いだいたらキリがない。
今回で大事なのは、ストリーミング化しても、決して使いやすさは損なわれていないということです。ツリー構築型と全く同じように快適に書ける。それが何より大切。「性能のために書きやすさが犠牲になるぐらいなら性能なんていらない!」と、現実は言えなくても心では言ってしまいます。ユーザー視点だと逆ですが……。ただ、中長期的には、スパゲティコードは開発者を幸せにしない→機能追加速度低下/洗練が鈍る→ユーザーも不幸せになる、のループが回るので綺麗さは重要。勿論、そこが性能上本当にボトルネックになっているならば気合入れて叩く必要がありますが、気分的に、もしくはマイクロベンチマーク的にちょっと性能Downな程度でパフォーマンスチューニングとか言い出すのならシバいてよし。
といったわけかで、私なりにWindows Phone 7プログラミングへの準備を進めています。これで、準備になってる?……だと?ご冗談を。ですね、はい、すみません。開発キットのベータ版が出たので、次回はWindows Phone 7で何か作ろう紹介でも書く予定は未定。
IEnumerableのCastを抹殺するためのT4 Templateの使い方
- 2010-07-07
.NET Framework 1.0の負の遺産、HogeCollection。それらの大半はIEnumerable<T>を実装していない、つまるところ一々Cast<T>しなければLinqが使えない。ほんとどうしょうもない。大抵のHogeCollectionは実質Obsoleteみたいなもので、滅多に使わないのだけれど、ただ一つ、RegexのMatchCollectionだけは頻繁に使うわけで、Castにイラつかされるので殺害したい。RegexにはMatchCollection、GroupCollection、CaptureCollectionという恐怖の連鎖が待っているので余計に殺したい。(ところで全く本題とは関係ないのですが、Captureは今ひとつ使い道がわからな……)
// わざとらしい例ですが
var q = Regex.Matches("aag0 hag5 zag2", @"(.)ag(\d)")
.Cast<Match>()
.SelectMany(m => m.Groups.Cast<Group>().Skip(1).Select(g => g.Value))
.ToArray(); // a0h5z2
おお、何というCast地獄!つーか.NET 4でBCL書き直したとか言うんなら、その辺も少し融通聞かせてIEnumerable<T>にしてくれてもさー。あ、要望出さないのが悪いとかなのでしょうか……。それなら自己責任ですね、ちゃんと出していかないと。なのはともかく、自己責任ならば自己責任なりに、文句だけ言っててもしょうがないので自前で何とかしましょう。
ようするに.Cast<Hoge>()を自動で挟めばいいわけですよね。んー、ぴこーん!T4でジェネレートすればいいんじゃね?というわけで、T4 Templateを使ってみました。実際のところT4試してみたかったんだけどネタがなかったので、ネタが出てきて万歳!が本音だったりはします。
何もないところからテンプレートじゃあ作りようもないので、ひとまず完成系を書いてみる。
public static class MatchCollectionExtensions
{
public static IEnumerable<TResult> Select<TResult>(this MatchCollection source, Func<Match, TResult> selector)
{
return source.Cast<Match>().Select(selector);
}
// Where, Aggregate, ....
}
こんな形。グッとイメージしやすくなります。型引数のTSourceを消して、Castを挟んで……。やるべき事が大体見えてきました。まずは、Enumerableの拡張メソッドの抽出を。
var extMethods = typeof(Enumerable)
.GetMethods()
.Where(mi => Attribute.IsDefined(mi, typeof(ExtensionAttribute)));
特にBindingFlagsは設定しませんが、ExtensionAttributeが指定されているものがあれば拡張メソッド、という判定で問題なく取り出すことが出来ます。続いて戻り値を抽出。
var returnType = extMethods
.Select(mi => mi.ReturnType)
.Select(mi => Regex.Replace(mi.Name, "`.*$", "")
+ (mi.IsGenericType ? ("<" + string.Join(", ", mi.GetGenericArguments().Select(t => t.Name)) + ">") : ""));
IEnumerable<T>のNameはIEnumerable1になっているので
1を正規表現で削除。そして引数を並べる。ただまあ、これだけだとジェネリック引数がネストしたものに対応出来ていなかったりTSourceが除去できてなかったりダメなのですが、それはそれ(最終的なコードは下記の実例のほうを見てください)。
といったわけで、相変わらずリフレクション+Linqは鉄板ですね。というかLinqなしのリフレクションとかやりたくない……。こんな感じにポチポチと素材集めをしたら、T4化します。
<#@ template language="C#" #>
<#@ output extension="cs" #>
<#@ assembly Name="System.Core.dll" #>
<#@ import namespace="System" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Runtime.CompilerServices" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ import namespace="System.Reflection" #>
<#
var target = new Dictionary<string, string>
{
{"MatchCollection", "Match"},
{"GroupCollection", "Group"},
{"CaptureCollection", "Capture"}
};
#>
<#
var ignoreMethods = new HashSet<string>
{
"Max", "Min", "Average", "Sum", "Zip", "OfType", "Cast",
"Join", "GroupJoin", "ThenBy", "ThenByDescending", "LongCount"
};
#>
using System;
using System.Collections.Generic;
using System.Linq;
namespace System.Text.RegularExpressions
{
<#
foreach (var kvp in target)
{
#>
public static class <#= kvp.Key.Replace(".","") #>Extensions
{
<#
foreach (var methodInfo in typeof(Enumerable).GetMethods().Where(mi => Attribute.IsDefined(mi, typeof(ExtensionAttribute))))
{
if(ignoreMethods.Contains(methodInfo.Name)) continue;
#>
public static <#= MakeReturnType(methodInfo, kvp.Value) #> <#= methodInfo.Name #><#= MakeGenericArguments(methodInfo) #>(this <#= kvp.Key #> source<#= MakeParameters(methodInfo, kvp.Value) #>)
{
return source.Cast<<#= kvp.Value #>>().<#= MakeMethodBody(methodInfo) #>;
}
<#}#>
}
<#}#>
}
<#+
const string TSource = "TSource";
static string ConstructTypeString(Type type, string castType)
{
var result = type.Name.Contains(TSource)
? type.Name.Replace(TSource, castType)
: Regex.Replace(type.Name, "`.*$", "");
if (type.IsGenericType)
{
result += string.Format("<{0}>", string.Join(", ", type.GetGenericArguments().Select(t => ConstructTypeString(t, castType))));
}
return result;
}
static string MakeReturnType(MethodInfo info, string castType)
{
return ConstructTypeString(info.ReturnType, castType);
}
static string MakeGenericArguments(MethodInfo info)
{
var types = info.GetGenericArguments().Select(t => t.Name).Where(s => s != TSource);
return types.Any() ? string.Format("<{0}>", string.Join(", ", types)) : "";
}
static string MakeParameters(MethodInfo info, string castType)
{
var param = info.GetParameters()
.Skip(1)
.Select(pi => new { pi.Name, ParameterType = ConstructTypeString(pi.ParameterType, castType) });
return param.Any()
? ", " + string.Join(", ", param.Select(a => a.ParameterType + " " + a.Name))
: "";
}
static string MakeMethodBody(MethodInfo info)
{
var args = info.GetParameters().Skip(1).Select(pi => pi.Name);
return string.Format("{0}({1})", info.Name, args.Any() ? string.Join(", ", args) : "");
}
#>
上のほうの、ディクショナリ(target)の初期化子を弄ることで対象の型を増減できます。namespaceはテンプレートに埋め込みなので変える場合は適当に変えてください。ハッシュセット(ignoreMethods)はその名の通り、除外したい拡張メソッドを指定します。今回はMax,Minなどと、Zip,Join,GroupJoin(これらは若干弄らないと対応出来ないので見送り)を除外しています。あとLongCountも外してます、理由はRxのSystem.InteractiveがLongCountで競合するから(多分、Rxチームのミスだと思うのでそのうち直ると思います)。
どんなクラスにも対応出来る(はず)ので、もしキャストが必要なウザいHogeCollectionがあったら、このテンプレートを使ってみると良いかもしれません。WinFormsのControl.ControlCollectionとかWPFのUIElementCollectionとか(そういうのは、元よりごった煮で詰め込むの前提なので、UIElementでSelect出来ても嬉しくはないかなー)。ともあれ、利用はご自由にどうぞ。
こんな感じに、MatchCollection, GroupCollection, CaptureCollectionだと合計1100行ぐらいのコードが生成されます。これで、CastいらずにLinqが書けるようになりました。メデタシメデタシ。
T4 Template
T4 Templateはかなり良いですね。VisualStudioと密接に動作して、生成出来ないようならエラーですぐ知らせてくれるのが嬉しい。これ大事。超大事。それがないと書けません。C#もそうだけれど、とりあえず書く→コンパイラエラー→直す、をリアルタイムで繰り返せるのは素晴らしい。現代のプログラミング環境はこうでないと、な良さに溢れてます。アドインを入れれば入力補完やシンタックスハイライトも付いてくるので非常に快適。
T4 Templateは標準搭載の機能だし実に強力なので、積極的に使っていきたいものです。MSDNだとコード生成とテキスト テンプレート辺りかな。例によって、読んでもさっぱり意味がわかりません(笑) 今のところオフィシャルだとこんなドキュメントしかないのかなあ、少し厳しめ。いやまあ、T4自体は構文がシンプルなので、ただ書くだけならサンプル改変で何とかなる、というか、私もサンプル改変以上の機能は知らないのですががが。
Rx(Reactive Extensions)を自前簡易再実装する
- 2010-07-05
という表題でUstreamやりました。Reactive Extensions for .NET (Rx)のSelect, Where, ToObservable, FromEventを実装することで、挙動を知ろうという企画。結果?酷いものです!
Shift+Alt+F10はお友達。それにしたってぐだぐだ。想像以上に頭が真っ白。セッションやライブコーディングしてる人は凄いね、と実感する。プレゼンどころか人と話すのも苦手です、な私には敷居が高かった。とにかく説明ができない。デバッガで動かせば分かりやすいよねー、なんてやる前は思ってたんですが、人がデバッガ動かしてるの見ててもさっぱり分かりやすくないよ!ということに途中で気づいて青ざめる。
まあ、こういうのも経験積まないとダメよね、と考えると、リスクゼロ(見てくれた人には申し訳ないですが)で練習出来るので、これからもネタがあればやっていきたいとは思います。反省は活かして。ネタはあまりないのでリクエストあればお願いします。Ustreamの高画質配信については、去年に書いた高画質配信するためのまとめ記事が自分で役に立ったぜ、経験が活きたな、的な。私自身の環境はちょっと、というかかなり変わったのですが、配信の基本的部分に関しては今も昔も(といっても1年前か)変わってなかったね。
さて、そんなUstreamはともかくとして、Rxの基本的な拡張メソッド「Select, Where」と、基本的な生成メソッド「ToObservable, FromEvent」を自前で実装してみる/デバッガで追ってみましょう。自分の手で動かして追うと理解しやすくなります。なので、以下に出すソースはコピペでもいいので、実際にVisualStudio上で動かしてもらえればと思います。
IEnumerableで考える
IObservableの拡張メソッド実装、の前に復習を兼ねてIEnumerableの拡張メソッドを実装してみましょう。
public static IEnumerable<TR> Select<T, TR>(IEnumerable<T> source, Func<T, TR> selector)
{
foreach (var item in source)
{
yield return selector(item);
}
}
恐ろしく簡単です。こんなにも簡単に書けるのは、yield returnのお陰。裏では、コンパイラが自動で対応するIEnumerable, IEnumeratorを生成してくれます。もしこれを教科書通りに自前で書くとしたら
public static IEnumerable<TR> Select<T, TR>(IEnumerable<T> source, Func<T, TR> selector)
{
return new SelectEnumerable<TR>(); // 本当は引数も必要ですが省略
}
class SelectEnumerable<T> : IEnumerable<T>
{
public IEnumerator<T> GetEnumerator()
{
return new SelectEnumerator<T>();
}
// 以下略
// IEnumerator IEnumerable.GetEnumerator()
}
class SelectEnumerator<T> : IEnumerator<T>
{
// Current, Dispose, MoveNextが必要ですが略
}
ああ、長い。やってられない。こんなものがオブジェクト指向だなどと言うならば、クソったれだと唾を吐きたくなる。そこで、AnonymousHogeパターンを用いれば……
public static IEnumerable<TR> Select<T, TR>(this IEnumerable<T> source, Func<T, TR> selector)
{
return new AnonymousEnumerable<TR>(() =>
{
var enumerator = source.GetEnumerator();
return new AnonymousEnumerator<TR>(
() => enumerator.MoveNext(),
() => selector(enumerator.Current),
() => enumerator.Dispose()
);
});
}
驚くほどスッキリ。デザインパターンの本はC#でラムダ式全開でやり直すと、考え方はともかく、コードは全然違った内容になるんじゃないかなあ、とか思いつつ。この突然出てきたAnonymousEnumerableに関しては.NET Reactive Framework メソッド探訪第二回:AnonymousEnumerableを参照にどうぞ。去年の9月ですか……。AnonymousObservableも紹介する、といって10ヶ月後にようやく果たせている辺りが、やるやる詐欺すぎて本当にごめんなさい。
簡単に説明すれば、コンストラクタにラムダ式で各メソッドの本体を与えてあげることで、その場でクラスを作ることが出来るという代物です。クロージャによる変数キャプチャにより、引数を渡し回す必要もないため非常にすっきり書く事ができます。
これってようするにJavaの無名クラスでしょ?と言うと、その通り。おお、Java、大勝利。なんてこたぁーない。大は小を兼ねない、むしろこれは、小は大を兼ねる事の証明。
AnonymousObservable
IObservableはIEnumerableのようなコンパイラサポートはないので、自前で書かなければなりません。が、普通に書くと面倒なので、AnonymousObservableを使って書くことにしましょう。
public class AnonymousObservable<T> : IObservable<T>
{
Func<IObserver<T>, IDisposable> subscribe;
public AnonymousObservable(Func<IObserver<T>, IDisposable> subscribe)
{
this.subscribe = subscribe;
}
public IDisposable Subscribe(IObserver<T> observer)
{
return subscribe(observer);
}
}
public class AnonymousObserver<T> : IObserver<T>
{
Action<T> onNext;
Action<Exception> onError;
Action onCompleted;
public AnonymousObserver(Action<T> onNext, Action<Exception> onError, Action onCompleted)
{
this.onNext = onNext;
this.onError = onError;
this.onCompleted = onCompleted;
}
public void OnCompleted()
{
onCompleted();
}
public void OnError(Exception error)
{
onError(error);
}
public void OnNext(T value)
{
onNext(value);
}
}
public class AnonymousDisposable : IDisposable
{
Action dispose;
bool isDisposed = false;
public AnonymousDisposable(Action dispose)
{
this.dispose = dispose;
}
public void Dispose()
{
if (!isDisposed)
{
isDisposed = true;
dispose();
}
}
}
そのまま書き出すだけなので、難しいことは何一つありませんが、面倒くさい……。なお、今回はRx抜きでの実装のためこうして自前で定義していますが、RxにはObservable.Create/CreateWithDisposable、Observer.Create、Disposable.Createというメソッドが用意されていて、それらは今回定義したAnonymousHogeと同一です。new ではなくCreateメソッドで生成するため型推論が効くのが嬉しい。
Observable.Select/Where
下準備が済んだので実装していきましょう。まずはSelect。
public static IObservable<R> Select<T, TR>(this IObservable<T> source, Func<T, TR> selector)
{
return new AnonymousObservable<TR>(observer => source.Subscribe(
new AnonymousObserver<T>(
t => observer.OnNext(selector(t)),
observer.OnError,
observer.OnCompleted)));
}
Enumerableと似ているようで非常に分かりにくい。AnonymousObservableの引数のラムダ式は、Subscribeされた時に実行されるもの。というわけで、突然出てきているかのような引数のobserverは、Subscribeによって一つ後ろのメソッドチェーンから渡されるものとなります。
Observable.Range(1, 10) // これがsource
.Select(i => i * i)
.Subscribe(i => Console.WriteLine(i)); // これがobserver
こんな前後関係の図式になっています。ドットの一つ前のメソッドがsource、一つ後ろのメソッドがobserver。 最終的な目的としては元ソースからOnNext->OnNext->OnNextと値を伝搬させる必要があるわけですが、元ソースは末端どころか次に渡す先すら知りません。そのため、まず最初(Subscribeされた時)にsource.Subscribeの連鎖で元ソースまで遡ってやる必要がある、というわけです。非常に説明しづらいのでデバッガで追ってみてください。
public static IObservable<T> Where<T>(this IObservable<T> source, Func<T, bool> predicate)
{
return new AnonymousObservable<T>(observer => source.Subscribe(
new AnonymousObserver<T>(
t => { if (predicate(t)) observer.OnNext(t); },
observer.OnError,
observer.OnCompleted)));
}
WhereはSelectのOnNext部分が違うだけのもの。コピペ量産体制。
ToObservable
Selectなどと同じくreturn new AnonymousObservableですが、もうSubscribeはしません(そもそもIObservable sourceがないので出来ないですが)。ここからは、末端から伝達されてきたobserverに対して値をPushしてやります。
public static IObservable<T> ToObservable<T>(this IEnumerable<T> source)
{
return new AnonymousObservable<T>(observer =>
{
var isErrorOccured = false;
try
{
foreach (var item in source)
{
observer.OnNext(item);
}
}
catch (Exception e)
{
isErrorOccured = true;
observer.OnError(e);
}
if (!isErrorOccured) observer.OnCompleted();
return new AnonymousDisposable(() => { });
});
}
Subscribeされると即座にforeachが回ってOnNext呼びまくる。ToObservableはHot or ColdのうちColdで、Subscribeされるとすぐに値が列挙されるわけです。Coldってのは、なんてことはなく、ようはすぐforeachされるからってだけの話でした。
戻り値のIDisposableは、FromEventではイベントのデタッチなどの処理がありますが、ToObservableでは何もする必要がないので何も無し。
FromEvent徹底解剖
Coldだけでは、別にEnumerbaleと全然変わらなくて全く面白くないので、Hot Observableも見てみます。Hotの代表格はFromEvent。そんなFromEventには4つのオーバーロードがあります。せっかくなので、細かく徹底的に見てみましょう。
public class EventSample
{
public event EventHandler BlankEH;
public event EventHandler<SampleEventArgs> GenericEH;
public event SampleEventHandler SampleEH;
}
public class SampleEventArgs : EventArgs { }
public delegate void SampleEventHandler(object sender, SampleEventArgs e);
static void Main(string[] args)
{
var sample = new EventSample();
// 1. EventHandlerに対応するもの
Observable.FromEvent(
h => sample.BlankEH += h, h => sample.BlankEH -= h);
// 2. EventHandler<EventArgs>に対応するもの
Observable.FromEvent<SampleEventArgs>(
h => sample.GenericEH += h, h => sample.GenericEH -= h);
// 3. 独自EventHandlerに対応するもの
Observable.FromEvent<SampleEventHandler, SampleEventArgs>(
h => new SampleEventHandler(h),
h => sample.SampleEH += h, h => sample.SampleEH -= h);
// 4. リフレクション
Observable.FromEvent<SampleEventArgs>(sample, "GenericEH");
Observable.FromEvent<SampleEventArgs>(sample, "SampleEH");
}
FromEventと言ったら文字列で渡して―― という感じだったりですが、むしろそれのほうが例外的なショートカットで、基本はeventをadd/removeする関数を渡します。3つもありますが、基本的には三番目、conversionが必要なものが最も多く出番があるでしょうか。ただのEventHandlerなんて普通は使わないし、ジェネリクスのEventHandlerもほとんど見かけないしで、どうせみんな独自のEventHandlerなんでしょ、みたいな。もしEventHandler<T>で統一されていれば、こんな面倒くさいconversionなんて必要なかったのに!もしくは、みんなAction<object, TEventArgs>で良かった。名前付きデリゲートの氾濫の弊害がこんなところにも……。
実際のとこ文字列渡しで良いよねー、と思います。リフレクションのコストはどうせ最初の一回だけだし。リファクタリング効かないといっても、別にイベントの名前なんて変更しないっしょっていうか、フレームワークに用意されてるイベントは固定だし、って話ですし。
FromEventの作成
そんなわけで、今回は3引数のFromEventを作ります。FromEventの戻り値はIEventなので、IEventの定義も一緒に。
public interface IEvent<TEventArgs> where TEventArgs : EventArgs
{
object Sender { get; }
TEventArgs EventArgs { get; }
}
public class AnonymousEvent<TEventArgs> : IEvent<TEventArgs> where TEventArgs : EventArgs
{
readonly object sender;
readonly TEventArgs eventArgs;
public AnonymousEvent(object sender, TEventArgs eventArgs)
{
this.sender = sender;
this.eventArgs = eventArgs;
}
public object Sender
{
get { return sender; }
}
public TEventArgs EventArgs
{
get { return eventArgs; }
}
}
public static IObservable<IEvent<TEventArgs>> FromEvent<TDelegate, TEventArgs>(
Func<EventHandler<TEventArgs>, TDelegate> conversion,
Action<TDelegate> addHandler,
Action<TDelegate> removeHandler) where TEventArgs : EventArgs
{
return new AnonymousObservable<IEvent<TEventArgs>>(observer =>
{
var handler = conversion((sender, e) =>
{
observer.OnNext(new AnonymousEvent<TEventArgs>(sender, e));
});
addHandler(handler);
return new AnonymousDisposable(() => removeHandler(handler));
});
}
感覚的にはToObservableの時と一緒。Subscribeされたら実行される関数を書く。Subscribe時に実際に実行されるのはaddHandlerだけ。つまりイベント登録。そしてイベントが発火した場合は、conversionのところのラムダ式に書いたものが呼び出される、つまり次のobserverに対してOnNextでIEventを送る。そして、DisposeされたらremoveHandlerの実行。
これが、Hotなわけですね。つまりSubscribeだけではOnNextが呼ばれず、もう一段階、奥から実行される。
// 実行例としてObservableCollectionなどを用意。
var collection = new ObservableCollection<int>();
var collectionChanged = Observable.FromEvent<NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventArgs>(
h => new NotifyCollectionChangedEventHandler(h),
h => collection.CollectionChanged += h,
h => collection.CollectionChanged -= h)
.Select(e => (int)e.EventArgs.NewItems[0]);
// attach
collectionChanged.Subscribe(new AnonymousObserver<int>(i => Console.WriteLine(i), e => { }, () => { }));
collectionChanged.Subscribe(new AnonymousObserver<int>(i => Console.WriteLine(i * i), e => { }, () => { }));
collection.Add(100); // 100, 10000
collection.Add(200); // 200, 40000
利用時は大体こんな感じになります。いたって普通。
まとめ
というわけで実装を見ていきましたが、意外と簡単です。リフレクタでToObservable見たけどこんな簡単じゃなかったぞ!と言われると、そうですね、実際のRxはScheduler(カレントスレッドで実行するかスレッドプールで実行するか、などなどが選べる)が絡むので実装はもう少し、というかもうかなり複雑です。だからこそ惑わされてしまうというわけで、基本的な骨格部分にのみ絞ってみれば十二分にシンプル、というのを掴むのが肝要じゃないかと思います。
次回は前回予告の通りに、後回しにしちゃってるけれど結合周りを紹介できればいいなあ。あと、FromAsyncか、Timer周辺か、Schedulerか……。RxJSもちゃんと例を出したいし、例を出したいといえば、そう、メソッド紹介だけじゃなく実例も出していきたいなあ、だし。うーん。まあ、ボチボチとやっていきます。最近ほんとRxの知名度・注目度が高まってるような気がしてます。ぐぐる検索で私のへっぽこ記事が上位に出てしまうという現状なので、申し訳ない、じゃなくて、それ相応の責任を果たすという方向で頑張りたいと思います。つまりは記事をちゃんと充実させよう。
linq.js ver.2.2.0.0 - 配列最適化, QUnitテスト, RxJSバインディング
- 2010-06-29
CodePlex - linq.js - LINQ for JavaScript
linq.jsをver 2.2に更新しました。変更事項は、メソッドの追加、配列ラッピング時の動作最適化、ユニットテストのQUnitへの移行、RxJSバインディング追加の4つです(あと、若干のバグフィックスと、RxJS用vsdoc生成プログラムの同梱)。まずは、追加した二つのメソッドについて。
var seq = Enumerable.From([1, 5, 10, 4, 3, 2, 99]);
// TakeFromLastは末尾からn個の値を取得する
var r1 = seq.TakeFromLast(3).ToArray(); // [3, 2, 99]
// 2.0から追加されているTakeExceptLast(末尾からn個を除く)と対になっています
var r2 = seq.TakeExceptLast(3).ToArray(); // [1, 5, 10, 4]
// ToJSONはjson文字列化します(列挙をJSON化なので必ず配列の形になります)
// JSON.stringifyによるJSON化のため、
// ネイティブJSON対応ブラウザ(IE8以降, Firefox, Chrome, Opera...)
// もしくはjson2.jsをインポートしていないと動作しません
var objs = [{ hoge: "huga" }, { tako: 3}];
var json = Enumerable.From(objs).ToJSON(); // [{"hoge":"huga"},{"tako":3}]
TakeFromLast/TakeExceptLastはRxからの移植です(Rxについては後でまた少し書きます)。RxではTakeLast, SkipLastという名前ですが、諸般の都合により名前は異なります。より説明的なので悪くはないかな、と。
もう一つはToJSONの復活。ver 1.xにはあったのですが、2.xでばっさり削ってたました。復活といっても、実装は大きく違います。1.xでは自前でシリアライズしていたのですが、今回はJSON.stringifyに丸投げしています。と、いうのも、IE8やそれ以外のブラウザはJSONのネイティブ実装があるので、それに投げた方が速いし安全。ネイティブ実装はjson2.jsと互換性があるので、IE6とかネイティブ実装に対応していないブラウザに対しては、json2.jsを読み込んでおくことでToJSONは動作します。
json2.jsはネイティブ実装がある場合は上書きせずネイティブ実装を優先するようになっているので、JSON使う場合は何も考えずとりあえず読み込んでおくといいですね。
配列ラップ時の最適化
Any, Count, ElementAt, ElementAtOrDefault, First, FirstOrDefault, Last, LastOrDefault, Skip, SequenceEqual, TakeExceptLast, TakeFromLast, Reverse, ToString。
Enumerable.From(array)の直後に、以上のメソッドを呼んだ際は最適化された挙動を取るように変更しました。各メソッドに共通するのは、lengthが使えるメソッドということです。Linqは基本的に長さの情報を持っていない(無限リストとか扱えるから)ため、例えばCountだったら最後まで列挙して長さを取っていました。しかし、lengthが分かっているのならば、Countはlengthに置き換えられるし、Reverseは[length - 1]から逆順に列挙すればいい。ElementAt(n)はまんま[n]だしLastは[length - 1]だし、などなど、lengthを使うことで計算量が大幅に低減されます。
C#でも同様のことをやっている(ということは以前にLinqとCountの効率という記事で書いてあったりはする)のですが、今になってようやく再現。先の記事にあるように、C#では中でisやasを使ってIEnumerableの型を調べて分岐させてますが、linq.jsでは Enumerable.FromでEnumerableを生成する際に、今まではEnumerableを返していたところを、ArrayEnumerable(継承して配列用にメソッドをオーバーライドしたもの)を返す、という形を取っています。
これが嬉しいかどうかというと、そこまで気にするほどではありません。C#では array.Last() のように使えますが、linq.jsではわざわざ Enumerable.From(array).Last() と、ラップしなきゃいけませんから、それならarray[array.length - 1]でいいよ、という。ちなみに当然ですがFrom(array).Where().Count()とか、他のLinqメソッドを挟むと、ArrayEnumerableじゃなくEnumerableになるため最適化的なものは消滅します。
でもまあ、意味はあるといえばあります。配列を包んだだけのEnumerableは割と色々なところで出てきます。例えばGroupJoin。これのresultSelectorの引数のEnumerableは、配列をラップしただけです。又は、ToLookup。Lookupを生成後、Getで取得した際の戻り値のEnumerableは配列を包んだだけです。GroupByの列挙(Grouping)もそう。特にGroupingで、グループの個数を使うってシーンは多いように思います。そこで今まではCount()で全件列挙が廻っていたのが、一度も列挙せずに値が取れるというのは精神衛生上喜ばしい。
パフォーマンス?
このArrayへの最適化は勿論パフォーマンスのためなのですが、じゃあ全体的にlinq.jsのパフォーマンスはどうなの?というと、遅いよ!少し列挙するだけで山のように関数呼び出しが間に入りますから、速そうな要素が一つもない。ただ、遅さがクリティカルに影響するほどのものかは、場合によりけりなので分かりません。遅い遅いと言っても、jQueryでセレクタ使って抽出してDOM弄りするのとどちらが重いかといったら、(データ量にもよりますが)圧倒的にDOM弄りですよね?的な。
JavaScriptは、どうでもいいようなレベルの高速化記事がはてブなんかにも良く上がってくるんですが、つまらない目先に囚われず、全体を見てボトルネックをしっかり掴んでそこを直すべきだと思うんですよね。「1万回の要素追加で9msの高速化」とか、意味無いだろそれ絶対と思うのですが……。
ただ、アプリケーションとライブラリだと話は別で、ライブラリならば1msでも速いにこしたことはないのは事実です。linq.jsは仕組み的には遅いの確定なのはしょうがないとしても、もう少しぐらいは、速度に気を使って努力すべきな気はとてもします。今後の課題。
コードスニペット
無名関数書くのに、毎回function(x) { return って書くの、面倒くさいですよね。ということで、Visual Studio用のコードスニペットを同梱しました。func1->Tab->Tabで、linq.jsで頻繁に使う一行の無名関数 function(x){ return /* キャレットここ */ } を生成してくれます。これは激しく便利で、C#の快適さの3割ぐらいを占めていると言っても過言ではないぐらいに便利なのですが、動画じゃないと伝わらないー、けれど動画撮ってる体力的余裕がないので省略。
func0, func1, func2, action0, action1, action2を定義しています。0だの1だのは引数の数。funcはreturn付き、actionはreturn無しのスニペットです。また、Enumerable.RangeとEnumerable.Fromにもスニペットを用意しました。erange, efromで展開されます。jQueryプラグイン版の場合はjqrange, jqfromになります。
インストールは、Visual Studio 2010でツール→コードスニペットマネージャーを開いてインポートでsnipetts/.snippetを全部インポート。
binding for RxJS
RxJS -Reactive Extensions for JavaScriptと接続出来るようになりました。ToObservableとToEnumerableです(jQuery版のTojQueryとtoEnumerableと同じ感覚)。
// enumerable sequence to observable
var source = Enumerable.Range(1, 10)
.Shuffle()
.ToObservable()
.Publish();
source.Where(function (x) { return x % 2 == 0 })
.Subscribe(function (x) { document.writeln("Even:" + x + "<br>") });
source.Where(function (x) { return x % 2 != 0 })
.Subscribe(function (x) { document.writeln("Odd:" + x + "<br>") });
source.Connect();
// observable to enumerable
var subject = new Rx.ReplaySubject();
subject.OnNext("I");
subject.OnNext(4);
subject.OnNext("B");
subject.OnNext(2);
subject.OnNext("M");
var result = subject.ToEnumerable()
.OfType(String)
.Select(function (x) { return x.charCodeAt() - 1 })
.Select(function (x) { return String.fromCharCode(x) })
.ToString("-");
alert(result); // H-A-L
ToObservableでは、こないだ例に出したPublishによる分配を。ToEnumerableは、例が全然浮かばなかったので適当にOnNextを発火させた奴をEnumerable化出来ますねー、と。なお、cold限定です。hotに対して適用すると空シーケンスが返ってくるだけです(ちなみにC#版のRxでhotに対してToEnumerableするとスレッドをロックして無限待機になる)
それと、RxVSDocGeneratorの最新版を同梱してあります。以前に公開していたのは、RxJSのバージョンアップと同時に動かなくなっちゃってたのよね。というわけで、修正したうえで同梱商法してみました。
プレースホルダの拡張
無名関数のプレースホルダが少し拡張されました。今までは引数が一つの時のみ$が使えたのですが、今回から二引数目は$$、三引数目は$$$、四引数目は$$$$が使えるようになりました。
// 連続した重複がある場合最初の値だけを取る
// (RxのDistinctUntilChangedをlinq.jsでやる場合)
// 1, 5, 4, 3, 4
Enumerable.From([1, 1, 5, 4, 4, 3, 4, 4])
.PartitionBy("", "", "key,group=>group.First()")
// $$でニ引数目も指定出来るようになった
Enumerable.From([1, 1, 5, 4, 4, 3, 4, 4])
.PartitionBy("", "", "$$.First()")
便利といえば便利ですが、あまりやりすぎると見た目がヤバくなるので適度に抑えながらでどうぞ。なお、以前からある機能ですが""は"x=>x"の省略形です。PartitionByでは、それぞれkeySelectorとelementSelector。
入門QUnit
今までlinq.jsのユニットテストはJSUnitを使用していたんですが、相当使いにくくてやってられなかったため、QUnitに移しました。QUnitはjQueryの作者、John Resigの作成したテストフレームワークで、流石としか言いようがない出来です。物凄く書きやすい。JSUnitだとテストが書きづらくて、だから苦痛でしかなかった。テストドリブンとか言うなら、まずはテストが書きやすい環境じゃないとダメだ。
JSUnitのダメな点―― 導入が非常に面倒。大量のファイルを抱えたテスト実行環境が必要だし、クエリストリングでファイル名を渡さなければならなかったり、しかも素ではFirefox3で動かなかったりと(Firefox側のオプションを調整)下準備が大変。面倒くささには面倒くささなりのメリット(Java系の開発環境との連携とかあるらしいけど知らない)があるようですが、俺はただテスト書いて実行したいだけなんだよ!というには些か重たすぎる。一方、QUnitはCSSとJSとHTMLだけで済む。
また、JSUnitはアサーションのメソッドが微妙。大量にあるんだけど、逆に何が何だか分からない。assertObjectEqualsとかassertArrayEqualsとか。ArrayEqualsはオブジェクトの配列を値比較してくれない上に、それならせめて失敗してくれればいいものの成功として出されるから役に立たなかったり、ね……。QUnitは基本、equal(参照比較)とdeepEqual(値比較)だけという分かりやすさ。deepEqualはしっかりオブジェクト/配列をバラして再帰的に比較してくれるという信頼感があります。
テスト結果画面の分かりやすさもQUnitに軍配が上がる。というかJSUnitは致命的に分かりづらい。一つのテスト関数の中に複数のアサートを入れると、どれが失敗したか分からないという有様。なのでJSUnitではtestHoge1, testHoge2といった形にせざるを得ないのだけど、大変面倒。更に、JSUnitのテスト実行は遅くて数百件あるとイライラする。
そもそもJSUnitはコードベースが古いし最近更新されてるかも微妙(GitHubに移って開発は進んでるようですが)。というわけで今からJavaScriptでユニットテストやるならQUnitがいいよ!残念ながらか、ネットを見ると古い紹介記事ばかりが見当たるので、ていうかオフィシャルのドキュメントまで古かったりしてアレゲなので、簡単に解説します。と、思ってたのですが、一月程前に素晴らしいQUnitの記事が出ていました。なので基本無用なのですが、文章を書いてしまってあったので(これ書いてたのは4月頃なのです)出します、とほほ。
まず、QUnit - jQuery JavaScript LibraryのUsing QUnitのところにあるqunit.jsとqunit.cssを落とし、下記のテンプレHTMLは自前で作る。ファイル名はなんでもいいんですが、私はtestrunner.htmとでもしておきました。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>linq.js test</title>
<link href="qunit.css" rel="stylesheet" type="text/css" />
<script src="qunit.js" type="text/javascript"></script>
<!-- テストに必要な外部ライブラリは好きに読み込む -->
<script src="linq.js" type="text/javascript"></script>
<!-- ここにテスト直書きもアリだし -->
<script type="text/javascript">
test("Range", function()
{
deepEqual(Enumerable.Range(1, 3).ToArray(), [1, 2, 3]);
});
// 自動的にロード後に実行されるので、これも問題なく動く
test("hoge", function ()
{
var h2 = document.getElementsByTagName("h2");
equal(h2.length, 2);
});
</script>
<!-- 外部jsファイルにして読み込むのもアリ -->
<script src="testEnumerable.js" type="text/javascript"></script>
<script src="testProjection.js" type="text/javascript"></script>
</head>
<body>
<h1 id="qunit-header">linq.js test</h1>
<h2 id="qunit-banner"></h2>
<h2 id="qunit-userAgent"></h2>
<ol id="qunit-tests"></ol>
</body>
</html>
実行用のHTMLにqunit.jsとqunit.cssを読み込み、body以下の4行を記述すれば準備は完了(bodyの4行はid決め打ちで面倒だし、どうせ空なので、qunit.js側で動的に生成してくれてもいいような気がする、というか昔はそうだった気がするけどjQuery依存をなくした際になくしたのかしらん)。
あとは、test("テスト名", 実行される関数) を書いていけばいいだけ。そうそう、test関数はHTMLが全てロードされてから実行が始まるので、jQuery読み込んでjQuery.readyで囲む必要とかは特にありません。testProjection.jsは大体↓のような感じ。
/// <reference path="testrunner.htm"/>
module("Projection");
test("Select", function ()
{
actual = Enumerable.Range(1, 10).Select("i=>i*10").ToArray();
deepEqual(actual, [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]);
actual = Enumerable.Range(1, 10).Select("i,index=>i*10+index").ToArray();
deepEqual(actual,[10, 21, 32, 43, 54, 65, 76, 87, 98, 109]);
});
reference pathはVisualStudio用のパスなのであんま気にせずにー。詳細はJavaScriptエディタとしてのVisual Studioの使い方入門のほうで。読み込み元のHTMLを指定しておくとIntelliSenseが効いて、書くのが楽になります。
actual(実行結果)は私は別変数で受けてますが、当然、直書きでも構いません。アサーション関数は、基本は(actual, expected(期待する結果), message(省略可能)) の順番になっています。
equal(1, "1"); // okay - 参照比較(==)
notEqual
strictEqual(1, "1"); // failed - 厳密な比較(===)
notStrictEqual
deepEqual([1], [1]); // okay - 値比較
notDeepEqual
ok(1 == "1"); // okay - boolean
ok(1 !== "1"); // okay - notの場合は!で
基本的に使う関数はこれらだけです。ドキュメントへの記載はないのですが、以前にあったequalsとsameはequalとdeepEqualに置き換わっています。後方互換性のためにequals/sameは残っていますが、notと対称が取れるという点で、equal/deepEqualを使ったほうが良いんじゃないかと思います。
非同期テストとかは、またそのうちに。
まとめ
linq.js ver.2出したときには、もう当分更新することなんてないよなあ、なんて思っていたのですが、普通にポコポコと見つかったり。でもさすがに、もうないと思いたい。C#との挙動互換性も、私の知る限りでは今回の配列最適化が最後で、やり残しはない。そして今回がラストだー、とばかりに思いつく要素を全部突っ込んでやりました。
そんなわけなので、使ってやってください。私がVisualStudio使いなのでVS関連の補助が多めですが、別にVS必須というわけじゃなくプラスアルファ的なもの(入力補完ドキュメントだのコードスニペットだの)でしかないので、エディタ書きでも何ら問題なく使える、かな、きっと。
Reactive Extensions for .NET (Rx) メソッド探訪第7回:IEnumerable vs IObservable
- 2010-06-24
物凄く期間を開けてしまいましたが、Reactive Extensions for .NET (Rx)紹介を再開していきます。もはやRxってなんだっけ?という感じなので、今回は最も基本である、IObservableについて扱います。ボケーッとしている間にIQbservable(IQueryableのデュアル)とか出てきてて置いてかれちゃってるし。
そんなこんなで、IObservableはIEnumerableのデュアルなんだよ、とか言われてもぶっちゃけさっぱり分かりません。なので、その辺のことはスルーして普通にコードで対比させながら見ていくことにします。
// IEnumerable (RunはForEachです、ようするに)
Enumerable.Range(1, 10)
.Where(i => i % 2 == 0)
.Select(i => i * 2)
.Run(Console.WriteLine, () => Console.WriteLine("completed!"));
// IObservable
Observable.Range(1, 10)
.Where(i => i % 2 == 0)
.Select(i => i * 2)
.Subscribe(Console.WriteLine, () => Console.WriteLine("completed!"));
ボタンを押して確認する、までもなく同じ結果です。1から10までを偶数だけ通して二倍して出力。見た目は同じですが、中身は丸っきり違います。見た目が一緒すぎて言葉で表現出来ないので図に表してみました。
何という下手っぴな図、さっぱり伝わらん。……。というのはおいておいて、矢印の向きに注目。IEnumerableの連鎖は、列挙を消費する時にIEnumeratorの伝搬に変わります。Run->Select->Where->RangeとMoveNextが駆け上がったら、今度はRange->Where->Select->RunとCurrentが降りていきます。末尾(Run)が値を要求(MoveNext)して値(Current)を取り出すという連鎖。末端から根元の値を引っ張ってくる(Pull)ようなイメージ。
IObservableは、根元自体が値を押し出していく(Push)ようなイメージ。こちらはIObserverの連鎖になっていて、根元からOnNextで値を伝えていきます。
Pushのメリット
Observable.Rangeのような、もしくはEunmerableに対してToObservableした時のような、普通のPull型シーケンスをPush型に変換することのメリットは?イベントや非同期など、他の形式から生成されたIObservableと連携出来る、というのは当然一番の話ですが、もう一つ、要素を分配出来るようになります。
このイミフな図の言わんとしていることが伝わる、わけはないので説明。Pull型はソースと1対1の関係である必要があるため、複数の列挙の消費者(RunだったりCountだったりSumだったりLastだったり)がいる場合、接続した回数だけ列挙が最初から回ることになります。かたやPush型は、1対多の関係を持つことが出来るため、一度の列挙で全ての消費者に値を配分することが可能です。
Hot vs Cold
同じように見えるIObservableにも、HotとColdという性質があります。それはyield returnで作る遅延評価のIEnumerableと、配列のように既に値が生成済みのIEnumerableとの違い、のようなものかもしれません。
var seq = Observable.Range(1, 5)
.Do(i => Console.WriteLine("source -> " + i));
button1.Click += (sender, e) =>
seq.Subscribe(i => Console.WriteLine("button1 -> " + i));
button2.Click += (sender, e) =>
seq.Subscribe(i => Console.WriteLine("button2 -> " + i));
Doは、列挙に通ったものを取り出しつつも素通しします。つまり、 Select(i => { action(i); return i; }) です。今回は列挙がその箇所を通ったかどうかを書き出しています。余談ですが、IEnumerableならNyaRuRuさんの作成されたAchiralにはHookというメソッドがあって、細かい列挙中のモニタリングが出来るようになっています。
実行結果を見てみると、ボタンを押す=Subsribeを繋げると、即座に列挙が開始されていて、これだとIEnumerableのforeachと何も変わません。よって、このIObservableはColdです。もう値は生成され終わっているので。Subscribeの度に即座に全ての値をPushします。
ではHotは?
// FromEvent(canvas,"MouseMove")は手軽ですが、丁寧にこう書くほうが理想的かしら
Func<IObservable<Point>> GetMouseMovePosition = () =>
Observable.FromEvent<MouseEventHandler, MouseEventArgs>(
h => (sender, e) => h(sender, e),
h => canvas.MouseMove += h,
h => canvas.MouseMove -= h)
.Select(e => e.EventArgs.GetPosition(canvas));
// ICollection<IDisposable>です。
var disposables = new CompositeDisposable();
evenButton.Click += (sender, e) =>
{
disposables.Add(
GetMouseMovePosition()
.Where(p => p.X % 2 == 0 && p.Y % 2 == 0)
.Subscribe(p => Console.WriteLine("Even -> " + p.X + ":" + p.Y)));
};
oddButton.Click += (sender, e) =>
{
disposables.Add(
GetMouseMovePosition()
.Where(p => p.X % 2 != 0 && p.Y % 2 != 0)
.Subscribe(p => Console.WriteLine("Odd -> " + p.X + ":" + p.Y)));
};
disposeButton.Click += (sender, e) =>
{
// Disposeでイベントのデタッチ + 再登録不可
// Clearでイベントのデタッチ + 再登録可
disposables.Clear();
};
例えばマウスイベント。クリックの度にOnNextに値を送る、ムーブの度に値を送るといったイベントをIObservable化するFromEventはHot。無限リスト状態になっているものは、接続しただけでは値が送られてこないとも言えるので、幾つでもSubscribeすることが出来ます。サンプルでは、ボタンをクリックすればしただけ、右側のログ表示に同内容のものが連続して表示されるのが確認出来ます。
両者が混ざったような挙動をするIObservableもあります(例えばReplaySubject)ので、HotなのかColdなのか両方なのか。というのを意識してみると理解が深まるかもしれません。また、メソッドの動作確認などの際にHotとColdを区別せずにいると、思わぬ挙動で混乱するかもしれないので注意。というか、私はよくやります……。Observable.Rangeばかりで確認していてイミフ!と思ったら、FromEventでチェックしたら何て分かりやすいこと!というのが何度も。
CompositeDisposable
本題と離れますがTips。イベントのデタッチが簡単なのもRxのメリットの一つです。さて、複数イベントをデタッチする場合はどうしましょうか?List<IDisposable>に格納してforeachで列挙してDispose、というのも悪くないですが、そういう用途で使うためのCompositeDisposableというICollection<IDisposable>なクラスが用意されているので、そちらを使ったほうがよりスマートに書けます。
上のSilverlightのHotのサンプルコードでは、ボタンを押す(=Subscribeする=イベントを登録する)度にCompositeDisposableにAdd。そしてDisposeAllボタンでまとめてデタッチしています。
var subject = new Subject<int>();
var d1 = subject.Subscribe(i => Console.WriteLine(i));
var d2 = subject.Subscribe(i => Console.WriteLine(i * i));
using (new CompositeDisposable(d1, d2))
{
subject.OnNext(2); // 2, 4
subject.OnNext(3); // 3, 9
}
subject.OnNext(2); // usingを抜けデタッチ済みなので何も起こらない
List<IDisposable>に対するCompositeDisposableのメリットは、Disposeで解除出来るということ。つまり、using構文に放りこむことが可能です。多段Usingよりも綺麗に見えるのでお薦め。
上の例にコソッと出したSubjectクラスはPush型シーケンスの大本で、OnNextやOnCompletedを後続に送ることが出来ます。イベントのラップじゃなく、Rxネイティブなクラスを作る場合に使います。Subjectはちゃんと詳しく書かなきゃいけない大事なクラスの一つなので、また次にでもきっちり紹介する予定は未定。
列挙の分配
Pushのメリットとして分配可能なことを挙げたのに、Coldなので分配出来ません。以上終了。で終わるわけは当然ないわけで、Cold to Hot変換メソッドが使えます。Publishです。Publishの戻り値はIConnectableObservable。
public interface IConnectableObservable<out T> : IObservable<T>
{
IDisposable Connect();
}
IObservableなのでメソッドチェインを繋げることが出来ます。そして、Subscribeしても列挙は始まりません。Connectを呼んだ時に、一度だけ列挙することが出来ます(二度以降Connectを呼んでも何もしない)
私はダムの堰止をイメージしています。何もしないとドバドバと水が流れてしまうのでPublishで一時的に止めて、Connectで放水。放水後は空っぽ。みたいな。
Max/Sumなど集計系
インターフェイスを挙げただけじゃよく分からないので実例を。SumやMaxといった集計系メソッドと合わせて使ってみます。そこら中にモニタリング用のDoが入っていてコードが若干分かりづらいですが、実行結果で、どのタイミングで値が通過するのかを確認してみてください。
var source = Enumerable.Range(1, 5)
.Do(i => Console.WriteLine("Source -> " + i));
enumerableButton.Click += (sender, e) =>
{
var sum = source.Sum();
var max = source.Max();
var all = source.All(i => i < 3);
Console.WriteLine("sum = " + sum);
Console.WriteLine("max = " + max);
Console.WriteLine("all = " + all);
};
observableButton.Click += (sender, e) =>
{
var connectable = source.ToObservable().Publish();
connectable.Subscribe(_ => { }, () => Console.WriteLine("OnCompleted"));
var sum = default(int);
connectable
.Do(i => Console.WriteLine("BeforeSum -> " + i))
.Sum()
.Do(i => Console.WriteLine("AfterSum -> " + i))
.Subscribe(i => sum = i);
var max = default(int);
connectable
.Do(i => Console.WriteLine("BeforeMax -> " + i))
.Max()
.Do(i => Console.WriteLine("AfterMax -> " + i))
.Subscribe(i => max = i);
var all = default(bool);
connectable
.Do(i => Console.WriteLine("BeforeAll -> " + i))
.All(i => i < 3)
.Do(b => Console.WriteLine("AfterAll -> " + b))
.Subscribe(b => all = b);
connectable.Connect();
Console.WriteLine("sum = " + sum);
Console.WriteLine("max = " + max);
Console.WriteLine("all = " + all);
};
値が確定した時、Allならば全ての列挙が完了した(OnCompletedを受信する)か、条件がfalseのものが見つかったときに、1つだけSubscribeに値が届きます。SumやMaxは、全ての列挙が完了しないと算出出来ないので、全て完了したとき。こういった結果の確定するタイミングは、Enumerableでの場合と変わりません。
このような動作(戻り値が長さ1のIObservable)をするものには、 Aggreagte, Count, Any... 、ようするにIEnumerableにもあって戻り値がIEnumerableじゃないメソッドは全てそうです。全部似たりよったりなので具体的な紹介は省きます。
Pushのデメリット
IObservable便利すぎてIEnumerableいらなくネ? と、言いたいところですが、例えばこれら集計系メソッドは全て長さ1のIObservableになります。Sumの場合、欲しいのはintであってIObservableではありません。長さ1のIObservableは、いつConnectされるか分からないのでSubscribeで外の値に受け渡してやらなければならないわけですが、見た目が美しくなく宣言も冗長になる。
また、集計するのに複数回列挙は確かに格好悪いな!よし、そういう場合はRx使おう。と思った場合はとりあえず待った。ただの配列からの列挙程度の場合は、ふつーに複数回列挙したほうがPublishで分岐させるよりも遥かに速かったりします。元ソースが複雑にLinqで繋いであって重たかったり、ファイルやネットワーク経由だったりで複数呼び出しを避けたい、副作用があって複数回呼びだすと内容が変化している、という場合はRxです。が、一旦ToListしてキャッシュすれば済むシーンならば、キャッシュした方が分かりやすく速い場合が多かったりします。
Publishの具体的な使い処としては、以前に、TwitterのStreamAPIをRxを使って分配するという記事で紹介しました。
まとめ
PullとPushは、むしろ動作的にはPushのほうが素直で分かりやすい雰囲気。難解だと思って避けていたそこのアナタ、さあ、Rxを使おう! しかしColdとHotは大いなる罠。初見ではきっとつまづく。この区別は本当に大事。Rxが難解っぽいとしたら、Cold/Hotのせい。挙動がまるっと変わるんだもの。でも、ゆっくり紐解けば全然大丈夫。さあ、Rxを使おう!Publishや集計系はそんなには使わないかもですが、覚えておくと便利な時も割とある。さあ、Rxを(ry
個人的にRxの特色・使いどころは「イベントの合成」「タイマー・ネットワーク・スレッドなど非同期処理の一元化」「シーケンスの分配」の3つだと思っているのですが、このブログでは、延々とシーケンス分配という、3つの中で一番どうでもいい機能しか紹介していない!という酷い事実に気がつきました。そんなんじゃRxのポテンシャルを全然伝えられない。
というわけで、次回はタイマー辺りを紹介したいと思います予定は未定。というか計画ではObservableの合流周りとMarble Diagramについてを書く予定。Rxの知名度も徐々に上がってきているようなので、しっかり紹介していきたいですし、他の人も書いて欲すぃ。
Linq雑話
- 2010-06-08
ここ数日Twitterで見た/出したLinqネタまとめ。私の広くない観測範囲(@neuecc)での話ですが。
SelectManyとクエリ構文でUsing
ネタ元、コード元はCode, code and more code.: SelectMany; combining IDisposable and LINQから。
static void Main(string[] args)
{
var firstLines =
from path in new[] { "foo.txt", "bar.txt" }
from stream in File.OpenRead(path)
from reader in new StreamReader(stream)
select path + "\t" + reader.ReadLine();
}
public static IEnumerable<TResult> SelectMany<TSource, TDisposable, TResult>(
this IEnumerable<TSource> source,
Func<TSource, TDisposable> disposableSelector,
Func<TSource, TDisposable, TResult> resultSelector) where TDisposable : IDisposable
{
foreach (var item in source)
{
using (var disposableItem = disposableSelector(item))
yield return resultSelector(item, disposableItem);
}
}
自前定義の拡張メソッドはメソッド構文だけのものと思っていませんでしたか?私はそう思っていました。でも、クエリ構文でも同名のものがあれば拡張メソッドが使用されるんです、というお話。それを利用してusingのネストをクエリ構文で華麗に表現してやったぜー、というサンプルで、確かにこれはクール!素晴らしすぎる。
でも、クエリ構文使いたいかというと、そんなことはなく変わらずメソッド構文派です、私は。クエリ構文自体は悪いとは思わないし、良さがあるのも分かるんですが、他の拡張メソッドに繋げる時に前後にカッコで括ると途端に書き/読みにくくなることと、拡張性の乏しさが如何ともし難い。クエリ構文とメソッド構文のちゃんぽんになるぐらいなら、メソッド構文だけで書いたほうが美しいよね、と思ってしまう。あと、クエリ構文の存在が「LINQ = SQLみたいなの」という図式を産んでしまっているくさいのも、憎んでしまいますね……。
ラムダ式の引数の名前とシャッフルについて
お馴染み感溢れるOrderByでのシャッフル。
var rand = new Random();
var shuffle = Enumerable.Range(1, 10).OrderBy(_ => rand.Next());
それはそれとして、ラムダ式の引数の名前どうする?というお話が。私は、引数を使わない場合は _ を、使う場合は型の1~2文字(i(Int32)とかs(String)とかa(AnonymousType)とか、考えるの面倒なときはx、配列系はarかxs)という自分ルールを敷いています。以前にラムダ式の引数の名前という記事を書いたのですが、その時から変わっていません。ですが、最近ネットで見かけるコードでは全部_でまかなう例もよく見るね、と。Scalaでは匿名関数の引数として_が使える(プレースホルダ構文って言うんですね、名前知らなかった)ようなので、_をダメとは言い辛いのですけど、私はちょち苦手(linq.jsで$をゴリゴリ使ってるくせに、って話ではあるけど) 。
C#にもプレースホルダ構文みたいなの欲しいね、というのは、若干ある。プロパティの「value」とか最初からそこにある良く分からない変数、みたいなのはあるし。ただ、IntelliSenseとの兼ね合いもあるし、そういうのが入れられるか、入って本当に幸せになれるのかどうかの判断は保留。短絡的に欲しい!って言うのは簡単だけど、それの及ぼす影響となると分からないものだ。
それともう一つ。OrderByの引数は比較関数ではなくキーセレクターにすぎないのでちゃんとシャッフルされる、とか言ったりなどした私ですが、そうじゃなくてシャッフルの精度はランダムの範囲に影響されるね(実際上は問題ないとしても)、という話が。完全に頭から抜け落ちていて、かつ、全くもってその通りで恥ずかしかったりしたのですが確認できてよかったです、感謝。
OrderByのComparison
全然使わないけどOrderByの第二引数。
class MyClass
{
public int Hoge { get; set; }
public int Fuga { get; set; }
}
static void Main(string[] args)
{
var array = new[] { new MyClass(), new MyClass() };
// コンパイルは通るけど例外出る
var ordered = array.OrderBy(x => x).ToArray();
// 上のはこれに等しい(当然、例外出る)
array.OrderBy(x => x, Comparer<MyClass>.Default);
// AnonymousComparerを使えばComparisonを使った比較が出来る
array.OrderBy(x => x, (x, y) => x.Fuga - y.Hoge);
}
OrderByついでですが、キーセレクターは制約かかってないので別にIComparableじゃなくても動いたりします。そういう時はComparer<T>.Defaultが指定されることになって、例外出て死ぬだけです。意味ナイネ。
DescendingとThenByがあるので滅多に使わないであろう第二引数はIComparer。一々クラス作ってnewですってよ、C#らしくないですね。Comparisonじゃないなんて!大変ウザい。そんな人のためのAnonymousComparer。ラムダ式でIEqualityComparer/IComparerを作ることが出来ます。また、Linq標準演算子への拡張メソッドとしてOrderBy/ThenByのオーバーロードとしてComparisonが使えるようになります。便利ですね!是非使ってください、という宣伝。
Empty -> Sum
Empty.Sum()は0。言われてみれば当たり前といえば当たり前なのですが……。
// SumはAggregateで表現出来る
var sum = Enumerable.Range(1, 10).Aggregate((x, y) => x + y); // 55
// でもEmptyで例外出るから表現出来ない(キリッ
sum = Enumerable.Empty<int>().Sum(); // 0
sum = Enumerable.Empty<int>().Aggregate((x, y) => x + y); // 例外
// 実はseed与えればおk
sum = Enumerable.Empty<int>().Aggregate(0, (x, y) => x + y); // 0
SumやMax, Minなどは全てAggregateで表現出来ます。でも、Sumは空シーケンスの時はゼロ出すけど(MaxやAverageは例外)Aggregateを使うと例外が出てしまうので表現出来ない、とか言ったのですが0を最初に与えとけばいいよね、という話が。ぬお、そうでした!
発端はlinq.jsでこの問題(というかC#と互換が取れてないこと)に気づいたことで、linq.jsではAggregateでやってるため、AggregateはScan.LastだからScan.LastOrDefault(0)にするー、なんて考えてたんですが、初項0で済むというシンプルさを完全に失念。標準演算子外のメソッドを大量に用意してあるので、そっち側で解決しちゃおうとしてしまう姿勢は、ちょっと頭硬直化しちゃってる、全くもってよろしくない。
シャッフルの話といい、Aggregateの話といい、最近はLinqに慣れすぎて逆に見方が定型的になりすぎていると実感したので、少し気を引き締めないと。あ、で、そんなこんなでlinq.jsの空シーケンスでのSumの問題は次のリリースで直します。他にもバグがあったり(MemoizeAllが少しマズい)、加えたいことが数点あったりするので、もう少し先になりますが。
世の中の主流はまだVS2005ですか?
開発言語としてのJavaとC#を10の視点から比較
共通点が多いが、今後は違いが大きくなるかも
しかし近年のC#はLINQ(Language Integrated Query:言語統合クエリ)プロジェクトが重視されています。これはクエリ、集合操作、変換、および型推測などのデータ指向機能の多くを直接的にC#言語に統合しようとするものです。今後は違いがさらに大きくなっていくかもしれません。
プログラマが知っておきたいJavaと.NETの違い (3/4) - @IT
Linqは、VS2008出たのは3年前だよね(プレビュー版から言えばどれだけ前なのかしら)。今後は違いが大きくなるかも、じゃなくて既に違いは大きすぎるような。そして10の比較というけれど、最大の違いはデリゲートの有無では?特に、匿名メソッド/ラムダ式の有無。A.R.N [ Top > 書庫 > Microsoftの「Delegate」について ]にある、Javaには無名クラスがあるからdelegateは不要、とは10年以上前のSunの言で、さすがに10年以上も前のを持ち出してどうこう言ってもしょうがないのですが(比較対象に匿名メソッドないし)、価値観は移り変わっていくものなのだと思わずにはいられない。匿名クラスで代用出来るって、いやまあ出来なくもないのは分かりますがUglyすぎ。今、クロージャなんて不要、とか言ったらフルボッコなはず。
言語面で見ると、Java5から進化の足を止めている(そしてJava7延期しすぎ)ように見えるJavaと、ひたすら貪欲に(無節操に)取り込み続けるC#。スタート時には似たようなものだったとして、今はもうコードの見た目からして全然似てるようには見えない。Java畑の人は、今でもC#はJavaに似たようなもの、という認識なのかしら。
確かに、古典的に書けば似てますが……。そして、他の言語を考えれば、やっぱ似てるといえば似てるのですが。しかし……。ふむ。そろそろModern C# Designが出版されるべき。 Bart De Smetが書くC# 4.0 Unleashed
には超期待。
.NET(C#)におけるシリアライザのパフォーマンス比較
- 2010-05-29
ちょっとしたログ解析(細々としたのを結合して全部で10万件ぐらい)に書き捨てコンソールアプリケーションを使って行っていたのですが(データ解析はC#でLinqでコリっと書くのが楽だと思うんです、出力するまでもなく色々な条件を書いておいてデバッガで確認とか出来るし)、実行の度に毎回読んでパースして整形して、などの初期化に時間がかかってどうにも宜しくない。そこで、データ丸ごとシリアライズしてしまえばいいんじゃね?と思い至り、とりあえずそれならバイナリが速いだろうとBinaryFormatterを使ってみたら異常に時間がかかってあらあら……。
というしょうもない用途から始まっているので状況としては非現実的な感じではありますが、標準/非標準問わず.NET上で実装されている各シリアライザで、割と巨大なオブジェクトをシリアライズ/デシリアライズした時間を計測しました。そんなヘンテコな状況のパフォーマンスなんてどうでもいー、という人は、各シリアライザの基本的な使いかたの参考にでもしてください(いやまあ、どれもnewしてSerializeメソッド呼ぶだけですが)。ソースは後で出しますが、具体的に計測に使ったオブジェクトはテスト用クラスが10万件含まれたListです。まずは結果のほうを。
Serialize BinaryFormatter
00:00:06.4701421
53MB
Serialize XmlSerializer
00:00:07.7035246
59MB
Serialize DataContractSerializer
00:00:02.1545153
149MB
Serialize DataContractSerializer Binary
00:00:01.4706517
78MB
Serialize DataContractJsonSerializer
00:00:02.6021908
47MB
Serialize DataContractJsonSerializer Binary
00:00:02.5019512
75MB
Serialize NetDataContractSerializer
00:00:09.1802584
183MB
Serialize NetDataContractSerializer Binary
00:00:08.1960399
99MB
Serialize Formatter`1 - Protocol Buffers
00:00:00.7043000
13MB
Serialize MsgPackFormatter - MessagePack
00:01:33.3844083
46MB
Serialize JsonSerializer - JSON.NET
00:00:07.4997295
38MB
Serialize JsonSerializer - JSON.NET BSON
00:00:11.7767353
44MB
Deserialize BinaryFormatter
00:00:59.1693980
Check => OK
Deserialize XmlSerializer
00:00:02.7073623
Check => NG
Deserialize DataContractSerializer
00:00:06.3459340
Check => OK
Deserialize DataContractSerializer Binary
00:00:03.5622500
Check => OK
Deserialize DataContractJsonSerializer
00:00:10.5392504
Check => OK
Deserialize DataContractJsonSerializer Binary
00:00:07.2658857
Check => OK
Deserialize NetDataContractSerializer
00:00:09.1020073
Check => OK
Deserialize NetDataContractSerializer Binary
00:00:07.4024345
Check => OK
Deserialize Formatter`1 - Protocol Buffers
00:00:00.9176016
Check => OK
Deserialize MsgPackFormatter - MessagePack
00:00:29.8292134
Check => OK
Deserialize JsonSerializer - JSON.NET
00:00:11.7517757
Check => OK
Deserialize JsonSerializer - JSON.NET BSON
00:00:12.0099519
Check => OK
対象シリアライザ、かかった時間、シリアライズ時は出力ファイルサイズ、デシリアライズ時は正しく復元できたかを表示しています。
.NET標準ライブラリからはBinaryFormatter, XmlSerializer, DataContractSerializerとそのバイナリ出力, DataContractJsonSerializerとそのバイナリ出力, NetDataContractSerializerとそのバイナリ出力。オープンソースライブラリからは、GoogleのProtocol Buffersの.NET移植protobuf-net、国産のMessagePackのC#実装、Json.NETのJSONシリアライズとBSON(Mong DBで使われているバイナリ形式のJSON)のシリアライズを出力しました。
BinaryFormatter遅くね?が発端であり裏付けるように、BinaryFormatterのデシリアライズが異常に遅い。他が数秒なのに1分かかってます。テスト用データの詳細は後で述べますが、どうもObjectの配列が含まれていると遅くなるようです。テストデータにはKeyValuePair[]を含んでいるので、それが引っかかって激遅に。シリアライザする対象によって速度が変化するのは分かりますが、幾らなんでも限度を超えた速度低下。バグじゃないかしら?と言いたい。DataContractSerializerでもバイナリが吐ける昨今、もはやObsoleteにしてもいい雰囲気すら漂う。
XmlSerializerが速い・サイズ少ないという感じですがデシリアライズでCheck => NGと表記されているように、シリアライズに失敗しています。テストデータにKeyValuePair[]が含まれているのですが、それがシリアライズ出来なくて空配列になってしまったため。Dictionaryがシリアライズ出来なかったりと、XmlSerializerは割と使いにくいところがあります。今だとガイドライン的にも、XmlのAttributeの設定とか出力するXMLを細かく制御するならXmlSerializer、そうでないならDataContractSerializerのほうを推奨、とのことです。
そのDataContractSerializerはファイルサイズが嵩んでいるのが難点。属性などを付与していないため、テストデータ中の自動プロパティが <_x003C_MyProperty1_x003E_k__BackingField>hoge1</_x003C_MyProperty1_x003E_k__BackingField> といったような、とんでもなく長ったらしいタグになってしまっているのが原因。DataMember属性で名前を振ってあげればマシになりますが、今回は属性未使用で計測としました。そんなDataContractSerializerですが、バイナリXMLとして保存するとファイルサイズが縮むので気になる場合はそちらを使えばいいのかも。パフォーマンスも良くなります。
DataContractJsonSerializerは、シリアライズ・デシリアライズの速度はXMLよりも劣っていますが、ファイルサイズに関しては(JSONなので当然とはいえ)比較にならないほど小さい。バイナリXMLよりも小さくて、中々優秀のようです。しかし、バイナリ化して保存すると逆にファイルサイズが膨らむという罠が。
NetDataContractSerializerはシリアライザ作る時に型指定がいらなかったりと利便性はちょびっと○。また、循環参照が含まれていても問題なくシリアライズ出来たりと、DataContractSerializer(素の状態だと循環参照が含まれると例外)とは若干毛色が違います。といった点から言っても、BinaryFormatterの後継はこれになるのでしょう。
Protocol Buffersは爆速な上に非常に縮んで素晴らしい!さすがGoogle。ということだけじゃなく、protobuf-netの実装も良いのでしょうね。APIも非常に練られていて使いやすいし、C#でのクラスから.protoファイルの生成とかも出来て大変便利。Silverlightで使えるし、Windows Phone 7でも動かせるよう調整中、とのことでかなり気に入りました。
MsgPackFormatterはシリアライズ、デシリアライズ共にかなり時間が……。これは、MessagePackが、というよりも、実装の方でオブジェクトグラフの生成に時間がかかってるようです。
JSON.NETは中々優秀な結果を出しています。しかし、Performance ComparisonでDataContractSerializerよりも速いぜ!と謳っていたけれど、そんなことはなく。こういうのは計測する内容によって変わってくるので一概にどうこう言えないんですねー、というのを知るなど。バイナリ化で逆にサイズが膨らむのはDataContractJsonSerializerと同様。なお、JSON.NETはAPIが正直使いにくくてどうにかならないのかねー、と思うので個人的には好きじゃありません。使いやすいAPI設計ってセンスが必要ですよね……。
コード
割と長ったらしいので、分割して。追試したい方はこちらに元ソース置いておくので使ってみてください。VS2010用。ライブラリ全部揃えるのも大変だと思うので、測定を省きたい項目はMainメソッドのリスト初期化子の部分で該当するものをコメントアウトすれば省けます。
[ProtoContract]
[Serializable]
public class TestClass : IEquatable<TestClass>
{
[ProtoMember(1)]
public string MyProperty1 { get; set; }
[ProtoMember(2)]
public int MyProperty2 { get; set; }
[ProtoMember(3)]
public DateTime MyProperty3 { get; set; }
[ProtoMember(4)]
public bool MyProperty4 { get; set; }
[ProtoMember(5)]
public KeyValuePair<string, string>[] MyProperty5 { get; set; }
public bool Equals(TestClass other)
{
return this.MyProperty1 == other.MyProperty1
&& this.MyProperty2 == other.MyProperty2
&& this.MyProperty3 == other.MyProperty3
&& this.MyProperty4 == other.MyProperty4
&& this.MyProperty5.SequenceEqual(other.MyProperty5);
}
public override int GetHashCode()
{
return this.MyProperty1.GetHashCode() + this.MyProperty2.GetHashCode();
}
}
これがテスト用クラスの中身です。string, int, DateTime, bool, KeyValuePair<string,string>[] とそこそこ満遍なく散りばめて、それとデシリアライズがちゃんと出来たか確認出来るようIEquatable<TestClass>を実装して値比較出来るようにしています。&&や||や三項演算子の:は前置にする派。綺麗に揃う感じが好き。属性はProtocol Buffersはつけないと動かないのでProtoMemberを指定していますが、それ以外は無指定です。指定した方が縮んだりとかありそうですが、私的には無指定の状態で調べたいなあ、と思ったのでなし。最適じゃない状態からのそれぞれのシリアライズ形式への生成、書き戻しの具合を見たいと思ったので。いや、単純に各実装で使う属性を調べるのが面倒だったという手抜きな理由もありますが。
public abstract class SerializerBenchmark
{
public static readonly List<TestClass> TestData;
static SerializerBenchmark()
{
TestData = Enumerable.Range(1, 100000)
.Select(i => new TestClass
{
MyProperty1 = "hoge" + i,
MyProperty2 = i,
MyProperty3 = new DateTime(1999, 12, 11).AddDays(i),
MyProperty4 = i % 2 == 0,
MyProperty5 = Enumerable.Range(1, 10)
.ToDictionary(x => x.ToString(), _ => i.ToString()).ToArray()
})
.ToList();
}
public static SerializerBenchmark<T> Create<T>(T serializer, Func<T, Action<Stream, Object>> serializeSelector, Func<T, Func<Stream, Object>> deserializeSelector, string optional = null)
{
return new SerializerBenchmark<T>(serializer, serializeSelector, deserializeSelector, optional);
}
public abstract void Serialize();
public abstract void Deserialize();
}
ベンチマークで重複コードのコピペ(ストップウォッチを前後に挟んだり)を省くために抽象クラスを作りました。このList<TestClass> TestDataが実際にシリアライズ/デシリアライズに使ったデータになります。Enumerable.Range(1, 100000)のToListで10万件のリスト生成。KeyValuePairの配列は、全て長さ10で、DictionaryのToArrayで作っています。
public class SerializerBenchmark<T> : SerializerBenchmark
{
private T serializer;
private string name;
Action<Stream, Object> serialize;
Func<Stream, Object> deserialize;
private string FileName { get { return name + ".temp"; } }
public SerializerBenchmark(T serializer, Func<T, Action<Stream, Object>> serializeSelector, Func<T, Func<Stream, Object>> deserializeSelector, string optional = null)
{
this.serializer = serializer;
this.name = serializer.GetType().Name + ((optional == null) ? "" : " " + optional);
this.serialize = serializeSelector(serializer);
this.deserialize = deserializeSelector(serializer);
}
private void Bench(string label, Action action)
{
GC.Collect();
Console.WriteLine(label + " " + name);
var sw = Stopwatch.StartNew();
action();
Console.WriteLine(sw.Elapsed);
}
private void OpenAndExecute(string path, Action<FileStream> action)
{
using (var fs = File.Open(path, FileMode.OpenOrCreate))
{
action(fs);
}
}
public override void Serialize()
{
Bench("Serialize", () => OpenAndExecute(FileName, fs => serialize(fs, TestData)));
Console.WriteLine(new FileInfo(FileName).Length / 1024 / 1024 + "MB");
}
public override void Deserialize()
{
List<TestClass> data = null;
Bench("Deserialize", () => OpenAndExecute(FileName, fs => data = (List<TestClass>)deserialize(fs)));
Console.Write("Check => ");
Console.WriteLine(TestData.SequenceEqual(data) ? "OK" : "NG");
}
}
こちらがベンチマークの中身。処理を共通化したかったのですが、各シリアライザがIFormatter(Serialize,Deserializeメソッドを持つインターフェイス)を実装している、なんてことは全くなくて共通化しようがなくてうぎゃー。せめてメソッド名が全てSerializeならdynamic使うという最終手段もあるけれど、DataContractSerializerのメソッド名はWriteObjectだし、ダメだこりゃ。
で、気づいたのがSerializeはAction<Stream, Object>、DeserializeはFunc<Stream, Object>だということ。というわけで、各メソッドそのものをコンストラクタで渡してあげる形にすることで共通化できた。めでたしめでたし。もはやFuncやActionのない世界は考えられませんね!え、Javaのことですか知りません。
Deserialize時には、正しくデシリアライズ出来たかのチェックを仕込んでいます。データがListなのでLinqのSequenceEqualで元データと値比較。Listの要素であるTestClassもIEquatableなので、全部値比較で確認。
static class Program
{
static void Main(string[] args)
{
var bench = new List<SerializerBenchmark>
{
SerializerBenchmark.Create(new BinaryFormatter(), x => x.Serialize, x => x.Deserialize),
SerializerBenchmark.Create(new XmlSerializer(typeof(List<TestClass>)), x => x.Serialize, x => x.Deserialize),
SerializerBenchmark.Create(new DataContractSerializer(typeof(List<TestClass>)), x => x.WriteObject, x => x.ReadObject),
SerializerBenchmark.Create(new DataContractSerializer(typeof(List<TestClass>)),
x => (s, data) => XmlDictionaryWriter.CreateBinaryWriter(s).Using(xw => x.WriteObject(xw, data)),
x => s => XmlDictionaryReader.CreateBinaryReader(s,XmlDictionaryReaderQuotas.Max).Using(xr => x.ReadObject(xr)),
"Binary"),
SerializerBenchmark.Create(new DataContractJsonSerializer(typeof(List<TestClass>)), x => x.WriteObject, x => x.ReadObject),
SerializerBenchmark.Create(new DataContractJsonSerializer(typeof(List<TestClass>)),
x => (s, data) => XmlDictionaryWriter.CreateBinaryWriter(s).Using(xw => x.WriteObject(xw, data)),
x => s => XmlDictionaryReader.CreateBinaryReader(s,XmlDictionaryReaderQuotas.Max).Using(xr => x.ReadObject(xr)),
"Binary"),
SerializerBenchmark.Create(new NetDataContractSerializer(), x => x.Serialize, x => x.Deserialize),
SerializerBenchmark.Create(new NetDataContractSerializer(),
x => (s, data) => XmlDictionaryWriter.CreateBinaryWriter(s).Using(xw => x.WriteObject(xw, data)),
x => s => XmlDictionaryReader.CreateBinaryReader(s,XmlDictionaryReaderQuotas.Max).Using(xr => x.ReadObject(xr)),
"Binary"),
SerializerBenchmark.Create(ProtoBuf.Serializer.CreateFormatter<List<TestClass>>(), x => x.Serialize, x => x.Deserialize, "- Protocol Buffers"),
SerializerBenchmark.Create(new MsgPackFormatter(), x => x.Serialize, x => x.Deserialize, "- MessagePack"),
SerializerBenchmark.Create(new JsonSerializer(),
x => (s, data) => new StreamWriter(s).Using(sw => new JsonTextWriter(sw).Using(tw=> x.Serialize(tw, data))),
x => s => new StreamReader(s).Using(sr => new JsonTextReader(sr).Using(tr=> x.Deserialize<List<TestClass>>(tr))),
"- JSON.NET"),
SerializerBenchmark.Create(new JsonSerializer(),
x => (s, data) => new BsonWriter(s).Using(bw => x.Serialize(bw, data)),
x => s => new BsonReader(s){ReadRootValueAsArray = true}.Using(br => x.Deserialize<List<TestClass>>(br)),
"- JSON.NET BSON")
};
bench.ForEach(b => b.Serialize());
Console.WriteLine();
bench.ForEach(b => b.Deserialize());
Console.ReadKey();
}
// IDisposable extensions
static void Using<T>(this T disposable, Action<T> action) where T : IDisposable
{
using (disposable) action(disposable);
}
static TR Using<T, TR>(this T disposable, Func<T, TR> func) where T : IDisposable
{
using (disposable) return func(disposable);
}
}
Listに突っ込んで、まとめてForEachで計測。でも何だかゴチャゴチャしててキタナイですね……。Listの最初の3つまでは良いんです。それぞれ一行で。SerializeとDeserialize登録するだけで。その次から想定外でして……。そう、Serializeは決して必ずしもAction<Stream, Object>じゃあなかった!例えばDataContractSerializerでバイナリXML化を行うには、WriteObjectにStreamじゃなくてXmlDictionaryWriterを渡さなきゃいけない。と、いうわけで、そういった特別対応が必要なメソッドには入れ子のラムダ式を作って処理しています。そのせいでゴチャゴチャと。もはや可読性の欠片もない。がくり。入れ子のラムダが出てくると読むのシンドイんですよね、可能な限り避けたいとは思ってるんですが……。
Using拡張メソッドは、using構文使うと式じゃなくて文になってラムダ式で書きづらくてウザいので無理やり式にするためのシロモノ。そのせいで入れ子が更に入れ子になって可読性落としてる気がする。何事もやりすぎはいけない。
まとめ
DataContractSerializer良いよね。XmlDictionaryWriter.CreateBinaryWriter突っ込めばバイナリ化も出来るし。あとprotobuf-netはこうして数字見ると本当に凄いね。と、いったことことぐらいしか言うことがなく。
それとは関係あったりなかったりで、Twitter素晴らしい。毎回思うけれど、やっぱTwitterは素晴らしい。もともとBinaryFormatterなんか遅くね?とTwitterに愚痴ったところからスタートして、色々な人にアドバイスを受けて最終的にこんな記事(斜め上なだけな気はする)になったわけで、Twitterなかったら「遅くね?以上終了」で終わってた。
私がTwitterをはじめたのはTwitterはじめましたとかいう記事で確認出来るところでは2008/01/04(CoD4について書いてますけど、CoD4のシングルはあんま好きじゃなかった、でもその後マルチプレイヤーにはまったので結局MW2買ってますね!)。しかも、この後も当分は何もポストしないままでいて、同年9月の三度目の正直とかいう記事でTwitter→はてダへの転送ツールを作ってからようやく始めてました。コードの一部が載ってますけど、postList.Addとかいうのが初々しいなあ。今だとforeach使ったら負け、.ToList()だろ常識的に考えて。ですね。あと、TrimStartの使い方を間違えてるという。1年半前の私のド素人っぷりが伺えて面白い。ていうか、考えてみると物凄く成長しましたね……。
そして今ではすっかりTwitter中毒です。あらあらうふふ。
アドバイス受けてばかりというのもアレですので、私でも応えられそうなのがTLに流れてきたり検索(キーワード「Linq」を割と頻繁に眺めてる)にかかった時は答えるようにしています。いや、あんましてないかも。積極的に答えるようにしていきたいなあ、と。そんなわけで、@neueccでTwitterやってるので気が向いたらフォローしてやってください。
JavaScriptエディタとしてのVisual Studioの使い方入門
- 2010-05-24
linq.jsってデバッグしにくいかも……。いや、やり方が分かればむしろやりやすいぐらい。という解説を動画で。HDなので文字が見えない場合はフルスクリーンなどなどでどうぞ。中身の見えないEnumerableは、デバッガで止めてウォッチウィンドウでToArrayすれば見えます。ウォッチウィンドウ内でメソッドチェーンを繋げて表示出来るというのは、ループが抽象化されているLinqならではの利点。sortしようが何しようが、immutableなので元シーケンスに影響を与えません。ラムダ式もどきでインタラクティブに条件を変えて確認出来たりするのも楽ちん。
ところで、JavaScript開発でもIDE無しは考えられません。デバッグというだけならFirebugもアリではありますが、入力補完や整形が可能な高機能エディタと密接に結びついている、という点でIDEに軍配があがるんじゃないかと私は思っています。動画中ではVisual Studioの無料版、Visual Web Developerを使っています。Visual Studioというと、何か敷居が高く感じられるかもしれませんが、使う部分を絞ってみれば、超高性能なHTML/JavaScriptエディタとして使えちゃいます。有料版の最高級エディションは170万円ですからね(MSDNという何でも使えるライセンスがセットなので比較は不公平ですが)、機能限定版とはいえ、その実力は推して知るべし、です(機能限定部分は、主にC#でのASP.NET開発部分に絡むものなのでJavaScript周りでは全く関係ありません)。
VSを使うと何が嬉しいのでしょう?JavaScriptでの強力な入力補完、自動整形、使いやすいデバッガ、リアルタイムエラー通知。そしてこっそり地味に大切なことですが、jQueryの完璧な日本語ドキュメント付き入力補完が同梱されています。と、嬉しいことはいっぱいあるのですが、ASP.NETの開発用ではあるので、JS開発には不要なメニューが多くて戸惑う部分も多いのは事実。分かれば不要部分はスルーするだけなので簡単なのですが、そこまでが大変かもしれない。なので、JavaScript開発で使うVisualStudio、という観点に絞って、何が必要で不要なのかを解説していきます。
インストール
何はともあれまずはインストール。Microsoft Visual Studio ExpressからVisual Web Developerを選び、リンク先のWeb Platform Installerとかいうのをダウンロード&実行。
PHPとかWordPressとか色々ありますがどうでもいいので、Visual Web Developer 2010 Expressだけ入れましょう。クリックして指示に従って適当に待つだけ、10分ぐらいあれば終わるはず。10分は短くはないですが、インストール自体は非常に簡単です。
プロジェクト作成
実行すると初回起動時はイニシャライズが若干長いですが、それを超えれば新しいプロジェクトと新しいWebサイトの違いが分からねえええええ。で、ここは新しいWebサイトです。プロジェクトのほうはC#でASP.NETが基本なので関係ありません。スタートページから、もしくはファイル→新規作成→Webサイト。
更に項目があって分からねえ、けどここはASP.NET空のウェブサイトを選びます。次にソリューションエクスプローラーウィンドウを見ます(なければ表示→ソリューションエクスプローラー)。web.configとかいうゴミがありますが、それはスルーしておきましょう(消してもいいですが復活します)。空なので、ルートを右クリックして新しい項目の追加。
いっぱいあると思いますが、ほとんど関係ありません、ノイズです。真ん中ぐらいにあるHTMLページかJScriptファイルを選びましょう。あとは、エディタでガリガリと書いたら、Ctrl+F5を押せば簡易サーバーが立ち上がり、ブラウザ上に現在編集中のHTMLが表示されます。
以上が基本です。手順は簡単なので一度覚えればすんなり行くはずです。最初は如何せんHTML/JS用としてはダミー項目が多いのがやや難点。なお、保存時はデフォルトではMy DocumentのVS2010のWebSites下にHTMLとかが、Projects下に.slnファイル(プロジェクトを束ねている設定とかが書かれたファイル)が置かれています。以後プロジェクトをVSで開くときは.slnのほうをダブルクリック、もしくはスタートページの最近使ったプロジェクトから。
では、Visual Studioを使ってJavaScriptを書いて嬉しい!機能を幾つか挙げていきます。
エラー表示
小括弧が、波括弧が、足らなかったり足しすぎだったりを見落とすことは割とあります。そして起こる実行時エラー。こんなのコンパイルエラーで弾かれてくれ、あばばばば。と思うときはいっぱいあります。そこでVisual Studioのリアルタイムエラー検出。
hoge = functionではなくhoge : function。下のは波括弧が一個多い。というのを、リアルタイムで検出してくれて、疑わしいところには波線を敷いてくれます。エラー一覧にも表示されるので、このウィンドウは常時表示させておくと書くのが楽になります。私は縦置きにしてエディタの左側にサイドバーとして常時表示。カラムはカテゴリと説明だけにしています。
エラー通知のためのコード走査はバックグラウンドで定期的に動いているようですが、任意に発動させたい場合はCtrl + Shift + Jで行えます。修正結果が正しいのかとっとと確認したいんだよ馬鹿やろー、って時に便利。というか普通に押しまくります、私は。
コードフォーマット
コード整形は大事な機能だと思っています。手動でスペース入れていくとか面倒くさいし。かといって整形が汚いコードは萎えます。
ショートカットはCtrl+K、で、Ctrlを押しながら続けてD。微妙に覚えにくいショートカット。ちなみに選択範囲のコメント化はCtrl+K, Cで、非コメント化はCtrl+K, U。ようするに整形系はCtrl+K始まりで、DはDocumentFormat、CはComment、UはUncommentの意味になるようです。フォーマットのルール(改行をどこに入れるか、とか)は設定で変えられます。
デバッグ
当然のようにブレークポイントの設定、ステップイン、ステップアウトなどのデバッグをサポートしています。
F9でブレークポイントを設定してF5でデバッグ実行。が基本です。ローカルウィンドウで変数の値表示、そして便利なのがウォッチウィンドウで、見たい値を好きに記述出来ます。式も書けるので平気で副作用かませます。で、デバッガで良いのはthisが見れるところですねー。JavaScriptはthisが不定で、いったいこの中のthisは何を指しているんだ!と悩んでしまうわけですが、そんなものデバッガで見れば一発で分かりますね、はは。考えるより前にとりあえずデバッグ実行。
さて、そんなデバッグですが、初回時には何やら怪しげなダイアログが上がります。ここはYESで。そして、デバッグ出来ましたか?出来なかった人も多いかもしれません。実は、IEじゃないとデバッガ動かないのです。というわけで、ソリューションエクスプローラーからプロジェクトのルート部分を右クリックしてブラウザの選択を選ぶ。
IEをデフォルトにしてください。一度設定すれば、以降はこの設定が継続されます。IEとか冗談じゃない。と思うかもしれませんが、えーと、IEで常に書くことで、IEで動かないスクリプトを書くことを避けられるのです、とかいうどうでもいい効用はあります。でもまあ、Firefox拡張とかChrome拡張を書くのにはデバッガが使えなくなるも同然なのは不便ですね。その時はデバッグは当然ブラウザ固有のデバッガを使い(デバッガを使わないと言う選択肢はないよ!)、エディタとしてだけに使えばいいぢゃない。
入力補完/日本語jQuery
入力補完(IntelliSense)は素敵。ローカル変数があればそれが出てくる。もう変数名打ち間違えで動かない、とかない。ドットを打てば、補完候補に文字列であればreplaceとか、配列であればjoinとか、DOMであればappendChildとか出てくる。メソッド名を暗記する必要もなければ、打ち間違えることもない。
補完は割と賢くて、関数では引数を見て(というか裏でインタプリタ走ってるんですね、きっと)、ちゃんと返す値を判別してくれます。
ところでですが、最初の釣り画像にあるjQueryの日本語化ドキュメントはどこにあるのでしょうか?
ファイル→新規作成→プロジェクトからASP.NET Webアプリケーションを選びます。すると、Scriptsフォルダの下にjquery-1.4.1-vsdoc.jsとかいうものが!こいつを、コピペって頂いてしまいましょう。ASP.NET Web Application自体はどうでもいいので破棄です、破棄。でもせっかくなので、Default.aspxを開いてCtrl+F5で実行してみてください。出来た!ウェブアプリが出来た!そう、C#+ASP.NETは驚くほど簡単にウェブアプリが作れるんです。あとは安レンタルサーバーさえ普及してくれれば……。
vsdocについて
-vsdoc自体は<script src>で読み込む必要はありませんし、実際にサーバーにアップロードする必要もありません。仕組みとしてはhoge.jsとhoge-vsdoc.jsが同じ階層にあると、VisualStudioの入力補完解析はhoge-vsdoc.jsを見に行く、といった感じになっています。なので、jquery-1.4.1.jsだけを読み込めばOKです。
HTMLファイルに記述する場合はscript srcで読み込めて補完が効くのは分かるけど、単独JSファイルの場合は読み込みの依存関係をどう指定すればよいでしょうか。答えは、ファイルの先頭にreference pathを記載します。
これで、JScript1.jsという単独JSファイルでもjQueryの補完が効かせられるようになりました。reference pathというのはVSだけで効果のあるタグで、ブラウザの解釈上はコメントに過ぎないので、ブラウザ表示時に問題が出ることもありません。
なお、このreference pathというのを覚えている必要はありません。refと記述してTabを二回押すとこのタグが展開されるはずです。コードスニペットというコード挿入の仕組みに予め用意されているわけです。なお、コードスニペットは、この他にもfor->Tab x2でforが展開されたりなど色々あって便利です(自分で作成することも出来る)。
その他設定など
その他、好みもありますが設定など。ツール→オプションから。
何はともかくフォントの変更。MSゴシックとかありえん。フォントをConsolasにしましょう! Consolasはプログラミング用のClearTypeに最適化された見やすい素敵フォントです。勿論、スラッシュドゼロ。サイズは私は9で使ってます。
Ctrl+F5押す度にアウトプットウィンドウが立ち上がるのが猛烈にウザいので、「ビルド開始時に出力ウィンドウを表示」のチェックは外しておく。
HTMLでの属性の引用符自動挿入はチェックつけといたほうが幸せ気分。
入力候補の、このTabかEnterのみで確定させるってのはチェックを外す。だってメソッド書くときは「(」で確定させたいし、オブジェクトを開くときは「.」で確定させたいもの。例えばdocument.getElementByIdは「doc -> Enter -> . -> get -> Enter -> (」じゃなくて「doc -> . -> get -> (」というように、スムーズに入力したい。一々Enterを挟むのは流れを止めてしまう。
まとめ
IDEを知ってて使わない、というのは個人の好き好きなのですが、単純に知らないというのは勿体無いな、と。特に初心者ほどIDEを必要とすると思います。初心者がプログラミング始めるなら、導入がメモ帳とブラウザだけで開発出来るJavaScriptお薦め!って台詞は、あまりよろしくないんじゃないかなー。初心者ほど些細なスペルミスや構文ミスでつまづく上に、目を皿のようにしてみても原因が分からない。たとえ導入までの敷居が若干高くなろうとも、親切にエラー箇所に波線を敷いてくれるIDEこそ必要なんじゃないかな。あと、デバッガ。ビジュアルに変数が動き変わることほど分かりやすいものもないでしょう。
IDEもEclipseのプラグインとか色々ありますが、Visual Studioの強力なjQuery対応度は何にも代え難いんじゃないでしょうか。導入もオールインワンなので何も考えなくてもいい簡単さですし。是非一度、試してみてもらえればいいなあ。
ついでですが、冒頭動画のlinq.jsは便利なJavaScriptライブラリ(無名関数を多用して関数型言語的にコレクション操作を可能にする)でいて、更にVisual Studioの入力補完に最適化してあるので使ってみてください、と宣伝。いや、作者私なので。ごほごほ。jQueryプラグインとして動作するバージョンも同梱してあります。
それと、勿論Visual Studioは有料版のほうが高機能な面もあります。JavaScript開発のみだとあまり差はないのですが、WindowsScriptHostをJavaScriptで書いてもデバッグ出来るとか無料版に比べて大したことない利点があるにはあります。C#でSilverlightなどもごりごり書きたい、とかになれば断然、有料版のほうが輝いてきます。
Ultimateは100万オーバーで無理なので、Professional買いましょう、私は買います。(メインはC#の人間なので。JSの人は正直Expressでイイと思うよ……)。まだ発売されてないのでこれから買います。「アップグレード」ですが、Express(無料版)からのアップグレードも認められているという意味不明仕様なので(誰が倍額する通常版買うんでしょうかね……)皆様も是非、上のリンクからamazonで買ってくれれば、ごほごほ。
DynamicJson ver 1.2.0.0
- 2010-05-21
DynamicJsonをver1.2.0.0に更新しました。おうぁぁぁぁ。Dynamicのまま配列をばらせないことに気づいた!foreachで列挙出来るのはいいけれど、Linqで、例えばSelectManyに渡せないぢゃん!ヤバいヤバい。バグではないけど、そんなんじゃ使い物にならない。ということでその辺のを修正しました。
何言ってるのかよくわからないですね、なわけで例を。
// page1から5までを取得して平らにする(で、古い順に並び替える)
var wc = new WebClient();
var statuses = Enumerable.Range(1, 5)
.Select(i =>
wc.DownloadString("http://twitter.com/statuses/user_timeline/neuecc.json?page=" + i))
.SelectMany(s => (dynamic[])DynamicJson.Parse(s))
.OrderBy(j => j.id);
foreach (var status in statuses)
{
Console.WriteLine(status.text);
}
neueccさん(私だ、私)の投稿を取得するわけですが、一回に20件しか取得出来ないので(countを指定すれば200件まで取れますけど)、複数ページ分取得して結合することにします。そういう場合はforでグルグル?ご冗談を、Modern C#(そんな定義ない)ではLINQを使います。
1ページのJSONは[{text:nantoka},{text:kantoka}] といった形に配列にデータが入った形で取得出来ます。それが5ページ分。[[{},{}],[{},{}]]といった感じなので、これを真平らにしてやりましょう。[{},{},{},{}]といった風に。それがSelectManyです。SelectManyは微妙にわかりにくいけれど、Linqの中でもかなり重要なんですよー。図解 SelectMany - NyaRuRuの日記なども参考に。
さて、この「.SelectMany(s => (dynamic[])DynamicJson.Parse(s))」って部分が、前は出来なかったのです、とほほほ。IEnumerable<T>を実装していれば(IEnumerable<dynamic>)で済んだのだけどなあ。そうすれば配列に変換しなくても済むので効率的にも良いし。Dynamic Viewと結果ビューが共存出来さえすれば……。
というわけですが、今回の修正で無事、出来るようになりました。配列変換は気にくわないけどしょうがない。IEnumerableを実装しているけれどDynamic Viewが使えるやり方募集中です。方法あったら誰か教えてくださいお願いします。
あと、キャストついでに、読み取り専用プロパティに対しても値をセットしようとして例外出るのを修正しました。これは、まあ、バグですね。仕様とか言ったら怒る。ていうかすみません。
linq.js ver 2.1.0.0 - ToDictionary, Share, Let, MemoizeAll
- 2010-05-18
CodePlex - linq.js - LINQ for JavaScript
linq.jsを2.0から2.1に更新しました。今回はただのメソッド追加というだけじゃなく、1.*の時から続いてる微妙コードを完全抹殺の一掃で書き換えた結果、内部的にはかなり大きな変更が入りました。その影響で挙動が変わってるところも割とあります。
まず、OrderByのロジックを変更しました。これで動作はC#完全準拠です(多分)。前のはかなりアレだったのでずっと書きなおしたいと思ってたのですが、やっと果たせました。従来と比べるとThenByを複数個繋げた時の挙動が変わってくる(今まではOrderByのみ安定ソートで、ThenBy以降は非安定ソートだったのが、今回からは幾つ繋げても安定ソートになります)ので、複雑なソートをやろうとしていた場合は違う結果が出る可能性はなきにしもあらず、ですが、基本的には変化なしと見て良いと思います。
ちなみにJavaScriptのArray.sortは破壊的だし、安定である保証もないので、linq.jsのOrderBy使うのは素敵な選択だと思いますよ!非破壊的で安定で並び替え項目を簡単に複数連結出来る(ThenBy)という、実に強力な機能を提供しています。代償は、ちょっと処理効率は重いかもですね、例によってそういうのは気にしたら負けだと思っている。
他に関係あるところで大きな変更はToLookup。戻り値を、今まではJSのオブジェクトだったのですが、今回からはlinq.jsの独自クラスのLookupになります。すみませんが、破壊的変更です、前と互換性ありません。変えた理由はJSのオブジェクトを使うとキーが文字列以外使えないため。そのことはわかっていて、でもまあいっかー、と思っていたのですがGroupByとか内部でToLookupを使ってるメソッドの挙動が怪しいことになってる(という報告を貰って気づいた)ので、ちゃんとした文字列以外でもキーに使えるLookupを作らないとダメだなー、と。
GroupByでのcompareSelector
そんなわけで、GroupByのキーが全部文字列に変換されてしまう、というアレゲなバグが修正されました。あと、オーバーロードを足して、compareKey指定も出来るようにしました。何のこっちゃ?というと、例えばDateもオブジェクトも参照の比較です。
alert(new Date(2000, 1, 1) == new Date(2000, 1, 1)); // false
alert({ a: 0} == { a: 0 }); // false
JavaScriptではどちらもfalse。別のオブジェクトだから。C#だとどちらもtrue、匿名型もDateTimeも、値が比較されます。そんなわけでJavaScriptで値で比較したい場合はJSONにでもシリアライズして文字列にして比較すればいいんじゃね?とか適当なことを言ってみたりはしますが、実際Linqだと参照比較のみだと困るシーン多いんですねえ。そんなわけで、GroupBy/ToLookup、その他多数のメソッドに比較キー選択関数を追加しました。例を一つ。
var objects = [
{ Date: new Date(2000, 1, 1), Id: 1 },
{ Date: new Date(2010, 5, 5), Id: 2 },
{ Date: new Date(2000, 1, 1), Id: 3 }
]
// [0] date:Feb 1 2000 ids:"1"
// [1] date:Jun 5 2010 ids:"2"
// [2] date:Feb 1 2000 ids:"3"
var test = Enumerable.From(objects)
.GroupBy("$.Date", "$.Id",
function (key, group) { return { date: key, ids: group.ToString(',')} })
.ToArray();
キーにDateを指定し、日付でグルーピングしたいと思いました(この程度の指定で関数書くのは面倒くさいし視認性もアレなので、文字列指定は非常に便利です)。しかし、それだけだと、参照比較なので同じ日付でも別物として扱われてしまうのでグルーピングされません。$.Date.toString()として文字列化すれば同一日時でまとめられるけれど、Keyが文字列になってしまう。後で取り出す際にKeyはDateのまま保っていて欲しい、といった場合にどうすればいいか、というと、ここで今回新設した第四引数のcompareSelectorの出番です。
// [0] date:Feb 1 2000 ids:"1,3"
// [1] date:Jun 5 2010 ids:"2"
var test2 = Enumerable.From(objects)
.GroupBy("$.Date", "$.Id",
function (key, group) { return { date: key, ids: group.ToString(',')} },
function (key) { return key.toString() })
.ToArray();
比較はキー(この場合$.Date)をtoStringで値化したもので行う、と指定することで、思い通りにグループ化されました。なお、C#でもこういうシーン、割とありますよね。C#の場合はIEqualityComparerを指定するのですが、わざわざ外部にクラス作るのは大変どうかと思う。といった時はAnonymousComparerを使えばlinq.jsと同じようにラムダ式でちゃちゃっと同値比較出来ます。
なお、今回からGroupByの第三引数(resultSelector)が未指定の場合はGroupingクラスが列挙されるように変更されました。GroupingはEnumerableを継承しているので全てのLinqメソッドが使えます。その他に、.Key()でキーが取り出しできるというクラスです。
Lookup
LookupはGroupByの親戚です。むしろGroupByは実はToLookupしたあと即座に列挙してるだけなのだよ、ナンダッテー。で、何かというとMultiDictionaryとかMultiMapとか言われてるような、一つのキーに複数個の要素が入った辞書です。そして、immutableです。不変です。変更出来ません。
var list = [
{ Name: "temp", Ext: "xls" },
{ Name: "temp2", Ext: "xLS" },
{ Name: "temp", Ext: "pdf" },
{ Name: "temp", Ext: "jpg" },
{ Name: "temp2", Ext: "PdF" }
];
var lookup = Enumerable.From(list).ToLookup("$.Ext", "$.Name", "$.toLowerCase()");
var xls = lookup.Get("XlS"); // toLowerCaseが適用されるため大文字小文字無視で取得可
var concat = xls.ToString("-"); // temp-temp2 <- lookupのGetの戻り値はEnumerable
var zero = lookup.Get("ZZZ").Count(); // 0 <- Getで無いKeyを指定するとEnumerable.Emptyが返る
// ToEnumerableでEnumerableに変換、その場合はGroupingクラスが渡る
// Groupingは普通のLinqと同じメソッド群+.Key()でキー取得
lookup.ToEnumerable().ForEach(function (g)
{
// xls:temp-temp2, pdf:temp-temp2, jpg:temp
alert(g.Key() + ":" + g.ToString("-"));
});
ToLookup時に第三引数を指定すると、戻り値であるLookupにもその比較関数が有効になり続けます。今回はtoLowerCaseを指定したので、大文字小文字無視でグルーピングされたし、Getによる取得も大文字小文字無視になりました。なお、GroupByでもそうですが、キーは文字列以外でも何でもOKです(compareSelectorを利用する場合はその結果が数字か文字列か日付、そうでない場合はそれそのものが数字か文字列か日付を使う方が速度的に無難です、後で詳しく述べますが)。
Dictionary
Lookupは内部でDictionaryを使うためDictionaryも作成、で、せっかく作ったのだから公開しますか、といった感じにToDictionaryが追加されました。ToObjectと違い、文字列以外をキーに指定出来るのが特徴です。
// 従来は
var cls = function (a, b)
{
this.a = a;
this.b = b;
}
var instanceA = new cls("a", 100);
var instanceB = new cls("b", 2000);
// オブジェクトを辞書がわりに使うのは文字列しか入れられなかった
var hash = {};
hash[instanceA] = "zzz";
hash[instanceB] = "huga";
alert(hash[instanceA]); // "huga" ([Object object]がキーになって上書きされる)
// linq.jsのDictionaryを使う場合……
// new Dictionaryはできないので、新規空辞書作成はこれで代用(という裏技)
// 第三引数を指定するとハッシュ値算出+同値比較にその関数を使う
// 第三引数が不要の場合はToDictionary()でおk
var dict = Enumerable.Empty().ToDictionary("", "",
function (x) { return x.a + x.b });
dict.Add(instanceA, "zzz");
dict.Add(instanceB, "huga");
alert(dict.Get(instanceA)); // zzz
alert(dict.Get(instanceB)); // huga
// ...といったように、オブジェクト(文字列含め、boolでも何でも)をキーに出来る。
// ToEnumerableで列挙も可能、From(obj)と同じく.Key .Valueで取り出し
dict.ToEnumerable().ForEach(function (kvp)
{
alert(kvp.Key.a + ":" + kvp.Value);
});
空のDictionaryを作りたい場合は、空のEnumerableをToDictionaryして生成します。微妙に裏技的でアレですが、まあ、こういう風に空から使うのはオマケみたいなものなので。というかToDictionaryメソッド自体がオマケです。DictionaryはLookupに必要だから作っただけで、当初は外部には出さないつもりでした。
第三引数を指定しないとオブジェクトを格納する場合は線形探索になるので、格納量が多くなると重くなります(toStringした結果をハッシュ値に使うので、Dateの場合は値でバラつくので大丈夫です、普通のオブジェクトの場合のみ)。第三引数を指定するとハッシュ値の算出にそれを使うため、格納量が増えても比較的軽量になります(ハッシュ衝突時はベタにチェイン法で探索してます)。なお、第三引数はハッシュ関数、ではあるのですが、それだけじゃなくて同値比較にも利用します。GetHashCodeとEqualsが混ざったようなものなので、ようするにAnonymousComparerのデフォルト実装と同じです。
勿論、ハッシュ関数と同値比較関数は別々の方が柔軟性が高いんですが(特にJavaScriptはハッシュ関数がないから重要性は高いよね!)、別々に設定って面倒くさいしぃー、結局一緒にするシーンのほうが割と多くない?と思っているためこのようなことになっています。というだけじゃなくて、もしequalsとgetHashCodeを共に渡すようにするなら{getHashCode:function(), equals:function()} といった感じのオブジェクト渡しにすると思うんですが、私はIntelliSenseの効かない、こういうオブジェクト渡しが好きではないので……。
メソッドはIDictionaryを模しているためAdd, Remove, Contains, Clear、それにインデクサが使えないのでGet, Set、EnumerableではないかわりにToEnumerableでKeyValuePairの列挙に変換。Addは重複した場合は例外ではなく上書き、Getは存在しない要素を取得しようとした場合は例外ではなくundefinedを返します。この辺はC#流ではなく、JavaScript風に、ということで。
GroupingはEnumerableを継承しているのに、DictionaryとLookupは継承していないのでToEnumerableで変換が必要です。C#準拠にするなら、Groupingと同じく継承すべきなのですが、あえて対応を分けた理由は、Groupingはそのまま列挙するのが主用途ですが、LookupやDictionaryはそのまま使うのがほとんどなので、IntelliSenseに優しくしたいと思ったのからです。90近いメソッドが並ぶと、本来使いたいGetとかが見えなくなってしまうので。
なお、Dictionaryの列挙順は、キーが挿入された順番になります。不定ではありません。JavaのLinkedHashMapみたいな感じです(C#だとOrderedDictionary、Genericsじゃないけど)。順序保持の理由は、DictionaryはLookupで使う->LookupはGroupByで使う->GroupByの取り出し順は最初にキーが見つかった順番でなければならない(MSDNにそう記載がある)。といった理由からです。ちなみにですが、GroupByが順番通りに来るってのは経験則では知ってたのですがMSDNに記載があったのは見落としていて、むしろ不定だと考えるべきじゃないか、とかTwitterでデマ吹いてたんですが即座にツッコミを頂いて大変助かりました、毎回ありがとうございます。
Share, Let, MemoizeAll
そして3つの新メソッド。これらはReactive Extensionsに含まれるSystem.Interactive.dllのEnumerableに対する拡張メソッドから移植しています。
// Shareはenumeratorを共有する
// 一つの列挙終了後に再度呼び出すと、以前中断されたところから列挙される
var share = Enumerable.Range(1, 10).Share();
var array = share.Take(4).ToArray(); // [1,2,3,4]
var arrayRest = share.ToArray(); // [5,6,7,8,9,10]
// 例えば、これだと二度列挙してしまうことになる!
// 1,1,2,3とアラートが出る
var range = Enumerable.Range(1, 3).Do("alert($)")
var car = range.First(); // 1
var cdr = range.Skip(1).ToArray(); // [2,3]
// Shareを使えば無駄がなくなる(アラートは1,2,3)
var share = range.Share();
var car = share.First(); // 1
var cdr = share.ToArray(); // [2,3]
Shareは、列挙の「再開」が出来ると捉えると良いかもしれません。ちなみに、再開が出来るというのは列挙完了までDispose(終了処理)しないということに等しいのには、少しだけ注意が必要かもしれません。
// Letの関数の引数は自分自身
// [1,2], [2,3], [3,4], [4,5]
Enumerable.Range(1, 5).Let(function (e)
{
return e.Zip(e.Skip(1), "x,y=>[x,y]");
});
// 上のLetはこれと等しい
var range = Enumerable.Range(1, 3);
range.Zip(range.Skip(1), "x,y=>[x,y]");
// 余談:Pairwiseは↑と同じ結果です、一つ先の自分との結合
Enumerable.Range(1, 5).Pairwise("x,y=>[x,y]");
Letは、Enumerableを受け取ってEnumerableを返す関数を渡します。何のこっちゃですが、外部変数に置かなくても、メソッドチェーンを切らさずに自分自身が使えるということになります。使い道は主に自分自身との結合を取りたい場合、とか。なお、何も対処せずそのまま結合すると二度列挙が回ることには注意が必要かもしれません。
// MemoizeAllは、そこを一度通過したものはキャッシュされる
var memo = Enumerable.Range(1, 3)
.Do("alert($)")
.MemoizeAll();
memo.ToArray(); // 一度目の列挙なのでDoでalertが出る
memo.ToArray(); // 二度目の列挙はキャッシュからなのでDoを通過しない
// Letと組み合わせて、自己結合の列挙を一度のみにする
Enumerable.Range(1, 5)
.MemoizeAll()
.Let(function (e) { return e.Zip(e.Skip(1), "x,y=>[x,y]") });
MemoizeAllはメモ化です。二度三度列挙するものは、一度.ToArray()とかして配列に置いたりすることが少なくなかったのですが、MemoizeAllはその辺を遅延評価のままやってくれます。使いかっては良さそう。ただし、これもShareと同じく列挙完了まで例外が起ころうが何だろうがDispose(終了処理)しないのは注意が必要かもしれません。素のJavaScriptではリソース管理は滅多にないので関係ないですが、例えばメモ化が威力を発揮しそうな(linq.jsではWSHで、C#だったら普通にやりますよね)ファイル読み込みに使おうとすると、ちょっと怖い。ていうか私はそういう場合は素直にToArrayします。
この3つは「注意が必要かもしれません」ばかりですね!ただ、MemoizeAll->Let->Zipで自分自身と結合するのは便利度鉄板かと思われます。便利すぎる。
まとめ
Dictionaryの導入は地味に影響範囲が大きいです。集合演算系メソッドもみんなDictionary利用に変えたので。なんでかというと、ええと、そう、今まではバグっておりました……。trueと"true"区別しないとか。Enumerable.From(["true",true]).Distinct()の結果が["true"]になってました。ほんとすみません。今回から、そういう怪しい挙動は潰れたと思います。いや、Dictionaryがバグッてたら元も子もないのですが多分大丈夫だと思います思いたい。
とにかく一年前の私はアホだな、と。来年も同じこと思ってそうですが。
Share, MemoizeAll, LetのRx移植三点セットは遊んでる分には面白いんですが、使いどころは難しいですね。ちなみに、MemoizeAllということで、RxにはAllじゃないMemoizeもあるのですが、かなり挙動が胡散臭いので私としては採用見送りだし、C#でも使う気はしません。内部の動きを相当意識してコントロールしないと暴走するってのが嫌。
VS2010のJavaScript用IntelliSenseは本当に強化されましたねー。ということで、VS2010ならDictionaryやLookupでIntelliSenseが動作するのですが、VS2008では動作しません。完全にVS2010用に調整してたら2008では動かないシーンも幾つか出てきてしまった……。まあでも、2010は素敵なのでみんな2010使えばいいと思うよ。私はまだExpressですががが。一般パッケージ販売まだー?(6月です)
そういえば、2.0は公開からひと月でDL数100到達。本当にありがとうございます。そろそろ実例サンプルも作っていきたいのですがネタがな。Google Waveって結局どうなんでしょうかね。ソーシャルアプリは何か肌に合わない雰囲気なのでスルーで、Waveに乗り込むぜ、とか思ってたんですが思ってただけで乗り込む以前に触ってもいないという有様。HTML5は、ええと、基本はC#っ子なのでSilverlight押しだから。うー、じゃあChrome拡張辺りで……。いやでもHTML5かな、よくわからないけれど。
今のところ最優先事項はlinq.javaなんですけどね、忘れてない忘れてない。というか、linq.js 2.0も今回のOrderByのロジック変更も、linq.javaで書いてあったのを持ってきただけだったりして。もうほんと、とっとと出したいです。出してスッキリして次に行きたい。
そんなわけで、この2.1でlinq.jsはようやくスタート地点に立てたぜ、という感じなので使ってやってください。
C# DynamicObjectの基本と細かい部分について
- 2010-05-06
DynamicJsonをver.1.1に更新しました。ver.1.0の公開以降に理解したDynamicObjectについての諸々を反映させてあります。具体的には、IsDefinedやDeleteといったメソッド名を書かずに、それらが呼び出せるようにし、また、foreach時にキャストが不要になりました。DynamicはIntelliSenseが効かないので、メソッド名を書かせたら負け。大まかに分けて「プロパティ」「名前付きメソッド」「名前無しメソッド」「キャスト」の4つをうまいこと振り分けて、不自然ではなく使えるようにすれば、DynamicObjectとして良い設計となるのではないかな、と思っています。
というわけで、今回は微妙にはまったDynamicObjectの挙動の色々について書きますが、その前に1.1の更新事項を。DynamicJson自体については、ver1.0のリリース時の記事を参照ください。
// ただの入れ物クラスなのであまり気にしないで。
public class FooBar
{
public string foo { get; set; }
public int bar { get; set; }
}
// こんなJSONがあったとするとする
var json = DynamicJson.Parse(@"{""foo"":""json"", ""bar"":100, ""nest"":{ ""foobar"":true } }");
var arrayJson = DynamicJson.Parse(@"[1,10,200,300]");
var objectJson = DynamicJson.Parse(@"{""foo"":""json"",""bar"":100}");
// .プロパティ名()はIsDefined("プロパティ名")と同じになります
var b1_1 = json.IsDefined("foo"); // true
var b2_1 = json.IsDefined("foooo"); // false
var b1_2 = json.foo(); // true
var b2_2 = json.foooo(); // false;
// .("プロパティ名")はDelete("プロパティ名")と同じになります
json.Delete("foo");
json("bar");
// キャストはDeserialize<T>()と同じになります
var array1 = arrayJson.Deserialize<int[]>();
var array2 = (int[])arrayJson; // array1と一緒
int[] array3 = arrayJson; // こう書いてもDeserialize呼び出しと同じだったりする
// 配列だけではなく、パブリックプロパティ名で対応を取るマッピングも可能です
var foobar1 = objectJson.Deserialize<FooBar>();
var foobar2 = (FooBar)objectJson;
FooBar foobar3 = objectJson;
// 勿論、配列+オブジェクトでも可。Linqに繋げる時はキャストで囲みましょう(asはダメ)
var objectJsonList = DynamicJson.Parse(@"[{""bar"":50},{""bar"":100}]");
var barSum = ((FooBar[])objectJsonList).Select(fb => fb.bar).Sum(); // 150
var hoge = objectJsonList as FooBar[]; // これはnullになる、asとキャストは挙動が違う
// array状態のDynamicJsonにforeachはdynamicが渡る
// 中の型が分かっている場合は、varではなく型名指定するといいかも
// ちなみに、数字はdynamicのままだと全てdoubleです
foreach (int item in arrayJson)
{
Console.WriteLine(item); // 1, 10, 200, 300
}
// オブジェクト状態のDynamicJsonへのforeachはKeyValuePair
// .Key、.Valueなのは分かってる、というならdynamicで受けると楽かも
foreach (KeyValuePair<string, object> item in objectJson)
{
Console.WriteLine(item.Key + ":" + item.Value); // foo:json, bar:100
}
foreachを自然に使えるようにしたのと、IsDefined、Remove、DeserializeをDynamicとして自然に呼び出せるようにした、というのが更新内容になります。IsDefined("name")が.name()で自然なのか?というと、どうなんでしょうねー、という感じですが、しかしDynamicはIntelliSenseが効かないのです!なので、多少ややこしくても、こうして使える方が便利だと思われます。
ところで、キャストとasの関係は、DynamicObjectだとより正確に意識する必要が出てきます。キャストとasは例外が飛ぶかnullになるかの違いしかない、と思っていたりしたのは私なのですが、そんなことはなくて、asはユーザ定義の変換演算子を呼ばないという性質があります。DynamicObjectでもそれが反映されているため、キャストするとTryConvertが呼ばれるのですが、asは純粋にそのクラスの継承関係しか見ません。
DynamicObjectとは
では本題。まずは基本から。型宣言がdynamicの場合に、挙動が変わるオブジェクトの作成方法は、DynamicObjectを継承して、挙動を変えたいメソッド(Tryなんたら)をオーバーライドするだけです。
override->スペースでIntelliSenseに候補が出てきますね、素敵。Try何とかかんとかの第一引数はbinderですが、とりあえずbinder.Nameだけで何とかなります。成否はboolで返し(falseの場合は、呼び出しを解決するため更に連鎖が重なったりする)、trueの場合はresultにセットした値が呼び出し元に返る。といった仕組みになっています。
呼び出しの解決
簡単な例、ということで呼び出し名を返す、というだけの単純なTryInvokeMemberを定義してみます。
// MyDynamic:DynamicObjectのオーバーライド、メソッド呼び出し名を文字列としてそのまま返す、引数がある場合はfalse
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
result = binder.Name;
return (args.Length > 0) ? false : true;
}
dynamic d = new MyDynamic();
var t1 = d.Hoge(); // Hoge - dynamic(string)
var t2 = d.ToString(); // ToStringにはならない、MyDynamicのToStringが呼ばれる
さて、この場合本来あるメソッドであるToStringやEquals、その他自分で定義したメソッドがあれば、それらを呼んだ場合はどちらが優先されるでしょうか、というと、定義されたメソッドが優先です。なので、TryInvokeMemberにだけ挙動を定義しておくと、どうやっても呼べないメソッドが出てきます。例えばDynamicJsonで言えば、プロパティ名がToStringのJSONに対してToString()で定義されているか確認は出来ません。そのための回避策として、TryInvokeMemberはIsDefinedの簡易記法としています。IsDefined("ToString")ならば問題なく呼べますので。
引き続いて、TryInvokeMemberがfalseの場合も見てみます。
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
result = new MyDynamic();
return true;
}
public override bool TryInvoke(InvokeBinder binder, object[] args, out object result)
{
result = ((string)args[0]).ToUpper();
return true;
}
dynamic d = new MyDynamic();
var t = d.Hoge("aaa"); // AAA(InvokeMemberが失敗したらGetMemberが呼ばれ、それのTryInvokeが呼ばれる)
d.Hoge("aaa")は、まずTryInvokeMemberが呼ばれます。今回は引数がある場合はfalseとしているので、失敗します。すると、呼び出しの解決のためTryGetMemberが呼ばれます。ここでtrueの場合は、引数を持ってTryInvokeが呼ばれます。TryInvokeはd("aaa")のような、メソッド名なしでの関数呼び出しです。C#にはない記法となるので、TryInvokeMemberからの失敗の連鎖でTryGetMemberでDynamicObject以外を返すと、TryInvokeに失敗という形で例外が出ます。
ややこしいですね!この失敗の連鎖はDynamicJsonではオブジェクトがネストしている際の呼び出しの解決に利用しています。
var json = DynamicJson.Parse(@"{""tes"":10,""nest"":{""a"":0}");
json.nest(); // これはjson.IsDefined("nest")
json.nest("a"); // これはjson.nest.Delete("a")
json.nest("a")は、まずTryInvokeMemberが呼んでいます。これは原則IsDefinedと等しいのですが、引数がある場合はfalseにしています。そのためTryGetMemberが呼ばれて.nestを取得。そして、TryInvoke(これはDeleteに等しい)を呼ぶという流れになっています。真面目にDynamicObjectを使って構造を作る場合、呼び出し解決順序などを意識する必要は、間違いなくあります。ややこしいですけどねー。
DynamicObjectとforeach
DynamicObjectがIEnumerableならば、foreachはそれを呼びます。ていうかforeach可能なものはIEnumerableにするでしょ常識的に考えて。と、言いたいのですが世の中そうもいかなかったりします。
Dynamicの変数はデバッガで見るとDynamic Viewというものが用意されていて、展開すると全てのプロパティ名と値を表示してくれます(これに対応させるにはGetDynamicMemberNamesをオーバーライドする必要がある、DynamicObject作るなら必須!)。大変便利なのですが、これ、クラスがIEnumerableを実装している場合はIEnumerableの結果ビューに置き換わってしまい、Dynamic Viewがなくなってしまいます。
Dynamic Viewがないと非常に不便極まりないので、DynamicJsonではIEnumerableの実装は断念しました。しかし、foreachでそのまま呼べないのは不便だ。どうする?となって思い浮かんだのは、IEnumerableじゃなくてもGetEnumeratorがあればforeachって呼ばれるんだよねー、普通のクラスは。という仕様がC#にはあるので、Dynamicでも行けるかな?と思いきやそんなことはなく、呼ばれませんでした。じゃあ、かわりにforeach時に何が呼ばれていたか、というと、TryConvertが呼ばれます。
public override bool TryConvert(ConvertBinder binder, out object result)
{
// foreachで呼ばれた時はbinder.TypeがIEnumerable
if (binder.Type == typeof(IEnumerable))
{
result = Enumerable.Range(1, 10); // resultはIEnumerableとIEnumerator、どちらでも可
return true;
}
// 通常のキャストは適当に分岐させるか、キャストの演算子オーバーロードでもどちらでもいい
// 演算子オーバーロードがある場合は、そちらが優先されます
result = (binder.Type == typeof(string)) ? "hogehoge" : null;
return true;
}
dynamic d = new MyDynamic();
foreach (var item in d)
{
Console.WriteLine(item); // 1,2,3,4,5,6,7,8,9,10
}
var ie = (IEnumerable)d; // これは失敗する、インターフェイスへの明示的型変換は不可!
C#ではインターフェイスの演算子オーバーロードは定義出来ないし、Dynamicでも呼び出しは不可能になっています。が、foreach呼び出し時のみ、TryConvertにTypeがIEnumerableとして渡るようになっているので、そこでtrueを返せば、IEnumerableではないDynamicObjectでもforeachで列挙出来ます。
正直なところ、セコいハックに過ぎないです。本当はデバッガでDynamic Viewと結果ビューが共存できればいいんですよ、ていうか出来るべき。あと、Dynamic Viewは今のところ値がnullのものは表示しないようになっているのですが、これも不便な仕様ですね、改善して欲しいところ。とはいえ、IEnumerableじゃないから不便になってる!ということは無いと思われます。どちらにせよIEnumerableじゃなくてもdynamicはキャストしないと拡張メソッドが呼べない(つまりそのままではLinqが使えない)ため、利用感は犠牲にしていません。
そういえばExpandoObjectはIEnumerableなのにdynamic viewが出るので、何か方法はあるかもしれませんね。
まとめ
dynamicは当初思っていたよりも、遥かに使いがいのある仕組みでした。dynamicはDSL。だと思います。そして、DSLとして便利に使わせるならば、メソッド名で呼ばせるのは厳禁。用意された機構を上手く使ってIntelliSenseレスでも快適に操作出来るようにしなければならない。というか、それで操作出来ないならば普通にC#で組んでIntelliSense効かせた方がずっと良い。
と、まあ、そんなわけでDynamicJsonは400行程度の小さいコードですが、割と色々考えて作ってありますので、是非使ってみてください。お手製ライブラリにありがちなJSON解析の出来が怪しい、といった問題を、解析部を.NET FrameworkのJsonReaderWriterFactoryに丸投げしているため避けられている、というのも大きな利点かと思われます。
ソースコード本体は、ArrayとObjectを一つのクラスに統合しているため、各メソッド行頭でifで分ける、というのが若干怪しいのですが(オブジェクト指向的にはポリモーフィズム、ていうかDynamicならその辺考えずに分けても問題ない、だろうけど400行のコードですからねえ、分割したほうが手間かつ分かりにくくなるだろうしで、まだリファクタリングの出番ではない、と思いますです、これ以上規模が膨れるなら別ですが)、全体的にはDynamicObjectの機能を満遍なく使っているので、参考になるかと思います。あと、JSON書き出し時のLinq to XmlでのXML組み立て部分は若干トリッキーなものの、割とよく出来ているかな? Deserializeはもう少し気合入れて実装し直さないとダメな感じですがががが。
DynamicJson - C# 4.0のdynamicでスムーズにJSONを扱うライブラリ
- 2010-04-30
C#4.0の新機能といったらdynamic。外部から来る型が決まってないデータを取り扱うときは楽かしら。とはいえ、実際に適用出来る範囲はそんなに多くはないようです。例えばXMLをdynamicで扱えたら少し素敵かも、と一瞬思いつつもElementsもDescendantsも出来なくてAttributeの取得出来ないXMLは、実際あんまり便利じゃなかったりする。ただ、ちょうどジャストフィットするものがあります。それは、JSONですよ、JSON。というわけで、dynamicでJSONを扱えるライブラリを書いてみました。ライブラリといっても300行程度のクラス一個です。
使い方は非常にシンプルで直感的。まずは、文字列JSONの読み込みの例を。DynamicJson.Parseメソッド一発です。
// Parse (from JsonString to DynamicJson)
var json = DynamicJson.Parse(@"{""foo"":""json"", ""bar"":100, ""nest"":{ ""foobar"":true } }");
var r1 = json.foo; // "json" - dynamic(string)
var r2 = json.bar; // 100 - dynamic(double)
var r3 = json.nest.foobar; // true - dynamic(bool)
var r4 = json["nest"]["foobar"]; // インデクサでのアクセスも可
// 定義されてるかチェック
var b1 = json.IsDefined("foo"); // true
var b2 = json.IsDefined("foooo"); // false
Parseしたら、あとはJavaScriptと同じ感じにプロパティ名をドット打つだけで値が取り出せます。dynamicとJSONは相性が良いですね、JavaScriptと全く同じ感覚です。注意点としては、存在しないプロパティ名を読むと例外が出ます。Dictionaryみたいなものだと思ってください。さすがにそれだと使いにくいところもあるので、IsDefinedメソッドであるかないかチェック出来ます。dynamicなため、IntelliSenseには出てこないということは注意してください。もう一つ気をつけなきゃいけないのは、数字は全てdoubleです。JSONでは数値類は全部一緒くたにNumberなので、適宜自分でキャストしてください。
オブジェクトからJSON文字列への変換
dynamicとは関係ないのですが、JSON文字列へのシリアライズも可能です。こちらもDynamicJson.Serializeメソッド一発。
// Serialize (from Object to JsonString)
var obj = new
{
Name = "Foo",
Age = 30,
Address = new
{
Country = "Japan",
City = "Tokyo"
},
Like = new[] { "Microsoft", "Xbox" }
};
// {"Name":"Foo","Age":30,"Address":{"Country":"Japan","City":"Tokyo"},"Like":["Microsoft","Xbox"]}
var jsonStringFromObj = DynamicJson.Serialize(obj);
匿名型でサクッとJSONを作り上げられます。非常にお手軽。DataContractJsonSerializerはちょっと大仰すぎなのよねえ、という時にはこれでサクサクッと作ってやってください。匿名型だけじゃなく、普通のオブジェクトでも大丈夫です(その場合はパブリックプロパティからKeyとValueを生成します)。
JSONオブジェクトの再編集・作成
生成したDynamicJsonは可変です。自由に編集して再シリアライズとか出来ます。
var json = DynamicJson.Parse(@"{""foo"":""json"", ""bar"":100, ""nest"":{ ""foobar"":true } }");
// 追加、編集、削除が出来ます
json.Arr = new string[] { "NOR", "XOR" }; // Add
json.foo = 5000; // Replace
json.Delete("bar"); // Delete
// DynamicJsonから文字列へのシリアライズはToStringを呼ぶだけ
var reJson = json.ToString(); // {"foo":5000,"nest":{"foobar":true},"Arr":["NOR","XOR"]}
// 配列はちょっと特殊で、foreachなので扱いたい場合はobject[]にキャストしてください
Console.WriteLine(json.Arr[1]); // XOR
foreach (var item in (object[])json.Arr)
Console.WriteLine(item); // NOR XOR
// 新しく作成することも出来ます
dynamic root = new DynamicJson(); // ルートのコンテナ
root.obj = new { }; // 空のオブジェクトの追加は匿名型をどうぞ
root.obj.str = "aaa";
root.obj.@bool = true; // C#の予約語と被る場合は@を先頭につけるとアクセス出来るよ!
root.array = new[] { 1, 200 }; // 配列の追加
root.obj2 = new { str2 = "bbbb", ar = new object[] { "foobar", null, 100 } }; // オブジェクトの追加と初期化
// {"obj":{"str":"aaa","bool":true},"array":[1,200],"obj2":{"str2":"bbbb","ar":["foobar",null,100]}}
Console.WriteLine(root.ToString());
追加は存在しないプロパティ名に直接突っ込めばOK。編集はそのまま上書き。型名とか関係ないので、元から入っているものの型に合わせる必要はありません。削除はDeleteメソッドを呼べば出来ます。配列はちょっと扱いが特殊でして、foreachしたかったりLinqメソッド使いたい場合はobject[]にキャストする必要があります。この辺は仕様です。諸事情によりIEnumerableじゃないんです。ごめんなさい。ちなみにobjectってものなあ、intが欲しいんすよ、っていう時は.Cast
一から新しいDynamicJsonオブジェクトを作成することも出来ます。普通にnewするだけ。注意点としては、変数はdynamicで受けてください。varで受けても何の嬉しいこともありませんので。あとは、普通にぽこぽこ足すだけ。オブジェクトを作る場合は空の匿名型でやります。決してDynamicJsonを足したりしないでください、がっかりなことになりますので。
実装の裏側
300行のクラス一個、ということで、勿論自前でパーサー書いてるわけがありません。ていうか、その手のは自分で書きたくないんだよね、ソートアルゴリズムとかもそうだけど、こういうのはちゃんと検証されてるものを使うべき。(そしてそもそも、ちゃんとしたのが書けるかというと、書けません……)。で、何を使っているかというと先日の記事でLinq to Jsonとか言ってたように、JsonReaderWriterFactoryを使用しています。
ようするに、ただのJsonReaderWriterFactoryのラッパーです。内部ではJSONの構造をXMLとして保持していて、書き出しの際にJsonReaderWriterFactoryを通しています。ただですね、Readerのほうは使い易くてJsonReaderWriterFactoryお薦め!なのですが、Writerのほうは結構厳しいです。ルールに則って書いたXMLを通すとJSONになる、という仕組みなのですが、ルールに則ってないと即弾かれるということでもあって、かなり面倒くさいです。
// 例えばこんなDynamicJSONは
dynamic root = new DynamicJson();
root.Hoge = "aiueo";
root.Arr = new[] { "A", "BC", "D" };
// 内部ではこんなコードに変換されています
new XElement("root", new XAttribute("type", "object"),
new XElement("Hoge", new XAttribute("type", "string"), "aiueo"),
new XElement("Arr", new XAttribute("type", "array"),
new XElement("item", new XAttribute("type", "string"), "A",
new XElement("item", new XAttribute("type", "string"), "BC",
new XElement("item", new XAttribute("type", "string"), "D")))));
このXElementを素で書いていくのは地獄でしょう。DynamicJsonはこの変換を自動で行います。dynamicでラッピングすることで、煩わしい部分を完全に包み隠すことができました。ここまで簡略化出来ると、DSLの域です。C#は大変素晴らしいデスネ。いや、マジで。
まとめ
クラス一個なので、csファイルをコピペって使ってもいいですし(その場合は追加でSystem.Runtime.Serializationの参照を)、DLLを参照設定に加えても、どちらでもお好きな方をどうぞ。数あるJSONライブラリの中でも、使いやすさはトップクラスなのではないでしょうか(自画自賛)。いや、これは、単純にdynamicの威力の賜物ですね。これを作るまではdynamicについて割と勘違いしていたところもあったのですが、なんというか、DSL向けだと思います。で、DSL指向で行くなら全部プロパティだけで組まないとダメですねえ。IntelliSenseが動かないのでメソッドを使うのは今ひとつ。そういう意味で、IsDefinedじゃなくて.property? とかって感じに、末尾に?をつけるとかどうかな!とか考えてみたんですが、コンパイル通らないのでダメでした、残念。「.あいうえお」なら行けるので、日本語プログラミングDSLが待たれるところです。嘘。
static void Main()
{
var publicTL = new WebClient().DownloadString(@"http://twitter.com/statuses/public_timeline.json");
var statuses = DynamicJson.Parse(publicTL);
foreach (var status in (dynamic[])statuses)
{
Console.WriteLine(status.user.screen_name);
Console.WriteLine(status.text);
}
}
最後に例として、Twitterのpublic_timeline.jsonを引っこ抜くコードを。凄まじく簡潔です。C#はどこをどう見てもLightWeightですね、本当にありがとうございました。
C#とLinq to JsonとTwitterのChirpUserStreamsとReactive Extensions
- 2010-04-29
何か盛り沢山になったのでタイトルも盛り沢山にしてみました。SEO(笑)
最近話題のTwitterのChirpUserStreamsを使ってみましょー。ChirpUserStreamsとは、自分のタイムラインのあらゆる情報がストリームAPIによりリアルタイムで取得出来る、というもの。これを扱うには、まずはストリームをIEnumerable化します。そのまま扱うよりも、一度IEnumerable化すると非常に触りやすくなる、というのがLinq時代の鉄則です。C#でのストリームAPIの取得方法は以前にも記事にしましたが、かなり汚かったのでリライト。WebClient愛してる。
public static IEnumerable<XElement> ConnectChirpStream(string username, string password)
{
const string StreamApiURL = "http://chirpstream.twitter.com/2b/user.json";
var wc = new WebClient() { Credentials = new NetworkCredential(username, password) };
using (var stream = wc.OpenRead(StreamApiURL))
using (var reader = new StreamReader(stream))
{
var query = reader.EnumerateLines() // 1行に1JSONなのです
.Where(s => !string.IsNullOrEmpty(s)) // 空文字が来るので除去
.Select(s => // 文字列JSONからXElementへ変換
{
using (var jsonReader = JsonReaderWriterFactory.CreateJsonReader(Encoding.Default.GetBytes(s), XmlDictionaryReaderQuotas.Max))
return XElement.Load(jsonReader);
});
foreach (var item in query) yield return item; // 無限列挙
}
}
// StreamReaderの補助用拡張メソッド(あると大変便利)
public static IEnumerable<string> EnumerateLines(this StreamReader streamReader)
{
while (!streamReader.EndOfStream)
{
yield return streamReader.ReadLine();
}
}
中々シンプルに書けます。C#もLLと比べても全然引けを取らないでしょう(誰に言ってる)。ChirpUserStreamsはJSONでしか取れないのですが、StreamAPIではXmlよりもJSONのほうが使い易いので、JSONだけでも全然問題ありません。とはいえ、C#でJSONはちょっと扱いにくいんだよねー?と思いきや、意外に普通に標準ライブラリだけで何とかなりました。
参照設定にSystem.Runtime.Serializationを加えます(注:VS2008ではSystem.ServiceModel.Webを参照設定に加えてください)。この参照で通常使うのはDataContractJsonSerializerだと思いますが、もう一つ、JsonReaderWriterFactoryというクラスが用意されていて、このReaderはJSONをXmlReaderとして扱うことが出来ます。この図式は以前のLinq to HtmlのためのSGMLReader利用法と同じです。そのままXElementに流しこめば、JSONをXmlとして扱って、Linq to Jsonが成り立ちます。
さて、StreamAPIなのでEndOfStreamは来ない。繋ぎっぱなし、つまりはConnectChirpStreamメソッドは無限リストになります。この中には色々な情報が混ぜこぜになって来ます。投稿した、という他にも、誰かをフォローした、何かをふぁぼった、何かをリツイートした、などなどなどなど。クライアントソフト作るなら、当然どの情報も漏れ無く扱いたいわけですが、どうしましょう?foreachでグルッと回して、ifで分ける、しか、ない、かもですね? しかしそれは敗北です。退化です。foreachを使ったら負けだと思っている。
ところで突然に、今のところ思うReactive Extensions for .NET (Rx)を使うメリットは3つ。「複雑になりがちな複数イベントの合成」「同じく複雑になりがちな非同期処理のLinq化」そして、「列挙の分配」。従来型のLinqでは、一回の列挙には一個の処理しか挟めませんでした。例えば、MaxとCountを同時に取得する方法はなかった。MaxとCountを別々に二度列挙するか、または旧態依然なやり方、つまりforeachでグルグルと回してMaxとCountを手動で計算するかしかなかった。それはIEnumerableがPullモデルなためで、PushモデルのIObservableならば、出来ないこともない。
では、Rxでこのストリームを分配してみましょう。
static void Main(string[] args)
{
// IEnumerableをIObservableに変換し、Publish(Connectするまで列挙されない(ので分配が可能になる))
var connecter = ConnectChirpStream("username", "password")
.ToObservable()
.Publish();
// 1件目は必ず自分のフレンドのIDリストが来るらしいっぽいのでまるっと保存
HashSet<int> friendList;
connecter.Take(1).Subscribe(x => friendList = new HashSet<int>(
x.Element("friends").Elements().Select(id => (int)id)));
// どんなのが来るのかよく分からないのでモニタ用にテキストにまるっと保存
var sw = new StreamWriter("streamLog.txt") { AutoFlush = true };
connecter.Subscribe(x => sw.WriteLine(x));
// userがあるなら普通の投稿(ってことにしておく)
connecter.Where(x => x.Element("user") != null)
.Select(x => new
{
Text = x.Element("text").Value,
Name = x.Element("user").Element("screen_name").Value
})
.Subscribe(a => Console.WriteLine(a.Name + ":" + a.Text));
// favoriteとかretweetは "event":"favorite" というJSONが来る
var events = connecter.Where(x => x.Element("event") != null);
// favoriteの場合の処理
events.Where(x => x.Element("event").Value == "favorite")
.Subscribe(x => Console.WriteLine(x)); // favorite用の何か処理
// retweetの場合の処理
events.Where(x => x.Element("event").Value == "retweet")
.Subscribe(x => Console.WriteLine(x)); // retweet用の何か処理
// 同期か非同期かは、ToObservableの引数で変わる。デフォルトは同期
// Scheduler.ThreadPoolを引数に入れるとThreadPoolで非同期になる
connecter.Connect(); // 列挙開始
}
ID一覧取得、テキスト保存、投稿時処理、Fav時処理、リツイート時処理の5つへの分配が非常にスマートに出来ました。ToObservable、Publish、Connect。たったこれだけで一つストリームを複数に分配することが出来ます。普通にそれぞれをWhereだのSelectだの、独立してLinqでコネコネ出来ました。で、何が嬉しいかっていうと、それぞれが完全に独立していて見やすいってのは勿論あります。あと、部品化されてるので外部に分割しやすくなるんですね、物凄く。組み合わせたりもしやすいし。
結論
Rxヤバい。というわけで、みんなRx触ろう! .NET Framework 4.0ではRxで使うIObservableとIObserverインターフェイスが搭載されています。インターフェイスだけでどうすんだよボケ、っていうと、実際のところどうにもなりませんね、たはー。それでもインターフェイスだけ先行搭載ということは、RxはDevLabs内だけで終わる実験的プロジェクトではなく、必ず標準搭載するから安心しろよ!というメッセージだと受け取ることにしました。きっと.NET Framework 4.0 SP1には標準搭載されます。される、と、いいなあ。ちなみにRxは初期の頃と結構変わってますし、まだ変わるかも。でも、だからこそ、それに付き合うのも楽しいってものですよ?
ああ、あと、ChirpUserStreamsもヤバいですね。リアルタイムでゴリゴリ迫ってくる感覚は素敵というか、なんか別次元のメディアになった感じでもあります。今、新規にTwitterクライアント作るならStreamAPI完全対応すれば差別化出来て良いですね!私は作りませんが。ただ、ストリームAPIとRxは相性良いと思うので、ストリームAPI時代の到来と同時にRx時代も到来!する、かなあ?
ToDo
linq.jsがようやく一段落したので、Rxの紹介もまたやっていきたいですねー(すみません、かなり長いこと放置していて)。ToObservableによる列挙の分配は中々に強烈なので、linq.jsとRxJSを橋渡しするようなコードというかJSというかも用意したいなあ(軽く作ってみたんですが、RxJSでもPublishで分配出来て中々に威力ありそうでした)。うーん、考えてみるとやることはいっぱいあるねえ。適当に待っていてください。
linq.js ver 2.0 / jquery.linq.js - Linq for jQuery
- 2010-04-23
無駄に1280x720なので、文字が小さくて見えない場合はフルスクリーンにするかYouTubeに飛んでそちらで大きめで見てください。というわけで、動画です。linq.js + Visual Studio 2010で補完でウハウハでjQueryプラグインで世界系です。ここ最近、RxJS、JSINQとJavaScript系の話が続く中で、ふと、乗るしかない このビックウェーブに、という妄念が勢いづいてlinq.jsをver.2.0に更新しました。
内部コードを全面的に変更し、丸っきり別物になりました。破壊的な変更も沢山あります。名前空間がE、もしくはLinq.EnumerableだったのがEnumerableになり、幾つかのメソッドを廃止、廃止した以上にメソッドを大量追加。そして、WindowsScriptHostに対応しました。その他色々細かい変更事項の詳細は下の方で。あ、そうそう、名前空間はEnumerableを占有するので、prototype.jsとは被るため一緒に使えません。
今回の最大のポイントは、jquery.linq.jsという、jQueryのプラグイン化したlinq.jsを追加です。基本機能は完全に同一ですが、jQueryプラグイン版のみの特徴として、jQueryとEnumerableとの相互変換用のメソッドが定義されています。呼び出しは$.Enumerableを使います(グローバル名前空間は一つも汚しません)。
Linqとは?
耳タコなぐらい繰り返していますが、C#知らない人やこのサイトを始めて訪れた人にも使って欲しいんです!ということで、Linqとは何かを紹介します。簡単に言うと、そのふざけたforをぶち殺す。ための代物。Linq導入以降、当社比100%でfor, foreachの出番はなくなりました。ifも半減。ネスト量激減。えー、forー?forなんて使うの小学生までだよねー。
Linq(to Objects)とは?便利コレクション処理ライブラリです。語弊は、大いにある。あるのだけど、言い切るぐらいでいいと思うことにしている近頃。具体的には、mapやfilterやreduceが使えます。非破壊的なsortができます。Shuffle(ランダムに並びかえ)やDistinct(重複除去)などがあります。流れるようにメソッドチェーンで記述出来ます。index付きのforeachが書けます。DOMに対しても同じようにforeachやmapを適用できます。遅延評価が基本になっているので、幾つmapやfilterを繋げても、ムダの無い列挙が可能になっています。また、無限リストが扱えます。JavaScriptでプログラミングするにあたって不足しまくるコレクション処理を大幅に下支えするのが、linq.jsです。
Linqの世界、jQueryの世界
100億の言葉より1の実例。
// こんな感じ(1から10個、偶数のみを二乗したのをアラートに出す)
$.Enumerable.Range(1, 10)
.Where("$%2==0") // 他言語でfilterとか言われているもの
.Select("$*$") // 他言語でmapとか言われているもの
.ForEach("alert($)");
// Linq側はTojQueryでjQueryオブジェクトに変換
$.Enumerable.Range(1, 10)
.Select(function (i) { return $("<option>").text(i) })
.TojQuery()
.appendTo("#select1");
// jQueryオブジェクト側はtoEnumerableでLinqに変換
var sum = $("#select1").children()
.toEnumerable()
.Select("parseInt($.text())")
.Sum(); // 55
シームレスに結合されているようであまり統合されてはいません。お互い、コレクション操作中心であったり、連鎖で操作するという形なので、融け合うことはできません。toEnumerableに関しても、jQuery世界とは切り離された、Linqの世界へと移行するだけ。とはいえ、toEnumerableとTojQueryにより、チェーンを切らさずに相互変換して、スムーズに世界を切り替えられるというのは、中々に楽しい。
図にするとこんな感じ。jQueryの世界はメソッド名小文字、Linqの世界はメソッド名大文字、というので自分の今居る世界が分かります。Linqは世界系。世界に囚われると逃げ出すことは出来ないのです!いくらもがいても(Select)もがいても(Where)変わらない。鏡の中に逃げこむか(tojQuery)、鏡を割ってしまうか(ToArray)、どちらにせよ、代償を払うわけですね? 意味不明。そんな感じで非常に楽しいです。
IDE Lover
linq.jsの特徴として、入力補完サポートを徹底的に行っているという点があります。なぜなら、私自身がIDE大好きっ子、入力補完大好きっ子だから。テキストエディタで書いているとか言う人は、そのエディタは窓から投げ捨ててIDE使おうぜ! どんな感じで書けるかというと、動画を見てください、一番上の。今回は無料のVisual Web Developer 2010 Expressを使っています。インストールも拍子抜けするぐらい簡単ですよ!超お薦め。HTML/JSエディタとして使うポイントは、「Webサイトを作成」から「空のASP.NETウェブサイト」を選ぶことです。詳しくはまた後日にでも。
linq.js ver.2.0.0.0
では、本体の更新事項の方もご紹介。jquery.linq.jsも、この2.0から作られているので以下の事項は当てはまります。まず、名前空間を大々的に弄りました。Linq名前空間は全面的に廃止、Enumerableのみに。また、ショートカットとして用意していたEも廃止。それとLinq.ObjectとLinq.Enumerableは統合されて、Enumerableになりました。
次に廃止ですが、いっぱいあります。ToJSONの廃止、ちゃんと出力可能なのかを保証出来ないのでやめた。JSONは専用ライブラリ使ってください。ToTableの廃止、明らかに不要で邪魔臭かったから。なんでこんなもの搭載したのかすら謎。TraceFの廃止、というか、今までのTraceFをTraceとし、TraceFを消滅させました。TraceFはconsole.logに書き込んでいたのですが、IE8からはIEでも使えるようなので、F(Firebug)じゃなくていいかな、と。あと元のTraceはdocument.writeだったので、それは意味ねーだろ、ということで。あとは、RangeDownToの廃止。かわりにRangeToをマイナス方向への移動に対応させました。これは、ToなんだからUpとDownを区別するほうがオカシイってことにやっと気づいたからです、しょぼーん。
次に名前変更の類。ZipWithの名称をZipに変更(.NET 4.0と名前を合わせるため)。Sliceの名称をBufferWithCountに変更(Rxと名前を合わせるため)。Makeの名称をReturnに変更(Rxと名前を合わせるため)。Timesの名称をGenerateに変更(Rxと名前を合わせるため、RxのGenerateは基本Unfoldですが)。
新メソッドとしてMaxBy, MinBy, Alternate, TakeExceptLast, PartitionBy, Catch, Finallyを追加しました。MaxByとMinByはそのまんま。Alternateは一個毎に値を挟み込みます。TakeExceptLastは最後のn個(引数を省いた場合は1個)を除いて列挙します(Skipの逆バージョンみたいなもの)。PartitionByはGroupByの特殊系みたいな。CatchとFinallyはエラーハンドリング用。いつもなら、ここで例を出すところなのですが今回は省略、詳しくはリファレンスのほうで。
それと、OfTypeの追加。JavaScriptは型がゆるふわだからCastもOfTypeもイラナイし!と思ってたんですが、そしてCastは実際いらないのですが、OfTypeは翌々考えてみると型がゆるふわだからこそ、フィルタリングするために超絶必要じゃないか!ということに気づきました。ので追加。そして、これでCast以外は.NETのLinq演算子の全てを搭載ということになりました。
Windows Script Host
最後に、Enumerable.FromをJScriptのEnumeratorに対応させました(linq.jsのEnumeratorじゃなくて組み込みの方ね)。これにより、Windows Script Hostやエディタマクロでlinqが使えるようになりました。Linq for WSH!えー、WSH?WSHなんて使うの小学生までだよねー。今はPowerShellっすよ。と思うかもしれませんが違います。WSHならばJavaScriptで強烈に補完効かせながらガリガリ書けるのです。しかもLinqが使える。むしろPowerShellの時代は終わったね、WSH復権の時よ来たれ!だからそろそろアップデートかけて.NET Frameworkのライブラリが全面的に使えるようになって欲すぃ(今は、極一部のは使えないことはないのですが相当微妙)。では実例など少し。
// フォルダ下のフォルダ名とファイル名を取得する
var dir = WScript.CreateObject("Scripting.FileSystemObject").GetFolder("C:\\");
// 通常の場合
var itemNames = [];
for (var e = new Enumerator(dir.SubFolders); !e.atEnd(); e.moveNext())
{
itemNames.push(e.item().Name);
}
for (var e = new Enumerator(dir.Files); !e.atEnd(); e.moveNext())
{
itemNames.push(e.item().Name);
}
// linq.js
var itemNames2 = Enumerable.From(dir.SubFolders)
.Concat(dir.Files)
.Select("$.Name")
.ToArray();
WSHでJScriptを使う場合の最大の欠点は、foreachがないこと、でしょうか。生のEnumeratorを回すのは苦痛でしかない。しかし、linq.jsを用いればFrom.ForEachで簡単に列挙できます。それだけでなく、Linqの多様なメソッドを用いて自然なコレクション操作が可能です。上記例では、Concatでファイル一覧とサブフォルダ一覧を連結することで、名前の取り出しを共通化することができました。
WSHに関して詳しくはウェブで、じゃなくて後日、と言いたいのですが(既に記事がかなり長いので)、そんなこと言ってるといつまでたっても書かなさそうなので、簡単に説明を。WSH(WindowsScriptHost)とはWindows用のスクリプト環境で、自動化など、非常に強力で大抵のことが出来ます。で、標準ではVBScriptと、JScriptで書けます。つまりJavaScriptで書ける。つまりlinq.jsが読み込める。つまりLinqがWSHでも使える。WSHで扱うあらゆるコレクションに対して、Linqを適用させることが可能です。WSHだけでなく、Windows上でJScriptを用いるもの、例えばWindowsのデスクトップガジェットなどでも利用することが出来ます。
ライブラリを用いる場合は、wsfファイルで記述すると良いかもです。中身は簡単なXMLで、下のような感じに。
<job id="Main">
<script language="JScript" src="linq.js"></script>
<script language="JScript">
function EnumerateLines(filePath)
{
return Enumerable.RepeatWithFinalize(
function () { return WScript.CreateObject("Scripting.FileSystemObject").OpenTextFile(filePath) },
function (ts) { ts.Close() })
.TakeWhile(function (ts) { return !ts.AtEndOfStream })
.Select(function (ts) { return ts.ReadLine() });
}
EnumerateLines("C:\\test.txt").Take(10).ForEach(function (s)
{
WScript.Echo(s);
});
</script>
</job>
そうそう、自動Closeも含めたリソース管理も出来ます。ストリームなどにはRepeatWithFinalizeを使ってLinq化することで、Closeまで一本にまとめあげられます。といったような複雑なことが必要なくても、E.From(collection).ForEach()がふつーに便利です。というか、こんな単純なことすら素の状態じゃ出来ないJScriptに絶望した!でもそれがいい。JScript可愛いよJScript。
エディタマクロ
WSHによるJavaScript実行はEmEditorやサクラエディタ、Meryなど様々なテキストエディタで利用できます。せっかくなのでちょっと実用的なものを、と思って電卓を作ってみました。EmEditor用マクロです。選択範囲内の一行を計算し、その行の右側に出力します。ちゃちゃっと計算出来て便利。計算し直しを頻繁にする時は、新規ファイルを開いてまっさらなものに数式を書いて、Ctrl+A -> マクロ実行を繰り返すと良い感じ。
#include = "linq.js"
with (document.Selection)
{
Enumerable
.RangeTo(GetTopPointY(eePosLogical), GetBottomPointY(eePosLogical))
.Select(function($i)
{
var $line = document.GetLine($i);
try { eval($line) } catch ($e) { } // SetVariableToGlobal
var $expr = $line.split("=")[0].replace(/^ +| +$/g, "");
try { var $result = eval($expr) } catch ($e) { }
return { PointY: $i, Result: $result, Line: $expr }
})
.Where(function(a) { return a.Result !== undefined })
.ForEach(function(a)
{
SetActivePoint(eePosLogical, 1, a.PointY, false)
SelectLine(); Delete();
document.writeln(a.Line + " = " + a.Result);
SelectLine(); UnIndent(); StartOfLine();
});
}
一行のうち=の左側をevalして、戻り値を返すものは出力、evalに失敗したものはスルー。ということなので、JavaScriptのMath関数は全部使えます。Select内の変数が$iとか、$始まりなのは変数定義による名前の衝突を避けるためです。慣れないマクロ書きなので色々酷い箇所も多いとは思いますが、なんとなく伝わるでしょうか? テキストエディタのマクロは、一行ずつの配列として、一行に対して操作をかけることが多いので、RangeToで範囲を作って、行ごとに適当に変形(Select)させて例外条件を省いて(Where)、処理(ForEach)。煩雑になりがちな行番号管理などを、流れるように簡単に記述出来ます。
なお、EmEditorは先頭に#includeを記述するだけでライブラリ読み込みが出来ますが、インクルード機能のないエディタでは以下のようなコードを先頭に置くことでライブラリが読み込めます。
eval(new ActiveXObject("Scripting.FileSystemObject").OpenTextFile("linq.js").ReadAll().replace(/^・ソ/,""));
ようするに、丸ごとテキストとして読み込んでevalです。/^・ソ/はUTF-8のBOM対策(適当すぎる)。一応、Meryでlinq.jsが動くのは確認しました。電卓マクロは動きません(eePosLogicalとかがEmEditorにしかないので、まあ、その辺は当然だししょうがないわなあ)
まとめ
今回のリニューアルは大変気合が入ってます。コード全部作り替えたぐらいには。最初はjQueryプラグインだけ足せばいいや、とか思ってたのですが、破壊的変更をかけるなら中途半端はいくない、と思い、やれるのは今だけということで徹底的に作り替えました。
ちなみに、ちっとも気にしてないパフォーマンスは悪化しました。ver.1.xがそれでもまだシンプルな構造だったのに比べ、今回は一回の呼び出し階層がかなり深くなった影響があります。列挙終了後にRepeatWithFinalizeとFinallyでしか使ってないDisposeのために階層駆け上がるし(これほんと入れようか悩んだんですけど、WSHだけでなく、将来的にはJavaScriptでもきっちりリソース管理でCloseって機会も増えそうなので入れました)。しかし遅いといっても、ベンチ取るとChromeなら爆速で誤差範囲に収まってしまうのですよ!Google Chrome、恐ろしい子。Firefoxだと?聞くな。いえいえ、JSINQよりは速かったですよ?←何だこの対抗心は
jQueryのおともに。WSHのおともに。エディタマクロのおともに。調度良い感じの隙間にピタリはまるようになっております。JavaScriptにおいてコレクションライブラリはニッチ需要だし、WSH用ライブラリなんて完全隙間産業なわけですが、むしろだからこそ、幾分か価値があるかな?合計83メソッドと、大充実なので、きっと要求に答えられると思います。
linq.jsをjQueryと一緒に使う
- 2010-04-15
linq.jsとjQueryを一緒に使うとするとどうなるのかな、という今まで微妙に避けてきた話。実用的なことを考えるなら避けて通れないのですが、実用的なことなんて考えたこともなかった!今まで!すみません。というわけで、少し考えてみます。あ、Linq to Xml(linq.xml.js)はボツの方向で行きます。RxJSを見習って、jQueryと仲良くやる方向で考えるつもりです。割とイイ線行ってたかなあ、とは思うんですが、クロスブラウザ対応の面倒くささが半端なくて実装しきる気力が持ちそうにないのと、やっぱセレクタ操作が冗長になりすぎてダメだったかなあ、なんて思ってます。
```javascript var p_arr = from p_ele in $("p") where "hoge" in p_ele.classes select p_elefor(var p in p_arr){ p.toggleClass("hilight"); }
こんな風なことができるのかと思ったがそんなわけはなく。<br /> <a href="http://d.hatena.ne.jp/beta_magnus/20100412/1271070326">Linq.js - 人生がベータ版</a> </blockquote> お試しありがとうございます。この例は、出来るといえば出来るし、出来ないといえば出来ないです。まず、クエリ構文では書けなくて、というのはともかくメソッド構文で実際にlinq.jsで組んでみるとこうなります。 ```javascript E.From($("p")) // 内包するDOM要素を列挙 .Select("jQuery($)") // 単一要素のjQueryオブジェクトにラップ .Where(function (q) { return q.attr("class") == "hoge" }) .ForEach(function (q) { q.toggleClass("hilight") }); // jQueryで同じような形にするならこうでしょうか $("p.hoge").each(function () { var q = $(this); q.toggleClass("hilight"); });
FromでjQueryの選択された要素を列挙に変換します。jQueryの列挙はjQueryオブジェクトではなく、DOM要素をそのまま返すので(これは.eachで回した時も一緒ですね)、jQueryのメソッドを使いたい時は再度ラップしてあげます。ただまあ、単純なフィルタリングなら、jQueryのセレクタで書いたほうが当然すっきり仕上がりますね。更にまあ、そもそもeachする必要はなく、
// jQuery自体が集合を扱うので一行で書けるんですよね…… $("p.hoge").toggleClass("hilight");
となってしまうわけで、DOMから選択してDOMに作用を加える、という一連のjQueryチェーンで綺麗に成り立っている場合に、linq.jsを挟む余地はありません。jQuery自体が集合を含有しているので、jQueryだけで綺麗に完結しちゃうんですね。そしてlinq.jsは、それ自体はクロスブラウザ対応だったりのDOM操作は一切持っていないので、その辺はjQueryにお任せします。そうなると、どうしても出番が限られてくるという。
じゃあ無価値なのかといえば勿論そんなことはなくて、DOMをDOMのまま扱って抽出して作用を加えるのではなく、そこからテキストだったり数値を抽出する、というシーンでは生き生きとします。DOMの選択、フィルタリングまではjQueryのセレクタで行ったらlinq.jsに渡す。数値だったら集計系のメソッドが使えるし、他にもテーブル内の文字をキーにして複数テーブルの結合(join)、なんかも簡単に出来ます。
ところで、 E.From($("selector")).Select("jQuery($)") というのは定型文になるので、jQuery自体に拡張してしまいます。こんなんでもプラグインって呼んでいいですか?(プラグインとしての部分は一行でも、繋がってる先はある意味ヘビー級なので)
// これでjQueryオブジェクトからtoEnumerable()を呼ぶとlinqが使える jQuery.fn.toEnumerable = function () { return E.From(this).Select("jQuery($)"); } // 例えば、input要素の数値を合計する、とか var sum = $("input").toEnumerable() .Select("parseInt($.val())") .Sum();
割と便利。かな?具体例に乏しくて非常に説得力に欠ける感じですが、良ければ使ってやってください。