Listから重複した要素を削除するには?[C#/VB]:.NET TIPS
Listから重複した要素を削除するには、Distinctメソッドを使う他、IEqualityComparer<T>を使用する、Equalsメソッドをオーバーライドするなどの方法がある。
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>インタフェースを実装したクラスを作って引数に与える
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
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
クラスの場合
要素としてクラスのインスタンスを持っている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
前述の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
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
ここでは、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
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
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
最もシンプルに書ける。
引数に与えるラムダ式は、等価比較するための値(またはオブジェクト)を返すものである。
まとめ
List<T>コレクションから重複を排除したコレクションを作るには、LINQのDistinct拡張メソッドを使う。ただし、標準とは異なる方法で重複を判定させるには、IEqualityComparer<T>インタフェースを利用する/LINQを工夫する/MoreLINQライブラリを使うといった方法がある。
利用可能バージョン:.NET Framework 2.0以降(サンプルコードにはそれ以降の機能/構文も含む)
カテゴリ:クラス・ライブラリ 処理対象:コレクション
使用ライブラリ:Listクラス(System.Collections.Generic名前空間)
関連TIPS:Listの要素を並べ替えるには?[C#/VB]
関連TIPS:Listの要素を検索するには?[C#/VB]
関連TIPS:Listの各要素を処理するには?[C#/VB]
関連TIPS:Listに要素を追加/挿入するには?[C#/VB]
関連TIPS:構文:メソッドやプロパティをラムダ式で簡潔に実装するには?[C# 6.0]
関連TIPS:構文:コレクションのインスタンス化と同時に要素を追加するには?[C#/VB]
関連TIPS:構文:クラス名を書かずに静的メソッドを呼び出すには?[C# 6.0]
関連TIPS:VB.NETでクラス名を省略してメソッドや定数を利用するには?
関連TIPS:数値を右詰めや0埋めで文字列化するには?[C#、VB]
関連TIPS:Visual Studioでコンソール・アプリケーションのデバッグ実行時にコマンド・プロンプトを閉じないようにするには?
Copyright© Digital Advantage Corp. All Rights Reserved.