ソートを行うためには、並べ替えを行う個々の要素についてその大小関係を調べる必要があります。 また、独自に作成したクラスをソートするためには、その大小関係を定義する必要があります。

ここでは、コレクションとソートの基本と、大小関係を定義したり比較するためのインターフェイスであるIComparable・IComparerおよび関連するクラスについて見ていきます。 また、比較演算子のオーバーロードとこれらのインターフェイスの実装についても触れています。 ここで解説するソート方法やDictionaryのソートなどについてはソートでも解説しているので、必要に応じて参照してください。

§1 コレクションとソート

.NET Frameworkでは、ソートを行うためのメソッドとしてArray.SortArrayList.SortList<T>.Sortなどのメソッドが用意されています。

Array.Sortメソッドは引数で指定された配列のソートを行い、ArrayList.SortメソッドとList<T>.Sortメソッドはインスタンス内に格納されている要素をソートします。 これらのメソッドは、いずれもクイックソートを行います。 以下のコードは、これらソートを行うメソッドを使った例です。

using System;
using System.Collections;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    string[] arr = new string[] {"Charlie", "Dave", "Alice", "Eve", "Bob"};
    ArrayList al = new ArrayList(arr);
    List<string> l = new List<string>(arr);

    Console.WriteLine("[Array.Sort]");

    Print(arr);

    Array.Sort(arr);

    Print(arr);

    Console.WriteLine("[ArrayList.Sort]");

    Print(al);

    al.Sort();

    Print(al);

    Console.WriteLine("[List.Sort]");

    Print(l);

    l.Sort();

    Print(l);
  }

  static void Print(IEnumerable c)
  {
    foreach (string e in c) {
      Console.Write("{0}, ", e);
    }

    Console.WriteLine();
  }
}
実行結果
[Array.Sort]
Charlie, Dave, Alice, Eve, Bob, 
Alice, Bob, Charlie, Dave, Eve, 
[ArrayList.Sort]
Charlie, Dave, Alice, Eve, Bob, 
Alice, Bob, Charlie, Dave, Eve, 
[List.Sort]
Charlie, Dave, Alice, Eve, Bob, 
Alice, Bob, Charlie, Dave, Eve, 

これらのメソッドでは、引数でIComparerインターフェイスやComparisonデリゲートなどを指定することで並べ替えの際の動作を変更することができるようになっています。 詳細については後述していきます。

§1.1 ソートできる型

先の例では、文字列型の値をコレクションに格納してソートしましたが、int、double、DateTimeなど他の基本型でも同様にソートすることが出来ます。

using System;
using System.Collections;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    int[] arr = new int[] {3, 0, 1, 4, 2};
    ArrayList al = new ArrayList(arr);
    List<int> l = new List<int>(arr);

    Console.WriteLine("[Array.Sort]");

    Print(arr);

    Array.Sort(arr);

    Print(arr);

    Console.WriteLine("[ArrayList.Sort]");

    Print(al);

    al.Sort();

    Print(al);

    Console.WriteLine("[List.Sort]");

    Print(l);

    l.Sort();

    Print(l);
  }

  static void Print(IEnumerable c)
  {
    foreach (int e in c) {
      Console.Write("{0}, ", e);
    }

    Console.WriteLine();
  }
}
実行結果
[Array.Sort]
3, 0, 1, 4, 2, 
0, 1, 2, 3, 4, 
[ArrayList.Sort]
3, 0, 1, 4, 2, 
0, 1, 2, 3, 4, 
[List.Sort]
3, 0, 1, 4, 2, 
0, 1, 2, 3, 4,

§1.2 ソートできない型

このように、基本型ならばソートは可能なように見えます。 どのような型がソート可能となり、ソート可能とならないのか、その違いを見るために文字列をラップするクラスMyStringを作成し、先の例と同様にソートしてみます。

using System;
using System.Collections;
using System.Collections.Generic;

class MyString {
  string val;

  public MyString(string val)
  {
    this.val = val;
  }

  public override string ToString()
  {
    return val;
  }
}

class Sample {
  static void Main()
  {
    MyString[] arr = new MyString[] {
      new MyString("Charlie"),
      new MyString("Dave"),
      new MyString("Alice"),
      new MyString("Eve"),
      new MyString("Bob")
    };
    ArrayList al = new ArrayList(arr);
    List<MyString> l = new List<MyString>(arr);

    Console.WriteLine("[Array.Sort]");

    Print(arr);

    try {
      Array.Sort(arr);

      Print(arr);
    }
    catch (Exception ex) {
      Console.WriteLine(ex);
    }

    Console.WriteLine("[ArrayList.Sort]");

    Print(al);

    try {
      al.Sort();

      Print(al);
    }
    catch (Exception ex) {
      Console.WriteLine(ex);
    }

    Console.WriteLine("[List.Sort]");

    Print(l);

    try {
      l.Sort();

      Print(l);
    }
    catch (Exception ex) {
      Console.WriteLine(ex);
    }
  }

  static void Print(IEnumerable c)
  {
    foreach (MyString e in c) {
      Console.Write("{0}, ", e);
    }

    Console.WriteLine();
  }
}
実行結果
[Array.Sort]
Charlie, Dave, Alice, Eve, Bob, 
System.InvalidOperationException: 配列にある 2 つの要素を比較できませんでした。 ---> System.ArgumentException: 少なくとも 1 つのオブジェクトで IComparable を実装しなければなりません。
   場所 System.Collections.Comparer.Compare(Object a, Object b)
   場所 System.Collections.Generic.ObjectComparer`1.Compare(T x, T y)
   場所 System.Collections.Generic.ArraySortHelper`1.SwapIfGreaterWithItems(T[] keys, IComparer`1 comparer, Int32 a, Int32 b)
   場所 System.Collections.Generic.ArraySortHelper`1.QuickSort(T[] keys, Int32 left, Int32 right, IComparer`1 comparer)
   場所 System.Collections.Generic.ArraySortHelper`1.Sort(T[] keys, Int32 index, Int32 length, IComparer`1 comparer)
   --- 内部例外スタック トレースの終わり ---
   場所 System.Collections.Generic.ArraySortHelper`1.Sort(T[] keys, Int32 index, Int32 length, IComparer`1 comparer)
   場所 System.Array.Sort[T](T[] array, Int32 index, Int32 length, IComparer`1 comparer)
   場所 System.Array.Sort[T](T[] array)
   場所 Sample.Main()
[ArrayList.Sort]
Charlie, Dave, Alice, Eve, Bob, 
System.InvalidOperationException: 配列にある 2 つの要素を比較できませんでした。 ---> System.ArgumentException: 少なくとも 1 つのオブジェクトで IComparable を実装しなければなりません。
   場所 System.Collections.Comparer.Compare(Object a, Object b)
   場所 System.Array.SorterObjectArray.SwapIfGreaterWithItems(Int32 a, Int32 b)
   --- 内部例外スタック トレースの終わり ---
   場所 System.Array.SorterObjectArray.SwapIfGreaterWithItems(Int32 a, Int32 b)
   場所 System.Array.SorterObjectArray.QuickSort(Int32 left, Int32 right)
   場所 System.Array.Sort(Array keys, Array items, Int32 index, Int32 length, IComparer comparer)
   場所 System.Collections.ArrayList.Sort(Int32 index, Int32 count, IComparer comparer)
   場所 System.Collections.ArrayList.Sort()
   場所 Sample.Main()
[List.Sort]
Charlie, Dave, Alice, Eve, Bob, 
System.InvalidOperationException: 配列にある 2 つの要素を比較できませんでした。 ---> System.ArgumentException: 少なくとも 1 つのオブジェクトで IComparable を実装しなければなりません。
   場所 System.Collections.Comparer.Compare(Object a, Object b)
   場所 System.Collections.Generic.ObjectComparer`1.Compare(T x, T y)
   場所 System.Collections.Generic.ArraySortHelper`1.SwapIfGreaterWithItems(T[] keys, IComparer`1 comparer, Int32 a, Int32 b)
   場所 System.Collections.Generic.ArraySortHelper`1.QuickSort(T[] keys, Int32 left, Int32 right, IComparer`1 comparer)
   場所 System.Collections.Generic.ArraySortHelper`1.Sort(T[] keys, Int32 index, Int32 length, IComparer`1 comparer)
   --- 内部例外スタック トレースの終わり ---
   場所 System.Collections.Generic.ArraySortHelper`1.Sort(T[] keys, Int32 index, Int32 length, IComparer`1 comparer)
   場所 System.Array.Sort[T](T[] array, Int32 index, Int32 length, IComparer`1 comparer)
   場所 System.Collections.Generic.List`1.Sort(Int32 index, Int32 count, IComparer`1 comparer)
   場所 Sample.Main()

実行結果を見て分かる通り、Sortメソッドを呼び出した時点で例外がスローされ、ソートは出来ませんでした。 何が原因でソート出来なかったのか、スローされた例外について詳しく見みます。 スローされた例外はInvalidOperationExceptionで、例外のメッセージは次のようになっています。

スローされたはInvalidOperationExceptionの例外メッセージ
System.InvalidOperationException: 配列にある 2 つの要素を比較できませんでした。 ---> System.ArgumentException: 少なくとも 1 つのオブジェクトで IComparable を実装しなければなりません。

つまり、配列(コレクション)内にある要素を比較しようとしたが、その要素がIComparableインターフェイスを実装していないことを理由にArgumentExceptionをスローしたために、InvalidOperationExceptionとなったことがソート出来なかった原因となります。 言い換えると、ソートを行うには、その要素がIComparableを実装している必要があるということです。 ここまで例に挙げたstring、int、double、DateTimeは、すべてIComparableを実装しているためソートが可能だったのです。



§2 IComparable

それでは、ソートを行う上で必要とされるIComparableインターフェイス(System名前空間)を実装するためにIComparableについて詳しく見ていきます。 IComparableインターフェイスを実装する場合は一つのメソッドCompareToを実装しなければなりません。 このメソッドでは、一つのobject型の引数を取り、インターフェイスを実装するインスタンスと引数のオブジェクトとの大小関係に応じて次の値を返す必要があります。

IComparable.CompareToの戻り値
インスタンスと引数objの大小関係 戻り値として返す値
並べ替えたときに、現在のインスタンスの方が引数objよりもとなる
(thisはobjよりも小さい)
0より小さい値
並べ替えたときに、現在のインスタンスと引数objは同じ位置となる
(thisはobjと等しい)
0
並べ替えたときに、現在のインスタンスの方が引数objよりもとなる
(thisはobjよりも大きい)
0より大きい値

早速、上記の仕様を満たすようIComparableを実装したクラスを用意し、ソートしてみます。 次のコードでは、メンバにIDと名前を持つクラスAccountを作成し、IComparableを実装しています。

using System;
using System.Collections;

class Account : IComparable {
  int id;
  string name;

  public Account(int id, string name)
  {
    this.id = id;
    this.name = name;
  }

  // IComparable.CompareToの実装
  public int CompareTo(object obj)
  {
    // 引数がnullの場合はArgumentNullExceptionをスローする
    if (obj == null) throw new ArgumentNullException();

    // 引数をAccountに型変換
    Account other = obj as Account;

    // 引数をAccountに型変換できなかった場合はArgumentExceptionをスローする
    if (other == null) throw new ArgumentException();

    // フィールドidの値の大小関係をCompareToの戻り値とする
    if (this.id < other.id) {
      // 現在のインスタンスは、引数よりも小さい
      return -1;
    }
    else if (this.id > other.id) {
      // 現在のインスタンスは、引数よりも大きい
      return 1;
    }
    else {
      // 現在のインスタンスは、引数よりは等しい
      return 0;
    }

    // より簡単に、以下のように記述することもできる
    //return this.id - other.id;
    // また、Integer.CompareToメソッドの結果を使用するように記述することもできる
    //return this.id.CompareTo(other.id);
  }

  public override string ToString()
  {
    return string.Format("{0}:{1}", id, name);
  }
}

class Sample {
  static void Main()
  {
    Account[] arr = new Account[] {
      new Account(1, "Eve"),
      new Account(4, "Dave"),
      new Account(2, "Alice"),
      new Account(0, "Charlie"),
      new Account(3, "Bob"),
    };

    Console.WriteLine("[before sort]");

    Print(arr);

    Console.WriteLine("[after sort]");

    Array.Sort(arr);

    Print(arr);
  }

  static void Print(IEnumerable c)
  {
    foreach (Account e in c) {
      Console.WriteLine(e);
    }
  }
}
実行結果
[before sort]
1:Eve
4:Dave
2:Alice
0:Charlie
3:Bob
[after sort]
0:Charlie
1:Eve
2:Alice
3:Bob
4:Dave

結果を見てわかるとおり、ソートの際にInvalidOperationExceptionがスローされることもなく、Accountクラスのidフィールドの値にしたがってソート出来ていることが分かると思います。

この例ではArray.Sortメソッドを使ったため、Accountクラスのインスタンス同士のみの比較しか行われませんが、ArrayListなどを使う場合はすべて同じ型同士の比較になるとは限りません。 IComparable.CompareToメソッドは引数がobject型であるため、どのような型との比較になるかは呼び出し元次第となります。 そのため、IComparableインターフェイスを実装する側で引数の型をチェックする必要があります。 上記のコードでは異なる型との比較ではArgumentExceptionをスローするようにしていますが、場合によっては例外とはせず適切な比較結果を返すように実装する事も可能です。

また、null(Nothing)との比較についても、上記のコードではArgumentNullExceptionをスローするようにしていますが、string型のようにnullはすべてのオブジェクトよりも小さいと定義することもできます。

§3 IComparer

IComparableに似たインターフェイスとして、.NET FrameworkにはIComparerインターフェイス(System.Collections名前空間)が用意されています。 IComparableがインターフェイスを実装するオブジェクトと他のオブジェクトとの比較処理を提供するのに対し、IComparerは任意の二つのオブジェクトの比較処理を提供するためのものです。 言い換えると、IComparableでは比較処理は常に比較される対象と結びついていますが、IComparerでは比較処理と比較される対象を分離させることが出来ます。

このことにどのような利点があるかというと、例えば先に例に挙げたAccountクラスでは名前とIDのフィールドを持っていますが、IComparableで実装したのはIDによる比較のみです。 これを名前順やIDの降順で並べ替えたりしたい場合、IComparableの実装を書き換える必要が出てきますが、IComparerを使うことで実装を書き換えずに新たな比較処理を個別に用意することができるようになります。

IComparerインターフェイスを実装する場合は一つのメソッドCompareを実装しなければなりません。 IComparable.CompareToメソッドが一つのobject型の引数を取るのに対し、IComparer.Compareメソッドでは二つのobject型の引数を取ります。 戻り値は、IComparable.CompareToメソッドと同様、引数同士の大小関係に応じて次の値を返す必要があります。

IComparer.Compareの戻り値
引数xとyの大小関係 戻り値として返す値
並べ替えたときに、引数xが引数yよりもとなる
(xはyよりも小さい)
0より小さい値
並べ替えたときに、引数xが引数yと同じ位置となる
(xはyと等しい)
0
並べ替えたときに、引数xが引数yよりもとなる
(xはyよりも大きい)
0より大きい値

早速、上記の仕様を満たすようIComparerを実装したクラスを用意し、ソートしてみます。 次のコードでは、IComparerを実装してint型の配列を昇順・降順で並べ替えるためのクラスを作成し、ソートに使っています。

using System;
using System.Collections;

// intを昇順で並べ替えるIComparerの実装
class AscendingOrderComparer : IComparer {
  public int Compare(object x, object y)
  {
    int ix = (int)x;
    int iy = (int)y;

    return ix - iy;
    // 次のように記述することもできる
    //return ix.CompareTo(iy);
  }
}

// intを降順で並べ替えるIComparerの実装
class DescendingOrderComparer : IComparer {
  public int Compare(object x, object y)
  {
    int ix = (int)x;
    int iy = (int)y;

    return iy - ix;
    // 次のように記述することもできる
    //return iy.CompareTo(ix);
    //return -ix.CompareTo(iy);
  }
}

class Sample {
  static void Main()
  {
    int[] arr = new int[] {3, 4, 2, 0, 1};

    Console.WriteLine("[before sort]");

    Print(arr);

    // AscendingOrderComparerを使ってソート
    Console.WriteLine("[AscendingOrderComparer]");

    Array.Sort(arr, new AscendingOrderComparer());

    Print(arr);

    // DescendingOrderComparerを使ってソート
    Console.WriteLine("[DescendingOrderComparer]");

    Array.Sort(arr, new DescendingOrderComparer());

    Print(arr);

    // Array.SortメソッドとArray.Reverseメソッドを使ってソート
    Console.WriteLine("[Sort + Reverse]");

    Array.Sort(arr);

    Print(arr);

    Array.Reverse(arr);

    Print(arr);
  }

  static void Print(IEnumerable c)
  {
    foreach (int e in c) {
      Console.Write("{0}, ", e);
    }

    Console.WriteLine();
  }
}
実行結果
[before sort]
3, 4, 2, 0, 1, 
[AscendingOrderComparer]
0, 1, 2, 3, 4, 
[DescendingOrderComparer]
4, 3, 2, 1, 0, 
[Sort + Reverse]
0, 1, 2, 3, 4, 
4, 3, 2, 1, 0, 

Array.Sortメソッドでは引数にIComparerを指定すると、そのIComparerを使って並べ替えが行われるようになります。 その結果、上記のように昇順・降順でのソートが行えるようになります。 なお、上記のコードでは、比較のためにIComparerを使わずArray.SortメソッドとArray.Reverseメソッドを使って降順にソートする例も記述しています。

もう少し違った例を挙げてみます。 先ほどのAccountクラスと、これを比較するためのAccountComparerクラスを用意してソートします。 AccountComparerには、比較の際に昇順・降順および名前順・ID順をオプションとして選べるようプロパティを用意します。

using System;
using System.Collections;

class Account {
  private int id;
  private string name;

  public int ID {
    get { return id; }
  }

  public string Name {
    get { return name; }
  }

  public Account(int id, string name)
  {
    this.id = id;
    this.name = name;
  }

  public override string ToString()
  {
    return string.Format("{0}:{1}", id, name);
  }
}

class AccountComparer : IComparer {
  public enum SortMode {
    ByID, // ID順でソート
    ByName, // 名前順でソート
  }

  public enum SortOrder : int {
    Ascending = +1, // 昇順でソート
    Descending = -1, // 降順でソート
  }

  private SortMode mode = SortMode.ByID;
  private SortOrder order = SortOrder.Ascending;

  public SortMode Mode {
    get { return mode; }
    set { mode = value; }
  }

  public SortOrder Order {
    get { return order; }
    set { order = value; }
  }

  public int Compare(object x, object y)
  {
    // 引数がnullの場合はArgumentNullExceptionをスローする
    if (x == null) throw new ArgumentNullException("x");
    if (y == null) throw new ArgumentNullException("y");

    Account ax = x as Account;
    Account ay = y as Account;

    // Accountクラスに型変換出来ない場合は、ArgumentExceptionをスローする
    if (ax == null) throw new ArgumentException();
    if (ay == null) throw new ArgumentException();

    switch (mode) {
      case SortMode.ByID:
        return (int)order * (ax.ID - ay.ID);

      case SortMode.ByName:
        return (int)order * string.Compare(ax.Name, ay.Name);

      default:
        throw new InvalidOperationException("未定義のSortMode");
    }
  }
}

class Sample {
  static void Main()
  {
    Account[] arr = new Account[] {
      new Account(1, "Eve"),
      new Account(4, "Dave"),
      new Account(2, "Alice"),
      new Account(0, "Charlie"),
      new Account(3, "Bob"),
    };

    AccountComparer comparer = new AccountComparer();

    Print(arr);

    Console.WriteLine("[{0} {1}]", comparer.Mode, comparer.Order);

    Array.Sort(arr, comparer);

    Print(arr);

    comparer.Mode = AccountComparer.SortMode.ByName;

    Console.WriteLine("[{0} {1}]", comparer.Mode, comparer.Order);

    Array.Sort(arr, comparer);

    Print(arr);

    comparer.Order = AccountComparer.SortOrder.Descending;

    Console.WriteLine("[{0} {1}]", comparer.Mode, comparer.Order);

    Array.Sort(arr, comparer);

    Print(arr);
  }

  static void Print(IEnumerable c)
  {
    foreach (Account e in c) {
      Console.WriteLine(e);
    }
  }
}
実行結果
1:Eve
4:Dave
2:Alice
0:Charlie
3:Bob
[ByID Ascending]
0:Charlie
1:Eve
2:Alice
3:Bob
4:Dave
[ByName Ascending]
2:Alice
3:Bob
0:Charlie
4:Dave
1:Eve
[ByName Descending]
1:Eve
4:Dave
0:Charlie
3:Bob
2:Alice

この例を見てわかるとおり、比較される側であるAccountクラスでは、必ずしもIComparerを実装している必要はありません。 また、IComparerを実装する側では、比較対象に合わせて比較処理を定義することができます。

§4 IComparable<T>

IComparable<T>インターフェイス(System名前空間)はIComparableインターフェイスのジェネリック版で、型パラメータTで指定された型との比較が可能であることを表すインターフェイスです。 IComparableと同様CompareToメソッドを実装する必要がありますが、引数の型はobjectではなく型パラメータで指定された型となるため、実装に際して型のチェックをする必要が無くなります。

以下の例は、IComparableでの例をIComparable<T>インターフェイス使ったものに書き換えたものです。

using System;
using System.Collections.Generic;

class Account : IComparable<Account> {
  int id;
  string name;

  public Account(int id, string name)
  {
    this.id = id;
    this.name = name;
  }

  // IComparable<Account>.CompareToの実装
  public int CompareTo(Account other)
  {
    // 引数がnullの場合はArgumentNullExceptionをスローする
    if (other == null) throw new ArgumentNullException("other");

    // フィールドidの値の大小関係をCompareToの戻り値とする
    return this.id - other.id;
  }

  public override string ToString()
  {
    return string.Format("{0}:{1}", id, name);
  }
}

class Sample {
  static void Main()
  {
    Account[] arr = new Account[] {
      new Account(1, "Eve"),
      new Account(4, "Dave"),
      new Account(2, "Alice"),
      new Account(0, "Charlie"),
      new Account(3, "Bob"),
    };

    Console.WriteLine("[before sort]");

    Print(arr);

    Console.WriteLine("[after sort]");

    Array.Sort(arr);

    Print(arr);
  }

  static void Print(IEnumerable<Account> c)
  {
    foreach (Account e in c) {
      Console.WriteLine(e);
    }
  }
}
実行結果
[before sort]
1:Eve
4:Dave
2:Alice
0:Charlie
3:Bob
[after sort]
0:Charlie
1:Eve
2:Alice
3:Bob
4:Dave

実装は省略しますが、次のコードのように型パラメータTで指定する型を変えて複数のIComparable<T>を実装することも出来ます。

複数の型に対応するIComparable<T>を実装する
using System;

class Account : IComparable<Account>, IComparable<int>, IComparable<string> {
  // IComparable<Account>.CompareToの実装
  public int CompareTo(Account other)
  {
    // Accountとの比較処理
  }

  // IComparable<int>.CompareToの実装
  public int CompareTo(int other)
  {
    // intとの比較処理
  }
  // IComparable<string>.CompareToの実装
  public int CompareTo(string other)
  {
    // stringとの比較処理
  }
}

§5 IComparer<T>

IComparer<T>インターフェイス(System.Collections.Generic名前空間)はIComparerインターフェイスのジェネリック版で、型パラメータTで指定された型に特化した比較処理を提供するためのインターフェイスです。 IComparerと同様Compareメソッドを実装する必要がありますが、引数の型はobjectではなく型パラメータで指定された型となるため、実装に際して型のチェックをする必要が無くなります。

以下の例は、IComparerでの例をIComparer<T>インターフェイス使ったものに書き換えたものです。

using System;
using System.Collections.Generic;

// 昇順で並べ替えるIComparer<int>の実装
class AscendingOrderComparer : IComparer<int> {
  public int Compare(int x, int y)
  {
    return x - y;
  }
}

// 降順で並べ替えるIComparer<int>の実装
class DescendingOrderComparer : IComparer<int> {
  public int Compare(int x, int y)
  {
    return y - x;
  }
}

class Sample {
  static void Main()
  {
    int[] arr = new int[] {3, 4, 2, 0, 1};

    Console.WriteLine("[before sort]");

    Print(arr);

    // AscendingOrderComparerを使ってソート
    Console.WriteLine("[AscendingOrderComparer]");

    Array.Sort(arr, new AscendingOrderComparer());

    Print(arr);

    // DescendingOrderComparerを使ってソート
    Console.WriteLine("[DescendingOrderComparer]");

    Array.Sort(arr, new DescendingOrderComparer());

    Print(arr);
  }

  static void Print(IEnumerable<int> c)
  {
    foreach (int e in c) {
      Console.Write("{0}, ", e);
    }

    Console.WriteLine();
  }
}
実行結果
[before sort]
3, 4, 2, 0, 1, 
[AscendingOrderComparer]
0, 1, 2, 3, 4, 
[DescendingOrderComparer]
4, 3, 2, 1, 0, 

§6 Comparison<T>

Comparison<T>デリゲート(System名前空間)は、二つのオブジェクトを比較するメソッド、つまりIComparer<T>.Compareメソッドのような比較処理を行うメソッドを表すデリゲートです。 Array.SortおよびList<T>.Sortメソッドはこのデリゲートを引数に取ることができ、ソートの際の比較処理に使用することが出来ます。

このデリゲートを用いる利点は、IComparer<T>やIComparable<T>を実装したクラスを用意しなくても、比較処理を定義したメソッドを用意するだけでソートが行える点にあります。 このデリゲートと匿名メソッドやラムダ式を組み合わせて用いることでより簡単に比較処理を記述することも出来ます。

using System;
using System.Collections.Generic;

class Sample {
  // int型の値を降順で比較するメソッド
  static int CompareDescendingOrder(int x, int y)
  {
    return y - x;
  }

  static void Main()
  {
    int[] arr = new int[] {3, 4, 2, 0, 1};

    Console.WriteLine("[before sort]");

    Print(arr);

    // Comparison<int>を指定せずにソート
    Console.WriteLine("[Sort]");

    Array.Sort(arr);

    Print(arr);

    // Comparison<int>を指定してソート
    Comparison<int> comparison = CompareDescendingOrder;

    Console.WriteLine("[Sort + Comparison<int>]");

    Array.Sort(arr, comparison);

    // 単に次のように記述することも出来る
    //Array.Sort(arr, CompareDescendingOrder);
    // 匿名メソッドを使った場合は次のように記述出来る
    //Array.Sort(arr, delegate(int x, int y) {
    //  return y, x
    //});
    // ラムダ式を使った場合は次のように記述出来る
    //Array.Sort(arr, (x, y) => y - x);

    Print(arr);
  }

  static void Print(IEnumerable<int> c)
  {
    foreach (int e in c) {
      Console.Write("{0}, ", e);
    }

    Console.WriteLine();
  }
}
実行結果
[before sort]
3, 4, 2, 0, 1, 
[Sort]
0, 1, 2, 3, 4, 
[Sort + Comparison(Of Integer)]
4, 3, 2, 1, 0, 

§7 Comparer<T>

Comparer<T>クラス(System.Collections.Generic名前空間)は、IComparer非ジェネリックインターフェイスとIComparer<T>ジェネリックインターフェイスを実装する抽象クラスです。 このクラスの機能と存在意義はIComparer<T>と同じですが、IComparerとIComparer<T>の二つのインターフェイスを実装するクラスを作成しなくても、このクラスを継承して抽象メソッドであるCompareメソッドを実装するだけで同じ機能を提供することができるようになります。

ジェネリックな比較処理を提供することを考えたとき、ArrayList.Sortなど非ジェネリックなコレクションのソートでも動作するようにしたい場合は、IComparer<T>だけでなくIComparerも実装する必要があります。 Comparer<T>クラスでは引数の型のチェックなどは既に実装されているため、簡単にジェネリック・非ジェネリック両方の比較処理を提供できるようになります。

以下の例では、Comparer<T>を継承したクラスを作成し、ArrayList.Sortでのソートで使用しています。 ソートしようとしているArrayListの要素にnull(Nothing)が含まれている点に注目してください。

using System;
using System.Collections;
using System.Collections.Generic;

// int型の値を降順で並べ替えるComparer
class DescendingOrderComparer : Comparer<int> {
  public override int Compare(int x, int y)
  {
    return y - x;
  }
}

class Sample {
  static void Main()
  {
    ArrayList al = new ArrayList(new object[] {3, null, 4, 2, 0, 1});

    Console.WriteLine("[before sort]");

    Print(al);

    Console.WriteLine("[after sort]");

    al.Sort(new DescendingOrderComparer());

    Print(al);
  }

  static void Print(IEnumerable c)
  {
    foreach (object e in c) {
      Console.Write("{0}, ", e);
    }

    Console.WriteLine();
  }
}
実行結果
[before sort]
3, , 4, 2, 0, 1, 
[after sort]
, 4, 3, 2, 1, 0, 

なお、Comparer<T>クラスで実装されているIComparer.Compareの比較処理では、null(Nothing)はnull以外のどのような値よりも小さいとみなされます。 この動作を変えたい場合は、派生クラスでIComparer.Compareを再実装する必要があります。

§7.1 Comparer.Create

.NET Framework 4.5からは、任意のComparison<T>デリゲートからComparer<T>クラスのインスタンスを作成するメソッドComparer<T>.Createが追加されました。 このメソッドを使うことにより、Comparer<T>を継承したクラスを作成しなくても、Comparison<T>デリゲートをIComparer<T>として使用することができるようになります。

このメソッドは、IComparer<T>インターフェイスはサポートしているがComparison<T>デリゲートはサポートしていないクラスやメソッドを使用する際に特に便利です。 例えばSortedListでは、コンストラクタにIComparer<T>を指定することで要素のソート順を変更することができますが、Comparison<T>デリゲートを直接指定することはできません。 そのため、既存の比較用メソッドをSortedListで使用しようとした場合、メソッドをComparer<T>を継承(もしくはIComparer<T>を実装)したクラスに書き換える必要がありますが、Comparer<T>.Createメソッドを使えばコードを一切変更することなくそのまま活用することができます。

次の例では、int型の値を降順で比較するメソッドからComparer<int>を作成し、SortedListの並び替え順として使用するようにしています。 これにより、SortedListの内容はキーを降順にした並べ替えられます。

using System;
using System.Collections.Generic;

class Sample {
  // int型の値を降順で比較するメソッド
  static int CompareDescendingOrder(int x, int y)
  {
    return y - x;
  }

  static void Main()
  {
    // メソッドからComparer<int>を作成
    Comparer<int> comparer = Comparer<int>.Create(CompareDescendingOrder);

    // 作成したComparer<int>を指定してSortedListを作成
    SortedList<int, string> idAndName = new SortedList<int, string>(comparer);

    idAndName.Add(1, "Eve");
    idAndName.Add(4, "Dave");
    idAndName.Add(2, "Alice");
    idAndName.Add(0, "Charlie");
    idAndName.Add(3, "Bob");

    // SortedListの内容を表示
    foreach (KeyValuePair<int, string> pair in idAndName) {
      Console.WriteLine("{0}:{1}", pair.Key, pair.Value);
    }
  }
}
実行結果
4:Dave
3:Bob
2:Alice
1:Eve
0:Charlie

§8 StringComparer

StringComparerクラス(System名前空間)は、文字列比較に特化したIComparerの実装です。 大文字小文字を無視した比較、現在のカルチャ・インバリアントカルチャの規則に基づいた比較などを行う実装が用意されています。 詳細については、StringComparerについての個別の解説を参照してください。

using System;

class Sample {
  static void Main()
  {
    string[] arr = new string[] {"亜", "絵", "井", "尾", "宇"};

    Print(arr);

    Console.WriteLine("[StringComparer.CurrentCulture]");

    Array.Sort(arr, StringComparer.CurrentCulture);

    Print(arr);

    Console.WriteLine("[StringComparer.InvariantCulture]");

    Array.Sort(arr, StringComparer.InvariantCulture);

    Print(arr);
  }

  static void Print(string[] arr)
  {
    foreach (string e in arr) {
      Console.Write("{0}, ", e);
    }

    Console.WriteLine();
  }
}
実行結果
亜, 絵, 井, 尾, 宇, 
[StringComparer.CurrentCulture]
亜, 井, 宇, 絵, 尾, 
[StringComparer.InvariantCulture]
井, 亜, 宇, 尾, 絵,