
O tipo de base do objeto e implementação de interfaces. Boxe
Parece que chegamos ao inferno e à maré alta e podemos acertar qualquer entrevista, mesmo a da equipe .NET CLR. No entanto, não vamos correr para o microsoft.com e procurar vagas. Agora, precisamos entender como os tipos de valor herdam um objeto se eles não contêm uma referência ao SyncBlockIndex, não um ponteiro para uma tabela de métodos virtuais. Isso explicará completamente nosso sistema de tipos e todas as peças de um quebra-cabeça encontrarão seus lugares. No entanto, precisaremos de mais de uma frase.
Agora, lembremos novamente como os tipos de valor são alocados na memória. Eles conseguem o lugar na memória exatamente onde estão. Os tipos de referência obtêm alocação no monte de objetos pequenos e grandes. Eles sempre dão uma referência ao local na pilha onde o objeto está. Cada tipo de valor possui métodos como ToString, Equals e GetHashCode. Eles são virtuais e substituíveis, mas não permitem herdar um tipo de valor substituindo métodos. Se os tipos de valor usassem métodos substituíveis, eles precisariam de uma tabela de métodos virtuais para rotear chamadas. Isso levaria aos problemas de passar estruturas para o mundo não gerenciado: campos extras iriam para lá. Como resultado, há descrições de métodos de tipo de valor em algum lugar, mas você não pode acessá-los diretamente através de uma tabela de métodos virtuais.
Isso pode trazer a ideia de que a falta de herança é artificial
Este capítulo foi traduzido do russo em conjunto pelo autor e por tradutores profissionais . Você pode nos ajudar com a tradução do russo ou do inglês para qualquer outro idioma, principalmente para chinês ou alemão.
Além disso, se você quiser nos agradecer, a melhor maneira de fazer isso é nos dar uma estrela no github ou no fork do repositório
github / sidristij / dotnetbook .
Isso pode trazer a ideia de que a falta de herança é artificial:
- há herança de um objeto, mas não é direta;
- existem ToString, Equals e GetHashCode dentro de um tipo base. Nos tipos de valor, esses métodos têm seu próprio comportamento. Isso significa que os métodos são substituídos em relação a um
object
; - além disso, se você converter um tipo em um
object
, terá todo o direito de chamar ToString, Equals e GetHashCode; - ao chamar um método de instância para um tipo de valor, o método obtém outra estrutura que é uma cópia de um original. Isso significa que chamar um método de instância é como chamar um método estático:
Method(ref structInstance, newInternalFieldValue)
. De fato, essa chamada passa por this
, com uma exceção, no entanto. Uma JIT deve compilar o corpo de um método, portanto, seria desnecessário compensar os campos da estrutura, saltando sobre o ponteiro para uma tabela de métodos virtuais, que não existe na estrutura. Existe para tipos de valor em outro local .
Os tipos são diferentes no comportamento, mas essa diferença não é tão grande no nível de implementação no CLR. Falaremos sobre isso um pouco mais tarde.
Vamos escrever a seguinte linha em nosso programa:
var obj = (object)10;
Isso nos permitirá lidar com o número 10 usando uma classe base. Isso é chamado de boxe. Isso significa que temos uma VMT para chamar métodos virtuais como ToString (), Equals e GetHashCode. Na realidade, o boxe cria uma cópia de um tipo de valor, mas não um ponteiro para um original. Isso ocorre porque podemos armazenar o valor original em qualquer lugar: na pilha ou como um campo de uma classe. Se o convertermos em um tipo de objeto, podemos armazenar uma referência a esse valor pelo tempo que desejarmos. Quando o boxe acontece:
- o CLR aloca espaço no heap para uma estrutura + SyncBlockIndex + VMT de um tipo de valor (para chamar ToString, GetHashCode, Equals);
- copia uma instância de um tipo de valor lá.
Agora, temos uma variante de referência de um tipo de valor. Uma estrutura possui absolutamente o mesmo conjunto de campos do sistema que um tipo de referência ,
tornando-se um tipo de referência completo após o boxe. A estrutura se tornou uma classe. Vamos chamar de cambalhota .NET. Este é um nome justo.
Veja o que acontece se você usar uma estrutura que implementa uma interface usando a mesma interface.
struct Foo : IBoo { int x; void Boo() { x = 666; } } IBoo boo = new Foo(); boo.Boo();
Quando criamos a instância Foo, seu valor vai para a pilha de fato. Em seguida, colocamos essa variável em uma variável do tipo de interface e a estrutura em uma variável do tipo de referência. Em seguida, há o boxe e temos o tipo de objeto como saída. Mas é uma variável do tipo de interface. Isso significa que precisamos de conversão de tipo. Portanto, a chamada acontece da seguinte maneira:
IBoo boo = (IBoo)(box_to_object)new Foo(); boo.Boo();
Escrever esse código não é eficaz. Você precisará alterar uma cópia em vez de um original:
void Main() { var foo = new Foo(); foo.a = 1; Console.WriteLite(foo.a); // -> 1 IBoo boo = foo; boo.Boo(); // looks like changing foo.a to 10 Console.WriteLite(foo.a); // -> 1 } struct Foo: IBoo { public int a; public void Boo() { a = 10; } } interface IBoo { void Boo(); }
Na primeira vez em que analisamos o código, não precisamos saber com o que lidamos no código que não seja o nosso e ver uma interface para a interface do IBoo. Isso nos faz pensar que Foo é uma classe e não uma estrutura. Então não há divisão visual em estruturas e classes, o que nos faz pensar o
os resultados da modificação da interface devem entrar no foo, o que não acontece, pois boo é uma cópia do foo. Isso é enganoso. Na minha opinião, esse código deve receber comentários, para que outros desenvolvedores possam lidar com ele.
A segunda coisa diz respeito aos pensamentos anteriores de que podemos converter um tipo de um objeto para o IBoo. Essa é outra prova de que um tipo de valor em caixa é uma variante de referência de um tipo de valor. Ou, todos os tipos em um sistema de tipos são tipos de referência. Podemos apenas trabalhar com estruturas como com tipos de valor, passando seu valor inteiramente. Desreferenciando um ponteiro para um objeto, como você diria no mundo do C ++.
Você pode objetar que, se fosse verdade, ficaria assim:
var referenceToInteger = (IInt32)10;
Obteríamos não apenas um objeto, mas uma referência digitada para um tipo de valor em caixa. Isso destruiria toda a idéia dos tipos de valor (isto é, integridade do valor), permitindo uma ótima otimização, com base em suas propriedades. Vamos derrubar essa idéia!
public sealed class Boxed<T> { public T Value; [MethodImpl(MethodImplOptions.AggressiveInlining)] public override bool Equals(object obj) { return Value.Equals(obj); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public override string ToString() { return Value.ToString(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public override int GetHashCode() { return Value.GetHashCode(); } }
Temos um análogo completo do boxe. No entanto, podemos alterar seu conteúdo chamando métodos de instância. Essas alterações afetarão todas as partes com uma referência a essa estrutura de dados.
var typedBoxing = new Boxed<int> { Value = 10 }; var pureBoxing = (object)10;
A primeira variante não é muito atraente. Em vez de lançar um tipo, criamos bobagens. A segunda linha é muito melhor, mas as duas linhas são quase idênticas. A única diferença é que não há limpeza de memória com zeros durante o encaixe usual após alocar memória no heap. A estrutura necessária retira a memória imediatamente, enquanto a primeira variante precisa de limpeza. Isso faz com que ele trabalhe mais do que o boxe usual em 10%.
Em vez disso, podemos chamar alguns métodos para o nosso valor em caixa.
struct Foo { public int x; public void ChangeTo(int newx) { x = newx; } } var boxed = new Boxed<Foo> { Value = new Foo { x = 5 } }; boxed.Value.ChangeTo(10); var unboxed = boxed.Value;
Temos um novo instrumento. Vamos pensar no que podemos fazer com isso.
- Nosso tipo
Boxed<T>
faz o mesmo que o tipo usual: aloca memória na pilha, passa um valor para lá e permite obtê-lo, fazendo uma espécie de unbox; - Se você perder uma referência a uma estrutura em caixa, o GC a coletará;
- No entanto, agora podemos trabalhar com um tipo de caixa, ou seja, chamando seus métodos;
- Além disso, podemos substituir uma instância de um tipo de valor no SOH / LOH por outra. Não podíamos fazer isso antes, pois teríamos que desembalar, mudar a estrutura para outra e voltar ao boxe, dando uma nova referência aos clientes.
O principal problema do boxe é criar tráfego na memória. O tráfego de um número desconhecido de objetos, cuja parte pode sobreviver até a geração um, onde temos problemas com a coleta de lixo. Haverá muito lixo e poderíamos ter evitado. Mas quando temos o tráfego de objetos de vida curta, a primeira solução é o pool. Esse é o fim ideal do salto mortal do .NET.
var pool = new Pool<Boxed<Foo>>(maxCount:1000); var boxed = pool.Box(10); boxed.Value=70; // use boxed value here pool.Free(boxed);
Agora o boxe pode funcionar usando um pool, o que elimina o tráfego de memória durante o boxe. Podemos até fazer os objetos voltarem à vida no método de finalização e se colocar de volta na piscina. Isso pode ser útil quando uma estrutura em caixa passa para código assíncrono que não seja o seu e você não consegue entender quando se tornou desnecessário. Nesse caso, ele retornará ao pool durante o GC.
Vamos concluir:
- Se o boxe for acidental e não acontecer, não faça acontecer. Isso pode levar a problemas com o desempenho.
- Se o boxe for necessário para a arquitetura de um sistema, pode haver variantes. Se o tráfego de estruturas em caixas for pequeno e quase invisível, você poderá usar o boxe. Se o tráfego estiver visível, convém fazer o agrupamento do boxe, usando uma das soluções mencionadas acima. Ele gasta alguns recursos, mas faz o GC funcionar sem sobrecarga;
Por fim, vejamos um código totalmente impraticável:
static unsafe void Main() { // here we create boxed int object boxed = 10; // here we get the address of a pointer to a VMT var address = (void**)EntityPtr.ToPointerWithOffset(boxed); unsafe { // here we get a Virtual Methods Table address var structVmt = typeof(SimpleIntHolder).TypeHandle.Value.ToPointer(); // change the VMT address of the integer passed to Heap into a VMT SimpleIntHolder, turning Int into a structure *address = structVmt; } var structure = (IGetterByInterface)boxed; Console.WriteLine(structure.GetByInterface()); } interface IGetterByInterface { int GetByInterface(); } struct SimpleIntHolder : IGetterByInterface { public int value; int IGetterByInterface.GetByInterface() { return value; } }
O código usa uma função pequena, que pode obter um ponteiro de uma referência a um objeto. A biblioteca está disponível no endereço do github . Este exemplo mostra que o boxe usual transforma int em um tipo de referência digitado. Vamos lá
veja as etapas do processo:
- Faça boxe para um número inteiro.
- Obter o endereço de um objeto obtido (o endereço do Int32 VMT)
- Obter a VMT de um SimpleIntHolder
- Substitua a VMT de um número inteiro em caixa pela VMT de uma estrutura.
- Transforme unboxing em um tipo de estrutura
- Exiba o valor do campo na tela, obtendo o Int32, que foi
encaixotado.
Faço-o através da interface de propósito, pois quero mostrar que funcionará
dessa maneira.
Anulável \ <T>
Vale mencionar sobre o comportamento do boxe com tipos de valor Nullable. Esse recurso dos tipos de valor Nullable é muito atraente, pois o boxe de um tipo de valor que é uma espécie de nulo retorna nulo.
int? x = 5; int? y = null; var boxedX = (object)x; // -> 5 var boxedY = (object)y; // -> null
Isso nos leva a uma conclusão peculiar: como null não tem um tipo, o
A única maneira de obter um tipo diferente do in a box é o seguinte:
int? x = null; var pseudoBoxed = (object)x; double? y = (double?)pseudoBoxed;
O código funciona apenas porque você pode converter um tipo para o que quiser
com nulo.
Aprofundando no boxe
Como parte final, gostaria de falar sobre o tipo System.Enum . Logicamente, esse deve ser um tipo de valor, pois é uma enumeração usual: alternar números para nomes em uma linguagem de programação. No entanto, System.Enum é um tipo de referência. Todos os tipos de dados enum, definidos em seu campo e no .NET Framework, são herdados de System.Enum. É um tipo de dados de classe. Além disso, é uma classe abstrata, herdada de System.ValueType
.
[Serializable] [System.Runtime.InteropServices.ComVisible(true)] public abstract class Enum : ValueType, IComparable, IFormattable, IConvertible { // ... }
Isso significa que todas as enumerações são alocadas no SOH e, quando as usamos, sobrecarregamos o heap e o GC? Na verdade não, pois apenas as usamos. Então, supomos que haja um conjunto de enumerações em algum lugar e apenas obtemos suas instâncias. Não de novo Você pode usar enumerações em estruturas durante o empacotamento. Enumerações são números comuns.
A verdade é que o CLR corta a estrutura do tipo de dados ao formá-la se houver enum transformando uma classe em um tipo de valor :
// Check to see if the class is a valuetype; but we don't want to mark System.Enum // as a ValueType. To accomplish this, the check takes advantage of the fact // that System.ValueType and System.Enum are loaded one immediately after the // other in that order, and so if the parent MethodTable is System.ValueType and // the System.Enum MethodTable is unset, then we must be building System.Enum and // so we don't mark it as a ValueType. if(HasParent() && ((g_pEnumClass != NULL && GetParentMethodTable() == g_pValueTypeClass) || GetParentMethodTable() == g_pEnumClass)) { bmtProp->fIsValueClass = true; HRESULT hr = GetMDImport()->GetCustomAttributeByName(bmtInternal->pType->GetTypeDefToken(), g_CompilerServicesUnsafeValueTypeAttribute, NULL, NULL); IfFailThrow(hr); if (hr == S_OK) { SetUnsafeValueClass(); } }
Por que fazer isso? Em particular, como a idéia de herança - para fazer uma enum personalizada, você, por exemplo, precisa especificar os nomes dos possíveis valores. No entanto, é impossível herdar tipos de valor. Assim, os desenvolvedores o projetaram para ser um tipo de referência que pode transformá-lo em um tipo de valor quando compilado.
E se você quiser ver o boxe pessoalmente?
Felizmente, você não precisa usar um desmontador e entrar na selva de códigos. Temos os textos de todo o núcleo da plataforma .NET e muitos deles são idênticos em termos do .NET Framework CLR e CoreCLR. Você pode clicar nos links abaixo e ver a implementação do boxe imediatamente:
Aqui, o único método é usado para descompactar:
JIT_Unbox (..) , que é um invólucro em torno de JIT_Unbox_Helper (..) .
Além disso, é interessante que ( https://stackoverflow.com/questions/3743762/unboxing-does-not-create-a-copy-of-the-value-is-this-right ), unboxing não significa copiar dados para a pilha. Boxe significa passar um ponteiro para a pilha enquanto testa a compatibilidade dos tipos. O código de operação da IL após o unboxing definirá as ações com esse endereço. Os dados podem ser copiados para uma variável local ou a pilha para chamar um método. Caso contrário, teríamos uma cópia dupla; primeiro ao copiar do heap para algum lugar e depois para o local de destino.
Perguntas
Por que o .NET CLR não pode fazer pool para o boxe em si?
Se conversarmos com qualquer desenvolvedor Java, saberemos duas coisas:
- Todos os tipos de valor em Java são em caixa, o que significa que não são essencialmente tipos de valor. Inteiros também são in a box.
- Por motivos de otimização, todos os números inteiros de -128 a 127 são obtidos do conjunto de objetos.
Então, por que isso não acontece no .NET CLR durante o boxe? É simples Como podemos alterar o conteúdo de um tipo de valor em caixa, é possível fazer o seguinte:
object x = 1; x.GetType().GetField("m_value", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(x, 138); Console.WriteLine(x); // -> 138
Ou assim (C ++ / CLI):
void ChangeValue(Object^ obj) { Int32^ i = (Int32^)obj; *i = 138; }
Se lidássemos com o pool, alteraríamos todos os aplicativos em 138 para 138, o que não é bom.
O próximo é a essência dos tipos de valor no .NET. Eles lidam com valor, o que significa que trabalham mais rápido. O boxe é raro e a adição de números em caixas pertence ao mundo da fantasia e da arquitetura ruim. Isso não é de todo útil.
Por que não é possível fazer boxe na pilha em vez da pilha, quando você chama um método que usa um tipo de objeto, que é realmente um tipo de valor?
Se o boxe do tipo de valor for feito na pilha e a referência for para o heap, a referência dentro do método poderá ir para outro lugar, por exemplo, um método pode colocar a referência no campo de uma classe. O método será interrompido e o método que criou o boxe também será interrompido. Como resultado, a referência apontará para um espaço morto na pilha.
Por que não é possível usar o Tipo de valor como um campo?
Às vezes, queremos usar uma estrutura como um campo de outra estrutura que use a primeira. Ou mais simples: use a estrutura como um campo de estrutura. Não me pergunte por que isso pode ser útil. Não pode. Se você usar uma estrutura como seu campo ou através da dependência de outra estrutura, criará recursão, o que significa estrutura de tamanho infinito. No entanto, o .NET Framework tem alguns lugares onde você pode fazer isso. Um exemplo é System.Char
, que contém a si mesmo :
public struct Char : IComparable, IConvertible { // Member Variables internal char m_value; //... }
Todos os tipos primitivos do CLR são projetados dessa maneira. Nós, meros mortais, não podemos implementar esse comportamento. Além disso, não precisamos disso: isso é feito para dar aos tipos primitivos um espírito de OOP no CLR.
Este charper traduzido do russo como idioma do autor por tradutores profissionais . Você pode nos ajudar a criar a versão traduzida deste texto para qualquer outro idioma, incluindo chinês ou alemão, usando as versões russa e inglesa do texto como fonte.
Além disso, se você quiser dizer "obrigado", a melhor maneira de escolher é dar uma estrela no repositório do github ou do fork
https://github.com/sidristij/dotnetbook