Pergunta

A maneira .NET 1.0 de criar coleção de números inteiros (por exemplo) foi:

ArrayList list = new ArrayList();
list.Add(i);          /* boxing   */
int j = (int)list[0]; /* unboxing */

A penalidade de usar isso é a falta de segurança e desempenho devido ao boxe e Unboxing.

A maneira .NET 2.0 é usar genéricos:

List<int> list = new List<int>();
list.Add(i);
int j = list[0];

O preço do boxe (para o meu entendimento) é a necessidade de criar um objeto na pilha, copiar o número inteiro alocado para o novo objeto e vice-versa para unboxing.

Como o uso de genéricos supera isso? O número inteiro alocado pela pilha permanece na pilha e é apontado para a pilha (acho que não é esse o caso por causa do que acontecerá quando sairá do escopo)? Parece que ainda há necessidade de copiá -lo em outro lugar da pilha.

O que realmente está acontecendo?

Foi útil?

Solução

Quando se trata de coleções, os genéricos possibilitam evitar o boxe/unboxing utilizando T[] matrizes internamente. List<T> por exemplo, usa um T[] Array para armazenar seu conteúdo.

o variedade, é claro, é um tipo de referência e, portanto, é (na versão atual do CLR, yada yada) armazenada na pilha. Mas já que é um T[] e não um object[], os elementos da matriz podem ser armazenados "diretamente": isto é, eles ainda estão na pilha, mas eles estão na pilha na matriz Em vez de ser encaixotado e ter a matriz, contém referências às caixas.

Então, para um List<int>, por exemplo, o que você teria na matriz "pareceria" assim:

[ 1 2 3 ]

Compare isso com um ArrayList, que usa um object[] e, portanto, "olharia" algo assim:

[ *a *b *c ]

...Onde *a, etc. são referências a objetos (números inteiros em caixa):

*a -> 1
*b -> 2
*c -> 3

Desculpe essas ilustrações grosseiras; Espero que você saiba o que quero dizer.

Outras dicas

Sua confusão é resultado de entender mal qual é o relacionamento entre a pilha, a pilha e as variáveis. Aqui está a maneira correta de pensar sobre isso.

  • Uma variável é um local de armazenamento que possui um tipo.
  • A vida útil de uma variável pode ser curta ou longa. Por "curto", queremos dizer "até que a função atual retorne ou jogue" e por "longa" queremos dizer "possivelmente mais que isso".
  • Se o tipo de variável for um tipo de referência, o conteúdo da variável é uma referência a um local de armazenamento de longa duração. Se o tipo de variável for um tipo de valor, o conteúdo da variável é um valor.

Como detalhe de implementação, um local de armazenamento que é garantido para ter curta duração pode ser alocado na pilha. Um local de armazenamento que pode ter vida há muito tempo é alocado na pilha. Observe que isso não diz nada sobre "os tipos de valor são sempre alocados na pilha". Os tipos de valor são não Sempre alocado na pilha:

int[] x = new int[10];
x[1] = 123;

x[1] é um local de armazenamento. É de vida longa; Pode viver mais do que esse método. Portanto, deve estar na pilha. O fato de ele ter um int é irrelevante.

Você diz corretamente por que um int em caixa é caro:

O preço do boxe é a necessidade de criar um objeto na pilha, copiar o número inteiro alocado para o novo objeto e vice-versa para unboxing.

Onde você dá errado é dizer "a pilha alocada inteira". Não importa onde o número inteiro foi alocado. O que importa foi que seu armazenamento continha o número inteiro, em vez de conter uma referência a um local de heap. O preço é a necessidade de criar o objeto e fazer a cópia; Esse é o único custo que é relevante.

Então, por que uma variável genérica não é cara? Se você possui uma variável do tipo T, e t é construído para ser int, você terá uma variável de tipo int, período. Uma variável do tipo int é um local de armazenamento e contém um int. Se esse local de armazenamento está na pilha ou o heap é completamente irrelevante. O que é relevante é que o local de armazenamento contém um int, em vez de conter uma referência a algo na pilha. Como o local de armazenamento contém um INT, você não precisa obter os custos de boxe e unboxing: alocando um novo armazenamento na pilha e copiando o INT para o novo armazenamento.

Agora está claro?

Os genéricos permitem que a matriz interna da lista seja digitada int[] em vez de efetivamente object[], que exigiria boxe.

Aqui está o que acontece sem genéricos:

  1. Você chama Add(1).
  2. O número inteiro 1 é encaixotado em um objeto, que exige que um novo objeto seja construído na pilha.
  3. Este objeto é passado para ArrayList.Add().
  4. O objeto em caixa está recheado em um object[].

Existem três níveis de indireção aqui: ArrayList -> object[] -> object -> int.

Com genéricos:

  1. Você chama Add(1).
  2. O INT 1 é passado para List<int>.Add().
  3. O int é recheado em um int[].

Portanto, existem apenas dois níveis de indireção: List<int> -> int[] -> int.

Algumas outras diferenças:

  • O método não genérico exigirá uma soma de 8 ou 12 bytes (um ponteiro, um int) para armazenar o valor, 4/8 em uma alocação e 4 no outro. E isso provavelmente será mais devido ao alinhamento e preenchimento. O método genérico exigirá apenas 4 bytes de espaço na matriz.
  • O método não genérico requer alocar um int; O método genérico não. Isso é mais rápido e reduz a rotatividade do GC.
  • O método não genérico requer elencos para extrair valores. Isso não é TypeAfe e é um pouco mais lento.

Uma lista de Arrays apenas lida com o tipo object Então, para usar esta aula, exige fundição de e para object. No caso de tipos de valor, esse fundição envolve boxe e unboxing.

Quando você usa uma lista genérica, o compilador produz código especializado para esse tipo de valor para que o valores reais são armazenados na lista em vez de uma referência a objetos que contêm os valores. Portanto, nenhum boxe é necessário.

O preço do boxe (para o meu entendimento) é a necessidade de criar um objeto na pilha, copiar o número inteiro alocado para o novo objeto e vice-versa para unboxing.

Eu acho que você está assumindo que os tipos de valor são sempre instanciados na pilha. Este não é o caso - eles podem ser criados na pilha, na pilha ou nos registros. Para mais informações sobre isso, consulte o artigo de Eric Lippert: A verdade sobre os tipos de valor.

No .NET 1, quando o Add O método é chamado:

  1. O espaço é alocado na pilha; Uma nova referência é feita
  2. O conteúdo do i A variável é copiada na referência
  3. Uma cópia da referência é colocada no final da lista

No .NET 2:

  1. Uma cópia da variável i é passado para o Add método
  2. Uma cópia dessa cópia é colocada no final da lista

Sim o i A variável ainda é copiada (afinal, é um tipo de valor e os tipos de valor são sempre copiados - mesmo que sejam apenas parâmetros de método). Mas não há cópia redundante feita na pilha.

Por que você está pensando em termos de WHERE Os valores objetos são armazenados? Nos tipos de valor C#, podem ser armazenados na pilha e na pilha, dependendo do que o CLR escolhe.

Onde os genéricos fazem a diferença é WHAT é armazenado na coleção. No caso de ArrayList A coleção contém referências a objetos em caixa, onde List<int> contém int valores.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top