[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
-
精致碼農
帶你洞悉編程與架構
↑長按圖片識別二維碼關注,不要錯過網海相遇的緣分
總結
以上是生活随笔為你收集整理的[C#.NET 拾遗补漏]14:使用结构体实现共用体的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 微软向Chromium贡献代码以优化浏览
- 下一篇: 使用 C# 9 的records作为强类