Este artigo mostrará o básico dos tipos internals, como é claro um exemplo no qual a memória para o tipo de referência será alocada completamente na pilha (isso porque eu sou um programador de pilha completa).

Isenção de responsabilidade
Este artigo não contém material que deve ser usado em projetos reais. É simplesmente uma extensão dos limites em que uma linguagem de programação é percebida.
Antes de prosseguir com a história, recomendo vivamente que leia o primeiro post sobre o
StructLayout , porque há um exemplo que será usado neste artigo (no entanto, como sempre).
Pré-história
Começando a escrever o código deste artigo, eu queria fazer algo interessante usando a linguagem assembly. Eu queria, de alguma forma, quebrar o modelo de execução padrão e obter um resultado realmente incomum. E lembrando com que frequência as pessoas dizem que o tipo de referência difere dos tipos de valor, pois os primeiros estão localizados na pilha e os segundos estão na pilha, decidi usar um assembler para mostrar que o tipo de referência pode viver no pilha. No entanto, comecei a encontrar todos os tipos de problemas, por exemplo, retornando o endereço e sua apresentação como um link gerenciado (ainda estou trabalhando nele). Então comecei a trapacear e a fazer algo que não funciona na linguagem assembly, em c #. E no final, não havia montador.
Leia também a recomendação - se você estiver familiarizado com o layout dos tipos de referência, recomendo pular a teoria sobre eles (apenas o básico será fornecido, nada de interessante).
Um pouco sobre os internos dos tipos (para estruturas antigas, agora algumas compensações são alteradas, mas o esquema geral é o mesmo)
Gostaria de lembrar que a divisão da memória em uma pilha e uma pilha ocorre no nível do .NET, e essa divisão é puramente lógica; fisicamente, não há diferença entre as áreas de memória sob a pilha e a pilha. A diferença de produtividade é fornecida apenas por algoritmos diferentes de trabalho com essas duas áreas.
Então, como alocar memória na pilha? Para começar, vamos entender como esse tipo de referência misterioso é organizado e o que tem, esse tipo de valor não possui.
Portanto, considere o exemplo mais simples com a classe Employee.
Código Funcionáriopublic class Employee { private int _id; private string _name; public virtual void Work() { Console.WriteLine(“Zzzz...”); } public void TakeVacation(int days) { Console.WriteLine(“Zzzz...”); } public static void SetCompanyPolicy(CompanyPolicy policy) { Console.WriteLine("Zzzz..."); } }
E vamos dar uma olhada em como é apresentado na memória.
Essa classe é considerada no exemplo de um sistema de 32 bits.

Assim, além da memória para os campos, temos mais dois campos ocultos - o índice do bloco de sincronização (título da palavra do cabeçalho do objeto na figura) e o endereço da tabela de métodos.
O primeiro campo (o índice do bloco de sincronização) realmente não nos interessa. Ao colocar o tipo, decidi pular. Eu fiz isso por dois motivos:
- Sou muito preguiçoso (não disse que os motivos serão razoáveis)
- Para a operação básica do objeto, este campo não é obrigatório.
Mas como já começamos a conversar, acho certo dizer algumas palavras sobre esse campo. É usado para diferentes finalidades (código hash, sincronização). Em vez disso, o próprio campo é simplesmente um índice de um dos blocos de sincronização associados ao objeto especificado. Os próprios blocos estão localizados na tabela de blocos de sincronização (algo como matriz global). Criar um bloco como esse é uma operação bastante grande, portanto, ele não será criado se não for necessário. Além disso, ao usar bloqueios finos, o identificador do segmento que recebeu o bloqueio (em vez do índice) será gravado lá.
O segundo campo é muito mais importante para nós. Graças à tabela de métodos de tipos, é possível uma ferramenta tão poderosa como o polimorfismo (que, a propósito, estruturas, empilham reis, não possuem).
Suponha que a classe Employee adicionalmente implemente três interfaces: IComparable, IDisposable e ICloneable.
Em seguida, a tabela de métodos será mais ou menos assim.

A imagem é muito legal, tudo é mostrado e tudo é claro. Em resumo, o método virtual não é chamado diretamente pelo endereço, mas pelo deslocamento na tabela de métodos. Na hierarquia, os mesmos métodos virtuais estarão localizados no mesmo deslocamento na tabela de métodos. Ou seja, na classe base, chamamos o método por deslocamento, sem saber qual tipo de tabela de método será usado, mas sabendo que esse deslocamento será o método mais relevante para o tipo de tempo de execução.
Também vale lembrar que a referência do objeto aponta apenas para o ponteiro da tabela de métodos.
Exemplo há muito esperado
Vamos começar com aulas que nos ajudarão em nosso objetivo. Usando StructLayout (eu realmente tentei sem ele, mas não deu certo), escrevi mapeadores simples - ponteiros para tipos gerenciados e vice-versa. Obter um ponteiro de um link gerenciado é muito fácil, mas a transformação inversa me causou dificuldades e, sem pensar duas vezes, apliquei meu atributo favorito. Para manter o código em uma chave, feita em duas direções de uma maneira.
Código dos mapeadores // Provides the signatures we need public class PointerCasterFacade { public virtual unsafe T GetManagedReferenceByPointer<T>(int* pointer) => default(T); public virtual unsafe int* GetPointerByManagedReference<T>(T managedReference) => (int*)0; } // Provides the logic we need public class PointerCasterUnderground { public virtual T GetManagedReferenceByPointer<T>(T reference) => reference; public virtual unsafe int* GetPointerByManagedReference<T>(int* pointer) => pointer; } [StructLayout(LayoutKind.Explicit)] public class PointerCaster { public PointerCaster() { pointerCaster= new PointerCasterUnderground(); } [FieldOffset(0)] private PointerCasterUnderground pointerCaster; [FieldOffset(0)] public PointerCasterFacade Caster; }
Primeiro, escrevemos um método que leva um ponteiro para alguma memória (não necessariamente na pilha, por sinal) e configura o tipo.
Para simplificar a localização do endereço da tabela de métodos, crio um tipo na pilha. Estou certo de que a tabela de métodos pode ser encontrada de outras maneiras, mas não me propus a otimizar esse código; era mais interessante para mim torná-lo compreensível. Além disso, usando os conversores descritos anteriormente, obtemos um ponteiro para o tipo criado.
Este ponteiro aponta exatamente para a tabela de métodos. Portanto, basta obter o conteúdo da memória para a qual aponta. Este será o endereço da tabela de métodos.
E como o ponteiro passado para nós é uma espécie de referência de objeto, também devemos escrever o endereço da tabela de métodos exatamente para onde ela aponta.
Na verdade, é tudo. De repente, certo? Agora nosso tipo está pronto. Pinóquio, que nos alocou memória, cuidará de inicializar os campos ele mesmo.
Resta apenas usar nosso lançador ultra-mega para converter o ponteiro em um link gerenciado.
public class StackInitializer { public static unsafe T InitializeOnStack<T>(int* pointer) where T : new() { T r = new T(); var caster = new PointerCaster().Caster; int* ptr = caster.GetPointerByManagedReference(r); pointer[0] = ptr[0]; T reference = caster.GetManagedReferenceByPointer<T>(pointer); return reference; } }
Agora temos um link na pilha que aponta para a mesma pilha, onde, de acordo com todas as leis dos tipos de referência (bem, quase), reside um objeto construído a partir de terra preta e paus. Polimorfismo está disponível.
Deve-se entender que, se você passar esse link fora do método, depois de retornar dele, obteremos algo pouco claro. Sobre chamadas de métodos virtuais e fala não pode ser, a exceção ocorrerá. Os métodos normais são chamados diretamente, o código terá apenas endereços para métodos reais, portanto eles funcionarão. E no lugar dos campos haverá ... e ninguém sabe o que estará lá.
Como é impossível usar um método separado para inicialização na pilha (como o quadro da pilha será substituído após o retorno do método), o método que deseja aplicar o tipo na pilha deve alocar memória. A rigor, existem algumas maneiras de fazer isso. Mas o mais adequado para nós é o
stackalloc . Apenas a palavra-chave perfeita para nossos propósitos. Infelizmente, ele traz o
inseguro no código. Antes disso, havia uma idéia de usar o Span para esses fins e sem código inseguro. No código inseguro, não há nada de ruim, mas, como em todo lugar, não é uma bala de prata e possui suas próprias áreas de aplicação.
Então, depois de receber o ponteiro para a memória na pilha atual, passamos esse ponteiro para o método que compõe o tipo em partes. Foi tudo o que ouviu - muito bem.
unsafe class Program { public static void Main() { int* pointer = stackalloc int[2]; var a = StackInitializer.InitializeOnStack<StackReferenceType>(pointer); a.StubMethod(); Console.WriteLine(a.Field); Console.WriteLine(a); Console.Read(); } }
Você não deve usá-lo em projetos reais, o método de alocação de memória na pilha usa o novo T (), que por sua vez usa a reflexão para criar um tipo no heap! Portanto, esse método será mais lento do que a criação usual do tipo de tempos, em 40-50. Além disso, não é multiplataforma.
Aqui você pode encontrar o projeto inteiro.
Fonte: no guia teórico, foram utilizados exemplos do livro Sasha Goldstein - Pro .NET Performace