Salut. Je voudrais vous montrer un exemple d'utilisation de
StructLayout pour quelque chose de plus intéressant que des exemples avec des octets, des entiers et d'autres types primitifs, quand tout se passe de façon assez évidente.
Avant de procéder à une violation ultra-rapide de l'encapsulation, il convient de rappeler brièvement ce qu'est StructLayout. À strictement parler, il s'agit même de
StructLayoutAttribute , un attribut qui vous permet de créer des structures et des classes similaires à l'union en C ++. Cet attribut vous permet de prendre le contrôle du placement des membres de la classe dans la mémoire (à l'aide de décalages). En conséquence, il est placé au-dessus de la classe.
Habituellement, si une classe a 2 champs, nous nous attendons à ce qu'ils soient organisés séquentiellement, c'est-à-dire qu'ils seront indépendants les uns des autres (ne se chevauchent pas). Cependant,
StructLayout vous permet de spécifier que l'emplacement des champs sera défini non pas par l'environnement, mais par l'utilisateur. Pour spécifier explicitement le décalage des champs, nous devons utiliser le paramètre
LayoutKind.Explicit .
Pour indiquer quel décalage par rapport au début de la classe / structure (ci-après juste "classe") nous voulons placer le champ, nous devons lui mettre l'attribut
FieldOffset . Cet attribut prend en paramètre le nombre d'octets - le décalage par rapport au début de la classe. Il est impossible de passer une valeur négative, afin de ne pas gâcher les pointeurs vers la table des méthodes ou l'index du bloc de synchronisation. Ça va donc être un peu plus compliqué.
Commençons à écrire le code. Pour commencer, je suggère de commencer par un exemple simple. Créez la classe suivante:
public class CustomClass { public override string ToString() { return "CUSTOM"; } public virtual object Field { get; } = new object(); }
Ensuite, nous utilisons le mécanisme décrit ci-dessus pour spécifier explicitement les décalages de champ.
[StructLayout(LayoutKind.Explicit)] public class CustomStructWithLayout { [FieldOffset(0)] public string Str; [FieldOffset(0)] public CustomClass SomeInstance; }
Pour l'instant, je vais reporter les explications et utiliser la classe écrite comme suit:
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(); } }
L'appel de la méthode
GetType () renvoie une chaîne, la méthode
ToString () est coquine et nous donne la chaîne "4564".
Décharge cérébrale: qu'est-ce qui sera affiché après avoir appelé la propriété virtuelle
CustomClass ?
Comme vous l'avez déjà deviné, nous avons initialisé
CustomStructWithLayout , les deux liens sont nuls, puis nous initialisons le champ de notre type, puis attribuons la chaîne au champ Str. Par conséquent, le lien
CustomClass ne pointe pas vers l'objet
CustomClass , il pointe vers l'objet System.string (y compris le tableau des méthodes et l'index de l'unité de synchronisation). Mais le compilateur voit que le champ est toujours du type de notre classe.
Pour preuve, voici un petit extrait de WinDbg:

Ici, vous pouvez voir des choses inhabituelles.
- Dans CustomStructWithLayout , les champs d'objet ont des adresses de tables de méthodes différentes (très attendues), mais les adresses des objets sont les mêmes.
- La seconde est que vous pouvez voir que les deux champs sont situés à l'offset 4. Je pense que la plupart comprendront, mais juste au cas où, j'expliquerai, directement à l'adresse de l'objet placé un lien vers le tableau des méthodes. Les champs commencent par un décalage de 4 octets (pour 32 bits) et l'index du bloc de synchronisation est localisé avec un décalage de -4. Ainsi, les deux objets sont au même décalage.
Maintenant que vous avez compris ce qui se passe, vous pouvez essayer d'utiliser des décalages pour appeler ce qui n'aurait pas dû être appelé.
Pour cela, j'ai répété la structure de la classe string dans l'une de mes classes. Mais je n'ai répété que le début, car la chaîne de classe est assez volumineuse et je suis très paresseux.
Remarque: consts et statiques ne sont pas nécessaires, juste pour le plaisir.
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; } }
Eh bien, la structure avec la mise en page sera un peu modifiée.
[StructLayout(LayoutKind.Explicit)] public class CustomStructWithLayout { [FieldOffset(0)] public string Str; [FieldOffset(0)] public CustomClassLikeString SomeInstance; }
De plus, lors de l'appel de
FakeLength ou de la méthode
CompareTo () , en raison du décalage identique de ces membres de classe par rapport à l'adresse de l'objet lui-même, la méthode de chaîne correspondante sera appelée (dans ce cas).
Arriver à la première méthode privée de la chaîne que je peux utiliser était trop long, donc je me suis arrêté sur une méthode publique. Mais le terrain est privé, tout est honnête. Soit dit en passant, les méthodes sont rendues virtuelles pour se protéger contre toute optimisation qui interfère avec le travail (par exemple, l'incorporation), et aussi pour que la méthode soit appelée par l'offset dans la table des méthodes.
Donc, la performance. Il est clair qu'un concurrent direct en appelant des choses qui ne devraient pas être appelées et en violation de l'encapsulation est la réflexion. Je pense qu'il est clair que nous sommes plus rapides que cette chose, car nous n'analysons pas les métadonnées. Valeurs exactes:
La méthode | Job | Moyenne | Erreur | Stddev | Médiane |
---|
StructLayoutField | Clr | 0,0597 ns | 0,0344 ns | 0,0396 ns | 0,0498 ns |
ReflectionField | 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 |
Voici un long morceau de code avec la façon dont j'ai mesuré les performances (si quelqu'un en a besoin):
Code [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>(); } }
Version russe