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
.
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
.
Protocol | Methods |
accessing | size, capacity, at:, at:put: |
testing | isEmpty, includes:, contains:, occurrencesOf: |
adding | add:, addAll: |
removing | remove:, remove:ifAbsent:, removeAll: |
enumerating | do:, collect:, select:, reject: detect:, detect:ifNone:, inject:into: |
converting | asBag, asSet, asOrderedCollection, asSortedCollection, asArray, asSortedCollection |
creation | with:, 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 elementofirst
e seguem em uma ordem bem definida até um elementolast
. Instâncias deSet
,Bag
eDictionary
, 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 deanArray
, eanArray at: n put: v
muda o n-ésimo elemento parav
.LinkedLists
é sequencial mas não indexável, ou seja, eles entendemfirst
elast
, mas não a mensagemat:
. - 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
eSymbols
não são. UmaInterval
é uma coleção imutável que representa uma gama deIntegers
, por exemplo,5 to: 16 by: 2
é um intervalo que contém os elementos5, 7, 9, 11, 13
e15
. É indexável com a mensagemat: anIndex
, mas não pode ser alterado com a mensagemat: anIndex put: aValue
. - Growable: Instâncias de
Interval
eArray
são sempre de um tamanho fixo. Outros tipos de coleções (sorted collections
,ordered collections
, elinked lists
) podem crescer após a criação. A classeOrderedCollection
é mais geral do queArray
; o tamanho de umaOrderedCollection
cresce sob demanda, e define mensagensaddFirst: anElement
eaddLast: anElement
, assim como mensagensat: anIndex
eat: anIndex put: aValue
. - Accepts duplicates: Um
Set
filtra as duplicatas, mas umBag
não. As classesDictionary
,Set
eBag
utilizam o método=
fornecido pelos elementos; as variantesIdentity
dessas classes utilizam o método==
, que testa se os argumentos são o mesmo objeto, e as variantesPluggable
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
ouSymbol
, no entanto, contém apenasCharacters
. UmArray
contém qualquer mistura de objetos, mas umByteArray
contém apenasBytes
. UmaLinkedList
é limitada a conter elementos que estejam de acordo com o protocolo de acessoLink
.
1.3 Implementações de coleções
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
eSortedCollections
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 dabag
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 tamanhoanInteger
cujos elementos serão todosnil
.with: anObject
cria uma coleção e adicionaanObject
à 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 índiceanInteger
.at: anInteger put: anObject
colocaanObject
no índiceanInteger
.
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: anIndex
e 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, eSets
eBags
, 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 deString
. UmIdentityDictionary
utiliza a identidade do objeto para comparar chaves. É mais adequado quandoSymbols
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, umString
suporta uma forma simples de correspondência de padrões. Para uma aplicação mais avançada, veja antes o pacoteRegEx
. - 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 utilizarcollect:
,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 ooverride
dehash
!