コメント付きの例を次に示します。
class Program
{
// first version of structure
public struct D1
{
public double d;
public int f;
}
// during some changes in code then we got D2 from D1
// Field f type became double while it was int before
public struct D2
{
public double d;
public double f;
}
static void Main(string[] args)
{
// Scenario with the first version
D1 a = new D1();
D1 b = new D1();
a.f = b.f = 1;
a.d = 0.0;
b.d = -0.0;
bool r1 = a.Equals(b); // gives true, all is ok
// The same scenario with the new one
D2 c = new D2();
D2 d = new D2();
c.f = d.f = 1;
c.d = 0.0;
d.d = -0.0;
bool r2 = c.Equals(d); // false! this is not the expected result
}
}
それで、あなたはこれについてどう思いますか?
バグは_System.ValueType
_の次の2行にあります:(参照ソースに足を踏み入れました)
_if (CanCompareBits(this))
return FastEqualsCheck(thisObj, obj);
_
(両方のメソッドは[MethodImpl(MethodImplOptions.InternalCall)]
です)
すべてのフィールドが8バイト幅の場合、CanCompareBits
は誤ってtrueを返し、2つの異なるが意味的には同一の値のビットごとの比較を行います。
少なくとも1つのフィールドが8バイト幅ではない場合、CanCompareBits
はfalseを返し、コードはリフレクションを使用してフィールドをループし、各値に対してEquals
を呼び出します。これにより_-0.0
_は_0.0
_と等しい。
SSCLIからのCanCompareBits
のソースは次のとおりです。
_FCIMPL1(FC_BOOL_RET, ValueTypeHelper::CanCompareBits, Object* obj)
{
WRAPPER_CONTRACT;
STATIC_CONTRACT_SO_TOLERANT;
_ASSERTE(obj != NULL);
MethodTable* mt = obj->GetMethodTable();
FC_RETURN_BOOL(!mt->ContainsPointers() && !mt->IsNotTightlyPacked());
}
FCIMPLEND
_
http://blogs.msdn.com/xiangfan/archive/2008/09/01/magic-behind-valuetype-equals.aspx で答えを見つけました。
核となる部分は、CanCompareBits
のソースコメントであり、memcmp
スタイルの比較を使用するかどうかを決定するためにValueType.Equals
が使用します。
CanCompareBitsのコメントには、「valuetypeにポインターが含まれておらず、密集している場合にtrueを返す」と書かれています。 FastEqualsCheckは「memcmp」を使用して比較を高速化します。
著者は、OPで記述されている問題を正確に述べています。
フロートのみを含む構造があると想像してください。一方に+0.0が含まれ、もう一方に-0.0が含まれる場合はどうなりますか?それらは同じでなければなりませんが、基礎となるバイナリ表現は異なります。 Equalsメソッドをオーバーライドする他の構造をネストすると、その最適化も失敗します。
Vilxの予想は正しい。 「CanCompareBits」が行うことは、問題の値の型がメモリに「密に詰め込まれている」かどうかを確認することです。密集した構造体は、構造体を構成するバイナリビットを比較するだけで比較されます。疎パック構造は、すべてのメンバーでEqualsを呼び出して比較されます。
これは、SLaksが観測した構造がすべて二重であるという再現を説明しています。そのような構造体は常に密集しています。
残念ながら、ここで見たように、doubleのビットごとの比較とdoubleのEquals比較は異なる結果をもたらすため、セマンティックの違いが生じます。
半分の答え:
Reflectorは、ValueType.Equals()
が次のようなことを行うことを示しています。
_if (CanCompareBits(this))
return FastEqualsCheck(this, obj);
else
// Use reflection to step through each member and call .Equals() on each one.
_
残念ながら、CanCompareBits()
とFastEquals()
(両方の静的メソッド)は両方ともextern([MethodImpl(MethodImplOptions.InternalCall)]
)であり、利用可能なソースがありません。
1つのケースをビットで比較できる理由と、もう1つのケースがビットで比較できない理由の推測に戻ります(おそらくアライメントの問題でしょうか?)
それはdoes Monoのgmcs 2.4.2.3で私に真を与える。
より簡単なテストケース:
Console.WriteLine("Good: " + new Good().Equals(new Good { d = -.0 }));
Console.WriteLine("Bad: " + new Bad().Equals(new Bad { d = -.0 }));
public struct Good {
public double d;
public int f;
}
public struct Bad {
public double d;
}
[〜#〜] edit [〜#〜]:バグもfloatで発生しますが、構造体のフィールドが8バイトの倍数になる場合にのみ発生します。
0.0
は信号ビットのみが-0.0
と異なるため、ビットごとの比較に関連する必要があります。
…これについてどう思う?
値型のEqualsおよびGetHashCodeを常にオーバーライドします。それは速くて正しいでしょう。
このようにD2を作成する場合
public struct D2
{
public double d;
public double f;
public string s;
}
それは本当です。
このようにすると
public struct D2
{
public double d;
public double f;
public double u;
}
まだ間違っています。
i tは、構造体がdoubleのみを保持している場合はfalseのようです。
行を変更するため、ゼロに関連する必要があります
d.d = -0.0
に:
d.d = 0.0
比較が真になるという結果になります...