C#

【C#】LinqのDistinctを使ってList内の重複した要素をなくす

アイキャッチ(c_sharp)

こんにちは。
なかむぅです。

Listの中のデータ被ってる。同じデータ出したくないから被ってるやる削除したいなぁ。

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を継承したクラスを用意する必要があります。

IEqualityComparerEqualsGetHashCodeメソッドが実装を強制されているので、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の中身まで確認してみたかったのですが残念。

Distinctの中身

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は重複削除不可
いやここは違うだろ

と思った方はコメントいただけると嬉しいです。

記事で検証したこと以外にも気になることがあればコメントお願いします。

最後までお読みいただきありがとうございました!

COMMENT

メールアドレスが公開されることはありません。 が付いている欄は必須項目です