==かEqualsか Equals および等値演算子 (==) 実装のガイドライン
[引用]
いや、値型と参照型を混在させるまでもなく、参照型同士の比較でも、Equals の方はタイプセーフでなく、==はタイプセーフなのです。
Equals の「オーバーライド」がタイプセーフではない、というのは同意だ。
なので Equals の「オーバーロード」を行い、タイプセーフ版も用意しておく。
菊池氏のコードを私なりに記述すると以下のようになる。
class Test
{
public int a = 0;
public int b = 1;
public override bool Equals( object obj )
{
Test other = obj as Test;
if( other == null )
{
return false;
}
return Equals( other );
}
public bool Equals( Test value )
{
if( ReferenceEquals( this, value ) )
{
return true;
}
return ( a == value.a ) && ( b==value.b );
}
public static bool Equals( Test t1, Test t2 )
{
if( ReferenceEquals( t1, t2 ) )
{
return true;
}
// ReferenceEquals( null, null ) は True だからこれでOK
if( ( t1 == null ) || ( t2 == null ) )
{
return false;
}
return t1.Equals( t2 );
}
// 書き換えた。
public override int GetHashCode()
{
return a ^ b;
}
public static bool operator ==( Test t1, Test t2 )
{
return Test.Equals( t1, t2 );
}
public static bool operator !=( Test t1, Test t2 )
{
return !Test.Equals( t1, t2 );
}
}
しかし、タイプセーフ版の Equals を用意しても、タイプセーフでない Equals も使えてしまうので意味がない。
これは値型のときに大きな意味を成す。そう、無駄なボックス化を回避するためだ。
このとき、
public override bool Equals( object obj )
は意味あるか?って感じだが、こいつは
static bool Equals( Object objA, Object objB )
が内部で呼び出しているので必要だ。どっちにしろ削除することは出来ないし。
== 演算子を用意すべきかどうかという問題に関しては、「Equals および等値演算子 (==) 実装のガイドライン」には次のようにある。私なりに解釈した。
プログラミング言語によっては、参照型の == 演算子をオーバーロードしたとしても、「同じ参照を指しているか」で判断するかもしれない。既定の実装というやつだ。
つまり、参照型の場合には == 演算子をオーバーロードしても、それが使われる保障がない。
プログラミング言語によっては、値型の == 演算子の既定の実装が用意されておらず、Equals をオーバーライドするような型は == 演算子をオーバーロードする必要がある。また別の言語によっては、== と書かれれば、Equals を呼び出すことを既定の実装にしているかもしれない。
言語を特定しない .NET の場合、用意した演算子のオーバーロードが使用されるかどうかが不明だ(と言っていると思う。勘違いかもしれない。意見求ム)。だから演算子のオーバーロードのみ行うというのは言語道断だ。== を変更するときは、いつだって Equals も変更する。
逆はどうだ?
Equals のみ変更して、== は変更しない。これはアリかもしれない。しかし、クラスに用意された演算子のオーバーロードを使用する言語にとっては、Equals と == の意味が同様であることを求めるはずだ。よって Equals を変更するときは == も変更するほうがよい。同じ参照を指しているかを比較するときは、いつだって ReferenceEquals を使えばよいのだ。
今度はクライアントの話。== を使うべきか、Equals を使うべきか。
C# に限って話をしよう。
「コンパイラが単純型とみなす型」というのが存在する。Int32 や Double、Object 等だ。(String はどうやら違う)
int a = 0;
int b = 1;
Test t1 = new Test();
Test t2 = new Test();
string s1 = "a";
string s2 = "b";
object o1 = new object();
object o2 = new object();
bool result;
result = a == b;
result = a.Equals( b );
result = t1 == t2;
result = t1.Equals( t2 );
result = s1 == s2;
result = s1.Equals( s2 );
result = o1 == o2;
result = o1.Equals( o2 );
以上を、ILDASM で解析してみると、Equals に違いはない。全ての型で Equals メソッドを呼び出しているだけだ。int ではその前にボックス化の処理が入っていた。Int32 にタイプセーフ版の Equals が用意されていないのが悲しかった。(.NET2.0 ではしっかり用意されているので心配無用だ!)
対して == を使用している箇所で違いが現れている。
int と object は「ceq」という命令で比較しているのに対し、Test と string は「op_Equality」というメソッドを呼び出している。
op_Equality は == 演算子のオーバーロードなので、内側で Equals を呼び出しているという実装だろう。(逆かも。Equqls が内側で == を呼び出している。)
ceq は IL の命令レベルで比較できるという事。
つまり効率が圧倒的に違う。
int は何から出来ている?でも述べたが、int は Int32 のエイリアスではない。Int32 が int のエイリアスなのだ。
「コンパイラが単純型とみなす型」については、Equals が == に意味を合わせている。
「コンパイラが単純型とみなす型」については、絶対に Equqls よりも == を使用すべきだ。
他の型はどうだろう。
Equals を使っても == を使っても大差ない。もちろん Equals をオーバーライドしていて == 演算子もオーバーロードしている型については、だ。
じゃあどっちを使う?
== だ。何故なら「コンパイラが単純型とみなす型」と区別したくないからだ。どんな型でも同様に使いたい。だから比較するときは == で統一する。
「コンパイラが単純型とみなさない型」については、クラスの実装者が Equals と == の意味を合わせる。
結論。
1. Equals をオーバーライドするときは、== もオーバーロードする。
2. 1 が成立している事が前提で、C# では Equals よりも == を使用する。