Definição de operador "==" para Double

126

Por alguma razão, eu estava entrando na fonte do .NET Framework para a classe Doublee descobri que a declaração de ==é:

public static bool operator ==(Double left, Double right) {
    return left == right;
}

A mesma lógica se aplica a todos os operadores.


  • Qual é o sentido dessa definição?
  • Como funciona?
  • Por que não cria uma recursão infinita?
Thomas Ayoub
fonte
17
Eu esperaria uma recursão sem fim.
HimBromBeere
5
Tenho certeza de que não é usado para comparação em lugar nenhum com o dobro, em vez disso ceqé emitido em IL. Isso é apenas para preencher alguns propósitos de documentação, embora não seja possível encontrar a fonte.
Habib Habib
2
Provavelmente, para que este operador possa ser obtido através do Reflection.
#
3
Isso nunca será chamado, o compilador possui a lógica de igualdade (ceq opcode). Consulte Quando o operador == da Double é invocado?
Alex K.
1
@ZoharPeled dividir um duplo com zero é válido e resultará em infinito positivo ou negativo.
Magnus

Respostas:

62

Na realidade, o compilador transformará o ==operador em um ceqcódigo IL e o operador mencionado não será chamado.

É provável que o motivo do operador no código-fonte possa ser chamado de outros idiomas que não o C # que não o CEQconvertam diretamente em uma chamada (ou através de reflexão). O código dentro do operador será compilado para a CEQ, portanto, não haverá recursão infinita.

De fato, se você chamar o operador por meio de reflexão, poderá ver que o operador é chamado (em vez de uma CEQinstrução) e obviamente não é infinitamente recursivo (já que o programa termina conforme o esperado):

double d1 = 1.1;
double d2 = 2.2;

MethodInfo mi = typeof(Double).GetMethod("op_Equality", BindingFlags.Static | BindingFlags.Public );

bool b = (bool)(mi.Invoke(null, new object[] {d1,d2}));

IL resultante (compilada pelo LinqPad 4):

IL_0000:  nop         
IL_0001:  ldc.r8      9A 99 99 99 99 99 F1 3F 
IL_000A:  stloc.0     // d1
IL_000B:  ldc.r8      9A 99 99 99 99 99 01 40 
IL_0014:  stloc.1     // d2
IL_0015:  ldtoken     System.Double
IL_001A:  call        System.Type.GetTypeFromHandle
IL_001F:  ldstr       "op_Equality"
IL_0024:  ldc.i4.s    18 
IL_0026:  call        System.Type.GetMethod
IL_002B:  stloc.2     // mi
IL_002C:  ldloc.2     // mi
IL_002D:  ldnull      
IL_002E:  ldc.i4.2    
IL_002F:  newarr      System.Object
IL_0034:  stloc.s     04 // CS$0$0000
IL_0036:  ldloc.s     04 // CS$0$0000
IL_0038:  ldc.i4.0    
IL_0039:  ldloc.0     // d1
IL_003A:  box         System.Double
IL_003F:  stelem.ref  
IL_0040:  ldloc.s     04 // CS$0$0000
IL_0042:  ldc.i4.1    
IL_0043:  ldloc.1     // d2
IL_0044:  box         System.Double
IL_0049:  stelem.ref  
IL_004A:  ldloc.s     04 // CS$0$0000
IL_004C:  callvirt    System.Reflection.MethodBase.Invoke
IL_0051:  unbox.any   System.Boolean
IL_0056:  stloc.3     // b
IL_0057:  ret 

Curiosamente - os mesmos operadores não existem (ou na fonte de referência ou através de reflexão) para tipos integrais, única Single, Double, Decimal, String, e DateTime, o que refuta a minha teoria de que eles existem para ser chamado de outros idiomas. Obviamente, você pode igualar dois números inteiros em outros idiomas sem esses operadores, então voltamos à pergunta "por que eles existem double"?

D Stanley
fonte
12
O único problema que vejo com isso é que a especificação da linguagem C # diz que os operadores sobrecarregados têm precedência sobre os operadores internos. Portanto, certamente, um compilador C # em conformidade deve ver que um operador sobrecarregado está disponível aqui e gerar a recursão infinita. Hmm. Incomodador.
Damien_The_Unbeliever
5
Isso não responde à pergunta, imho. Explica apenas para o que o código é traduzido, mas não o porquê. De acordo com a seção 7.3.4, Resolução de sobrecarga do operador binário da especificação da linguagem C # também esperaria recursão infinita. Eu diria que a fonte de referência ( referenceource.microsoft.com/#mscorlib/system/… ) não se aplica aqui.
Dirk Vollmar 01/02
6
@ DStanley - não estou negando o que é produzido. Estou dizendo que não consigo reconciliá-lo com a especificação de idioma. Isso é preocupante. Eu estava pensando sobre debruçado através de Roslyn e ver se eu poderia encontrar qualquer especial manipulação aqui, mas eu não estou bem configurado para fazer isso no momento (máquina de errado)
Damien_The_Unbeliever
1
@Damien_The_Unbeliever É por isso que acho que é uma exceção à especificação ou uma interpretação diferente dos operadores "embutidos".
D Stanley
1
Como o @Jon Skeet ainda não respondeu ou comentou isso, suspeito que seja um bug (ou seja, violação das especificações).
TheBlastOne
37

A principal confusão aqui é que você está assumindo que todas as bibliotecas .NET (neste caso, a Biblioteca Numérica Estendida, que não faz parte da BCL) são gravadas em C # padrão. Isso nem sempre é o caso, e diferentes idiomas têm regras diferentes.

No C # padrão, o trecho de código que você vê resultaria em um estouro de pilha, devido à maneira como a resolução de sobrecarga do operador funciona. No entanto, o código não está realmente no C # padrão - ele basicamente usa recursos não documentados do compilador C #. Em vez de chamar o operador, ele emite este código:

ldarg.0
ldarg.1
ceq
ret

É isso :) Não há código C # 100% equivalente - isso simplesmente não é possível em C # com seu próprio tipo.

Mesmo assim, o operador real não é usado ao compilar o código C # - o compilador faz várias otimizações, como neste caso, onde substitui a op_Equalitychamada pelo simples ceq. Novamente, você não pode replicar isso em sua própria DoubleExestrutura - é mágica do compilador.

Certamente, essa não é uma situação única no .NET - há muito código que não é válido, C # padrão. Os motivos são geralmente (a) hacks de compilador e (b) um idioma diferente, com os hacks de tempo de execução ímpares (c) (estou olhando para você Nullable!).

Como o compilador C # da Roslyn é uma fonte oepn, posso apontar o local onde a resolução de sobrecarga é decidida:

O local em que todos os operadores binários são resolvidos

Os "atalhos" para operadores intrínsecos

Ao olhar para os atalhos, você verá que a igualdade entre duplo e duplo resulta no operador duplo intrínseco, nunca no ==operador real definido no tipo. O sistema de tipos .NET precisa fingir que Doubleé um tipo como outro qualquer, mas o C # não - doubleé um primitivo no C #.

Luaan
fonte
1
Não sei se concordo que o código na fonte de referência seja apenas "engenharia reversa". O código possui diretivas do compilador #ife outros artefatos que não estariam presentes no código compilado. Além disso, se fosse engenharia reversa para doubleentão por que não foi submetido a engenharia reversa para intou long? Eu acho que há uma razão para o código-fonte, mas acredito que o uso de ==dentro do operador é compilado para CEQimpedir a recursão. Como o operador é um operador "predefinido" para esse tipo (e não pode ser substituído), as regras de sobrecarga não se aplicam.
D Stanley
@ DStanley Eu não queria sugerir que todo o código tenha engenharia reversa. E, novamente, doublenão faz parte da BCL - está em uma biblioteca separada, que por acaso está incluída na especificação de C #. Sim, o ==compilado é para a ceq, mas isso ainda significa que este é um hack do compilador que você não pode replicar em seu próprio código e algo que não faz parte da especificação C # (assim como o float64campo na Doubleestrutura). Não é uma parte contratual do C #, portanto, não faz sentido tratá-lo como um C # válido, mesmo que tenha sido compilado com o compilador C #.
Luaan 02/02
@ DStanely Não consegui encontrar como a estrutura real está organizada, mas na implementação de referência do .NET 2.0, todas as partes complicadas são apenas intrínsecas do compilador, implementadas em C ++. Ainda há muito código nativo do .NET, é claro, mas coisas como "comparar duas duplas" não funcionariam muito bem no .NET puro; esse é um dos motivos pelos quais os números de ponto flutuante não estão incluídos na BCL. Dito isso, o código também é implementado em C # (não padrão), provavelmente exatamente pelo motivo mencionado anteriormente - para garantir que outros compiladores .NET possam tratar esses tipos como tipos .NET reais.
Luaan 02/02
@ DStanley Mas tudo bem, ponto de vista. Eu removi a referência "engenharia reversa" e reformulei a resposta para mencionar explicitamente "C # padrão", em vez de apenas C #. E não trate doubleda mesma maneira que inte long- inte longsão tipos primitivos aos quais todas as linguagens .NET devem suportar. float, decimalE doublenão são.
Luaan 02/02
12

A fonte dos tipos primitivos pode ser confusa. Você viu a primeira linha da Doubleestrutura?

Normalmente você não pode definir uma estrutura recursiva como esta:

public struct Double : IComparable, IFormattable, IConvertible
        , IComparable<Double>, IEquatable<Double>
{
    internal double m_value; // Self-recursion with endless loop?
    // ...
}

Os tipos primitivos também têm suporte nativo no CIL. Normalmente eles não são tratados como tipos orientados a objetos. Um duplo é apenas um valor de 64 bits se for usado como float64no CIL. No entanto, se for tratado como um tipo .NET usual, ele conterá um valor real e métodos como qualquer outro tipo.

Então, o que você vê aqui é a mesma situação para os operadores. Normalmente, se você usar o tipo de tipo duplo diretamente, ele nunca será chamado. BTW, sua fonte se parece com isso no CIL:

.method public hidebysig specialname static bool op_Equality(float64 left, float64 right) cil managed
{
    .custom instance void System.Runtime.Versioning.NonVersionableAttribute::.ctor()
    .custom instance void __DynamicallyInvokableAttribute::.ctor()
    .maxstack 8
    L_0000: ldarg.0
    L_0001: ldarg.1
    L_0002: ceq
    L_0004: ret
}

Como você pode ver, não há loop infinito (o ceqinstrumento é usado em vez de chamar o System.Double::op_Equality). Portanto, quando um double é tratado como um objeto, o método do operador será chamado, que eventualmente o manipulará como o float64tipo primitivo no nível CIL.

György Kőszeg
fonte
1
Para aqueles que não entendem a primeira parte deste post (talvez porque geralmente não escrevem seus próprios tipos de valor), tente o código public struct MyNumber { internal MyNumber m_value; }. Não pode ser compilado, é claro. O erro é o erro CS0523: O membro do Struct 'MyNumber.m_value' do tipo 'MyNumber' causa um ciclo no layout da estrutura
Jeppe Stig Nielsen
8

Dei uma olhada no CIL com JustDecompile. O interno ==é traduzido para o código operacional CQ ceq . Em outras palavras, é igualdade primitiva de CLR.

Fiquei curioso para ver se o compilador C # faria referência ceqou o ==operador ao comparar dois valores duplos. No exemplo trivial que eu criei (abaixo), ele usou ceq.

Este programa:

void Main()
{
    double x = 1;
    double y = 2;

    if (x == y)
        Console.WriteLine("Something bad happened!");
    else
        Console.WriteLine("All is right with the world");
}

gera o seguinte CIL (observe a instrução com o rótulo IL_0017):

IL_0000:  nop
IL_0001:  ldc.r8      00 00 00 00 00 00 F0 3F
IL_000A:  stloc.0     // x
IL_000B:  ldc.r8      00 00 00 00 00 00 00 40
IL_0014:  stloc.1     // y
IL_0015:  ldloc.0     // x
IL_0016:  ldloc.1     // y
IL_0017:  ceq
IL_0019:  stloc.2
IL_001A:  ldloc.2
IL_001B:  brfalse.s   IL_002A
IL_001D:  ldstr       "Something bad happened!"
IL_0022:  call        System.Console.WriteLine
IL_0027:  nop
IL_0028:  br.s        IL_0035
IL_002A:  ldstr       "All is right with the world"
IL_002F:  call        System.Console.WriteLine
IL_0034:  nop
IL_0035:  ret
Daniel Pratt
fonte
-2

Conforme indicado na documentação da Microsoft para o espaço para nome System.Runtime.Versioning: Os tipos encontrados neste espaço para nome destinam-se ao uso no .NET Framework e não a aplicativos do usuário.O espaço para nome System.Runtime.Versioning contém tipos avançados que oferecem suporte à versão implementações lado a lado do .NET Framework.

Thomas Papamihos
fonte
O que tem System.Runtime.Versioninga ver com isso System.Double?
precisa saber é o seguinte