字节流与结构、类之间的转换

    技术2025-10-04  4

    准备知识


    1.  StructLayoutAttribute

     

     

     

    可将该属性应用于类或结构。

     

    通常,公共语言运行库控制类或结构的数据字段在托管内存中的物理布局。如果类或结构需要按某种方式排列,则可以使用 StructLayoutAttribute。如果要将类传递给需要指定布局的非托管代码,则显式控制类布局是重要的。

     

     

     

    参数说明

    LayoutKind

    LayoutKind  值之一,它指定类或结构的排列方式。

     

    Sequential 对象的成员按照它们在被导出到非托管内存时出现的顺序依次布局。这些成员根据在StructLayoutAttribute.Pack中指定的封装进行布局,并且可以是不连续的。

    Explicit 对象的各个成员在非托管内存中的精确位置被显式控制。每个成员必须使用 FieldOffsetAttribute 指示该字段在类型中的位置。

    Auto 运行库自动为非托管内存中的对象的成员选择适当的布局。使用此枚举成员定义的对象不能在托管代码的外部公开。尝试这样做将引发异常。

    CharSet

    指示在默认情况下应如何将类中的字符串数据字段作为 LPWSTRLPSTR 进行封送处理。

     

    如果 CharSet 字段设置为 CharSet.Unicode,则所有字符串参数在传递到非托管实现之前都转换成 Unicode 字符(LPWSTR)。如果该字段设置为 CharSet.Ansi,则字符串将转换成 ANSI 字符串 (LPSTR)。如果 CharSet 字段设置为CharSet.Auto,则转换与平台相关(在 Windows NT、Windows 2000、Windows XP 和 Windows Server 2003 系列上为 Unicode;在 Windows 98 和 Windows Me 上为 ANSI)。

     

    Pack

    控制类或结构的数据字段在内存中的对齐方式。

     

    此字段指示在指定 LayoutKind.Sequential 值时应使用的封装大小。Pack 的值必须为 0、1、2、4、8、16、32、64 或 128。值为 0 则指示封装对齐方式设置为当前平台的默认值。

     Size

    指示类或结构的绝对大小。

     

     

    必须大于或等于所有成员的总和。此字段主要由编译器编写器使用,以指定类或结构的总大小(以字节为单位),而且在扩展由结构占用的内存(用于直接的非托管访问)时,此字段也很有用。例如,在使用未直接在元数据中表示的联合时可使用此字段。

     

     

    示例

    [StructLayout(LayoutKind.Explicit, Size=16, CharSet=CharSet.Ansi)] public class MySystemTime { [FieldOffset(0)]public ushort wYear; [FieldOffset(2)]public ushort wMonth; [FieldOffset(4)]public ushort wDayOfWeek; [FieldOffset(6)]public ushort wDay; [FieldOffset(8)]public ushort wHour; [FieldOffset(10)]public ushort wMinute; [FieldOffset(12)]public ushort wSecond; [FieldOffset(14)]public ushort wMilliseconds; }

     

     2. MarshalAsAttribute类

     

    可将该属性应用于参数、字段或返回值。

     

    该属性为可选属性,因为每个数据类型都有默认的封送处理行为。仅在可以将给定类型封送到多个类型时需要此属性。例如,可将字符串作为 LPStrLPWStrLPTStrBStr 封送到非托管代码。默认情况下,公共语言运行时将字符串参数作为 BStr 封送到 COM 方法。可将 MarshalAsAttribute 属性应用于个别的字段或参数,使该字符串作为 LPStr 而不是 BStr 进行封送。类型库导出程序 (Tlbexp.exe) 将封送处理首选项传递给公共语言运行时。

     

    当用于 COM 互操作或平台调用时,某些参数和返回值将具有不同的默认封送处理行为。默认情况下,运行时将字符串参数(以及值类型中的字段)作为 LPStr 封送到平台调用方法或函数。有关更多信息,请参见 默认封送处理行为

     

    参数说明

     UnmanagedType

    数据将被作为该值封送。

     

    System.Runtime.InteropServices.MarshalAsAttribute 中使用 UnmanagedType 枚举,以指定在与非托管代码进行交互的过程中如何对类型进行封送处理。可以使用此枚举对使用简单值类型(I1、I2、I4、I8、R4、R8、U2、U4 和 U8)、.NET Framework 中未提供的非托管类型以及各种杂项类型的代码进行封送处理。

    SizeConst 

    指示固定长度数组中的元素数,或要导入的字符串中的字符(不是字节)数。

     

     

    对于 System.Runtime.InteropServices.UnmanagedType 枚举的 ByValArrayByValTStr 成员,此字段是必需的。因为元数据的压缩格式限制为 0x1FFFFFFF,所以 SizeConst 的允许值的范围为(>= 0 且 <= 0x1FFFFFFF)。

     

     

     3. 不安全代码

     

    为了保持类型安全,默认情况下,C# 不支持指针运算。不过,通过使用 unsafe 关键字,可以定义可使用指针的不安全上下文。

     

    不安全代码具有下列属性:

    方法、类型和可被定义为不安全的代码块。

    在某些情况下,通过移除数组界限检查,不安全代码可提高应用程序的性能。

    当调用需要指针的本机函数时,需要使用不安全代码。

    使用不安全代码将引起安全风险和稳定性风险。

    在 C# 中,为了编译不安全代码,必须用 /unsafe 编译应用程序。

     4. Unsafe

     

     unsafe 关键字表示不安全上下文,该上下文是任何涉及指针的操作所必需的。

     

     

    可以在类型或成员的声明中使用 unsafe 修饰符。因此,类型或成员的整个正文范围均被视为不安全上下文。例如,以下是用 unsafe 修饰符声明的方法:

     

    unsafe static void FastCopy(byte[] src, byte[] dst, int count){    // Unsafe context: can use pointers here.}

    不安全上下文的范围从参数列表扩展到方法的结尾,因此指针在以下参数列表中也可以使用:

      unsafe static void FastCopy ( byte* ps, byte* pd, int count ) {...}

    还可以使用不安全块从而能够使用该块内的不安全代码。例如:

      unsafe{    // Unsafe context: can use pointers here.}

    若要编译不安全代码,必须指定 /unsafe 编译器选项。无法通过公共语言运行库验证不安全代码。

     

    示例

     

    // cs_unsafe_keyword.cs // compile with: /unsafe using System; class UnsafeTest { // Unsafe method: takes pointer to int: unsafe static void SquarePtrParam(int* p) { *p *= *p; } unsafe static void Main() { int i = 5; // Unsafe method: uses address-of operator (&): SquarePtrParam(&i); Console.WriteLine(i); } }

     

     5. fixed

     

     

    fixed 语句禁止垃圾回收器重定位可移动的变量。fixed 语句只能出现在不安全的上下文中。

     

    fixed 语句设置指向托管变量的指针,并在执行该语句期间“固定”此变量。如果没有 fixed 语句,则指向可移动托管变量的指针的作用很小,因为垃圾回收可能不可预知地重定位变量。C# 编译器只允许在 fixed 语句中分配指向托管变量的指针。

     

    unsafe static void TestMethod() { // assume class Point { public int x, y; } // pt is a managed variable, subject to garbage collection. Point pt = new Point(); // Using fixed allows the address of pt members to be // taken, and "pins" pt so it isn't relocated. fixed (int* p = &pt.x) { *p = 1; } }

     

    可以用数组或字符串的地址初始化指针:

     

    unsafe void Test2() { Point point = new Point(); double[] arr = { 0, 1.5, 2.3, 3.4, 4.0, 5.9 }; string str = "Hello World"; fixed (double* p = arr) { /*...*/ } // equivalent to p = &arr[0] fixed (char* p = str) { /*...*/ } // equivalent to p = &str[0] fixed (int* p1 = &point.x) { fixed (double* p2 = &arr[5]) { // Do something with p1 and p2. } } }

     

    只要指针的类型相同,就可以初始化多个指针:

     

    fixed (byte* ps = srcarray, pd = dstarray) {...}

     

    示例

     

    class Point { public int x, y; } class FixedTest2 { // Unsafe method: takes a pointer to an int. unsafe static void SquarePtrParam (int* p) { *p *= *p; } unsafe static void Main() { Point pt = new Point(); pt.x = 5; pt.y = 6; // Pin pt in place: fixed (int* p = &pt.x) { SquarePtrParam (p); } // pt now unpinned Console.WriteLine ("{0} {1}", pt.x, pt.y); } } /* Output: 25 6 */

     

    Fixed 还可用于创建固定大小的缓冲区

     

     public struct MyArray // This code must appear in an unsafe block { public fixed char pathName[128]; }

     

     

    在此结构中,pathName 数组具有固定的大小和位置,因此可用在其他不安全的代码中。

    128 个元素的 char 数组的大小为 256 字节。在固定大小的 char 缓冲区中,每个字符始终占用两个字节,而与编码无关。即使将 char 缓冲区封送到具有 CharSet = CharSet.AutoCharSet = CharSet.Ansi 的 API 方法或结构,也是如此。有关更多信息,请参见 CharSet

    另一种常见的固定大小的数组是 bool 数组。bool 数组中的元素的大小始终为一个字节。bool 数组不适合用于创建位数组或位缓冲区。

    说明:

    除了用 stackalloc 创建的内存之外,C# 编译器和公共语言运行时 (CLR) 不执行任何安全缓冲区溢出检查。与所有不安全代码一样,请谨慎使用。

    不安全缓冲区与常规数组在以下方面不同:

    不安全缓冲区只能用在不安全上下文中。

    不安全缓冲区始终是向量(或一维数组)。

    数组的声明应包括计数,如 char id[8]。而不能使用 char id[]

    不安全缓冲区只能是不安全上下文中的结构的实例字段。

     6. stackalloc

     

     

    stackalloc 关键字用于不安全的代码上下文中,以便在堆栈上分配内存块。

     

     int* fib = stackalloc int[100];

     

    下面的示例输出斐波那契数列的 100 个数字,其中每个数字都是前两个数字之和。在代码中,大小足够容纳 100 个 int 类型元素的内存块是在堆栈上分配的,而不是在堆上分配的。该块的地址存储在 fib 指针中。此内存不受垃圾回收的制约,因此不必将其钉住(通过使用 fixed)。内存块的生存期受限于定义它的方法的生存期。不能在方法返回之前释放内存。

     

    stackalloc 仅在局部变量的初始值设定项中有效。

     

    由于涉及指针类型,因此 stackalloc 要求不安全上下文。有关更多信息,请参见 不安全代码和指针(C# 编程指南)

     

    stackalloc 类似于 C 运行时中的 _alloca

     

    不安全代码的安全性低于安全替代代码。但是,通过使用 stackalloc 可以自动启用公共语言运行时 (CLR) 中的缓冲区溢出检测功能。如果检测到缓冲区溢出,进程将尽快终止,以最大限度地减小执行恶意代码的机会。

     

     class Test { static unsafe void Main() { const int arraySize = 20; int* fib = stackalloc int[arraySize]; int* p = fib; *p++ = 1; for (int i = 2; i < arraySize; ++i, ++p) { // Sum the previous two numbers. *p = p[-1] + p[-2]; } for (int i = 0; i < arraySize - 1; ++i) { Console.WriteLine(fib[i]); } // Keep the console window open in debug mode. System.Console.WriteLine("Press any key to exit."); System.Console.ReadKey(); } } /* Output 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 */

     

     


     

     

    字节流与结构、类之间的转换

    实际案例

     

     using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Runtime.InteropServices; namespace ConsoleApplication1 { [StructLayout(LayoutKind.Sequential,CharSet=CharSet.Ansi,Pack=1)] public struct ConfigInfo { public int i; public short s; public byte c; //public fixed byte str[10]; //public string GetStr() { // fixed (byte* p = str) { // return Marshal.PtrToStringAnsi((IntPtr)p); // } //} [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 11)] public string str; } class Program { static void Main(string[] args) { FileStream f = new FileInfo(@"e:/test.dat").OpenRead(); byte[] data = new byte[17]; f.Read(data, 0, 17); f.Close(); unsafe { fixed (byte* pContext = data) { ConfigInfo info = (ConfigInfo)Marshal.PtrToStructure((IntPtr)pContext, typeof(ConfigInfo)); Console.WriteLine(info.str); } } Console.ReadLine(); } } }

     

     

    最新回复(0)