我知道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>
实例是由一个(而不是 native 指针)创建的,则它用于跟踪原始对象。ref T
和Unsafe.As<T, byte>(ref T)
都相同),是否有特定原因将Pinnable<T>
类设置为泛型?实际上,DotNetCross here中的原始设计具有一个Pinnable
类,该类只有一个byte
字段,并且其工作原理相同。除了避免在写入/读取/返回引用时间时强制转换引用时间之外,在这种情况下使用通用类会有什么优势吗? Unsafe.As
进行的这种不安全广播之外,还有其他方法可以获取对对象的引用(我的意思是对对象内容的引用,否则它将与类类型的任何变量相同)?我的意思是,任何获取引用的方法(首先应该与实际对象变量具有相同的地址,对吗?),而无需通过某些自定义的辅助类。 最佳答案
首先,[StructLayout(LayoutKind.Sequential)]
中的Struct并不意味着它仅对结构有效,而是意味着字段的实际结构在内存中的布局,无论是在类中还是在值类型中。这将控制数据的实际运行时布局,而不仅仅是控制将类型编码为非托管代码的方式。顺序很重要,因为如果没有它,运行时几乎可以自由地存储内存,但是它认为合适,这意味着数据之前可能有一些填充。
我已经注意到,它是在一次提交中引入的,描述中说它使Span更加“轻便”(一个粗体字表示做很多不安全的事情)。除了通用性之外,我想不出其他任何原因。我想用相对于另一个
T
的偏移量来表示T
更好,而不是相对于byte
的偏移量来表示。即使第一个字段的类型用LayoutKind.Sequential标记,也可能会在其实际地址中起作用。 lock
)和方法表指针(即对象类型)的指针开头。在32位上, header 为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中完成的(例如引用算术)。
关于c# - 为什么用C#7.2中的Pinnable <T>类定义了它的方式?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/48120879/