Olá pessoal. Gostaria de compartilhar um exemplo de uso do StructLayout para algo mais interessante do que exemplos com bytes, ints e outros números, nos quais tudo acontece um pouco mais do que o esperado.
Antes de prosseguir com uma violação de encapsulamento extremamente rápida, vale lembrar em poucas palavras o que é o StructLayout. A rigor, esse é mesmo um StructLayoutAttribute, ou seja, um atributo que permite criar estruturas e classes semelhantes à união no C ++. Mais detalhadamente, esse atributo permite que você assuma o controle da colocação dos alunos na memória. Por conseguinte, é colocado acima da classe. Normalmente, se uma classe tiver 2 campos, esperamos que sejam organizados sequencialmente, ou seja, eles serão independentes um do outro (não se sobrepõem). No entanto, o StructLayout possibilita especificar que a localização dos campos será determinada não pelo ambiente, mas pelo usuário. Para indicar explicitamente deslocamentos de campo, use o parâmetro LayoutKind.Explicit. Para indicar em qual deslocamento em relação ao início da classe / estrutura (a seguir denominada classe) queremos colocar o campo, devemos colocar o atributo FieldOffset acima dele, que toma como parâmetro o número de bytes - o recuo do início da classe. Você não poderá transmitir um valor negativo; portanto, nem pense em estragar os ponteiros para a tabela de métodos ou o índice do bloco de sincronização, será um pouco mais complicado.
Vamos começar a escrever o código. Para começar, sugiro começar com um exemplo simples. Crie uma classe do seguinte formulário:
public class CustomClass { public override string ToString() { return "CUSTOM"; } public virtual object Field { get; } = new object(); }
Em seguida, usamos o mecanismo acima para definir explicitamente as compensações de campo.
[StructLayout(LayoutKind.Explicit)] public class CustomStructWithLayout { [FieldOffset(0)] public string Str; [FieldOffset(0)] public CustomClass SomeInstance; }
Por enquanto, vou adiar as explicações e usar a aula escrita da seguinte maneira:
class Program { static void Main(string[] args) { CustomStructWithLayout instance = new CustomStructWithLayout(); instance.SomeInstance = new CustomClass(); instance.Str = "4564"; Console.WriteLine(instance.SomeInstance.GetType()); //System.String Console.WriteLine(instance.SomeInstance.ToString()); //4564 Console.Read(); } }
Total A chamada para o método GetType () retorna uma string, o método ToString () é impertinente e nos fornece a string "4564".
Descarga para o cérebro: o que será exibido quando a propriedade virtual CustomClass for chamada?
Como você deve ter adivinhado, inicializamos CustomStructWithLayout, os dois links são nulos, inicializamos um campo do nosso tipo e depois atribuímos uma string ao campo Str. Como resultado, resta um pouco mais da CustomClass do que nada. No topo, havia uma linha com toda a sua estrutura interna, incluindo uma tabela de métodos e um índice de um bloco de sincronização. Mas o compilador vê que o campo ainda é do tipo de nossa classe.
Como prova, darei um pequeno recorte do WinDbg:

Aqui você pode ver algumas coisas incomuns. A primeira é que, no objeto, os endereços nas tabelas de métodos possuem campos diferentes para os campos da classe, conforme o esperado, mas o endereço do valor do campo é um. A segunda - você pode ver que os dois campos estão localizados no deslocamento 4. Acho que a maioria entenderá, mas, apenas no caso, explicarei, diretamente no endereço do objeto, há um link para a tabela de métodos. Os campos começam com um deslocamento de 4 bytes (para z 32 bits) e o índice do bloco de sincronização está localizado com um deslocamento de -4.
Agora que você descobriu o que está acontecendo, tente usar compensações para chamar algo que não deve chamar.
Para fazer isso, repeti a estrutura da classe string em uma das minhas classes. É verdade que repeti apenas o começo, já que a classe de cordas é muito volumosa.
public class CustomClassLikeString { public const int FakeAlignConst = 3; public const int FakeCharPtrAlignConst = 3; public static readonly object FakeStringEmpty; public char FakeFirstChar; public int FakeLength = 3; public const int FakeTrimBoth = 3; public const int FakeTrimHead = 3; public const int FakeTrimTail = 3; public CustomClassLikeString(){} public CustomClassLikeString(int a){} public CustomClassLikeString(byte a){} public CustomClassLikeString(short a){} public CustomClassLikeString(string a){} public CustomClassLikeString(uint a){} public CustomClassLikeString(ushort a){} public CustomClassLikeString(long a){ } public void Stub1(){} public virtual int CompareTo(object value) { return 800; } public virtual int CompareTo(string value) { return 801; } }
Bem, a estrutura com Layout muda um pouco
[StructLayout(LayoutKind.Explicit)] public class CustomStructWithLayout { [FieldOffset(0)] public string Str; [FieldOffset(0)] public CustomClassLikeString SomeInstance; }
Além disso, ao chamar o FakeLength ou o método CompareTo (), devido ao deslocamento idêntico desses membros da classe em relação ao endereço do próprio objeto, o método de string correspondente (neste caso) será chamado. Demorou um pouco para chegar ao primeiro método privado na linha que eu posso usar, então decidi pelo método público. Mas o campo é privado, tudo é justo. A propósito, os métodos são virtualizados para proteger contra todos os tipos de otimizações, o que interfere no trabalho (por exemplo, incorporação) e também para que o método seja chamado em um deslocamento na tabela de métodos.
Então, desempenho. É fato que um concorrente direto no desafio do que não é necessário causar e na violação do encapsulamento é reflexo. Eu acho que já está claro que somos mais rápidos do que isso, todos nós não analisamos metadados. Valores exatos:
Método | Job | Mean | Erro | Stddev | Mediana |
---|
StructLayoutField | Clr | 0,0597 ns | 0,0344 ns | 0,0396 ns | 0,0498 ns |
Campo de reflexão | Clr | 197.1257 ns | 1.9148 ns | 1,7911 ns | 197.4787 ns |
StructLayoutMethod | Clr | 3.5195 ns | 0,0382 ns | 0,0319 ns | 3.5285 ns |
ReflectionMethod | Clr | 743.9793 ns | 13.7378 ns | 12.8504 ns | 743.8471 ns |
Aqui está um longo pedaço de código com a forma como medi o desempenho (se alguém precisar):
Código [ClrJob] [RPlotExporter, RankColumn] [InProcessAttribute] public class Benchmarking { private CustomStructWithLayout instance; private string str; [GlobalSetup] public void Setup() { instance = new CustomStructWithLayout(); instance.SomeInstance = new CustomClassLikeString(); instance.Str = "4564"; str = "4564"; } [Benchmark] public int StructLayoutField() { return instance.SomeInstance.FakeLength; } [Benchmark] public int ReflectionField() { return (int)typeof(string).GetField("m_stringLength", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(str); } [Benchmark] public int StructLayoutMethod() { return instance.SomeInstance.CompareTo("4564"); } [Benchmark] public int ReflectionMethod() { return (int)typeof(string).GetMethod("CompareTo", new[] { typeof(string) }).Invoke(str, new[] { "4564" }); } } class Program { static void Main(string[] args) { var summary = BenchmarkRunner.Run<Benchmarking>(); } }