将带有 LayoutKind.Explicit 的结构中的额外私有字段排除在结构布局的一部分之外

Exclude extra private field in struct with LayoutKind.Explicit from being part of the structure layout

假设我们有一个结构:

[StructLayout(LayoutKind.Explicit, Size=8)] // using System.Runtime.InteropServices;
public struct AirportHeader {
    [FieldOffset(0)]
    [MarshalAs(UnmanagedType.I4)]
    public int Ident; // a 4 bytes ASCII : "FIMP" { 0x46, 0x49, 0x4D, 0x50 }
    [FieldOffset(4)]
    [MarshalAs(UnmanagedType.I4)]
    public int Offset;
}

我想要的:对于此结构中的字段 Ident,都可以直接访问类型 stringint 值,不会破坏结构的 8 字节大小,也不必每次都从 int 值计算字符串值。

int 结构中的字段 Ident 很有趣,因为如果匹配,我可以快速与其他标识进行比较,其他标识可能来自与此结构无关的数据,但是采用相同的 int 格式。

问题:有没有办法定义一个不属于结构布局的字段?喜欢:

[StructLayout(LayoutKind.Explicit, Size=8)]
public struct AirportHeader {
    [FieldOffset(0)]
    [MarshalAs(UnmanagedType.I4)]
    public int Ident; // a 4 bytes ASCII : "FIMP" { 0x46, 0x49, 0x4D, 0x50 }
    [FieldOffset(4)]
    [MarshalAs(UnmanagedType.I4)]
    public int Offset;
    
    [NoOffset()] // <- is there something I can do the like of this
    string _identStr;
    public string IdentStr {
        get { // EDIT ! missed the getter on this property
            if (string.IsNullOrEmpty(_identStr)) _identStr =
                System.Text.Encoding.ASCII.GetString(Ident.GetBytes());
            // do the above only once. May use an extra private bool field to go faster.
            return _identStr;
        }
    }
}

PS :我使用指针('*'和'&',不安全)因为我需要处理字节顺序(本地系统,二进制files/file 格式,网络)和快速类型转换,快速数组填充。我还使用了多种 Marshal 方法(固定字节数组上的结构),以及一些 PInvoke 和 COM 互操作。太糟糕了,我正在处理的一些程序集还没有对应的 dotNet。


TL;DR;仅供参考

问题就是这样,我只是不知道答案。以下应该回答大多数问题,如“其他方法”,或“为什么不这样做”,但可以忽略,因为答案是直截了当。不管怎样,我先发制人地把所有东西都放好,所以从一开始就很清楚我想做什么。 :)

Options/Workaround我目前正在使用(或考虑使用):

  1. 创建一个每次计算字符串值的getter(不是字段):

    public string IdentStr {
        get { return System.Text.Encoding.ASCII.GetString(Ident.GetBytes()); }
        // where GetBytes() is an extension method that converts an int to byte[]
    }
    

    这种方法在完成工作时表现不佳:GUI 显示默认航班数据库中的飞机,并以一秒的刷新率从网络中注入其他航班(我应该将其增加到 5 秒) .我在一个区域内有大约 1200 个航班,涉及 2400 个机场(出发和到达),这意味着我每秒有 2400 次调用上述代码以在 DataGrid 中显示身份。

  2. 创建另一个结构(或class),其唯一目的是管理 GUI 端的数据,当不是 reading/writing 到流或文件时。也就是说,读 具有显式布局结构的数据。使用创建另一个结构 字段的字符串版本。使用图形用户界面。那将执行 从整体上看更好,但是,在定义过程中 游戏二进制文件的结构,我已经有 143 个结构了 那种(只有旧版本的游戏数据;有一堆我还没有写,我计划为最新的数据类型添加结构)。 ATM,超过半数需要额外加一个或多个 有意义的用途。如果我是唯一一个使用程序集的人也没关系,但是 其他用户可能会迷失 AirportHeaderAirportHeaderExAirportEntryAirportEntryExAirportCoords, AirportCoordsEx...我会避免这样做。

  3. 优化 选项 1 使计算执行得更快(感谢 SO, 有很多想法需要寻找——目前正在研究这个想法)。对于 Ident 字段,我 我想我可以使用指针(我会的)。已经为我必须以小字节序显示和 read/write 以大字节显示的字段执行此操作 字节序。还有其他值,例如 4x4 网格信息 打包在单个 Int64(ulong)中,需要移位到 公开实际值。 GUID 或对象相同 pitch/bank/yaw.

  4. 尝试利用重叠字段(正在学习)。这适用于 GUID。也许它可能适用于 Ident 示例,如果 MarshalAs 可以约束 ASCII 字符串的值。然后我只需要指定相同的 FieldOffset,在本例中为“0”。但我不确定设置字段 value (entry.FieldStr = "FMEP";) 实际上在托管代码端使用 Marshal 约束。我的理解是它会将字符串存储在托管端的 Unicode 中(?)。 此外,这不适用于打包位(包含 几个值,或连续的字节托管值必须是 位移)。我相信不可能指定值的位置、长度和格式 在位级别。

何必呢?上下文 :

我正在定义一堆结构来解析字节数组 (IO.File.ReadAllBytes) 或流中的二进制数据,并将它们写回,与游戏相关的数据。应用程序逻辑应该使用结构来按需快速访问和操作数据。程序集预期的功能是读取、验证、编辑、创建和写入,在游戏范围之外(插件构建、控制)和在游戏范围之内(API、实时修改或监控)。其他目的是理解二进制文件(十六进制)的内容,并利用这种理解来构建游戏中缺少的内容。

程序集的目的是为 c# 插件贡献者提供现成可用的基础组件(我不打算使代码可移植)。为游戏创建应用程序或处理从源代码到编译成游戏二进制文件的插件。有一个 class 可以将文件的全部内容加载到内存中很好,但是某些上下文要求您不要这样做,并且只从文件中检索必要的内容,因此选择了结构模式。

我需要弄清楚信任和法律问题(受版权保护的数据),但这不在主要关注范围之内。如果那很重要,微软多年来确实提供了 public 可免费访问的 SDK,在游戏的早期版本中公开二进制结构,为了我正在做的事情(我不是第一个,也可能不是最后一个)这样做)。不过,我不敢公开未记录的二进制文件(例如最新的游戏数据),也不敢助长对版权 materials/binaries.

的版权侵犯

我只是想确认是否有办法让私有字段不成为结构布局的一部分。天真的信念 ATM 是“那是不可能的,但有变通办法”。只是我的 c# 经验很少,所以也许我错了,为什么我要问。 谢谢!


正如所建议的,有几种方法可以完成这项工作。这是我在结构中提出的 getters/setters。稍后我将衡量每个代码在各种场景下的表现。在许多情况下,dict 方法非常诱人,我需要一个可直接访问的 (59000) 个机场的全球数据库,其中包括跑道和停车位(不仅仅是 Ident),但结构字段之间的快速检查也很有趣。

    public string IdentStr_Marshal {
        get {
            var output = "";
            GCHandle pinnedHandle; // CS0165 for me (-> c# v5)
            try { // Fast if no exception, (very) slow if exception thrown
                pinnedHandle = GCHandle.Alloc(this, GCHandleType.Pinned);
                IntPtr structPtr = pinnedHandle.AddrOfPinnedObject();
                output = Marshal.PtrToStringAnsi(structPtr, 4);
                // Cannot use UTF8 because the assembly should work in Framework v4.5
            } finally { if (pinnedHandle.IsAllocated) pinnedHandle.Free(); }
            return output;
        }
        set {
            value.PadRight(4);  // Must fill the blanks - initial while loop replaced (Charlieface's)
            IntPtr intValuePtr = IntPtr.Zero;
            // Cannot use UTF8 because some users are on Win7 with FlightSim 2004
            try { // Put a try as a matter of habit, but not convinced it's gonna throw.
                intValuePtr = Marshal.StringToHGlobalAnsi(value);
                Ident = Marshal.ReadInt32(intValuePtr, 0).BinaryConvertToUInt32(); // Extension method to convert type.
            } finally { Marshal.FreeHGlobal(intValuePtr); // freeing the right pointer }
        }
    }
    
    public unsafe string IdentStr_Pointer {
        get {
            string output = "";
            fixed (UInt32* ident = &Ident) { // Fixing the field
                sbyte* bytes = (sbyte*)ident;
                output = new string(bytes, 0, 4, System.Text.Encoding.ASCII); // Encoding added (@Charlieface)
            }
            return output;
        }
        set {
            // value must not exceed a length of 4 and must be in Ansi [A-Z,0-9,whitespace 0x20].
            // value validation at this point occurs outside the structure.
            fixed (UInt32* ident = &Ident) { // Fixing the field
                byte* bytes = (byte*)ident;
                byte[] asciiArr = System.Text.Encoding.ASCII.GetBytes(value);
                if (asciiArr.Length >= 4) // (asciiArr.Length == 4) would also work
                    for (Int32 i = 0; i < 4; i++) bytes[i] = asciiArr[i];
                else {
                    for (Int32 i = 0; i < asciiArr.Length; i++) bytes[i] = asciiArr[i];
                    for (Int32 i = asciiArr.Length; i < 4; i++) bytes[i] = 0x20;
                }
            }
        }
    }
    
    static Dictionary<UInt32, string> ps_dict = new Dictionary<UInt32, string>();
    
    public string IdentStr_StaticDict {
        get {
            string output; // logic update with TryGetValue (@Charlieface)
            if (ps_dict.TryGetValue(Ident, out output)) return output;
            output = System.Text.Encoding.ASCII.GetString(Ident.ToBytes(EndiannessType.LittleEndian));
            ps_dict.Add(Ident, output);
            return output;
        }
        set { // input can be "FMEE", "DME" or "DK". length of 2 characters is the minimum.
            var bytes = new byte[4]; // Need to convert value to a 4 byte array
            byte[] asciiArr = System.Text.Encoding.ASCII.GetBytes(value); // should be 4 bytes or less
            // Put the valid ASCII codes in the array.
            if (asciiArr.Length >= 4) // (asciiArr.Length == 4) would also work
                for (Int32 i = 0; i < 4; i++) bytes[i] = asciiArr[i];
            else {
                for (Int32 i = 0; i < asciiArr.Length; i++) bytes[i] = asciiArr[i];
                for (Int32 i = asciiArr.Length; i < 4; i++) bytes[i] = 0x20;
            }
            Ident = BitConverter.ToUInt32(bytes, 0); // Set structure int value
            if (!ps_dict.ContainsKey(Ident)) // Add if missing
                ps_dict.Add(Ident, System.Text.Encoding.ASCII.GetString(bytes));
        }
    }

这是不可能的,因为结构必须以特定顺序包含其所有值。通常此顺序由 CLR 本身控制。如果你想改变数据顺序的顺序,你可以使用StructLayout。但是,您不能排除某个字段,否则该数据根本不存在于内存中。

您可以使用指针直接指向该字符串,并在您的结构中结合 StructLayout 使用它,而不是字符串(这是一种引用类型)。要获取此字符串值,您可以使用直接从非托管内存中读取的只读 属性。

正如其他人所提到的,不可能从结构中排除字段以进行编组。

您也不能在大多数地方将指针用作 string

如果不同的可能字符串的数量相对较少(而且它可能会,因为它只有 4 个字符),那么 你可以使用静态 Dictionary<int, string> 作为一种字符串-实习机制.

然后你写一个属性到add/retrieve真正的字符串。

请注意,字典访问是 O(1),散列 int 只是 returns 本身,所以它会非常非常快,但会占用一些内存。

[StructLayout(LayoutKind.Explicit, Size=8)]
public struct AirportHeader
{
    [FieldOffset(0)]
    [MarshalAs(UnmanagedType.I4)]
    public int Ident; // a 4 bytes ASCII : "FIMP" { 0x46, 0x49, 0x4D, 0x50 }

    [FieldOffset(4)]
    [MarshalAs(UnmanagedType.I4)]
    public int Offset;
    

    static Dictionary<int, string> _identStrings = new Dictionary<int, string>();

    public string IdentStr =>
        _identStrings.TryGetValue(Ident, out var ret) ? ret :
            (_identStrings[Ident] = Encoding.ASCII.GetString(Ident.GetBytes());
}