はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【C#】シーケンスが等しいかどうかを同じ要素なら等しくする

はじめに

今回はシーケンスが等しいかどうかを同じ要素なら等しくする記事になります!

突然ですが、以下のコードをみてみてください。

static void Main(string[] args)
{
    var a = Enumerable.Range(1, 5);
    var b = Enumerable.Range(1, 5);

    Console.WriteLine(a == b);          // false
    Console.WriteLine(Equals(a, b));    // false
}

これの結果は両方ともfalseになります。

もしかしたら間違えてしまった人もいるのではないでしょうか。少なくとも私は間違えました。

これについて少し掘り下げていきましょう。

等しくならない理由

これが等しくならない理由はインスタンスが等しいかを判定し、等しくないのでfalseになったという訳です。

もう一つだけこのコードをみてみてください。

static void Main(string[] args)
{
    int a = 1;
    int b = 1;

    Console.WriteLine(a == b);          // true
    Console.WriteLine(Equals(a, b));    // true
}

こちらの結果はどちらもtrueになります。

しかし先程の知識を使うと、インスタンスが異なるのにもかかわらずtrueが出力されていて変ですよね。

一方はインスタンスによる比較を行い、一方は値(プロパティ)によって比較をする。

鋭い方はこれは値型・参照型による違いだと感じるかもしれませんが、合っているようであっていません。

等しいかを決めているのは比較される対象(今回はab)の型によって定義をされているのです。

Equalsメソッド

==演算子は今回は触れずにEqualメソッドについてみていきます。

詳しくみたい方はこちらを参考してみてください。
型の値の等価性を定義する方法 - C# プログラミング ガイド | Microsoft Docs

まずユーザーが定義をしなければObject.Equalメソッドが呼ばれ、以下のような性質をもちます。

  • 参照型なら比較されるオブジェクト変数が同じオブジェクトを参照しているかどうかをみる(ReferenceEquals メソッドと同じ)
  • 値型なら値が等しいかどうかをみる(ReferenceEquals メソッドと異なる)

しかしデフォルトはこの規則になっていますが、オーバーライドすることができるので必ずそうである訳ではありません。

public class Slime
{
    int hp;
    int mp;

    public Slime(int hp, int mp)
    {
        this.hp = hp;
        this.mp = mp;
    }

    /// <summary>
    /// Slimeオブジェクト同士の比較方法を定義する
    /// </summary>
    public override bool Equals(object obj)
    {
        var other = obj as Slime;

        if (other == null) return false;
        else return hp == other.hp && mp == other.mp;
    }

    /// <summary>
    /// Equalsメソッドと一緒についてくるサブキャラ
    /// 等しい2つのオブジェクトが等しい => ハッシュコードが等しい (逆は成り立たない可能性あり)
    /// </summary>
    public override int GetHashCode()
    {
        int hCode = hp ^ mp;
        return hCode.GetHashCode();
    }
}

この場合はhp,mpが等しければEqualメソッドtrueになるようになります。

クラスは参照型なのにもかかわらず、オブジェクトが同じかをみるはずがそうではなくなってしましました。

また自前で実装するときは以下のことに注意してください。

  • EqualsメソッドをオーバーロードするときはGetHashCodeもオーバーロードをする
  • GetHashCodeメソッドの返り値は等しい2つのオブジェクトが等しい => ハッシュコードが等しいになるようにする

特にGetHashCodeはどのように実装すれば良いか難しいかもしれませんが、慣例的にxorを用いて書くようです。

ただこの定義を守っていれば動きはするので普通にプロパティを返しても大丈夫そう?みたい。

LINQを使う

今回の目的はシーケンスが等しいかを要素によって判断させるでしたが、普通にはできないことがわかりました。

var a = Enumerable.Range(1, 10);
var b = Enumerable.Range(1, 10);

Console.WriteLine(a == b);              // false
Console.WriteLine(Equals(a, b));        // false


しかし要素が等しいかどうかをみるにはLINQのSequenceEqualメソッドを使うことで実現できます。

static void Main(string[] args)
{
    var a = Enumerable.Range(1, 10);
    var b = Enumerable.Range(1, 10);
    var c = Enumerable.Range(1, 10).Reverse();

    Console.WriteLine(a.SequenceEqual(b));  // true
    Console.WriteLine(a.SequenceEqual(c));  // false
}

もう少し詳しくみていきましょう。

SequenceEqualメソッド

public static bool SequenceEqual<TSource> (this IEnumerable<TSource> first, IEnumerable<TSource> second);
public static bool SequenceEqual<TSource> (this IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer);

オーバーロードが二つあり、どちらも解説していきたいと思います。

上の方が先程使ったもので、要素の型に対して既定の等値比較子を使用して要素を比較することで、2 つのシーケンスが等しいかどうかを判断できます。

ただしEqualメソッドのところで紹介したように、参照型の場合では思惑通りの動きをしてくれません。

static void Main(string[] args)
{
    var a = new Slime[] { new Slime(1, 1), new Slime(2, 2) };
    var b = new Slime[] { new Slime(1, 1), new Slime(2, 2) };

    Console.WriteLine(a.SequenceEqual(b));  // false
}

public class Slime
{
    int hp;
    int mp;

    public Slime(int hp, int mp)
    {
        this.hp = hp;
        this.mp = mp;
    }
}

これを解決してくれるのは二つ目のオーバーロードになりますが、その使い方の前に少し説明を加えさせてください。

IEquatableとIEqualityComparerインターフェイス

SequenceEqualメソッドの二つ目のオーバーロードの引数にはIEqualityComparerインターフェイス型の変数をとります。

IEquatableインターフェイスIEqualityComparerインターフェイスはよく一緒に説明されますが、これらは以前紹介したIComparableインターフェイスIComparerインターフェイスに非常に似ています。
【C#】IComparerインターフェイスを使って並べ方を変更する - はなちるのマイノート

  • IComparableソート方法が既に決まっているとき
  • IComparerソート方法が複数あり場合に応じて変更したいとき
  • IEquatable等価性の比較方法が既に決まっているとき
  • IEqualityComparer等価性の比較方法が複数あり場合に応じて変更したいとき

IEquatableインターフェイス

IEquatableインターフェイス型パラメータTで指定された型との等価性の比較が可能であることを表すインターフェイスです。

public interface IEquatable<T>
{
	bool Equals (T other);
}

ただこのインターフェイスを実装したとしても、メソッドによってはObject.Equalsメソッドを呼び出すものもあるので結構厄介な代物です。

このあとの実装例にてどんな感じに書くかを見れますが、IEquatableインターフェイスを実装するときはObject.Equalsメソッドをオーバーロードする必要があることに注意してください。

これを怠ると思わぬバグに繋がる可能性があるかもです。

IEqualityComparerインターフェイス

IEqualityインターフェイス等価性の比較処理を提供するためのインターフェイスです。

普通のとジェネリック版の二つがありますが、定義的にさほど大差はありません。

using System.Runtime.InteropServices;

[ComVisible (true)]
public interface IEqualityComparer
{
	new bool Equals (object x, object y);

	int GetHashCode (object obj);
}
public interface IEqualityComparer<in T>
{
	bool Equals (T x, T y);

	int GetHashCode (T obj);
}

ジェネリックの方は型が決まっているので不便に感じるかも知れませんが、色んな型チェックを自分でする必要がないのでむしろ積極的に使っていくべきでしょう。

またこのインターフェイスを使用すると、コレクションに対してカスタマイズされた等値比較を実装できるというところが今回の大目玉になります。

参照型のコレクションの等値比較

早速今までの知識を総動員して、参照型のコレクションの等値比較をしてみましょう。

IEquatableを使った例

static void Main(string[] args)
{
    var a = new Slime[] { new Slime(1, 1), new Slime(2, 2) };
    var b = new Slime[] { new Slime(1, 1), new Slime(2, 2) };

    Console.WriteLine(a.SequenceEqual(b));  // true
}

public class Slime : IEquatable<Slime>
{
    public int hp;
    public int mp;

    public Slime(int hp, int mp)
    {
        this.hp = hp;
        this.mp = mp;
    }

    public bool Equals(Slime other)
    {
        return hp == other.hp && mp == other.mp;
    }

    // 基本的にIEquatableインターフェイスを実装しているときはObject.Equalsメソッドをオーバーロードしておく
    public override bool Equals(object obj)
    {
        var other = obj as Slime;
        if (other == null) return false;
        return Equals(other);
    }

    public override int GetHashCode()
    {
        var hCode = hp ^ mp;
        return hCode.GetHashCode();
    }
}

IEqualityComparerを使った例

static void Main(string[] args)
{
    var a = new Slime[] { new Slime(1, 1), new Slime(2, 2) };
    var b = new Slime[] { new Slime(1, 1), new Slime(2, 2) };

    Console.WriteLine(a.SequenceEqual(b, new SlimeComparer()));  // true
}

public class Slime
{
    public int hp;
    public int mp;

    public Slime(int hp, int mp)
    {
        this.hp = hp;
        this.mp = mp;
    }
}

public class SlimeComparer : IEqualityComparer<Slime>
{
    public bool Equals(Slime x, Slime y)
    {
        return x.hp == y.hp && x.mp == y.mp;
    }

    public int GetHashCode(Slime obj)
    {
        var hCode = obj.hp ^ obj.mp;
        return hCode.GetHashCode();
    }
}

こうすることでhp,mpが同じなら同じとみなすようにすることができました。

さいごに

色々ととっ散らかってしまって見辛い記事になってしまい申し訳ないです。

特にIEqualityComparerはLINQの引数のみならず、DictionaryHashtableといったコレクションのコンストラクタの引数にも使われたりします。

また今度それらの記事を書こうと思ったり、思わなかったり。

かなり長い記事になってしまいましたがこれくらいで。

ではまた。