结构中包含结构,可以更改内部结构类型。

6

这个问题并不需要太多的解释,以下是我所拥有的内容:

public struct PACKET_HEADER
    {
        public string computerIp;
        public string computerName;
        public string computerCustomName;
    };

    public struct PACKET
    {
        public PACKET_HEADER pktHdr;
        public PACKET_DATA pktData;
    };


    public struct PACKET_DATA
    {
        public Command command;
        public string data;  
    };

    public struct DATA_MESSAGE
    {
        public string message;
    };

    public struct DATA_FILE
    {
        public string fileName;
        public long fileSize;       
    };

基本上我希望 PACKET_DATA 中的数据字段可以是 DATA_FILE 或 DATA_MESSAGE。我知道类型需要更改,但我不知道应该改为什么,泛型是一个选择吗?
最终结果应该是我可以执行以下操作之一: pktData.data.fileName 或 pktData.data.message 编辑:
我可以这样做:
public struct PACKET_DATA
{
    public Command command;
    public string data;
    public DATA_MESSAGE data_message;
    public DATA_FILE data_file;
};

当我不需要数据消息或文件时,只需将它们设置为null?这会对序列化/字节数组和发送的数据有什么影响?如果我使用类,我不会有同样的问题吗?
public struct PACKET_MESSAGE
{
    public PACKET_HEADER pktHdr;
    public Command command;
    public DATA_MESSAGE pktData;
};

public struct PACKET_FILE
{
    public PACKET_HEADER pktHdr;
    public Command command;
    public DATA_FILE pktData;
};

编辑 3

我有一台可以使用我的原始示例运行的消毒器和脱菌器,如果没有任何问题,那么实际的序列化就完成了。

编辑 4

似乎一切都在运行,除了一个问题:我的序列化器出现“尝试读取或写入受保护的内存。这通常是其他内存已损坏的迹象。” 我将在发布工作解决方案时查看它 :)

编辑 5

    public static byte[] Serialize(object anything)
    {
        int rawsize = Marshal.SizeOf(anything);
        byte[] rawdatas = new byte[rawsize];
        GCHandle handle = GCHandle.Alloc(rawdatas, GCHandleType.Pinned);
        IntPtr buffer = handle.AddrOfPinnedObject();
        Marshal.StructureToPtr(anything, buffer, false);
        handle.Free();
        return rawdatas;
    }

    public static object Deserialize(byte[] rawdatas, Type anytype)
    {
        int rawsize = Marshal.SizeOf(anytype);
        if (rawsize > rawdatas.Length)
            return null;
        GCHandle handle = GCHandle.Alloc(rawdatas, GCHandleType.Pinned);
        IntPtr buffer = handle.AddrOfPinnedObject();
        object retobj = Marshal.PtrToStructure(buffer, anytype);
        handle.Free();
        return retobj;
    } 

最终版

结构体:

public struct PACKET_HEADER
{
    public string computerIp;
    public string computerName;
    public string computerCustomName;
};

public struct PACKET
{
    public PACKET_HEADER pktHdr;
    public PACKET_DATA pktData;
};

public struct PACKET_DATA
{
    public Command command;
    public IDATA data;
    public T GetData<T>() where T : IDATA
    {
        return (T)(data);
    }
}

public interface IDATA { }

public struct DATA_MESSAGE : IDATA
{
    public string message;
}

public struct DATA_FILE : IDATA
{
    public string fileName;
    public long fileSize;
}

如何创建一个新的数据包(可能可以合并在一起):
    public static PACKET CreatePacket(Command command)
    {
        PACKET packet;
        packet.pktHdr.computerIp = Settings.ComputerIP;
        packet.pktHdr.computerName = Settings.ComputerName;
        packet.pktHdr.computerCustomName = Settings.ComputerCustomName;

        packet.pktData.command = command;
        packet.pktData.data = null;

        return packet;
    }

    public static PACKET CreatePacket(Command command, DATA_MESSAGE data_message)
    {
        PACKET packet;
        packet.pktHdr.computerIp = Settings.ComputerIP;
        packet.pktHdr.computerName = Settings.ComputerName;
        packet.pktHdr.computerCustomName = Settings.ComputerCustomName;

        packet.pktData.command = command;
        packet.pktData.data = data_message;

        return packet;
    }

    public static PACKET CreatePacket(Command command, DATA_FILE data_file)
    {
        PACKET packet;
        packet.pktHdr.computerIp = Settings.ComputerIP;
        packet.pktHdr.computerName = Settings.ComputerName;
        packet.pktHdr.computerCustomName = Settings.ComputerCustomName;

        packet.pktData.command = command;
        packet.pktData.data = data_file;

        return packet;
    }

(de)序列化如上所述。

简单的例子:

PACKET packet = Packet.CreatePacket(command, data_file);
byte[] byData = Packet.Serialize(packet);

另一端:

PACKET returnPacket = (PACKET)Packet.Deserialize(socketData.dataBuffer, typeof(PACKET));
                        // Get file
string fileName = returnPacket.pktData.GetData<DATA_FILE>().fileName;
long fileSize = returnPacket.pktData.GetData<DATA_FILE>().fileSize;

一切似乎都很正常,没问题 :)

只是就您的编辑进行澄清:结构体作为值类型,永远不会为 null。您需要一个指示哪个成员有效的标志。 - Paul Wheeler
是的,我刚意识到它们不能为null。我在考虑使用两种类型的数据包,如上所述,这样会更有意义吗? - Metalstorm
好的,但请记住反序列化代码需要确定类型。我实际上很喜欢你的两个子结构版本,你只需要一个 bool isMessagePacket; - Paul Wheeler
关于您的内存访问错误,您使用什么方法将字节数组转换为结构体,反之亦然?由于您的结构体已经包含了字符串,而字符串是一个类引用,因此不会被内联存储在结构体中,我不确定为什么您要使用结构体。您可以直接使用类,标记整个类为[Serializable],并使用BinaryFormatter进行序列化。 - Paul Wheeler
@Paul,请看一下第五次编辑,嗯,类似乎是更好的方式。 - Metalstorm
串行化/反串行化没有问题,问题在于我仍然发送了一个旧的数据包格式,基本上是将字符串转换为字节数组,所以当它尝试进行反序列化时,它没有完整的信息。所以一切看起来都很好,不过我还是乐意听取进一步的建议 :) - Metalstorm
4个回答

4
这个问题需要一个明确的答案,所以我会尝试总结一下:
如果您想将C#数据结构转换为字节数组,可以使用结构体和Marshaling,或使用类(或结构体,但为什么要这样做)和序列化框架(如BinaryFormatter),或自定义序列化逻辑(如BinaryWriter)。我们可以就哪种方法更好进行辩论,但现在假设我们选择了结构体,并且使用了Marshaling。虽然我会说,结构体非常有限,应该主要用于与Win32 API函数交互时必要的情况。
因此,问题是,我们有一个容器结构体,其中可能包含两种类型的子结构体。如果您要Marshal结构体,则不能使用诸如泛型或为子结构体类型使用通用接口之类的东西。基本上,您的唯一选择是让容器同时具有两个结构体和一个布尔标志,指示要使用哪个结构体。这会增加数据包的大小,因为您还会发送未使用的子结构体。
在这种情况下,结果如下:
public struct PACKET_DATA
{
    public Command command;
    public string data;
    public bool is_message_packet;
    public DATA_MESSAGE data_message;
    public DATA_FILE data_file;
};

尽管如此,在您的情况下,使用结构体和Marshalling只会在您自己的进程中起作用,因为您的结构体包含字符串。当结构体包含指向非固定长度字符串的指针时,这些字符串被分配到其他位置,不会成为您复制的字节数组的一部分,只有指向它们的指针才会成为数组的一部分。您还需要调用Marshal.DestroyStructure函数,并传递给StructureToPtr的IntPtr来清理这些字符串资源。
所以故事的寓意是:您能否制作最初要求的那样的结构体:可以。但您应该像现在这样使用它们吗:不应该。因为您正在尝试发送可变大小的数据结构(我假设是因为结构体被称为“PACKET”),因此结构体无法起作用,您真的需要使用某种序列化框架或自定义序列化逻辑。

看一下我的最终编辑,Paul,所有的东西都似乎正常工作。 - Metalstorm
我认为它“工作”的事实是一种幻想,因为你正在单个进程中使用测试代码。拿起那段代码,尝试通过网络套接字发送数据,我几乎可以肯定它会失败。这是因为你的结构体包含的字符串并没有与结构体一起存储,序列化后的结构体只是指向它们的指针,它们被分配在其他地方。 - Paul Wheeler
此外,当一个结构体包含一个接口成员时,它会将该成员视为引用。换句话说,父结构体包含一个指针,而不是内联包含子结构体。这绝对行不通。 - Paul Wheeler

1
    public struct PACKET_DATA
    {
        public IData data;
        public T GetData<T>() where T : IDATA
        {
           return (T)data;
        }
    }

    public interface IDATA { }

    public struct DATA_MESSAGE : IDATA
    {
        public string message;
    }

    public struct DATA_FILE : IDATA
    {
        public string fileName;
        public long fileSize;
    }

PACKET_DATA packetData = new PACKET_DATA();
packetData.data = new DATA_MESSAGE();
var message = packetData.GetData<DATA_MESSAGE>().message;

公共结构体 PACKET { 公共结构体 PACKET_HEADER pktHdr; 公共结构体 PACKET_DATA<> pktData; }; 这不是之前的问题吗?如果在 <> 中放 T,那么 PACKET 结构体也必须带一个 T,这会让它变得混乱。 - Metalstorm
我尝试着实现这个,看起来除了在“return T(data);”中的T下面出现了错误“'T'是一个'type parameter'但被用作'variable'”之外一切都很好。 - Metalstorm
@Metalstorm:这是一个打字错误,应该是(T)data - Cheng Chen

0
如何定义一个虚假接口,让这两个结构体都继承它呢?当然,这并不能解决你的序列化问题,但正如之前所说,你可能需要自定义序列化方法。
public interface IDataType
{
}

public struct PACKET_DATA
{
    public Command command;
    public IDataType data;  
};

public struct DATA_MESSAGE : IDataType
{
    public string message;
};

public struct DATA_FILE : IDataType
{
    public string fileName;
    public long fileSize;       
};

这看起来很不错,但是当我尝试 PACKET packet; packet.pktData.data. 除了标准的ToString等之外,下面什么也没有。 - Metalstorm

0
你可以使用泛型来实现,但是类型参数会传播到PACKET结构体中,可能会使它难以处理并且不是你想要的。
在这里使用struct的目的是什么,而不是类?是为了互操作性吗?(在这种情况下,互操作场景将决定正确的解决方案)。还是为了避免装箱/堆分配?

你可以使用泛型来实现,但是类型参数会传递到 PACKET 结构体中,我猜这会让它难以使用,并且不符合你的要求。是的,我看了一下,但确实变得非常混乱。Packet 结构体(及其内容)被序列化并作为字节数组发送到网络上。目的只是为了在短时间内存储少量信息,我读到结构体比类更适合这样做。 - Metalstorm
你主要是想要高效的序列化吗?如果是,最优的方法是编写自定义序列化器。将这些类型类而非结构体(就垃圾回收而言)的开销并不大。 - Joe Albahari

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接