O Perigo do Método Contains

Neste artigo mostro o que acontece ao usarmos o método Contains em determinadas situações que precisamos realizar um filtro em um Gridview. O projeto utilizado como exemplo será uma cópia do projeto do artigo Paginação no Gridview, obviamente com pequenas alterações para refletir este artigo. O link estará disponível ao final deste artigo.

Introdução

Quando desejamos realizar uma busca por um valor específico dentro de um conjunto de valores podemos utilizar o método Contains, da classe String. Ele é útil quando sabemos ao menos uma parte do que procurar e funciona de forma muito parecida com o operador Like, do T-SQL.

Apesar de ser um método muito útil iremos ver um exemplo do problema que ele pode causar ao ser utilizado em filtros de busca e do que fazer para contornar esse problema, de uma forma um pouco mais complexa, à primeira vista, mas que resolve o problema e não consome muita memória em relação ao Contains.

Projeto

Lembrando novamente que o projeto a ser usado é uma cópia do mesmo usado no link do início deste artigo. Fiz algumas alterações no projeto e adicionei um campo texto e um botão para fazer o filtro no Gridview, veja na imagem.

Design

Como serão feitos várias buscas no mesmo conjunto de dados e quero fazer testes de performance, pra não atrapalhar a veracidade dos testes irei fazer apenas uma busca no banco, na primeira vez que o projeto é carregado e nas vezes seguintes a lista genérica com os dados serão armazenados em uma variável do ViewState, assim economizando “idas” ao banco e fazendo os testes de performance terem valores mais “reais”, evitando o tempo gasto de ir ao banco e trazer sempre a mesma lista. Até porque são 77 registros da tabela Products, trazendo 3 colunas cada registro, nada muito custoso para se armazenar em um ViewState.

Abaixo segue a propriedade que irá receber a lista vindo do banco, do mesmo tipo da lista que carregará o Gridview.

private List<Produtos> vwListaProdutos
 {
 get
 {
 List<Produtos> listaProdutos = new List<Produtos>();
 if (ViewState["vwListaProdutos"] != null)
 listaProdutos = (List<Produtos>)ViewState["vwListaProdutos"];
 return listaProdutos;
 }
 set
 {
 ViewState["vwListaProdutos"] = value;
 }
 }

Importante: como a ViewState irá armazenar os valores de uma classe é obrigatório o uso do atributo Serializable, em cima da classe Products, como o código abaixo mostra:

[Serializable]
 private class Products
 {
 public int ProductID { get; set; }
 public string ProductName { get; set; }
 public int UnitsInStock { get; set; }
 }

Dessa forma a classe Products se torna serializável, mas informações sobre serialização na documentação da MSDN sobre o assunto.

O Problema

Sem mais delongas vamos ao problema, este é o método PopularGrid, como eu pensei em usá-lo após fazer as alterações no layout da página:

private void PopularGrid(string filtro, bool temFiltro = false)
 {
 grdProdutos.DataSource = temFiltro ? vwListaProdutos.FindAll(l => l.ProductName.Contains(filtro)) : vwListaProdutos;
 grdProdutos.DataBind();
 }

Note que o método tem dois parâmetros, o filtro, que é o valor do textbox, e um parâmetro opcional, do tipo booleano. Na atribuição do DataSource é feito um operador ternário, verificando se esse parâmetro booleano tem o valor true, se tiver é utilizado o método FindAll, verificando dentro dele se o nome do produto contém (por meio do método Contains, principal “personagem” desse artigo) o valor informado no filtro. Por outro lado se o valor do parâmetro for false significa que a busca não utiliza filtros, então é apenas atribuído o ViewState, com os 77 registros do GridView.

O evento click do botão criado:

protected void btnFiltrar_Click(object sender, EventArgs e)
 {
 this.PopularGrid(txtFiltro.Text, !string.IsNullOrEmpty(txtFiltro.Text));
 }

No segundo parâmetro é validado a existência de valores no campo filtro.

Aparentemente ele não parece ter problemas, já que ao rodar e pesquisar por exemplo por Chai (primeiro produto da lista) ele retorna o respectivo produto, veja (note o campo informado no textbox):

FiltoMaiuscula

Mas, por outro lado, se for informado o texto chai, em minúscula, olha o que acontece:

FiltroMinuscula

Esse é o problema! Basicamente o método Contains (e também a linguagem C# como um todo) é case-sensitive, ou seja, ele diferencia maiúsculas de minúsculas, o que neste caso atrapalha as buscas no banco de dados, nos dando a falsa impressão que o registro não existe, mas ele existe!

A Solução

Após muito tempo pesquisando (mentira, gastei nem 5 minutos) cheguei à esta dúvida, do já muitas vezes mencionado por aqui Stack Overflow. Nela, um usuário faz basicamente o mesmo questionamento, querendo saber se existe uma forma do método Contains ser case-insensitive, e uma das respostas, marcada como resposta pelo usuário que fez a pergunta (mas que não foi a mais votada) é que a forma para resolver isso é usando o seguinte código:

 culture.CompareInfo.IndexOf(paragraph, word, CompareOptions.IgnoreCase)

A variável culture é uma instância da classe CultureInfo, do namespace System.Globalization.

O problema está relacionado a definição do que é case-sensitive e o que é case-insensitive, e diz respeito a linguagem (não de programação) de cada país. Peço que leiam o artigo para uma explicação detalhada do problema, e se quiserem algo ainda mais detalhado tem um artigo da Wikipédia falando sobre Internacionalização e Localização.

Então, o código acima adaptado e ligeiramente melhorado ao artigo deixaria o método PopularGrid da seguinte forma (não se esqueça de adicionar o namespace abaixo):

using System.Globalization;
private void PopularGrid(string filtro, bool temFiltro = false)
 {
 var cultura = new CultureInfo("pt-BR");
 grdProdutos.DataSource = temFiltro ?
 vwListaProdutos.FindAll(l => cultura.CompareInfo.IndexOf(l.ProductName, filtro, CompareOptions.IgnoreCase) >= 0) : vwListaProdutos;
 grdProdutos.DataBind();
 }

Vamos entender a alteração: basicamente é utilizado o método IndexOf (que retorna um valor do tipo int, verificando se determinada expressão existe), da classe CompareInfo, que por sua vez pertence a classe CultureInfo. Esse método verifica se o valor de retorno é maior ou igual a 0 (se for -1 é porque não existe mesmo termo para a busca informada).

Dentro do IndexOf são passados 3 parâmetros: o source, que o campo a ser verificado, a fonte, no caso o ProductName, o value, que é o valor a ser enviado para comparação, no caso a variável filtro, que nada mais é que o valor do textbox, e finalmente um enumerador que é a opção de comparação, representado pelo CompareOptions. Note na imagem abaixo que temos diversos tipos de comparadores:

CompareOptions

Poderíamos usar o OrdinalIgnoreCase também, como foi apontado na resposta mais votada do Stack Overflow (e aceita pela maioria dos usuários).

Notem que passei como parâmetro da classe CultureInfo o “pt-BR”, poderia muito bem passar “en-US” (padrão americano) que funcionaria da mesma forma. Fazendo estas alterações confira o resultado:

FiltroMinusculaQueFunciona

Gostei muito dessa solução que resolvi fazer alguns testes…

Testes de Performance

Essa parte em diante já é um plus do artigo, você não precisa ler se não quiser, mas se quiser saber como fazer alguns testes simples (nada complexo) de performance com seus objetos continue lendo.

Basicamente irei usar um objeto da classe StopWatch, pertencente ao namespace System.Diagnostics. Essa classe basicamente nos permite utilizar um contador para verificar quantos milissegundos se passaram desde a chamado do método Start até a chamada do método Stop. Favor ver o link referente a classe para ler a documentação oficial da MSDN sobre o assunto.

Então veja o código usado no método PopularGrid, com testes de performance (o tempo testado só foi levado em conta com o filtro “Chai”, para o Contains também retornar o mesmo resultado e não atrapalhar nos testes):

using System.Diagnostics;
private void PopularGrid(string filtro, bool temFiltro = false)
 {
 var cultura = new CultureInfo("en-US");
 Stopwatch contador = new Stopwatch();
 long milissegundosContains = 0;
 long milissegundosIndexOf = 0;

 contador.Start();
 grdProdutos.DataSource = temFiltro ? vwListaProdutos.FindAll(l => l.ProductName.Contains(filtro)) : vwListaProdutos;
 contador.Stop();
 milissegundosContains = contador.ElapsedMilliseconds;

 contador.Reset();

 contador.Start();
 grdProdutos.DataSource = temFiltro ?
 vwListaProdutos.FindAll(l => cultura.CompareInfo.IndexOf(l.ProductName, filtro, CompareOptions.IgnoreCase) >= 0) : vwListaProdutos;
 contador.Stop();
 milissegundosIndexOf = contador.ElapsedMilliseconds;

 grdProdutos.DataBind();
 }

Primeiro testei com cada método separado, usando um e comentando o outro, olhem os resultados (clique na imagem para vê-la no tamanho original):

TestePerformanceContains

Contains = 416 milissegundos.

TestePerformanceIndexOf

IndexOf = 496 milissegundos.

Depois testei com ambos descomentados, em sequência, vejam:

TestePerformanceContains&IndexOf

Contains = 609 milissegundos;

IndexOf = 1712 milissegundos (1,7 segundos).

Em ambos os testes o Contains é superior, mais rápido que o IndexOf, acredito que em partes isso se deve porque o IndexOf é ligeiramente mais complexo do que o Contains, e envolve mais parâmetros também.

Conclusão

Vimos que mesmo o Contains sendo superior nos testes de performance a melhor solução é o IndexOf, já que o Contains tem essa deficiência de que se um usuário pesquisa informando um valor com letras diferentes do “esperado” pelo banco de dados não irá retornar nada, o que pode gerar uma certa frustação para o usuário.

Acredito que este artigo possa ter ajudado e elucidado algumas dúvidas de muitos que, assim como eu, sempre usaram o método Contains e se deparavam com esse problema do case-sensitive. Como (quase) tudo em programação sempre temos diversas formas de resolver um mesmo problema, essa forma pra mim é a melhor, se não for a única.

É isso, acredito que o artigo tenha sido útil, comentem o que acharam, que deu trabalho escrever esse monte de texto! Como de praxe, o código-fonte deste projeto se encontra neste link.

Fonte de Consulta: Stack Overflow, melhor site de perguntas e respostas de programação em geral que conheço, se não conhece recomendo a visita, vai te poupar stress desnecessários em suas buscas!

Obrigado e até o próximo artigo.

Um comentário em “O Perigo do Método Contains

Expresse sua opinião!

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s