NOTE: この記事は、当初、ココログの「いげ太のブログ」で公開していたものです。
.NET 4 から、BCL にタプルが入りました。
結局、タプルは参照型として実装されましたが、それはほとんど値型として振る舞うべきものであるようにも見えます。タプルはただ複数の値をまとめるためにあり、興味があるのはそのまとめられた値の方です。となれば、なにか 2 つのタプルを比較するときには、タプルのインスタンスの参照比較ではなくタプルの中身の値によって比較を行いたい、という場合がほとんどではないでしょうか。
「Equals および等値演算子 (==) 実装のガイドライン」によれば、参照型においては、Point、String、BigNumber などの基本型を除き、等値演算子 (==) をオーバーロードしない方針がとられるようです。タプルはどうでしょうか。
var x = Tuple.Create(42, "Foo");
var y = Tuple.Create(42, "Foo");
var z = Tuple.Create(42, 256);
Console.WriteLine(x == y); // False
Console.WriteLine(x == z); // Compile Error
タプルは基本型とはみなさない、ということでしょうか。等値演算子によるタプルの比較は参照比較になります。等値演算子を用いた場合は引数に同じ型の値が要求されますが、それ以外は ReferenceEquals メソッドを使った場合となんら変わりません。
Console.WriteLine(Object.ReferenceEquals(x, y)); // False
Console.WriteLine(Object.ReferenceEquals(x, z)); // False
タプルの内容物がそれぞれ等値であるかどうか、つまり構造的等値を評価したい場合は、前述のガイドラインが示すように Equals メソッドを使います。
Console.WriteLine(x.Equals(y)); // True
Console.WriteLine(x.Equals(z)); // False
ここで考えてみたいのは x.Equals(z) です。これは妥当でしょうか。x は Tuple<Int32, String> で、z は Tuple<Int32, Int32> です。このような処理が必要な場面もあるでしょう。しかし、強い静的型付けによる便益を最大限活用しようとするなら、これはなるべく避けたい事態です。そもそも型が違う値どうしが構造的な等値関係になることはありえませんから、コンパイル エラーで弾いてもらった方が賢明でしょう。
そこで、他の手段を考えます。
Console.WriteLine(EqualityComparer<Tuple<int, string>>.Default.Equals(x, y)); // True
Console.WriteLine(EqualityComparer<Tuple<int, string>>.Default.Equals(x, z)); // Compile Error
EqualityComparer<T>.Default プロパティから等値比較子クラス EqualityComparer<Tuple<Int32, String>> のインスタンスを得て、その Equals メソッドで等値比較を行います。EqualityComparer<Tuple<Int32, String>>.Equals メソッドによって、比較する 2 つの値がともに Tuple<Int32, String> 型であることがコンパイル時に保証され、そして構造的な等値比較が行われます。
なお、EqualityComparer<T> は構造的等値のためだけにあるのではありません。あくまで、型 T によって定義された等値比較を行うためのものです。タプルに定義された等値比較が構造的等値を行うものであるので、このような結果が得られるのです。
さて、等値比較とあわせて大小比較についても見ておきましょう。比較対象が同じ型であることをコンパイル時に保証し、かつ、構造的比較を行うコードです。
var x = Tuple.Create(1, 0);
var y = Tuple.Create(1, 9);
var z = Tuple.Create(2, 0);
var foo = Tuple.Create(1, "Foo");
Console.WriteLine(Comparer<Tuple<int, int>>.Default.Compare(x, y)); // -1
Console.WriteLine(Comparer<Tuple<int, int>>.Default.Compare(x, x)); // 0
Console.WriteLine(Comparer<Tuple<int, int>>.Default.Compare(y, x)); // 1
Console.WriteLine(Comparer<Tuple<int, int>>.Default.Compare(y, z)); // -1
Console.WriteLine(Comparer<Tuple<int, int>>.Default.Compare(x, foo)); // Compile Error
EqualityComparer<T> クラスによる等値比較とほとんど同じです。Comparer<T>.Default プロパティから比較子クラス Comparer<Tuple<Int32, Int32>> のインスタンスを得て、Compare メソッドで比較を行います。
IComparable インターフェイス経由で比較を行うこともできますが、タプルが実装しているのは IComparable<T> でなく IComparable ですので、比較対象の値の型が異なる場合は実行時に例外がスローされます。ほとんどの場合、タプルの比較は Comparer<T> で行うのがよいでしょう。
Console.WriteLine(((IComparable)x).CompareTo(y)); // -1
Console.WriteLine(((IComparable)x).CompareTo(foo)); // ArgumentException
Console.WriteLine(((IComparable)x).CompareTo("foo")); // ArgumentException
このとき、タプルを IComparable にキャストするのを忘れないでください。タプルでは、IComparable.CompareTo メソッドを明示的に実装しているので、キャストしなければ呼び出すことができません。
最後に、この記事のサンプル コードでは考慮していませんが、EqualityComparer<T>.Default および Comparer<T>.Default を大量に呼び出すようなケースでは、それらをいったん変数に代入しておいた方がパフォーマンスがよいかもしれません。
Conclusion
.NET のジェネリックを十分に使いこなしている方々にとっては、なにをいまさらといった話かもしれません。しかしながら、僕が今回書いたようなことにずばり言及している情報は、すこしググっただけでは見つかりませんでした。
知らなければ、ただ回りくどいコードを量産していたかもしれません。
var x = Tuple.Create(42, "Foo");
var y = Tuple.Create(42, "Foo");
Console.WriteLine(x.Item1 == y.Item1 && x.Item2 == y.Item2);
便利なものは便利なままに使いたい。タプルというとても便利なクラスが BCL 入りしたことで、EqualityComparer<T> や Comparer<T> は、これから、よりいっそう使われていくことになるのではないでしょうか。