【LINQ】任意のクラスでDistinctを使う方法

C#

こんにちは、働くC#プログラマーのさんさめです。

Distinctは、
リストから重複を除いた要素を返すメソッドです。

var list = new[] { 1, 2, 3, 2, 2, 2 };
var distinctResult = list.Distinct();
// 1, 2, 3
Console.WriteLine(string.Join(", ", distinctResult));

ところが、
任意のクラスのコレクションの場合、
Distinctを呼んでもうまくいきません。

var card = new Card() { Mark = "Spade", Number = 1 };
var card2 = new Card() { Mark = "Spade", Number = 1 };

var list = new[] { card, card2 };
// マークと数字が同じなら重複とみなしたいが…
var distinctResult = list.Distinct();
// 結果は2になっちゃう
Console.WriteLine(distinctResult.Count());

自作クラスじゃ、
Distinctは使えないのかな…?

いえ、そんなことはありません。
少々準備は必要ですが、
任意の条件を用いて
Distinctを使えるようにする方法がありますよ。

本記事では、
任意のクラスのコレクションにおいて
Distinctを使えるようにするための、
3つの方法について解説します。

  • 自作クラスでDistinct使いたい!
  • Distinct使いづらいわ~ないわ~
  • なんか、色々実装すれば使えるらしいけど、
    よく分からないんだよね…

という方におススメの記事となっております。

スポンサーリンク

方法1.クラス側でIEquatableを実装する

なぜ、任意のクラスでは、
Distinctがうまくいかないのでしょうか。

それは、Distinctは「既定の等値比較子」
を使って一致した時に重複とみなしているからです。

既定の等値比較子…とは

全てのクラスが持っている、
Equalsメソッドのことだと考えてください。

既定では、クラスのEqualsは参照の比較となっています。
つまり、実体が完全に同じじゃないと
「同じ要素」とは判断しないのです。

しかし、このEqualsメソッドを、
クラス側でオーバーライドしてあげることで、
Distinctを使ったときに、
同じ要素かどうかの判定に採用させることができます。

ただし、実際には、
IEquatableインタフェースを実装する
のがおススメです。
object.Equalsをオーバーライドせずとも、
そっちを使ってくれます。

クラスの実装を以下のようにします。

public class Card : IEquatable<Card>
{
    public int Number { get; set; }
    public string Mark { get; set; }

    public bool Equals(Card other)
    {
        return Number.Equals(other.Number)
            && string.Equals(Mark, other.Mark, StringComparison.OrdinalIgnoreCase);
    }

    // Distinctは先にHashCodeによる比較を行うので、
    // こちらも実装する必要がある
    public override int GetHashCode()
        => Number.GetHashCode() ^ Mark?.GetHashCode() ?? 0;
}

ちょっと長いので少しずつ見ていきましょう。

まずは、IEquatableインタフェースの要件である、
Equals(T other)の実装です。

public bool Equals(Card other)
{
    // nullチェック
    if (other is null) { return false; }
    // 参照がそもそも同じなら、明らかに同一
    if (object.ReferenceEquals(this, other)) { return true; }
    return Number.Equals(other.Number)
        && string.Equals(Mark, other.Mark, StringComparison.OrdinalIgnoreCase);
}

1行目は簡単なnullチェックですね。
自分自身、つまりthisはnullにはなりえないため、
otherがnullなら、自身とは別物であることが自明です。

2行目は、ReferenceEqualsメソッドによって、
参照を比較しています。

これはつまり、

a = b

をしたときに「aとbが同じか?」
をチェックしているようなものです。

「参照しているメモリが一緒なら、
そりゃあ一緒でしょう」
ということをチェックしています。

3行目が、本当にやりたいことですね。
持っているプロパティそれぞれを比較して、
どっちも一致しているならtrueを返しています。

ちなみに、文字列の比較の
StringComparison.OrdinalIgnoreCaseとは、
大文字小文字を区別せずに比較するオプションです。

【C#】大文字小文字を区別せずに文字列比較
で解説しているのでよろしければご覧ください。

話を戻しましょう。

IEquatableの実装はEqualsだけで良いのですが、
Distinctを使うためには、
もう1つオーバーライドしなければいけない
メソッドがあります。

それが、GetHashCodeです。
ざっくり言うと簡易チェックですね。

ここもEqualsと同様に、
各プロパティのGetHashCodeを採用するようにします。
「^(XOR演算子)」で結合しているのは、
ここでは、「そういうもの」だと思ってください。

これで、最初のコードを実行すると次のようになります。

無事に重複解消されました。

方法1の問題点

さて、方法1では
クラス自体に比較の規則を実装する方法を
解説しました。

…が、しかし、
お気づきのように
実はこの「方法1」にはいくつか問題点があります。

  • 元のクラスが自身では変更できない場合無理
  • 必ずしも「既定の比較」をしたいとは限らない

元のクラスが自身では変更できない場合無理

重複解消したいクラスが、
ライブラリ側にあったりすると、
そもそもクラスの実装を変更することができません。

ライブラリ側に依頼してすぐ修正が見込めるのなら、
まだ希望がありますが、
それでも足は遅くなってしまいます。

また、ライブラリ側の想定する比較と
こちらの使用用途が一致するとは限りません。

必ずしも「既定の比較」をしたいとは限らない

比較のルールを、
その時専用の独自の形で行いたいケースは、
IEquatableの実装では賄いきれません。

例えば、冒頭の例でいうと、
マークだけで比較して重複解消したかったり、
数字だけで比較したかったり…といったケースです。

そのような場合も想定しているのでしょう。
Distinctには、
任意の条件を渡せるオーバーロードがあります。

方法2.IEqualityComparerを実装したクラスを渡す

それが、
Distinct(IEqualityComparer comparer)
つまり、
IEqualityComparerを実装したクラスを
渡す方法です。

サンプルとして、
先ほどのCardクラスを
「数値だけで判断して重複チェック」する
ケースを考えてみましょう。

この場合、数値だけで比較する、
以下のようなクラスを作ります。

public class CardComparer : IEqualityComparer<Card>
{
    public bool Equals(Card x, Card y)
    {
        if (x != null && y == null) { return false; }
        if (x == null && y != null) { return false; }
        if (object.ReferenceEquals(x, y)) return true;
        return x.Number.Equals(y.Number);
    }

    public int GetHashCode(Card obj)
        => obj?.Number.GetHashCode() ?? 0;
}

「Equals」に「GetHashCode」と、
実装すべきコードは、
方法1の時とほとんど一緒ですね。
thisと比較するわけではないので、
引数がやや異なる点は注意が必要です。

使う側のコードは以下のようになります。

var card = new Card() { Mark = "Spade", Number = 1 };
var card2 = new Card() { Mark = "Clover", Number = 1 };
var list = new[] { card, card2 };
// 数字が同じなら重複とみなす比較用クラスを渡す
var distinctResult = list.Distinct(new CardComparer());
// 結果は1になる
Console.WriteLine(distinctResult.Count());

Distinct(new CardComparer())
の部分で、先ほど作った比較用クラスを渡しています。

大きい特徴として挙げられるのは
方法2のIEqualityComparer実装の方では
元のクラスには全く追加実装をしていない
…ということです。

元のクラスには一切変更を加える必要が無いのが、
このIEqualityComparer実装の方法の良いところです。

ただし、この方法2ですが、
作った本人しか存在を知らない…
というパターンに陥ることも少なくありません。

クラス名に命名規則などありませんし、
当然インテリセンスも働きません。

クラスを別途作らないといけないので、
やりたいことの割に記述量が多
ことも気になるところです。

方法3.比較に使うラムダを渡せるようにする

方法1と方法2をどちらか使えば
やりたかったことは実現できますが、
どちらの方法も、
「記述量が多い」という残念な問題があります。

比較に使ってほしいプロパティを
ラムダで指定できるようにする、
という方法が考えられます。

使い方のイメージはこんな感じです。

list.Distinct(x => x.Number);

とっても簡潔になりました。
これが方法3です。

しかし、これを実現するためには、
いくつか準備をしておく必要があります。

準備するべきものは以下の2種類です。

  • ラムダ式を引数にインスタンス生成できる
    IEqualityComparer実装クラス
  • 上記をラムダで作ってくれる自作Distinct拡張メソッド

まずは、

  • ラムダ式を引数にインスタンス生成できる
    IEqualityComparer実装クラス

から見ていきましょう。

こんな感じのクラスになります。

public class DelegateComparer<T, TKey> : IEqualityComparer<T>
{
    private readonly Func<T, TKey> selector;
    public DeleteComparer(Func<T, TKey> keySelector)
    {
        // キーを指定する関数を受け取って…
        selector = keySelector;
    }

    // 比較の際に関数を通してからEqualsやGetHashCodeをする
    public bool Equals(T x, T y)
        => selector(x).Equals(selector(y));
    public int GetHashCode(T obj)
        => selector(obj).GetHashCode();
}

コンストラクタで、
比較したい型から別の型を返す関数を
受け取って保持しておきます。

そしてその関数を、
EqualsおよびGetHashCodeの時に
あらかじめ呼んでから比較を行うわけです。

このクラス単品で使ったときの
使う側のコードは
以下のようになります。

var card = new Card() { Mark = "Spade", Number = 1 };
var card2 = new Card() { Mark = "Clover", Number = 1 };
var list = new[] { card, card2 };
// Cardから数字を返すラムダを引数に比較用クラスを生成
var distinctResult = list.Distinct(new DelegateComparer<Card, int>(x => x.Number));
// 結果は1になる
Console.WriteLine(distinctResult.Count());

Disctinctの中身の記述量が多いですね…。

このクラスを用意するだけでも、
「毎回クラスを作らなければならない」
という煩わしさはある程度低減できます。

でもここは、
先の使い方のイメージに近づくべく、
もう一つの準備も進めましょう。

すなわちこれです。

  • 上記をラムダで作ってくれる自作Distinct拡張メソッド

以下のようなクラスとメソッドを準備します。

public static class EnumerableExtensions
{
    public static IEnumerable<T> Distinct<T, TKey>(
        this IEnumerable<T> source,
        Func<T, TKey> keySelector)
        => source.Distinct(new DelegateComparer<T, TKey>(keySelector));
}

いわゆる拡張メソッドですね。
ジェネリクスだらけなので、
読むのは「ウッ」となるかもしれません。
読み飛ばしていただいても大丈夫です。

ともあれ、これで
OrderByなんかと同じような使い心地で、
Distinctが使えるようになりました。

実行結果はこちらです。

ちなみに、
比較に2つ以上のプロパティを使いたい時は、
Tupleクラスを使うか、
ValueTuple構造体を使うと良いでしょう。

// Tupleクラスを使う場合
list.Distinct(x => new Tuple<int, string>(x.Number, x.Mark));
// ValueTuple構造体を使う場合(.NET Framework 4.7以降)
list.Distinct(x => (x.Number, x.Mark));

ValueTupleは、.NET Framework 4.7以降か、
System.ValueTupleを参照に含めないといけないので、
古いFrameworkで動かしているアプリでは、
Tupleを使った方が既存状態への影響が少なくすみます。

ValueTupleは記述量が減ったり何かと便利なので、
もし上げられるなら上げることをおススメしますけどね。

ともあれ、
これでDistinctを任意のクラスで使えるようになりました。

大変お疲れさまでした。
快適な重複解消ライフをお過ごしください!

まとめ

まとめです。

  • Distinctはリストの重複要素を取り除くメソッド
  • 自作クラスの重複要素を取り除こうとすると、
    標準では参照比較になってしまう
  • IEquableを実装したクラスにすれば、
    単なるDistinct呼びで重複解消できる
  • 独自の比較をしたい場合は、
    Distinctのオーバーロードを利用する
    • IEqualityComparer実装クラスを渡す
  • ラムダ式で比較に使いたいプロパティを
    指定できるクラスを紹介
    • ラムダを渡せば簡単に重複解消できる
      のでとても快適になる

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

コメント

タイトルとURLをコピーしました