このページの翻訳:
ソースの表示
最近の変更サイトマップ

C#のジェネリックで特殊化っぽいことをする

新年明けましておめでとうございます(遅。開設14年目となるクソゲ~製作所を本年もよろしくお願い致します。

C++のテンプレートには特殊化という、特定の型パラメータの時にテンプレートの実体を別に定義する機能がある。だが、C#版テンプレートとも言えるジェネリックでは、なんということでしょう、特殊化が使えないではありませんか!それをどうにかしてジェネリック特殊化っぽい事をしてみたっていうお話。

こんなコードがあったとする。

public class Node
{
    // 子ノード
    public List<Node> Children;
    // 特定の子ノードを取得
    public List<T> FindChildren<T>() : where Node
    {
        List<T> list = new List<T>();
        Type type = typeof(T);
        foreach (Node e in Children)
        {
            if (type == e.GetType())
            {
                list.Add(e);
            }
            list.AddRange(e.FindChildren<T>());
        }
        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<SectionNode> allSections = doc.FindChildren<SectionNode>(); //正しく取得できる
    // 全タイトルを取得
    List<TitleNode> allTitles = doc.FindChildren<TitleNode>(); //★正しく取得できない!!
 
    ...
}
◆データ構造
[DocumentNode]
 -Children
   -[PageNode]
     -Children
       -[SectionNode]
         -Title
         -Children
       -[SectionNode]
         -Title
         -Children

文章を模したデータ構造を作り、FindChildrenメソッドで型をパラメータに子ノードを取得しているが、 doc.FindChildren<SectionNode>()は正しい挙動をするものの、doc.FindChildren<TitleNode>()の方は空のリストが帰ってくる。FindChildren()Node.Childrenしか見てないので、独立したメンバSectionNode.Titleが入るはずはなく当然の挙動である。

こんな時、C++なら特殊化でFindChildren<TitleNode>専用の処理が書け同一のインタフェースを提供できるのだが、前述の通りジェネリックは特殊化が使えないの。C#的にはFindChildTitleNodes()的な別メソッドで提供するのが正しいのかもしれないが、やっぱり統一感に欠けて美しくない(そもそもこんな糞いデータ構造にすんなって話だが例ってことで許してね。)

で、知恵を振り絞ってFindChildrenを以下のようにした。

public List<T> FindChildren<T>() : where Node
{
    List<T> list = new List<T>();
    Type type = typeof(T);
    if (type == typeof(TitleNode))                           // (1)
    {
        List<T> titles = new List<T>();                      // (2)-a
        var sections = FindChildrenInternal<SectionNode>();
        section.ForEach(e => { titles.Add(e.Title as T); }); // (3)
        return titles;                                       // (2)-b
    }
 
    return FindChildrenInternal<T>();
}
 
List<T> FindChildrenInternal<T>() : where Node
{
    List<T> list = new List<T>();
    Type type = typeof(T);
    foreach (Node e in Children)
    {
        if (type == e.GetType())
        {
            list.Add(e);
        }
        list.AddRange(e.FindChildren<T>());
    }
    return list;
}

元のFindChildren()FindChildrenInternal()とし、FindChildren()には型パラメータTに応じた処理を書くようにした。C++でコンパイラが自動で行ってくれる特殊化による処理の振り分けを、実行時に手動で行うような感じかしら。原理上、実行時コストは増えるが、この程度なら大した影響はないだろう。JIT様もあることだし。

型が増えるとifと特殊化処理の嵐になるが、型ごとに処理デリゲートを作ってTypeとデリゲート辞書でも作ればすっきりするので大した問題ではない。そもそも、そんな状況は最早「特殊化」の本質から外れてるので設計から見直すべきだろう。

このコードのミソは(1)~(3)の部分。

(1)で処理すべきTの型が明確になったものの、(2)-aでList<T>としているのはコンパイルを通すため。List<TitleNode>でも良さそうに見えるが、T == TitleNodeになるとは限らないので(2)-bで型不一致エラーになる。List<TitleNode>とした場合にT = SectionNodeで呼び出した時にどうなるかを考えれば、すぐにおかしさに気づいて頂けるかなと。

(3)も一見titles.Add(e.Title)で良さそうだが、これまたTitleNodeが必ずしもTに変換可能とは限らないのでCS1503エラーとなる。よって、ちょっとアホくさいがTitleNodeを自身の型Tにキャストしてやらなければならない。

メソッドの引数に型パラメータを持てる場面では、拡張メソッドを使った特殊化もどきが使える模様。

リフレクションは楽しいなー。C++にも軽量な型情報システムがあればいいのになぁ…。

Comments




blog/2016/2016-01-20.txt · 最終更新: 2016-01-20 14:51 by Decomo
CC Attribution-Noncommercial-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0