Alguém pode explicar esse comportamento estranho com carros alegóricos assinados em c #?

247

Aqui está o exemplo com comentários:

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

Então, o que você pensa sobre isso?

Alexander Efimov
fonte
2
Para tornar as coisas mais estranhas c.d.Equals(d.d)avalia a truecomo o fazc.f.Equals(d.f)
Justin Niessner
2
Não compare carros alegóricos com comparação exata como .Equals. É simplesmente uma má ideia.
precisa
6
@ Thorsten79: Como isso é relevante aqui?
Ben M
2
Isso é muito estranho. Usar um longo em vez de um duplo para f apresenta o mesmo comportamento. E adicionar outro campo curta corrige-lo novamente ...
Jens
1
Estranho - só parece acontecer quando ambos são do mesmo tipo (float ou double). Altere um para flutuar (ou decimal) e D2 funciona da mesma forma que D1.
tvanfosson

Respostas:

387

O erro está nas duas linhas a seguir System.ValueType: (Entrei na fonte de referência)

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

(Ambos os métodos são [MethodImpl(MethodImplOptions.InternalCall)])

Quando todos os campos têm 8 bytes de largura, CanCompareBitsretorna true por engano, resultando em uma comparação bit a bit de dois valores diferentes, mas semanticamente idênticos.

Quando pelo menos um campo não tem 8 bytes de largura, CanCompareBitsretorna false e o código continua a usar a reflexão para fazer um loop sobre os campos e chamar Equalscada valor, que é corretamente tratado -0.0como igual a 0.0.

Aqui está a fonte CanCompareBitsdo 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
SLaks
fonte
158
Entrar no System.ValueType? Isso é mano muito hardcore.
Pierreten
2
Você não explica qual é o significado de "8 bytes de largura". Uma estrutura com todos os campos de 4 bytes não teria o mesmo resultado? Eu estou supondo que ter um único campo de 4 bytes e um de 8 bytes apenas é acionado IsNotTightlyPacked.
Gabe
1
@Gabe eu escrevi anteriormente queThe bug also happens with floats, but only happens if the fields in the struct add up to a multiple of 8 bytes.
SLaks
1
Com o .NET sendo um software de código aberto agora, aqui está um link para a implementação do Core CLR do ValueTypeHelper :: CanCompareBits . Não quis atualizar sua resposta, pois a implementação foi ligeiramente alterada da fonte de referência que você postou.
11nspectable
59

Encontrei a resposta em http://blogs.msdn.com/xiangfan/archive/2008/09/01/magic-behind-valuetype-equals.aspx .

A peça principal é o comentário da fonte CanCompareBits, que é ValueType.Equalsusado para determinar se é usada a memcmpcomparação de estilo:

O comentário de CanCompareBits diz "Retornar verdadeiro se o tipo de valor não contiver ponteiro e estiver compactado". E o FastEqualsCheck usa "memcmp" para acelerar a comparação.

O autor continua declarando exatamente o problema descrito pelo OP:

Imagine que você tem uma estrutura que contém apenas um flutuador. O que ocorrerá se um contiver +0,0 e o outro contiver -0,0? Eles devem ser os mesmos, mas a representação binária subjacente é diferente. Se você aninhar outra estrutura que substitua o método Equals, essa otimização também falhará.

Ben M
fonte
Gostaria de saber se o comportamento do Equals(Object)para double, floate Decimalmudou durante os primeiros rascunhos de .net; Eu acho que é mais importante que o virtual X.Equals((Object)Y)retorne apenas truequando Xe Yé indistinguível, do que fazer com que o método corresponda ao comportamento de outras sobrecargas (especialmente porque, devido à coerção implícita do tipo, os Equalsmétodos sobrecarregados nem definem uma relação de equivalência !, por exemplo, 1.0f.Equals(1.0)gera false, mas 1.0.Equals(1.0f)gera true!) O verdadeiro problema do IMHO não é com a maneira como as estruturas são comparadas ... #
687
1
... mas da maneira que esses tipos de valor substituem Equalssignifica algo diferente de equivalência. Suponha, por exemplo, que se queira escrever um método que pegue um objeto imutável e, se ainda não foi armazenado em cache, o executa ToStringe armazena em cache o resultado; se tiver sido armazenado em cache, simplesmente retorne a sequência em cache. Não é uma coisa irracional, mas falharia mal Decimalporque dois valores podem comparar iguais, mas produzir seqüências diferentes.
Super dec
52

A conjectura de Vilx está correta. O que "CanCompareBits" faz é verificar se o tipo de valor em questão está "compactado" na memória. Uma estrutura compactada é comparada simplesmente comparando os bits binários que compõem a estrutura; uma estrutura fracamente compacta é comparada chamando Igual a todos os membros.

Isso explica a observação de SLaks de que ele é reprogramado com estruturas que são todas duplas; tais estruturas são sempre bem compactadas.

Infelizmente, como vimos aqui, isso introduz uma diferença semântica porque a comparação bit a bit de duplas e a comparação igual de duplas fornece resultados diferentes.

Eric Lippert
fonte
3
Então por que não é um bug? Mesmo que a MS recomende substituir sempre iguais em tipos de valor.
Alexander Efimov
14
Bate o inferno fora de mim. Não sou especialista em assuntos internos do CLR.
precisa
4
... você não é? Certamente, seu conhecimento dos componentes internos do C # levaria a um conhecimento considerável de como o CLR funciona.
CaptainCasey
37
@CaptainCasey: Passei cinco anos estudando os componentes internos do compilador C # e provavelmente, no total, algumas horas estudando os componentes internos do CLR. Lembre-se, eu sou consumidor do CLR; Entendo sua área pública superficial razoavelmente bem, mas seus internos são uma caixa preta para mim.
precisa
1
Meu erro, eu pensei que o CLR eo # compiladores VB / C foram mais fortemente acoplados ... então C # / VB -> CIL -> CLR
CaptainCasey
22

Meia resposta:

Refletor nos diz que ValueType.Equals()faz algo parecido com isto:

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

Infelizmente, ambos CanCompareBits()e FastEquals()(ambos os métodos estáticos) são externos ( [MethodImpl(MethodImplOptions.InternalCall)]) e não têm fonte disponível.

Voltando a adivinhar por que um caso pode ser comparado por bits e o outro não (problemas de alinhamento, talvez?)

Vilx-
fonte
17

Isso é verdadeiro para mim, com os gmcs do Mono 2.4.2.3.

Matthew Flaschen
fonte
5
Sim, eu também tentei em Mono, e isso também é verdade. Looks como o MS faz algum dentro mágica :)
Alexander Efimov
3
interessante, todos nós enviamos para Mono?
WeNeedAnswers
14

Caso de teste mais simples:

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 : O bug também acontece com carros alegóricos, mas só acontece se os campos na estrutura somam um múltiplo de 8 bytes.

SLaks
fonte
Looks como uma regra de otimizador que diz: se seus todas as duplas que fazem um, senão fazer chamadas double.Equal separados comparar bits
Henk HOLTERMAN
Eu não acho que esse seja o mesmo caso de teste que o problema apresentado aqui parece ser que o valor padrão para Bad.f não é 0, enquanto o outro caso parece ser um problema Int vs. Duplo.
Driss Zouak
6
@Driss: o valor padrão para double é 0 . Você está errado.
SLaks
10

Ele deve estar relacionado à comparação pouco a pouco, pois 0.0deve diferir -0.0apenas do bit de sinal.

João Angelo
fonte
5

…o que você pensa sobre isso?

Sempre substitua Equals e GetHashCode nos tipos de valor. Será rápido e correto.

Viacheslav Ivanov
fonte
Além de uma ressalva de que isso só é necessário quando a igualdade é relevante, é exatamente isso que eu estava pensando. Por mais divertido que seja analisar as peculiaridades do comportamento de igualdade do tipo de valor padrão, como as respostas mais votadas, há uma razão pela qual CA1815 existe.
21815 Joe Amenta
@JoeAmenta Desculpe por uma resposta tardia. Na minha opinião (apenas na minha opinião, é claro), a igualdade é sempre ( ) relevante para os tipos de valor. A implementação de igualdade padrão não é aceitável em casos comuns. ( ) Exceto casos muito especiais. Muito. Muito especial. Quando você sabe exatamente o que está fazendo e por quê.
Viacheslav Ivanov
Acho que concordamos que a substituição das verificações de igualdade por tipos de valor é praticamente sempre possível e significativa, com muito poucas exceções, e geralmente tornará estritamente mais correta. O ponto que eu estava tentando transmitir com a palavra "relevante" era que existem alguns tipos de valores cujas instâncias nunca serão comparadas com outras instâncias de igualdade; portanto, a substituição resultaria em código morto que precisa ser mantido. Esses (e os casos especiais estranhos aos quais você alude) seriam os únicos lugares que eu ignoraria.
Joe Amenta
4

Apenas uma atualização para esse bug de 10 anos: foi corrigida ( Isenção de responsabilidade : sou o autor deste PR) no .NET Core, que provavelmente seria lançado no .NET Core 2.1.0.

A postagem do blog explicou o erro e como eu o corrigi.

Jim Ma
fonte
2

Se você faz D2 assim

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

é verdade.

se você faz assim

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

Ainda é falso.

i t parece que é falso se a estrutura só tem duplos.

Morten Anderson
fonte
1

Deve ser zero, pois alterar a linha

dd = -0,0

para:

dd = 0,0

resulta na comparação verdadeira ...

user243357
fonte
Por outro lado, os NaN poderiam comparar-se entre si para uma mudança, quando eles realmente usam o mesmo padrão de bits.
Harold