Por que o compilador C # traduz isso! = Comparação como se fosse uma comparação?

147

Por puro acaso, descobri que o compilador C # vira esse método:

static bool IsNotNull(object obj)
{
    return obj != null;
}

... neste CIL :

.method private hidebysig static bool IsNotNull(object obj) cil managed
{
    ldarg.0   // obj
    ldnull
    cgt.un
    ret
}

… Ou, se você preferir examinar o código C # descompilado:

static bool IsNotNull(object obj)
{
    return obj > null;   // (note: this is not a valid C# expression)
}

Como é que isso !=é traduzido como " >"?

stakx - não está mais contribuindo
fonte

Respostas:

201

Resposta curta:

Como não há instrução "compare-not-equal" em IL, o !=operador C # não tem correspondência exata e não pode ser traduzido literalmente.

Existe, no entanto, uma instrução "compare-equal" ( cequma correspondência direta com o ==operador), portanto, no caso geral, x != yé traduzida como seu equivalente um pouco mais longo (x == y) == false.

também uma instrução "compare-than-than" em IL ( cgt), que permite ao compilador pegar certos atalhos (ou seja, gerar código IL mais curto), sendo que comparações de desigualdade de objetos com nulos obj != nullsão traduzidas como se fossem " obj > null"

Vamos entrar em mais alguns detalhes.

Se não houver instrução "compare-not-equal" em IL, como o método a seguir será traduzido pelo compilador?

static bool IsNotEqual(int x, int y)
{
    return x != y;
}

Como já foi dito acima, o compilador transformará o arquivo x != yem (x == y) == false:

.method private hidebysig static bool IsNotEqual(int32 x, int32 y) cil managed 
{
    ldarg.0   // x
    ldarg.1   // y
    ceq
    ldc.i4.0  // false
    ceq       // (note: two comparisons in total)
    ret
}

Acontece que o compilador nem sempre produz esse padrão bastante longo. Vamos ver o que acontece quando substituímos ypela constante 0:

static bool IsNotZero(int x)
{
    return x != 0;
}

A IL produzida é um pouco menor do que no caso geral:

.method private hidebysig static bool IsNotZero(int32 x) cil managed 
{
    ldarg.0    // x
    ldc.i4.0   // 0
    cgt.un     // (note: just one comparison)
    ret
}

O compilador pode tirar proveito do fato de que números inteiros assinados são armazenados no complemento de dois (onde, se os padrões de bits resultantes são interpretados como números inteiros não assinados - é isso que .unsignifica - 0 tem o menor valor possível), portanto é traduzido x == 0como se fosse unchecked((uint)x) > 0.

Acontece que o compilador pode fazer o mesmo para verificações de desigualdade contra null:

static bool IsNotNull(object obj)
{
    return obj != null;
}

O compilador produz quase o mesmo IL que para IsNotZero:

.method private hidebysig static bool IsNotNull(object obj) cil managed 
{
    ldarg.0
    ldnull   // (note: this is the only difference)
    cgt.un
    ret
}

Aparentemente, o compilador pode assumir que o padrão de bits da nullreferência é o menor padrão de bits possível para qualquer referência de objeto.

Este atalho é mencionado explicitamente no Common Language Infrastructure Annotated Standard (1ª edição de outubro de 2003) (na página 491, como nota de rodapé da Tabela 6-4, "Comparações binárias ou operações de filial"):

" cgt.uné permitido e verificável em ObjectRefs (O). Isso é comumente usado ao comparar um ObjectRef com null (não há instruções" compare-not-equal ", que de outra forma seriam uma solução mais óbvia)."

stakx - não está mais contribuindo
fonte
3
Excelente resposta, apenas um ponto: o complemento de dois não é relevante aqui. É importante apenas que números inteiros assinados sejam armazenados de forma que os valores não negativos no intintervalo tenham a mesma representação intque eles uint. Esse é um requisito muito mais fraco que o complemento de dois.
3
Os tipos não assinados nunca possuem números negativos; portanto, uma operação de comparação comparada a zero não pode tratar nenhum número diferente de zero como menor que zero. Todas as representações correspondentes aos valores não negativos de intjá foram assumidas pelo mesmo valor em uint, portanto, todas as representações correspondentes aos valores negativos de intdevem corresponder a algum valor uintmaior que 0x7FFFFFFF, mas não importa realmente qual valor esse é. (Na verdade, tudo o que é realmente necessário é que o zero é representado da mesma maneira em ambos inte uint.)
3
@ DVD: Obrigado por explicar. Você está certo, não é o complemento de dois que importa; é o requisito que você mencionou e o fato de cgt.untratar um intcomo um uintsem alterar o padrão de bits subjacente. (Imagine que cgt.unseria primeiro tentar underflows correção mapeando todos os números negativos a 0. Nesse caso, você, obviamente, não poderia substituir > 0a != 0.)
stakx - não está mais contribuindo
2
Acho surpreendente que comparar uma referência de objeto a outra usando >seja IL verificável. Dessa forma, é possível comparar dois objetos não nulos e obter um resultado booleano (que não é determinístico). Esse não é um problema de segurança de memória, mas parece um design impuro que não faz parte do espírito geral do código gerenciado com segurança. Esse design vaza o fato de que as referências a objetos são implementadas como ponteiros. Parece uma falha de design da CLI do .NET.
usr
3
@usr: Absolutamente! A Seção III.1.1.4 do padrão CLI diz que "As referências a objetos (tipo O) são completamente opacas" e que "as únicas operações de comparação permitidas são igualdade e desigualdade ...". Talvez porque referências de objeto são não definido em termos de endereços de memória, o padrão também cuida para manter conceitualmente a referência nula além de 0 (ver por exemplo, as definições de ldnull, initobj, e newobj). Portanto, o uso de cgt.unpara comparar referências de objetos com referência nula parece contradizer a seção III.1.1.4 de mais de uma maneira.
stakx - não está mais contribuindo em