Oi Gostaria de mostrar um exemplo do uso de
StructLayout para algo mais interessante do que exemplos com bytes, ints e outros tipos primitivos, quando tudo acontece de maneira óbvia.
Antes de prosseguir com a violação extremamente rápida do encapsulamento, vale a pena relembrar brevemente o que é o StructLayout. A rigor, é mesmo o
StructLayoutAttribute , um atributo que permite criar estruturas e classes semelhantes à união em C ++. Este atributo permite que você assuma o controle da colocação dos membros da classe na memória (usando deslocamentos). 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 permite que você especifique que o local dos campos será definido não pelo ambiente, mas pelo usuário. Para especificar explicitamente o deslocamento dos campos, devemos usar o parâmetro
LayoutKind.Explicit .
Para indicar qual deslocamento do início da classe / estrutura (daqui em diante apenas "classe") queremos colocar o campo, precisamos colocar o atributo
FieldOffset nele. Este atributo assume como parâmetro o número de bytes - o deslocamento desde o início da classe. É impossível passar um valor negativo, para não estragar os ponteiros para a tabela de métodos ou o índice do bloco de sincronização. Entã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 a seguinte classe:
public class CustomClass { public override string ToString() { return "CUSTOM"; } public virtual object Field { get; } = new object(); }
Em seguida, usamos o mecanismo descrito acima para especificar 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(); } }
Chamar o método
GetType () retorna uma string, o método
ToString () é impertinente e nos fornece a string "4564".
Descarga cerebral: o que será exibido depois de chamar a propriedade virtual
CustomClass ?
Como você já adivinhou, inicializamos
CustomStructWithLayout , os dois links são nulos, inicializamos o campo do nosso tipo e atribuímos a string ao campo Str. Como resultado, o link
CustomClass não aponta para o objeto
CustomClass , mas para o objeto System.string (incluindo a tabela de métodos e o índice da unidade de sincronização). Mas o compilador vê que o campo ainda é do tipo de nossa classe.
Para prova, aqui está um pequeno recorte do WinDbg:

Aqui você pode ver algumas coisas incomuns.
- No objeto CustomStructWithLayout, os campos têm endereços diferentes de tabelas de métodos (altamente esperados), mas os endereços dos objetos são os mesmos.
- A segunda é que você pode ver que os dois campos estão localizados no deslocamento 4. Acho que a maioria entenderá, mas, apenas no caso, explicarei, diretamente para o endereço do objeto colocado um link para a tabela de métodos. Os campos começam com um deslocamento de 4 bytes (para 32 bits) e o índice do bloco de sincronização está localizado com um deslocamento de -4. Assim, ambos os objetos estão no mesmo deslocamento.
Agora que você descobriu o que está acontecendo, tente usar compensações para chamar o que não deveria ter sido chamado.
Para isso, repeti a estrutura da classe string em uma das minhas classes. Mas repeti apenas o começo, já que a série é bastante volumosa e sou muito preguiçosa.
Nota: consts e estática não são necessários, apenas por diversão.
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 o layout será alterada um pouco.
[StructLayout(LayoutKind.Explicit)] public class CustomStructWithLayout { [FieldOffset(0)] public string Str; [FieldOffset(0)] public CustomClassLikeString SomeInstance; }
Além disso, ao chamar o método
FakeLength ou
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 será chamado (neste caso).
O acesso ao primeiro método privado da string que eu posso usar foi muito longo, então parei em um método público. Mas o campo é privado, tudo é honesto. A propósito, os métodos são virtualizados para proteger contra otimizações que interferem no trabalho (por exemplo, incorporação) e também para que o método seja chamado pelo deslocamento na tabela de métodos.
Então, desempenho. É claro que um concorrente direto chamando coisas que não devem ser chamadas e violando o encapsulamento é reflexo. Eu acho que está claro que somos mais rápidos que isso, porque 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>(); } }
Versão russa