Sign in
Log inSign up
Coleções

Coleções

Crab Log's photo
Crab Log
·Nov 6, 2021·

31 min read

Traduzido de Collections.

Este post faz parte do Tutorial de Pharo Smalltalk.

Para fazer bom uso das classes de coleção, o leitor precisa pelo menos de um conhecimento superficial da grande variedade de coleções que existem, e de suas semelhanças e diferenças. É disso que se trata neste capítulo.

As classes de coleção formam um grupo livremente definido de subclasses de Collection e Stream. Algumas dessas subclasses, tais como Bitmap, ou CompiledMethod são classes de propósito especial criadas para uso em outras partes do sistema ou em aplicações e, portanto, não são categorizadas como Collections pela organização do sistema.

Neste capítulo, utilizamos o termo Collection Hierarchy para significar Collection e suas subclasses que também estão nos pacotes rotulados como Collections-*. Utilizamos o termo Stream Hierarchy para significar Stream e suas subclasses que também estão nos pacotes de Collections-Streams.

Pasted image 20211026093504.png Figura 1-1 Algumas das principais classes de coleção do Pharo.

Neste capítulo, focalizamos principalmente o subconjunto de classes de coleção mostradas na Figura 1-1. "Streams" serão discutidos separadamente no capítulo Streams.

O Pharo, por padrão, fornece um bom conjunto de coleções. Além disso, o projeto "Containers" disponível em github.com/Pharo-Containers propõe implementações alternativas ou novas coleções e estruturas de dados.

Comecemos com um ponto importante sobre o formato das coleções no Pharo. Suas APIs utilizam fortemente funções de alta ordem (high-order functions): assim, embora possamos usar para loops como no Java antigo, na maioria das vezes os desenvolvedores do Pharo usarão o estilo iterator baseado em funções de alta ordem.

1.1 High-order functions

A programação com coleções usando funções de alta ordem em vez de elementos individuais é uma forma importante de elevar o nível de abstração de um programa. A função Lisp map, que aplica uma função como argumento a cada elemento de uma lista e retorna uma nova lista contendo os resultados, é um exemplo precoce deste estilo. Seguindo sua origem no Smalltalk, Pharo adota esta programação de alta ordem baseada em coleções como um princípio central. Linguagens modernas de programação funcional, como ML e Haskell, seguiram o exemplo de Smalltalk.

Por que isso é uma boa idéia? Suponhamos que você tenha uma estrutura de dados contendo uma coleção de registros de estudantes e deseje realizar alguma ação sobre todos os estudantes que atendam a alguns critérios. Os programadores educados para usar uma linguagem imperativa irão imediatamente procurar um loop, mas o programador de Pharo irá escrever:

 students
       select: [ :each | each gpa < threshold ]

Esta expressão retorna uma nova coleção contendo precisamente aqueles elementos de students para os quais o bloco (a função entre parênteses) retorna true. Este bloco pode ser pensado como uma expressão lambda que define uma function x. x gpa < threshold anônima. Este código tem a simplicidade e a elegância de uma domain-specific query language.

A mensagem select: é entendida por 'todas' as coleções em Pharo. Não há necessidade de descobrir se a estrutura de dados students é um array ou uma linked list: a mensagem select: é entendida por ambos. Note que isto é bem diferente de utilizar um loop, onde é preciso saber se students é um array ou uma linked list antes que o loop possa ser configurado.

Em Pharo, quando se fala de uma coleção sem ser mais específico sobre o tipo de coleção, significa um objeto que suporta protocolos bem definidos para testar a inclusão e enumerar os elementos. Todas as coleções entendem as mensagens de teste includes:, isEmpty e occurrencesOf:. Todas as coleções entendem as mensagens de enumeration: do:, select:, reject: (que é o oposto de select:), collect: (que é como o map de Lisp), detect:ifNone:, inject:into: (que executa uma left fold) e muito mais. É a ubiqüidade deste protocolo, bem como sua variedade, que o torna tão poderoso.

A tabela abaixo resume os protocolos padrão suportados pela maioria das classes da hierarquia de coleção. Estes métodos são definidos, redefinidos, otimizados ou ocasionalmente interditados (forbidden) por subclasses de Collection.

ProtocolMethods
accessingsize, capacity, at:, at:put:
testingisEmpty, includes:, contains:, occurrencesOf:
addingadd:, addAll:
removingremove:, remove:ifAbsent:, removeAll:
enumeratingdo:, collect:, select:, reject: detect:, detect:ifNone:, inject:into:
convertingasBag, asSet, asOrderedCollection, asSortedCollection, asArray, asSortedCollection
creationwith:, with:with:, with:with:with:, with:with:with:with:, withAll:

1.2 As variedades de coleções

Além desta uniformidade básica, há muitos tipos diferentes de coleções, seja apoiando protocolos diferentes ou proporcionando comportamentos diferentes para as mesmas solicitações. Vamos observar brevemente algumas das principais diferenças:

  • Sequenceable: Instâncias de todas as subclasses de SequenceableCollection começam a partir de um elemento first e seguem em uma ordem bem definida até um elemento last. Instâncias de Set, Bag e Dictionary, por outro lado, não são sequenciáveis.
  • Sortable: Uma SortedCollection mantém seus elementos em ordem.
  • Indexable: A maioria das coleções sequenciáveis também são indexáveis, ou seja, os elementos podem ser recuperados com a mensagem at: anIndex. Array é a estrutura de dados indexáveis familiar com um tamanho fixo; anArray at: n recupera o n-ésimo elemento de anArray, e anArray at: n put: v muda o n-ésimo elemento para v. LinkedLists é sequencial mas não indexável, ou seja, eles entendem first e last, mas não a mensagem at:.
  • Keyed: Instâncias de Dictionary e suas subclasses são acessadas por chaves (keys) ao invés de índices.
  • Mutable: A maioria das coleções são mutáveis, mas Intervals e Symbols não são. Uma Interval é uma coleção imutável que representa uma gama de Integers, por exemplo, 5 to: 16 by: 2 é um intervalo que contém os elementos 5, 7, 9, 11, 13 e 15. É indexável com a mensagem at: anIndex, mas não pode ser alterado com a mensagem at: anIndex put: aValue.
  • Growable: Instâncias de Interval e Array são sempre de um tamanho fixo. Outros tipos de coleções (sorted collections, ordered collections, e linked lists) podem crescer após a criação. A classe OrderedCollection é mais geral do que Array; o tamanho de uma OrderedCollection cresce sob demanda, e define mensagens addFirst: anElement e addLast: anElement, assim como mensagens at: anIndex e at: anIndex put: aValue.
  • Accepts duplicates: Um Set filtra as duplicatas, mas um Bag não. As classes Dictionary, Set e Bag utilizam o método = fornecido pelos elementos; as variantes Identity dessas classes utilizam o método ==, que testa se os argumentos são o mesmo objeto, e as variantes Pluggable utilizam uma relação de equivalência arbitrária fornecida pelo criador da coleção.
  • Heterogeneous: A maioria das coleções contém qualquer tipo de elemento. Uma String, CaracterArray ou Symbol, no entanto, contém apenas Characters. Um Array contém qualquer mistura de objetos, mas um ByteArray contém apenas Bytes. Uma LinkedList é limitada a conter elementos que estejam de acordo com o protocolo de acesso Link.

1.3 Implementações de coleções

Pasted image 20211026135223.png Figure 1-2 Algumas classes de coleção categorizadas por técnica de implementação.

Estas categorizações por funcionalidade não são nossa única preocupação; devemos também considerar como as classes de coleção são implementadas. Como mostrado na Figura 1-2, cinco técnicas principais de implementação são empregadas.

  • Arrays armazenam seus elementos nas variáveis de instância (indexáveis) do próprio objeto de coleção; como conseqüência, os arrays devem ser de tamanho fixo, mas podem ser criados com uma única alocação de memória.
  • As OrderedCollections e SortedCollections armazenam seus elementos em um array que é referenciado por uma das variáveis de instância da coleção. Consequentemente, o array interna pode ser substituída por uma maior se a coleção crescer além de sua capacidade de armazenamento.
  • Os vários tipos de conjuntos e dicionários também fazem referência a um array subsidiário para armazenamento, mas utilizam o array como uma tabela de hash. Bags utilizam um dicionário subsidiário, com os elementos da bag como chaves e o número de ocorrências como valores.
  • As LinkedLists utilizam uma representação padrão interligada unidirecional.
  • Intervals são representados por três inteiros que registram os dois pontos extremos e o tamanho do passo.

Além dessas classes, há também variantes fracas (weak) de Array, Set e dos vários tipos de dicionário. Essas coleções ligam de forma fraca aos seus elementos, ou seja, de forma que não impede que os elementos sejam coletados [N.T.: Pelo garbage collector]. A máquina virtual do Pharo está ciente dessas classes e as manipula especialmente.

1.4 Exemplos de classes chave

Apresentamos agora as classes de coleção mais comuns ou importantes, utilizando exemplos simples de códigos. Os principais protocolos de coleções são:

  • mensagens at:, at:put: - para acessar um elemento,
  • mensagens add:, remove: - para adicionar ou remover um elemento,
  • mensagens size, émpty, include:- para obter algumas informações sobre a coleção,
  • mensagens do:, collect:, select: - para iterar sobre a coleção.

Cada coleção pode implementar (ou não) tais protocolos, e quando o fazem, elas os interpretam para se adequar à sua semântica. Sugerimos que você navegue pelas próprias classes, a fim de identificar protocolos específicos e mais avançados.

Vamos nos concentrar nas classes de coleção mais comuns: OrderedCollection, Set, SortedCollection, Dictionary, Interval, e Array.

1.5 Protocolos comuns de criação

Há várias maneiras de criar instâncias de coleções. As mais genéricas utilizam a mensagem new: aSize e with: anElement.

  • new: anInteger cria uma coleção de tamanho anInteger cujos elementos serão todos nil.
  • with: anObject cria uma coleção e adiciona anObject à coleção criada.

Coleções diferentes concretizarão este comportamento de forma diferente.

Você pode criar coleções com elementos iniciais utilizando os métodos with:, with: with: etc. para até seis elementos.

Array with: 1 >>> #(1) 

Array with: 1 with: 2 >>> #(1 2) 

Array with: 1 with: 2 with: 3 >>> #(1 2 3) 

Array with: 1 with: 2 with: 3 with: 4 >>> #(1 2 3 4) 

Array with: 1 with: 2 with: 3 with: 4 with: 5 >>> #(1 2 3 4 5) 

Array with: 1 with: 2 with: 3 with: 4 with: 5 with: 6 
>>> #(1 2 3 4 5 6)

Você também pode utilizar a mensagem addAll: aCollection para adicionar todos os elementos de um tipo de coleção a outro tipo:

(1 to: 5) asOrderedCollection 
    addAll: '678'; 
    yourself >>> an OrderedCollection(1 2 3 4 5 $6 $7 $8)

Tome cuidado porque addAll: devolve seu argumento, e não o receptor! Você também pode criar muitas coleções com withAll: aCollection.

Array withAll: #(7 3 1 3) >>> #(7 3 1 3) 

OrderedCollection withAll: #(7 3 1 3) 
>>> an OrderedCollection(7 3 1 3) 

SortedCollection withAll: #(7 3 1 3) 
>>> a SortedCollection(1 3 3 7) 

Set withAll: #(7 3 1 3) >>> a Set(7 1 3) 

Bag withAll: #(7 3 1 3) >>> a Bag(7 1 3 3)

1.6 Array

Um Array é uma coleção de elementos de tamanho fixo acessados por índices inteiros. Ao contrário da convenção em C em Pharo, o primeiro elemento de um array está na posição 1 e não 0. O principal protocolo para acessar elementos de array é o método at: e at:put:.

  • at: anInteger retorna o elemento no índice anInteger.
  • at: anInteger put: anObject coloca anObject no índice anInteger.

Arrays' são coleções de tamanho fixo, portanto não podemos adicionar ou remover elementos no final de um array. O seguinte código cria um array de tamanho 5, coloca valores nas primeiras 3 posições e retorna o primeiro elemento.

| anArray |  
anArray := Array new: 5.

anArray at: 1
anArray at: 2
anArray at: 3
anArray at: 1
>>> 4

Há várias maneiras de criar instâncias da classe Array. Podemos utilizar

  • new:, with:,
  • #( ) construção de arrays literais e
  • { . } sintaxe dinâmica compacta.

Criação com new:

A mensagem new: anInteger cria um array de tamanho anInteger. A mensagem Array new: 5 cria um array de tamanho 5.

Nota: O valor de cada elemento é inicializado em nil.

Criação usando with:

As mensagens with :* permitem especificar o valor dos elementos. O seguinte código cria um conjunto de três elementos que consistem no número 4, a fração 3/2 e a string 'lulu'.

Array with: 4 with: 3/2 with: 'lulu' >>> {4. (3/2). 'lulu'}

Criação de literal com #()

A expressão #() cria arrays literais com elementos constantes ou literais que têm de ser conhecidos quando a expressão é compilada, e não quando é executada. O seguinte código cria uma matriz de tamanho 2 onde o primeiro elemento é o número (literal) 1 e o segundo a string (literal) 'aqui'.

#(1 'here') size >>> 2

Agora, se você executar a expressão #(1+2), você não obtém um array com um único elemento 3, mas em vez disso obtém o array #(1 #+2), ou seja, com três elementos: 1, o símbolo #+ e o número 2.

#(1+2) >>>  #(1 #+ 2)

Isto ocorre porque a construção #() não executa as expressões que ela contém. Os elementos são apenas objetos que são criados ao analisar a expressão (chamados objetos literais). A expressão é escaneada e os elementos resultantes são inseridos em um novo array. Os arrays literais contêm numbers',nil', true',false', symbols',strings' e outros arrays literais. Durante a execução das expressões #(), não há mensagens enviadas.

Criação dinâmica com { . }

Finalmente, você pode criar um array dinâmico utilizando a construção { . }. A expressão { a . b } é totalmente equivalente a Array with: a with: b. Isto significa, em particular, que as expressões contidas entre { e } são executadas (ao contrário do caso de #()).

{1+2} >>> #(3)

{(1/2) asFloat} at: 1 >>> 0.5

{10 atRandom. 1/3} at: 2 >>> (1/3)

Acesso aos elementos

Elementos de todas as coleções sequenciais podem ser acessados com mensagens at: anIndexe at: anIndex put: anObject.

| anArray |  
anArray := #(1 2 3 4 5 6) copy. 
anArray at: 3 >>> 3  
anArray at: 3 put: 33.  
anArray at: 3  
>>> 33

Cuidado: o princípio geral é que os arrays literais não devem ser modificados! Os arrays literais são mantidos em frames literais do método compilado (um espaço onde os literais que aparecem em um método são armazenados), portanto, a menos que você copie o array, a segunda vez que você executar o código seu array literal pode não ter o valor que você espera. No exemplo, sem copiar o array, na segunda vez, o literal #(1 2 3 4 5 6) será na verdade #(1 2 33 4 5 6)! Os arrays dinâmicos não têm este problema porque não são armazenados em frames literais.

1.7 OrderedCollection

OrderedCollection é uma das coleções que podem crescer, e a qual elementos podem ser acrescentados seqüencialmente. Ela oferece uma variedade de mensagens tais como add:, addFirst:, addLast:, e addAll:.

| ordCol |

ordCol := OrderedCollection new.
ordCol add: 'Seaside'; add: 'SmalltalkHub'; addFirst: 'GitHub'.
ordCol
>>> anOrderedCollection('GitHub' 'Seaside' 'SmalltalkHub')

Remoção de elementos

A mensagem remove: anObject remove a primeira ocorrência de um objeto da coleção. Se a coleção não incluir tal objeto, ela lança um erro.

ordCol add: 'GitHub'. 
ordCol remove: 'GitHub'.
ordCol
>>> anOrderedCollection('Seaside' 'SmalltalkHub' 'GitHub')

Há uma variante de remove: denominada remove:ifAbsent: que permite especificar como segundo argumento um bloco que é executado no caso de o elemento a ser removido não estar na coleção.

result := ordCol remove: 'zork' ifAbsent: [33]. 
result >>> 33

Conversão

É possível obter uma OrderedCollection de uma Array (ou qualquer outra coleção) enviando a mensagem asOrderedCollection:

#(1 2 3) asOrderedCollection
>>> an OrderedCollection(1 2 3) 

'hello' asOrderedCollection  
>>> an OrderedCollection($h $e $l $l $o)

1.8 Intervalo

A classe Interval representa intervalos de números. Por exemplo, o intervalo de números de 1 a 100 é definido da seguinte forma:

 Interval from: 1 to: 100 >>> (1 to: 100)

O resultado de printString revela que a classe Number nos fornece um método de conveniência chamado to: para gerar intervalos':

(Interval from: 1 to: 100) = (1 to: 100) >>> true

Podemos utilizar Interval class>>from:to:by: ou Number>>to:by: para especificar o passo (step) entre dois números como segue:

(Interval from: 1 to: 100 by: 0.5) size >>> 199

(1 to: 100 by: 0.5) at: 198 >>> 99.5

 (1/2 to: 54/7 by: 1/3) last >>> (15/2)

1.9 Dictionary

Os dicionários são importantes coleções cujos elementos são acessados através de chaves. Entre as mensagens mais utilizadas no dicionário você encontrará at: aKey, at: aKey put: aValue, at: aKey ifAbsent: aBlock, keys e values.

| colors |  
colors := Dictionary new.  
colors at: #yellow put: Color yellow. 
colors at: #blue put: Color blue. 
colors at: #red put: Color red. 
colors at: #yellow >>> Color yellow

 colors keys >>> #(#red #blue #yellow) 

colors values >>> {Color red . Color blue . Color yellow}

Os dicionários comparam chaves por igualdade. Duas chaves são consideradas iguais se elas retornarem verdadeiras quando comparadas utilizando =. Um bug comum e difícil de detectar é utilizar como chave um objeto cujo método = foi redefinido, mas não seu método hash. Ambos os métodos são utilizados na implementação do dicionário e quando se comparam objetos.

Em sua implementação, um Dictionary pode ser visto como consistindo de um conjunto de associações (key value) criadas utilizando a mensagem ->. Podemos criar um Dictionary a partir de uma coleção de associações, ou podemos converter um dicionário em um conjunto de associações.

| colors |  
colors := Dictionary newFrom: { 
    #blue    ->    Color blue. 
    #red    ->    Color red. 
    #yellow    ->    Color yellow }.  
colors removeKey: #blue.  
colors associations >>> {
    #yellow    ->    Color yellow. 
    #red    ->    Color red
}

1.10 IdentityDictionary

Enquanto um dicionário utiliza o resultado das mensagens = e hash para determinar se duas chaves são iguais, a classe IdentityDictionary utiliza a identidade (mensagem ==) da chave em vez de seus valores, ou seja, considera que duas chaves são iguais "somente" se forem o mesmo objeto.

Muitas vezes, Symbols são utilizados como chaves, e nesse caso é natural utilizar um IdentityDictionary, uma vez que um Symbol é garantidamente único globalmente. Se, por outro lado, suas chaves são Strings, é melhor utilizar um simples Dictionary ou você pode se meter em problemas:

a := 'foobar'.  
b := a copy.  
trouble := IdentityDictionary new. 
trouble 
    at: a put: 'a'; 
    at: b put: 'b'. 
    trouble at: a >>> 'a'

trouble at: b >>> 'b'

trouble at: 'foobar' >>> 'a'

Como a e b são objetos diferentes, eles são tratados como objetos diferentes. Curiosamente, o literal 'foobar' é atribuído apenas uma vez, é realmente o mesmo objeto que a. Você não quer que seu código dependa de um comportamento como este! Um simples Dictionary daria o mesmo valor para qualquer chave igual a 'foobar'.

Utilize apenas objetos globalmente únicos (como Symbols ou SmallIntegers) como chaves para um IdentityDictionary, e Strings (ou outros objetos) como chaves para um simples Dictionary.

Exemplo de IdentityDictionary

A expressão Smalltalk globals retorna uma instância de SystemDictionary, uma subclasse de IdentityDictionary, portanto todas as suas chaves são ByteSymbols (ByteSymbol é uma subclasse de Symbol).

Smalltalk globals keys collect: [ :each | each class ] as: Set
>>> a Set(ByteSymbol)

Aqui estamos utilizando collect:as: para especificar que a coleção de resultados seja da classe Set, dessa forma coletamos cada tipo de classe utilizada como uma chave apenas uma vez.

1.11 Set

A classe Set é uma coleção que se comporta como um conjunto matemático, ou seja, como uma coleção sem elementos duplicados e sem qualquer ordem. Em um Set, os elementos são adicionados utilizando a mensagem add: e não podem ser acessados utilizando a mensagem at:. Os objetos colocados em um conjunto devem implementar os métodos hash e =.

s := Set new.  
s 
    add: 4/2; 
    add: 4; 
    add: 2. 
s size >>> 2

Você também pode criar conjuntos utilizando Set class>>newFrom: ou a mensagem de conversão Collection>>asSet:

(Set newFrom: #( 1 2 3 1 4 )) = #(1 2 3 4 3 2 1) asSet >>> true

A mensagem asSet nos oferece uma maneira conveniente de eliminar duplicatas de uma coleção:

{ 
    Color black. 
    Color white. 
    (Color red + Color blue + Color green) 
} asSet size
>>> 2

Note: red + blue + green = white.

Um Bag é muito parecido com um Set, exceto que ele permite duplicatas:

{ 
    Color black. 
    Color white. 
    (Color red + Color blue + Color green) 
} asBag size
>>> 3

O conjunto de operações union, intersection e membership são implementados pelas mensagens de Collection union:, intersection:, e includes:. O receptor é primeiramente convertido em um Set, de modo que estas operações funcionam para todos os tipos de coleções!

(1 to: 6) union: (4 to: 10) >>> a Set(1 2 3 4 5 6 7 8 9 10). 

'hello' intersection: 'there' >>> 'eh'.

#Pharo includes: $a >>> true.

Como explicamos abaixo, os elementos de um conjunto são acessados usando iteradores (ver seção 1.14).

1.12 SortedCollection

Em contraste com uma OrderedCollection, uma SortedCollection mantém seus elementos ordenados. Por padrão, uma coleção ordenada utiliza a mensagem <= para estabelecer a ordem, para que possa ordenar instâncias de subclasses da classe abstrata Magnitude, que define o protocolo de objetos comparáveis (<, =, >, >=, between:and:...). (Ver Capítulo: Classes Básicas).

Você pode criar uma SortedCollection criando uma nova instância e adicionando elementos a ela:

SortedCollection new 
    add: 5; add: 2; add: 50; 
    add: -10; yourself. 
>>> a SortedCollection(-10 2 5 50)

Mais geralmente, porém, a mensagem de conversão asSortedCollection será enviada a uma coleção existente:

#(5 2 50 -10) asSortedCollection 
>>> a SortedCollection(-10 2 5 50)

'hello' asSortedCollection  
>>> a SortedCollection($e $h $l $l $o)

Como você obtém uma String de volta com este resultado? Infelizmente, a asString retorna a representação printString, que não é o que nós queremos:

'hello' asSortedCollection asString  
>>> 'a SortedCollection($e $h $l $l $o)'

A resposta correta é utilizar String class>>newFrom:, String class>>withAll: ou Object>>as:.

'hello' asSortedCollection as: String >>> 'ehllo'

String newFrom: 'hello' asSortedCollection >>> 'ehllo'

String withAll: 'hello' asSortedCollection >>> 'ehllo'

É possível ter diferentes tipos de elementos em uma SortedCollection, desde que todos sejam comparáveis. Por exemplo, podemos misturar diferentes tipos de números, tais como integers, floats e fractions:

{ 5 . 2/ -3 . 5.21 } asSortedCollection 
>>> a SortedCollection((-2/3) 5 5.21)

Imagine que você quer ordenar objetos que não definem o método <= ou que você gostaria de ter um critério de ordenação diferente. Você pode fazer isso fornecendo um bloco de dois argumentos, chamado de bloco de ordenação, para a coleção ordenada. Por exemplo, a classe Color não é uma Magnitude e não implementa o método <=, mas podemos especificar um bloco declarando que as cores devem ser classificadas de acordo com sua luminosidade (luminance - uma medida de brilho).

col := SortedCollection  
sortBlock: [ :c1 :c2 | c1 luminance <= c2 luminance ].

col addAll: { 
    Color red. 
    Color yellow. 
    Color white. 
    Color black 
}. 
col  
>>> a SortedCollection(
    Color black 
    Color red 
    Color yellow 
    Color white
)

1.13 Strings

No Pharo, uma String é uma coleção de Characters. É sequenciável, indexável, mutável e homogênea, contendo apenas instâncias de Character. Assim como os Arrays, as Strings têm uma sintaxe dedicada, e normalmente são criados especificando diretamente um String literal dentro de aspas simples, mas os métodos usuais de criação de coleções também funcionarão.

'Hello' >>> 'Hello'

String with: $A >>> 'A'

String with: $h with: $i with: $! >>> 'hi!'

String newFrom: #($h $e $l $l $o) >>> 'hello'

Na verdade, String é abstrata. Quando instanciamos um String, na verdade, obtemos um 8-bit ByteString ou uma 32-bit WideString. Para manter as coisas simples, geralmente ignoramos a diferença e apenas falamos de exemplos de String.

Enquanto as strings são delimitadas por aspas simples ('), uma string pode conter uma única aspas simples ('): para definir uma string com uma única aspas simples ('), devemos digitá-la duas vezes (''). Note que a string conterá apenas um elemento e não dois, como mostrado abaixo:

'l''idiot' at: 2 >>> $'

'l''idiot' at: 3 >>> $i

A mensagem , concatena duas instâncias de String. Tais mensagens podem ser encadeadas da seguinte forma:

s := 'no', ' ', 'worries'. 
s >>> 'no worries'

Já que uma string é uma coleção mutável, também podemos mudá-la utilizando a mensagem at:put:. Do ponto de vista do design, é melhor evitar a mutação de strings, uma vez que as strings são freqüentemente compartilhadas na execução de métodos.

s 
    at: 4 put: $h; 
    at: 5 put: $u. 
s >>> 'no hurries'

Note que o método de vírgula (,) é definido por Collection, portanto funcionará para qualquer tipo de coleção!

(1 to: 3), '45' >>> #(1 2 3 $4 $5)

Também podemos modificar uma string existente utilizando replaceAll:with: ou replaceFrom:to:with: como mostrado abaixo. Observe que o número de caracteres e o intervalo devem ter o mesmo tamanho.

s replaceAll: $n with: $N. 
s >>> 'No hurries'

s replaceFrom: 4 to: 5 with: 'wo'. 
s >>> 'No worries'

Ao contrário dos métodos descritos acima, o método copyReplaceAll: cria uma nova string. (Curiosamente, aqui os argumentos são substrings e não caracteres individuais, e seus tamanhos não têm que combinar).

s copyReplaceAll: 'rries' with: 'mbats' >>> 'No wombats'

Uma rápida análise da implementação desses métodos revela que eles são definidos não apenas para Strings, mas para qualquer tipo de SequenceableCollection, portanto, o seguinte também funciona:

(1 to: 6) copyReplaceAll: (3 to: 5) with: { 'three' . 'etc.' } 
>>> #(1 2 'three' 'etc.' 6)

String matching

É possível perguntar se um padrão corresponde a uma string, enviando a mensagem match:. O padrão pode utilizar * para corresponder a uma série arbitrária de caracteres e # para corresponder a um único caractere. Note que match: é enviado para o padrão e não para a string a ser correspondida.

'Linux *' match: 'Linux mag' >>> true

'GNU#Linux #ag' match: 'GNU/Linux tag' >>> true

Facilidades mais avançadas de correspondência de padrões também estão disponíveis no pacote Regex.

Substrings

Para manipulação de substring, podemos utilizar mensagens como first, first:, allButFirst:, copyFrom:to: e outras, definidas em `SequenceableCollection'.

'alphabet' at: 6 >>> $b

'alphabet' first >>> $a

'alphabet' first: 5 >>> 'alpha'

'alphabet' allButFirst: 3 >>> 'habet'

'alphabet' copyFrom: 5 to: 7 >>> 'abe'

'alphabet' copyFrom: 3 to: 3 >>> 'p' (not $p)

Esteja ciente de que o tipo de resultado pode ser diferente, dependendo do método utilizado. A maioria dos métodos relacionados a substring retornam instâncias de String. Mas as mensagens que sempre retornam um elemento da coleção String, retornam um Character por exemplo (por exemplo, 'alphabet' at: 6 returns the character $b). Para obter uma lista completa das mensagens relacionadas a substring, consulte a classe SequenceableCollection (especialmente o protocolo accessing).

Alguns predicados em strings

Os exemplos a seguir ilustram a utilização de isEmpty, includes: e anySatisfy: que também são mensagens definidas não apenas em Strings, mas mais geralmente nas coleções (Collections).

'Hello' isEmpty >>> false

'Hello' includes: $a >>> false

'JOE' anySatisfy: [ :c | c isLowercase ] >>> false

'Joe' anySatisfy: [ :c | c isLowercase ] >>> true

String templating

Há três mensagens que são úteis para gerenciar os string templating: format:, expandMacros, e expandMacrosWith:.

'{1} is {2}' format: {'Pharo' . 'cool'} >>> 'Pharo is cool'

As mensagens da família expandMacros oferecem substituição de variáveis, utilizando <n> para carriage return, <t> para tabulação, <1s>, <2s>, <3s> para argumentos (<1p>, <2p>, envolve a string com aspas simples), e <1?value1:value2> para condicional.

'look-<t>-here' expandMacros 
>>> 'look- -here'

'<1s> is <2s>' expandMacrosWith: 'Pharo' with: 'cool' 
>>> 'Pharo is cool'

'<2s> is <1s>' expandMacrosWith: 'Pharo' with: 'cool' 
>>> 'cool is Pharo'

'<1p> or <1s>' expandMacrosWith: 'Pharo' with: 'cool' 
>>> '''Pharo'' or Pharo'

'<1?Quentin:Thibaut> plays' expandMacrosWith: true 
>>> 'Quentin plays'

'<1?Quentin:Thibaut> plays' expandMacrosWith: false 
>>> 'Thibaut plays'

Alguns outros métodos úteis

A classe String oferece inúmeros outros métodos úteis, incluindo as mensagens asLowercase, asUppercase e capitalized.

'XYZ' asLowercase >>> 'xyz'

'xyz' asUppercase >>> 'XYZ'

'tintin' capitalized >>> 'Tintin'

'Tintin' uncapitalized >>> 'tintin'

'this sentence is without a doubt far too long' contractTo: 20 
>>> 'this sent...too long'

asString vs. printString

Observe que geralmente há uma diferença entre pedir a um objeto sua representação em string, enviando a mensagem printString e convertê-la em string, enviando a mensagem asString. Aqui está um exemplo da diferença.

#ASymbol printString >>> '#ASymbol'

#ASymbol asString >>> 'ASymbol'

Um símbolo (Symbol) é semelhante a uma string, mas é garantido que é globalmente único. Por esta razão, os símbolos são preferidos às strings como chaves para dicionários, em particular para instâncias de IdentityDictionary. Veja também o Capítulo: Basic Classes para saber mais sobre String e Symbol.

1.14 Iteradores de coleção

Em Pharo loops e condicionais são simplesmente mensagens enviadas para coleções ou outros objetos como integers ou blocks (ver também o capítulo: "Entendendo a sintaxe de mensagens"). Além das mensagens de baixo nível, tais como to:do: que avalia um bloco com um argumento que varia de um número inicial a um número final, a hierarquia de coleção oferece vários iteradores de alto nível. A utilização desses iteradores tornará seu código mais robusto e compacto.

Iterador (do:)

O método do: é o iterador básico de coleções. Ele aplica seu argumento (um bloco tomando um único argumento) a cada elemento do receptor. O seguinte exemplo imprime todas as strings contidas no receptor para o transcript.

#('bob' 'joe' 'toto') do: [:each | Transcript show: each; cr].

Variantes de iteradores

Há muitas variantes de do:, tais como do:without:, doWithIndex: e reverseDo:.

Para as coleções indexadas (Array, OrderedCollection, SortedCollection) a mensagem doWithIndex: também dá acesso ao índice corrente. Esta mensagem está relacionada a to:do: que é definida na classe Number.

#('bob' 'joe' 'toto')  
doWithIndex: [ :each :i | (each = 'joe') ifTrue: [ ^ i ] ]
>>> 2

Para ordered collections, a mensagem reverseDo: percorre a coleção na ordem inversa.

O código a seguir mostra uma mensagem interessante: do:separatedBy: que executa o segundo bloco apenas entre dois elementos.

| res |  
res := ''.  
#('bob' 'joe' 'toto')

do: [ :e | res := res, e ]

separatedBy: [ res := res, '.' ]. 
res >>> 'bob.joe.toto'

Observe que este código não é especialmente eficiente, pois cria strings intermediárias e seria melhor usar uma string de escrita (write stream) como buffer para o resultado (ver Capítulo: Streams):

String streamContents: [ :stream |  
    #('bob' 'joe' 'toto') asStringOn: stream delimiter: '.' 
]
>>> 'bob.joe.toto'

Dicionários

Quando a mensagem do: é enviada a um dicionário, os elementos levados em conta são os valores e não as associações. As mensagens apropriadas a serem usadas são keysDo:, valuesDo:, e associationsDo:, que iteram respectivamente em keys, values ou associations.

colors := Dictionary newFrom: { 
    #yellow -> Color yellow. 
    #blue -> Color blue. 
    #red -> Color red 
}.

colors keysDo: [ :key | Transcript show: key; cr ].  
colors valuesDo: [ :value | Transcript show: value; cr ]. 
colors associationsDo: [:value | Transcript show: value; cr ].

1.15 Coletando resultados (collect:)

Se você quiser aplicar uma função aos elementos de uma coleção e obter uma nova coleção com os resultados, em vez de utilizar do:, provavelmente é melhor utilizar collect:, ou um dos outros métodos de iteradores. A maioria deles pode ser encontrada no protocolo enumerating de Collection e suas subclasses.

Imagine que queremos uma coleção contendo o dobro dos elementos de uma outra coleção. Utilizando o método do: devemos escrever o seguinte:

| double |  
double := OrderedCollection new.  
#(1 2 3 4 5 6) do: [ :e | double add: 2 * e ]. double  
>>> an OrderedCollection(2 4 6 8 10 12)

A mensagem collect: executa seu bloco de argumentos para cada elemento e retorna uma nova coleção contendo os resultados. Utilizando collect: ao invés do:, o código é muito mais simples:

#(1 2 3 4 5 6) collect: [ :e | 2 * e ]
>>> #(2 4 6 8 10 12)

As vantagens do collect: sobre o do: são ainda mais importantes no exemplo a seguir, onde pegamos uma coleção de inteiros e geramos como resultado uma coleção de valores absolutos desses inteiros:

aCol:= #(2-3 4 -35 4 -11).  
result := aCol species new: aCol size. 
1 to: aCol size do: [ :each |
    result at: each put: (aCol at: each) abs 
]. 
result >>> #(2 3 4 35 4 11)

Contraste o acima exposto com a expressão muito mais simples que se segue:

#( 2 -3 4 -35 4 -11) collect: [ :each | 
    each abs 
] 
>>> #(2 3 4 35 4 11)

Uma outra vantagem da segunda solução é que ela também funcionará para sets e bags. Geralmente você deve evitar utilizar do:, a menos que você queira enviar mensagens para cada um dos elementos de uma coleção.

Note que o envio da mensagem collect: retorna o mesmo tipo de coleção que o receptor. Por este motivo, o seguinte código falha. (Uma string não pode conter valores inteiros).

'abc' collect: [ :ea | ea asciiValue ] 
>>> "error!"

Em vez disso, devemos primeiro converter a string em um Array ou uma OrderedCollection:

'abc' asArray collect: [ :ea | ea asciiValue ] 
>>> #(97 98 99)

Na verdade, collect: não é garantido devolver uma coleção exatamente da mesma classe do receptor, mas apenas da mesma species. No caso de um Interval, a species é um Array!

 (1 to: 5) collect: [ :ea | ea * 2 ]
 >>> #(2 4 6 8 10)

1.16 Selecionando e rejeitando elementos

A mensagem select: devolve os elementos do receptor que satisfazem uma condição particular:

 (2 to: 20) select: [ :each | each isPrime ]
 >>> #(2 3 5 7 11 13 17 19)

A mensagem reject: faz o contrário:

(2 to: 20) reject: [ :each | each isPrime ]
>>> #(4 6 8 9 10 12 14 15 16 18 20)

Identificação de um elemento com detecção:

A mensagem detect: devolve o primeiro elemento do receptor que condiz com o argumento do bloco.

'through' detect: [ :each | each isVowel ] 
>>> $o

A mensagem detect:ifNone: é uma variante do método detect:. Seu segundo bloco é avaliado quando não há nenhum elemento que corresponda ao bloco.

Smalltalk globals allClasses  
    detect: [:each | '*cobol*' match: each asString] 
    ifNone: [ nil ]
>>> nil

Acumulando resultados com inject:into:

Linguagens funcionais de programação freqüentemente fornecem uma função higher-order chamada fold ou reduce para acumular um resultado aplicando iterativamente algum operador binário sobre todos os elementos de uma coleção. No Pharo, isto é feito por Collection>>inject:into:.

O primeiro argumento é um valor inicial, e o segundo argumento é um bloco de dois argumentos que é aplicado ao resultado até este ponto, e cada elemento por sua vez.

Uma aplicação trivial de inject:into: é produzir a soma de uma coleção de números. No Pharo, poderíamos escrever esta expressão para somar os primeiros 100 números inteiros:

(1 to: 100) inject: 0 into: [ :sum :each | sum + each ]
>>> 5050

Outro exemplo é o seguinte bloco de um único argumento que calcula os fatores:

factorial := [ :n | (1 to: n)
    inject: 1
    into: [ :product :each | product * each ] ]. 

factorial value: 10  
>>> 3628800

1.17 Outras mensagens high-order

Há muitas outras mensagens iteradoras. Você pode verificar a classe Collection. Aqui está uma seleção.

count: A mensagem count: devolve o número de elementos que satisfazem uma condição. A condição é representada como um bloco booleano.

Smalltalk globals allClasses  count: [ :each | 
    'Collection*' match: each asString 
]
>>> 10

includes: A mensagem includes: verifica se o argumento está contido na coleção.

| colors |  
colors := {
    Color white . 
    Color yellow . 
    Color blue . 
    Color orange
}. 
colors includes: Color blue.  
>>> true

anySatisfy: A mensagem anySatisfy: responde true se pelo menos um elemento da coleção satisfaz a condição representada pelo argumento.

colors anySatisfy: [ :c | c red > 0.5 ] 
>>> true

1.18 Um erro comum: usar o resultado de add:

O seguinte erro é um dos erros mais freqüentes do Smalltalk.

| collection |  
collection := OrderedCollection new 
    add: 1; add: 2. 
collection >>> 2

Aqui a variável collection não contém a coleção recém-criada, mas sim o último número adicionado. Isto é porque o método add: devolve o elemento adicionado e não o receptor.

O código a seguir produz o resultado esperado:

| collection |  
collection := OrderedCollection new. 
collection 
    add: 1; 
    add: 2. 
collection >>> an OrderedCollection(1 2)

Você também pode utilizar a mensagem yourself para devolver o receptor de uma cascata de mensagens:

| collection |  
collection := OrderedCollection new 
    add: 1; 
    add: 2; 
    yourself 
>>> an OrderedCollection(1 2)

1.19 Um erro comum: Remoção de um elemento durante a iteração

Outro erro que você pode cometer é remover um elemento de uma coleção sobre a qual você está presentemente fazendo iteração. Os bugs criados, mas tais erros podem ser realmente difíceis de detectar porque a ordem de iteração pode ser alterada, dependendo da estratégia de armazenamento da coleção.

| range |  
range := (2 to: 20) asOrderedCollection. 
range do: [ :aNumber | 
    aNumber isPrime
        ifFalse: [ range remove: aNumber ] 
].
range >>> "error!"

A solução é copiar a coleção antes de iterar.

| range |  
range := (2 to: 20) asOrderedCollection. 
range copy do: [ :aNumber | 
    aNumber isPrime
        ifFalse: [ range remove: aNumber ] 
]. 
>>> an OrderedCollection(2 3 5 7 11 13 17 19)

1.20 Um erro comum: Redefinição = mas não de hash

Um erro difícil de detectar é quando se redefine = mas não hash. Os sintomas são que você perderá elementos que você coloca em conjuntos ou outro comportamento estranho. Uma solução proposta por Kent Beck é utilizar bitXor: para redefinir hash. Suponhamos que queremos que dois livros sejam considerados iguais se seus títulos e autores forem os mesmos. Então redefiniremos não apenas = mas também hash como se segue:

Lista 1-3 Redefinição = e hash.

Book >> = aBook  
    self class = aBook class ifFalse: [ ^ false ].  
    ^ title = aBook title and: [ authors = aBook authors ]

Book >> hash
    ^ title hash bitXor: authors hash

Outro problema desagradável surge se você utiliza um objeto mutável, ou seja, um objeto que pode mudar seu valor hash ao longo do tempo, como um elemento de um Set ou como uma chave de um Dictionary. Não faça isso a menos que você goste de depuração!

1.21 Resumo do capítulo

A hierarquia de coleções fornece um vocabulário comum para manipular uniformemente uma variedade de tipos diferentes de coleções.

  • Uma distinção chave é entre SequenceableCollections, que mantêm seus elementos em uma determinada ordem, Dictionary e suas subclasses, que mantêm associações de chave para valor, e Sets e Bags, que são desordenadas.
  • Você pode converter a maioria das coleções em outro tipo de coleção, enviando-lhes as mensagens asArray, asOrderedCollection, etc...
  • Para ordenar uma coleção, envie a mensagem asSortedCollection.
  • #( ... ) cria arrays contendo apenas objetos literais (ou seja, objetos criados sem enviar mensagens).
  • { ... } cria arrays dinâmicos utilizando uma forma compacta.
  • Um Dictionary compara as chaves por igualdade. É mais útil quando as chaves são instâncias de String. Um IdentityDictionary utiliza a identidade do objeto para comparar chaves. É mais adequado quando Symbols são utilizados como chaves, ou quando se mapeia referências de objetos a valores.
  • As Strings também entendem as mensagens habituais de coleção. Além disso, um String suporta uma forma simples de correspondência de padrões. Para uma aplicação mais avançada, veja antes o pacote RegEx.
  • A mensagem básica de iteração é do:. Ela é útil para códigos imperativos, que pode modificar cada elemento de uma coleção, ou enviar uma mensagem a cada elemento.
  • Em vez de utilizar do:, é mais comum utilizar collect:, select:, reject:, includes:, inject:into: e outras mensagens de nível superior para processar coleções de uma maneira uniforme.
  • Nunca remova um elemento de uma coleção sobre a qual você está iterando. Se você tiver que modificá-lo, itere sobre uma cópia em seu lugar.
  • Se você fizer o override de =, lembre-se de fazer também o override de hash!