Desenvolvimento - C#

Comparação de Objetos em .NET com C#

Veja neste artigo como implementar meios que possibilitem a comparação entre objetos de um mesmo tipo em C#, de forma a permitir que outros recursos do .NET Framework consigam determinar se duas instâncias são iguais ou não.

por Renato Groffe



Comparar duas ou mais informações representa, basicamente, um tipo de atividade bastante comum dentro do desenvolvimento de software. Isto não é diferente no contexto da implementação de sistemas concebidos segundo conceitos de Orientação a Objetos (OO) e, em especial, para aplicações elaboradas sob o .NET Framework. Considerando a construção de softwares a partir da plataforma .NET, não é raro se deparar com a existência de instruções que executam operações sobre agrupamentos/coleções de objetos, muitas vezes procurando inclusive verificar a igualdade entre duas ou mais instâncias de um tipo ou classe.

Esta necessidade frequente de manipular agrupamentos de objetos foi também uma das razões que motivaram o surgimento do mecanismo conhecido como Language-Integrated Query ou, simplesmente, LINQ (http://msdn.microsoft.com/en-us/library/bb397926.aspx). A extensão LINQ to Objects (http://msdn.microsoft.com/en-us/library/bb397919.aspx) é uma das partes que compõem esta tecnologia, permitindo que consultas sejam aplicadas sobre coleções de objetos. O grande benefício de se empregar LINQ to Objects está, sem sombra de dúvidas, na possibilidade de utilizar uma sintaxe semelhante àquela da linguagem SQL em bancos de dados relacionais.

Importante destacar que as funcionalidades providas pela extensão LINQ to Objects fazem uso em larga escala do recurso conhecido como Generics. Toda coleção manipulada por este componente da tecnologia LINQ implementa, normalmente, a interface IEnumerable (namespace System.Collections.Generics), em que T corresponde a um tipo qualquer. List é um exemplo de estrutura que deriva desta interface, fornecendo uma representação genérica para a utilização de uma lista sequencial (em que caso não indicado de maneira explícita, cada item é adicionado ao final do último elemento da coleção em questão).

Além de todos estes pontos apresentados, a interface IEnumerable disponibiliza várias operações que, em conjunto com recursos de LINQ, possibilitam a produção de novas informações a partir de coleções de objetos pré-existentes. Exemplos disto são os métodos Where (para filtrar um conjunto de instâncias, gerando assim um novo agrupamento de objetos), OrderBy (ordenação dos itens constantes em uma coleção, com a criação de um novo agrupamento cuja ordem segue algum critério especificado como parâmetro), Max (obtenção do maior valor existente numa lista de objetos), Min (similar à operação anterior, retornando o menor valor) e Average (obtém o valor médio para um conjunto de dados).

Analisando ainda as coleções de objetos sob um ponto de vista conceitual, estruturas deste tipo podem ser equiparadas em certa medida aos conjuntos numéricos da Matemática. Operações como união, intersecção e diferença também possuem equivalentes na definição da interface IEnumerable:

  • Union: gera uma nova coleção de objetos a partir de dois agrupamentos de objetos, merecendo destaque o fato de que itens em duplicidade serão eliminados do resultado final;
  • Intersect: realiza uma intersecção entre duas coleções de objetos, ou seja, o resultado será um novo agrupamento formado apenas pelos itens que são comuns às estruturas que foram comparadas;
  • Except: efetua uma operação de diferença, produzindo como retorno uma nova coleção com itens constantes em um agrupamento, mas ausentes na segunda coleção tomada como base.

Dentro de uma ótica baseada em OO, a execução destas instruções de união, intersecção ou diferença de conjuntos envolverá, obrigatoriamente, comparações para determinar se objetos pertencentes a duas coleções representam ou não uma mesma informação. As implementação de List, por exemplo, contam com este característica, atuando de modo transparente e dispensando os desenvolvedores da necessidade de construir loops para desencadear uma série de verificações. Contudo, é importante ressaltar que tal comportamento dependerá que a classe através da qual se criam objetos adote algum critério para a realização das comparações, com o objetivo deste artigo sendo justamente demonstrar uma das formas possíveis para se fazer isto.

Este processo pode ser exemplificado por uma aplicação que conte com um módulo de controle de acesso. Uma das prováveis funcionalidades deste sistema poderia ser uma consulta em que constam:

  • Os diversos perfis de acesso de um usuário a recursos do sistema que se está tomando por base;
  • Quais perfis existentes não foram associados a um usuário que se está analisando.

Na Listagem 1 é apresentado o código-fonte da classe PerfilAcesso. Este tipo serve de base para a geração de instâncias que correspondem aos diferentes perfis atribuídos ao login de um usuário, além daqueles ainda disponíveis e que não foram vinculados a tal conta de acesso. PerfilAcesso é uma classe formada pelas propriedades CodigoPerfil (código identificador de um perfil de acesso) e DescricaoPerfil (descrição que identifica um perfil de acesso).

Listagem 1: Classe PerfilAcesso


...

namespace TesteComparacaoObjetos
{
    public class PerfilAcesso
    {
        public string CodigoPerfil { get; set; }
        public string DescricaoPerfil { get; set; }
    }
}

Já na Listagem 2 está a estrutura da classe ControleAcesso, na qual se encontram definidos dois dos métodos utilizados no exemplo aqui abordado:

  • ObterPerfisUsuario: lista os perfis de acesso que foram associados à conta de um usuário;
  • ObterPerfisExistentes: retorna uma coleção com todos os perfis de acesso cadastrados para a aplicação.

Para efeitos de simplificação, está sendo omitida a implementação das operações ObterPerfisUsuario e ObterPerfisExistentes. O acesso à base de dados correspondente e, posteriormente, a conversão das informações obtidas para a classe PerfilAcesso pode ser feito, basicamente, empregando diversas tecnologias de acesso a bancos relacionais (o ADO.NET, o Entity Framework e o NHibernate são alguns exemplos disto).

Listagem 2: Classe ControleAcesso


...

namespace TesteComparacaoObjetos
{
    public static class ControleAcesso
    {
        ...

        public static IEnumerable<PerfilAcesso> ObterPerfisExistentes()
        {
            // Devolve uma coleção com todos os perfis existentes
            ...

        }

        public static IEnumerable<PerfilAcesso> ObterPerfisUsuario(
            string loginUsuario)
        {
            // Lista apenas perfis de acesso que foram associados
            // ao login de um usuário
            ...

        }

        ...
    }   
}

Chega então o momento de codificar as instruções para a consulta aos direitos de acesso atribuídos para um usuário, assim como de perfis que não foram associados ao mesmo. A implementação de tal funcionalidade (Listagem 3) dependerá, basicamente, do uso dos métodos ObterPerfisExistentes e ObterPerfisUsuario, além da operação Except (declarada originalmente na interface IEnumerable).

Partindo do pressuposto que a consulta existirá dentro de uma aplicação do tipo Web Forms, duas GridViews se prestarão à exibição de informações sobre perfis de acesso: grdPerfisAssociados (perfis vinculados ao login de um usuário) e grdPerfisDisponiveis (perfis disponíveis e que ainda não foram associados à conta selecionada).

Inicialmente é invocado o método estático ObterPerfisUsuario da classe ControleAcesso. A coleção resultante é daí atribuída à variável perfisUsuario, com esta referência sendo associada à propriedade DataSource do controle visual grdPerfisAssociados (acionar a operação DataBind sobre este último componente concluirá este procedimento, exibindo com isto as informações relativas aos perfis associados a um usuário em tela).

A fim de gerar uma coleção contendo quais perfis não se encontram vinculados ao usuário selecionado para consulta, a operação estática ObterPerfisExistentes deverá então ser acionada. Na sequência, o método Except é invocado sobre a instância retornada, recebendo como parâmetro o valor que foi vinculado anteriormente à referência perfisUsuario. O resultado esperado para isto é uma nova coleção de instâncias de PerfilAcesso (que é atribuída à variável perfisDisponiveis e, posteriormente, à propriedade DataSource da GridView grdPerfisDisponiveis), constando nesta última apenas perfis que não foram liberados para um determinado usuário.

Listagem 3: Consulta aos direitos de acesso de um usuário e perfis ainda disponíveis


...

IEnumerable<PerfilAcesso> perfisUsuario = ControleAcesso
    .ObterPerfisUsuario(loginUsuario);
grdPerfisAssociados.DataSource = perfisUsuario;
grdPerfisAssociados.DataBind();

IEnumerable<PerfilAcesso> perfisDisponiveis = ControleAcesso
    .ObterPerfisExistentes()
    .Except(perfisUsuario);
grdPerfisDisponiveis.DataSource = perfisDisponiveis;
grdPerfisDisponiveis.DataBind();

...

Executando a funcionalidade de consulta de perfis associados ou não a um usuário, será exibida uma tela similar àquela que consta na Figura 1. Percebe-se, rapidamente, que as informações apresentadas pelo item “Perfis ainda disponíveis para associação” estão incorretas: perfis que já foram vinculados a uma conta não podem, basicamente, constar na nova coleção que é obtida por meio da utilização do método Except.

Informações sendo exibidas erroneamente na consulta de perfis de acesso de um usuário

Figura 1: Informações sendo exibidas erroneamente na consulta de perfis de acesso de um usuário

O que realmente aconteceu nesta situação é que a operação Except não funcionou da maneira planejada. Por mais que as operações ObterPerfisUsuario e ObterPerfisExistentes do tipo ControleAcesso tenham retornado os objetos esperados, o método Except foi incapaz de encontrar qualquer traço de igualdade entre instâncias presentes nestas duas estruturas.

Considerando a forma como a plataforma efetua a comparação entre objetos, duas instâncias são consideradas equivalentes quando:

  • O valor do método GetHashCode de ambas as referências retornar o mesmo valor;
  • Acionando o método Equals de uma instância e passando a outra referência como parâmetro, o resultado de tal operação deverá ser true.

As operações GetHashCode e Equals estão definidas na classe Object e, portanto, são automaticamente herdadas por qualquer outro tipo criado dentro de aplicações baseadas no .NET Framework. Esta é razão pela qual o trecho envolvendo o uso do método Except não produziu o resultado aguardado.

Logo, será preciso sobrescrever os métodos GetHashCode e Equals na implementação da classe PerfilAcesso, conforme especificado na Listagem 4.

A sobrecarga do método Equals levou em conta as seguintes premissas:

  • Caso a instância passada como parâmetro seja um valor nulo ou não represente uma referência do tipo PerfilAcesso, o valor a ser devolvido como resultado desta operação será false;
  • Se o parâmetro realmente for uma instância de PerfilAcesso, será comparada a string que consta na propriedade CodigoPerfil desta referência ao valor correspondente no objeto a partir do qual a operação Equals foi invocada.

Já a operação GetHashCode devolve como resultado o código equivalente para a string associada à propriedade CodigoPerfil (acionando o método GetHashCode da própria string, desde que tal referência não seja nula).

Conforme já foi mencionado, sobrescrever os métodos Equals e GetHashCode exigirá sempre a escolha por um ou mais critérios de comparação. No exemplo discutido até aqui, optou-se pelo uso da propriedade CodigoPerfil. Não somente Except, mas outras operações geralmente associadas a coleções (como Union e Intersect) também dependem dos métodos Equals e GetHashCode para desempenhar suas funções.

Listagem 4: Versão da classe PerfilAcesso que sobrescreve os métodos Equals e GetHashCode


...

namespace TesteComparacaoObjetos
{
    public class PerfilAcesso
    {
        public string CodigoPerfil { get; set; }
        public string DescricaoPerfil { get; set; }

        public override bool Equals(object obj)
        {
            if (obj == null || obj.GetType() != typeof(PerfilAcesso))
                return false;

            PerfilAcesso p = (PerfilAcesso)obj;
            return (this.CodigoPerfil == p.CodigoPerfil);
        }

        public override int GetHashCode()
        {
            if (this.CodigoPerfil != null)
                return this.CodigoPerfil.GetHashCode();
            return base.GetHashCode();
        }
    }
}

Tão logo os ajustes na classe PerfilAcesso tenham sido concluídos, é hora de se proceder com uma nova execução da funcionalidade de consulta. A Figura 2 apresenta a tela que exibe informações de perfis disponíveis e associados a um usuário após as correções.

Funcionalidade de consulta de perfis de acesso de um usuário após correção

Figura 2: Funcionalidade de consulta de perfis de acesso de um usuário após correção

Uma ressalva importante deve ser feita a respeito da sobrecarga dos métodos Equals e GetHashCode. Muito embora operações executadas sobre coleções de objetos empreguem os mesmos, o operador de igualdade “==” por outro lado não faz isto: a comparação de duas diferentes instâncias de objetos retornará, por padrão, o valor false em tais casos.

E chegamos assim ao fim deste artigo. Busquei demonstrar, ao longo do texto, como o .NET realiza comparações entre diferentes instâncias de uma classe quando da manipulação de coleções contendo as mesmas. O uso de agrupamentos de objetos é uma prática bastante comum no desenvolvimento de aplicações, sobretudo na conversão de informações vindas de um banco de dados relacional para estruturas equivalentes no mundo OO. Espero que os conceitos expostos possam ser úteis no seu dia-a-dia. Até uma próxima oportunidade!

Renato Groffe

Renato Groffe