Revista MSDN Magazine Edição 02 - Tipos de Array no .NET
Tipos de Array no .NET
por Jeffrey Richter
Arrays, ou vetores, são mecanismos que permitem tratar vários itens como uma única coleção. O CLR (Common Language Runtime) suporta arrays unidimensionais, multidimensionais e jagged arrays (arrays de arrays). Todos os tipos de arrays derivam implicitamente de System.Array, que por sua vez deriva de System.Object. Isso significa que todos os arrays são sempre tipos de referência que são alocados no managed heap, e a variável do seu aplicativo contêm apenas uma referência ao array, e não o array propriamente dito. O exemplo a seguir ilustra melhor esse ponto.
Int32[] myIntegers; // Declara a referência do array
myIntegers = new Int32[100]; // Cria o array com 100 Int32s
Na primeira linha, myIntegers é uma variável que é capaz de apontar para um array unidimensional de Int32. Inicialmente, myIntegers será definido como nulo (null) já que eu não aloquei um array. A segunda linha mostrada acima aloca um array de 100 valores do tipo Int32; todos os Int32 são inicializados com 0. Embora os Int32 sejam tipos de valor, o bloco de memória que possui tamanho suficiente para armazenar esses valores é alocado a partir da managed heap. O bloco de memória contém 100 valores Int32 unboxed. O endereço desse bloco de memória é retornado e salvo na variável myIntegers. Você também pode criar arrays de tipos de referência conforme a abaixo.
Control[] myControls; // Declare a referência do array
myControls=new Control[50]; // Cria o array com 50 referências Control
Na primeira linha, myControls é uma variável que é capaz de apontar para um array unidimensional de referências de Control. Inicialmente, myControls será definido (setado) como nulo (null) já que eu não declarei um array. A segunda linha aloca um array de 50 referências de Control; todas essas referências são inicializadas como nulas. Como Control é um tipo de referência, a criação do array cria apenas referências; os objetos reais não são criados nessa hora. O endereço desse bloco de memória é retornado e salvo na variável myControls.
Figura 1. Arrays na Managed Heap
A Figura 1 mostra como os arrays de tipos de valor e os arrays de tipos de referência aparecem na managed heap. Veja que o array Controls está apenas alocado na memória, sem os devidos valores atribuídos. Para atribuir os controles ao array myControls, execute as seguintes linhas:
myControls[3] = myControls[2]; // 2 elementos referindo-se ao mesmo objeto
A compatibilidade com a CLS (Common Language Specification) requer que todos os arrays sejam baseados em zero (zero-based). Isso permite que um método escrito em C# crie um array e passe sua referência ao código escrito em outras linguagens, como o Visual Basic®. Além disso, como os arrays baseados em zero (zero-based) são de longe os mais comuns, a Microsoft dedicou um tempo enorme na otimização de seu desempenho. No entanto, o CLR suporta arrays baseados em valores diferentes de zero (non-zero-based), embora eles não sejam recomendados. Para aqueles que não estão preocupados com o desempenho e com a portabilidade entre linguagens, mais a frente nesta seção demonstrarei como criar e usar arrays baseados em valores diferentes de zero.
Na Figura 1, você notará que cada array possui alguma informação de overhead adicional associada a ele. Essas informações contêm a classificação do array (número de dimensões), os limites inferiores de cada dimensão do array (quase sempre 0) e o comprimento de cada dimensão. O overhead também contém o tipo de cada elemento do array. Em poucas palavras, mencionarei a seguir os métodos que permitem que você consulte essas informações do overhead.
Até agora, mostrei apenas exemplos de como criar arrays unidimensionais. Sempre que possível, você deve tentar manter-se fiel a esses tipos de array (unidimensionais, baseados em zero), também conhecidos como arrays SZ ou vetores. Faço essa recomendação porque os arrays SZ proporcionam o melhor desempenho, já que existem instruções de linguagens intermediárias (IL) específicas (como newarr, ldelem, ldelema, ldlen e stelem) para manipulá-los. No entanto, se preferir trabalhar com arrays multidimensionais, você pode fazê-lo. Veja aqui alguns exemplos:
// Cria um array de 2 dimensões do tipo Double
// Cria um array com 3 dimensões do tipo String
Conforme mencionei anteriormente, o CLR também suporta jagged arrays; os jagged arrays baseados em zero (zero-based) têm o mesmo desempenho dos arrays SZ normais. No entanto, para se acessar os elementos de um jagged array, é necessário que tenham ocorrido dois ou mais acessos a um array. Observe que os jagged arrays não são compatíveis com CLS (porque o CLS não permite que um objeto System.Array seja um elemento de um array) e, por esse motivo, não podem ser passados entre códigos escritos em diferentes linguagens de programação. Felizmente, o C# suporta jagged arrays. Veja na Listagem 1 alguns exemplos de como criar um array de polígonos, onde cada polígono consiste em um array de instâncias Point.
Listagem 1. Arrays na managed heap
// Cria um array de 1 dimensão de Point-arrays
// myPolygons[0] faz referência ao array com 10 instâncias Point
// myPolygons[1] faz referência ao array com 20 instâncias Point
// myPolygons[2] faz referência ao array com 30 instâncias Point
// Exibe os Points no segundo polygon
for (Int32 x = 0, l = myPolygons[1].Length; x < l; x++)
Console.WriteLine(yPolygons[1][x]);
Observe que o CLR sempre verifica se o índice de um array é válido. Em outras palavras, você não poderá criar um array com 100 elementos nele (numerados de 0 a 99) e, em seguida, tentar acessar o elemento no índice 100 ou -5. Essa tentativa poderá fazer com que seja gerado um System.IndexOutOfRangeException. Permitir o acesso a memória fora do intervalo do array seria uma brecha à segurança do tipo, além é claro, do CLR não permitir que código execute esse tipo de acesso.
Geralmente, a performance associada com a verificação de índice não é significativa porque o compilador JIT (just-in-time) sempre verifica os limites do array uma vez antes de um loop ser executado, em vez de cada iteração for each. No entanto, se você ainda estiver preocupado com problemas no desempenho das verificações de índice do CLR, existe a opção de usar o código não seguro (unsafe code) no C# para acessar o array.
Todos os arrays derivam implicitamente de System.Array
O tipo System.Array oferece vários membros de instância e estáticos (static members). Como todos os arrays derivam implicitamente de System.Array, esses membros podem ser usados para manipular arrays de tipos de valor ou de referência. Você também notará que o Array implementa várias interfaces: ICloneable, IEnumerable, ICollection e IList. Essas interfaces permitem que os arrays sejam convenientemente usados em muitos cenários diferentes. A Tabela 1 resume os métodos oferecidos pelo System.Array e as interfaces que ele implementa.
Tabela 1 – Métodos do System.Array
Propriedade de instância somente leitura
Retorna o número de dimensões no array.
Retorna o número de elementos em uma dimensão especificada do array.
Retorna o número total de elementos no array.
Retorna o limite inferior da dimensão especificada. Esse limite é quase sempre 0.
Retorna o limite superior da dimensão especificada. Esse limite é quase sempre o número de elementos na dimensão menos 1.
Indica se o array é somente leitura. Nos arrays, esse valor é sempre False.
Indica se o acesso do array é thread-safe. Nos arrays, esse valor é sempre False.
Recupera um objeto que pode ser usado para sincronizar o acesso ao array. Nos arrays, ele é sempre uma referência ao próprio array.
Indica se o array tem um tamanho fixo. Nos arrays, esse valor é sempre True.
Retorna uma referência ao elemento localizado na posição especificada no array. Se o array contiver tipos de valor, o valor de retorno fará referência à cópia boxed do elemento. Esse método é raramente usado e é exigido apenas quando você não sabe (em tempo de design) o número de dimensões de um array.
Posiciona o elemento no local especificado no array. Esse método é raramente usado e é exigido apenas quando você não sabe (em tempo de design) o número de dimensões de um array.
Retorna um IEnumerator para o array. Permite usar a instrução foreach do C# (ou a equivalente em outra linguagem). No caso de arrays multidimensionais, o enumerador percorre todos os elementos do array.
Classifica os elementos em um array, em dois arrays ou em uma seção do array. O tipo do elemento do array precisa implementar a interface IComparer ou passar um objeto cujo tipo implemente a interface IComparer.
Pesquisa o array especificado para o elemento especificado, usando um algoritmo de pesquisa binário. Esse método presume que os elementos do array estejam ordenados. O tipo do elemento do array precisa implementar a interface IComparer. Geralmente, você usa o método Sort antes de chamar BinarySearch.
Retorna o índice da primeira ocorrência de um valor em um array unidimensional (ou numa parte dele).
Retorna o índice da última ocorrência de um valor em um array unidimensional (ou numa parte dele).
Inverte a ordem dos elementos no array unidimensional especificado (ou numa parte dele).
Cria um novo array que é uma cópia superficial do array que o originou.
Copia os elementos de um array para o outro.
Copia uma seção de um array para outro, executando qualquer conversão necessária.
Define uma faixa de elementos no array como 0 ou como objetos de referência nulos.
Cria uma instância de um array. Esse método (raramente usado) permite que você defina dinamicamente (no runtime) arrays de qualquer tipo, classificação e limite.
Chama o construtor padrão para cada elemento em um array de tipos de valor. Esse método não fará nada se os elementos no array forem tipos de referência. Como o C# não permite que você defina construtores padrão para tipos de valor, esse método não é útil para arrays de tipos em C#. Esse método é basicamente para fornecedores de compilador.
Conversão de arrays
No caso de arrays que contêm elementos de tipo de referência, o CLR permite que você converta implicitamente o tipo de elemento de array da origem para o tipo de destino. Para que essa conversão seja bem-sucedida, os dois tipos de array precisam ter o mesmo número de dimensões e é necessário ocorrer uma conversão implícita ou explícita do tipo de elemento da origem para o tipo de elemento de destino. O CLR não permite o casting de arrays com elementos de tipo de valor em nenhum outro tipo. No entanto, com o método Array.Copy, você pode criar um novo array que possua o efeito desejado (veja a Listagem 2). O método Array.Copy é incrivelmente útil e é usado freqüentemente pela biblioteca de classes do framework (Framework Class Library) do .NET. A Listagem 3 mostra outro exemplo da utilidade de Copy.
Listagem 2 – Usando o Array.Copy
// Cria um array FileStream de 2 dimensões
FileStream[,] fs2dim = new FileStream[5, 10];
// Cast implícito para os objetos do array de 2 dimensões
Object[,] o2dim = fs2dim;
// Não pode fazer Cast de 2 para 1 array
// Compiler error CS0030: Cannot convert type 'object[*,*]' to
// 'System.IO.Stream[]'
Stream[] s1dim = (Stream[]) o2dim;
// Cast explícito para 2-dim Stream array
Stream[,] s2dim = (Stream[,]) o2dim;
// Cast explícito para 2-dim Type array
// Compila, mas o InvalidCastException é gerado em runtime
Type[,] t2dim = (Type[,]) o2dim;
// Cria um array de 1 dimensão do tipo Int32 (value types)
Int32[] i1dim = new Int32[5];
// Não pode fazer Cast do array de valor para nada mais
// Compiler error CS0030: Cannot convert type 'int[]' to 'object[]'
Object[] o1dim = (Object[]) i1dim;
// Array.Copy cria um novo array para cada elemento
// do array original conforme o tipo do array final
// O código a seguir cria um array de referência para boxed Int32s
Object[] o1dim = new Object[i1dim.Length];
Array.Copy(i1dim, o1dim, i1dim.Length);
Listagem 3 – Mais usos para Copy
// Define o tipo de valor que implementa a interface
struct MyValueType : ICloneable {
// Cria um array com 100 elementos
MyValueType[] src = new MyValueType[100];
// Cria um array de referência ICloneable
ICloneable[] dest = new ICloneable[src.Length];
// Inicializa um array de elementos ICloneable para referenciar as versões boxed
// dos elementos do array de origem
Array.Copy(src, dest, src.Length);
Passando e retornando arrays
Os arrays são sempre passados por meio de referência a um método. Como o CLR não suporta a noção de parâmetros constantes, isso significa que o método é capaz de alterar os elementos no array. Caso não deseje permitir que o método modifique os elementos, você precisará fazer uma cópia do array e passá-la para o método. Observe que o método Array.Copy faz uma cópia superficial e, desse modo, se os elementos do array forem tipos de referência, o novo array fará referência aos objetos já existentes.
Para obter uma cópia mais completa, você pode querer clonar os elementos individuais, mas isso requer que cada tipo de objeto implemente a interface ICloneable. Como alternativa, você poderia serializar cada objeto em um System.IO.MemoryStream e, em seguida, desserializar imediatamente o fluxo de memória (memory stream) para construir um novo objeto. Dependendo dos tipos de objeto, a performance dessas operações poderá ser proibitiva e, além disso, nem todos os tipos são serializáveis.
Da mesma forma, alguns métodos retornam uma referência a um array. Caso seja o próprio método que construa e inicializa o próprio array, então, não há problema em retornar uma referência a ele. No entanto, se o método quiser retornar uma referência a um array interno mantido por um campo, você precisará decidir se deseja que o chamador do método tenha acesso direto a esse array. Em caso afirmativo, basta retornar a referência ao array. Porém, na maioria das vezes você não quer que o chamador tenha acesso ao array; desse modo, o método precisa construir um novo array e chamar Array.Copy (retornando uma referência ao novo array). Novamente, você pode querer clonar cada um dos objetos antes de retornar a referência ao array.
Se você definir um método que retorne uma referência a um array e esse array não contiver nenhum elemento, o seu método poderá retornar um valor nulo ou uma referência a um array com elementos zero. Ao implementar esse tipo de método, você será totalmente encorajado a implementar o método retornando um array de comprimento zero, já que ele simplifica o código a ser escrito pelo desenvolvedor que chama o método. Por exemplo, este código roda corretamente mesmo que não exista appointments na iteração.
// Este código é fácil de escrever e entender
Appointment[] appointments = GetAppointmentsForToday();
for (Int32 a = 0, l = appointments.Length; a < l; a++) {
Da mesma forma que o código anterior, o código a seguir roda corretamente se não existir nenhum elemento do array "appointments" para iteração, mas é um pouco mais difícil de escrever e entender.
// Este código é mais difícil de escrever e entender
Se os seus métodos retornam arrays com elementos zero no lugar de nulos, os chamadores de seus métodos terão mais facilidade para trabalhar com eles. A propósito, o mesmo vale para campos que fazem referência a um array - tente sempre fazer com que o campo refira-se ao array, mesmo que este não possua nenhum elemento. Permitir que um campo seja nulo complica, sem necessidade, o uso de seu tipo.
Criando arrays com um limite inferior diferente de zero
No início deste artigo, mencionei a possibilidade de se criar (e trabalhar com) arrays que possuíssem limites inferiores diferentes de zero. Você pode criar dinamicamente seus próprios arrays, chamando o método estático CreateInstance do array. Esse método tem várias sobrecargas (overloads), mas todas elas permitem especificar o tipo dos elementos no array, o número de dimensões no array, os limites inferiores de cada dimensão e o número de elementos nelas contidos. O método CreateInstance aloca memória para o array, salva as informações sobre parâmetros na área do descritor do bloco de memória do array e retorna uma referência ao array. É possível converter (cast) a referência retornada pelo CreateInstance em uma variável, de modo a facilitar o acesso aos elementos do array. O código a seguir demonstra como criar dinamicamente um array bidimensional de valores System.Decimal.
// Temos um array 2-dim [1995..2004][1..4]
Int32[] lowerBounds = { 1995, 1 };
Decimal[,] quarterlyRevenue = (Decimal[,])
A primeira dimensão representa os anos do calendário e vai de 1995 a 2004, inclusive. A segunda dimensão representa os trimestres e vai de 1 a 4, inclusive. O código na Listagem 4 faz a iteração de todos os elementos no array dinâmico. Eu poderia ter criado rotinas específicas para os limites do array dentro do código, o que teria melhorado o desempenho. Porém, decidi usar alguns dos métodos do System.Array como os métodos GetLowerBound e GetUpperBound para fins de demonstração.
Listagem 4 – Como criar um array dinamicamente.
Int32 firstYear = quarterlyRevenue.GetLowerBound(0);
Int32 lastYear = quarterlyRevenue.GetUpperBound(0);
Console.WriteLine("{0,4} {1,9} {2,9} {3,9} {4,9}",
Console.Write(year +" ");
for (Int32 quarter = quarterlyRevenue.GetLowerBound(1);
quarter <= quarterlyRevenue.GetUpperBound(1); quarter++) {
Console.Write("{0,9:C} ", quarterlyRevenue[year, quarter]);
Acesso rápido ao array
Cada vez que o elemento de um array é acessado, o CLR assegura que o índice permaneça dentro dos limites do array. Isso evita que você acesse a memória localizada fora do array, o que pode corromper outros objetos. Se um índice inválido for usado para acessar um elemento do array, o CLR gera uma System.IndexOutOfRangeException.
Como você deve imaginar, a verificação do índice do CLR tem algum custo no desempenho. Se você confia no seu código e não se importa de recorrer a um código não seguro (unsafe code), existe uma maneira de acessar um array sem que o CLR execute a verificação de índice (veja a Listagem 5).
Listagem 5 – Acesso sem verificação de índice.
// Define um array de 5 elementos do tipo Int32
// Obtém o ponteiro do primeiro array
// Interage através de cada elemento do array
// NOTA: O código a seguir tem um bug!
Para compilar esse código, você deve usar a seguinte linha de comando:
Uma vez criado esse pequeno aplicativo, sua execução produzirá os seguintes resultados:
Observe que deveriam aparecer somente cinco valores, mas, em vez disso, aparecem seis. Isso acontece porque existe um bug no código-fonte. No bloco do For, a expressão de teste deveria ser x < l, e não x <= l. Isso demonstra o quão cuidadoso você precisa ser ao usar um código não seguro.
Para fins de comparação, veja na Listagem 6 uma versão que utiliza código seguro (safe code). O código desta listagem será compilado, no entanto, na execução será gerada uma Exception (System.IndexOutOfRangeException) informando que o array está fora do escopo.
Listagem 6 –Versão que não usa unsafe code.
Console.WriteLine(arr[x]);
Se você usar ILDasm para visualizar o código IL gerado para as listagens 5 e 6, verá que a listagem 6 (type-safe) gera um código menor (os dois códigos IL estão disponíveis para download). No entanto, é a instrução ldelem da versão de tipificação segura que faz com que o CLR execute a verificação do índice. Já a versão não segura usa a instrução ldind.i4; ela simplesmente obtém um valor de 4 bytes a partir de um endereço de memória. Observe que você só pode usar técnicas não seguras para acessar um array de tipos unmanaged (não-gerenciados), que incluem SByte, Byte, Int16, UInt16, Int32, UInt32, Int64, UInt64, Char, Single, Double, Decimal, Boolean e um tipo enumerado (ou uma estrutura de tipo de valor cujos campos sejam qualquer dos tipos acima mencionados).
Redimensionando um array
O método estático CreateInstance do array permite que, durante o tempo de compilação, você construa dinamicamente um array se não souber os tipos de elemento que o array deverá manter. Ele também é útil quando você não sabe quantas dimensões o array deverá ter nem quais os limites dessas dimensões. Na seção "Criando arrays com um limite inferior diferente de zero", demonstrei como construir dinamicamente um array utilizando limites arbitrários. O método CreateInstance também pode ser usado para redimensionar um array arbitrário, veja a Listagem 7. Se você criar e executar esse aplicativo, verá a seguinte saída: