2014年9月25日木曜日

[C#]複製可能読み取り専用リスト

嵌ったのでメモ


タイトルの通り、読み取り専用だが複製(ディープコピーで作成されたインスタンス)を作ることができるリストを作りたかった。
C#様々ならちょろっと書けると思ったら丸一日掛かった。別にC#で作るのが困難だったからではなく、単に知識が著しく不足していたせいだが。


まず、複製可能なListを作るのにいろいろと発見があった。
最初、丁寧に一つ一つforeachでコピーしていくという方法を思い浮かべていた。

public class CopyableList<t> : List<t>
{
    public CopyableList<t> Copy()
    {
        var result = new CopyableList<t>();
        foreach( var data in this )
        {
            result.Add(data);
        }
        return result;
    }
}

これで複製はちゃんとできるしなかなかシンプルだと思ったが、なんというかC#らしさが欠片もない。で、普通はどんな風に実装するのか調べたらこの実装が非常に美しかった。(下に引用)

public class CloneableList<T> : List<T>, ICloneable<CloneableList<T>> where T : ICloneable
{
    public CloneableList<T> Clone()
    {
        var result = new CloneableList<T>();
        result.AddRange(this.Select(item => (T) item.Clone()));
        return result;
    }

    object ICloneable.Clone()
    {
        return ((ICloneable<CloneableList<T>>) this).Clone();
    }
}

public interface ICloneable<T> : ICloneable where T : ICloneable<T>
{
    new T Clone();
}

ジェネリック、LINQなどなど、これぞC#といって文句ないだろう。
本題から少し逸れるが、ICloneableというインターフェースを初めて知ったのでこれもいろいろ調べたが、何とも悩ましい限りのインターフェースだった。
ジェネリック実装前に作成されたインターフェースなので当然ジェネリックは考慮されてない(なので上の人は自分でジェネリック対応インターフェースを定義しているわけだ)。
で、このインターフェースはCloneメソッドを提供しているのだが、この実装がシャローコピーかディープコピーかは実装側の自由となっている[1]。あくまでインスタンスのコピーを作るメソッドということらしい。当然こんなインターフェースを外部に公開出来るはずもなく、APIなどにICloneableを使わないでくれと開発者直々にお達しが出されている[2]


Listを書き込み不可にする場合、AsReadOnlyメソッドで書き換え不可にするんだよなとか、でもそれだと一見普通のListと区別がつかないんだよなとか思ってたら、IReadOnlyListなんてまさに求めているものそのものが存在していた。こんな汎用性の高いインターフェースなんで知らなかったんだと思ったら、.NET 4.5で追加されたインターフェースだった(.NET 4.5が公開されてから既に2年が経っているが)。
IReadOnlyListのItemプロパティは読み取り専用で、AddメソッドやSortメソッドは定義されていない。
これでうっかり読み込み専用にしたListでAddメソッド呼び出して怒られるという悲劇からもおさらばだ。


ということで、複製可能読み取り専用リスト(のインターフェース)は次のように実装できる。

public class CloneableList<T> : List<T>, IReadOnlyCloneableList<T>, ICloneable<CloneableList<T>> where T : ICloneable
{
    public CloneableList<T> Clone()
    {
        var result = new CloneableList<T>();
        result.AddRange(this.Select(item => (T)item.Clone()));
        return result;
    }

    object ICloneable.Clone()
    {
        return ((ICloneable<CloneableList<T>>)this).Clone();
    }
}

public interface ICloneable<T> : ICloneable where T : ICloneable<T>
{
    new T Clone();
}

public interface IReadOnlyCloneableList<T> : IReadOnlyList<T>, ICloneable<CloneableList<T>> where T : ICloneable
{}

もっとスマートな実装が有るんだろうと思うけど、目的は叶ったしちゃんと動くし、全く問題はない。どのみちICloneableをなんとかしないかぎり自分用で使うし。


[1] http://msdn.microsoft.com/library/system.icloneable(v=vs.110).aspx
[2] http://blogs.msdn.com/b/brada/archive/2003/04/09/49935.aspx

0 件のコメント:

コメントを投稿