C#中byte[]和byte*的复制和转换
C#中,byte数组在很多数据流中具有普遍的适用,尤其是和其他程序语言、其他架构设备、不同通讯协议等打交道时,字节流能够保证数据的传输安全可靠,可以认为是最接近底层的数据类型了,因此对字节数据的操作就很常见和必要了。常见的场景是字节数组的复制,截断等,常规、最简单粗暴的循环系列代码,这里就不啰嗦了,主要总结一些现有类所提供的方法。
一、byte[]的复制
byte[]具有数组的一般特性,复制数据可以使用如下方式。
0. 打印数组元素
为了显示操作的结果,先写一个打印字节数组元素的函数:
static void PrintArray(byte[] x) { foreach(byte b in x) { Console.Write(b + " "); } Console.WriteLine(); }
1. Array.Copy方法
这是Array的静态方法,示例代码如下:
byte[] barr = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }; byte[] x=new byte[5]; Array.Copy(barr, x, 4); PrintArray(x);
显示数组x的结果是1 2 3 4 0,红色的来自源字节数组,因为初始化的数组,具有默认值0。这种方法可以从一个数组中复制从索引0开始的部分或全部元素。这种方式可以作为拷贝形式的字节数组截断。
2. ConstrainedCopy方法
这个也是Array类的静态方法,函数原型为:
public static void ConstrainedCopy (Array sourceArray, int sourceIndex, Array destinationArray, int destinationIndex, int length);
这个函数从指定的源索引开始,复制 Array 中的一系列元素,将它们粘贴到另一 Array 中(从指定的目标索引开始),保证在复制未成功完成的情况下撤消所有更改。
测试代码如下:
byte[] barr = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }; byte[] x = new byte[10]; Array.ConstrainedCopy(barr, 2, x, 6, 3); PrintArray(x);
运行结果为:0 0 0 0 0 0 3 4 5 0,从源数组barr的第2个元素开始拷贝,放入目标数组x的第6个位置,且拷贝长度为3。
3. CopyTo方法
这是继承了ICollection接口的类需要实现的方法,该方法将元字节数组中的所有元素,都拷贝到了目标数组中,其中第二个参数是指定了源字节数组第0个字节在目标数组中的位置,测试代码如下:
byte[] barr = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
byte[] x=new byte[20];
barr.CopyTo(x, 10);
PrintArray(x);
显示的结果是0 0 0 0 0 0 0 0 0 0 1 2 3 4 5 6 7 8 9 0,其中红色的是源目标数组中的值。
这种方法相对于第一种方法,更多的是应用于字节流的拼接。
4. Linq扩展方法
还有一种方式是Linq的扩展方法,这种方法比前面两种提供了更加灵活的操作,相关的扩展方法有Skip/Take, SkipLast/TakeLast,测试代码如下:
//需要using System.Linq; byte[] barr = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }; byte[] x=barr.Skip(1).Take(3).ToArray(); PrintArray(x);
显示的结果是2 3 4,Skip是去掉前面的元素,Take是截取多少个数据。也可以是从后面获取,例如:
byte[] x = barr.TakeLast(4).ToArray();
则最后的结果是7 8 9 0,即获取最后的4个元素。
当然,使用linq还可以有很多高级操作,例如我们只需要提取其中的奇数,可以使用:
byte[] x = barr.Where(b => b % 2 == 1).ToArray();
关于Linq的使用,可以参考其他资料,此处不做展开论述了。
5. Clone方法
Array.Clone方法也可以实现拷贝,但是这种方式太生硬,只能完全复制,而且还需要做强制类型转换,个人不推荐使用,也不展开叙述。
6. MemoryStream类
这个类提供了内存中的流式读写,所以对数组的部分或全部拷贝,也是很方便的,虽然在实现上有种“曲线救国”的感觉,但是在涉及到流操作的时候,其实还是很常见且实用的,测试代码如下:
byte[] barr = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }; MemoryStream ms = new MemoryStream(barr); ms.Position = 3; byte[] x = new byte[5]; ms.Read(x, 0, 5); PrintArray(x); ms.Close();
执行的结果是显示4 5 6 7 8,因为MemoryStream支持随机访问,通过设置Position,然后再按顺序读数据,甚至还可以多次使用目标数组,例如我们要把barr的前2个值和最后3个值放入x数字中,可以这么写:
ms.Position = 0; byte[] x = new byte[5]; ms.Read(x, 0, 2); ms.Position = 7; ms.Read(x, 2, 3);
当然这种方法相对来说也是比较占用内存的,通常用于数组不大,而又需要多次、随机访问的场合。
二、byte[]的截断
前面通过复制的方式,可以获取字节数组的部分元素,但是需要一个新的数组,那么有没有在位截断的方法呢?也是有的,使用Array的静态方法Resize:
byte[] barr = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }; Array.Resize(ref barr, 5); PrintArray(barr);
测试结果是1 2 3 4 5。这种方式不需要额外的内存。
三、byte[]和byte*的互换
在C#中,偶尔还会碰到byte*的指针类型 ,这就会涉及到了byte*和byte[]之间的转换,以及byte*的复制等问题。byte*在C#中的出镜率不高,毕竟是unsafe的,不过在一些诸如Socket等的方法中还是有露脸的机会。
目前发现,从byte[]到byte*,或者反过来,没有直接的转换方法,不能像C语言那样有直接取数组的首地址,毕竟C#是一个强类型语言。能做的只是分配地址,然后在其中拷贝数据,其中会牵扯到Iunsafe代码,以及ntPtr指针类型,可以将byte*理解为是IntPtr的强制类型转换。
1. 从byte[]到byte*
测试代码如下:
byte[] barr = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }; byte* bp = (byte*)Marshal.AllocHGlobal(5); PrintPtr(bp, 5); Marshal.Copy(barr, 3, (IntPtr)bp, 5); PrintPtr(bp, 5); Marshal.FreeHGlobal((IntPtr)bp);
其中PrintPtr函数如下:
static void PrintPtr(byte* bp,int n) { for(int i=0;i<n;++i) Console.Write(bp[i] + " "); Console.WriteLine(); }
程序输出的结果是:
176 63 95 127 17
4 5 6 7 8
因为是堆上分配的,所以其中第一行的值(红色)是乱码,每次都不一样,第二行的值是通过复制Marshal.Copy将其填充到了byte*所指向的地址。
2.从byte*转byte[]
同样是使用Marshal的静态方法。测试代码如下:
byte[] barr = new byte[10]; byte* bp = (byte*)Marshal.AllocHGlobal(10); for (int i = 0; i < 10; i++) bp[i] = (byte)i; Marshal.Copy((IntPtr)bp, barr, 0, 10); PrintArray(barr); Marshal.FreeHGlobal((IntPtr)bp);
执行结果是输出0 1 2 3 4 5 6 7 8 9。
四、byte*的复制
byte*的复制,可以理解为是void*类型的复制,即内存数据的复制,在C#中表现为IntPtr所指向的两个内存,那么就有相关的函数可以使用,例如平台调用,最常见的是CopyMemory或者MoveMemory。
[DllImport("kernel32.dll", EntryPoint = "RtlCopyMemory", CharSet = CharSet.Ansi)] public extern static long CopyMemory(IntPtr dest, IntPtr source, int size);
测试代码:
IntPtr p1 = Marshal.AllocHGlobal(10); IntPtr p2 = Marshal.AllocHGlobal(20); for (int i=0;i<10;i++) { ((byte*)p1)[i] = (byte)i; } CopyMemory(p2, p1, 10); PrintPtr((byte*)p1, 10); PrintPtr((byte*)p2, 20); Marshal.FreeHGlobal(p1); Marshal.FreeHGlobal(p2);
运行的结果是:
0 1 2 3 4 5 6 7 8 9
0 1 2 3 4 5 6 7 8 9 232 85 67 0 92 0 32 186 184 215
第二行后面十个元素是乱码(红色数字)。
有CopyMemory函数,自然也可以使用MoveMemory函数,定义如下:
[DllImport("kernel32.dll", EntryPoint = "RtlMoveMemory", CharSet = CharSet.Ansi)] public extern static long MoveMemory(IntPtr dest, IntPtr source, int size);
两者的使用方法差不多,但是MoveMemory有个好处是当内存中出现重叠时,结果是唯一的,而CopyMemory似乎不能保证(简单的测试显示效果相同,但是可能是由于测试环境和数据偏少)。