quarta-feira, setembro 04, 2013

implementar os métodos GetHashCode(), Equals(Object o), ToString() e CompareTo(Object o) em c#

Um desabafo sobre implementar os métodos GetHashCode(), Equals(Object other), String ToString() e CompareTo(Object other) em c#.

Não sei por que causa tanto espanto aos programadores do universo .Net (c# no meu caso) a sobrecarga de métodos de System.Object, como GetHashCode(), Equals(Object other), String ToString() e CompareTo(Object other) e o básico conhecimento de serialização de objetos.
Quando eu falo sobre esses métodos ou exibo uma classe que os sobrescrevam sempre há uma pergunta de para que eles servem ou se estão no lugar certo(?).
Anos atrás, quando eu era um padawan hehehe, fiz uma classe em Java que deveria retornar uma lista com os elementos ordenados dentro dela. E eu não entendia porque a lista não exibia ordenadamente os dados. Então um colega de trabalho me falou "se você não implementar a tua regra de comparação a lista não irá saber como ordenar os dados". Lá fui eu implementar a minha regra de comparação (método compareTo). E voilà!
Em c# se estivermos tentando chamar Short() em uma coleção em que a classe de conteúdo dessa coleção não realiza a interface IComparable é lançada uma exceção em tempo de execução do tipo System.InvalidOperationException. Em Java não acontece isso, simplesmente é chamado o método compareTo da classe Object e a ordenação não vai acontecer como você previa.
Então para ordenar o conteúdo de uma coleção em c# temos que realizar a interface IComparable na classe de conteúdo da nossa coleção e sobrescrever o método CompareTo. Imaginemos uma coleção de objetos do tipo país:

List<Country> countries = new List<Country>();
 
countries.Add(new Country("Switzerland"));
countries.Add(new Country("Italy"));
countries.Add(new Country("France"));
countries.Add(new Country("Brazil"));

Para podermos chamar o método Sort() de List a classe Country deve implementar o método CompareTo. Exemplo:

/// <summary>
/// Compares two objects for ordering.
/// </summary>
/// <param name="other">The object to be compared.</param>
/// <returns>Compares most representatives object's fields
/// lexicographically, this method has to return one of the
/// following alternatives:
///    0 if this instance is equal to the object argument;
///   -1 if this instance is lexicographically less than the object
///      argument;
///    1 if this instance is lexicographically greater than the object
///      argument.
/// </returns>
public int CompareTo(Object other)
{
    if (other == null)
    {
        return 1;
    }
   
    try
    {
        Country cOther = (Country)other;
        return this.Name.CompareTo(cOther.Name);
    }
    catch (Exception e)
    {
        throw new ArgumentException("Object is not a Country", e);
    }
}

Agora sim se fizermos isso:

List<Country> countries = new List<Country>();
 
countries.Add(new Country("Switzerland"));
countries.Add(new Country("Italy"));
countries.Add(new Country("France"));
countries.Add(new Country("Brazil"));
 
countries.Sort()
 
foreach (Country c in countries) 
    Console.WriteLine(c.Name);

Iremos ter o resultado:

Brazil
France
Italy
Switzerland

Só vou comentar um detalhe do método ToString(). Já conversei com colegas que achavam que o mesmo não serve para nada, pois não é este o método que irá exibir em tela os dados do objeto, etc. Claro que não. Mas este método nos poderá dar uma resposta rápida de quem a instância do objeto se trata.
Eu, por exemplo, implemento o ToString com a seguinte regra:

namespace.Classe [<campos mais importantes da classe>]

Para a minha classe Country:

/// <summary>
/// Returns a string representation of this instance.
/// The format of the string is:
/// namespace + class name + [ + the most representative class fields + ].
/// </summary>
/// <returns>Returns a string representation of this instance.</returns>
public override String ToString()
{
    StringBuilder sb = new StringBuilder();
    
    sb.Append("com.blogspot.jeanjmichel.examples.model.Country [");
    sb.Append(this.Name);
    sb.Append("]");
    
    return sb.ToString();
}

Os campos mais importantes são os mesmos que eu utilizo para Equals, HashCode, etc.
Uma das aplicações mais úteis desta implementação está na hora de debugar. Olhe a coluna Value:

Debug de objetos que implementam ToString()

Ou até mesmo na hora de logar alguma exceção eu poderia logar o estado do objeto fazendo:

log.Error("Object ToString(): " + c.ToString());

O método Equals(Object other) é a regra de comparação não para ordenação, mas para qualquer finalidade. Eu costumo não incluir como campos importantes Id e campos que podem ser nulos. Então a comparação entre duas instâncias do objeto Country só levará em conta o campo Name:

/// <summary>
/// Compares this instance to the specified object.
/// The result is true if and only if the argument is not null, is of
/// the same type of this instance and the rest of the most
/// representative fields of the object has the same value.
/// </summary>
/// <param name="other">The object to compare this country against.</param>
/// <returns>true if the given object represents an equivalent object to
/// this instance, false otherwise</returns>
public bool Equals(Country other)
{
    if (other == null)
    {
        return false;
    }
 
    if (!this.Name.Equals(other.Name))
    {
        return false;
    }
    
    return true;
}

Para o método HashCode() é a mesma coisa:

/// <summary>
/// This method generates a hash code of this instance. To calculate this
/// code is used object's most representative fields.
/// </summary>
/// <returns>Country's hashcode.</returns>
public override int GetHashCode()
{
    int hashCode;
    int prime = 31;
    
    hashCode = String.IsNullOrEmpty(this.Name) ? prime : this.Name.GetHashCode() * prime;
    
    return hashCode;
}

Na trajetória básica de um padawan para virar Jedi em Java, o Java Tutorial passa pela lição falando da classe Object e seus métodos, passando pelos que citei aqui. Me estranha muito a galera que trabalha com c# (ou .Net como um todo) torcer o nariz para esses métodos. Enfim... na minha migração de plataforma procurei por esses métodos em c# assim que comecei a modelar minhas primeiras classes ;)

Nenhum comentário: