C#ならではかな?不変型。変わらない良さ。変わるとき。

こんにちは。ヤマヤタケシです。

プログラミングは奥深いというかなんというか、またまた新しい概念と出会いました。
それは不変型、Immutableです。

書き換わってほしくないときに、間違って書き換わらないようにC++ではconstを使っていました。
しかし、C#のconstはC++と違ってあちこちにつけることはできません。

C#では値が定数というときだけconstを使います。
C++では定数はもちろん、関数の引数、クラスのメンバ関数、メンバ変数につけて読み取り専用にできます。

じゃあ、C#で引数とか、メンバ関数とか、メンバ変数を読み取り専用にするってどうすんのよ?

メンバ変数はreadonlyがありまーす!コンストラクタでのみ初期化できるんだぜ。ほっとしました。

次、関数の引数を読み取り専用にするには・・・、ありません。
え?
ないの?
ありません。
じゃあ、書籍「リファクタリング」でいうところの引数の群れをクラスにしたものを、関数内部で変更されないことを保証するにはどうするのか?

そのクラスを不変型にします。

なんやー、不変型ってなんやー?

immutableなクラスです。

しらんがなー。
不変型(immutable)はコンストラクタでのみ初期化できるクラスです。
C#のstringは不変型だったので、調べました。
たとえば、abc->ABCと大文字に変換する場合、こうなります。

string small="abc";
string big = small.ToUpper ();

直接、smallが書き換わるのではなく、書き換わった新しいインスタンスが返ってきます。
全ての関数がこんな感じでなにか書き換えたいような操作をする場合は新規作成になります。
不変型ならもとのインスタンスに影響はでないので安心です。

で、自分のクラスを不変型にしようとしたのですが、気が狂いそうになりました。

不変型にするにはインスタンスをコピーして、書き換えて返すのですが、コピーが問題になります。
インスタンスのコピーのためには、Cloneとコピーコンストラクタが必要です。

しかも、コピーの方法が浅いコピーと深いコピーがあって、浅いコピーはMemberwiceCloneのおかげで楽ができますが、深いコピーの場合はいちいち手書きする必要があるし、しかも実行コストが大きいし・・・という意外にしんどいことになります。
しかも、メンバーにクラス型がいると浅いコピーでは意図しない動作になるので、結局深いコピーが必要なの?

マジで?

問題を整理しましょう。
1. メンバーにクラスがあると浅いコピーでは意図しない動作になる。
2. 深いコピーはメンバーごとの実装が必要。漏れるかもしれない。
3. 深いコピーは実行時のコストが大きい。

ついに、解決方法が見つかりました!
それは、不変型の連鎖です。
不変型を作るには、不変型のメンバーしか使わないようにするのです。
プリミティブ型は値型なので不変型ですし。
不変型を使うと・・・・
1. メンバーのクラスを不変型にすると浅いコピーで意図通りに動作します。
2. 浅いコピーなので、MemberwiceCloneができるので実装が楽。漏れもない。
3. 浅いコピーなので実行コストは小さい。
完璧!
・・・じゃなかった。
インスタンスのメンバー変数の値を変更する度にメンバー変数単位で深いコピーが走ります。
丸ごとのコピーよりは実行コストが小さいですが、回数が多いと問題になります。

3種類のコピーの特徴はこんな感じです。

実装の手間 Cloneの実行コスト 操作コスト 予想外の変更
浅いコピー 小さい 小さい 小さい 発生する
深いコピー 大きい 大きい 小さい 発生しない
不変型連鎖 小さい 小さい 大きい 発生しない

今回の不変型の概念を確認するために書いたコードです。
コメントもいっぱい書いたので参考にしてください。

//
// 不変型 immutableの学習
// 2016/06/25 yamaya takeshi
//

using System;
using System.Text;
using System.Collections.Generic;
using System.Collections.Immutable;

namespace ImmutableTest
{


    //浅いコピーをして失敗するクラス
    class ShallowClone
    {
        readonly List<int> list = new List<int>();

        public ShallowClone Clone()
        {
            return MemberwiseClone() as ShallowClone;
        }

        public void Add(int value)
        {
            list.Add(value);
        }

        public override string ToString()
        {
            var sb = new StringBuilder();
            foreach (var n in list) { sb.Append(n.ToString() + ","); }
            return sb.ToString();
        }

    }

    //深いコピーで失敗しないクラス。コピーのコストが大きい
    class DeepClone
    {
        readonly List<int> list = new List<int>();

        public DeepClone() { }
 
        // コピーコンストラクタ
        public DeepClone(DeepClone src)
        {
            // メンバーの深いコピー
            this.list = new List<int>(src.list);
            // メンバーの数だけ書かなきゃいけないし。
            // 人間がやるから漏れるよね。
        }

        public DeepClone Clone()
        {
            return new DeepClone(this);
        }

        public void Add(int value)
        {
            list.Add(value);
        }

        public override string ToString()
        {
            var sb = new StringBuilder();
            foreach (var n in list) { sb.Append(n.ToString() + ","); }
            return sb.ToString();
        }

    }

    //浅いコピーをしても、不変型のリストを使っているから失敗しないクラス
    class UseImmutableList
    {
        public ImmutableList<int> list = ImmutableList.Create<int>();


        public void Add(int value)
        {
            list = list.Add(value);
        }

        public override string ToString()
        {
            var sb = new StringBuilder();
            foreach (var n in list) { sb.Append(n.ToString() + ","); }
            return sb.ToString();
        }

        public UseImmutableList Clone()
        {
            return MemberwiseClone() as UseImmutableList;
        }
    }


    class Program
    {
        static void Main(string[] args)
        {
            Console.Out.WriteLine("stringは不変型なので内部を変更する関数がない。その代わり別のインスタンスを変更して返す");
            {
                string small = "abc";
                string big = small.ToUpper();
                Console.Out.WriteLine("small=" + small);
                Console.Out.WriteLine("big=" + big);
            }

            Console.Out.WriteLine("浅いコピーなので意図しない動作になる");
            {
                ShallowClone a1 = new ShallowClone();
                // Aのlistはreadonlyだがらエラーになる。
                //a.list = new List<int>();//     エラー CS0191  読み取り専用フィールドに割り当てることはできません(コンストラクター、変数初期化子では可)。	

                // しかしlistに値の追加はできる。
                a1.Add(1);
                Console.Out.WriteLine("a1=" + a1);

                // Cloneが浅いので同じlistを参照することになる。
                ShallowClone a2 = a1.Clone();
                a2.Add(2);
                Console.Out.WriteLine("a2=" + a2);

                // だからa1にも同時に追加されてしまう!これは意図していない。
                Console.Out.WriteLine("a1=" + a1);

            }


            Console.Out.WriteLine("深いコピーなので意図どおり。");
            {
                var b1 = new DeepClone();
                b1.Add(1);
                Console.Out.WriteLine("b1 = " + b1);

                var b2 = b1.Clone();
                b2.Add(2);
                Console.Out.WriteLine("b2 = " + b2);
                Console.Out.WriteLine("b1 = " + b1);

            }

            Console.Out.WriteLine("不変型のリストに変更したので浅いコピーでもAのようにならない。");
            {
                var c1 = new UseImmutableList();
                c1.Add(1);
                Console.Out.WriteLine("c1 = " + c1);

                var c2 = c1.Clone();
                c2.Add(2);
                Console.Out.WriteLine("c2 = " + c2);
                Console.Out.WriteLine("c1 = " + c1);

            }
        }

    }
}

実行結果。
immutable-run-result

あ、そうそう、System.Collecions.Immutableは、標準では入っていないのでNuGetでインストールします。
system-collections-immutable

(追記!)
constがそもそもの同期なら、不変型よりも読み取り専用のインタフェースを書くべき!みたい。
今回の状況的には不変型が正しい選択だったと思うけども、IReadOnlyCollectionみたいなものあるし、IReadOnlyXXXXを実装すれば良いときも多々ありますね。

この記事を書いた後にこのスライドを発見しました。理解が深まります。超オススメです。

そんじゃまた。

C#ならではかな?不変型。変わらない良さ。変わるとき。” への3件のコメント

  1. ピンバック: C#的なクローン手法のコスト比較 | ヤマヤタケシのブログ

  2. ピンバック: C#的なクローンの各種コスト比較 | ヤマヤタケシのブログ

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です