[C#.NET 拾遗补漏]14:使用结构体实现共用体

动态规划解题方法

在 C 和 C# 编程语言中,结构体(Struct)是值类型数据结构,它使得一个单一变量可以存储多种类型的相关数据。在 C 语言中还有一种和结构体非常类似的语法,叫共用体(Union),有时也被直译为联合或者联合体。而在 C# 中并没有共用体这样一个定义,本文将介绍如何使用 C# 实现 C 语言中的共用体。

理解 C 语言的共用体

在 C 语言中,共用体是一种特殊的数据类型,允许你使用相同的一段内存空间存储不同的成员数据。光看定义有点抽象,我们来看一个 C 语言的共用体示例:

#include <stdio.h>

union data{
    int n;
    char ch;
    short m;
};

int main(){
    union data a;
    printf("%d, %d\n", sizeof(a), sizeof(union data) );
    a.n = 0x40;
    printf("%X, %c, %hX\n", a.n, a.ch, a.m);
    a.ch = '9';
    printf("%X, %c, %hX\n", a.n, a.ch, a.m);
    a.m = 0x2059;
    printf("%X, %c, %hX\n", a.n, a.ch, a.m);
    a.n = 0x3E25AD54;
    printf("%X, %c, %hX\n", a.n, a.ch, a.m);

    return 0;
}

运行结果:

4, 4
40, @, 40
39, 9, 39
2059, Y, 2059
3E25AD54, T, AD54

要想理解上面的输出结果,就得了解共用体各个成员在内存中的分布。此示例中的 data 各个成员在内存中的分布示意图如下:

也就是说共用体的所有成员占用的是同一段内存,所占内存等于最长的成员占用的内存,修改一个成员会影响其它所有成员。而结构体的各个成员占用的是各自不同的内存,所占内存大于等于所有成员占用的内存的总和(成员之间可能会存在缝隙),成员相互之间没有影响。这是共用体和结构的主要区别。

使用 C# 实现共用体

和 C 语言不同的是,C# 中没有共用体的定义。那在 C# 中如何来实现这种定义呢?

C# 不仅可以实现共用体,而且可以实现比 C 语言更强大的共用体。C 语言的共用体每个成员在共用的内存中都必须从相同的起始位置开始存储,而在 C# 中可以指定各成员的起始位置(相对偏移)。好处是,不仅可以节省内存空间,还可以实现一些自动转换操作。

以 IP 地址的存储为例,IP 地址是以 4 段数字来表示的(如 192.168.1.10),每一段是一个字节(Byte),长度是 2^8,最大值是 255。我们可以用很多类型来表示 IP 地址,比如字符串、整型、自定义类和结构等。但如果我们有时要访问或修改其中一段,怎样存储最为方便呢?

我们可以使用 C# 的显示布局结构体来实现类似 C 语言中的共用体,以方便灵活地操作 IP 地址的每一段。实现方式如下:

借助Docker搭建JMeter+Grafana+Influxdb监控平台

using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Explicit)]
public struct IpAddress
{
    // FieldOffset 表示偏移的位置(以字节为单位)
    // sizeof(int) = 4, sizeof(byte) = 1
    [FieldOffset(0)] public int Address;
    [FieldOffset(0)] public byte Byte1;
    [FieldOffset(1)] public byte Byte2;
    [FieldOffset(2)] public byte Byte3;
    [FieldOffset(3)] public byte Byte4;

    public IpAddress(int address) : this()
    {
        // 给 Address 赋值时,所有成员的值都会自动被修改
        Address = address;
    }

    public override string ToString() => $"{Byte1}.{Byte2}.{Byte3}.{Byte4}";
}

这里我们使用了 StructLayout 特性标注了 IpAddress,声明其内存分布是显示(Explicit)的,然后使用 FieldOffset 特性来标注成员在共用内存中相对起始位置的偏移量(以字节为单位)。

如此我们就用 C# 实现了和 C 语言一样的共用体。可能你不能马上体会这样实现的妙处,让来我们来看一个应用场景。

假设我要在 IP 段内随机生成一个 IP,比如前两段不变,后两段随机,形如:192.163.X.X。使用上面定义好的“共用体”,我们可以这样做:

var ip = new IpAddress(new Random().Next());
Console.WriteLine($"{ip} = {ip.Address}");
ip.Byte1 = 192;
ip.Byte2 = 168;
Console.WriteLine($"{ip} = {ip.Address}");

输出结果:

47.29.249.122 = 2063146287
192.168.249.122 = 2063182016

这样不仅节省内存,而且可以很灵活方便地读取和修改 IP 中的某一段。由于成员 Address 和其它成员共用内存,所以修改一个成员,其余就自动修改。

共用体作为另一个共用体的成员

既然“共用体”是值类型,那么共用体自然也可以作为作为另一个共用体的成员。让我们来看一个较为复杂的例子,使用共用体实现由协议、IP 和端口三部分组成的服务端地址的表示,形如:协议://IP:端口。

using System;
using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Explicit)]
public struct IpAddress
{
    [FieldOffset(0)] public int Address;
    [FieldOffset(0)] public byte Byte1;
    [FieldOffset(1)] public byte Byte2;
    [FieldOffset(2)] public byte Byte3;
    [FieldOffset(3)] public byte Byte4;

    public IpAddress(int address) : this()
    {
        Address = address;
    }

    public override string ToString() => $"{Byte1}.{Byte2}.{Byte3}.{Byte4}";
}

public enum Protocol : byte { http, https, ftp, sftp, tcp };

[StructLayout(LayoutKind.Explicit)]
public struct Server
{
    [FieldOffset(0)] public IpAddress Address;
    [FieldOffset(4)] public ushort Port;
    [FieldOffset(6)] public Protocol Protocol;
    [FieldOffset(0)] public long Payload;

    public Server(IpAddress addr, ushort port, Protocol prot) : this()
    {
        Address = addr;
        Port = port;
        Protocol = prot;
    }

    public Server(long payload)
    {
        // 参数长度可能不足填满每个成员,所以这里先对成员设初始值
        Address = new IpAddress(0);
        Port = 80;
        Protocol = Protocol.http;

        // 填值
        Payload = payload;
    }

    public Server Copy() =>  new Server(Payload);

    public override string ToString() => $"{Protocol}://{Address}:{Port}";
}

我们来用一段测试代码验证一下这个 Server 结构体的内存使用情况:

var ip = new IpAddress(new Random().Next());
Console.WriteLine($"Size: {Marshal.SizeOf(ip)} bytes. Value: {ip.Address} = {ip}");

var s1 = new Server(ip, 8080, Protocol.https);
var s2 = new Server(s1.Payload);
s2.Address.Byte1 = 100;
s2.Protocol = Protocol.ftp;
Console.WriteLine($"Size: {Marshal.SizeOf(s1)} bytes. Value: {s1.Address} = {s1}");
Console.WriteLine($"Size: {Marshal.SizeOf(s2)} bytes. Value: {s2.Address} = {s2}");

输出结果:

Size: 4 bytes. Value: 2102736192 = 64.53.85.125
Size: 8 bytes. Value: 64.53.85.125 = https://64.53.85.125:8080
Size: 8 bytes. Value: 100.53.85.125 = ftp://100.53.85.125:8080

示例中,IP 地址偏移 0 字节,长度为 4 字节;端口号偏移 4 字节,长度为 2 字节;协议偏移 6 字节,长度为 1 字节。总长度应为 4+2+1=7 字节,但实际打印出来却是 8 字节,请问是为什么?

参考:https://bit.ly/3qmH92V

ASP.NET Core中的数据保护

给TA买糖
共{{data.count}}人
人已赞赏
经验教程

MyBatis初级实战之一:Spring Boot集成

2021-1-15 7:56:00

经验教程

动态规划解题方法

2021-1-15 8:46:00

⚠️
免责声明:根据《计算机软件保护条例》第十七条规定“为了学习和研究软件内含的设计思想和原理,通过安装、显示、传输或者存储软件等方式使用软件的,可以不经软件著作权人许可,不向其支付报酬。”您需知晓本站所有内容资源均来源于网络,仅供用户交流学习与研究使用,版权归属原版权方所有,版权争议与本站无关,用户本人下载后不能用作商业或非法用途,需在24个小时之内从您的电脑中彻底删除上述内容,否则后果均由用户承担责任;如果您访问和下载此文件,表示您同意只将此文件用于参考、学习而非其他用途,否则一切后果请您自行承担,如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。 本站为个人博客非盈利性站点,所有软件信息均来自网络,所有资源仅供学习参考研究目的,并不贩卖软件,不存在任何商业目的及用途,网站会员捐赠是您喜欢本站而产生的赞助支持行为,仅为维持服务器的开支与维护,全凭自愿无任何强求。本站部份代码及教程来源于互联网,仅供网友学习交流,若您喜欢本文可附上原文链接随意转载。
无意侵害您的权益,请发送邮件至 momeis6@qq.com 或点击右侧 私信:momeis 反馈,我们将尽快处理。
0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
今日签到
有新私信 私信列表
搜索