本文将向您展示类型内部的基础知识,当然,这是一个示例,其中引用类型的内存将完全在堆栈上分配(这是因为我是全栈程序员)。

免责声明
本文不包含实际项目中应使用的材料。 它只是感觉到编程语言的界限的扩展。
在继续讲故事之前,我强烈建议您阅读有关
StructLayout的第一篇文章,因为本文中将使用一个示例(但是,一如既往)。
史前史
开始为本文编写代码时,我想使用汇编语言做一些有趣的事情。 我想以某种方式打破标准执行模型,并得到一个非常不寻常的结果。 记得人们经常说引用类型与值类型的不同之处在于,第一个位于堆上,第二个位于堆栈上,我决定使用汇编程序来表明引用类型可以存在于值类型中。堆栈。 但是,我开始遇到各种问题,例如,将地址及其表示形式作为托管链接返回(我仍在研究)。 因此,我开始作弊,并在C#中做一些在汇编语言中不起作用的事情。 最后,根本没有汇编程序。
另请阅读建议-如果您熟悉引用类型的布局,我建议跳过有关它们的理论(仅提供基本知识,没有什么有趣的意思)。
有关类型内部的一些知识(对于旧框架,现在一些偏移已更改,但总体架构相同)
我想提醒一下,将内存分为堆栈和堆是在.NET级别进行的,并且这种划分纯粹是逻辑上的。 堆和堆栈下的内存区域在物理上没有区别。 生产率的差异仅由使用这两个区域的不同算法提供。
那么,如何在堆栈上分配内存? 首先,让我们了解这种神秘的引用类型的排列方式以及它具有的值类型所没有的。
因此,请考虑Employee类的最简单示例。
代码员工public 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..."); } }
让我们看一下它在内存中的呈现方式。
在32位系统的示例中考虑此类。

因此,除了用于这些字段的存储器外,我们还有两个其他隐藏字段-同步块的索引(图片中的对象标题单词标题)和方法表的地址。
第一个字段(同步块索引)对我们并不真正感兴趣。 当放置类型时,我决定跳过它。 我这样做有两个原因:
- 我很懒惰(我没有说理由会合理)
- 对于对象的基本操作,此字段不是必需的。
但是,既然我们已经开始讨论,我认为对这一领域说几句话是正确的。 它用于不同的目的(哈希码,同步)。 而是,字段本身只是与给定对象关联的同步块之一的索引。 块本身位于同步块表中(类似于全局数组)。 创建这样的块是一个相当大的操作,因此如果不需要它就不会创建。 此外,当使用细锁时,接收到该锁的线程的标识符(而不是索引)将被写入那里。
第二个领域对我们来说更为重要。 多亏了类型方法表,才有可能使用诸如多态性这样的强大工具(顺便说一下,结构,堆栈国王不具备)。
假设Employee类另外实现了三个接口:IComparable,IDisposable和ICloneable。
然后,方法表将如下所示。

图片非常酷,所有内容均已显示且所有内容均清晰可见。 总结起来,虚拟方法不是直接由地址调用,而是由方法表中的偏移量调用。 在层次结构中,相同的虚拟方法将位于方法表中的相同偏移处。 也就是说,在基类上,我们通过偏移量调用方法,而不知道将使用哪种类型的方法表,但是知道此偏移量将是与运行时类型最相关的方法。
同样值得记住的是,对象引用仅指向方法表指针。
期待已久的例子
让我们从有助于我们实现目标的课程开始。 使用StructLayout(我确实尝试过不带它,但没有成功),我编写了简单的映射器-指向托管类型和返回类型的指针。 从托管链接获取指针非常容易,但是逆变换给我带来了麻烦,并且我三思而后行地应用了我最喜欢的属性。 为了将代码保持在一个键中,以一种方式在两个方向上进行编码。
映射器的代码 // 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; }
首先,我们编写一个方法,该方法采用指向某个内存的指针(顺便说一句,不一定要在堆栈上)并配置类型。
为了简化方法表地址的查找,我在堆上创建一个类型。 我确信可以通过其他方式找到方法表,但是我没有设定优化代码的目标,对我来说,使其变得可理解更有趣。 此外,使用前面描述的转换器,我们获得了指向所创建类型的指针。
该指针恰好指向方法表。 因此,从它所指向的存储器中简单地获取内容就足够了。 这将是方法表的地址。
并且由于传递给我们的指针是一种对象引用,因此我们还必须将方法表的地址确切地写到它指向的位置。
实际上,仅此而已。 突然吧 现在我们的类型准备好了。 分配给我们内存的Pinocchio将亲自初始化这些字段。
仍然仅需使用我们的超大型脚轮将指针转换为托管链接。
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; } }
现在,我们在堆栈上有一个指向同一堆栈的链接,根据所有引用类型的定律(差不多),该对象位于一个由黑土和木棍构成的对象中。 多态是可用的。
应该理解的是,如果您将此链接传递到方法之外,那么从它返回之后,我们将得到一些不清楚的地方。 关于虚拟方法和语音的调用不能进行,会发生异常。 普通方法被直接调用,代码将只包含实际方法的地址,因此它们将起作用。 取代田地的地方是……而没人知道那里会有什么。
由于不可能在堆栈上使用单独的方法进行初始化(因为从方法返回后堆栈帧将被覆盖),因此要在堆栈上应用类型的方法必须分配内存。 严格来说,有一些方法可以做到这一点。 但是最适合我们的是
stackalloc 。 仅仅是我们目的的完美关键字。 不幸的是,它带来了代码中的
不安全 。 在此之前,有一个想法将Span用于这些目的,并且没有不安全的代码。 在不安全的代码中,没有什么不好,但是像其他地方一样,它不是灵丹妙药,它有自己的应用领域。
然后,在收到指向当前堆栈上的内存的指针之后,我们将此指针传递给组成类型的方法。 那就是所有听的人-做得好。
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(); } }
您不应该在实际项目中使用它,在堆栈上分配内存的方法使用new T(),后者又使用反射在堆上创建类型! 因此,此方法将比通常创建的时间类型慢40-50。 而且它不是跨平台的。
在这里您可以找到整个项目。
资料来源:在理论指南中,使用了Sasha Goldstein-Pro .NET Performace一书中的示例