我一直在使用System.Runtime.CompilerServices.Unsafe
MSIL软件包进行一些极其不安全且有点无用的混乱,该软件包使您可以使用C#中无法使用的指针来做很多事情。我创建了一个扩展方法,该方法返回一个ref byte
,该字节在对象的开始处是“方法表”指针的开始,它使您可以使用固定语句中的任何对象,并以一个字节指针作为对象的开始。物体:
public static unsafe ref byte GetPinnableReference(this object obj)
{
return ref *(byte*)*(void**)Unsafe.AsPointer(ref obj);
}
然后,我决定使用以下代码对其进行测试:
[StructLayout(LayoutKind.Explicit, Pack = 0)]
public class Foo
{
[FieldOffset(0)]
public string Name = "THIS IS A STRING";
}
[StructLayout(LayoutKind.Explicit, Pack = 0)]
public struct Bar
{
[FieldOffset(0)]
public string Name;
}
然后在方法
var foo = new Foo();
//var foo = new Bar { Name = "THIS IS A STRING" };
fixed (byte* objPtr = foo)
{
char* stringPtr = (char*)(objPtr + (foo is Foo ? : 12));
for (var i = 0; i < foo.Name.Length; i++)
{
Console.Write(*(stringPtr + i /* Char offset */));
}
Console.WriteLine();
}
Console.ReadKey();
真正奇怪的是,它成功打印了“这是一个字符串”吗?该代码如下所示:
获取指向对象开头的字节指针
objPtr
加16即可得出实际数据
再添加16个,以使字符串标头超过字符串的实际数据
加4以跳过字符串的前4个字节,即
int
_stringLength
(作为我们的Length
属性公开)将结果解释为char指针
编辑:重要点-切换
foo
键入Bar
时,我只添加12个字节而不是36个字节(36 = 16 + 16 + 4)。为什么在结构中标头只有8个字节,而在类中却只有32个?结构具有较小的标头(我相信没有syncblk)会很有意义,但是为什么字符串还没有16字节的标头呢?我希望偏移量是8 + 16 + 4(28),而不只是8 + 4(12)但是,这种假设有很大的缺陷。假定该字符串以内联方式存储在
class/struct
中。但是,字符串是引用类型,据我所知,仅对它们的引用存储在对象内部。特别是,我认为引用类型只能放在堆上-并且因为此结构是局部变量,所以我认为它在堆栈上。如果不是,代码肯定会看起来更像这样以获得stringPtr
byte** stringRefptr = objPtr + 16;
char* stringPtr = (char*)(*stringRefPtr + 20);
在这里将字符串引用作为
byte**
,然后使用它来获取字符。而且如果内部字符串是char[]
还是没有意义的(我不确定是否是)那么,当string是引用类型时,即使它错误地假定string是内联存储的,为什么仍能工作并打印字符串呢?
注意:需要具有
System.Runtime.CompilerServices.Unsafe
nuGet软件包和C#7.3+的.NET Core 2.0+。 最佳答案
因为字符串确实是内联存储的。您的假设问题在于字符串不是普通对象,而是CLR作为特殊情况处理的(可能出于性能原因)。
对于对象,由于字符串是唯一的成员,因此这自然是分配内存的最有效方法。尝试在您的字符串成员之后添加更多成员,这样您的代码就会中断。
以下是有关如何在CLR中存储字符串的一些参考
https://mattwarren.org/2016/05/31/Strings-and-the-CLR-a-Special-Relationship/
https://codeblog.jonskeet.uk/2011/04/05/of-memory-and-strings/
编辑:我没有核对,但我相信您在抵销额后面的推理已关闭。 36 = 24(对象的大小)+ 8(字符串标题?)+ 4(int的大小),而对于struct,24字节变为0,因为它没有标题。