Por que devemos definir ambos == e! = Em C #?

346

O compilador C # exige que sempre que um tipo personalizado defina operador ==, ele também deve definir !=(veja aqui ).

Por quê?

Estou curioso para saber por que os designers consideraram necessário e por que o compilador não pode usar como padrão uma implementação razoável para um dos operadores quando apenas o outro está presente. Por exemplo, Lua permite definir apenas o operador de igualdade e você obtém o outro gratuitamente. O C # pode fazer o mesmo solicitando que você defina == ou ambos == e! = E depois compile automaticamente o operador! = Ausente como !(left == right).

Entendo que existem casos de canto estranhos em que algumas entidades podem não ser iguais nem desiguais (como as NaN da IEEE-754), mas essas parecem ser a exceção, não a regra. Portanto, isso não explica por que os designers do compilador C # fizeram da exceção a regra.

Eu já vi casos de mão de obra deficiente em que o operador de igualdade é definido, então o operador de desigualdade é um copiar-colar com todas as comparações revertidas e todos os&& alternados para um || (você entendeu ... basicamente! (a == b) expandido pelas regras de De Morgan). Essa é uma prática ruim que o compilador poderia eliminar por design, como é o caso de Lua.

Nota: O mesmo vale para os operadores <> <=> =. Não consigo imaginar casos em que você precisará defini-los de maneiras não naturais. Lua permite definir apenas <e <= e define> = e> naturalmente através da negação dos formadores. Por que o C # não faz o mesmo (pelo menos 'por padrão')?

EDITAR

Aparentemente, existem razões válidas para permitir que o programador implemente verificações de igualdade e desigualdade da maneira que desejar. Algumas das respostas apontam para casos em que isso pode ser bom.

O núcleo da minha pergunta, no entanto, é por que isso é exigido à força no C # quando normalmente não é logicamente necessário?

Também está em contraste impressionante com as opções de design para interfaces .NET como Object.Equals, IEquatable.Equals IEqualityComparer.Equalsonde a falta de uma NotEqualscontrapartida mostra que a estrutura considera os !Equals()objetos como desiguais e é isso. Além disso, classes Dictionarye métodos como .Contains()dependem exclusivamente das interfaces mencionadas acima e não usam os operadores diretamente, mesmo que sejam definidos. De fato, quando o ReSharper gera membros de igualdade, ele define ambos ==e !=em termos de, Equals()e mesmo assim, somente se o usuário optar por gerar operadores. Os operadores de igualdade não são necessários pela estrutura para entender a igualdade de objetos.

Basicamente, a estrutura .NET não se preocupa com esses operadores, apenas se preocupa com alguns Equalsmétodos. A decisão de exigir que os operadores == e! = Sejam definidos em conjunto pelo usuário está relacionada exclusivamente ao design da linguagem e não à semântica de objetos no que diz respeito ao .NET.

Stefan Dragnev
fonte
24
+1 para uma excelente pergunta. Não parece haver nenhuma razão ... é claro que o compilador poderia assumir que a! = B pode ser traduzido para! (A == b), a menos que seja dito o contrário. Estou curioso para saber por que eles decidiram fazer isso também.
Patrick87
16
Este é o mais rápido que já vi uma pergunta ser votada e favorita.
Christopher Currens
26
Tenho certeza que @Eric Lippert também pode fornecer uma resposta sólida.
Yuck
17
"O cientista não é uma pessoa que dá as respostas certas, é aquele que faz as perguntas certas". É por isso que, no SE, as perguntas são incentivadas. E funciona!
Adriano Carneiro
4
A mesma pergunta para Python: stackoverflow.com/questions/4969629/…
Josh Lee

Respostas:

161

Não posso falar pelos designers da linguagem, mas pelo que posso raciocinar, parece que foi uma decisão de design intencional e adequada.

Observando esse código F # básico, você pode compilar isso em uma biblioteca de trabalho. Este é um código legal para o F # e sobrecarrega apenas o operador de igualdade, não a desigualdade:

module Module1

type Foo() =
    let mutable myInternalValue = 0
    member this.Prop
        with get () = myInternalValue
        and set (value) = myInternalValue <- value

    static member op_Equality (left : Foo, right : Foo) = left.Prop = right.Prop
    //static member op_Inequality (left : Foo, right : Foo) = left.Prop <> right.Prop

Isso faz exatamente o que parece. Ele cria apenas um comparador de igualdade ==e verifica se os valores internos da classe são iguais.

Embora não seja possível criar uma classe como esta em C #, você pode usar uma que foi compilada para .NET. É óbvio que ele usará nosso operador sobrecarregado para == Então, para que o tempo de execução usa !=?

O padrão C # EMCA possui várias regras (seção 14.9) que explicam como determinar qual operador usar ao avaliar a igualdade. Para simplificar demais e, portanto, não ser perfeitamente preciso, se os tipos que estão sendo comparados forem do mesmo tipo e houver um operador de igualdade sobrecarregado, ele usará essa sobrecarga e não o operador de igualdade de referência padrão herdado de Object. Não é surpresa, portanto, que, se apenas um dos operadores estiver presente, ele usará o operador de igualdade de referência padrão, que todos os objetos possuem, não haverá sobrecarga para ele. 1

Sabendo que esse é o caso, a verdadeira questão é: por que isso foi projetado dessa maneira e por que o compilador não o descobre por si próprio? Muitas pessoas estão dizendo que essa não foi uma decisão de design, mas eu gosto de pensar que foi pensado dessa maneira, especialmente no fato de todos os objetos terem um operador de igualdade padrão.

Então, por que o compilador não cria automaticamente o !=operador? Não sei ao certo, a menos que alguém da Microsoft confirme isso, mas é isso que posso determinar com base nos fatos.


Para evitar comportamento inesperado

Talvez eu queira fazer uma comparação de valores ==para testar a igualdade. No entanto, quando se tratava de !=eu não me importava se os valores eram iguais, a menos que a referência fosse igual, porque para o meu programa considerá-los iguais, eu me importo apenas se as referências corresponderem. Afinal, isso é descrito como comportamento padrão do C # (se os dois operadores não estivessem sobrecarregados, como seria o caso de algumas bibliotecas .net escritas em outro idioma). Se o compilador estava adicionando código automaticamente, eu não podia mais confiar no compilador para gerar o código que deveria ser compatível. O compilador não deve escrever código oculto que altera o seu comportamento, especialmente quando o código que você escreveu está dentro dos padrões de C # e da CLI.

Em termos de forçá- lo a sobrecarregá-lo, em vez de seguir o comportamento padrão, posso apenas afirmar com firmeza que ele está no padrão (EMCA-334 17.9.2) 2 . O padrão não especifica o porquê. Eu acredito que isso se deve ao fato de que o C # empresta muito comportamento do C ++. Veja abaixo mais sobre isso.


Quando você substitui !=e ==, não precisa retornar bool.

Este é outro motivo provável. Em C #, esta função:

public static int operator ==(MyClass a, MyClass b) { return 0; }

é tão válido quanto este:

public static bool operator ==(MyClass a, MyClass b) { return true; }

Se você estiver retornando algo diferente de bool, o compilador não poderá inferir automaticamente um tipo oposto. Além disso, no caso em que o seu operador faz retorno bool, ele simplesmente não faz sentido para eles criar gerar código que só existiria no caso específico de um ou, como eu disse acima, o código que esconde o comportamento padrão do CLR.


C # empresta muito do C ++ 3

Quando o C # foi introduzido, havia um artigo na revista MSDN que escrevia, falando sobre C #:

Muitos desenvolvedores desejam que haja uma linguagem fácil de escrever, ler e manter como o Visual Basic, mas que ainda forneça o poder e a flexibilidade do C ++.

Sim, o objetivo do projeto do C # era fornecer quase a mesma quantidade de energia do C ++, sacrificando apenas um pouco por conveniências como segurança rígida de tipos e coleta de lixo. C # foi fortemente modelado após C ++.

Você não ficará surpreso ao saber que, em C ++, os operadores de igualdade não precisam retornar bool , conforme mostrado neste programa de exemplo

Agora, o C ++ não exige diretamente que você sobrecarregue o operador complementar. Se você compilou o código no programa de exemplo, verá que ele é executado sem erros. No entanto, se você tentou adicionar a linha:

cout << (a != b);

você vai ter

erro do compilador C2678 (MSVC): binário '! =': nenhum operador encontrado que leva um operando esquerdo do tipo 'Teste' (ou não há conversão aceitável) `.

Portanto, embora o próprio C ++ não exija sobrecarga em pares, ele não permitirá que você use um operador de igualdade que você não sobrecarregou em uma classe personalizada. É válido no .NET, porque todos os objetos têm um padrão; C ++ não.


1. Como observação lateral, o padrão C # ainda exige que você sobrecarregue o par de operadores, se desejar sobrecarregar um dos dois. Isso faz parte do padrão e não simplesmente do compilador . No entanto, as mesmas regras relativas à determinação de qual operador chamar se aplicam quando você está acessando uma biblioteca .net escrita em outro idioma que não possui os mesmos requisitos.

2. EMCA-334 (pdf) ( http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-334.pdf )

3. E Java, mas esse não é realmente o ponto aqui

Christopher Currens
fonte
75
" você não precisa retornar bool " .. acho que essa é a nossa resposta ali; bem feito. Se! (O1 == o2) ainda não está bem definido, você não pode esperar que o padrão dependa disso.
Brian Gordon
10
Ironicamente, em um tópico sobre operadores lógicos, você usou um duplo negativo na frase "O C # não permite que você substitua apenas um dos dois", o que realmente significa logicamente que "O C # permite substituir apenas um dos dois".
Dan Diplo
13
@ Dan Diplo, está tudo bem, não é como se eu não estivesse proibido de não fazer isso ou não.
Christopher Currens
6
Se for verdade, suspeito que o número 2 esteja correto; mas agora a pergunta se torna: por que permitir ==retornar algo além de um bool? Se você retornar um int, não poderá mais usá-lo como uma declaração condicional, por exemplo. if(a == b)não será mais compilado. Qual é o objetivo?
BlueRaja - Danny Pflughoeft
2
Você pode retornar um objeto que tenha uma conversão implícita em bool (operador verdadeiro / falso), mas ainda não consigo ver onde isso seria útil.
Stefan Dragnev 3/08
53

Provavelmente, se alguém precisar implementar lógica de três valores (ou seja null). Em casos como esse - SQL padrão ANSI, por exemplo - os operadores não podem simplesmente ser negados, dependendo da entrada.

Você poderia ter um caso em que:

var a = SomeObject();

E a == trueretorna falsee a == falsetambém retorna false.

que nojo
fonte
11
Eu diria que nesse caso, como anão é um booleano, você deve compará-lo com um enum de três estados, não um real trueou false.
Brian Gordon
18
By the way, eu não posso acreditar que ninguém fez referência à piada FileNotFound ..
Brian Gordon
28
Se a == trueé false, então eu sempre esperar a != truepara ser true, apesar de a == falseestarfalse
Fede
20
@Fede acertou a unha na cabeça. Isso realmente não tem nada a ver com a pergunta - você está explicando por que alguém pode querer substituir ==, não por que eles devem ser forçados a substituir ambos.
BlueRaja - Danny Pflughoeft
6
@ Yuck - Sim, existe uma boa implementação padrão: é o caso comum. Mesmo no seu exemplo, a implementação padrão de !=estaria correta. No extremamente raro caso em que nós queremos x == ye x != ypara ser ambos falsos (Eu não consigo pensar em um único exemplo que faz sentido) , eles ainda poderiam substituir a implementação padrão e fornecer a sua própria. Sempre torne o caso comum o padrão!
BlueRaja - Danny Pflughoeft
25

Fora isso, o C # difere o C ++ em muitas áreas, a melhor explicação que posso pensar é que, em alguns casos, você pode querer adotar uma abordagem ligeiramente diferente para provar "não igualdade" do que provar "igualdade".

Obviamente, com a comparação de cadeias, por exemplo, você pode apenas testar a igualdade e returnsair do loop quando vir caracteres sem correspondência. No entanto, pode não ser tão limpo com problemas mais complicados. O filtro de floração vem à mente; é muito fácil saber rapidamente se o elemento não está no conjunto, mas difícil saber se o elemento está no conjunto. Embora a mesma returntécnica possa ser aplicada, o código pode não ser tão bonito.

Brian Gordon
fonte
6
Ótimo exemplo; Eu acho que você está no motivo. Um simples !trava você em uma única definição de igualdade, na qual um codificador informado pode otimizar ambos == e !=.
Yuck
13
Portanto, isso explica por que eles devem ter a opção de substituir os dois, não o motivo pelo qual devem ser forçados .
BlueRaja - Danny Pflughoeft
11
Nesse sentido, eles não precisam otimizar os dois, apenas precisam fornecer uma definição clara de ambos.
Jesper
22

Se você observar implementações de sobrecargas de == e! = Na fonte .net, elas geralmente não implementam! = As! (Esquerda == direita). Eles o implementam totalmente (como ==) com lógica negada. Por exemplo, DateTime implementa == como

return d1.InternalTicks == d2.InternalTicks;

e! = as

return d1.InternalTicks != d2.InternalTicks;

Se você (ou o compilador fez isso implicitamente) implementasse! = As

return !(d1==d2);

então você está assumindo a implementação interna de == e! = nas coisas que sua classe está referenciando. Evitar essa suposição pode ser a filosofia por trás de sua decisão.

machado - feito com SOverflow
fonte
17

Para responder sua edição, a respeito de por que você é forçado a substituir ambos, se você substituir um, está tudo na herança.

Se você substituir ==, provavelmente fornecerá algum tipo de igualdade semântica ou estrutural (por exemplo, DateTimes serão iguais se suas propriedades InternalTicks forem iguais, mesmo que possam haver instâncias diferentes), você estará alterando o comportamento padrão do operador de Object, que é o pai de todos os objetos .NET. O operador == é, em C #, um método cuja implementação básica Object.operator (==) executa uma comparação referencial. O Object.operator (! =) É outro método diferente, que também executa uma comparação referencial.

Em quase qualquer outro caso de substituição de método, seria ilógico presumir que a substituição de um método também resultaria em uma mudança comportamental em um método antônimo. Se você criou uma classe com os métodos Increment () e Decrement () e substituiu Increment () em uma classe filho, esperaria que Decrement () também fosse substituído pelo oposto do seu comportamento substituído? O compilador não pode ser inteligente o suficiente para gerar uma função inversa para qualquer implementação de um operador em todos os casos possíveis.

No entanto, os operadores, embora implementados de maneira muito semelhante aos métodos, trabalham conceitualmente em pares; == e! =, <e> e <= e> =. Seria ilógico, neste caso, do ponto de vista de um consumidor, pensar que! = Funcionou de maneira diferente de ==. Portanto, o compilador não pode assumir que a! = B ==! (A == b) em todos os casos, mas geralmente é esperado que == e! = Operem de maneira semelhante, portanto o compilador força você implementar em pares, no entanto, você acaba fazendo isso. Se, para a sua classe, a! = B ==! (A == b), basta implementar o operador! = Usando! (==), mas se essa regra não se aplicar em todos os casos do seu objeto (por exemplo, , se a comparação com um valor específico, igual ou desigual, não for válida), você deverá ser mais inteligente que o IDE.

A pergunta REAL que deve ser feita é por que <e> e <= e> = são pares para operadores comparativos que devem ser implementados simultaneamente, quando em termos numéricos! (A <b) == a> = be! (A> b) == a <= b. Você deve implementar todos os quatro se substituir um e provavelmente também substituir == (e! =), Porque (a <= b) == (a == b) se a for semanticamente igual a b.

KeithS
fonte
14

Se você sobrecarregar == para o seu tipo personalizado, e não! =, Ele será tratado pelo operador! = Para objeto! = Objeto, pois tudo é derivado do objeto, e isso seria muito diferente de CustomType! = CustomType.

Além disso, os criadores de idiomas provavelmente desejavam dessa maneira permitir a maior flexibilidade aos codificadores e também para que eles não fizessem suposições sobre o que você pretende fazer.

Chris Mullins
fonte
Você inspirou esta pergunta: stackoverflow.com/questions/6919259/…
Brian Gordon
2
O motivo da flexibilidade também pode ser aplicado a muitos recursos que eles decidiram deixar de fora, por exemplo blogs.msdn.com/b/ericlippert/archive/2011/06/23/… . Assim, não explica sua escolha por a implementação de operadores de igualdade.
Stefan Dragnev 3/08
Isso é muito interessante. Parece que isso seria um recurso útil. Não tenho ideia de por que eles teriam deixado isso de fora.
Chris Mullins
11

É isso que me vem à mente primeiro:

  • E se testar a desigualdade for muito mais rápido que testar a igualdade?
  • E se, em alguns casos você quiser voltar false, tanto para ==e!= (ou seja, se eles não podem ser comparados, por algum motivo)
Dan Abramov
fonte
5
No primeiro caso, você pode implementar o teste de igualdade em termos de teste de desigualdade.
Stefan Dragnev 2/08
O segundo caso quebrará estruturas como Dictionarye Listse você usar seu tipo personalizado com elas.
Stefan Dragnev 2/08
2
@ Stefan: Dictionaryusa GetHashCodee Equals, não os operadores. Listusos Equals.
Dan Abramov
6

Bem, provavelmente é apenas uma opção de design, mas como você diz, x!= ynão precisa ser o mesmo que !(x == y). Ao não adicionar uma implementação padrão, você tem certeza de que não pode esquecer de implementar uma implementação específica. E se é realmente tão trivial quanto você diz, você pode simplesmente implementar um usando o outro. Não vejo como isso é 'má prática'.

Também pode haver outras diferenças entre C # e Lua ...

GolezTrol
fonte
11
É uma prática excelente implementar um em termos do outro. Acabei de ver o contrário - o que é propenso a erros.
Stefan Dragnev 2/08
3
Esse é o problema do programador, não o problema do C #. Os programadores que não conseguirem implementar esse direito também falharão em outras áreas.
GolezTrol
@GolezTrol: Você poderia explicar essa referência a Lua? Não estou familiarizado com Lua, mas sua referência parece sugerir algo.
Zw324
@Ziyao Wei: OP aponta que Lua faz isso de maneira diferente do C # (Lua aparentemente adiciona contrapartidas padrão para os operadores mencionados). Apenas tentando banalizar essa comparação. Se não houver diferenças, pelo menos um deles não tem o direito de existir. Devo dizer que também não conheço Lua.
GolezTrol 02/08/19
6

As palavras-chave na sua pergunta são " por que " e " deve ".

Como um resultado:

Responder dessa maneira, porque eles o projetaram para ser assim, é verdade ... mas não responde à parte "por que" da sua pergunta.

Responder que às vezes pode ser útil substituir ambos independentemente, é verdade ... mas não responder à parte "obrigatória" da sua pergunta.

Eu acho que a resposta simples é que não nenhuma razão convincente para que o C # exija que você substitua os dois.

A linguagem deve permitir-lhe apenas para substituir ==, e fornecer-lhe uma implementação padrão !=que é !isso. Se você deseja substituir !=também, participe.

Não foi uma boa decisão. Humanos projetam linguagens, humanos não são perfeitos, C # não é perfeito. Encolher de ombros e QED

Greg Hendershott
fonte
5

Apenas para adicionar as excelentes respostas aqui:

Considere o que aconteceria no depurador , ao tentar entrar em um !=operador e acabar em um ==operador! Fale sobre confuso!

Faz sentido que o CLR permita a você deixar de fora um ou outro dos operadores - pois ele deve funcionar com muitos idiomas. Mas há uma abundância de exemplos de C # não expondo características CLR ( refretornos e habitantes locais, por exemplo), e uma abundância de exemplos de características de execução não no próprio CLR (por exemplo: using, lock, foreach, etc).

Andrew Russell
fonte
3

Linguagens de programação são rearranjos sintáticos de afirmações lógicas excepcionalmente complexas. Com isso em mente, você pode definir um caso de igualdade sem definir um caso de não-igualdade? A resposta é não. Para que um objeto a seja igual ao objeto b, o inverso do objeto a não é igual a b também deve ser verdadeiro. Outra maneira de mostrar isso é

if a == b then !(a != b)

isso fornece a capacidade definida para a linguagem determinar a igualdade de objetos. Por exemplo, a comparação NULL! = NULL pode lançar uma chave inglesa na definição de um sistema de igualdade que não implementa uma declaração de não igualdade.

Agora, no que diz respeito à idéia de! = Simplesmente ser uma definição substituível como em

if !(a==b) then a!=b

Eu não posso discutir com isso. No entanto, provavelmente foi uma decisão do grupo de especificação de linguagem C # que o programador fosse forçado a definir explicitamente a igualdade e a não igualdade de um objeto juntas.

Josh
fonte
Nullseria apenas um problema porque nullé uma entrada válida para a ==qual não deveria ser, embora obviamente seja massivamente conveniente. Pessoalmente, acho que isso permite grandes quantidades de programação vaga e desleixada.
precisa
2

Em suma, consistência forçada.

'==' e '! =' são sempre verdadeiros opostos, não importa como você os defina, definidos como tal pela definição verbal de "iguais" e "não iguais". Ao definir apenas um deles, você se abre para uma inconsistência do operador de igualdade em que '==' e '! =' Podem ser verdadeiros ou falsos para dois valores fornecidos. Você deve definir ambos, pois, ao escolher definir um, também deve definir o outro adequadamente, para que fique claramente claro qual é a sua definição de "igualdade". A outra solução para o compilador é permitir apenas que você substitua '==' OR '! =' E deixe o outro como inerentemente negando o outro. Obviamente, esse não é o caso do compilador C # e tenho certeza de que existe '

A pergunta que você deve fazer é "por que preciso substituir os operadores?" Essa é uma decisão forte a ser tomada, que requer um forte raciocínio. Para objetos, '==' e '! =' Compare por referência. Se você deseja substituí-los para NÃO comparar por referência, você está criando uma inconsistência geral do operador que não é aparente para qualquer outro desenvolvedor que examine esse código. Se você está tentando fazer a pergunta "o estado dessas duas instâncias é equivalente?", Você deve implementar IEquatible, definir Equals () e utilizar essa chamada de método.

Por fim, IEquatable () não define NotEquals () pelo mesmo raciocínio: potencial para abrir inconsistências no operador de igualdade. NotEquals () deve SEMPRE retornar! Equals (). Ao abrir a definição de NotEquals () para a classe que implementa Equals (), você está forçando novamente a questão da consistência na determinação da igualdade.

Edit: Este é simplesmente o meu raciocínio.

Emoire
fonte
Isso é ilógico. Se o motivo fosse a consistência, o contrário seria aplicado: você só seria capaz de implementar operator ==e o compilador inferiria operator !=. Afinal, é para isso que serve operator +e operator +=.
Konrad Rudolph
11
foo + = barra; é uma atribuição composta, abreviação de foo = foo + bar ;. Não é um operador.
Blenderfreaky 24/10/19
Se, de fato, eles são 'sempre verdadeiros opostos', esse seria um motivo para definir automaticamente a != bcomo !(a == b)... (o que não é o que o C # faz).
andrewf
-3

Provavelmente, apenas algo em que não pensaram não teve tempo de fazer.

Eu sempre uso seu método quando sobrecarrego ==. Então eu apenas uso no outro.

Você está certo, com uma pequena quantidade de trabalho, o compilador pode nos fornecer isso de graça.

Rhyous
fonte