【问题标题】:In C#, how can I reinterpret byte[] as T[], where T is a struct?在 C# 中,如何将 byte[] 重新解释为 T[],其中 T 是一个结构?
【发布时间】:2022-01-15 10:36:25
【问题描述】:

我正在使用 C# (.NET 5)。想象一下,我有一个存储结构数组(比如浮点数)的类:

public class StoresArray
{
    private float[] floats;
}

这个类的数据是从一个序列化的二进制文件中加载的。为了分配floats 数组,我使用辅助函数从序列化文件中读取字节。重要的是,此函数随后会尝试重新解释加载的字节直接作为float[],而不是复制到新数组。

public static class Deserializer
{
    public static float[] Load(string file)
    {
        byte[] bytes = LoadBytesFromFile(file);

        // This is a compiler error, of course.
        return (float[])bytes;
    }
}

预期用途如下:

// Within the StoresArray class...
floats = Deserializer.Load("MyFile.file");

值得注意的是,我试图float[] 存储为成员变量,而不仅仅是在本地迭代byte[]。因此,通过Span<T> (Span<float> floatSpan = MemoryMarshal.Cast<byte, float>(bytes.AsSpan())) 进行投射是不够的。与Memory<T>MarshalMemoryMarshal 关联的函数也同样失败。当然,我可以使用 spans(连同其他方法,如 BitConverter 或不安全指针)从 byte[] 构建 new float[],但这会导致额外的数组分配,如以及转换字节的附加操作。在我提出的问题(动态加载视频游戏资产)的上下文中,我想尽可能优化性能。

在现代 C# 中,是否可以在不产生额外分配的情况下重新解释和存储结构数组?

【问题讨论】:

  • 这取决于您要序列化的格式。
  • 您尝试了Marshal 类的哪些功能,它是如何“失败”的?
  • @dan04 在此上下文 (imo) 中最值得注意的 Marshal 函数是 PtrToStructure,它确实让我成功地创建了 one 结构 (T item = Marshal.PtrToStructure<T>(new IntPtr(address))。不幸的是,它并没有让我像我希望的那样重新解释数组
  • 嗨!我觉得这个问题过早地结束了(就像很多人一样)。尽管链接的问题从根本上归结为相同的答案(不,您不能在 C# 中重新解释强制转换数组),但该问题是在五年前提出的,甚至在 Span<T> 存在之前。此外,我从另一个可能对其他人有价值的问题空间中处理了这个问题(“如何重新解释转换数组?”)。最后,Matthew Watson 下面的回答给出了另一个问题中没有的重要见解(将T[] 直接传递给输入流)。
  • @Boann 在读取大型基元数组(例如双精度数)时绝对不是这种情况。常规(旧式)方法将让您使用 BitConverter 将每个 double 转换为字节数组,以便从流中读取/写入流。我在 BenchmarkDotNet 上的时间表明,使用 Span<T>MemoryMarshal.AsBytes() 在写入和读取 MemoryStream 时快五倍以上。

标签: c# arrays reinterpret-cast


【解决方案1】:

要写,你可以这样做:

public static void WriteArrayToStream<T>(Stream output, T[] array) where T: unmanaged
{
    var span = array.AsSpan();
    var bytes = MemoryMarshal.AsBytes(span);
    output.Write(bytes);
}

为了阅读,你可以这样做:

public static (T[] Result, int Count) ReadArrayFromStream<T>(Stream input, int n) where T: unmanaged
{
    T[] result = new T[n];
    var span   = result.AsSpan();
    var bytes  = MemoryMarshal.AsBytes(span);
    int count  = input.Read(bytes);

    return (result, count/Marshal.SizeOf<T>());
}

注意它返回一个元组,因为如果没有足够的数据可用,只有第一个Count 元素会有有效数据。

这里有一个例子来展示写入和读取 double[] 数组:

MemoryStream mem = new MemoryStream();
double[] data = Enumerable.Range(0, 100).Select(x => (double)x).ToArray();
WriteArrayToStream(mem, data);
Console.WriteLine(mem.Length); // 800

mem.Position       = 0;
var (array, count) = ReadArrayFromStream<double>(mem, 200);
Console.WriteLine(count); // 100
Console.WriteLine(array[42]);  // 42

【讨论】:

  • 感谢您的回答。不幸的是,在这种情况下,我试图读取数据(反序列化),而不是写入文件(序列化)。你说得对,我可以(谢天谢地)在没有额外分配的情况下写作。
  • @Grimelios 你也可以阅读,我会更新答案
  • 这是一种有趣的方法,将T[](作为字节跨度)直接传递给输入流。那......可能会解决我的问题(与大多数 SO 问题一样,我省略了一些复杂性以简化问题)。如果是这样(我必须先修补),我会将此响应标记为已接受。谢谢!
  • 修修补补后,我可以确认 Matthew 回答的关键见解(将 Span&lt;byte&gt; 直接传递到输入流中,而不是先缓冲到 byte[])实现了我反序列化二进制文件的目标以最少的分配。请注意,从这些字节转换多个不同的值时,首先复制到临时缓冲区(例如fileStream.Read(bytes, 0, 4096))仍然经常有用。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-03-31
  • 2021-03-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-09-24
相关资源
最近更新 更多