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(またはそれ以降)が必要である。
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コレクションの要素が数値のときは、算術的な等価比較だけで普通は十分だろう。シンプルに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
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
この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
では、要素のクラスの既定の等価比較とは異なる方法で重複を判定させるにはどうしたらよいだろうか? 幾つかの方法がある。ここでは、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
上で定義した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>インタフェースを実装するのが面倒だ。
そこで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
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.