【问题标题】:Exclude extra private field in struct with LayoutKind.Explicit from being part of the structure layout使用 LayoutKind.Explicit 将结构中的额外私有字段排除在结构布局的一部分之外
【发布时间】:2021-09-16 11:18:47
【问题描述】:

假设我们有一个结构:

[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 值计算一个字符串值。

该结构中的字段Ident int 很有趣,因为如果它们匹配,我可以快速与其他标识进行比较,其他标识可能来自与该结构无关但在同一个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:我使用指针('*' 和 '&',不安全)因为我需要处理字节序(本地系统、二进制文件/文件格式、网络)和快速类型转换,快速阵列填充。我还使用了多种Marshal 方法(修复字节数组上的结构),以及一些PInvoke 和COM 互操作。太糟糕了,我正在处理的一些程序集还没有对应的 dotNet。


TL;DR;仅供参考

问题就是它的全部,我只是不知道答案。以下应该回答大多数问题,例如“其他方法”或“为什么不这样做”,但可以忽略,因为答案很简单。无论如何,我先发制人地把所有东西都放在一边,所以从一开始就很清楚我要做什么。 :)

我目前正在使用的选项/解决方法(或正在考虑使用):

  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. 创建另一个结构(或类),其唯一目的是管理 GUI 端的数据,当不读取/写入流或文件时。这意味着,阅读 具有显式布局结构的数据。创建另一个结构 字段的字符串版本。使用 GUI。那将执行 从整体上看更好,但是,在定义的过程中 游戏二进制文件的结构,我已经有 143 个结构了 那种(只是旧版本的游戏数据;有一堆我还没有写,我计划为最新的数据类型添加结构)。 ATM,超过一半需要一个或多个额外的 字段是有意义的使用。如果我是唯一一个使用该程序集的人也没关系,但是 其他用户可能会迷路AirportHeaderAirportHeaderEx, AirportEntry, AirportEntryEx, AirportCoords, AirportCoordsEx.... 我会避免这样做。

  3. 优化选项 1 以使计算执行得更快(感谢 SO, 有很多想法要寻找——目前正在研究这个想法)。对于 Ident 字段,我 我想我可以使用指针(我会的)。已经为我必须以小端显示并以大端读/写的字段做这件事 字节序。还有其他值,例如 4x4 网格信息 打包在一个 Int64 (ulong) 中,需要移位到 暴露实际值。 GUID 或对象俯仰/倾斜/偏航也是如此。

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

何必呢?上下文

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

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

我需要弄清楚信任和法律问题(受版权保护的数据),但这超出了主要关注的范围。如果那样的话,微软多年来确实提供了公开的可免费访问的 SDK,在以前版本的游戏中公开二进制结构,目的是为了我正在做的事情(我不是第一个也可能不是最后一个这样做的)。不过,我不敢公开未记录的二进制文件(例如最新的游戏数据),也不敢促成对受版权保护的材料/二进制文件的侵犯版权行为。

我只是在询问是否有办法让私有字段不属于结构布局的一部分。天真的信念 ATM 是“这是不可能的,但有解决方法”。只是我的 c# 经验非常少,所以也许我错了,为什么我问。 谢谢!


正如建议的那样,有几种方法可以完成工作。这是我在结构中提出的 getter/setter。稍后我将测量每个代码在各种场景中的执行情况。 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));
        }
    }

【问题讨论】:

  • 如果您有更多需要帮助的代码,您真的应该打开另一个问题。思考点:在 marshal 版本中,try/catch 如果没有例外就非常有效,如果是则非常慢。将GCHandle.Alloc 也放在try 中。使用PadRight 而不是while。 Setter pinnedHandle 没有被使用,删除它,而是确保释放 finally 中的 HGlobal 指针。指针版本:您需要将Encoding.ASCII 提供给new string。您可能要考虑直接使用Encoding.GetBytes 指针版本。字典版本:TryGetValue 防止额外查找
  • 感谢您的承诺。在代码中进行了更改。我不能将指针分配放在 try 中(否则 finally 无法到达指针变量)。别担心,我会读很多关于 c# 的内容(只是从 vb 开始——我是新手)。我只是习惯性地写了所有内容(包括我的想法)(我知道这可能很烦人),我无意进行代码审查。无论如何,这里可能已经回答了所有问题,只是让 3 get/set 看看,但最后,我必须写一个充分利用它们的问题。 :) 谢谢。
  • GCHandle pinnedHandle; try { GCHandle.Alloc(.... } finally { if (pinnedHandle.IsAllocated) pinnedHandle.Free(); }
  • 捂脸,我太笨了。谢谢。

标签: c# struct field layoutkind.explicit


【解决方案1】:

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

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

【讨论】:

  • 谢谢。我明白了,我想要的不存在。您能否具体说明如何从结构内部创建指向外部字符串的仅 getter 持久指针?你的意思是写一个 getter,创建一个指向字符串的指针,从 int 值初始化字符串值......但是每次我访问 getter 时不会调用该指针的初始化吗?对不起,也许我都搞错了。问题是,我先从流中获取 int,然后才能将其转换为 ASCII 字符串。
  • 您已经在“1.”中发布了 getter 的代码。可以使用 Marshal (docs.microsoft.com/de-de/dotnet/api/…)、(docs.microsoft.com/de-de/dotnet/api/…) 创建指针。
  • 感谢您帮助我优化代码。我在帖子末尾添加了 3 个 getter/setter,其中一个使用 Marshal 方法。我会在以后做我理解称为基准测试的事情(我不是专家)来决定我将坚持使用的代码。再次感谢您的宝贵时间。
【解决方案2】:

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

在大多数情况下,您也不能将指针用作string

如果不同的可能字符串的数量相对较少(可能会,因为它只有 4 个字符),那么 您可以使用静态 Dictionary&lt;int, string&gt; 作为一种字符串暂留机制.

然后你写一个属性来添加/检索真正的字符串。

请注意,字典访问是O(1),对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;
    

    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());
}

【讨论】:

    猜你喜欢
    • 2020-04-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-02-16
    • 2017-07-12
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多