====== C#のジェネリックで特殊化っぽいことをする ======
新年明けましておめでとうございます(遅。開設14年目となるクソゲ~製作所を本年もよろしくお願い致します。
C++のテンプレートには特殊化という、特定の型パラメータの時にテンプレートの実体を別に定義する機能がある。だが、C#版テンプレートとも言えるジェネリックでは、なんということでしょう、特殊化が使えないではありませんか!それをどうにかしてジェネリック特殊化っぽい事をしてみたっていうお話。
こんなコードがあったとする。
public class Node
{
// 子ノード
public List Children;
// 特定の子ノードを取得
public List FindChildren() : where Node
{
List list = new List();
Type type = typeof(T);
foreach (Node e in Children)
{
if (type == e.GetType())
{
list.Add(e);
}
list.AddRange(e.FindChildren());
}
return list;
}
}
public class DocumentNode : Node {}
public class PageNode : Node {}
public class TitleNode : Node
{
public string Title;
public TitleNode(string title) { Title = title; }
}
public class SectionNode : Node
{
public TitleNode Title;
}
static void Main()
{
DocumentNode doc = new DocumentNode();
PageNode page = new PageNode();
doc.Children.Add(page);
SectionNode section1 = new SectionNode();
section1.Title = new TitleNode("はじめに");
page.Children.Add(section1);
SectionNode section2 = new SectionNode();
section2.Title = new TitleNode("つぎに")
page.Children.Add(section2);
// 全セクションを取得
List allSections = doc.FindChildren(); //正しく取得できる
// 全タイトルを取得
List allTitles = doc.FindChildren(); //★正しく取得できない!!
...
}
◆データ構造
[DocumentNode]
-Children
-[PageNode]
-Children
-[SectionNode]
-Title
-Children
-[SectionNode]
-Title
-Children
文章を模したデータ構造を作り、''FindChildren''メソッドで型をパラメータに子ノードを取得しているが、
''doc.FindChildren()''は正しい挙動をするものの、''doc.FindChildren()''の方は空のリストが帰ってくる。''FindChildren()''は''Node.Children''しか見てないので、独立したメンバ''SectionNode.Title''が入るはずはなく当然の挙動である。
こんな時、C++なら特殊化で''FindChildren''専用の処理が書け同一のインタフェースを提供できるのだが、前述の通りジェネリックは特殊化が使えないの。C#的には''FindChildTitleNodes()''的な別メソッドで提供するのが正しいのかもしれないが、やっぱり統一感に欠けて美しくない(そもそもこんな糞いデータ構造にすんなって話だが例ってことで許してね。)
で、知恵を振り絞って''FindChildren''を以下のようにした。
public List FindChildren() : where Node
{
List list = new List();
Type type = typeof(T);
if (type == typeof(TitleNode)) // (1)
{
List titles = new List(); // (2)-a
var sections = FindChildrenInternal();
section.ForEach(e => { titles.Add(e.Title as T); }); // (3)
return titles; // (2)-b
}
return FindChildrenInternal();
}
List FindChildrenInternal() : where Node
{
List list = new List();
Type type = typeof(T);
foreach (Node e in Children)
{
if (type == e.GetType())
{
list.Add(e);
}
list.AddRange(e.FindChildren());
}
return list;
}
元の''FindChildren()''を''FindChildrenInternal()''とし、''FindChildren()''には型パラメータ''T''に応じた処理を書くようにした。C++でコンパイラが自動で行ってくれる特殊化による処理の振り分けを、実行時に手動で行うような感じかしら。原理上、実行時コストは増えるが、この程度なら大した影響はないだろう。JIT様もあることだし。
型が増えると''if''と特殊化処理の嵐になるが、型ごとに処理デリゲートを作って''Type''とデリゲート辞書でも作ればすっきりするので大した問題ではない。そもそも、そんな状況は最早「特殊化」の本質から外れてるので設計から見直すべきだろう。
このコードのミソは(1)~(3)の部分。
(1)で処理すべき''T''の型が明確になったものの、(2)-aで''List''としているのはコンパイルを通すため。''List''でも良さそうに見えるが、''T == TitleNode''になるとは限らないので(2)-bで型不一致エラーになる。''List''とした場合に''T = SectionNode''で呼び出した時にどうなるかを考えれば、すぐにおかしさに気づいて頂けるかなと。
(3)も一見''titles.Add(e.Title)''で良さそうだが、これまた''TitleNode''が必ずしも''T''に変換可能とは限らないのでCS1503エラーとなる。よって、ちょっとアホくさいが''TitleNode''を自身の型''T''にキャストしてやらなければならない。
メソッドの引数に型パラメータを持てる場面では、[[https://arbel.net/2007/11/22/c-partial-specialization-with-extension-methods/|拡張メソッドを使った特殊化もどき]]が使える模様。
リフレクションは楽しいなー。C++にも軽量な型情報システムがあればいいのになぁ…。