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似乎不能保證(簡單的測試顯示效果相同,但是可能是由於測試環境和數據偏少)。