为什么 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>
实例是由一个(而不是本机指针)创建的。
- 鉴于引用类型在固定引用时无关紧要(固定
ref T
和 Unsafe.As<T, byte>(ref T)
工作相同),是否有特定原因 Pinnable<T>
class 是通用的吗? DotNetCross here 中的原始设计实际上有一个 Pinnable
class 只有一个 byte
字段,而且它的工作原理是一样的。除了避免在 writing/reading/returning 时转换参考时间之外,在这种情况下使用通用 class 有什么好处吗?
- 除了使用
Unsafe.As
完成的这种不安全转换之外,还有其他方法来获取对对象的引用(我的意思是对对象内容的引用,否则它与class 类型的任何变量)?我的意思是,任何方式都可以获取对象的引用(基本上应该首先具有与实际对象变量相同的地址,对吗?)而不必通过一些自定义的辅助 class.
首先,[StructLayout(LayoutKind.Sequential)]
中的Struct并不是说它只对struct有效,它是指字段实际结构的布局在内存中,无论是 class 还是值类型。这控制了数据的 实际 运行时布局,而不仅仅是类型将如何编组到非托管代码。 Sequential 很重要,因为没有它,运行时几乎可以自由存储内存,但它认为合适,这意味着 Data 可能有一些在它之前填充。
根据我对实现的理解,Pinnable 的原因是允许创建 Span 的实例到可以由 GC 移动的内存,而不必先固定 object。如果您不使用实际指针而只使用引用,则根本不需要固定任何内容。
我注意到它是在一个提交中引入的,描述中说它使 Span 更 "portable"(一个大胆的词表示一些不安全的事情事物)。除了与对齐有关的原因之外,我想不出任何其他原因来解释为什么它是通用的。我想用另一个 T
的偏移量来表示 T
比 byte
的偏移量要好。第一个字段的类型可能会在其实际地址中起作用,即使该类型被标记为 LayoutKind.Sequential.
对 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 中完成的(比如参考算术)。
我知道 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>
实例是由一个(而不是本机指针)创建的。
- 鉴于引用类型在固定引用时无关紧要(固定
ref T
和Unsafe.As<T, byte>(ref T)
工作相同),是否有特定原因Pinnable<T>
class 是通用的吗? DotNetCross here 中的原始设计实际上有一个Pinnable
class 只有一个byte
字段,而且它的工作原理是一样的。除了避免在 writing/reading/returning 时转换参考时间之外,在这种情况下使用通用 class 有什么好处吗? - 除了使用
Unsafe.As
完成的这种不安全转换之外,还有其他方法来获取对对象的引用(我的意思是对对象内容的引用,否则它与class 类型的任何变量)?我的意思是,任何方式都可以获取对象的引用(基本上应该首先具有与实际对象变量相同的地址,对吗?)而不必通过一些自定义的辅助 class.
首先,[StructLayout(LayoutKind.Sequential)]
中的Struct并不是说它只对struct有效,它是指字段实际结构的布局在内存中,无论是 class 还是值类型。这控制了数据的 实际 运行时布局,而不仅仅是类型将如何编组到非托管代码。 Sequential 很重要,因为没有它,运行时几乎可以自由存储内存,但它认为合适,这意味着 Data 可能有一些在它之前填充。
根据我对实现的理解,Pinnable 的原因是允许创建 Span 的实例到可以由 GC 移动的内存,而不必先固定 object。如果您不使用实际指针而只使用引用,则根本不需要固定任何内容。
我注意到它是在一个提交中引入的,描述中说它使 Span 更 "portable"(一个大胆的词表示一些不安全的事情事物)。除了与对齐有关的原因之外,我想不出任何其他原因来解释为什么它是通用的。我想用另一个
T
的偏移量来表示T
比byte
的偏移量要好。第一个字段的类型可能会在其实际地址中起作用,即使该类型被标记为 LayoutKind.Sequential.对 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 中完成的(比如参考算术)。