Arrays em AdvPL – Parte 01

Introdução

O Array (ou Matriz) no AdvPL é um tipo básico de dado que permite criar listas de tamanho e conteúdo dinâmico. É muito útil e prático trabalhar com arrays em AdvPL. Neste post, vamos ver tudo — ou quase tudo — que podemos fazer com um Array.

Criando um Array

Como o AdvPL padrão possui tipagem de variável dinâmica, criamos um array atribuindo ele a uma variável. Declaramos um array vazio usando abertura e fechamento de chaves  { }. Para declarar um array com conteúdo, basta informar o conteúdo de cada elemento dentro das chaves, e separar os elementos por vírgula. Vide exemplos abaixo:

Local aNumVazio := {}
Local aPrimos := {2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97}
Local aNomes := {"José","João"}

Nos exemplos mencionados, aNumVazio foi inicializado como um array sem elementos. Já o array aPrimos contém os números primos menores que 100, e o Array aNomes contém duas strings. Cada array criado como uma lista com elementos do mesmo tipo é conhecido como “Array de Dimensão Única”, ou um Array simples.

Cada elemento de um array pode conter elementos de qualquer tipo do AdvPL, inclusive os tipos “B” (CodeBlock), “O” (Objeto), “U” (NIL — ou Nulo), inclusive o tipo “A” (Array). Porém, existem ressalvas quanto a alguns tipos e algumas composições com Arrays, que veremos mais para a frente.

O conteúdo de cada elemento do array pode ser endereçado diretamente pela sua posição ordinal no Array, bastando para isso colocar em frente ao array a posição desejada entre [ ] (colchetes). Os Arrays em AdvPL começam na posição 1.

conout(aPrimos[1]) // deve mostrar o numero 2
conout(aPrimos[6]) // deve mostrar o numero 13
conout(aPrimos[26]) // deve dar erro de execução "Array out of bounds"

Se um array possui 25 elementos, como é o caso do aPrimos, eles são endereçados do elemento 1 ao 25. Caso eu tente acessar um elemento menor ou igual a zero, ou maior que o tamanho — ou número de elementos — do Array, a aplicação AdvPL é interrompida com o erro fatal de execução “Array out of bounds” — ou seja, tentativa de acesso de um elemento fora dos limites do Array.

Mais para frente veremos como acrescentar, remover e realizar diversas operações com um ou mais elementos de um array, usando funções básicas da linguagem AdvPL. Neste momento vamos focar no Array em si. Praticamente todos os exemplos deste tópico vão utilizar arrays com conteúdo declarado.

Arrays de Arrays

Um elemento de um Array pode ser outro Array. Isto permite criarmos array de múltiplas dimensões. Por exemplo, imagine uma lista de códigos numéricos, onde cada número possui uma descrição — por exemplo uma lista de cores. Podemos criar um array onde cada um dos elementos é um outro array, contendo a cor e seu nome.

aCores := { {0,"Preto"}, {1,"Azul"} , {2,"Verde"} }

Para endereçar os elementos do array, usamos a posição do elemento entre colchetes, certo? Logo, se acessarmos aCores[1], estamos acessando o array que está dentro da primeira posição do aCores — { 0 , “Preto” }. Se quisermos acessar um elemento de dentro desse array, podemos fazer de duas formas:

conout(aCores[1][1]) // Deve mostrar o numero 0 (zero)
conout(aCores[1][2]) // Deve mostrar a string "Preto"
conout(aCores[3][2]) // Deve mostrar a string "verde"

A segunda forma é, ao invés de usar colchetes em seguida de colchetes, podemos apenas usar uma abertura e fechamento de colchetes, e colocar os números dos elementos separados por vírgula. No caso, partindo do exemplo acima, temos:

conout(aCores[1,1]) // Deve mostrar o numero 0 (zero)
conout(aCores[1,2]) // Deve mostrar a string "preto"
conout(aCores[3,2]) // Deve mostrar a string "verde"

Como a estrutura do array é dinâmica, podemos criar arrays com mais sub-níveis. Por exemplo, imagine um array contendo uma lista de remédios, onde cada remédio é um array contendo o código do remédio, o nome dele, a quantidade de caixas em estoque, e um array de lotes, onde cada lote é um array com três posições, contendo o numero do lote, a quantidade de caixas deste lote, e a validade deste lote.

aRemedios := { { 1 , "Aspirina" , 5 , { { 21031 , 2 , "201906" } , { 21032 , 3, "201907" } } } }

Agora, sabendo que o Array têm esta estrutura, vamos varrer este array e mostrar o conteúdo dele no log de console:

For nI := 1 to len(aRemedios)
   aRem := aRemedios[nI]
   conout("Remedio ... "+cValToChaR(aRem[1])+ ;
          " : "+aRem[2] +" ( Estoque "+ cValToChaR(aRem[3])+ " )")
   For nJ := 1 to len(aRem[4])
      aLote := aRem[4][nJ]
      conout("   Lote "+cValToChar(aLote[1])+;
             " Estoque "+CValToChar(aLote[2])+" Validade "+aLote[3])
   Next
   conout('')
Next

O resultado esperado no console do Application Server é:

Remedio ... 1 : Aspirina ( Estoque 5 )
   Lote 21031 Estoque 2 Validade 201906
   Lote 21032 Estoque 3 Validade 201907

Observações importantes

Repare no exemplo de leitura do Array aRemedios, que eu criei dentro do primeiro loop uma variável chamada aRem, onde eu atribuo nela o array que está na posição atual do array aRemedios. E, ao mostrar os elementos deste array, como o quatro elemento é também um array de lotes, eu crio outra variável para acessar este conteúdo. Eu poderia ter escrito isso de outro modo, mas não o fiz por uma razão deveras interessante. Se eu tivesse escrito o programa sem criar estas variáveis intermediárias, o programa para mostrar os remédios ficaria assim:

For nI := 1 to len(aRemedios)
   conout("Remedio ... "+cValToChaR(aRemedios[nI][1])+ ;
          " : "+aRemedios[nI][2] +" ( Estoque "+ ;
          cValToChaR(aRemedios[nI][3])+ " )")
   For nJ := 1 to len(aRemedios[nI][4])
      conout(" Lote "+cValToChar(aRemedios[nI][4][nJ][1])+;
                " Estoque "+CValToChar(aRemedios[nI][4][nJ][2])+;
                " Validade "+aRemedios[nI][4][nJ][3])
   Next
   conout('')
Next

As duas formas estão corretas e vão funcionar. Porém a segunda forma será mais lenta. Vou explicar por quê: Dentro do loop eu uso a variável nI para acessar o remédio, e nJ para acessar os lotes. Cada propriedade de um determinado lote de um remédio é acessada com a expressão:

aRemedios[nI][4][nJ][1] // Codigo do lote nJ do remédio nI
aRemedios[nI][4][nJ][2] // Estoque do lote nJ do remédio nI
aRemedios[nI][4][nJ][3] // Validade do lote nJ do remédio nI

Desta forma, a primeira linha do exemplo acessa o elemento [nI] do Array aRemedios — que é um array — depois resgata o [4] elemento desse array , depois resgata o elemento [nJ] — que é outro array — de dentro desse array, e depois resgata a posição [1] desse array. Estas buscas internas para acessar o remédio, depois a lista de lotes, e depois um determinado lote, vai se repetir nas três linhas deste exemplo. Quando usamos por exemplo os arrays temporários dentro do loop, eliminamos a repetição desnecessária dessas estas etapas, vejamos o primeiro fonte de exemplo:

For nJ := 1 to len(aRem[4])
   aLote := aRem[4][nJ]
   conout(" Lote "+cValToChar(aLote[1])+;
          " Estoque "+CValToChar(aLote[2])+" Validade "+aLote[3])
Next

Ao entrar em um lote, como eu preciso de três informações deste lote, para não fazer a busca a cada elemento no array aRemedios, ou mesmo no aRem[4], o remédio atual já está na variável aRem, a lista de lotes estão em aRem[4], e cada elemento da lista é lido no array aLote. Logo, dentro do loop eu vou acessar direto os elementos de aLote[1], aLote[2] e aLote[3] para recuperar os dados do lote atual, sem precisar repetir a busca em todos os níveis caso eu usasse diretamente apenas o Array aRemedios.

Como os arrays são acessados diretamente pela posição do elemento na memória, seu acesso é muito rápido. Quando eu menciono que o segundo exemplo de código será mais lento, estamos falando de frações de milissegundo. Porém, em um código onde os loops mais internos são executados milhões de vezes, este tempo torna-se perceptível.

Características Especiais – Referência

O Tipo de dado “A” Array no AdvPL é tratado como uma variável de referência implícita. Desse modo, quando fazemos uma atribuição de uma variável que contém um array para outra variável, é criada uma referência ao array armazenado na primeira variável. Isto significa que, na memória o array é único, e agora eu tenho duas variáveis que apontam para o mesmo conteúdo. Se eu alterar o conteúdo de um elemento dentro desse array, as duas variáveis vão apontar para o mesmo conteúdo alterado. Vamos a um exemplo que é mais fácil de identificar.

aCores := { {0,"Preto"}, {1,"Azul"} , {2,"Verde"} }
aCor := aCores[1]
conout(aCor[1]) // deve mostrar 0 (zero)
conout(aCor[2]) // deve mostrar "Preto"
aCor[2] := "VERMELHO"
conout(aCores[1][2]) // deve mostrar VERMELHO

Quando criamos a variável aCor, ela passa a referenciar o primeiro elemento do array aCores. Desse modo, quando alteramos o conteúdo do segundo elemento do array aCor, isto reflete no array aCores. Veja bem, não foram dois conteúdos que foram alterados, a questão é que a referência de um array aponta para o mesmo conteúdo.

Quando passamos como parâmetro um array para uma função qualquer, é passado sempre uma referência desse array. Uma referência, em grosso modo, é como se duas variáveis apontassem para o mesmo endereço de memória onde está o conteúdo.

Se, dentro da função, fizermos alguma alteração no conteúdo do Array passado como parâmetro, estamos alterando o conteúdo do array original, no escopo da função que passou o parâmetro. Vamos refazer o exemplo acima de outra forma:

User Function aTest3()
Local aCores, aCor
aCores := { {0,"Preto"}, {1,"Azul"} , {2,"Verde"} }
aCor := aCores[1]
conout(aCor[1]) // deve mostrar 0 (zero)
conout(aCor[2]) // deve mostrar "Preto"
TrocaCor(aCor,"VERMELHO")
conout(aCores[1][2]) // deve mostrar VERMELHO 
Return

Static function TrocaCor(aArr,cVal)
aArr[2] := cVal
Return

O resultado será o mesmo, pois o array aCor foi passado implicitamente por referência para a função TrocaCor(). Se eu criar uma nova variável chamada aPaleta, e atribuir nela o conteúdo de aCores (aPaleta := aCores), a partir deste momento aPaleta é uma referência ao array aCores. Logo, ambas apontam para o mesmo conteúdo. Logo, como o conteúdo na pratica (e na memória) é o mesmo, mexer nele usando aPaleta ou aCores dá na mesma.

Quebra de Referência

Existem algumas situações onde é desejável que a referência com o array original seja quebrada. Para isso, pegamos a variável que contém o Array (não um elemento para o qual ele aponta, o Array mesmo) e atribuímos na variável um novo conteúdo. Vamos voltar ao exemplo original e fazer algumas alterações.

User Function aTest4()
Local aCores, aCor
aCores := { {0,"Preto"}, {1,"Azul"} , {2,"Verde"} }
aCor := aCores[1]
conout(aCor[1]) // deve mostrar 0 (zero)
conout(aCor[2]) // deve mostrar "Preto"
aCor := NIL
conout(aCores[1][1]) // ainda mostra 0 (zero)
conout(aCores[1][2]) // ainda mostra "Preto"
Return

aCor é uma variável que faz referência ao array contido no primeiro elemento do array aCores. Porém, quando eu atribuo nesta variável o conteúdo NIL, o AdvPL quebra a referência com o primeiro elemento do array aCores, e passa a ter o valor NIL.  Como vemos no exemplo, o array aCores não foi afetado.

Agora, quando fazemos uma atribuição endereçada especificamente a um elemento do array contido na variável, o conteúdo daquele elemento é alterado. Vamos fazer isso criando uma referência do array aCores, que vamos chamar de aPaleta.

User Function aTest5()
Local aCores, aPaleta
aCores := { {0,"Preto"}, {1,"Azul"} , {2,"Verde"} }
aPaleta := aCores
conout(aPaleta[2][1]) // deve mostrar 1 (um)
conout(aPaleta[2][2]) // deve mostrar "Azul"
aPaleta[2] := NIL
conout(valtype(aPaleta[2])) // deve mostrar "U" (Nil)
conout(valtype(aCores[2])) // também deve mostrar "U" (Nil)
Return

Neste caso, estamos fazendo uma atribuição direta a um elemento do array. O conteúdo daquela posição foi alterado, e como as duas variáveis apontam para o mesmo conteúdo … temos o resultado acima. aCores[2] e aPaleta[2] apontam para o mesmo conteúdo.

Quando queremos realizar criar uma nova variável de array, que não aponte para o conteúdo do array original, mas que crie uma cópia do conteúdo do array original e aponte para a cópia, usamos a função aClone(). No próximo post vamos ver em detalhes várias funções dedicadas à manutenção e conteúdo de arrays.

Referência circular

Ao usar Arrays com múltiplas dimensões (Arrays de Arrays) e trabalhar com referências deste Array, deve-se tomar cuidado para você não criar uma referência de um dos elementos de uma dimensão do array para uma dimensão superior. Este tipo de referência é conhecido por “referência circular”. Vamos ao exemplo mais simples:

User Function aTest6()
Local aTeste := { NIL }
aTeste[1] := aTeste
Return

Você cria o array aTeste com um elemento NIL. Agora, atribui dentro deste elemento uma referência ao próprio array. Logo, o array contém ele mesmo. Como eu disse, este é o exemplo mais simples. Se eu criar dois arrays, e fazer uma referência de um elemento para o outro, e vice-versa, eu também crio uma referência circular.

User Function aTest7()
Local aTeste1 := { NIL }
Local aTeste2 := { NIL }
aTeste1[1] := aTeste2
aTeste2[1] := aTeste1
Return

Se você criar uma referência circular, e fizer uma função para varrer os elementos de um array, e caso um deles seja um array, ao entrar em um nível inferior da busca, você está entrando em um nível superior que você já passou. Uma função recursiva de varredura dos elementos desse array com referência circular, caso não seja preparada para ligar com esta situação — marcando por exemplo cada nó visitado da estrutura — fatalmente pode ficar em LOOP por algum tempo, até esgotar o nível de recursão de chamadas do AdvPL — algo em torno de 200 chamadas empilhadas.

Conclusão

Pra molhar o bico, acho que esta introdução já têm bastante informação. Nos próximos posts, vamos começar a ver as funções básicas do AdvPL que permitem realizar manutenção dinamicamente em um Array.

Desejo a todos TERABYTES DE SUCESSO !!! 

Referências

 

 

4 respostas em “Arrays em AdvPL – Parte 01

  1. Excelente conteúdo! Me deu um norte certeiro para resolver uma demanda e com toda certeza agregou muito ao minha jornada como desenvolvedor júnior! Obrigado, Júlio!

    Curtido por 1 pessoa

Deixe um comentário