[C#] HtmlDom の実力を探る…つーかパフォーマンスが悪いけど

PP-Club メンバの美崎さんのライフハッカーの記事

SimpleStyle100回突破記念~編集部が選ぶ美崎薫氏のアプリ25選 : ライフハッカー[日本版]
http://www.lifehacker.jp/2012/05/120417misakihacks.html
プログラムのソースコードから変数を出力するヘルパー『makeMessagebox』(SimpleStyle第119回) : ライフハッカー[日本版]
http://www.lifehacker.jp/2012/08/12-08-30makemessageboxsimplestyle119_1.html

をなんとか活用できないかと、美崎さん当人に相談したところ「ライフハッカーの目次って用意されていないんですよね~ orz」の返事が orz ああ、目次ないんですね、これ。

一応、当人的には、http://www42.tok2.com/home/papermoon/ なところで、自動生成を試みているらしいのですが、ちょっとこれではアレすぎて全体の見通しが付きません。CSS で適当にレイアウトすればいいんでしょうが、自動生成の精度が悪いらしくというところだそうです。

で、本格的なところはライフラッカーへのクローリングか、google api を使うのがよいのですが、ひとまず、先のページからリンクとタイトルぐらいは抜き出して Excel で整理するところまではやりたいと思う訳ですよ。なので、手順としては、

  1. 該当するページを HTML ファイルで「手動」で保存。
  2. HtmlDom を使って、リンクとタイトルを抽出する。
  3. CSVファイルへ書き出し。
  4. Excel で開いてファンクションを使って適当に整形

「手動」なところが混じっていますが、全体の作業量を考えるとなにも全体を「自動化」する必要がありませんッ!!! ってのが PP-Club の信条でして(つーか、私自身の信条)、リンクとタイトルだけを取り出せばよいわけです。

元の HTML ソース

抽出後のソース

Excel に貼りつけ

とする訳です。
実は元の HTML ソースは、

  • タイトルが重複している
  • 別のリンクも含まれている

な感じで、ちょっとプログラム的にはややこしいのです。そう、トライ&エラーが必要なのですね。
…が、HtmlDom を使うと、これぐらい簡単に書けます。

static void Main(string[] args)
{
	string path = "\\temp\\simplestyle_idx.html";

	var doc = new HtmlDocument().Load(path);
	// Aタグで「SimpleStyle 第??回」を含むものを取得
	var cont = new HtmlNavigator(doc)
		.Where(n => n.TagName == "a" &&
			n.Value.Contains("SimpleStyle 第"))
		.Select(n => new { url = n % "href", text = n.Value })
		.Distinct();

	foreach (var n in cont)
	{
		Console.WriteLine("{0}\t{1}",
			n.url, n.text);
	}
}

まあ、これも一発で書けたわけではなくて何度か書き直している訳ですが、これぐらい短いとちまちまと設計を書いているよりも何かとコードを書いたほうが早い訳です。ええ、当然「設計」が必要な場合もあるので、それの話はまた別のときに。

HtmlDom は github で
https://github.com/moonmile/XmlDom

ちなみに、ライフハッカーの SimpleStyle の目次はもうちょっと考えます。元ソース自体が、第87回までしかないのでちょっと古いので。今は120回位です。

ちなみに、現状の HtmlDom は使い物にならない位遅いので、現在パフォーマンスを調節中です。どうやら、COM -> .NET 変換のところが遅すぎるみたいなので、C++/CLI で書き直せばよいかと模索中。

カテゴリー: C#, PP-Club, XmlDom | 1件のコメント

[C++] レガシーライブラリをクラスで包んで再利用する

手元にあるものがレガシー(遺産)と言う訳ではないのですが、15年程まえの技術で作ってあるので、これに機能を追加するのに苦労しています。
まあ、業務的には「元のコードスタイルに合わせてコーディングする」のが良い訳で、そこそこ手を加える量が少なければそれでも良いのですが、がっつりとコーディングしないといけないとなると、ひと苦労ってことになります。

そうなると、レガシーならばレガシーなりに捨て去ってしまう、というのもひとつの選択肢に上がるわけですが、現実的にはそうも言えません。予算的な問題とか、実現可能性の問題などがありもともとのコードを継承しないと駄目なわけです。COBOL やホストコンピュータの世界では普通に起こっている話で、ぼちぼち10年前のコード自体も辛くなってきていますよね、って話です。そういう分野の「考察」も必要かと思い始めていますが、そのあたりは別の機会に。

さて、既存のデータが「単純な構造体」と「ビット配列」で出来ていたとします。機能追加をするときには、このデータにアクセスしなければいけないのですが、ばらばらに保存されている「単純な構造体」と「ビット配列」はちょっとアクセスが手間です。あらかじめクラス構造になっていればよいのですが、諸々の事情があってそうもいかなかった模様です。

■データアクセスを #define マクロで包む

ひとつの方法としては「#define」マクロを使ってデータアクセスを包みます。

// conbiniance macro function
#define GRID_MAX()		GETNTGRID()
#define GRID_ID(_idx)		CPLOT2.GPTS[_idx][0]
#define GRID_X(_idx)		CPLOT2.GPTS[_idx][1]
#define GRID_Y(_idx)		CPLOT2.GPTS[_idx][2]
#define GRID_Z(_idx)		CPLOT2.GPTS[_idx][3]
#define TO_GRID_INDEX(_gid)	(_gid-1)

#define CQUAD4_MAX()	GETNTELEM()
static bool IS_CQUAD4(long index) {
	long n = index+1;
	int type = GETELETYPE(&n);
	return ( type == 5 || type == 6 );
}
#define CQUAD4_EID(_idx)	CPLOT2.IDCELM[_idx][4]
#define CQUAD4_GID1(_idx)	CPLOT2.IDCELM[_idx][0]
#define CQUAD4_GID2(_idx)	CPLOT2.IDCELM[_idx][1]
#define CQUAD4_GID3(_idx)	CPLOT2.IDCELM[_idx][2]
#define CQUAD4_GID4(_idx)	CPLOT2.IDCELM[_idx][3]
#define CQUAD4_PID(_idx)	CPLOT2.IDCELM[_idx][5]
#define CQUAD4_K(_idx)	CPLOT2.IDCELM[_idx][5]

見ると分かりますが、CPLOT2.GPTS と CPLOT2.IDCELM は単純な配列です。データ自体は Fortran 領域にあるので「配列」でアクセスしないと難しいという当時の理由がありました。これを直接アクセスするのは大変なので、#define マクロを作ってみたわけです。

これは結構定番な方法なのですが、関数アクセス風になってしまうのが難点です。アクセスする配列が増えるたびに、何らかの命名規約を作ってひとつひとつマクロを作らないといけません。作るのも面倒ですし、アクセスの方法も面倒です。

int idx   = 10;
int eid   = CQUAD4_EID(idx);
int gid1  = CQUAD4_GID1(idx);
int gid2  = CQUAD4_GID2(idx);
...

分かり易いと言えば分かり易いのですが、オブジェクト指向的には次のように書きたいのです。

int idx = 10;
int eid = CQUAD4(idx).EID;
int gid1 = CQUAD4(idx).GID(0);
int gid2 = CQUAD4(idx).GID(1);
...

更に参照を使って、こんな風に書きたい訳です。

int idx = 10;
auto &el = CQUAD4(idx);
int eid  = el.EID;
int gid1 = el.GID(0);
int gid2 = el.GID(1);
...

■データアクセスを一時オブジェクトを使って包む

C# の場合は、別に CQUAD4 クラスを作成してひとつひとつ作り込むことになるのですが、C++ の場合はもうちょっと便利な方法があります。
基本的に一時オブジェクト扱いで使うようにしてスコープが外れると自動的に解放される、という前提でクラスを作ります。C# の場合は、using を使うところですね。

///<summary>
/// CPLOT2.GPTS easy access class
///</summary>
class GRID {
protected:
	int _idx;
public:
	GRID(int idx) { _idx = idx; }
public:
	int GID() { return CPLOT2.GPTS[_idx][0]; }
	double X()  { return CPLOT2.GPTS[_idx][1]; }
	double Y()  { return CPLOT2.GPTS[_idx][2]; }
	double Z()  { return CPLOT2.GPTS[_idx][3]; }
	static int Size() { return GETNTGRID(); }
};

///<summary>
/// CPLOT2.IDCELM easy access class
///</summary>
class CQUAD4 {
protected:
	int _idx;
public:
	CQUAD4(int idx) { _idx = idx; }
public:
	int EID() { return CPLOT2.IDCELM[_idx][4]; }
	int GID(int n) { return CPLOT2.IDCELM[_idx][n]; }
	int PID() { return CPLOT2.IDCELM[_idx][5]; }
	int K() { return CPLOT2.IDCELM[_idx][5]; }
	static int Size() { return GETNTELEM(); }
};

それぞれのメソッドは #define マクロの時と変わりません。

int idx = 10;
auto &el = CQUAD4(idx);
int eid  = el.EID();
int gid1 = el.GID(0);
int gid2 = el.GID(1);
...

一見、プロパティのように見えますがメンバアクセスのメソッドです。実は、VC++の場合は独自にプロパティが作れるのですが、今回はやめておきます。プロパティを作りたい場合は、下記な記事で説明してあります。

MS-C++ では __declspec(property()) でプロパティを作れるよ | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/3557

内部的に一時オブジェクトが作られるのでスピードが問題ですが、まぁ、そのあたりは大丈夫みたいです。最近の CPU は早いですから。

カテゴリー: C++ | [C++] レガシーライブラリをクラスで包んで再利用する はコメントを受け付けていません

[C++] HtmlDom のパース部分を C++ で書き直す

HtmlDom は LINQ to HTML を目指していますが、かつ HTML が楽に編集できるように更新系(Update/Delete/Insertなど)のメソッドも準備します。

まあ、内部的には XML に直しているので操作は楽なのですが、なんと HTML のパース部分がちと面倒で。

もともとある System.Forms.HtmlDocument 自体には、Children に相当するコレクションがないので、全 DOM を取ることができないんですよね。

HtmlDocument クラス (System.Windows.Forms)
http://msdn.microsoft.com/ja-jp/library/system.windows.forms.htmldocument(v=vs.110).aspx

トリッキーな作りをすれば、これに沿って LINQ ぐらいは作れるのですが、ちょっと使いづらいということで、独自に HtmlDocument, HtmlNode を作っています。
このとき、HTML 文字列から COM の IHTMLDocument2 を使ってパースするのはこんな感じ。

/// <summary>
/// Loading method
/// HtmlDocument to create a HTML string
/// </summary>
/// <param name=&quot;html&quot;>HTML string</param>
/// <returns></returns>
public HtmlDocument LoadHtml(string html)
{
	// Creating an object using a mshtml.HTMLDocument
	var doc = new HTMLDocument() as IHTMLDocument2;
	doc.write(new object[] { html });
	Load(doc);
	return this;
}

非常に簡単で、IHTMLDocument2 インターフェースにキャストして、COM の write メソッドを呼び出すだけです。これは HTML DOM の document.write に対応しているので、javascript まで実行されてしまうのですが、まぁ、大丈夫みたいです。何故か、COM で直接呼び出した時は、javascript 実行でエラーになる(画面のUIコントロールを探してエラーになるという不具合のようです)らしいのですが、実は大丈夫です。

	CComPtr<IHTMLDocument2> pDoc;
	HRESULT hr = CoCreateInstance(CLSID_HTMLDocument, NULL, CLSCTX_INPROC_SERVER, IID_IHTMLDocument2, (void**)&pDoc);
	//put the code into SAFEARRAY and write it into document
	SAFEARRAY* psa = SafeArrayCreateVector(VT_VARIANT, 0, 1);
	VARIANT *param;
	hr = SafeArrayAccessData(psa, (LPVOID*)&param);
	param->vt = VT_BSTR;
	param->bstrVal = CComBSTR(strHTMLCode).Copy();
	hr = pDoc->write(psa);
	hr = pDoc->close();
	SafeArrayDestroy(psa);

ATL COM を使っています。どっかのサンプルから取ってきたので、実は SafeArrayCreateVector は必要ないかもしれません。サンプルを動作させるとエラーになっていたのですが「CComBSTR(strHTMLCode).Copy();」のように、一度コピーを取ることで、うまく実行できます。ためしに、自分の twitter サイトから HTML をダウンロードして来てパースするとうまく動きました。twitter サイトは javascript を多用しているので、これが大丈夫ならば大抵のサイトは大丈夫だと思います。

肝心のスピードですが、C# 経由の COM アクセスは何故か属性の配列を取るところが非常に遅くなっています。

	// append attributes
	IHTMLAttributeCollection attrs = node.attributes;
	if (attrs != null)
	{
		foreach (IHTMLDOMAttribute at in attrs)
		{
			if (at.specified)
			{
				string nodeValue = "";
				if (at.nodeValue != null)
					nodeValue = at.nodeValue.ToString();
				nn.Attrs.Add(new HtmlAttr { Key = at.nodeName, Value = nodeValue });
			}
		}
	}

部分的なコードですが、foreach のループのところで、attrs の要素を 150 程度廻るのが問題なようです。実は、IHTMLDocument がパースした後の要素では、何故か属性のコレクションが非常にたくさん用意されているのですよね。おそらく onclick などのフック関数のために用意されていると思うのですが、静的なデータを取りたい場合には、これが不要ですし非常に邪魔です。要素数が2,3しかないので、150程度のループを廻すものだから、耐えきれない位おそくなります。
これは、簡単な HTML の場合には問題がなくて、twitter の HTML のように大量な HTML の場合に発覚した現象です。これでは実用に耐えません。

なので、じゃあ、COM アクセス自体を高速化するためにひとまず、C++ で書いてみたのが以下です。

list<XAttr*> *getAttrs( CComQIPtr<IHTMLDOMNode> node )
{
	auto *xattrs = new list<XAttr*>();

	CComPtr<IDispatch> disp;
	node->get_attributes( &disp );
	CComQIPtr<IHTMLAttributeCollection> attrs = disp;
	if ( attrs ) {
		long length = 0;
		attrs->get_length( &length );
		CComPtr<IDispatch> dispa;
		for ( int i=0; i<length; i++ ) {
			CComVariant vt(i);
			attrs->item( &vt, &dispa );
			CComQIPtr<IHTMLDOMAttribute> attr = dispa ;
			if ( attr ) {
				VARIANT_BOOL vtb;
				attr->get_specified( &vtb );
				if ( vtb ) {
					CComBSTR key;
					attr->get_nodeName( &key );
					CComVariant value;
					attr->get_nodeValue( &value );

					xattrs->push_back(new XAttr( CString(key), CString(value.bstrVal)));
				}
			}
			attr.Release();
			dispa.Release();
		}
	}
	attrs.Release();
	disp.Release();
	return xattrs;
}

ループ変数となる length の値は 150 程度なので同じぐらいループが廻っていますが、非常に高速に動きます。多分、CComBSTR か CComVariant と .NET との相互変換の部分で遅くなっている感じがします。これは後で実測してみるつもりです。

という訳で、IE で使っている IHTMLDocument2 を直接使って自前で DOM を作ることができました。
ですが、このパース部分は C++ となっているので、C# の HtmlNode オブジェクトにしないと LINQ が使えないですよね。

って訳で、C++/CLI の出番なんですよ。ええ、VS2010 では C++/CLI のインテリセンスが効かないので、VS2012 で書いたものを VS2010 に戻しますってな感じです。本当は 2008 が良いんですが、間違ってアンインストールしちゃったんですよね。なので、仕方が無く別マシンの VS2012 を借りるという罰ゲームに(VS2010 に VS2012 を入れると MSTest が正常に動かないんですが、これは RTM でなおっているんでしょうか?)

 

カテゴリー: C++ | 1件のコメント

[C#] 何故 enum に拘りたくなるのか?

enum 値に任意の名称やその他の情報を保持する方法について | Masa’s Lab
http://blog.masa1115.com/?p=1062
どうして enum に拘ってるのか… | Masa’s Lab
http://blog.masa1115.com/?p=1078

のところをざっと見て、私なりに考えると、

使ってはいけない定数定義の一例 | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/2589

でも書いていて…ないか、もっと別なところかもしれませんが、基本はC言語の「#define」と「enum」の違いからです。

■#define は数値、enum は型

#define WEEKDAY_SUN 0
#define WEEKDAY_MON 1
#define WEEKDAY_TUE 2
...

enum WEEKDAY {
	SUN = 0,
	MON,
	TUE,
...
};

の違いは、#define のほうは単なる置き換えなので数値あるいは文字列として扱うのですが、enum は「enum WEEKDAY」という型になります。なので、コンパイルするときに、enum WEEKDAY 型にしないとエラーになるという便利な(つーか大抵の場合は面倒くさい)ものがあります。まあ、面倒なので、int 型として比較してしまいますがね。

C# の場合は、これを継承して

public enum WEEKDAY {
	SUN = 0,
	MON,
	TUE,
}

と書けます。当時 Java で書こうとすると

class WEEKDAY
{
	public static final int SUN = 0;
	public static final int MON = 1;
	public static final int TUE = 2;
}

あたりでげんなりしてまったのですが…まあ、最近のjava2 では enum があるようです。げんなりしていたのは、2000年前後ですね。

そうそう、C# の時になぜ「文字列」や別の型を許さなかったのか?と疑問なのですが、よくわかりませんね。string が使えると便利なのに。

public enum WEEKDAY {
	SUN = "Sunday",
	MON = "Monday",
	TUE = "Tuesday",
}

リフレクションがあるから大丈夫?ってことなんでしょうか?

■enumを使わずに実装する

「WeekDay.SUN」という値に、数値と英単語と日本語の3つを割り当てようとすると、なかなか大変なので、大抵は下記のようになります。

/// <summary>
/// 列挙がいらない場合
/// </summary>
public class WeekDay 
{
	// プロパティ定義
	public int Value { get; set; }
	public string Name { get; set; }
	public string NameJa { get; set; }

	// enum の代わり
	static public WeekDay SUN { get { return new WeekDay(0, &quot;Sunday&quot;, &quot;日曜日&quot;); }}
	static public WeekDay MON { get { return new WeekDay(1, &quot;Monday&quot;, &quot;月曜日&quot;); }}
	static public WeekDay TUE { get { return new WeekDay(2, &quot;Tuesday&quot;, &quot;火曜日&quot;);}}
	static public WeekDay WED { get { return new WeekDay(3, &quot;Wednesday&quot;, &quot;水曜日&quot;); } }
	static public WeekDay THU { get { return new WeekDay(4, &quot;Thursday&quot;,&quot;木曜日&quot;);}}
	static public WeekDay FRI { get { return new WeekDay(5, &quot;Friday&quot;, &quot;金曜日&quot;); }}
	static public WeekDay SAT { get { return new WeekDay(6, &quot;Satday&quot;, &quot;土曜日&quot;); }}

	protected WeekDay(int value, string name, string nameJa)
	{
		this.Value = value;
		this.Name = name;
		this.NameJa = nameJa;
	}
}

/// <summary>
/// 列挙が必要な場合はリストを作る
/// </summary>
public class WeekDayList : List<WeekDay>
{
	protected WeekDayList() { }

	static public WeekDayList Create()
	{
		var lst = new WeekDayList();
			
		lst.Add(WeekDay.SUN);
		lst.Add(WeekDay.MON);
		lst.Add(WeekDay.TUE);
		lst.Add(WeekDay.WED);
		lst.Add(WeekDay.THU);
		lst.Add(WeekDay.FRI);
		lst.Add(WeekDay.SAT);
		return lst;
	}
	public static WeekDayList List
	{
		get { return WeekDayList.Create(); }
	}
}

foreach が使えるように WeekDayList を定義するかどうかは別なのですが、WeekDay クラスみたいなのをだらだらと作っていく訳です。
0,1,2 といったマジックナンバーを記述するのが嫌なのですが、これはデータベース上にあるマスターデータの定数などが入るため、enum を使っても似たような感じになるでしょう。

で、テストコードがこんな感じ。

	/// <summary>
	/// UnitTest1 の概要の説明
	/// </summary>
	[TestClass]
	public class UnitTest1
	{
		[TestMethod]
		public void TestNormal()
		{
			Assert.AreEqual(0, WeekDay.SUN.Value);
			Assert.AreEqual(&quot;Sunday&quot;, WeekDay.SUN.Name);
			Assert.AreEqual(&quot;日曜日&quot;, WeekDay.SUN.NameJa);
		}

		[TestMethod]
		public void TestNormalList()
		{
			var days = WeekDayList.Create();
			Assert.AreEqual(7, days.Count);
			// 最初の要素
			var day = days.GetEnumerator();
			day.MoveNext();
			Assert.AreEqual(&quot;日曜日&quot;, day.Current.NameJa);
		}

		[TestMethod]
		public void TestNormalList2()
		{
			// foreach で列挙
			string s = &quot;&quot;;
			foreach (var d in WeekDayList.List)
			{
				s += string.Format(&quot;{0}&quot;, d.NameJa[0]);
			}
			Assert.AreEqual(&quot;日月火水木金土&quot;, s);
		}
	}

プロパティで扱えるのと、foreach で取り出せるほうがよいかと。

難点を云えば「enum の代わり」でしかないので、諸々の機能は実装していく必要があります。こんなのが 2,30あると面倒臭くてやっていけないので、Excel VBA を使って自動生成すると楽です。ちなみに、以前の仕事では、Excel VBA を使ってデータベース仕様書から 1万行 の型付Listとenumを自動生成させました。

■enum を使って実装する

Masa さんのエントリーに準じて、enum に属性を付ける方法をやってみましょう。
enum に属性を付けて、適当な文字列をあらかじめ設定しておくというのは良くやる手段なのですが、どうやって取り出すのかが不明ですよね。確かに、以前、私も考えていたもののうまい方法がなくて断念していました。

で、先のエントリーで enum の拡張メソッドを使えばなんとかなる、ってことが分かったので以下ように実装します。

/// <summary>
/// Enum で実装する場合
/// </summary>
public enum WeekDayEnum
{
	[WeekDay(&quot;Sunday&quot;,&quot;日曜日&quot;)]	SUN  = 0,
	[WeekDay(&quot;Monday&quot;,&quot;月曜日&quot;)]	MON,
	[WeekDay(&quot;Tuesday&quot;,&quot;火曜日&quot;)]	TUE,
	[WeekDay(&quot;Wednesday&quot;,&quot;水曜日&quot;)]	WED,
	[WeekDay(&quot;Thursday&quot;,&quot;木曜日&quot;)]	THU,
	[WeekDay(&quot;Friday&quot;,&quot;金曜日&quot;)]	FRI,
	[WeekDay(&quot;Satday&quot;,&quot;土曜日&quot;)]	SAT,
}

/// <summary>
/// 属性設定
/// </summary>
public class WeekDayAttribute : Attribute
{
	internal protected static List<WeekDayAttribute> _lst = null;

	public string Name { get; set; }
	public string NameJa { get; set; }
	public WeekDayAttribute(string name, string nameJa)
	{
		this.Name = name;
		this.NameJa = nameJa;
	}
}

public static class WeekDayEnumExtentions
{
	public static int Value(this WeekDayEnum self)
	{
		return (int)self;
	}
	public static WeekDayAttribute GetCustomAttribute(WeekDayEnum self)
	{
		Type t = typeof(WeekDayEnum);
		var mi = t.GetMember(self.ToString())[0];
		return Attribute.GetCustomAttribute(mi, typeof(WeekDayAttribute)) as WeekDayAttribute;
	}
	public static string Name(this WeekDayEnum self)
	{
		return WeekDayEnumExtentions.GetCustomAttribute(self).Name;
	}
	public static string NameJa(this WeekDayEnum self)
	{
		return WeekDayEnumExtentions.GetCustomAttribute(self).NameJa;
	}
}

カスタム属性 WeekDayAttribute を作るのと、リフレクションを利用した WeekDayEnumExtentions による拡張メソッドです。
リフレクションのあたりのエラー処理をさぼっていますが、まぁ結構すんなりと書けたかと。これだけ短いと、修正するのも楽でしょう、多分。

で、テストコードはこんな感じ。

	[TestClass]
	public class UnitTest2
	{
		[TestMethod]
		public void TestNormal()
		{
			WeekDayEnum day = WeekDayEnum.MON;

			Assert.AreEqual(1,(int)day );
			Assert.AreEqual("Monday",day.Name());
			Assert.AreEqual("月曜日", day.NameJa());
		}

		[TestMethod]
		public void TestNormal2()
		{
			Assert.AreEqual("Monday", WeekDayEnum.MON.Name());
			Assert.AreEqual("月曜日", WeekDayEnum.MON.NameJa());
		}

		[TestMethod]
		public void TestNormalList2()
		{
			// foreach で列挙
			string s = "";
			foreach (WeekDayEnum d in Enum.GetValues(typeof(WeekDayEnum)))
			{
				s += string.Format("{0}", d.NameJa()[0]);
			}
			Assert.AreEqual("日月火水木金土", s);
		}
	}

基本的に一緒なんですが、「拡張メソッド」なので「Name()」や「NameJa()」のように括弧をつけないといけないのいまいちですね。ってのは、Masa さんに同感。

カテゴリー: C# | [C#] 何故 enum に拘りたくなるのか? はコメントを受け付けていません

そろそろ Metro Design について考えてみよう

どうやら「Metro UI」が「Modarn UI」に切り替わってきているので、ワタクシとしてははれて Metro Design について再考してみようということで、少し書き下し。

東京メトロ UI | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/2907

なところで書きましたが…って書いてないか。Community Open Day 2012 だったか直前の.NETラボ勉強会でちらっと話しましたが「Metro UI」もとのコンセプトは地下鉄の標識デザインの「Metro」です。なので、Subway UI でもよいし、Tube UI でも良かったのですが、Metro UI ということになった模様です。この Metro UI

Japanese Dee//Party and Bullshit//

2010年10月のブログ|Japanese Dee//Party and Bullshit// より拝借

や、東京メトロの標識を見ると分かりますが、「単色に白抜き文字」あるいは「単色に黒文字」が普通です。文字に白を使うか黒を使うかは、背景に使われる色の明度により決まるわけです。東京メトロの路線図をみると、表示されている線の色と路線で使われているマークの色が共通になっています。

また、色自体は「白っぽいパステルカラー」ではなく「原色でもなく」、東京メトロの少し明度の高い淡い色を使って統一されています。ニューヨークの地下鉄の場合はビビッドめな色を使っていますが、すくなくとも全体のカラーは統一されているわけです。

さて、当時の Metro UI では盛んに「タイリング」が協調されていましたが(多分、Modarn UI もそうなると思いますが)、元ネタの Metro Design を見るとわかるように、特にタイリングを気にしているわけではありません。いや、格子状の配置を気にしてはいるものの、それはグリッドシステムを使っているデザインパターンがあります。

道路標識をあらためて見ると分かりますが、縦横の補助線を入れると確かにグリッド(格子状)になっています。ただし、デザインの本を読むと分かるのですが、文字によっては横幅を調節しないと、「きちんと格子状に見えない」という調節が必要になります。つまり、単純に格子状に文字を配置したからといって、文字が格子状に見えない。だから、左右の空白を調節して格子状でなくすることによって、格子状に見えるようにする。という矛盾を抱えています。

これを、Microsoft 社が出す、デザインガイドラインに当てはめると「文字や図形のマージンを固定値に取る」ことによって「格子状に配置する」ことを目指していますが、実際作ってみると「格子状には見えない」という現象が発生するのです。そうなると、美しいデザインするためには、あえて「格子状に配置をしない」という工夫が必要なわけです。ジレンマですね(苦笑)。

格子状(グリッド)の問題についてはさておき、おそらく Microsoft 社としては Metro UI の裏の指針を以下のようにすべきだったと思っています。

  • 格子状の配置は、解像度やディスプレイサイズが異なる環境において、高速に再配置が可能である。
  • Metro UI 風のアイコンやサムネール画像は、高速に描画するために重要である。
  • Apple の 3D デザインと、意匠特許の面で将来的に争わずに済む。

の3点です。

■グリッドの配置は、高速に再配置が可能

格子状に画像や文章を配置すると、ブラウザの文章の配置のように再配置が非常に高速にできます。CSS の margin や padding のように外側と内側の空白をあらかじめ準備しておくことによって、位置が変わっても計算が楽です。もちろん、スプライン曲線に文字を沿わせるなどのデザインテクニックもあるのですが、機械的に「綺麗に見えるような配置」をするためには、grid にしてしまう方がコーディングもスピードも楽なのです。

そのあたりのデザインを省力化するためにグリッドの配置は必要、ということですね。これは Mordarn UI にも当てはまることです。ただし、先の「格子状の配置のジレンマ」で示したように、格子状に配置したからといって「美しいデザイン」になるとは限らないので、そのあたり、デザインとしては一歩も二歩もダサいデザインになってしまいます。逆に言えば、他の modarn application と差別化するためには、「格子状に配置しているように見えるけれども、実は格子状ではない」というタイポグラフィのデザインテクニックを使うと良いでしょう。

■アイコンやサムネール画像を単色化する

かつて Aero と言う「半透明な」ウィンドウスタイルを Microsoft 社は「美しい」ものとしていましたが、現在は Mordan UI の単色のアイコンを「美しい」と言っています。「美しい」の定義はさておき、Apple が使う 3D & グラス処理は確かに「美しい」アイコンを作りますし、作業に適したアイコンとなっています。モノクロ時代の Classic Apple も良いのですが、まあ「クリエイティブ」な感じがすることは確かです。

この 3D とグラス処理、角が丸い処理のために Apple の画像処理は非常にコストをかけています。おそらく、計算で行っているのではなくて内部的にいくつかのパターンを持つという荒業をしているような気がしますが、まぁ、高速に動きます。すごいです。実は、スライドや回転については、一見「高速」に動いているように見えますが、実は違います。このあたりは、「高速に見える」ことを重視している Apple らしいです。

さて、Microsoft 社の Windows の場合は、Apple ほど高速には動きません。これは技術力の問題ではなくて、Apple の場合は、iPhone などのハードウェアに特化したコードを書けばよいのですが、Microsoft の場合は、各社の画像ボードの性能に依存するという問題があるためです。このため、非力なボードを使っていたり、グラフィックボードのドライバーがいまいちだったりすると、このグラス処理を高速におこなうことができません。

そこで、ひとつの方法として、アイコンやサムネール画像を「単色」あるいは「減色」してしまうことで低機能なグラフィックボードであっても高速に表示ができます。グラス処理や 3D 動作、角がまるい処理も外してしまえば、どんどん高速になるはずです。また、色を減らせば画像サイズも減るわけですから、インターネット上から直接ダウンロードしたり HDD から読み込んだりするスピードも速くなる(実際は、大量な小さなファイルは遅くなるのですが)可能性があります。画像をメモリ上に置いてもサイズが小さくなりますね。

という訳で、単色パターンにすると Windows のスタートメニューやアプリメニューなどの動作が早くなるハズなのです。が、まぁ、実際は XAML の描画ルーチンが駄目なまま(おそらくGDI+のままかと)なので、全然高速ではありませんがね。

■アイコンの意匠問題を避ける

アイコンの 3D 化はフルカラー表示、グラス表示を続けると、おそらく Apple と Microsoft のアイコンは非常に似通ってしまいます。Android のアイコンを見て分かる通り、極めて iPhone のアイコンに似ています(似せているというもあるけど)。また、Linux の Desktop アプリも Windows アイコンに似てますよね(これも似せているというもあるけど)。

アイコンや画像の意匠に関しては、デザイナの問題もありますが、OS を提供する側としては「Apple のデザインに似る」というのは避けたいところです。Window システム自体が Apple の真似なんだからどうでもよかろう、MS-DOS 自体が Unix の真似なんだから、今更。という感も無くもないのですが、だんだんとパイが固定になっている OS 業界で「意匠」に引っ掛かるのは避けたいところなのです。

となれば、Apple が提唱する 3D & グラス路線、とは真っ向から対立する「単色アイコン & グリッド配置」をキャンペーン化してしまえばよいのです。デザインの路線としては、「Metro Design」があるので、これが「実に読みやすい、コンテンツ主導のデザインである」と宣伝すればよいわけです。まあ、けばけばしい看板よりは、コンテンツが読みやすいでしょうが、

Apple の 3D & グラス路線も別に「コンテンツを侵害している」訳ではないんですけどね。むしろ、Windows XP のおもちゃのようなデザイン/色彩感覚が、創造力をげんなりさせていた訳で、Visual Studio のツールバーのアイコン表示はやっとこさ VS 2008 あたりで完成されたかと思いきや(ぱっとみて、どの機能か分かる位「道具化」している)、VS 2012 になって単色系アイコンになり「道具」を取り去られた感じなんですけどね。Visual Studio のリソースを切り替えるツールでも作りますか(真面目に)。

それはともかく、3D & グラス路線を目指さなくなったので、アイコンデザインとしては Apple と意匠戦争にはならなくなります。また「これが Microsoft 社が提供する次世代のデザインだ」と定義することで、自分がトップに立てる(自分で土俵を作ったからね、当たり前)というおまけもついてきます。一応、Modarn UI の前哨戦としては成功しているみたいですね。Google アナリシスの UI も Modarn UI 風にカラーが変わっていましたから。github は、だいぶ前に追随したし。

そんな訳で、Metro UI についてぼちぼちと裏側を推測してみましたが、じゃあ本来の Metro Desgin の場合はどうなるのか?っていうのは、また後日。

カテゴリー: windows 8 | 1件のコメント

[C++] 試しにデクリメント付きの bool 型を実装してみる

VC++2010 の bool 値の動きが、false -> true -> true なのだから、それに合わせてデクリメントを作ってみる。

class BOOL
{
	bool _b ;	// 内部でブール値を持つwww
public:
	BOOL( bool b = false ) {
		_b = b;
	}
	// bool型へキャスト
	operator bool() {
		return _b ;
	}
	// bool型から代入
	BOOL& operator =( bool b ) {
		_b = b;
		return *this;
	}
	// 前置インクリメント
	BOOL& operator ++() {
		_b = true;
		return *this;
	}
	// 後置インクリメント
	BOOL operator ++(int) {
		BOOL b; b._b = _b;
		_b = true;
		return b;
	}
	// 前置デクリメント
	BOOL& operator --() {
		_b = false;
		return *this;
	}
	// 後置デクリメント
	BOOL operator --(int) {
		BOOL b; b._b = _b;
		_b = false;
		return b;
	}
};

内部的に、bool 値を持っているのは GAG というかなんというか、意外と詰まらないコードになってしまいました。
最初は b |= 1; b &= 0; なことをやるつもりだったのですが、実は 0/1 しか値を取らないのだから、直接 false/ture を入れてしまえば良いわけです。

■結果

	// デクリメントが可能なbool値
	BOOL bl = false;
	cout << ++bl << endl;
	cout << --bl << endl;
カテゴリー: C++ | [C++] 試しにデクリメント付きの bool 型を実装してみる はコメントを受け付けていません

[C++] bool値をインクリメントすると、ture/false を繰り返す理由…をこじつける

bool値をインクリメントする……? – Togetter
http://togetter.com/li/356718
c++ – bool operator ++ and — – Stack Overflow
http://stackoverflow.com/questions/3450420/bool-operator-and

3.2 Increment and decrement [expr.pre.incr]
1 The operand of prefix ++ is modified by adding 1, or set to true if it is bool (this use is deprecated).
The operand shall be a modifiable lvalue. The type of the operand shall be an arithmetic type or a pointer
to a completely-defined object type. The value is the new value of the operand; it is an lvalue. If x is not
of type bool, the expression ++x is equivalent to x+=1. [Note: see the discussions of addition (5.7) and
assignment operators (5.17) for information on conversions. ]

を見て「えーッ!!! インクリメントすれば true/false を繰り返すほうが対称性が高いし、他との互換性も高いッ!!! 当然の規約だろう。昔 bool 型が int で実装されていたのはマシンの制約に関わる部分が多くて、コーディング的には、true/false が交互になるほうがよい」というのをぴきーんと考えたのですが…タイトルを見て分かるように「こじつけ」です。

実は手元にある VC++2010, VC++2012, G++4.5.2 で試したところ、bool をインクリメントすると true(1) -> ture(1) なんですわー、というわけで「非推奨」ですね、つーか、処理系に依存するコードを書いちゃダメですね。

■実験コードを作る

int main( void )
{
	bool b = false;

	// 後置型
	cout << &quot;bool: &quot; << b++ << endl;	// false
	cout << &quot;bool: &quot; << b++ << endl;	// true
	cout << &quot;bool: &quot; << b   << endl;	// false
	cout << endl;

	b = false;
	// 前置型
	cout << &quot;bool: &quot; << ++b << endl;	// true
	cout << &quot;bool: &quot; << ++b << endl;	// false
	cout << &quot;bool: &quot; << b   << endl;	// true
	cout << endl;

	b = false;
	for ( int i=0; i<5; i++ ) {
		cout << b << endl;
		b = !b ;	// 反転
	}
	cout << endl;
	// 利点としてはインクリメントを使うと1行で書ける
	b = false;
	for ( int i=0; i<5; i++ ) {
		cout << b++ << endl;	// 後でインクリメント
	}
	cout << endl;

	return 0;
}

期待するところは、コメントにある通り true/false を繰り返します。
期待通りに動くのであれば、「!b」で反転させるよりも、b++ にすると1行で済むことが分かります。

bool 値は、一見「論理式」のために用意されている on/off の状態を示すように見えますが、。反転を表す「!」演算子を使う場合 on <-> off の交互になる、つまりは、

  • on であれば off になる
  • off であれば on になる

のようになります。しかし bool 値を一般的な int 型と考えてみると、int型のように 0 -> 1 -> 2 -> … -> MAX -> 0 のように最大値を過ぎたら 0 に戻るとうのが普通なのです。となると、0,1 の値しか持たない=1ビットで表すことができる数値として bool をとらえると、

  • 0 に 1 を加えて 1 になる
  • 1 に 1 を加えて 0 になる

という論法になります。この考え方は、欧米の家電のスイッチが「0」と「1」になっていることから分かりますね。

  • スイッチが入っいない状態=0の状態
  • スイッチが入っている状態=1の状態

という訳です。

■3値のクラスを作る

さて、この論法を確認するために 3値のクラスを考えてみましょう。

// 例えば3値のクラスを作る
class Value
{
	int _v;
public:
	Value() : _v(0) {}
	// 前置
	Value& operator ++() {
		if ( ++_v >= 3 ) _v=0;
		return *this;
	}
	// 後置
	Value operator ++(int) {
		Value v;
		v._v = this->_v;
		if ( ++_v >= 3 ) _v=0;
		return v;
	}
	// int型へキャスト
	operator int() {
		return _v;
	}
};
ostream& operator << ( ostream &s, Value v )
{
	switch ((int)v) {
	case 0: s << &quot;one&quot; ; break;
	case 1: s << &quot;two&quot; ; break;
	case 2: s << &quot;three&quot; ; break;
	}
	return s;
}

0, 1, 2 の3つの値を取る整数値の場合には、++演算子は普通に使える、というのが期待されます。

	// 3値クラスを使う
	Value v ;
	cout << v++ << endl;		// one
	cout << v++ << endl;		// two
	cout << v++ << endl;		// three
	cout << v++ << endl;		// one
	cout << endl;

これは期待通りに、one -> two -> three -> one のように動きます。
チェックボックスの値も、unenabled -> off -> on -> unenabled のように動くことが期待できるわけです(まあ、内実がint型だからというのもありますが)。

■任意の値を最大値とするテンプレートを作る

3値ではなくて、任意の値「MAX」が取れるようにテンプレートクラスにします。

// 任意の値までのテンプレートクラス
template<int MAX>
class TValue
{
	int _v;
public:
	TValue<MAX>() : _v(0) {
	}
	// 前置
	TValue<MAX>& operator ++() {
		if ( ++_v >= MAX ) _v=0;
		return *this;
	}
	// 後置
	TValue<MAX> operator ++(int) {
		TValue<MAX> v;
		v._v = this->_v;
		if ( ++_v >= MAX ) _v=0;
		return v;
	}
	// int型へキャスト
	operator int() {
		return _v;
	}
};

この場合も期待通りに動きます。MAX の次が 0 なので、サイクリックな値として使えますね?

	TValue<3> v3 ;
	cout << v3++ << endl;		// one
	cout << v3++ << endl;		// two
	cout << v3++ << endl;		// three
	cout << v3++ << endl;		// one
	cout << endl;

■Bool へ typedef する

先のテンプレートに MAX=2 を指定すると、ほら、規約にある bool と同じ動作をしますね。

	typedef TValue<2> Bool;		// 2値のBoolクラス
	Bool bb;
	cout << bb++ << endl;		// 0
	cout << bb++ << endl;		// 1
	cout << bb++ << endl;		// 0
	cout << endl;

つまりは、bool 型というのは、true/false という特別な型ではなくて、int 型や char 型と同じように「最大値を指定してサイクリックにインクリメントできる型」の特殊なものして、定義できる訳です。これはなんか「数学的」で綺麗でいいですよね。

■で、最初に戻って bool 型をインクリメントすると

さて、ここまで薀蓄を含めて bool 値の考察をしていきましたが、実装はどうなっているでしょうか?

	bool b = false;

	// 後置型
	cout << &quot;bool: &quot; << b++ << endl;	// false
	cout << &quot;bool: &quot; << b++ << endl;	// true
	cout << &quot;bool: &quot; << b   << endl;	// false
	cout << endl;

	b = false;
	// 前置型
	cout << &quot;bool: &quot; << ++b << endl;	// true
	cout << &quot;bool: &quot; << ++b << endl;	// false
	cout << &quot;bool: &quot; << b   << endl;	// true
	cout << endl;

の結果は、VC++2010, VC++2012 で実行すると、

bool: 0
bool: 1
bool: 1

bool: 1
bool: 1
bool: 1

あーあー、うん、あーあ、どうでもいいや、ってな気分です。

# ちなみに、VC++2010 の場合はデクリメントをしようとするとコンパイルエラーになります。

処理としては、0 -> 1, 1 -> 1 という実装みたいですね。

もしデクリメントを実装するとすると、1 -> 0, 0 -> 0 が素直かと。

 

カテゴリー: C++ | [C++] bool値をインクリメントすると、ture/false を繰り返す理由…をこじつける はコメントを受け付けていません

[C#] XDocument から XmlNavigater を使えるように拡張

巷では、Windows 8 RTM, Visual Studio 2012 RTM で盛り上がっていますが、しばらくは平常運転で。
ちなみに 90日試用版は以下からダウンロードできます。

Download Windows 8 Enterprise Evaluation
http://msdn.microsoft.com/ja-jp/evalcenter/jj554510.aspx

# 英語のページだけど、きちんと日本語版がダウンロードできます。2.4GB 程度です。
# MSDN のほうも Japanese 版が用意されています。

さて、本題の XmlDom のほうはテストルーチンから作っていきます。
内部的には XML文字列 -> XDocument -> XmlDocument -> XmlNavigator の経由で渡しています。
ちまちま XML文字列をパースするコードを書いてもいいのですが、パース自体はあまりスピードを問わないので。
# 実は、System.Xml.Linq は Silverlight に含まれていない(今は含まれている?)ので避けていたのですが、
# Siverlight 自体が終了しそうな雰囲気なので、XDocument を使うことに決定

■テストコードを書く

以前に書いた XNode, XmlNode 構築のコードを XML文字列に直します。
Assert.AreEqual 部分はそのまま動くはず…というか、動かないと駄目です。

namespace TestXmlDom
{
	[TestClass]
	public class TestXmlDocument
	{
		[TestMethod]
		public void TestParse1()
		{
			string xml = &quot;<root><name>masuda tomoaki</name></root>&quot;;
			var doc = new XmlDocument(xml);

			var q = new XmlNavigator(doc)
				.Where(n => n.TagName == &quot;name&quot;)
				.FirstOrDefault();

			Assert.AreEqual(&quot;masuda tomoaki&quot;, q.Value);
		}

		[TestMethod]
		public void TestParse2()
		{
			string xml = @&quot;
<root>
 <name>masuda</name>
 <name>yamada</name>
 <name>yamasaki</name>
</root>&quot;;
			var doc = new XmlDocument(xml);
			var q = new XmlNavigator(doc)
				.Where(n => n.TagName == &quot;name&quot;)
				;
			
			Assert.AreEqual(3, q.Count());
			Assert.AreEqual(&quot;masuda&quot;, q.First().Value);
		}

		[TestMethod]
		public void TestParse3()
		{
			string xml = @&quot;
<root>
 <person id='1'>
  <name>masuda</name>
  <age>44</age>
 </person>
 <person id='2'>
  <name>yamada</name>
  <age>20</age>
 </person>
 <person id='3'>
  <name>tanaka</name>
  <age>10</age>
 </person>
</root>&quot;;
			var doc = new XmlDocument(xml);
			var q = new XmlNavigator(doc)
				.Where(n => n.Attrs[&quot;id&quot;] == &quot;2&quot;)
				.FirstOrDefault();
			Assert.AreEqual(&quot;person&quot;, q.TagName);
			// ExDoc記述
			Assert.AreEqual(&quot;yamada&quot;, q / &quot;name&quot;);
			Assert.AreEqual(&quot;20&quot;, q / &quot;age&quot;);
		}
	}
}

XNode じゃなくて、XmlNode を使う理由としては「q / “name”」という書き方ができるからです。
この部分はXElemnetを使って「q.Element(“name”).Value」と書いても同じです。

ただし、q 自体は XNode を返すので Element() を含んでいなくていまいち不便なんですよね。

■XDocument から XmlDoucmnet を構築する

インターフェースとして、XmlDocument を用意します。
単純に、XDocument の中身を XmlDocument に移し替えているだけです。

ざっとインターフェースを合わせていきます。

namespace Moonmile.XmlDom
{
	public class XmlDocument : XmlNode
	{
		public XmlNode documentElement { 
			get
			{
				if (this.Children.Count() > 0)
				{
					return Children[0];
				}
				else
				{
					return XmlNode.Empty;
				}
			}
		}

		/// <summary>
		/// コンストラクタ
		/// </summary>
		public XmlDocument() 
		{
		}

		/// <summary>
		/// コンストラクタ(XML文字列で初期化)
		/// </summary>
		/// <param name=&quot;xml&quot;></param>
		public XmlDocument( string xml)
		{
			this.LoadXml(xml);
		}

		/// <summary>
		/// ファイルから構築する
		/// </summary>
		/// <param name=&quot;path&quot;></param>
		/// <returns></returns>
		public XmlDocument Load(string path)
		{
			return this.Load(XDocument.Load(path));
		}

		/// <summary>
		/// XML文字列から構築する
		/// </summary>
		/// <param name=&quot;xml&quot;></param>
		/// <returns></returns>
		public XmlDocument LoadXml(string xml)
		{
			StringReader sr = new StringReader(xml);
			return this.Load(XDocument.Load(sr));
		}

		/// <summary>
		/// XDocumentオブジェクトから構築する
		/// </summary>
		/// <param name=&quot;doc&quot;></param>
		/// <returns></returns>
		public XmlDocument Load(XDocument doc)
		{
			var root = LoadXNode(doc.FirstNode);
			this.Children.Add(root);
			return this;
		}

		protected XmlNode LoadXNode(XNode node)
		{
			var nn = new XmlNode(node.TagName(), node.Value());
			if (node.NodeType == System.Xml.XmlNodeType.Element)
			{
				var el = node as XElement;
				foreach (var at in el.Attributes())
				{
					nn.Attrs.Add(new XmlAttr(at.Name.ToString(), at.Value));
				}
				foreach (var n in el.Nodes())
				{
					nn.Children.Add(LoadXNode(n));
				}
			}
			return nn;
		}
	}
}

という訳で、これで XML を自由に比較的自由に扱うことができました。
今度は HTML 文字列を扱えるように、HtmlNode, HtmlDocument へ移していきます。

カテゴリー: C#, XmlDom | [C#] XDocument から XmlNavigater を使えるように拡張 はコメントを受け付けていません

フリーなプログラマ、デザイナを緩く募集します

具体的なところは以下から

募集 | Moonmile Solutions Blog
http://www.moonmile.net/blog/recruiting

以前から考えていたのですが、少し具体的に動かそうと思いブログに公開します。

  • 依頼を受けるものが、直近かつ短期間のものが多い。
    →  ソフトウェア会社には頼みにくい。
    → 私自身のキャパでは無理。
  • そこそこ、仕事の幅がある(詳細設計がないなど)ものが多い。
    →  単なるコーダーでは駄目。
  • 中間マージンが無ければ、そこそこ収入が良い。
    → 「派遣」を通すと、マージン分だけ損してしまうので、馬鹿馬鹿しい。

というパターンが多いので、もう少し「私が紹介できるプログラマ、デザイナな方」を広く緩く募集ところです。

カテゴリー: 仕事, 雑談 | フリーなプログラマ、デザイナを緩く募集します はコメントを受け付けていません

[C#] XNode を使って XNavigator を作る

[C#] XElement とは違う LINQ できる XmlNode を作成する | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/3628

なところで、自前の XmlNode を使って LINQ 操作を実現した訳ですが、XNode との重複が激しいので、試しに System.Xml.Linq.XNode http://msdn.microsoft.com/ja-jp/library/system.xml.linq.xnode.aspx を使って書き直してみます、っていう実験を。

■XNavigatorを実装。

自前の XmlNavigator をそのままコピペして実装し直します。
いくつかのメソッド名を変更しますが、そのままですんなりコンパイルが通ります。

public class XNavigator : IEnumerable<XNode>
{
	XNode _root;

	public XNavigator(XNode root)
	{
		_root = root;
	}

	public IEnumerator<XNode> GetEnumerator()
	{
		return new Enumerator(_root);
	}

	System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
	{
		return new Enumerator(_root);
	}

	protected class Enumerator : IEnumerator<XNode>
	{
		XNode _root;
		XNode _cur;

		public Enumerator(XNode root)
		{
			_root = root;
		}

		public XNode Current
		{
			get { return _cur; }
		}

		public void Dispose()
		{
		}

		object System.Collections.IEnumerator.Current
		{
			get { return _cur; }
		}

		public bool MoveNext()
		{
			if (_cur == null)
			{
				_cur = _root;
				return true;
			}
			if (_cur.NodeType == System.Xml.XmlNodeType.Element &&
				((XElement)_cur).Nodes().Count() > 0)
			{
				_cur = ((XElement)_cur).FirstNode;
				return true;
			}
			if (_cur.NextNode != null)
			{
				_cur = _cur.NextNode;
				return true;
			}
			XNode cur = _cur;
			while (true)
			{
				XElement pa1 = cur.Parent;
				if (pa1 == null)
				{
					_cur = null;
					return false;
				}
				if (pa1.NextNode != null)
				{
					_cur = pa1.NextNode;
					return true;
				}
				cur = pa1;
			}
		}

		public void Reset()
		{
			_cur = null;
		}
	}
}

■テストコードを書く

XmlNavigator 用に書いたテストコードを XNavigator 用に直します。

  • XML 構築のところは、XDocument を使って書き換え。
  • XNode のままでは Where メソッドでうまく動かないので、ちょっと思案

という具合です。

XNode のままだと、実は Nodes コレクションとかが無いんですよね…このあたり、DOM の頃からそうなのですが、インターフェースを重視するあまり、利用する時に XElement にキャストをしないといけないという罠があります。
XNavigator 自体は、XElement, XText などのオブジェクトも返すので、この TagName や Value 当たりが鬼門となります。

	[TestClass]
	public class TestXNavi
	{
		[TestMethod]
		public void TestNormal1()
		{
			XDocument doc = new XDocument(
				new XElement(&quot;root&quot;,
					new XElement(&quot;person&quot;,
						new XElement(&quot;name&quot;, &quot;masuda&quot;))));

			var q = new XNavigator(doc.FirstNode)
				.Where(n => n.TagName() == &quot;name&quot;)
				.FirstOrDefault();
			Assert.AreEqual(&quot;masuda&quot;, q.Value());

		}
		
		[TestMethod]
		public void TestNormal2()
		{
			XDocument doc = new XDocument(
				new XElement(&quot;root&quot;,
					new XElement(&quot;person&quot;,
						new XElement(&quot;name&quot;, &quot;masuda&quot;),
						new XElement(&quot;name&quot;, &quot;yamada&quot;),
						new XElement(&quot;name&quot;, &quot;yamasaki&quot;))));

			// タグが検索できた場合
			var q = new XNavigator(doc.FirstNode)
				.Where(n => n.TagName() == &quot;name&quot;)
				.Select( n => n );

			Assert.AreEqual(3, q.Count());
			Assert.AreEqual(&quot;masuda&quot;, q.First().Value());
		}

		[TestMethod]
		public void TestNormal3()
		{
			XDocument doc = new XDocument(
				new XElement(&quot;root&quot;,
					new XElement(&quot;person&quot;,
						new XAttribute(&quot;id&quot;, &quot;1&quot;),
						new XElement(&quot;name&quot;, &quot;masuda&quot;),
						new XElement(&quot;age&quot;,&quot;44&quot;)),
					new XElement(&quot;person&quot;,
						new XAttribute(&quot;id&quot;, &quot;2&quot;),
						new XElement(&quot;name&quot;, &quot;yamada&quot;),
						new XElement(&quot;age&quot;,&quot;20&quot;)),
					new XElement(&quot;person&quot;,
						new XAttribute(&quot;id&quot;, &quot;3&quot;),
						new XElement(&quot;name&quot;, &quot;tanaka&quot;),
						new XElement(&quot;age&quot;,&quot;10&quot;))));

			// タグが検索できた場合
			var q = new XNavigator(doc.FirstNode)
				.Where(n => n.Attrs(&quot;id&quot;) == &quot;2&quot;)
				.FirstOrDefault();
			Assert.AreEqual(&quot;person&quot;, q.TagName());
			// 拡張メソッドを利用
			Assert.AreEqual(&quot;yamada&quot;, q.Child(&quot;name&quot;).Value());
			Assert.AreEqual(&quot;20&quot;, q.Child(&quot;age&quot;).Value());
		}
		
		[TestMethod]
		public void TestNormal4()
		{
			XDocument doc = new XDocument(
				new XElement(&quot;root&quot;,
					new XElement(&quot;person&quot;,
						new XAttribute(&quot;id&quot;, &quot;1&quot;),
						new XElement(&quot;name&quot;, &quot;masuda&quot;),
						new XElement(&quot;age&quot;,&quot;44&quot;)),
					new XElement(&quot;person&quot;,
						new XAttribute(&quot;id&quot;, &quot;2&quot;),
						new XElement(&quot;name&quot;, &quot;yamada&quot;),
						new XElement(&quot;age&quot;,&quot;20&quot;)),
					new XElement(&quot;person&quot;,
						new XAttribute(&quot;id&quot;, &quot;3&quot;),
						new XElement(&quot;name&quot;, &quot;tanaka&quot;),
						new XElement(&quot;age&quot;,&quot;10&quot;))));


			// クエリ文にしてみる
			var q = from n in new XNavigator(doc.FirstNode)
					where n.Attrs(&quot;id&quot;) == &quot;2&quot;
					select n;

			Assert.AreEqual(&quot;person&quot;, q.First().TagName());
			// 拡張メソッドを利用
			Assert.AreEqual(&quot;yamada&quot;, q.First().Child(&quot;name&quot;).Value());
			Assert.AreEqual(&quot;20&quot;, q.First().Child(&quot;age&quot;).Value());
		}

	}

■拡張メソッドを追加する

で、普通にやると断念してしまうので…無理矢理、XNode に拡張メソッドを追加します。
本来は、TagName プロパティ、Value プロパティのように「プロパティ」にしたいのですが、プロパティの拡張メソッドはできないので TagName() メソッド、Value() メソッドにします。

	/// <summary>
	/// XNodeをXElement風に使う拡張クラス
	/// </summary>
	public static class XNodeExtentions
	{
		public static string TagName(this XNode n)
		{
			if (n.NodeType == System.Xml.XmlNodeType.Element)
			{
				return ((XElement)n).Name.ToString();
			}
			else
			{
				return &quot;&quot;;
			}
		}
		public static string Value(this XNode n)
		{
			if (n.NodeType == System.Xml.XmlNodeType.Element)
			{
				return ((XElement)n).Value;
			}
			else
			{
				return &quot;&quot;;
			}
		}
		public static string Attrs(this XNode n, string key)
		{
			if (n.NodeType == System.Xml.XmlNodeType.Element)
			{
				var attr = ((XElement)n).Attribute(key);
				if ( attr != null ) {
					return attr.Value;
				}
			}
			return &quot;&quot;;
		}
		public static XNode Child(this XNode nd, string tag)
		{
			if (nd.NodeType == System.Xml.XmlNodeType.Element)
			{
				return ((XElement)nd).Nodes().Where(n => n.TagName() == tag).FirstOrDefault();
			}
			else
			{
				return null;
			}

		}
	
	}

すると、上記のテストコードが通るようになります。

	var q = new XNavigator(doc.FirstNode)
		.Where(n => n.TagName() == &quot;name&quot;)
		.FirstOrDefault();
	Assert.AreEqual(&quot;masuda&quot;, q.Value());

ラムダ式の中の n が XNode なので、TagName() メソッドを使います、ってな具合。XElement の場合は Value プロパティになる訳で、ここのところ、

  • XNode.Value() メソッド
  • XElement.Value プロパティ

という「メソッド名とプロパティ名が同じ」という(多分)VB では使えないメソッドが作られていますが、まあ良しとしましょう(後で確認してみますか)。

こうすると、XDocument もツリー構造でサーチができるのでコードが簡単になりますね。って話で。
自前の XmlNode でもいいんだけど、node / “tag” のような書き方ができないから、どうしようかね、って具合です。

カテゴリー: C#, XmlDom | [C#] XNode を使って XNavigator を作る はコメントを受け付けていません