O que é a “Melhor prática” para comparar duas instâncias de um tipo de referência?

Eu me deparei com isso recentemente, até agora tenho sido feliz substituindo o operador de igualdade ( == ) e / ou o método Equals para ver se dois tipos de referências realmente continham os mesmos dados (ou seja, duas instâncias diferentes que parecem iguais).

Eu tenho usado isso ainda mais desde que eu fui ficando mais em testes automatizados (comparando referência / dados esperados contra os retornados).

Ao olhar para algumas das diretrizes padrões de codificação no MSDN me deparei com um artigo que aconselha contra ele. Agora eu entendo porque o artigo está dizendo isso (porque eles não são a mesma instância ), mas não responde a pergunta:

  1. Qual é a melhor maneira de comparar dois tipos de referência?
  2. Devemos implementar o IComparable ? (Eu também vi mencionar que isso deve ser reservado apenas para tipos de valor).
  3. Existe alguma interface que eu não conheço?
  4. Devemos apenas rolar nossos próprios ?!

Muito obrigado ^ _ ^

Atualizar

Parece que eu tinha lido errado alguns dos documentos (tem sido um longo dia) e substituindo Equals pode ser o caminho a percorrer ..

Se você estiver implementando tipos de referência, deverá considerar a substituição do método Equals em um tipo de referência se o tipo for semelhante a um tipo base, como Point, String, BigNumber e assim por diante. A maioria dos tipos de referência não deve sobrecarregar o operador de igualdade , mesmo se eles substituírem Equals . No entanto, se você estiver implementando um tipo de referência destinado a ter semântica de valor, como um tipo de número complexo, substitua o operador de igualdade.

Parece que você está codificando em C #, que tem um método chamado Equals que sua class deve implementar, se você quiser comparar dois objects usando alguma outra métrica do que “são esses dois pointers (porque identificadores de object são apenas isso, pointers) para o mesmo endereço de memory? ”

Eu peguei um código de exemplo aqui :

 class TwoDPoint : System.Object { public readonly int x, y; public TwoDPoint(int x, int y) //constructor { this.x = x; this.y = y; } public override bool Equals(System.Object obj) { // If parameter is null return false. if (obj == null) { return false; } // If parameter cannot be cast to Point return false. TwoDPoint p = obj as TwoDPoint; if ((System.Object)p == null) { return false; } // Return true if the fields match: return (x == px) && (y == py); } public bool Equals(TwoDPoint p) { // If parameter is null return false: if ((object)p == null) { return false; } // Return true if the fields match: return (x == px) && (y == py); } public override int GetHashCode() { return x ^ y; } } 

Java tem mecanismos muito semelhantes. O método equals () é parte da class Object e sua class o sobrecarrega se você quiser esse tipo de funcionalidade.

A razão pela qual sobrecarregar ‘==’ pode ser uma má idéia para objects é que, geralmente, você ainda quer ser capaz de fazer as comparações do “são o mesmo ponteiro”. Geralmente, isso é usado para, por exemplo, inserir um elemento em uma lista em que nenhuma duplicação é permitida, e alguns dos itens da estrutura podem não funcionar se esse operador estiver sobrecarregado de uma maneira não padrão.

Implementar igualdade no .NET de maneira correta, eficiente e sem duplicação de código é difícil. Especificamente, para tipos de referência com semântica de valor (isto é, tipos imutáveis ​​que tratam equvialence como igualdade ), você deve implementar a interface System.IEquatable e implementar todas as operações diferentes ( Equals , GetHashCode e == != ) .

Por exemplo, aqui está uma class implementando igualdade de valor:

 class Point : IEquatable { public int X { get; } public int Y { get; } public Point(int x = 0, int y = 0) { X = x; Y = y; } public bool Equals(Point other) { if (other is null) return false; return X.Equals(other.X) && Y.Equals(other.Y); } public override bool Equals(object obj) => Equals(obj as Point); public static bool operator ==(Point lhs, Point rhs) => object.Equals(lhs, rhs); public static bool operator !=(Point lhs, Point rhs) => ! (lhs == rhs); public override int GetHashCode() => X.GetHashCode() ^ Y.GetHashCode(); } 

As únicas partes móveis no código acima são as partes em negrito: a segunda linha em Equals(Point other) e o método GetHashCode() . O outro código deve permanecer inalterado.

Para classs de referência que não representam valores imutáveis, não implemente os operadores == e != . Em vez disso, use seu significado padrão, que é comparar a identidade do object.

O código intencionalmente equivale a até mesmo objects de um tipo de class derivada. Muitas vezes, isso pode não ser desejável porque a igualdade entre a class base e as classs derivadas não está bem definida. Infelizmente, o .NET e as diretrizes de codificação não são muito claras aqui. O código que Resharper cria, postado em outra resposta , é suscetível a comportamento indesejado em tais casos porque Equals(object x) e Equals(SecurableResourcePermission x) tratarão esse caso de forma diferente.

Para alterar esse comportamento, uma verificação de tipo adicional deve ser inserida no método Equals fortemente tipado acima:

 public bool Equals(Point other) { if (other is null) return false; if (other.GetType() != GetType()) return false; return X.Equals(other.X) && Y.Equals(other.Y); } 

Abaixo, resumi o que você precisa fazer ao implementar o IEquatable e forneceu a justificativa das várias páginas de documentação do MSDN.


Resumo

  • Quando o teste para igualdade de valor é desejado (como ao usar objects em collections), você deve implementar a interface IEquatable, replace Object.Equals e GetHashCode para sua class.
  • Ao testar a igualdade de referência desejada, você deve usar operator ==, operator! = E Object.ReferenceEquals .
  • Você deve replace apenas o operador == e o operador! = Para ValueTypes e os tipos de referência imutáveis.

Justificação

IEquatable

A interface System.IEquatable é usada para comparar duas instâncias de um object para igualdade. Os objects são comparados com base na lógica implementada na class. A comparação resulta em um valor booleano indicando se os objects são diferentes. Isso está em contraste com a interface System.IComparable, que retorna um inteiro indicando como os valores do object são diferentes.

A interface IEquatable declara dois methods que devem ser substituídos. O método Equals contém a implementação para realizar a comparação real e retornar true se os valores do object forem iguais ou false se não forem. O GetHashCode método deve retornar um valor de hash exclusivo que pode ser usado para identificar exclusivamente objects idênticos que contêm valores diferentes. O tipo de algoritmo de hash usado é específico da implementação.

Método IEquatable.Equals

  • Você deve implementar IEquatable para seus objects para lidar com a possibilidade de que eles serão armazenados em uma matriz ou coleção genérica.
  • Se você implementar IEquatable, também deverá replace as implementações da class base Object.Equals (Object) e GetHashCode, para que seu comportamento seja consistente com o método IEquatable.Equals.

Diretrizes para replace equals () e operador == (guia de programação C #)

  • x.Equals (x) retorna verdadeiro.
  • x.Equals (y) retorna o mesmo valor que y.Equals (x)
  • if (x.Equals (y) && y.Equals (z)) retorna true e, em seguida, x.Equals (z) retorna true.
  • Invocações sucessivas de x. Equals (y) retorna o mesmo valor, desde que os objects referenciados por x e y não sejam modificados.
  • x. Equals (null) retorna false (somente para tipos de valor não anuláveis. Para obter mais informações, consulte Tipos anuláveis ​​(guia de programação C #) .)
  • A nova implementação do Equals não deve lançar exceções.
  • Recomenda-se que qualquer class que substitua Equals também substitua Object.GetHashCode.
  • É recomendado que, além de implementar Equals (object), qualquer class também implemente Equals (type) para seu próprio tipo, para melhorar o desempenho.

Por padrão, o operador == testa a igualdade de referência determinando se duas referências indicam o mesmo object. Portanto, os tipos de referência não precisam implementar o operador == para obter essa funcionalidade. Quando um tipo é imutável, ou seja, os dados que estão contidos na instância não podem ser alterados, o operador de sobrecarga == para comparar a igualdade de valor em vez da igualdade de referência pode ser útil porque, como objects imutáveis, eles podem ser considerados iguais como eles têm o mesmo valor. Não é uma boa ideia replace o operador == em tipos não imutáveis.

  • O operador sobrecarregado == implementações não deve lançar exceções.
  • Qualquer tipo que sobrecarregue o operador também deve sobrecarregar o operador.

== Operador (referência C #)

  • Para tipos de valores predefinidos, o operador de igualdade (==) retorna true se os valores de seus operandos forem iguais, caso contrário, false.
  • Para tipos de referência diferentes de string, == retorna true se seus dois operandos se referirem ao mesmo object.
  • Para o tipo de string, == compara os valores das strings.
  • Ao testar null usando == comparações dentro de suas substituições operator ==, certifique-se de usar o operador de class de object base. Se você não fizer isso, ocorrerá uma recursion infinita, resultando em um stackoverflow.

Método Object.Equals (Object)

Se sua linguagem de programação oferecer suporte à sobrecarga do operador e se você optar por sobrecarregar o operador de igualdade para um determinado tipo, esse tipo deverá replace o método Equals. Essas implementações do método Equals devem retornar os mesmos resultados que o operador de igualdade

As diretrizes a seguir são para implementar um tipo de valor :

  • Considere replace Equals para obter um desempenho melhor do que aquele fornecido pela implementação padrão de Equals em ValueType.
  • Se você replace Equals e o idioma oferecer suporte à sobrecarga do operador, você deverá sobrecarregar o operador de igualdade para seu tipo de valor.

As diretrizes a seguir são para implementar um tipo de referência :

  • Considere replace Equals em um tipo de referência se a semântica do tipo for baseada no fato de que o tipo representa algum valor (es).
  • A maioria dos tipos de referência não deve sobrecarregar o operador de igualdade, mesmo se eles substituírem Equals. No entanto, se você estiver implementando um tipo de referência destinado a ter semântica de valor, como um tipo de número complexo, será necessário replace o operador de igualdade.

Adicionais Gotchas

  • Ao replace GetHashCode (), certifique-se de testar os tipos de referência para NULL antes de usá-los no código de hash.
  • Eu tive um problema com programação baseada em interface e sobrecarga de operador descrita aqui: Sobrecarga de Operador com Programação Baseada em Interface em C #

Esse artigo apenas recomenda contra a substituição do operador de igualdade (para tipos de referência), não contra a substituição de Equals. Você deve replace Equals no seu object (referência ou valor) se as verificações de igualdade significarem algo mais do que verificações de referência. Se você quiser uma interface, você também pode implementar IEquatable (usado por collections genéricas). Se você implementar IEquatable, no entanto, você deve também replace equals, como a seção IEquatable remarks declara:

Se você implementar IEquatable , você também deve replace as implementações de class base de Object.Equals (Object) e GetHashCode para que seu comportamento seja consistente com o método IEquatable .Equals. Se você replace Object.Equals (Object), sua implementação substituída também será chamada em chamadas para o método Equals (System.Object, System.Object) estático em sua class. Isso garante que todas as invocações do método Equals retornem resultados consistentes.

Em relação a se você deve implementar Equals e / ou o operador de igualdade:

Implementando o método Equals

A maioria dos tipos de referência não deve sobrecarregar o operador de igualdade, mesmo se eles substituírem Equals.

Das Diretrizes para Implementar Iguais e o Operador de Igualdade (==)

Substitua o método Equals sempre que você implementar o operador de igualdade (==) e fazê-los fazer a mesma coisa.

Isso só diz que você precisa replace Equals sempre que você implementar o operador de igualdade. Ele não diz que você precisa replace o operador de igualdade quando você substitui Equals.

Para objects complexos que produzirão comparações específicas, implementar o IComparable e definir a comparação nos methods de comparação é uma boa implementação.

Por exemplo, temos objects “Veículo” onde a única diferença pode ser o número de registro e usamos isso para comparar para garantir que o valor esperado retornado no teste seja o desejado.

Eu costumo usar o que o Resharper faz automaticamente. por exemplo, ele criou isso automaticamente para um dos meus tipos de referência:

 public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; return obj.GetType() == typeof(SecurableResourcePermission) && Equals((SecurableResourcePermission)obj); } public bool Equals(SecurableResourcePermission obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; return obj.ResourceUid == ResourceUid && Equals(obj.ActionCode, ActionCode) && Equals(obj.AllowDeny, AllowDeny); } public override int GetHashCode() { unchecked { int result = (int)ResourceUid; result = (result * 397) ^ (ActionCode != null ? ActionCode.GetHashCode() : 0); result = (result * 397) ^ AllowDeny.GetHashCode(); return result; } } 

Se você quiser sobrescrever == e ainda fazer verificações de ref, você ainda pode usar Object.ReferenceEquals .

A Microsoft parece ter mudado de tom, ou pelo menos há informações conflitantes sobre não sobrecarregar o operador de igualdade. De acordo com este artigo da Microsoft intitulado Como definir a igualdade de valor para um tipo:

“Os operadores == e! = Podem ser usados ​​com classs, mesmo que a class não os sobrecarregue. No entanto, o comportamento padrão é executar uma verificação de igualdade de referência. Em uma class, se você sobrecarregar o método Equals, sobrecarregue == e! = operadores, mas não é necessário. ”

De acordo com Eric Lippert em sua resposta a uma pergunta que fiz sobre o código mínimo para igualdade em c # – ele diz:

“O perigo que você encontra aqui é que você obtém um operador == definido para você que faz referência a igualdade por padrão. Você pode facilmente acabar em uma situação onde um método Equals sobrecarregado valoriza igualdade e == faz referência a igualdade, e então você acidentalmente usa a igualdade de referência em coisas não-referência-iguais que são valor-iguais.Esta é uma prática propensa a erros que é difícil de detectar pela análise de código humano.

Alguns anos atrás, trabalhei em um algoritmo de análise estática para detectar estatisticamente essa situação, e encontramos uma taxa de defeitos de cerca de duas instâncias por milhão de linhas de código em todas as bases de código que estudamos. Ao considerar apenas bases de código que tinham substituído em algum lugar Equals, a taxa de defeito era obviamente consideravelmente maior!

Além disso, considere os custos versus os riscos. Se você já tem implementações do IComparable, então escrever todos os operadores é um one-liner trivial que não terá bugs e nunca será alterado. É o código mais barato que você vai escrever. Se fosse dada a escolha entre o custo fixo de escrever e testar uma dúzia de minúsculos methods versus o custo ilimitado de encontrar e corrigir um bug difícil de ver onde a igualdade de referência fosse usada em vez da igualdade de valor, eu sei qual escolheria. ”

O .NET Framework nunca usará == ou! = Com qualquer tipo que você escrever. Mas o perigo é o que aconteceria se outra pessoa o fizesse. Então, se a class for para um terceiro, eu sempre darei os operadores == e! =. Se a class destina-se apenas a ser usada internamente pelo grupo, eu provavelmente ainda implementaria os operadores == e! =.

Eu só implementaria os operadores < , <=,> e> = se IComparable fosse implementado. O IComparable só deve ser implementado se o tipo precisar dar suporte a pedidos – como quando estiver classificando ou sendo usado em um contêiner genérico ordenado, como SortedSet.

Se o grupo ou empresa tivesse uma política em vigor para nunca implementar os operadores == e! = – então, é claro, eu seguiria essa política. Se tal política estivesse em vigor, seria sábio aplicá-la com uma ferramenta de análise de código Q / A que sinaliza qualquer ocorrência dos operadores == e! = Quando usada com um tipo de referência.

Acredito que obter algo tão simples quanto verificar objects quanto à igualdade correta é um pouco complicado com o design do .NET.

Para Struct

1) Implemente IEquatable . Melhora o desempenho visivelmente.

2) Como você está tendo seus próprios Equals agora, substitua GetHashCode e, para ser consistente com várias verificações de igualdade, substitua object.Equals também.

3) Sobrecarregar operadores == e != Não precisam ser religiosamente feitos, pois o compilador avisará se você involuntariamente equacionar uma estrutura com outra com um == ou != , Mas é bom fazer isso para ser consistente com os methods Equals .

 public struct Entity : IEquatable { public bool Equals(Entity other) { throw new NotImplementedException("Your equality check here..."); } public override bool Equals(object obj) { if (obj == null || !(obj is Entity)) return false; return Equals((Entity)obj); } public static bool operator ==(Entity e1, Entity e2) { return e1.Equals(e2); } public static bool operator !=(Entity e1, Entity e2) { return !(e1 == e2); } public override int GetHashCode() { throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here..."); } } 

Para aula

Do MS:

A maioria dos tipos de referência não deve sobrecarregar o operador de igualdade, mesmo se eles substituírem Equals.

Para mim == parece igualdade de valor, mais como um açúcar sintático para o método Equals . Escrever a == b é muito mais intuitivo do que escrever a.Equals(b) . Raramente precisaremos verificar a igualdade de referência. Em níveis abstratos lidando com representações lógicas de objects físicos, isso não é algo que precisaríamos verificar. Eu acho que ter semânticas diferentes para == e Equals podem ser confusas. Eu acredito que deveria ter sido == para igualdade de valor e Equals para referência (ou um nome melhor como IsSameAs ) igualdade em primeiro lugar. Eu adoraria não levar a sério a diretriz MS aqui, não apenas porque não é natural para mim, mas também porque sobrecarregar == não causa nenhum grande dano. Isso é diferente de não replace Equals não-genéricos ou GetHashCode que pode atrapalhar, porque framework não usa == lugar algum, mas somente se nós mesmos o usarmos. O único benefício real que ganho de não sobrecarregar == e != Será a consistência com o design de todo o framework sobre o qual não tenho controle. E isso é realmente uma grande coisa, então , infelizmente, vou me ater a isso .

Com semântica de referência (objects mutáveis)

1) Substitua Equals e GetHashCode .

2) Implementar IEquatable não é obrigatório, mas será bom se você tiver um.

 public class Entity : IEquatable { public bool Equals(Entity other) { if (ReferenceEquals(this, other)) return true; if (ReferenceEquals(null, other)) return false; //if your below implementation will involve objects of derived classs, then do a //GetType == other.GetType comparison throw new NotImplementedException("Your equality check here..."); } public override bool Equals(object obj) { return Equals(obj as Entity); } public override int GetHashCode() { throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here..."); } } 

Com semântica de valor (objects imutáveis)

Essa é a parte complicada. Pode ser facilmente confuso se não for tomado cuidado ..

1) Substitua Equals e GetHashCode .

2) Sobrecarga == e != Para igualar Equals . Certifique-se de que funciona para nulos .

2) Implementar IEquatable não é obrigatório, mas será bom se você tiver um.

 public class Entity : IEquatable { public bool Equals(Entity other) { if (ReferenceEquals(this, other)) return true; if (ReferenceEquals(null, other)) return false; //if your below implementation will involve objects of derived classs, then do a //GetType == other.GetType comparison throw new NotImplementedException("Your equality check here..."); } public override bool Equals(object obj) { return Equals(obj as Entity); } public static bool operator ==(Entity e1, Entity e2) { if (ReferenceEquals(e1, null)) return ReferenceEquals(e2, null); return e1.Equals(e2); } public static bool operator !=(Entity e1, Entity e2) { return !(e1 == e2); } public override int GetHashCode() { throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here..."); } } 

Tome um cuidado especial para ver como deve ser feito se sua class puder ser herdada. Nesses casos, será necessário determinar se um object de class base pode ser igual a um object de class derivado. Idealmente, se nenhum object da class derivada for usado para verificação de igualdade, uma instância de class base pode ser igual a uma instância de class derivada e, nesses casos, não há necessidade de verificar a igualdade de Type em Equals da class base.

Em geral, tome cuidado para não duplicar o código. Eu poderia ter feito uma class base abstrata genérica ( IEqualizable ou mais) como um modelo para permitir a reutilização mais fácil, mas infelizmente em C # que me impede de derivar de classs adicionais.

É notável como isso é difícil de acertar …

A recomendação da Microsoft para ter Equals e == fazer coisas diferentes neste caso não faz sentido para mim. Em algum momento, alguém irá (corretamente) esperar que Equals e == produzam o mesmo resultado e o código irá bombardear.

Eu estava procurando por uma solução que:

  • produzir o mesmo resultado se Equals ou == for usado em todos os casos
  • ser totalmente polimórfico (chamando igualdade derivada através de referências de base) em todos os casos

Eu inventei isso:

 class MyClass : IEquatable { public int X { get; } public int Y { get; } public MyClass(int x = 0, int y = 0) { X = x; Y = y; } public override bool Equals(object obj) { var o = obj as MyClass; return o is null ? false : X.Equals(oX) && Y.Equals(oY); } public bool Equals(MyClass o) => object.Equals(this, o); public static bool operator ==(MyClass o1, MyClass o2) => object.Equals(o1, o2); public static bool operator !=(MyClass o1, MyClass o2) => !object.Equals(o1, o2); public override int GetHashCode() => HashCode.Combine(X, Y); } 

Aqui tudo acaba em Equals(object) que é sempre polimórfico para que ambos os alvos sejam alcançados.

Derive assim:

 class MyDerived : MyClass, IEquatable { public int Z { get; } public int K { get; } public MyDerived(int x = 0, int y = 0, int z=0, int k=0) : base(x, y) { Z = z; K = k; } public override bool Equals(object obj) { var o = obj as MyDerived; return o is null ? false : base.Equals(obj) && Z.Equals(oZ) && K.Equals(oK); } public bool Equals(MyDerived other) => object.Equals(this, o); public static bool operator ==(MyDerived o1, MyDerived o2) => object.Equals(o1, o2); public static bool operator !=(MyDerived o1, MyDerived o2) => !object.Equals(o1, o2); public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), Z, K); } 

Que é basicamente o mesmo, exceto por um gotcha – quando Equals(object) quer chamar base.Equals ter cuidado para chamar base.Equals(object) e não base.Equals(MyClass) (que causará uma recursion sem fim).

Uma ressalva aqui é que Equals(MyClass) irá implementar um boxe, no entanto o boxe / unboxing é altamente otimizado e isso vale para mim alcançar as metas acima.

demonstração: https://dotnetfiddle.net/cCx8WZ

(Observe que é para C #> 7.0)
(baseado na resposta de Konard)