ジェネリック版のIComparableと非ジェネリック版のIComparableの実装サンプル

2021年4月1日

今回はIComparableを実装してみました。

後ほどソースコードは掲載しますが、GitHubにもサンプルを公開しています。

背景

プログラマーになって3年を過ぎたあたりから、自分でインターフェースを定義して実装できるようになってきました。

それまでは、インターフェースって何?
何のために実装するの?
インターフェースで実装するメリットって何?

というような状態でした。
そんな時に親切な先輩がインターフェースの使いどころを説明してくれました。しかし、当時の私は理解できませんでした。

その当時の私にもう一度説明しても、たぶんわからないと思います。
例えうまく説明できたとしても、なんとか概念的に理解できるレベルでしょう。ただ、結局どこで使うの?という状況になると思います。

では、どうすればインターフェースを実際に使えるようになるのか。

その答えは、自分で実際に実装してみる。
より理想的なのは、実際に必要となるシチュエーションに出会って自分で考えて実装する。

結局本当の意味での理解、言い換えると自分で使えるというレベルになるためには、手を動かして実装してみるしかないと思います。一部の天才を除いては。

というわけで、今回はインターフェースを実装しようの回です。

使用シーンを想像してみる

hoge学校 で教鞭をとる山田先生は、非常にめんどくさがり屋です。
この度、進学シーズンとなり、自分の受け持つ生徒の推薦先を選ばないといけなくなりました。
しかし、推薦する学校ごとに重視される成績が異なり、受入れ人数も異なります。
山田先生は、だれを推薦すべきか?という問題に頭を抱えています。
めんどくさがり屋の山田先生は、手作業で生徒を決めたくなかったので、プログラムを組むことにしました。

学校ごとに重視する成績は以下の通りです。

A学校は、数学を重視する。
B学校は、国語を重視する。
C学校は、英語を重視する。
D学校は、総合点を重視する。
以上の条件から、生徒に勧める学校を決めていくことになります。

このような順序に関係するケースは内容は違えど、類似するケースとして扱うことができると思います。

結局、上記でやりたいことは、インスタンスの順序を決定することです。

下記のソースコードでは、今挙げたシーンを解決するための実装を例示したいと思います。

ソースコード

Program.cs

using System;
using System.Collections.Generic;
using Learning.CompareSample;

namespace Learning
{
    class Program
    {
        static void Main(string[] args)
        {
            var students1 = GetStudents();
            var comp1 = new CompareSample.CompareSample(students1,true);
            comp1.Execute();

            var students2 = GetStudentsMany();
            var comp2 = new CompareSample.CompareSample(students2,false);
            comp2.Execute();
        }
        /// <summary>
        /// サンプルCompareをデバッグしながら確認する
        /// </summary>
        /// <returns></returns>
        private static List<Student> GetStudents()
        {
            var students = new List<Student>()
            {
                new Student(3, "Taro Hoge", 60, 60, 60),
                new Student(2, "Hana Foo", 20, 28, 26),
                new Student(1, "山田 一郎", 42, 61, 33),
                new Student(4, "北村 善太", 90, 20, 50),
                new Student(8, "中村 裕也", 100, 100, 100),
            };
            return students;
        }
        /// <summary>
        /// 速度確認用に大量データを作る
        /// </summary>
        /// <returns></returns>
        private static List<Student> GetStudentsMany()
        {
            List<string> names = new List<string>()
            {
                "hoge",
                "fuga",
                "yamada",
                "oda"
            };
            var students = new List<Student>();
            var rand = new Random();
            for (int i = 0; i < 100000; i++)
            {
                var index = rand.Next(0, 3);
                students.Add(new Student(i, names[index] + i.ToString(), rand.Next(0,100), rand.Next(0, 100), rand.Next(0, 100)));
            }
            return students;
        }
    }
}

CompareSample.cs

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace Learning.CompareSample
{
    /// <summary>
    /// IComparer<Student>,IComparable<Student>,IComparableのサンプル
    /// </summary>
    class CompareSample
    {
        private List<Student> _students;
        private bool _isWriteConsole = false;
        private Stopwatch _sw;

        /// <summary>
        /// コンストラクターでリストを作成
        /// </summary>
        public CompareSample(List<Student> students,bool isWriteConsole)
        {
            this._students = students;
            this._isWriteConsole = isWriteConsole;
            _sw = new System.Diagnostics.Stopwatch();
        }

        /// <summary>
        /// サンプルの実行
        /// </summary>
        public void Execute()
        {
            // オブジェクト型の配列。Sortで何が使われるかを確認する用
            var objStudents = CreateArrayStudentObj();

            // IComparableが実行するところを確認する
            SortObjStudents(objStudents);

            // IComparable<Student>を使う
            SortGenericIComparable();

            // 名前でソート
            SortByName();
            // 数学でソート
            SortByMath();
            // 国語でソート
            SortByJapanese();
            // 英語でソート
            SortByEnglishUseLazyComparer();

            // こちらはComparison<T>のサンプルとなります。
            SortAllScoreUseComparison();

            // ちなみに、上記ではstaticでStudent内部にComparison<T>を実装しましたが、以下のようにラムダ式でも実装できる。
            // ところで以下の実装だとすべてが等しくなるので順序は変化しない。
            _students.Sort((x, y) =>
            {
                return 0;
            } );

            // IComparable.CompareTo()の説明用
            var student = new Student(0,"",0,0,0);
            var studentDummy = new StudentDummy();
            // 以下のコードはコンパイルできない。
            //if (student.CompareTo(studentDummy) > 0)
            //{

            //}

            // IComparable.CompareToの呼び出し。ここまで明示的にする必要があるので間違って使う可能性が低い。
            if (((IComparable)student).CompareTo(studentDummy) > 0)
            {
            }
        }

        /// <summary>
        /// ArrayListは非推奨。.NET Frameworkバージョン2.0以前で使われていたようです。
        /// ArrayListクラスの要素はObject型となります。
        /// .NET Frameworkバージョン2.0からはListが登場したので、それを使う。
        /// ただ、今回はサンプルでIComparableを使うので必要。
        /// </summary>
        /// <returns></returns>
        private ArrayList CreateArrayStudentObj()
        {
            // Studentに実装した非ジェネリック版のIComparableを使うためにArrayListを使う。
            // object型で格納される。
            ArrayList objStudents = new ArrayList();
            foreach (var student in _students)
            {
                objStudents.Add(student);
            }

            return objStudents;
        }

        /// <summary>
        /// 標準のソートルール,IDで比較(IComparableが使われる)を確認
        /// </summary>
        /// <param name="objStudents"></param>
        private void SortObjStudents(ArrayList objStudents)
        {
            _sw.Restart();
            // 標準のソートルール,IDで比較(IComparableが使われる)
            // StudentにIComparableを実装していないとSort()のタイミングでSystem.InvalidOperationExceptionが出る
            objStudents.Sort();
            _sw.Stop();
            Console.WriteLine(nameof(SortObjStudents) + ":" +_sw.Elapsed.TotalMilliseconds);
            var ret = objStudents.Cast<Student>();
            WriteSortResult(ret);
        }

        /// <summary>
        /// ジェネリック版の標準ソートルール確認
        /// </summary>
        private void SortGenericIComparable()
        {
            _sw.Restart();
            // 標準のソートルール(IComparable<Student>が使われる)
            // ちなみに、List<T>に実装されているSortを使うと、students自体の順番が変化する。
            // students.OrderBy...だとstudentsは変化しません。
            _students.Sort();
            _sw.Stop();
            Console.WriteLine(nameof(SortGenericIComparable) + ":" + _sw.Elapsed.TotalMilliseconds);
            WriteSortResult(_students);
        }

        /// <summary>
        /// 名前でソート
        /// </summary>
        private void SortByName()
        {
            _sw.Restart();
            // 名前のソートルール 以下コメントアウトの方法でソートするとstudents自体の順番が並び変わってしまう
            //students.Sort(new StudentNameComp());
            var tmpStudent = _students.OrderBy(x => x, new StudentNameComp()).ToList();
            _sw.Stop();
            Console.WriteLine(nameof(SortByName) + ":" + _sw.Elapsed.TotalMilliseconds);
            WriteSortResult(tmpStudent);
        }

        /// <summary>
        /// 数学のソート
        /// </summary>
        private void SortByMath()
        {
            _sw.Restart();
            // 数学のソートルール(OrderByだと評価の低い順)
            //students.Sort(new StudentMathScoreComp());
            var tmpStudent = _students.OrderBy(x => x, new StudentMathScoreComp()).ToList();
            _sw.Stop();
            Console.WriteLine(nameof(SortByMath) + ":" + _sw.Elapsed.TotalMilliseconds);
            WriteSortResult(tmpStudent);
        }


        /// <summary>
        /// 国語のソート
        /// </summary>
        private void SortByJapanese()
        {
            _sw.Restart();
            var tmpStudent = _students.OrderByDescending(x => x, new StudentJapaneseScoreComp()).ToList();
            _sw.Stop();
            Console.WriteLine(nameof(SortByJapanese) + ":" + _sw.Elapsed.TotalMilliseconds);
            WriteSortResult(tmpStudent);
        }

        /// <summary>
        /// 英語のソート
        /// </summary>
        private void SortByEnglishUseLazyComparer()
        {
            _sw.Restart();
            var tmpStudent = _students.OrderByDescending(x => x, Student.StudentEnglishScoreComarer).ToList();
            _sw.Stop();
            Console.WriteLine(nameof(SortByEnglishUseLazyComparer) + ":" + _sw.Elapsed.TotalMilliseconds);
            WriteSortResult(tmpStudent);
        }

        /// <summary>
        /// 数学,国語,英語の評価の合計で比較する。
        /// </summary>
        private void SortAllScoreUseComparison()
        {
            _sw.Restart();
            _students.Sort(Student.CompareAllScore);
            _sw.Stop();
            Console.WriteLine(nameof(SortAllScoreUseComparison) + ":" + _sw.Elapsed.TotalMilliseconds);
            WriteSortResult(_students);
        }

        /// <summary>
        /// 結果を出力
        /// </summary>
        /// <param name="students"></param>
        private void WriteSortResult(IEnumerable<Student> students)
        {
            if (!_isWriteConsole || students == null)
            {
                return;
            }
            foreach (var student in students)
            {
                Console.WriteLine(student.ToString());
            }

            Console.WriteLine();
        }
    }
}

Student.cs

using System;
using System.Collections.Generic;

namespace Learning.CompareSample
{
    /// <summary>
    /// 学生クラス
    /// </summary>
    class Student : IComparable<Student>, IComparable
    {
        /// <summary>
        /// 学生番号
        /// </summary>
        public int Id { get; }

        /// <summary>
        /// 名前
        /// </summary>
        public string Name { get; }

        /// <summary>
        /// 数学の評価
        /// </summary>
        public int MathScore { get; }

        /// <summary>
        /// 日本語の評価
        /// </summary>
        public int JapaneseScore { get; }

        /// <summary>
        /// 英語の評価
        /// </summary>
        public int EnglishScore { get; }

        /// <summary>
        /// ToString メソッドをオーバーライド
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            return $@"ID:{this.Id},名前:{this.Name},数学:{this.MathScore},国語:{this.JapaneseScore},英語:{this.EnglishScore}";
        }

        /// <summary>
        /// 標準の比較ルール(IDで比較)
        /// </summary>
        /// <param name="other"></param>
        /// <returns></returns>
        public int CompareTo(Student other)
        {
            return this.Id.CompareTo(other.Id);
        }

        /// <summary>
        /// 標準の比較ルール
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        int IComparable.CompareTo(object obj)
        {
            if (obj == null) return 1;

            var other = obj as Student;
            if (other != null)
            {
                return this.Id.CompareTo(other.Id);
            }
            else
            {
                throw new ArgumentException("Object is not a Student");
            }
        }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="id"></param>
        /// <param name="name"></param>
        /// <param name="mathScore"></param>
        /// <param name="japaneseScore"></param>
        /// <param name="englishScore"></param>
        public Student(int id, string name, int mathScore, int japaneseScore, int englishScore)
        {
            Id = id;
            Name = name;
            MathScore = mathScore;
            JapaneseScore = japaneseScore;
            EnglishScore = englishScore;
        }

        /// <summary>
        /// Comparison<T>のサンプル。すべての評価で比較する
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <returns></returns>
        public static int CompareAllScore(Student x, Student y)
        {
            if (x == null)
            {
                if (y == null)
                {
                    // xもyもnullならイコール
                    return 0;
                }
                else
                {
                    // x==null,y!=nullならyのほうが大きいと判断
                    return -1;
                }
            }
            else
            {
                // x!=nullの時
                if (y == null)
                {
                    // yがnullならxが大きいい
                    return 1;
                }
                else
                {
                    // すべての評価で比較
                    var xSum = x.MathScore + x.JapaneseScore + x.EnglishScore;
                    var ySum = y.MathScore + y.JapaneseScore + y.EnglishScore;
                    return xSum.CompareTo(ySum);
                }
            }

        }

        /// <summary>
        /// StudentEnglishScoreComarerが呼び出されるまでStudentEnglishScoreCompはインスタンス化しない
        /// </summary>
        private static readonly Lazy<StudentEnglishScoreComparer> studentEnglishScoreComarer =
            new Lazy<StudentEnglishScoreComparer>(() => new StudentEnglishScoreComparer());

        public static IComparer<Student> StudentEnglishScoreComarer => studentEnglishScoreComarer.Value;
    }
    /// <summary>
    /// IComparable.CompareTo()の説明用
    /// </summary>
    class StudentDummy
    {
    }
}

StudentNameComp.cs

using System.Collections.Generic;

namespace Learning.CompareSample
{
    /// <summary>
    /// Name比較
    /// </summary>
    class StudentNameComp : IComparer<Student>
    {
        public int Compare(Student x, Student y)
        {
            return x.Name.CompareTo(y.Name);
        }
    }
}

他のIComparerはGitHubのリポジトリで確認してください。

ソースの説明

ソース自体にもコメントを残していますが、念のため少し補足をしていきます。

IComparableを実装した理由

今回、 非ジェネリック版のIComparableを実装した理由は二つあります。

一つは、後方互換性のため

二つ目は、一部のBCL(Base Class Library)で互換性が要求される。

私はもともと IComparable < T > だけを実装する予定でしたが、 IComparableも実装するのがセオリーのようです。

BillWagner. Effective C# 6.0/7.0の 項目 20 IComparable < T > と IComparer < T > により順序関係を実装する

IComparable < T > は. NET の 新 しめ の API で 使用 さ れ て いる インター フェイス です。 しかし 一部 の API では 依然として IComparable インター フェイス が 使用 さ れ て い ます。 したがって、 IComparable < T > を 実装 する 場合 には IComparable も 併せ て 実装 す べき です。

IComparable.CompareTo()を明示的に呼び出す

こちらは、明示的にインターフェースを経由して使用することを義務付けています。こうすることで、誤って非ジェネリック版が使われないようになります。

IComparableでのSortが思ったより早い(途中)

今回の例で、大量のデータを並び替えたら順序比較の速度がどうなるか気になりました。

IComparableでのSortがもっと遅くなるのかな?と思いましたが、今回のケースではそこまで遅くなさそうでした。

それよりも文字列の比較のほうが時間がかかるなーという印象がありました。

IComparableを実装してみて

実装した感想としては、型に対してIComparableする機会はそんなに多くないかなと思いました。

例にしたクラスのように、並び替えを頻繁にするような時は、そのクラスに実装するかもしれません。

ただ、私は考えながら実装していくパターンが多いので、初めから順序を意識するクラスになるなという予見ができない気がします。

まずはラムダ式で必要なタイミングでソートかけて、そういう処理が多いなと感じ始めたらリファクタリングでIComparableを実装していくと思います。

もちろん、事前にそうなるなと想定できるのなら、IComparableをはじめから実装していけばよいと思います。

C#

Posted by takumioda