Como criar dicionários case insensitive

Dicionários são sempre listados entre as melhores estruturas de dados pois o custo de acesso a um dado registro é O(1), ou seja, não é necessário percorrer todo dataset para encontrar um elemento, basta acessar o seu índice diretamente. Mas como criar dicionários case insensitive? Ou em outras palavras, como criar dicionários que ignorem a forma como uma palavra foi escrita?

O código abaixo armazena o estoque de produtos para uma determinada loja. Sempre que um produto é adicionado ele verifica se o produto já existe, caso exista aumenta a quantidade em estoque, quando não existe cria um novo registro.

namespace Dictionary {
   public class Store {
      public Dictionary<string, int> Items = new();

      public void Add(string product, int quant) {
         if (Items.ContainsKey(product)) {
            Items[product] += quant;
         }
         else {
            Items.Add(product, quant);
         }
      }
   }
   public static class DictionaryExample {
      public static void Main() {
         var store = new Store();
         store.Add("Bike", 10);
         store.Add("bike", 15);
         store.Add("Skate", 8);

         foreach (var item in store.Items) {
            Console.WriteLine($"{item.Key} - {item.Value}");
         }
         Console.ReadKey();
      }
   }
}

O problema é que, por padrão, o runtime trata strings como case sensitive, então “Bike”, “BIKE”, …. e “bike” são palavras diferentes, resultando em dois diferentes produtos:

Bike - 10
bike - 15
Skate - 8

Uma solução simples

Uma solução simples para o problema é converter todas as letras do nome do produto para minúsculas ou maiúsculas, usando ToLower ou ToUpper no método Add():

public void Add(string product, int quant) {
   product = product.ToLower();
   if (Items.ContainsKey(product)) {
      Items[product] += quant;
   }
   else {
      Items.Add(product, quant);
   }
}

O que trará como resultado:

bike - 25
skate - 8

Funciona, mas não é o ideal.

Uma solução eficaz

Uma solução melhor para o problema é especificar ao runtime como ele deve tratar as strings armazenadas no dicionário usando a classe StringComparer no momento que o dicionário é instanciado.

Então, ao invés de instanciar o objeto simplesmente usando new(), basta passar o tipo de comparação desejada no construtor no mesmo. A opção InvariantCultureIgnoreCase fará com que as strings “Bike” e “bike” sejam consideradas a mesma sem a necessidade do uso do método ToLower e produzirá exatamente a mesma saída.

public Dictionary<string, int> Items = new(StringComparer.InvariantCultureIgnoreCase);

Quer saber mais sobre os diferentes tipos de comparação, então acesse a documentação oficial da Microsoft: StringComparer Class

 

Adição de elementos numa lista dentro de um laço sobre a mesma

Participei da seleção para uma empresa da Bélgica que desenvolve jogos para cassinos. Na primeira fase do processo foram feitas várias pequenas questões para verificar se o o candidato conhece os conceitos da linguagem. Quatro destas questões eram sobre listas ou listas e Linq. Abaixo uma delas, sobre a adição de novos elementos numa lista dentro de um laço foreach sobre a mesma lista:

Qual será a saída ao executar este código?

class Program {
   static void Main() {
      var ints = new List<int>(3) { 1, 2 };

      foreach (int i in ints) {
           ints.Add(i + 1);
      }
      Console.WriteLine("{0}", ints[2]);
   }
}

a) A saída será 2 e a lista conterá 4 elementos.
b) A saída será 1 e a lista conterá 6 elementos.
c) A saída será 2 e a lista conterá 6 elementos.
d) Não irá compilar.
e) Haverá uma exceção do tipo “fora do intervalo” em tempo de execução.
f) Haverá uma exceção do tipo “operação inválida” em tempo de execução.

O programa é bem simples: cria uma lista com três posições e preenche as duas primeiras com 1 e 2. Depois disto, para cada elemento da lista adiciona um novo elemento, cujo valor é o elemento atual mais 1. Por fim, mostra o terceiro elemento da lista.

Ou seja:

A lista começa com os elementos 1 e 2. Na primeira iteração adiciona um novo elemento com valor 2 (1 + 1) e lista passa a ter três elementos (1, 2, 2). Na segunda interação adiciona o elemento 3 (2 + 1) e lista passa a ter 4 elementos (1, 2, 2, 3). Resultado, letra  A: saída será 2 e a lista conterá 4 elementos. Certo? Não, errado. O sistema gera uma exceção em tempo de execução.

Mas por que ocorre uma exceção?

A exceção ocorre porque o código está tentando alterar a coleção dentro de um laço que percorre a própria coleção. Quando o programa é executado ele roda normalmente até inserir o primeiro novo elemento na lista, entretanto quando o cursor volta para o laço foreach ocorre o erro:

System.InvalidOperationException: ‘Collection was modified; enumeration operation may not execute.’

Ou seja, a coleção foi modifica e a operação sobre ela não pode ser executada. Resposta correta letra F: haverá uma exceção do tipo “operação inválida” em tempo de execução.

Execute o programa passo a passo usando o debug para ver o ponto exato onde a exceção é gerada

Este comportamento faz todo sentido, pois o laço foreach está preparado para percorrer toda a lista. Se a lista é modifica, seja por adição ou exclusão de elementos, como o compilador irá controlar quando o laço deve ser finalizado?

Mais uma dúvida….

Caso fosse possível adicionar novos elementos à lista dentro do foreach, surge uma nova questão: a lista foi declarada com tamanho três e iniciada com dois elementos, seria possível adicionar mais do que um elemento a ela ou neste caso teria uma exceção do tipo out of range (fora do intervalo)?

Isto pode ser testado modificando ligeiramente o código da questão, alterando o laço foreach para um laço for:

class Program {
   static void Main() {
      var ints = new List<int>(3) { 1, 2 };

      for(int i = 1; i < 3; i++) {
           ints.Add(i + 1);
      }
      Console.WriteLine("{0}", ints[2]);
   }
}

No código acima dois novos elementos (2 e 3) são adicionados a lista, que passa a ter quatro elementos (1, 2, 2, 3). Nenhuma exceção é levantada, pois o tamanho da lista é alterado automaticamento de 3 para 4. O “3” usado no momento que a lista é instanciada em memória determina o tamanho inicial alocado e não o tamanho máximo.

Origem da questão
País: Bélgica Tipo: Conceitos Assunto: List
Ramo de negócio da empresa: Games Grau de Dificuldade: fácil