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

.NET TIPS:Listから重複した要素を削除するには?[C#/VB]

Listから重複した要素を削除するには、Distinctメソッドを使う他、IEqualityComparer<T>を使用する、Equalsメソッドをオーバーライドするなどの方法がある。

[山本康彦,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から重複した要素を削除するには?

 LINQのDistinct拡張メソッド(System.Linq名前空間のEnumerableクラス)を使うのが基本だ(次のコード)。ただし、その要素が持っている等価比較の方法とは異なる方法で重複を判断させるには、Distinct拡張メソッドでは面倒なので工夫する余地がある(後述)。

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

// 標準の等価比較で重複を排除
IEnumerable<int> result1 = list.Distinct();

// 標準とは異なる等価比較で重複を排除
IEnumerable<int> result2 = list.Distinct({IEqualityComparer<T>のインスタンス});
// Distinct拡張メソッドで実現するには、
// IEqualityComparer<T>インタフェースを実装したクラスを作って引数に与える

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

' 標準の等価比較で重複を排除
Dim result1 As IEnumerable(Of T) = list.Distinct()

' 標準とは異なる等価比較で重複を排除
Dim result2 As IEnumerable(Of T) = list.Distinct({IEqualityComparer<T>のインスタンス})
' Distinct拡張メソッドで実現するには、
' IEqualityComparer<T>インタフェースを実装したクラスを作って引数に与える

Listから重複した要素を削除する基本(上:C#、下:VB)
LINQのDistinct拡張メソッドを引数なしで呼び出すと、コレクションの要素に備わっている等価比較を使って重複が判断される。
LINQのDistinct拡張メソッドで標準とは異なる等価比較を使って重複を判断させるには、IEqualityComparer<T>インタフェースを実装したクラスを定義する必要がある。それは面倒なので、工夫する余地がある(後述)。

数値の場合

 Listコレクションの要素が数値のときは、算術的な等価比較だけで普通は十分だろう。シンプルにLINQのDistinct拡張メソッドを使えばよい。次のコードに、コンソールアプリの例を示す。後でも使うためにDisplayItems<T>メソッドを切り出している。

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

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

  static void Main(string[] args)
  {
    List<int> list = new List<int> { 3, 1, 2, 3, 2, 5, };
    IEnumerable<int> result = list.Distinct();
    DisplayItems(result);
    // 出力:3, 1, 2, 5

#if DEBUG
    ReadKey();
#endif
  }
}

Imports System.Console

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

  Sub Main()
    Dim list As List(Of Integer) = New List(Of Integer) From {3, 1, 2, 3, 2, 5}
    Dim result As IEnumerable(Of Integer) = list.Distinct()
    DisplayItems(result)
    ' 出力:3, 1, 2, 5

#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でコンソール・アプリケーションのデバッグ実行時にコマンド・プロンプトを閉じないようにするには?」をご覧いただきたい。

文字列で大文字小文字を同一視するには?

 IEqualityComparer<string>インタフェース(System.Collections.Generic名前空間)を実装したStringComparerクラス(System名前空間)が標準で用意されているので、それを使ってLINQのDistinct拡張メソッドを呼び出せばよい(次のコード)。

List<string> list = new List<string>{ "Aa", "bb", "aa", };
IEnumerable<string> result =
  list.Distinct(StringComparer.InvariantCultureIgnoreCase);
DisplayItems(result);
// 出力:Aa, bb

Dim list As List(Of String) = New List(Of String) From {"Aa", "bb", "aa"}
Dim result As IEnumerable(Of String) _
  = list.Distinct(StringComparer.InvariantCultureIgnoreCase)
DisplayItems(result)
' 出力:Aa, bb

大文字小文字の違いを無視して重複を排除する(上:C#、下:VB)

クラスの場合

 要素としてクラスのインスタンスを持っているList<T>コレクションの場合、引数なしのDistinctメソッドでは要素のクラスのEqualsメソッドを使って重複が判定される。Equalsメソッドの既定の実装(=ObjectクラスのEqualsメソッド)は参照の比較である。従って、メンバ変数やプロパティによる等価判定をさせるには、Equalsメソッドをオーバーライドする必要がある(Equalsメソッドの変更に伴ってGetHashCodeメソッドもオーバーライドしなければならない)。

 例えば、2つのプロパティIdとNameを持つPersonクラスというものを考えてみよう。IdプロパティとNameプロパティの両方とも等しいときに等価である(重複している)と判定させるためには、Personクラスで次のコードのようにEqualsメソッドとGetHashCodeメソッドをオーバーライドする。

class Person
{
  // 2つのプロパティIdとNameを持つ
  public string Id { get; set; }
  public string Name { get; set; }

  public override string ToString()
    => $"{Id}:{Name}";

  public override bool Equals(object obj)
  {
    var p = obj as Person;
    if (p == null)
      return false;
    return (this.Id == p.Id && this.Name == p.Name);
  }
  public override int GetHashCode()
    => Id.GetHashCode() ^ Name.GetHashCode();
}

Class Person
  ' 2つのプロパティIdとNameを持つ
  Public Property Id As String
  Public Property Name As String

  Public Overrides Function ToString() As String
    Return $"{Id}:{Name}"
  End Function

  Public Overrides Function Equals(obj As Object) As Boolean
    Dim p = TryCast(obj, Person)
    If (p Is Nothing) Then
      Return False
    End If
    Return (Me.Id = p.Id AndAlso Me.Name = p.Name)
  End Function

  Public Overrides Function GetHashCode() As Integer
    Return Id.GetHashCode() Xor Name.GetHashCode()
  End Function
End Class

全てのプロパティが等しいときに等価であると判定されるクラスの例(上:C#、下:VB)
前述のDisplayItemsメソッドでうまく表示させるために、ToStringメソッドもオーバーライドしている。

 このPersonクラスのインスタンスを要素に持つListコレクションに対して、LINQのDistinct拡張メソッドを使うと次のコードのようになる。

List<Person> list = new List<Person>
{
  new Person{Id="101", Name="Serval"},
  new Person{Id="102", Name="Rockhopper"},
  new Person{Id="102", Name="Gentoo"},
  new Person{Id="102", Name="Rockhopper"},
  new Person{Id="103", Name="Graywolf"},
};
IEnumerable<Person> result = list.Distinct();
DisplayItems(result);
// 出力:101:Serval, 102:Rockhopper, 102:Gentoo, 103:Graywolf

Dim list As List(Of Person) = New List(Of Person) _
From {
  New Person With {.Id = "101", .Name = "Serval"},
  New Person With {.Id = "102", .Name = "Rockhopper"},
  New Person With {.Id = "102", .Name = "Gentoo"},
  New Person With {.Id = "102", .Name = "Rockhopper"},
  New Person With {.Id = "103", .Name = "Graywolf"}
}
Dim result As IEnumerable(Of Person) = list.Distinct()
DisplayItems(result)
' 出力:101:Serval, 102:Rockhopper, 102:Gentoo, 103:Graywolf

Personインスタンスを持つListから重複した要素を排除する例(上:C#、下:VB)
2番目と4番目の要素(太字にした部分)は、IdとNameがともに同じなので重複していると判定される。2番目と3番目の要素は、Idは同じ「102」だがNameが違うため重複とは判定されない。

標準とは異なる方法で重複を判定させるには?

 では、要素のクラスの既定の等価比較とは異なる方法で重複を判定させるにはどうしたらよいだろうか? 幾つかの方法がある。ここでは、3通りの方法を紹介する。

 前述したPersonクラスは、IdとNameの両方とも等しいときに重複と判断された。Personクラスは変更せずに、Idだけ一致していれば重複と判断されるようにしてみよう。

 まず、正攻法ともいえる方法から。LINQのDistinct拡張メソッドに引数を取るものがあるので、それを使う。引数に渡すものは、IEqualityComparer<T>インタフェースを実装したオブジェクトだ。何だか難しそうだが、前述のPersonクラスに実装したようなEqualsメソッドとGetHashCodeメソッドを独立させたものだと思えばよい(次のコード)。

class PersonEqualityComparer : IEqualityComparer<Person>
{
  public bool Equals(Person x, Person y)
  {
    if (x == null && y == null)
      return true;
    if (x == null || y == null)
      return false;
    return x.Id == y.Id;
  }

  public int GetHashCode(Person p)
    => p.Id.GetHashCode();
}

Class PersonEqualityComparer
  Implements IEqualityComparer(Of Person)

  Public Overloads Function Equals(x As Person, y As Person) As Boolean _
      Implements IEqualityComparer(Of Person).Equals
    If (x Is Nothing AndAlso y Is Nothing) Then
      Return True
    End If
    If (x Is Nothing OrElse y Is Nothing) Then
      Return False
    End If
    Return x.Id = y.Id
  End Function

  Public Overloads Function GetHashCode(p As Person) As Integer _
      Implements IEqualityComparer(Of Person).GetHashCode
    Return p.Id.GetHashCode()
  End Function
End Class

IEqualityComparer<T>インタフェースの実装例(上:C#、下:VB)
ここでは、Idプロパティさえ一致していれば等価であるとした。
IEqualityComparer<T>インタフェースで実装すべき内容は、Personクラスに実装したEqualsメソッド/GetHashCodeメソッドとよく似ている。

 上で定義したPersonEqualityComparerクラスを使ってLINQのDistinct拡張メソッドを呼び出すと次のコードのようになる。

List<Person> list = new List<Person>
{
  new Person{Id="101", Name="Serval"},
  new Person{Id="102", Name="Rockhopper"},
  new Person{Id="102", Name="Gentoo"},
  new Person{Id="102", Name="Rockhopper"},
  new Person{Id="103", Name="Graywolf"},
};
IEnumerable<Person> result = list.Distinct(new PersonEqualityComparer());
DisplayItems(result);
// 出力:101:Serval, 102:Rockhopper, 103:Graywolf

Dim list As List(Of Person) = New List(Of Person) _
From {
  New Person With {.Id = "101", .Name = "Serval"},
  New Person With {.Id = "102", .Name = "Rockhopper"},
  New Person With {.Id = "102", .Name = "Gentoo"},
  New Person With {.Id = "102", .Name = "Rockhopper"},
  New Person With {.Id = "103", .Name = "Graywolf"}
}
Dim result As IEnumerable(Of Person) _
  = list.Distinct(New PersonEqualityComparer())
DisplayItems(result)
' 出力:101:Serval, 102:Rockhopper, 103:Graywolf

IEqualityComparer<T>インタフェースの実装を使って重複を排除する例(上:C#、下:VB)
PersonEqualityComparerオブジェクトをLINQのDistinct拡張メソッドの引数に与えると、Idプロパティが一致していれば等価と判定される。そのため、2番目/3番目/4番目(太字の部分)の要素は重複と見なされ、2番目だけが出力されている。

 上の方法は、いちいちIEqualityComparer<T>インタフェースを実装するのが面倒だ。

 そこで2つ目の方法としては、LINQを工夫することだ。GroupBy拡張メソッドとSelect拡張メソッドを組み合わせると、Distinct拡張メソッドと同じ働きが得られるのだ。そして、GroupBy拡張メソッドにはラムダ式を与えられるのである(次のコード)。

List<Person> list = ……省略(前と同じ)……
IEnumerable<Person> result = list.GroupBy(p => p.Id)
                                 .Select(group => group.First());
DisplayItems(result);
// 出力:101:Serval, 102:Rockhopper, 103:Graywolf

Dim list As List(Of Person) = ……省略(前と同じ)……
Dim result As IEnumerable(Of Person) _
  = list.GroupBy(Function(p) p.Id) _
        .Select(Function(group) group.First())
DisplayItems(result)
' 出力:101:Serval, 102:Rockhopper, 103:Graywolf

LINQを工夫して重複を排除する例(上:C#、下:VB)
IEqualityComparer<T>インタフェースの実装を使わなくても、Distinct拡張メソッドの代わりにGroupBy拡張メソッドとSelect拡張メソッドを組み合わせれば同じ結果が得られる。
GroupBy拡張メソッドの引数には、グルーピングするための値(またはオブジェクト)を返すラムダ式を与える。

 3つ目の方法は、オープンソースのMoreLINQライブラリDistinctBy拡張メソッドを利用することだ(次のコード)。MoreLINQライブラリはNuGetから導入できる。

List<Person> list = ……省略(前と同じ)……

// NuGetからMoreLINQを導入する
// コード冒頭に「using MoreLinq;」の記述が必要
IEnumerable<Person> result = list.DistinctBy(p => p.Id);
DisplayItems(result);
// 出力:101:Serval, 102:Rockhopper, 103:Graywolf

Dim list As List(Of Person) = ……省略(前と同じ)……

' NuGetからMoreLINQを導入する
' コード冒頭に「Imports MoreLinq」の記述が必要
Dim result5 As IEnumerable(Of Person) = list.DistinctBy(Function(p) p.Id)
DisplayItems(result5)
' 出力:101:Serval, 102:Rockhopper, 103:Graywolf

MoreLINQライブラリのDistinctBy拡張メソッドで重複を排除する例(上:C#、下:VB)
最もシンプルに書ける。
引数に与えるラムダ式は、等価比較するための値(またはオブジェクト)を返すものである。

まとめ

 List<T>コレクションから重複を排除したコレクションを作るには、LINQのDistinct拡張メソッドを使う。ただし、標準とは異なる方法で重複を判定させるには、IEqualityComparer<T>インタフェースを利用する/LINQを工夫する/MoreLINQライブラリを使うといった方法がある。

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

.NET TIPS

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

@IT Special

- PR -

TechTargetジャパン

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

RSSについて

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

メールマガジン登録

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