連載
» 2017年03月22日 05時00分 UPDATE

.NET TIPS:Listの要素を並べ替えるには?[C#/VB]

LINQのOrderBy/ThenByなどの拡張メソッドとList<T>クラスのSortメソッドを利用して、Listの要素を並べ替える方法を解説する。

[山本康彦,BluewaterSoft/Microsoft MVP for Windows Development]
「.NET TIPS」のインデックス

連載目次

 最もよく使われるジェネリックコレクションといえばList<T>(C#)/List(Of T)(VB)クラス(System.Collections.Generic名前空間)であろう(以降、型引数はC#での表記だけとさせていただく)。本稿では、そのコレクションに含まれる要素をソートする方法を解説する。

 なお、List<T>クラスは.NET Framework 2.0で導入されたものだが、本稿はそれ以降の内容も含んでいる。サンプルコードをそのまま試すには、Visual Studio 2015(またはそれ以降)が必要である。

List<T>コレクションの要素を並べ替えるには?

 そのコレクションの内容を実際に並べ替えるには、List<T>クラスのSortメソッドを使う。並べ替えた結果を別のコレクションに書き出すには(元のコレクションの並び順を変えないようにするには)、LINQのOrderBy拡張メソッドを使う(次のコード)。

List<T> list = ……省略……

// コレクションの内容を実際に並べ替える(List<T>クラスのSortメソッド)
list.Sort();

// 並べ替えた結果を別のコレクションに書き出す(LINQのOrderBy拡張メソッド)
IOrderedEnumerable<T> sorted = list.OrderBy(x => x.Key);

Dim list As List(Of T) = ……省略……

' コレクションの内容を実際に並べ替える(List<T>クラスのSortメソッド)
list.Sort()

' 並べ替えた結果を別のコレクションに書き出す(LINQのOrderBy拡張メソッド)
Dim sorted As IOrderedEnumerable(Of T) = list.OrderBy(Function(x) x.Key)

List<T>コレクションをソートする基本(上:C#、下:VB)
List<T>クラスの引数を取らないSortメソッドを使うには、その要素が比較可能でなければならない(要素がIComparable<T>インタフェースまたはIComparableインタフェースを実装している必要がある)。
LINQのOrderBy拡張メソッドには、要素を比較するために使う値かオブジェクトを返すラムダ式を与える。例えばx.Keyが整数ならば、OrderBy拡張メソッドは整数の大小比較で並べ替えを行う。

 並べ替えの順序を逆にするには、ソートしてからReverseメソッド(List<T>クラスにもLINQ拡張にもある)を使う方法もあるが、次のコードのようにすれば1回で逆順ソートを実現できる。

List<T> list = ……省略……

// List<T>クラスのSortメソッドにはIComparer<T>インタフェースを使う
IComparer<T> reverseComparer = ……省略……
list.Sort(reverseComparer);

// LINQならOrderByDescending拡張メソッドが使える
IOrderedEnumerable<T> sorted = list.OrderByDescending(x => x.Key);

Dim list As List(Of T) = ……省略……

' List<T>クラスのSortメソッドにはIComparer<T>インタフェースを使う
Dim reverseComparer As IComparer(Of T) = ……省略……
list.Sort(reverseComparer)

' LINQならOrderByDescending拡張メソッドが使える
Dim sorted As IOrderedEnumerable(Of T) _
  = list.OrderByDescending(Function(x) x.Key)

List<T>コレクションを逆順にソートする方法(上:C#、下:VB)
IComparer<T>インタフェースの実装例は後ほど紹介する。

 また、複数のキーでソートするには、List<T>クラスではIComparer<T>インタフェース(System.Collections.Generic名前空間)の実装を書くことになる。LINQでは、OrderBy拡張メソッドの後ろにThenBy拡張メソッドを続けて書けばよい(ThenByDescending拡張メソッドもある)。

実際の例

 実際にList<T>コレクションで並べ替えを行う例を確認してみよう。

 まずは、整数の要素を昇順にソートする例だ。次のコードに、コンソールアプリの例を示す。後でも使うためにDisplayItems<T>メソッドを切り出している。

using System;
using System.Collections.Generic;
using System.Linq;
using static System.Console;

class Program
{
  static void DisplayItems<T>(string msg, IEnumerable<T> collection)
    => WriteLine($"{msg}: {string.Join(", ", collection)}");

  static void Main(string[] args)
  {
    List<int> list = new List<int> { 3, 1, 2 };

    // ソートした結果を別のコレクションにするにはLINQのOrderBy拡張メソッド
    IOrderedEnumerable<int> sorted = list.OrderBy(n => n);
    DisplayItems("list", list);
    DisplayItems("OrderBy", sorted);
    // 出力:
    // list: 3, 1, 2
    // OrderBy: 1, 2, 3

    // コレクションの内容をソートするにはList<T>クラスのSortメソッド
    list.Sort();
    DisplayItems("List.Sort", list);
    // 出力:
    // List.Sort: 1, 2, 3

#if DEBUG
    ReadKey();
#endif
  }
}

Imports System.Console

Module Module1
  Sub DisplayItems(Of T)(msg As String, collection As IEnumerable(Of T))
    WriteLine($"{msg}: {String.Join(", ", collection)}")
  End Sub

  Sub Main()
    Dim list As List(Of Integer) = New List(Of Integer) From {3, 1, 2}

    ' ソートした結果を別のコレクションにするにはLINQのOrderBy拡張メソッド
    Dim sorted As IOrderedEnumerable(Of Integer) = list.OrderBy(Function(n) n)
    DisplayItems("list", list)
    DisplayItems("OrderBy", sorted)
    ' 出力:
    ' list: 3, 1, 2
    ' OrderBy: 1, 2, 3

    ' コレクションの内容をソートするにはList<T>クラスのSortメソッド
    list.Sort()
    DisplayItems("List.Sort", list)
    ' 出力:
    ' List.Sort: 1, 2, 3

#If DEBUG Then
    ReadKey()
#End If
  End Sub
End Module

整数の要素を昇順にソートする例(上:C#、下:VB)
list変数の初期化方法は、「.NET TIPS:構文:コレクションのインスタンス化と同時に要素を追加するには?[C#/VB]」をご覧いただきたい。
C#コードの冒頭から4行目にある「using static System.Console;」という書き方は、Visual Studio 2015からのものだ。詳しくは、「.NET TIPS:構文:クラス名を書かずに静的メソッドを呼び出すには?[C# 6.0]」をご覧いただきたい。同様な機能がVBには以前から備わっており、「.NET TIPS:VB.NETでクラス名を省略してメソッドや定数を利用するには?」で解説している。
C#のこのDisplayItemsメソッドの書き方については、「.NET TIPS:構文:メソッドやプロパティをラムダ式で簡潔に実装するには?[C# 6.0]」をご覧いただきたい。
DisplayItemsメソッドに出てくる先頭に「$」記号が付いた文字列については、「.NET TIPS:数値を右詰めや0埋めで文字列化するには?[C#、VB]」の後半(「C# 6.0/VB 14で追加された補間文字列機能を使用する」)を見てほしい。
また、「Main」メソッド末尾にReadKeyメソッドを置く意味は、「.NET TIPS:Visual Studioでコンソール・アプリケーションのデバッグ実行時にコマンド・プロンプトを閉じないようにするには?」をご覧いただきたい。

逆順にソートする例

 逆順のソートは、LINQではOrderByDescending拡張メソッドで簡単にできる。List<T>クラスのSortメソッドの方は、少々面倒だがIComparer<T>インタフェースを使う方法を紹介しよう(次のコード)。

// List<int>のSortメソッドで降順に並べ替えるためのIComparer<int>実装
class ReverseComparer : IComparer<int>
{
  public int Compare(int x, int y)
  {
    // 既定のComparerとは反対の結果を返す
    return Comparer<int>.Default.Compare(y, x);
  }
}

……省略……

List<int> list = new List<int> { 3, 1, 2 };

// 逆順にソートするには、LINQではOrderByDescending拡張メソッド
IOrderedEnumerable<int> sorted = list.OrderByDescending(n => n);
DisplayItems("list", list);
DisplayItems("OrderByDescending", sorted);
// 出力:
// list: 3, 1, 2
// OrderByDescending: 3, 2, 1

// List<T>クラスのSortメソッドではIComparer<T>インタフェースを使う
IComparer<int> reverseComparer = new ReverseComparer();
list.Sort(reverseComparer);
DisplayItems("List.Sort + IComparer<T>", list);
// 出力:
// List.Sort + IComparer<T>: 3, 2, 1

' List<int>のSortメソッドで降順に並べ替えるためのIComparer<int>実装
Class ReverseComparer
  Implements IComparer(Of Integer)

  Public Function Compare(x As Integer, y As Integer) As Integer _
    Implements IComparer(Of Integer).Compare

    ' 既定のComparerとは反対の結果を返す
    Return Comparer(Of Integer).Default.Compare(y, x)
  End Function
End Class

……省略……

Dim list As List(Of Integer) = New List(Of Integer) From {3, 1, 2}

' 逆順にソートするには、LINQではOrderByDescending拡張メソッド
Dim sorted As IOrderedEnumerable(Of Integer) _
  = list.OrderByDescending(Function(n) n)
DisplayItems("list", list)
DisplayItems("OrderByDescending", sorted)
' 出力:
' list: 3, 1, 2
' OrderByDescending: 3, 2, 1

' List<T>クラスのSortメソッドではIComparer<T>インタフェースを使う
Dim reverseComparer As IComparer(Of Integer) = New ReverseComparer()
list.Sort(reverseComparer)
DisplayItems("List.Sort + IComparer<T>", list)
' 出力:
' List.Sort + IComparer<T>: 3, 2, 1

整数の要素を降順にソートする例(上:C#、下:VB)
IComparer<T>インタフェースを実装するとき、簡単なものならこの例のようにComparer<T>クラス(System.Collections.Generic名前空間)のDefaultオブジェクトを利用できることも多い。また、文字列の比較にはStringComparerクラス(System名前空間)に幾つかの実装が用意されている(大文字小文字を区別しない比較など)。

複数のキーでソートする例

 複数キーでのソートは、LINQではThenByThenByDescending拡張メソッドで簡単にできる。List<T>クラスのSortメソッドの方は、面倒でもIComparer<T>インタフェースを使わねばならない。

 例として、文字列をまず文字列の長さで比較し、同じだった場合は次に文字列の1文字目で比較するようにしてみよう(次のコード)。

// List<string>のSortメソッドで複数キーを使って並べ替えるIComparer<string>実装
class MyComparer : IComparer<string>
{
  public int Compare(string x, string y)
  {
    // nullへの対処は省略した

    // まず文字列長で比較する
    int lengthCompare = x.Length - y.Length;
    if (lengthCompare != 0)
      return lengthCompare;

    // 次に、同じ順位(=文字列長が同じ)の中では1文字目で比較する
    return (x[0] - y[0]);
  }
}

……省略……

List<string> list = new List<string> { "aa", "abc", "Abc", "Ab", };
// まず文字列長でソートして、次に、同じ順位の中は1文字目でソートしたい

// LINQではThenBy拡張メソッドを使う
var result1 = list.OrderBy(s => s.Length) // まず文字列長でソートして
                  .ThenBy(s => s[0]); // 同じ順位の中だけを1文字目でソート
DisplayItems("OrderBy→ThenBy", result1);
// 出力:
// OrderBy→ThenBy: Ab, aa, Abc, abc

// なお、OrderByの連続は、ソートした結果をまたソートするので異なる結果になる
var result2 = list.OrderBy(s => s.Length)
                  .OrderBy(s => s[0]);
DisplayItems("OrderBy→OrderBy", result2);
// 出力:
// OrderBy→OrderBy: Ab, Abc, aa, abc

// List<T>クラスのSortメソッドではIComparer<T>インタフェースの実装を使う
IComparer<string> comparer = new MyComparer();
list.Sort(comparer);
DisplayItems("List.Sort(IComparer)", list);
// 出力:
// List.Sort(IComparer): Ab, aa, Abc, abc

' List<string>のSortメソッドで複数キーを使って並べ替えるIComparer<string>実装
Class MyComparer
  Implements IComparer(Of String)

  Public Function Compare(x As String, y As String) As Integer _
    Implements IComparer(Of String).Compare

    ' Nothingへの対処は省略した

    ' まず文字列長で比較する
    Dim lengthCompare As Integer = x.Length - y.Length
    If (lengthCompare <> 0) Then
      Return lengthCompare
    End If

    ' 次に、同じ順位(=文字列長が同じ)の中では1文字目で比較する
    Return (AscW(x(0)) - AscW(y(0)))
  End Function
End Class

……省略……

Dim list As List(Of String) = New List(Of String) From {"aa", "abc", "Abc", "Ab"}
' まず文字列長でソートして、次に、同じ順位の中は1文字目でソートしたい

' LINQではThenBy拡張メソッドを使う
Dim result1 = list.OrderBy(Function(s) s.Length) _
                  .ThenBy(Function(s) AscW(s(0)))
' まず文字列長でソートして(OrderBy)
' 同じ順位の中だけを1文字目でソートする(ThenBy)
DisplayItems("OrderBy→ThenBy", result1)
' 出力:
' OrderBy→ThenBy: Ab, aa, Abc, abc

' なお、OrderByの連続は、ソートした結果をまたソートするので異なる結果になる
Dim result2 = list.OrderBy(Function(s) s.Length) _
                  .OrderBy(Function(s) AscW(s(0)))
DisplayItems("OrderBy→OrderBy", result2)
' 出力:
' OrderBy→OrderBy: Ab, Abc, aa, abc

' List<T>クラスのSortメソッドではIComparer<T>インタフェースの実装を使う
Dim comparer As IComparer(Of String) = New MyComparer()
list.Sort(comparer)
DisplayItems("List.Sort(IComparer)", list)
' 出力:
' List.Sort(IComparer): Ab, aa, Abc, abc

複数のキーでソートする例(上:C#、下:VB)
このIComparer<T>インタフェースの実装は、サンプルコードということでnull/Nothingへの対処を省略してある。実際に使う場合には、適切に補ってほしい。

まとめ

 List<T>コレクションの要素をソートするときは、そのコレクションの内容を実際に並べ替えるならList<T>クラスのSortメソッドを、並べ替えた結果を別のコレクションに書き出すならLINQ拡張を使うとよい。

「.NET TIPS」のインデックス

.NET TIPS

Copyright© 1999-2017 Digital Advantage Corp. All Rights Reserved.

@IT Special

- PR -

TechTargetジャパン

この記事に関連するホワイトペーパー

Focus

- PR -

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。