누구나 C #에서 부호가있는 수레로 이상한 행동을 설명 할 수 있습니까?


247

주석이있는 예는 다음과 같습니다.

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

그래서 이것에 대해 어떻게 생각하십니까?


2
일 낯선 만들려면 c.d.Equals(d.d)평가를 true처럼c.f.Equals(d.f)
저스틴 Niessner에게

2
플로트를 .Equals와 같은 정확한 비교와 비교하지 마십시오. 그것은 단지 나쁜 생각입니다.
Thorsten79

6
@ Thorsten79 : 여기에 어떤 관련이 있습니까?
Ben M

2
이것은 가장 이상합니다. f 대신 double을 사용하면 동일한 동작이 발생합니다. 그리고 또 다른 짧은 필드를 추가하면 다시 수정됩니다 ...
Jens

1
이상한-둘 다 같은 유형 (부동 또는 이중) 일 때만 발생하는 것 같습니다. 하나를 부동 (또는 10 진수)으로 변경하면 D2는 D1과 동일하게 작동합니다.
tvanfosson

답변:


387

버그는 다음 두 줄에 있습니다 System.ValueType: (참조 소스에 들어갔습니다)

if (CanCompareBits(this)) 
    return FastEqualsCheck(thisObj, obj);

(두 방법 모두 [MethodImpl(MethodImplOptions.InternalCall)])

모든 필드의 너비가 8 바이트 인 경우 CanCompareBits실수로 true를 반환하여 서로 다르지만 의미 적으로 동일한 두 값을 비트 단위로 비교합니다.

하나 이상의 필드가 8 바이트 너비가 아닌 경우 CanCompareBitsfalse를 반환하고 코드는 리플렉션을 사용하여 필드를 반복하고 Equals각 값을 호출 -0.0합니다 0.0.

CanCompareBitsSSCLI 의 소스는 다음과 같습니다 .

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

159
System.ValueType에 스테핑? 그것은 꽤 하드 코어 형제입니다.
Pierreten

2
"8 바이트 너비"의 의미가 무엇인지 설명하지 않습니다. 4 바이트 필드가 모두있는 구조체의 결과가 같지 않습니까? 단일 4 바이트 필드와 8 바이트 필드를 갖는 것은 단지 트리거한다고 생각합니다 IsNotTightlyPacked.
Gabe

1
@Gabe 내가 이전에 썼다The bug also happens with floats, but only happens if the fields in the struct add up to a multiple of 8 bytes.
SLaks

1
현재 .NET이 오픈 소스 소프트웨어이므로 ValueTypeHelper :: CanCompareBits 의 Core CLR 구현에 대한 링크가 있습니다. 구현이 게시 한 참조 소스에서 약간 변경되었으므로 답변을 업데이트하고 싶지 않았습니다.
IInspectable

59

http://blogs.msdn.com/xiangfan/archive/2008/09/01/magic-behind-valuetype-equals.aspx 에서 답을 찾았습니다 .

핵심 부분은에 대한 소스 주석으로 CanCompareBits, 스타일 비교 ValueType.Equals사용 여부를 결정하는 데 사용됩니다 memcmp.

CanCompareBits의 의견은 "값 유형에 포인터가 포함되어 있지 않고 압축되어 있으면 true를 반환합니다"라고 말합니다. FastEqualsCheck는 "memcmp"를 사용하여 비교 속도를 높입니다.

저자는 OP에 의해 설명 된 문제를 정확하게 진술합니다.

float 만 포함하는 구조가 있다고 가정하십시오. 하나에 +0.0이 포함되고 다른 하나에 -0.0이 포함되면 어떻게됩니까? 그것들은 동일해야하지만 기본 이진 표현은 다릅니다. Equals 메서드를 재정의하는 다른 구조를 중첩하면 해당 최적화도 실패합니다.


궁금의 행동 경우 Equals(Object)double, float그리고 Decimal.NET의 초기 초안 동안 변경; 나는 그것이 가상이 더 중요하다고 생각 X.Equals((Object)Y)만을 반환 true하는 경우 XY그 방법 때문에 암시 적 강제 형 변환의 오버로드 것을 특히 주어진 다른 과부하 (의 동작을 일치시키는 것보다, 구별 Equals방법도 등가 관계를 정의하지 않습니다 !, 예를 들어, 1.0f.Equals(1.0)거짓을 산출하지만 1.0.Equals(1.0f)진실을 산출합니다!) 실제 문제 IMHO는 구조를 비교하는 방식에 있지 않습니다 ...
supercat

1
...하지만 그 가치 유형 Equals이 동등성 이외의 것을 의미하기 위해 재정의하는 방식으로 . 예를 들어, 불변 개체를 가져 와서 아직 캐시하지 않은 경우 수행 ToString하고 결과를 캐시 하는 메서드를 작성하려고한다고 가정합니다 . 캐시 된 경우 캐시 된 문자열을 반환하면됩니다. 불합리한 일은 아니지만 Decimal두 값이 동일하지만 다른 문자열을 생성 할 수 있기 때문에 실패 합니다.
supercat

52

Vilx의 추측은 맞습니다. "CanCompareBits"가 수행하는 작업은 해당 값 유형이 메모리에 "꽉 조여져 있는지"확인하는 것입니다. 꽉 채워진 구조체는 구조를 구성하는 이진 비트를 간단히 비교하여 비교됩니다. 느슨하게 패킹 된 구조는 모든 멤버에서 Equals를 호출하여 비교됩니다.

이것은 SLaks가 두 배인 구조체로 재현한다는 관찰을 설명합니다. 그러한 구조체는 항상 단단히 포장되어 있습니다.

불행히도 여기서 보았 듯이, double의 비트 비교와 double의 Equals 비교가 다른 결과를 나타 내기 때문에 의미상의 차이가 발생합니다.


3
그렇다면 왜 버그가 아닌가? MS는 항상 값 유형에서 같음을 재정의하는 것이 좋습니다.
Alexander Efimov

14
도대체 나를 이겼다. 저는 CLR의 내부 전문가가 아닙니다.
Eric Lippert

4
... 당신은 아닌가요? C # 내부에 대한 지식은 CLR 작동 방식에 대한 상당한 지식으로 이어질 것입니다.
CaptainCasey

37
@CaptainCasey : C # 컴파일러의 내부를 연구하는 데 5 년을 보냈으며 아마도 CLR의 내부를 연구하는 데 총 2 시간이 걸렸습니다. 저는 CLR 의 소비자 입니다. 나는 대중의 표면 영역을 합리적으로 잘 이해하지만 그 내부는 블랙 박스입니다.
Eric Lippert

1
내 실수는 CLR과 VB / C # 컴파일러가 더 밀접하게 결합되었다고 생각했기 때문에 C # /
VB-

22

반 답변 :

리플렉터는 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)])이며 사용할 수있는 소스가 없습니다.

한 사례를 비트별로 비교할 수있는 이유와 다른 사례를 비교할 수없는 이유로 돌아 가기 (정렬 문제 일 수 있습니까?)


17

그것은 않습니다 모노의 gmcs 2.4.2.3으로, 나를 위해 진정한 제공합니다.


5
예, 모노로 시도했지만 사실입니다. MS가 내부에서 마술을하는 것 같습니다 :)
Alexander Efimov

3
흥미 롭습니다, 우리 모두 모노에게 배송합니까?
WeNeedAnswers

14

간단한 테스트 사례 :

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

편집 : 버그도 float와 함께 발생하지만 구조체의 필드가 8 바이트의 배수가 될 때만 발생합니다.


모든 최적화가 비트 비교보다 두 배 더 많으면 별도의 두 배를 수행하십시오. 동일한 통화
Henk Holterman

나는 이것이 여기에 제시 된 문제와 같은 테스트 사례라고 생각하지 않습니다 .Bad.f의 기본값은 0이 아니며 다른 경우는 Int 대 Double 문제 인 것 같습니다.
Driss Zouak

6
@Driss :의 기본값 double 0 입니다. 네가 틀렸어.
SLaks

10

신호 비트 만 0.0다르므로 비트 단위 비교와 관련이 있어야합니다 -0.0.


5

… 이것에 대해 어떻게 생각하십니까?

값 유형에서 항상 Equals 및 GetHashCode를 대체하십시오. 빠르고 정확합니다.


평등과 관련이있을 때만 필요하다는 경고 이외에, 이것이 바로 내가 생각한 것입니다. 가장 높은 투표 응답과 같이 기본값 유형 평등 동작의 단점을 보는 것이 재미 있기 때문에 CA1815 가 존재 하는 이유 가 있습니다.
Joe Amenta

@JoeAmenta 답변이 늦어서 죄송합니다. 내 견해 (물론 내 견해로는)에서 평등은 항상 가치 유형과 관련이있다 ( ). 일반적인 경우 기본 동등 구현은 허용되지 않습니다. ( ) 아주 특별한 경우를 제외합니다. 대단히. 매우 특별합니다. 당신이 정확히 무엇을하고 있는지 왜 알았을 때.
Viacheslav Ivanov

가치 유형에 대한 동등성 검사를 재정의하는 것은 거의 예외없이 거의 항상 가능하고 의미가 있으며 일반적으로 엄격하게 더 정확할 것입니다. "관련"이라는 단어로 전달하려는 요점은 인스턴스가 다른 인스턴스와 동등하게 비교되지 않는 값 유형이 있으므로 재정의하면 죽은 코드가 유지 관리되어야한다는 것입니다. 그 (그리고 당신이 암시하는 이상한 특별한 경우)는 내가 그것을 건너 뛸 유일한 장소 일 것입니다.
Joe Amenta

4

이 10 년 된 버그에 대한 업데이트 일뿐입니다. .NET Core에서 수정되어 ( 면책 조항 :이 PR의 저자입니다) .NET Core 2.1.0에서 릴리스 될 수 있습니다.

블로그 게시물 버그를 설명하고 어떻게 그것을 해결했습니다.


2

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

여전히 거짓입니다.

내가 구조체는 두 배를 보유하고있는 경우는 false처럼 t 보인다.


1

행을 변경하므로 0과 관련이 있어야합니다.

dd = -0.0

에:

dd = 0.0

비교 결과는 사실입니다 ...


반대로 NaN은 실제로 동일한 비트 패턴을 사용할 때 변경에 대해 서로 동일하게 비교할 수 있습니다.
해롤드
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.