O programador

Coleções .NET, Parte 2: Trabalhando com a C5

Ted Neward

 

 

Ted NewardBem-vindo mais uma vez.

Na primeira parte desta série, introduzi brevemente a biblioteca Copenhagen Comprehensive Collection Classes for C# (C5), um conjunto de classes desenvolvidas para suplementar (se não substituir) as classes System.Collections que são fornecidas com a biblioteca de tempo de execução do Microsoft .NET Framework. Há uma grande sobreposição entre as duas bibliotecas, em parte, porque a C5 deseja seguir muitas das mesmas linguagens utilizadas pela .NET Framework Class Library (FCL) e, em parte, porque há um número limitado de maneiras de representar razoavelmente um tipo de coleção específica. É difícil imaginar uma coleção indexada — como um Dicionário ou uma Lista — que não dê suporte à sintaxe da linguagem para propriedades indexadas: o operador “[]” em C# e o operador “()” no Visual Basic. No entanto, enquanto as coleções FCL são utilitárias, as coleções C5 vão um pouco além disso, e é nisso que desejamos gastar nosso tempo.

Observe também que é muito provável que existam diferenças entre as duas bibliotecas que os proponentes ou os críticos de cada uma apontarão rapidamente — o manual da coleção C5 discute algumas das implicações de desempenho, por exemplo. Dito isso, evito a maior parte dos benchmarks de desempenho em razão de que, geralmente, tudo que um benchmark prova é que, para um caso específico ou para um conjunto de casos, alguém obteve um que executou mais rapidamente do que outro, o que realmente não indica que isso será verdadeiro para todos os casos entre os dois. Isso não significa que todos os benchmarks são inúteis, apenas que o contexto é importante para o benchmark. Os leitores são fortemente encorajados a usar seus próprios cenários específicos, transformá-los em um benchmark e fazer um desempate entre os dois, apenas para ver se há uma diferença marcante nesses casos específicos.

Implementações

Antes de mais nada, vamos examinar as diferentes implementações da coleção fornecidas pela C5. Mais uma vez, conforme discutimos da última vez, o desenvolvedor que usa a C5 normalmente não deve se preocupar com a implementação em uso, exceto ao decidir qual implementação criar — no restante do tempo, a coleção deverá ser referenciada pelo tipo da interface. Para obter uma descrição dos tipos de interface, consulte a coluna anterior desta série em msdn.microsoft.com/magazine/jj883961 ou a documentação da C5 em bit.ly/UcOcZH. Estas são as implementações:

  • CircularQueue<T> implementa IQueue<T> e IStack<T> para fornecer a semântica de "primeiro a entrar, primeiro a sair" de IQueue<T> (via Enqueue e Dequeue) ou a semântica de "último a entrar, primeiro a sair" de IStack<T> (via Push e Pop), com apoio de uma lista vinculada. Ela cresce em capacidade, conforme necessário.
  • ArrayList<T> implementa IList<T>, IStack<T> e IQueue<T>, com apoio de uma matriz.
  • LinkedList<T> implementa IList<T>, IStack<T> e IQueue<T>, usando uma lista de nós duplamente vinculada.
  • HashedArrayList<T> implementa IList<T>, com apoio de uma matriz, mas também mantém uma tabela de hash internamente para localizar de maneira eficiente a posição de um item da lista. Além disso, ela não permite duplicatas na lista (uma vez que duplicatas iriam confundir a pesquisa da tabela de hash).
  • HashedLinkedList<T> implementa IList<T>, com apoio de uma lista vinculada, e como sua prima com apoio de matriz, usa uma tabela de hash interna para otimizar pesquisas.
  • WrappedArray<T> implementa IList<T>, encapsulando uma matriz unidimensional. A vantagem dessa classe é que ela simplesmente "decora" a matriz, acelerando muito a obtenção da funcionalidade da C5, ao contrário de copiar os elementos da matriz para uma ArrayList<T>.
  • SortedArray<T> implementa IIndexedSorted<T>, o que significa que a coleção pode ser indexada e classificada — discutiremos isso em um segundo. Ela mantém seus itens classificados e não permite duplicatas.
  • TreeSet<T> implementa IIndexedSorted<T> e IPersistedSorted<T> e é apoiada por uma árvore vermelho/preto, que é ideal para inserção, remoção e classificação. Como todos os conjuntos, ela não permite duplicatas.
  • TreeBag<T> implementa IIndexedSorted<T> e IPersistedSorted<T>, é apoiada por uma árvore vermelho/preto balanceada, mas é essencialmente um "recipiente" (algumas vezes chamada de "multiset"), indicando que permite duplicatas.
  • HashSet<T> implementa IExtensible<T> e apoia o conjunto (significando sem duplicatas) por uma tabela de hash com encadeamento linear. Isso significa que as pesquisas serão mais rápidas e que as modificações nem tanto.
  • HashBag<T> implementa IExtensible<T> e apoia o recipiente (significando que duplicatas são permitidas) por uma tabela de hash com encadeamento linear, novamente tornando as pesquisas rápidas.
  • IntervalHeap<T> implementa IPriorityQueue<T> usando um heap de intervalo armazenado como uma matriz de pares, tornando-a eficiente para efetuar pull do final “max” ou “min” da fila de prioridades.

Há mais algumas implementações, e o manual e a documentação da C5 têm mais detalhes, se você estiver interessado. No entanto, além das implicações de desempenho, o aspecto crítico a saber é quais implementações implementam quais interfaces, de forma que você possa ter uma boa ideia de cada na hora de escolher uma para criar uma instância. Sempre é possível trocar para uma implementação diferente mais tarde, desde que você siga a diretriz de design da C5 de sempre referenciar as coleções pelas interfaces e não pelos seus tipos de implementação.

Funcionalidade

Se a C5 fosse apenas uma coleção maior de implementações de coleção, ela seria interessante, mas provavelmente não seria suficiente para justificar interesse ou discussão. Felizmente, ela oferece alguns novos recursos para os desenvolvedores que merecem discussão.

Exibições Um das pequenas coisas boas interessantes da biblioteca C5 é a noção de “views” (exibições): subcoleções de elementos da coleção de origem que são, na verdade, não cópias, mas apoiadas pela coleção original. Na verdade, isso foi o que o código da coluna anterior fez no teste de exploração. Consulte a Figura 1 para saber como criar exibições em uma coleção.

Figura 1 Criando exibições em uma coleção

 

[TestMethod] public void GettingStarted() {   IList<String> names = new ArrayList<String>();   names.AddAll(new String[]     { "Hoover", "Roosevelt", "Truman", "Eisenhower", "Kennedy" });   // Print item 1 ("Roosevelt") in the list   Assert.AreEqual("Roosevelt", names[1]);   Console.WriteLine(names[1]);   // Create a list view comprising post-WW2 presidents   IList<String> postWWII = names.View(2, 3);   // Print item 2 ("Kennedy") in the view   Assert.AreEqual("Kennedy", postWWII[2]); }

A exibição é apoiada pela lista original, o que significa que, se a lista original for alterada por qualquer motivo, a exibição dela também será afetada. Consulte a Figura 2 para ver como as exibições são potencialmente mutáveis.

Figura 2 As exibições são potencialmente mutáveis

[TestMethod] public void ViewExploration() {   IList<String> names = new ArrayList<String>();   names.AddAll(new String[]     { "Washington", "Adams", "Jefferson",       "Hoover", "Roosevelt", "Truman",       "Eisenhower", "Kennedy" });   IList<String> postWWII = names.View(4, names.Count - 4);   Assert.AreEqual(postWWII.Count, 4);   IList<String> preWWII = names.View(0, 5);   Assert.AreEqual(preWWII.Count, 5);   Assert.AreEqual("Washington", preWWII[0]);   names.Insert(3, "Jackson");   Assert.AreEqual("Jackson", names[3]);   Assert.AreEqual("Jackson", preWWII[3]); }

Como mostra este teste, a alteração da lista subjacente (“names”) significa que as exibições definidas nela (neste caso, a exibição “preWWII”) também têm seu conteúdo alterado, de forma que agora o primeiro elemento dessa exibição é “Washington” e não “Hoover.”

No entanto, quando possível, a C5 preservará a inviolabilidade da exibição. Por exemplo, se a inserção ocorrer na frente da coleção (onde a C5 pode inseri-la sem alterar o conteúdo da exibição “preWWII”), o conteúdo da exibição permanecerá inalterado:

[TestMethod] public void ViewUnchangingExploration() {   IList<String> names = new ArrayList<String>();   names.AddAll(new String[]     { "Hoover", "Roosevelt", "Truman", "Eisenhower", "Kennedy" });   IList<String> preWWII = names.View(0, 2);   Assert.AreEqual(preWWII.Count, 2);   names.InsertFirst("Jackson");   Assert.AreEqual("Jackson", names[0]);   Assert.AreEqual("Hoover", preWWII[0]); }

Coleções imutáveis (protegidas) Com o aumento dos conceitos funcionais e dos estilos de programação, muita ênfase foi dada a dados e objetos imutáveis, principalmente porque objetos imutáveis oferecem muitos benefícios em comparação com programação de simultaneidade e paralela, mas também porque os desenvolvedores consideram que objetos imutáveis são mais fáceis de entender e ponderar. Corolário a esse conceito está o conceito de coleções imutáveis — a ideia de que, independentemente dos objetos dentro da coleção serem imutáveis, a própria coleção é fixa e não é possível alterar (adicionar ou remover) os elementos da coleção. Observação: É possível ver uma visualização de coleções imutáveis liberadas no NuGet no blog da Base Class Library (BCL) do MSDN em bit.ly/12AXD78.)

Na C5, coleções imutáveis são manipuladas com a criação de instâncias de coleções “wrapper” em torno da coleção que contém os dados de interesse, essas coleções são coleções "Protegidas" e são usadas em estilo clássico de padrão Decorador:

public void ViewImmutableExploration() {   IList<String> names = new ArrayList<String>();   names.AddAll(new String[]     { "Hoover", "Roosevelt", "Truman", "Eisenhower", "Kennedy" });   names = new GuardedList<String>(names);   IList<String> preWWII = names.View(0, 2);   Assert.AreEqual("Hoover", preWWII[0]);   names.InsertFirst("Washington");   Assert.AreEqual("Washington", names[0]); }

Se alguém tentar escrever código que adicione ou remova elementos da lista, a C5 rapidamente dissuadirá esse desenvolvedor da ideia: Uma exceção é gerada assim que qualquer um dos métodos de "modificação" (Add, Insert, InsertFirst etc.) é chamado.

A propósito, isso oferece uma oportunidade muito poderosa. Na coluna anterior, mencionei que um dos pontos chave do design que entra na C5 é a ideia de que as coleções só devem ser usadas por meio de interfaces. Supondo que os desenvolvedores que usam a C5 sigam essa ideia de design, agora será realmente simples garantir que uma coleção nunca seja modificada por um método pelo qual passe (consulte a Figura 3).

Figura 3 Coleções protegidas (imutáveis)

public void IWannaBePresidentToo(IList<String> presidents) {   presidents.Add("Neward"); } [TestMethod] public void NeverModifiedCollection() {   IList<String> names = new ArrayList<String>();   names.AddAll(new String[]     { "Hoover", "Roosevelt", "Truman","Eisenhower", "Kennedy" });   try   {     IWannaBePresidentToo(new GuardedList<String>(names));   }   catch (Exception x)   {     // This is expected! Should be a ReadOnlyException   }   Assert.IsFalse(names.Contains("Neward"));  }

Mais uma vez, quando o método IWannaBePresidentToo tenta modificar a coleção passada para ele (que, discutivelmente, é um design inadequado da parte do programador que o escreveu, mas infelizmente há muito código desse tipo por aí), uma exceção é gerada.

A propósito, se você preferir que a coleção não gere uma exceção e que, silenciosamente, não faça a modificação (o que acho que é muito sutil, mas alguns desenvolvedores podem precisar dessa funcionalidade), é relativamente fácil criar sua própria versão de Guarded­Array<T> que não gere a exceção.

Eventos Algumas vezes, modificações nas coleções são, na verdade, o que você deseja permitir — só que você deseja saber quando uma coleção é modificada. Está bem, você pode criar um thread e fazer esse thread girar indefinidamente sobre a coleção, comparando o conteúdo com o conteúdo da iteração anterior, mas, além de isso ser um terrível desperdício de recursos da CPU, também é muito difícil de escrever e manter, tornando-se provavelmente a pior solução de design possível — e certamente uma visão mais pobre do que simplesmente usar uma coleção que dê suporte a eventos nativamente. Todas as coleções em C5 oferecem a habilidade de recuar os delegados da coleção, para serem invocados quando determinadas operações ocorrerem na coleção (consulte a Figura 4).

Figura 4 “Quando eu me tornar presidente ...”

[TestMethod] public void InaugurationDay() {   IList<String> names = new ArrayList<String>();   names.AddAll(new String[]     { "Hoover", "Roosevelt", "Truman", "Eisenhower", "Kennedy" });   names.ItemsAdded +=     delegate (Object c, ItemCountEventArgs<string> args)   {     testContextInstance.WriteLine(       "Happy Inauguration Day, {0}!", args.Item);   };   names.Add("Neward");   Assert.IsTrue(names.Contains("Neward")); }

É claro que o manipulador de eventos pode ser escrito como uma lambda, ele é apenas um pouco mais descritivo para mostrar a você os tipos reais de argumento. O primeiro argumento é — como é a norma — a própria coleção.

A apenas um NuGet de distância

Nenhuma parte da C5 pôde ser criada em torno da .NET FCL (com exceção da ênfase em interfaces, que têm suporte da FCL, mas parece que realmente não endossa isso muito fortemente), mas a boa notícia sobre a C5 é que isso está pronto, testado e apenas a uma distância de um pacote de instalação do NuGet.

Boa codificação.

Ted Neward -é diretor da Neward & Associates LLC. Ele já escreveu mais de 100 artigos e é autor e coautor de dezenas de livros, incluindo “Professional F# 2.0” (Wrox, 2010). Ele é um MVP de F# e famoso especialista em Java, além de participar como palestrante sobre o Java e o .NET em conferências no mundo todo. Ele atua como consultor e mentor regularmente. Entre em contato com ele pelo email ted@tedneward.com ou Ted.Neward@neudesic.com se estiver interessado em que ele trabalhe com sua equipe. Ele mantém um blog em blogs.tedneward.com e pode ser seguido no Twitter em twitter.com/tedneward.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Immo Landwerth