为什么在 C# 中使用 FieldOffset(0) 会导致 char 数组和字符串的指针不同?

Why does using FieldOffset(0) in C# end up with different pointers for char array and string?

作为对字符串不变性 () 的良好回答的跟进,我已经开始尝试使用这种技术来理解可修改字节的偏移量。

最后我发现对两个引用字段使用 [FieldOffset(0)] 不会使指针具有相同的值。

这是测试:

using System;
using System.Runtime.InteropServices;

namespace ConsoleApp
{
    [StructLayout(LayoutKind.Explicit)]
    public struct MutableString
    {
        [FieldOffset(0)] 
        public readonly string AsString;

        [FieldOffset(0)] 
        public readonly char[] AsCharArray;

        public MutableString(string original)
        {
            AsCharArray = null;
            AsString = original;
        }
    }

    public static class Program
    {
        public static unsafe void Main(string[] args)
        {
            var mutableString = new MutableString("test");

            fixed (char* pString = mutableString.AsString, pCharArray = mutableString.AsCharArray)
            {
                Console.WriteLine((long)pString);    // 2229380919860
                Console.WriteLine((long)pCharArray); // 2229380919864
            }
        }
    }
}

上面的代码打印出不同的数字(当然,确切的值会不时不同)。

差异总是 4 个字节(2 个字符)。

这是 csproj 文件:

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFrameworks>netcoreapp2.0;net47</TargetFrameworks>
        <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    </PropertyGroup>
</Project>

.NET Core dll 和 net47 exe 的行为相同,Debug/Release,x86/x64 构建配置。

主机是 Win10 x64。

我想知道在仅分配 AsString 字段后,我如何在字段中获得具有相同偏移量的 另一个 值?

I'm wondering how it is possible that after assigning only AsString field I'm getting another value in the field with the same offset?

你不是。

如果将两个字段值与 object.ReferenceEquals(mutableString.AsString, mutableString.AsCharArray) 进行比较,您会发现这两个字段是相等的,正如预期的那样。

让您感到困惑的是从 stringchar[] 类型到指针的隐式转换。这两种类型都是托管类型,因此 fixed 语句必须固定对象,return 一个适当的指针指向 char 数据。出错的是这种转换,而不是实际存储在结构中的值。

至于为什么转换出错,是因为padding differences in arrays between 64-bit and 32-bit processes。 .NET Framework(桌面)和 Core 之间的差异出现是因为默认项目设置不同:桌面默认首选 32 位,而 Core 默认不首选 32 位(即 "Prefer 32-bit" 未选中 - 事实上, VS 中的 "Prefer 32-bit" 复选框似乎对核心项目没有任何作用……我必须明确地将平台类型设置为 x86 以获得核心的 32 位进程。

char[] 到指针的隐式转换需要额外的 4 个字节的填充。但是由于您的引用实际上不是对 char[] 对象的引用,而是对 string 对象的引用,因此该填充实际上并不存在,因此指针超出了 4 个字节。

鉴于确实没有理由期望对 string 对象的引用在重新解释为对 char[] 对象的引用时有效 — 对象布局巧合地兼容 32-位过程,但这不是 .NET 规范所承诺的(它是一个实现细节)——我认为这是 "reasonable"。如果您想创建一个有效的联合数据结构,您必须采取自己的保护措施,以确保您只将联合字段解释为您实际设置的字段。