Quando você cria uma instância de uma classe com o new
operador, a memória é alocada no heap. Quando você cria uma instância de uma estrutura com o new
operador onde a memória é alocada, no heap ou na pilha?
Quando você cria uma instância de uma classe com o new
operador, a memória é alocada no heap. Quando você cria uma instância de uma estrutura com o new
operador onde a memória é alocada, no heap ou na pilha?
Respostas:
Ok, vamos ver se posso deixar isso mais claro.
Em primeiro lugar, Ash está certo: a questão não é sobre onde as variáveis de tipo de valor são alocadas. Essa é uma pergunta diferente - e uma para a qual a resposta não está apenas "na pilha". É mais complicado do que isso (e tornado ainda mais complicado pelo C # 2). Eu tenho um artigo sobre o tópico e o expandirei, se solicitado, mas vamos lidar apenas com o new
operador.
Em segundo lugar, tudo isso realmente depende do nível que você está falando. Estou vendo o que o compilador faz com o código-fonte, em termos de IL que ele cria. É mais do que possível que o compilador JIT faça coisas inteligentes em termos de otimizar bastante a alocação "lógica".
Em terceiro lugar, estou ignorando os genéricos, principalmente porque na verdade não sei a resposta, e em parte porque isso complicaria demais as coisas.
Finalmente, tudo isso é apenas com a implementação atual. A especificação C # não especifica muito disso - é efetivamente um detalhe de implementação. Há quem acredite que os desenvolvedores de código gerenciado realmente não deveriam se importar. Não tenho certeza se chegaria tão longe, mas vale a pena imaginar um mundo onde, de fato, todas as variáveis locais residem na pilha - o que ainda estaria em conformidade com as especificações.
Existem duas situações diferentes com o new
operador nos tipos de valor: você pode chamar um construtor sem parâmetros (por exemplo new Guid()
) ou um construtor com parâmetros (por exemplo new Guid(someString)
). Estes geram IL significativamente diferente. Para entender o porquê, você precisa comparar as especificações C # e CLI: de acordo com C #, todos os tipos de valor têm um construtor sem parâmetros. De acordo com a especificação da CLI, nenhum tipo de valor possui construtores sem parâmetros. (Busque os construtores de um tipo de valor com reflexão algum tempo - você não encontrará um sem parâmetro.)
Faz sentido para o C # tratar a "inicializar um valor com zeros" como um construtor, porque mantém a linguagem consistente - você pode pensar new(...)
em sempre chamar um construtor. Faz sentido para a CLI pensar de maneira diferente, pois não há código real a ser chamado - e certamente não há código específico ao tipo.
Também faz diferença o que você fará com o valor depois de inicializá-lo. O IL usado para
Guid localVariable = new Guid(someString);
é diferente da IL usada para:
myInstanceOrStaticVariable = new Guid(someString);
Além disso, se o valor for usado como um valor intermediário, por exemplo, um argumento para uma chamada de método, as coisas serão ligeiramente diferentes novamente. Para mostrar todas essas diferenças, aqui está um pequeno programa de teste. Não mostra a diferença entre variáveis estáticas e variáveis de instância: a IL seria diferente entre stfld
e stsfld
, mas é tudo.
using System;
public class Test
{
static Guid field;
static void Main() {}
static void MethodTakingGuid(Guid guid) {}
static void ParameterisedCtorAssignToField()
{
field = new Guid("");
}
static void ParameterisedCtorAssignToLocal()
{
Guid local = new Guid("");
// Force the value to be used
local.ToString();
}
static void ParameterisedCtorCallMethod()
{
MethodTakingGuid(new Guid(""));
}
static void ParameterlessCtorAssignToField()
{
field = new Guid();
}
static void ParameterlessCtorAssignToLocal()
{
Guid local = new Guid();
// Force the value to be used
local.ToString();
}
static void ParameterlessCtorCallMethod()
{
MethodTakingGuid(new Guid());
}
}
Aqui está o IL da classe, excluindo bits irrelevantes (como nops):
.class public auto ansi beforefieldinit Test extends [mscorlib]System.Object
{
// Removed Test's constructor, Main, and MethodTakingGuid.
.method private hidebysig static void ParameterisedCtorAssignToField() cil managed
{
.maxstack 8
L_0001: ldstr ""
L_0006: newobj instance void [mscorlib]System.Guid::.ctor(string)
L_000b: stsfld valuetype [mscorlib]System.Guid Test::field
L_0010: ret
}
.method private hidebysig static void ParameterisedCtorAssignToLocal() cil managed
{
.maxstack 2
.locals init ([0] valuetype [mscorlib]System.Guid guid)
L_0001: ldloca.s guid
L_0003: ldstr ""
L_0008: call instance void [mscorlib]System.Guid::.ctor(string)
// Removed ToString() call
L_001c: ret
}
.method private hidebysig static void ParameterisedCtorCallMethod() cil managed
{
.maxstack 8
L_0001: ldstr ""
L_0006: newobj instance void [mscorlib]System.Guid::.ctor(string)
L_000b: call void Test::MethodTakingGuid(valuetype [mscorlib]System.Guid)
L_0011: ret
}
.method private hidebysig static void ParameterlessCtorAssignToField() cil managed
{
.maxstack 8
L_0001: ldsflda valuetype [mscorlib]System.Guid Test::field
L_0006: initobj [mscorlib]System.Guid
L_000c: ret
}
.method private hidebysig static void ParameterlessCtorAssignToLocal() cil managed
{
.maxstack 1
.locals init ([0] valuetype [mscorlib]System.Guid guid)
L_0001: ldloca.s guid
L_0003: initobj [mscorlib]System.Guid
// Removed ToString() call
L_0017: ret
}
.method private hidebysig static void ParameterlessCtorCallMethod() cil managed
{
.maxstack 1
.locals init ([0] valuetype [mscorlib]System.Guid guid)
L_0001: ldloca.s guid
L_0003: initobj [mscorlib]System.Guid
L_0009: ldloc.0
L_000a: call void Test::MethodTakingGuid(valuetype [mscorlib]System.Guid)
L_0010: ret
}
.field private static valuetype [mscorlib]System.Guid field
}
Como você pode ver, existem muitas instruções diferentes usadas para chamar o construtor:
newobj
: Aloca o valor na pilha, chama um construtor parametrizado. Utilizado para valores intermediários, por exemplo, para atribuição a um campo ou uso como argumento de método.call instance
: Usa um local de armazenamento já alocado (na pilha ou não). Isso é usado no código acima para atribuir a uma variável local. Se a mesma variável local recebe um valor várias vezes usando várias new
chamadas, ela apenas inicializa os dados por cima do valor antigo - ela não aloca mais espaço de pilha a cada vez.initobj
: Usa um local de armazenamento já alocado e apenas limpa os dados. Isso é usado para todas as nossas chamadas de construtor sem parâmetros, incluindo aquelas atribuídas a uma variável local. Para a chamada do método, uma variável local intermediária é efetivamente introduzida e seu valor é apagado por initobj
.Espero que isso mostre o quão complicado o tópico é, enquanto brilha um pouco de luz ao mesmo tempo. Em alguns aspectos conceituais, toda chamada new
aloca espaço na pilha - mas, como vimos, não é o que realmente acontece, mesmo no nível de IL. Eu gostaria de destacar um caso em particular. Tome este método:
void HowManyStackAllocations()
{
Guid guid = new Guid();
// [...] Use guid
guid = new Guid(someBytes);
// [...] Use guid
guid = new Guid(someString);
// [...] Use guid
}
Isso "logicamente" tem 4 alocações de pilha - uma para a variável e uma para cada uma das três new
chamadas - mas, de fato (para esse código específico), a pilha é alocada apenas uma vez e, em seguida, o mesmo local de armazenamento é reutilizado.
EDIT: Só para esclarecer, isso só é verdade em alguns casos ... em particular, o valor de guid
não será visível se o Guid
construtor lançar uma exceção, e é por isso que o compilador C # é capaz de reutilizar o mesmo slot de pilha. Consulte a postagem do blog de Eric Lippert sobre construção de tipo de valor para obter mais detalhes e um caso em que não se aplica.
Aprendi muito ao escrever esta resposta - peça esclarecimentos se algo não estiver claro!
List<Guid>
e adiciona esses 3 a ele? Seriam 3 alocações (mesma IL)? Mas eles são mantidos em algum lugar mágico
guid
foi apenas meia-substituídas, já que não será de qualquer maneira visível.
A memória que contém os campos de uma estrutura pode ser alocada na pilha ou na pilha, dependendo das circunstâncias. Se a variável do tipo struct for uma variável local ou parâmetro que não é capturada por alguma classe de representante ou iterador anônimo, ela será alocada na pilha. Se a variável fizer parte de alguma classe, ela será alocada dentro da classe no heap.
Se a estrutura estiver alocada no heap, não é necessário chamar o novo operador para alocar a memória. O único objetivo seria definir os valores do campo de acordo com o que estiver no construtor. Se o construtor não for chamado, todos os campos obterão seus valores padrão (0 ou nulo).
Da mesma forma para estruturas alocadas na pilha, exceto que o C # exige que todas as variáveis locais sejam definidas com algum valor antes de serem usadas, portanto, você deve chamar um construtor personalizado ou o construtor padrão (um construtor que não usa parâmetros está sempre disponível para estruturas).
Em termos compactos, new é um nome impróprio para estruturas, chamando new simplesmente chama o construtor. O único local de armazenamento para a estrutura é o local definido.
Se for uma variável de membro, ela será armazenada diretamente no que for definido, se for uma variável ou parâmetro local, será armazenada na pilha.
Compare isso com as classes, que têm uma referência onde quer que a estrutura fosse armazenada na íntegra, enquanto a referência aponta para algum lugar na pilha. (Membro dentro, local / parâmetro na pilha)
Pode ajudar a olhar um pouco para C ++, onde não há distinção real entre classe / estrutura. (Existem nomes semelhantes no idioma, mas eles se referem apenas à acessibilidade padrão das coisas) Quando você chama de novo, recebe um ponteiro para o local da pilha, enquanto que se você tiver uma referência que não seja de ponteiro, ele é armazenado diretamente na pilha ou dentro do outro objeto, ala structs em C #.
Como em todos os tipos de valor, as estruturas sempre vão para onde foram declaradas .
Veja esta pergunta aqui para obter mais detalhes sobre quando usar estruturas. E esta pergunta aqui para mais algumas informações sobre estruturas.
Edit: Eu tinha respondido obscankely que eles sempre vão na pilha. Isto está incorreto .
Provavelmente estou perdendo alguma coisa aqui, mas por que nos preocupamos com alocação?
Os tipos de valor são passados por value;) e, portanto, não podem ser alterados em um escopo diferente daquele em que são definidos. Para poder alterar o valor, você deve adicionar a palavra-chave [ref].
Os tipos de referência são passados por referência e podem ser alterados.
É claro que existem tipos de referências imutáveis, sendo as mais populares.
Layout / inicialização da matriz: Tipos de valor -> memória zero [nome, zip] [nome, zip] Tipos de referência -> memória zero -> nulo [ref] [ref]
Uma declaração class
ou struct
é como um blueprint usado para criar instâncias ou objetos em tempo de execução. Se você definir uma class
ou struct
chamada Pessoa, Pessoa é o nome do tipo. Se você declarar e inicializar uma variável p do tipo Person, p é considerado um objeto ou instância de Person. Várias instâncias do mesmo tipo de Pessoa podem ser criadas e cada instância pode ter valores diferentes em properties
e fields
.
A class
é um tipo de referência. Quando um objeto do class
é criado, a variável à qual o objeto está atribuído mantém apenas uma referência a essa memória. Quando a referência do objeto é atribuída a uma nova variável, a nova variável se refere ao objeto original. As alterações feitas através de uma variável são refletidas na outra variável porque ambas se referem aos mesmos dados.
A struct
é um tipo de valor. Quando a struct
é criado, a variável à qual struct
é atribuído retém os dados reais da estrutura. Quando o struct
é atribuído a uma nova variável, ele é copiado. A nova variável e a variável original, portanto, contêm duas cópias separadas dos mesmos dados. As alterações feitas em uma cópia não afetam a outra cópia.
Em geral, classes
são usados para modelar comportamentos mais complexos ou dados que devem ser modificados após a class
criação de um objeto. Structs
são mais adequados para pequenas estruturas de dados que contêm principalmente dados que não devem ser modificados após a struct
criação.
Praticamente as estruturas consideradas tipos de valor são alocadas na pilha, enquanto os objetos são alocados na pilha, enquanto a referência de objeto (ponteiro) é alocada na pilha.
As estruturas são alocadas para a pilha. Aqui está uma explicação útil:
Além disso, as classes quando instanciadas no .NET alocam memória no heap ou no espaço de memória reservado do .NET. Considerando que as estruturas produzem mais eficiência quando instanciadas devido à alocação na pilha. Além disso, deve-se notar que a passagem de parâmetros dentro de estruturas é feita por valor.