こんにちは。
なかむぅです。
Listや配列の中身が重複していて、重複したデータを削除したいときあります。
そんなときはLinqのDistinctを使うと簡単に削除できました。
ついでにDistinctを使って試したことや調べたことがあるのでまとめていきます。
LinqのDistinctを使うとListや配列内のデータを一意にできる
DistinctはListや配列の重複データを簡単に削除ですることができます。
使用するときはListや配列が格納された変数やプロパティのお尻に.Distinct()
をつけるだけです。
public static class Program
{
static void Main(string[] args)
{
//配列
var numbers = new int[] { 1, 2, 2, 3, 3, 4, 5, 5 };
var distinctNumbers = numbers.Distinct();
Console.WriteLine("■配列");
foreach (var number in distinctNumbers)
{
Console.WriteLine(number);
}
Console.WriteLine();
//List
var hiraganaList = new List<string>() {
"あいうえお",
"あいうえお",
"かきくけこ",
"かきくけこ",
"かきくけこ",
"さしすせそ",
"たちつてと",
"たちつてと",
"なにぬねの",
"なにぬねの"
};
var distinctHiragana = hiraganaList.Distinct();
Console.WriteLine("■List");
foreach (var hiragana in distinctHiragana)
{
Console.WriteLine(hiragana);
}
}
}
■配列
1
2
3
4
5
■List
あいうえお
かきくけこ
さしすせそ
たちつてと
なにぬねの
参照型のList
参照型のListはDistinct呼ぶだけでは重複を削除できませんでした。
公式サイトを見ると参照型のListの場合は自分でComparerを用意しないといけないようです。
public class Profile
{
public string Name { get; set; }
public int Age { get; set; }
}
public static class Program
{
static void Main(string[] args)
{
var profiles = new List<Profile>();
profiles.Add(new Profile() { Name = "トム・クルーズ", Age = 61 });
profiles.Add(new Profile() { Name = "トム・クルーズ", Age = 61 });
profiles.Add(new Profile() { Name = "ヘイリー・アトウェル", Age = 41 });
profiles.Add(new Profile() { Name = "ヘイリー・アトウェル", Age = 41 });
profiles.Add(new Profile() { Name = "ヘイリー・アトウェル", Age = 41 });
profiles.Add(new Profile() { Name = "イーサイ・モラレス", Age = 60 });
profiles.Add(new Profile() { Name = "サイモン・ペッグ", Age = 53 });
profiles.Add(new Profile() { Name = "サイモン・ペッグ", Age = 53 });
profiles.Add(new Profile() { Name = "ヴィング・レイムス", Age = 64 });
profiles.Add(new Profile() { Name = "ヴィング・レイムス", Age = 64 });
var distinctProfiles = profiles.Distinct();
foreach (var profile in distinctProfiles)
{
Console.WriteLine($"Name:{profile.Name}, Age:{profile.Age}");
}
}
}
Name:トム・クルーズ, Age:61
Name:トム・クルーズ, Age:61
Name:ヘイリー・アトウェル, Age:41
Name:ヘイリー・アトウェル, Age:41
Name:ヘイリー・アトウェル, Age:41
Name:イーサイ・モラレス, Age:60
Name:サイモン・ペッグ, Age:53
Name:サイモン・ペッグ, Age:53
Name:ヴィング・レイムス, Age:64
Name:ヴィング・レイムス, Age:64
参照型のListはComparerを用意
参照型のListの重複を削除するにはIEqualityComparer
を継承したクラスを用意する必要があります。
IEqualityComparer
はEquals
、GetHashCode
メソッドが実装を強制されているので、Equals
はインスタンスの中身を比較する処理、GetHashCode
はインスタンスのハッシュ値を返す処理を書きましょう。
public static class Program
{
static void Main(string[] args)
{
var profiles = new List<Profile>();
(略)
// 引数にComparerを渡す
var distinctProfiles = profiles.Distinct(new ProfileComparer());
foreach (var profile in distinctProfiles)
{
Console.WriteLine($"Name:{profile.Name}, Age:{profile.Age}");
}
}
}
// IEqualityComparerを継承したクラスを用意
class ProfileComparer : IEqualityComparer<Profile>
{
public bool Equals(Profile x, Profile y)
{
if (Object.ReferenceEquals(x, y)) return true;
if (Object.ReferenceEquals(x, null) || Object.ReferenceEquals(y, null)) return false;
return x.Name == y.Name && x.Age == y.Age;
}
public int GetHashCode(Profile Profile)
{
if (Object.ReferenceEquals(Profile, null)) return 0;
int hashProfileName = Profile.Name == null ? 0 : Profile.Name.GetHashCode();
int hashProfileCode = Profile.Age.GetHashCode();
return hashProfileName ^ hashProfileCode;
}
}
Name:トム・クルーズ, Age:61
Name:ヘイリー・アトウェル, Age:41
Name:イーサイ・モラレス, Age:60
Name:サイモン・ペッグ, Age:53
Name:ヴィング・レイムス, Age:64
IEqualityComparerの動き
IEqualityComparer
がどのような動きになるのか気になったので調べてみました。
Equals
は重複チェックしていることがわかったのですが、GetHashCode
がなんのためにあるのかが気になります。
Profile
クラスにNumber
プロパティを追加して、それぞれのインスタンスが呼び出されるタイミングを出力しました。
そこでわかったのは以下の2つです。
- まず
GetHashCode
が呼ばれる - HashCodeが重複しているインスタンスのみ
Equals
で比較される
// Numberプロパティを追加
public class Profile
{
public int Number { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
public static class Program
{
static void Main(string[] args)
{
var profiles = new List<Profile>();
profiles.Add(new Profile() { Number = 1, Name = "トム・クルーズ", Age = 61 });
profiles.Add(new Profile() { Number = 2, Name = "トム・クルーズ", Age = 61 });
profiles.Add(new Profile() { Number = 3, Name = "ヘイリー・アトウェル", Age = 41 });
profiles.Add(new Profile() { Number = 4, Name = "ヘイリー・アトウェル", Age = 41 });
profiles.Add(new Profile() { Number = 5, Name = "ヘイリー・アトウェル", Age = 41 });
profiles.Add(new Profile() { Number = 6, Name = "イーサイ・モラレス", Age = 60 });
profiles.Add(new Profile() { Number = 7, Name = "サイモン・ペッグ", Age = 53 });
profiles.Add(new Profile() { Number = 8, Name = "サイモン・ペッグ", Age = 53 });
profiles.Add(new Profile() { Number = 9, Name = "ヴィング・レイムス", Age = 64 });
profiles.Add(new Profile() { Number = 10, Name = "ヴィング・レイムス", Age = 64 });
(略)
}
}
class ProfileComparer : IEqualityComparer<Profile>
{
public bool Equals(Profile x, Profile y)
{
Console.WriteLine($"x => Number:{x.Number}, Name:{x.Name}, Age:{x.Age}");
Console.WriteLine($"y => Number:{y.Number}, Name:{y.Name}, Age:{y.Age}");
Console.WriteLine();
if (Object.ReferenceEquals(x, y)) return true;
if (Object.ReferenceEquals(x, null) || Object.ReferenceEquals(y, null)) return false;
return x.Name == y.Name && x.Age == y.Age;
}
public int GetHashCode(Profile profile)
{
Console.WriteLine($"Number:{profile.Number}");
if (Object.ReferenceEquals(profile, null)) return 0;
int hashProfileName = profile.Name == null ? 0 : profile.Name.GetHashCode();
int hashProfileCode = profile.Age.GetHashCode();
var hash = hashProfileName ^ hashProfileCode;
Console.WriteLine($"hash:{hash}");
return hash;
}
}
Number:1
hash:531422844
Number:2
hash:531422844
x => Number:1, Name:トム・クルーズ, Age:61
y => Number:2, Name:トム・クルーズ, Age:61
Number:3
hash:-1020683756
Number:4
hash:-1020683756
x => Number:3, Name:ヘイリー・アトウェル, Age:41
y => Number:4, Name:ヘイリー・アトウェル, Age:41
Number:5
hash:-1020683756
x => Number:3, Name:ヘイリー・アトウェル, Age:41
y => Number:5, Name:ヘイリー・アトウェル, Age:41
Number:6
hash:-666992800
Number:7
hash:-1951073838
Number:8
hash:-1951073838
x => Number:7, Name:サイモン・ペッグ, Age:53
y => Number:8, Name:サイモン・ペッグ, Age:53
Number:9
hash:-49220656
Number:10
hash:-49220656
x => Number:9, Name:ヴィング・レイムス, Age:64
y => Number:10, Name:ヴィング・レイムス, Age:64
最初にGetHashCode
が呼ばれて、同じHashCodeの場合はEquals
を呼び出すような動きになっていました。
実行結果からNumber:3
のインスタンスが呼ばれたあとにNumber:2
のインスタンスとの比較はスキップされています。
HashCodeが違うから中身違うよねっていう比較がすでにされているみたいですね。
この時点でNumber:3
のインスタンスがすでにListの中に入っていると思われます。
そのあとはNumber:4
のインスタンスとNumber:5
のインスタンスをNumber:3
比較して両方弾かれているはずです。
試しにGetHashCode
を0だけ返すようにすると、全インスタンスを比較するようになりました。
一応Distinctの中身を見てみましたが、MacのVS2022ではDistinctIterator
のインスタンスを返しているところまでしか見れませんでした。
DistinctIterator
の中身まで確認してみたかったのですが残念。
public static IEnumerable<TSource> Distinct<TSource> (this IEnumerable<TSource> source, IEqualityComparer<TSource>? comparer)
{
if (source == null) {
ThrowHelper.ThrowArgumentNullException (ExceptionArgument.source);
}
return new DistinctIterator<TSource> (source, comparer);
}
Dictionaryは重複削除されない
って感じですが、気になったので一応試してみました。
Dictionary
はKeyが一意なので重複扱いにはならないみたいですね。
public static class Program
{
static void Main(string[] args)
{
var names = new Dictionary();
names.Add(1, "トム・クルーズ");
names.Add(2, "トム・クルーズ");
names.Add(3, "ヘイリー・アトウェル");
names.Add(4, "ヘイリー・アトウェル");
names.Add(5, "ヘイリー・アトウェル");
names.Add(6, "ヘイリー・アトウェル");
names.Add(7, "イーサイ・モラレス");
names.Add(8, "サイモン・ペッグ");
names.Add(9, "サイモン・ペッグ");
names.Add(10, "ヴィング・レイムス");
names.Add(11, "ヴィング・レイムス");
var distinctNames = names.Distinct();
foreach (var name in distinctNames)
{
Console.WriteLine(name);
}
}
}
[1,トム・クルーズ]
[2,トム・クルーズ]
[3,ヘイリー・アトウェル]
[4,ヘイリー・アトウェル]
[5,ヘイリー・アトウェル]
[6,ヘイリー・アトウェル]
[7,イーサイ・モラレス]
[8,サイモン・ペッグ]
[9,サイモン・ペッグ]
[10,ヴィング・レイムス]
[11,ヴィング・レイムス]
私が働く会社では、一緒に働いてくれるエンジニアを募集しています♪
- 長く働いていても年収が全然上がらない…一気に年収を上げたい
- 残業をしたくてしているわけではないので、残業代はしっかりと出して欲しい
- やりたいことに挑戦させてもらえない…自分がしてみたいことに挑戦したい
- 通勤時間が長すぎるから、できるだけ短くしたい
- もっと勉強したいから、書籍代や資格の受験料など負担してくれるところに入りたい
- 現場に駆り出されてからは放置プレイ…相談できる人も先輩も居ないので、自分の状況をちゃんと理解してくれるところで働きたい
上記内容に1つでも当てはまる場合は、ぜひお声がけください。
私のサイトから応募していただいた方にはお好きなギフト券5000円分プレゼントさせていただきます(条件あり)。
まとめ
- Listや配列の重複はLinqのDistinctを使うと簡単に削除できる
- 参照型のListはComparerが必要
- ComparerはインスタンスごとにGetHushCodeが呼ばれてからEqualsで比較される
- 同じHushCodeのインスタンスのみ比較される
- Dictionaryは重複削除不可
と思った方はコメントいただけると嬉しいです。
記事で検証したこと以外にも気になることがあればコメントお願いします。
最後までお読みいただきありがとうございました!