[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 地址的每一段。实现方式如下:

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

-

精致码农

带你洞悉编程与架构

↑长按图片识别二维码关注,不要错过网海相遇的缘分

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/305720.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

linux新建文件夹灰色,Ubuntu新建文件夹灰了

#问题描述&#xff1a;WIn10Ubuntu双系统&#xff0c;其中WIn10系统和Ubuntu的目录“/”和“/boot”在同一块固态中&#xff0c;剩余的其他文件都在另一块机械硬盘里面。在机械硬盘下的软件(E&#xff1a;&分区5)和文档(F&#xff1a;&分区6)盘中无法新建文件夹&#x…

右击硬盘分区第一项出现Auto的解决办法

最近网上相信有不少的网友和我一样都中了这种病毒&#xff0e;就是说在右键单击你的硬盘的时候第一项不是打开而是&#xff21;&#xff35;&#xff34;&#xff2f;这个选项&#xff0e;而要想在这种情况下打开的话&#xff0c;就必须用右键单击打开&#xff0c;让我们心理真…

使用 C# 9 的records作为强类型ID - 初次使用

强类型ID实体通常是整数&#xff0c;GUID或者string类型&#xff0c;因为数据库直接支持这些类型&#xff0c;但是&#xff0c;如果实体的ID的类型是一样的&#xff0c;比如都是整数的ID&#xff0c;这有可能会出现ID值传错的问题&#xff0c;看下边的示例。public void AddPro…

linux配置文件引用时间,linux时间设置、screen使用、命令分类、hash作用、命令引用及history命令...

一、 生产环境发现一台服务器系统时间产生偏差&#xff0c;造成服务异常解决方法a、 如果服务器硬件时间准备的话&#xff0c;可使用命令&#xff1a;~#] hwclock -s将硬件时钟同步到系统b、date [MMDDhhmm[[CC]YY][.ss]] 分别为&#xff1a;月日小时分年.秒eg:~]# date 101420…

用拖拉实现设备驱动配置(EsayHMI最新驱动配置方式)

以前的IOServer 是用VB6开发&#xff0c;驱动的配置方式和一般的组态软件没有什么很大的区别&#xff0c;配置起来比较麻烦&#xff0c;对一般的生手来说&#xff0c;要费一番功夫才能配置完毕。 总觉得原有的程序和目前的HMI图元配置环境格格不入&#xff0c;所以重新编写了驱…

HttpClientFactory 结合 Polly 轻松实现重试机制

HttpClientFactory 结合 Polly 轻松实现重试机制Intro我们的服务里有一个 API 会去调用第三方的接口&#xff0c;设置了超时时间&#xff0c;最近偶尔会发生超时的情况&#xff0c;微软在提供 HttpClientFactory 的同时&#xff0c;也提供了一个基于 Polly 的一个扩展&#xff…

ASP.NET 状态管理概览

页面每次被回传到服务器的时候都会创建 Web 页面类的一个新实例。在传统的 Web 开发中&#xff0c;这意味着在每次回传的过程中&#xff0c;页面中所有与页面和控件相关联的信息都将被丢失。例如&#xff0c;如果用户在一个文本框中输入了信息&#xff0c;那么在浏览器或者其他…

linux udp端口大数据包,Linux协议栈中UDP数据报从网卡到用户空间流程总结

NAPI驱动流程&#xff1a;中断发生-->确定中断原因是数据接收完毕(中断原因也可能是发送完毕&#xff0c;DMA完毕&#xff0c;甚至是中断通道上的其他设备中断)-->通过netif_rx_schedule将驱动自己的napi结构加入softnet_data的poll_list链表&#xff0c;禁用网卡中断&am…

浅谈.NET技术公司的实习生培养

浅谈.NET技术公司的实习生培养背景近几年.NET开发者市场的越发不景气&#xff0c;一毕业就选择.NET技术的开发者更是少之又少。一方面是公司效益的日益提高&#xff0c;一方面却是招聘优秀人才的速度总是赶不上公司发展的速度&#xff0c;这样的矛盾不断的倾轧着技术管理者的内…

禁止自动播放功能

运行gpedit.msc打开组策略&#xff0c;在“管理模板--系统”右侧双击“关闭自动播放”&#xff0c;然后设置为“已启用”&#xff0c;并在“关闭自动播放”下拉列表中选择“所有驱动器”&#xff0c;最后“确定”即可。这个设置在“计算机配置”和“用户配置”中都有&#xff0…

MBR的Linux分区机制启动过程,linux系统启动流程(MBR)

总&#xff1a;POST---> Boot Squence ---> Bootloader ---> kernel ---> init---> 完成启动系统&#xff1a;CentOS61、按下电源&#xff0c;cpu找到CMOS中的BIOS并运行起来&#xff0c;这个小程序会完成硬件自检。BIOS&#xff1a;Basic Input and Output Sys…

.netcore 极速接入第三方登录

新年新气象&#xff0c;趁着新年的喜庆&#xff0c;肝了十来天&#xff0c;终于发了第一版&#xff0c;希望大家喜欢。如果有不喜欢看文字的童鞋&#xff0c;可以直接看下面的地址体验一下&#xff1a;Github: https://github.com/mrhuo/MrHuo.OAuth唯一官网&#xff1a;https:…

linux watch 文件大小,Linux watch命令的使用

1.命令简介watch 命令以周期性的方式执行给定的命令&#xff0c;并全屏显示执行结果。watch 是一个非常实用的命令&#xff0c;基本所有的 Linux 发行版都带有。如同名字一样&#xff0c;watch 可以帮助监测一个命令的运行结果&#xff0c;省得我们一遍遍地手动运行。比如 tail…

在ISA 2006标准版上定制带宽和限制流量(测试)

最的忙于工作上的事&#xff0c;有点烦躁&#xff0c;很少有心情能静下来去看看书或是做些实验了。这是个不好的现象。 一直想着在ISA SERVER上做一些带宽和流量方面的测试&#xff0c;没有找到较好的软件。偶尔从朋友那得到一款&#xff0c;虽说是英文的&#xff0c;也蛮不错…

.NET 中 async 和 await

前言C# 中的 Async 和 Await 关键字是异步编程的核心。使用这两个关键字可以轻松创建异步方法。使用 async 关键字定义的异步方法简称“异步方法”。异步编程并发的一种形式&#xff0c;它采用furture模式或回调(callback)机制&#xff0c;以避免产生不必要的线程。.Net中futur…

Javascript 常用技巧 [2]

Javascript 常用技巧 [2] /**请问如何去掉主页右面的滚动条&#xff1f; <!-- <body scroll"no"> --> <!-- <body style"overflow-y:hidden"> --> ******************************** 怎样不使用页面的缓存&#xff1f;即每一次打…

mongodb可视化工具 linux,Linux中安装启动MongoDB与可视化工具

MongoDB 提供了 linux 各发行版本 64 位的安装包&#xff0c;你可以在官网下载安装包。下载地址&#xff1a;https://www.mongodb.com/download-center#community下载完安装包&#xff0c;并解压 tgz(以下演示的是 64 位 Linux上的安装) 。curl -O https://fastdl.mongodb.org/…

年轻有为的老黄2020

0x01 不知道开篇要说什么好&#xff0c;那就来个经典的 0x01 吧。这个世界不止苟且&#xff0c;还有诗和远方。工作 工作上面还算是比较忙碌的&#xff0c;在20年也做了很多东西&#xff0c;本来面试进公司的时候title是高开&#xff0c;在基础架构组&#xff0c;光杆部门。过完…

VS.NET调试问题

Q:VS.NET调试问题 A:关于VisualStudio.NET里调试出现的一系列问题&#xff0c;例如没有权限调试Web服务器&#xff0c;不属于Debugger Users组、无法调试等情况&#xff0c;都可以在The VS7 Debugger doesn’t work. What can I do(http://blogs.msdn.co…

linux调用odbc接口乱码,linux中pypyodbc读取GB编码mdb中文乱码解决办法

当前1.3.3版本的pypyodbc在linux系统上面已经可以读取有中文字符的mdb文件,不再出现异常,但是读取的中文字符却全是乱码。下面是根据一些现象的合理推论:mdb文件来自于windows系统,其中的中文字符使用GB编码无疑,但linux系统上mdbtools提供的odbc驱动底层已经执行了编码转换,将…