为什么 C# 7.2 中的 Pinnable<T> class 是这样定义的?

Why is the Pinnable<T> class in C# 7.2 defined the way it is?

我知道 Pinnable<T> 是新的 Unsafe class 中的方法使用的内部 class,它不打算在其他任何地方使用除了 class。这个问题不是关于实际的问题,而只是为了理解为什么它被设计成这样,并更多地了解这种语言和它的各种"tricks"。

回顾一下,Pinnable<T> class 被定义为 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> class 是通用的吗? DotNetCross here 中的原始设计实际上有一个 Pinnable class 只有一个 byte 字段,而且它的工作原理是一样的。除了避免在 writing/reading/returning 时转换参考时间之外,在这种情况下使用通用 class 有什么好处吗?
  2. 除了使用 Unsafe.As 完成的这种不安全转换之外,还有其他方法来获取对对象的引用(我的意思是对对象内容的引用,否则它与class 类型的任何变量)?我的意思是,任何方式都可以获取对象的引用(基本上应该首先具有与实际对象变量相同的地址,对吗?)而不必通过一些自定义的辅助 class.

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

  1. 根据我对实现的理解,Pinnable 的原因是允许创建 Span 的实例到可以由 GC 移动的内存,而不必先固定 object。如果您不使用实际指针而只使用引用,则根本不需要固定任何内容。

    我注意到它是在一个提交中引入的,描述中说它使 Span 更 "portable"(一个大胆的词表示一些不安全的事情事物)。除了与对齐有关的原因之外,我想不出任何其他原因来解释为什么它是通用的。我想用另一个 T 的偏移量来表示 Tbyte 的偏移量要好。第一个字段的类型可能会在其实际地址中起作用,即使该类型被标记为 LayoutKind.Sequential.

  2. 对 object 的引用不同于对 object 的内部引用(对其数据的引用)。它是实现定义的,但在 .NET Framework 中,任何 class(或盒装值类型)的实例都以 header 开头,该 header 由一个同步块(对于 lock)和一个指向方法 table、a.k.a 的指针。 object 的类型。在 32 位上,header 是 8 个字节,但实际指针指向方法 table 的指针(出于性能原因,获取类型比锁定 object 更频繁) ).

    获取指向数据开始的指针的一种但不是 portable 的方法因此是将 object 引用转换为指针并向其添加 4 个字节。第一个字段应该从那里开始。

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

    [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);
    

    我认为这实际上是相当 portable,但仍然需要固定 object(在 中定义了 InternalAddrOfPinnedObject GCHandle,但即使不检查句柄是否实际固定,如果在 non-pinned object).[=17 上使用,返回值也可能无效=]

    不过,Span 使用的技术似乎是最可行的table 方法,因为很多基础工作都是在纯 CIL 中完成的(比如参考算术)。