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

Conceitos: Como chamar um Delegate

Como chamar um Delegate? Questões sobre delegates são incomuns em provas para candidatos iniciantes, aparecem com alguma frequência em vagas para profissionais plenos e é “figurinha carimbada” na maioria das provas para seniores.

Entretanto, nem sempre é uma pergunta difícil. Para acertar a questão abaixo, o candidato precisa apenas conhecimentos básicos sobre o assunto.

Qual(ais) da(s) próximas alternativas substitui(em) ***** na função main fazendo com que o programa retorne o valor 15?

using System;

namespace Program {
   class Program {
      static void Main(string[] args) {
         int result;

         *****

         Console.WriteLine(result);
      }

      static int Add(int a, int b) {
         return a + b;
      }

      static int DoIntMath(Func<int,int,int> function, int x, int y) {
         return function(x, y);
      }    
   }
}

A. result = DoIntMath(new delegate int(int a, int b){ return a + b; }, 5, 10);
B. result = DoIntMath(Add, 5, 10);
C. result = DoIntMath((a, b) => (a + b),  5,  10);
D. result = DoIntMath(delegate (int a, int b){ return a + b; }, 5, 10);
E. result = DoIntMath(Add(5, 10));

Mas o que é um delegate?

Um delegate é um tipo que permite fazer referência para um método com uma lista de parâmetros e um tipo de retorno específico. Ou seja, é um ponteiro que pode ser associado a qualquer método com uma assinatura e tipo de retorno compatíveis.

A função DoIntMath

static int DoIntMath(Func <int, int, int> function, int x, int y)

… possui três parâmetros. Os dois últimos (x e y ) são parâmetros inteiros comuns, já o primeiro (function) é uma função que retorna um número inteiro e recebe dois parâmetros também inteiros: Func< retorno, parâmetro 1, parâmetro 2>.

A função function aceita qualquer coisa com o formato int(parâmetro int, parâmetro int). Em outras palavras, para invocar a função DoIntMath, é preciso passar uma função semelhante à int Add(int a, int b), o que já elimina a alternativa “E”. A alternativa “A” também pode ser facilmente eliminada por causa da palavra new, uma vez que não se instancia um delegate.

Com isto, restam como corretas as alternativas “B”, onde é passada uma função já existente (Add) e as alternativas “C” e “D”, onde são passadas novas funções que foram declaradas diretamente no parâmetro function.

Origem da questão
País: Holanda Tipo: Conceitos Assunto: Delegate
Ramo de negócio da empresa: Consultoria Grau de Dificuldade: Médio

Conceitos: Tipos Anônimos (anonymous type)

Qual das próximas afirmações descrevem corretamente o código C# abaixo (selecione todas as corretas):

var pessoa = new {Id = 1, Nome = "Maria"};

A. O código tem um erro e não compila

B. pessoa pode ser reatribuída da seguinte forma: pessoa = new {Idade = 25, Nome = “Pedro”};

C. pessoa é do tipo anônimo (anonymous type)

D. pessoa deriva de Object.

E. Os atributos Id e Nome são somente leitura (read-only)

A primeira coisa a ser feita para resolver esta questão é analisar o código, que apresenta a criação de um novo objeto chamado pessoa, sem no entanto especificar um tipo. Portanto, trata-se de um “tipo anônimo” (anonymous type, em inglês).

Os Tipos Anônimos fornecem uma maneira rápida de encapsular um grupo de propriedades em único objeto, sem que seja necessário definir explicitamente o seu tipo. Os tipos dos atributos são inferidos pelo compilador e não estão disponíveis ao nível do código fonte. Além disto, eles são somente leitura (read-only).

Agora que você já sabe o que é um tipo anônimo, vamos analisar as opções:

  • Letra A:  errada, pois o código compila perfeitamente;
  • Letra B: errada, não é possível reatribuir um tipo anônimo;
  • Letra C: correta, trata-se de um tipo anônimo;
  • Letra D: correta, tupo em C# deriva de Object; e
  • Letra E: correta, já que os atributos de tipos anônimos são somente leitura;

Sendo assim, estão corretas as letras C, D e E.

Atenção

Não confunda um tipo anônimo com a nova forma de instânciar objetos introduzida no C#9, chamada target-typed object creation. Ela permite instanciar um objeto sem usar o nome da classe depois da palavra reservada new, desde que a classe tenha sido explicitamente declarada antes do nome do objeto (mais informações em docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-9.0/target-typed-new – texto em inglês).

public class Pessoa {
   public int Id;
   public string Nome;
}

(...)

//a classe "Pessoa" eh explicitamente declarada antes do objeto "pessoa", 
//não sendo necessario repetir a declaracao depois da palavra  "new"
Pessoa pessoa = new(1, "Maria");
Origem da questão
País: Holanda Tipo: Conceitos Assunto: Tipos Anônimos
Ramo de negócio da empresa: Consultoria Grau de Dificuldade: Médio