¿Puede alguien explicar este extraño comportamiento con flotadores firmados en C#?


Aquí está el ejemplo con comentarios:

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

Entonces, ¿qué piensas de esto?

Author: Dan Dascalescu, 2010-03-24

11 answers

El error está en las siguientes dos líneas de System.ValueType: (Entré en la fuente de referencia)

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

(Ambos métodos son [MethodImpl(MethodImplOptions.InternalCall)])

Cuando todos los campos tienen 8 bytes de ancho, CanCompareBits devuelve por error true, lo que resulta en una comparación bit a bit de dos valores diferentes, pero semánticamente idénticos.

Cuando al menos un campo no tiene 8 bytes de ancho, CanCompareBits devuelve false, y el código procede a usar reflexión para recorrer los campos y llamar a Equals para cada valor, que trata correctamente -0.0 como igual a 0.0.

Aquí está la fuente para CanCompareBits de SSCLI:

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
 373
Author: SLaks,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-07-05 16:15:13

Encontré la respuesta en http://blogs.msdn.com/xiangfan/archive/2008/09/01/magic-behind-valuetype-equals.aspx .

La pieza central es el comentario fuente sobre CanCompareBits, que ValueType.Equals utiliza para determinar si usar memcmp-comparación de estilo:

El comentario de CanCompareBits dice "Devuelve true si el valuetype no contener puntero y es firmemente lleno". Y Uso rápido de Qualscheck "memcmp" para acelerar la comparación.

El autor continúa indique exactamente el problema descrito por el OP:

Imagina que tienes una estructura que solo contiene un flotador. Qué ocurrirá si uno contiene +0.0, y el otro contiene -0.0? Deberían ser los lo mismo, pero el binario subyacente la representación es diferente. Si anida otra estructura que anula el método Igual, esa optimización también fallará.

 59
Author: Ben M,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-03-24 15:56:44

La conjetura de Vilx es correcta. Lo que hace " CanCompareBits "es comprobar si el tipo de valor en cuestión está" apretado " en la memoria. Una estructura compacta se compara simplemente comparando los bits binarios que componen la estructura; una estructura poco compacta se compara llamando Iguales en todos los miembros.

Esto explica la observación de SLaks de que repro con estructuras que son todas dobles; tales estructuras siempre están bien empaquetadas.

Desafortunadamente, como hemos visto aquí, eso introduce una diferencia semántica porque la comparación bit a bit de dobles e Igual a la comparación de dobles da resultados diferentes.

 52
Author: Eric Lippert,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-03-24 16:03:48

Media respuesta:

Reflector nos dice que ValueType.Equals() hace algo como esto:

if (CanCompareBits(this))
    return FastEqualsCheck(this, obj);
else
    // Use reflection to step through each member and call .Equals() on each one.

Desafortunadamente tanto CanCompareBits() como FastEquals() (ambos métodos estáticos) son externos ([MethodImpl(MethodImplOptions.InternalCall)]) y no tienen fuente disponible.

Volver a adivinar por qué un caso puede ser comparado por bits, y el otro no (problemas de alineación tal vez?)

 22
Author: Vilx-,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-03-24 15:53:01

hace dar verdadero para mí, con Mono gmcs 2.4.2.3.

 17
Author: Matthew Flaschen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-03-24 15:42:30

Caso de prueba más simple:

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

EDITAR : El error también ocurre con flotadores, pero solo ocurre si los campos en la estructura suman un múltiplo de 8 bytes.

 14
Author: SLaks,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-03-24 19:28:11

Debe estar relacionado con una comparación bit a bit, ya que 0.0 debe diferir de -0.0 solo por el bit de señal.

 10
Author: João Angelo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-03-24 15:52:59

{¿qué piensas de esto?

Siempre sobrescribe Equals y GetHashCode en los tipos de valor. Será rápido y correcto.

 5
Author: Viacheslav Ivanov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-11-28 06:13:36

Solo una actualización para este error de 10 años: se ha corregido (Descargo de responsabilidad: Soy el autor de este PR) en.NET Core que probablemente se publicaría en. NET Core 2.1.0.

La entrada del blog explicó el error y cómo lo arreglé.

 3
Author: Jim Ma,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-08-24 07:17:57

Si haces D2 así

public struct D2
{
    public double d;
    public double f;
    public string s;
}

Es verdad.

Si lo haces así

public struct D2
{
    public double d;
    public double f;
    public double u;
}

Sigue siendo falso.

it parece que es falso si la estructura solo contiene dobles.

 2
Author: Morten Anderson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-04-22 10:34:50

Debe estar relacionado con cero, ya que cambiando la línea

D. d = -0.0

A:

D. d = 0,0

Resulta que la comparación es verdadera...

 1
Author: user243357,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-03-24 15:50:34