はじめに
今回はシーケンスが等しいかどうかを同じ要素なら等しくする記事になります!
突然ですが、以下のコードをみてみてください。
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
が出力されていて変ですよね。
一方はインスタンスによる比較を行い、一方は値(プロパティ)によって比較をする。
鋭い方はこれは値型・参照型による違いだと感じるかもしれませんが、合っているようであっていません。
等しいかを決めているのは比較される対象(今回はa
b
)の型によって定義をされているのです。
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の引数のみならず、Dictionary
やHashtable
といったコレクションのコンストラクタの引数にも使われたりします。
また今度それらの記事を書こうと思ったり、思わなかったり。
かなり長い記事になってしまいましたがこれくらいで。
ではまた。