为什么C#7.2中的Pinnable <t>类定义它的方式?

时间:2018-01-05 20:32:32

标签: c# .net clr cil c#-7.2

我知道Pinnable<T>是新Unsafe类中的方法使用的内部类,并不打算在该类以外的任何其他地方使用。这个问题不是关于实用的东西,而只是为了理解为什么它是这样设计的,并且学习更多关于语言及其各种“技巧”的信息。

作为回顾,Pinnable<T>类定义为here,它看起来像这样:

[StructLayout(LayoutKind.Sequential)]
internal sealed class Pinnable<T>
{
    public T Data;
}

它主要用于Span<T>.DangerousCreate方法,here

public static Span<T> DangerousCreate(object obj, ref T objectData, int length)
{
    Pinnable<T> pinnable = Unsafe.As<Pinnable<T>>(obj);
    IntPtr byteOffset = Unsafe.ByteOffset<T>(ref pinnable.Data, ref objectData);
    return new Span<T>(pinnable, byteOffset, length);
}

Pinnable<T>的原因是它用于跟踪原始对象,以防Span<T>实例由一个(而不是本机指针)创建。

  1. 鉴于在引用引用时引用类型无关紧要(同时修复ref TUnsafe.As<T, byte>(ref T)的工作方式相同),是否有一个特定的原因可以使Pinnable<T>类成为通用? DotNetCross here中的原始设计实际上只有一个Pinnable类,只有一个byte字段,它的工作原理相同。在这种情况下使用泛型类会有什么好处,除了避免在写/读/返回它时抛出参考时间?
  2. 除了使用Unsafe.As进行的不安全转换之外,还有其他任何方法来获取对象的引用(我的意思是对对象内容的引用,否则它与任何变量相同)类型)?我的意思是,任何方式获得一个引用(它应该基本上与实际对象变量的地址基本相同,对吧?)到一个对象,而不必通过一些自定义的二级类。

1 个答案:

答案 0 :(得分:1)

首先,[StructLayout(LayoutKind.Sequential)]中的 Struct 并不意味着它只对结构有效,它意味着内存中字段的实际结构布局,无论是在类中还是在值类型中。这将控制数据的实际运行时布局,而不仅仅是类型将如何编组为非托管代码。 Sequential 非常重要,因为没有它,运行时可以非常自由地存储内存,但它认为合适,这意味着 Data 可能在它之前有一些填充。

  1. 根据我对实现的理解, Pinnable 的原因是允许将 Span 的实例创建到可能由GC移动的内存中,无需先固定物体。如果你不使用实际指针和引用,则根本不需要固定。

    我注意到它是在提交中引入的,其中描述说它使 Span 更多&#34;便携式&#34; (对于做很多不安全事情的事情,这是一个大胆的词)。我无法想到任何其他原因,而不是与对齐有关的原因。我认为根据与另一个T的偏移来表示T比从byte的偏移更好。可能会发生第一个字段的类型可能在其实际地址中起作用,即使该类型标有 LayoutKind.Sequential

  2. 对象的引用不同于对象的内部引用(对其数据的引用)。它是实现定义的,但在.NET Framework中,任何类(或盒装值类型)的实例都以包含同步块(用于lock)和指向方法表(也称为方法表)的指针开头。对象的类型。在32位上,标头是8个字节,但实际指针指向方法表的指针(出于性能原因,获取类型比锁定对象更频繁。)

    获取指向数据开头的指针的一种但不可移植的方法是将对象引用转换为指针并向其添加4个字节。第一个字段应该开始。

    我能想到的另一种方法是使用 GCHandle.AddrOfPinnedObject 。它通常用于访问数组或字符串数​​据,但它适用于其他对象:

    [StructLayout(LayoutKind.Sequential)]
    class Obj
    {
        public int A;
    }
    
    var obj = new Obj();
    var gc = GCHandle.Alloc(obj, GCHandleType.Pinned);
    IntPtr interior = gc.AddrOfPinnedObject();
    Marshal.WriteInt32(interior, 0, 16);
    Console.WriteLine(obj.A);
    

    我认为这实际上是非常便携的,但仍然需要固定对象( GCHandle 中定义了 InternalAddrOfPinnedObject ,但即使它没有检查如果句柄实际是固定的,如果在非固定对象上使用它,返回的值可能无效。)

    然而, Span 使用的技术似乎是最便携的方式,因为很多基础工作都是在纯CIL中完成的(比如参考算术)。