Oto przykład z komentarzami:
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
}
}
Więc, co o tym myślisz?
c#
.net
floating-point
Alexander Efimov
źródło
źródło
c.d.Equals(d.d)
ocenia siętrue
podobniec.f.Equals(d.f)
Odpowiedzi:
Błąd występuje w następujących dwóch wierszach
System.ValueType
: (wszedłem do źródła odniesienia)(Obie metody są
[MethodImpl(MethodImplOptions.InternalCall)]
)Gdy wszystkie pola mają szerokość 8 bajtów,
CanCompareBits
omyłkowo zwraca wartość true, co powoduje bitowe porównanie dwóch różnych, ale semantycznie identycznych wartości.Gdy co najmniej jedno pole nie ma 8 bajtów szerokości,
CanCompareBits
zwraca wartość false, a kod kontynuuje użycie odbicia w celu zapętlenia pól i wywołaniaEquals
każdej wartości, co poprawnie traktuje-0.0
jako równe0.0
.Oto źródło
CanCompareBits
SSCLI:źródło
IsNotTightlyPacked
.The bug also happens with floats, but only happens if the fields in the struct add up to a multiple of 8 bytes.
Odpowiedź znalazłem na stronie http://blogs.msdn.com/xiangfan/archive/2008/09/01/magic-behind-valuetype-equals.aspx .
Najważniejszy jest komentarz źródłowy
CanCompareBits
, któryValueType.Equals
służy do określenia, czy użyćmemcmp
porównania w stylu:Następnie autor dokładnie stwierdza problem opisany przez PO:
źródło
Equals(Object)
zadouble
,float
iDecimal
zmieniane podczas wczesnych projektów .net; Myślę, że to ważne, aby mieć wirtualnyX.Equals((Object)Y)
zwrócić tylkotrue
kiedyX
iY
są nie do odróżnienia, niż mieć że metoda dopasować zachowanie innych przeciążeń (szczególnie zważywszy, że ze względu na niejawny typu przymusu, przeciążoneEquals
metody nie nawet zdefiniować relację równoważności !, np.1.0f.Equals(1.0)
daje wynik fałszywy, ale1.0.Equals(1.0f)
daje wynik prawdziwy!) Prawdziwy problem IMHO nie polega na sposobie porównywania struktur ...Equals
coś innego niż równoważność. Załóżmy na przykład, że chcemy napisać metodę, która pobiera niezmienny obiekt i, jeśli nie został jeszcze zbuforowany, wykonujeToString
go i buforuje wynik; jeśli został buforowany, po prostu zwróć buforowany łańcuch. Nie jest to nierozsądne, ale nie powiedzie się to źle,Decimal
ponieważ dwie wartości mogą się równać, ale dają różne ciągi.Przypuszczenie Vilxa jest prawidłowe. „CanCompareBits” sprawdza, czy dany typ wartości jest „ściśle upakowany” w pamięci. Ciasno upakowaną strukturę porównuje się po prostu przez porównanie bitów binarnych tworzących strukturę; luźno spakowaną strukturę porównuje się przez wywołanie równości dla wszystkich członków.
To wyjaśnia spostrzeżenie SLaksa, że reprosuje strukturami, które są podwójne; takie struktury są zawsze szczelnie zapakowane.
Niestety, jak widzieliśmy tutaj, wprowadza to różnicę semantyczną, ponieważ porównanie bitowe podwójnych i równe porównanie podwójnych daje różne wyniki.
źródło
Połowa odpowiedzi:
Reflektor mówi nam, że
ValueType.Equals()
robi coś takiego:Niestety obie
CanCompareBits()
iFastEquals()
(obie metody statyczne) są extern ([MethodImpl(MethodImplOptions.InternalCall)]
) i nie mają dostępnego źródła.Wróć do zgadywania, dlaczego jeden przypadek może być porównywany bitami, a drugi nie. (Może problemy z wyrównaniem?)
źródło
To ma dać prawdziwe dla mnie, z GMC Mono 2.4.2.3.
źródło
Prostszy przypadek testowy:
EDYCJA : Błąd występuje również w przypadku liczb zmiennoprzecinkowych, ale występuje tylko wtedy, gdy pola w strukturze sumują się do wielokrotności 8 bajtów.
źródło
double
to0
. Jesteś w błędzie.Musi być związany z porównaniem bit po bicie, ponieważ
0.0
powinien różnić się-0.0
tylko bitem sygnału.źródło
Zawsze zastępuj Equals i GetHashCode dla typów wartości. Będzie szybko i poprawnie.
źródło
Tylko aktualizacja tego 10-letniego błędu: została naprawiona ( Disclaimer : Jestem autorem tego PR) w .NET Core, który prawdopodobnie zostałby wydany w .NET Core 2.1.0.
Blogu wyjaśnił błąd i jak go naprawiłem.
źródło
Jeśli stworzysz D2 w ten sposób
to prawda.
jeśli tak to zrobisz
To wciąż fałsz.
I t wydaje się, że to fałsz, jeśli struktura posiada tylko podwaja.
źródło
Musi to być zero, ponieważ zmiana linii
do:
powoduje, że porównanie jest prawdziwe ...
źródło