web-dev-qa-db-ja.com

誰もがC#で符号付きフロートを使用してこの奇妙な動作を説明できますか?

コメント付きの例を次に示します。

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        
    }
}

それで、あなたはこれについてどう思いますか?

247

バグは_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
_
386
SLaks

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メソッドをオーバーライドする他の構造をネストすると、その最適化も失敗します。

59
Ben M

Vilxの予想は正しい。 「CanCompareBits」が行うことは、問題の値の型がメモリに「密に詰め込まれている」かどうかを確認することです。密集した構造体は、構造体を構成するバイナリビットを比較するだけで比較されます。疎パック構造は、すべてのメンバーでEqualsを呼び出して比較されます。

これは、SLaksが観測した構造がすべて二重であるという再現を説明しています。そのような構造体は常に密集しています。

残念ながら、ここで見たように、doubleのビットごとの比較とdoubleのEquals比較は異なる結果をもたらすため、セマンティックの違いが生じます。

52
Eric Lippert

半分の答え:

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つのケースがビットで比較できない理由の推測に戻ります(おそらくアライメントの問題でしょうか?)

22
Vilx-

それはdoes Monoのgmcs 2.4.2.3で私に真を与える。

17

より簡単なテストケース:

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バイトの倍数になる場合にのみ発生します。

14
SLaks

0.0は信号ビットのみが-0.0と異なるため、ビットごとの比較に関連する必要があります。

10
João Angelo

…これについてどう思う?

値型のEqualsおよびGetHashCodeを常にオーバーライドします。それは速くて正しいでしょう。

5

この10年前のバグの更新:it 修正済み免責事項:私はこのPRの著者です).NET Core 2.1.0でおそらくリリースされる.NET Coreで。

ブログ投稿 でバグとその修正方法を説明しました。

4
Jim Ma

このように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のようです。

2
Morten Anderson

行を変更するため、ゼロに関連する必要があります

d.d = -0.0

に:

d.d = 0.0

比較が真になるという結果になります...

1
user243357