.NET基礎拾遺(3)字符串、集合和流

Index:html

(1)類型語法、內存管理和垃圾回收基礎程序員

(2)面向對象的實現和異常的處理web

(3)字符串、集合與流面試

(4)委託、事件、反射與特性算法

(5)多線程開發基礎數據庫

(6)ADO.NET與數據庫開發基礎設計模式

(7)WebService的開發與應用基礎數組

1、字符串處理

1.1 StringBuilder類型有什麼做用?

  衆所周知,在.NET中String是引用類型,具備不可變性,當一個String對象被修改、插入、鏈接、截斷時,新的String對象就將被分配,這會直接影響到性能。但在實際開發中常常碰到的狀況是,一個String對象的最終生成須要通過一個組裝的過程,而在這個組裝過程當中必將會產生不少臨時的String對象,而這些String對象將會在堆上分配,須要GC來回收,這些動做都會對程序性能產生巨大的影響。事實上,在String的組裝過程當中,其臨時產生的String對象實例都不是最終須要的,所以能夠說是沒有必要分配的。瀏覽器

  鑑於此,在.NET中提供了StringBuilder,其設計思想源於構造器(Builder)設計模式,致力於解決複雜對象的構造問題。對於String對象,正須要這樣的構造器來進行組裝。StringBuilder類型在最終生成String對象以前,將不會產生任何String對象,這很好地解決了字符串操做的性能問題緩存

  如下代碼展現了使用StringBuilder和不適用StringBuilder的性能差別:(這裏的性能檢測工具使用了老趙的CodeTimer類)

    public class Program
    {
        private const String item = "一個項目";
        private const String split = ";";

        static void Main(string[] args)
        {
            int number = 10000;
            // 使用StringBuilder
            CodeTimer.Time("使用StringBuilder: ", 1, () =>
            {
                UseStringBuilder(number);
            });
            // 不使用StringBuilder
            CodeTimer.Time("使用不使用StringBuilder: : ", 1, () =>
            {
                NotUseStringBuilder(number);
            });

            Console.ReadKey();
        }

        static String UseStringBuilder(int number)
        {
            System.Text.StringBuilder sb = new System.Text.StringBuilder();
            for (int i = 0; i < number; i++)
            {
                sb.Append(item);
                sb.Append(split);
            }
            sb.Remove(sb.Length - 1, 1);
            return sb.ToString();
        }

        static String NotUseStringBuilder(int number)
        {
            String result = "";
            for (int i = 0; i < number; i++)
            {
                result += item;
                result += split;
            }
            return result;
        }
    }
View Code

  上述代碼的運行結果以下圖所示,能夠看出因爲StringBuilder不會產生任何的中間字符串變量,所以效率上優秀很多!

  看到StringBuilder這麼優秀,不由想發出一句:臥槽,牛逼!

  因而,咱們拿起咱們的錘子(Reflector)撕碎StringBuilder的外套,看看裏面到底裝了什麼?咱們發現,在StringBuilder中定義了一個字符數組m_ChunkChars,它保存StringBuilder所管理着的字符串中的字符。

  通過對StringBuilder默認構造方法的分析,系統默認初始化m_ChunkChars的長度爲16(0x10),當新追加進來的字符串長度與舊有字符串長度之和大於該字符數組容量時,新建立字符數組的容量會增長到2n+1(假如當前字符數組容量爲2n)。

  此外,StringBuilder內部還有一個同爲StringBuilder類型的m_ChunkPrevious,它是內部的一個StringBuilder對象,前面提到當追加的字符串長度和舊字符串長度之合大於字符數組m_ChunkChars的最大容量時,會根據當前的(this)StringBuilder建立一個新的StringBuilder對象,將m_ChunkPrevious指向新建立的StringBuilder對象。

  下面是StringBuilder中實現擴容的核心代碼:

private void ExpandByABlock(int minBlockCharCount)
{
    ......
    int num = Math.Max(minBlockCharCount, Math.Min(this.Length, 0x1f40));
    this.m_ChunkPrevious = new StringBuilder(this);
    this.m_ChunkOffset += this.m_ChunkLength;
    this.m_ChunkLength = 0;
    ......
    this.m_ChunkChars = new char[num];
}

  能夠看出,初始化m_ChunkPrevious在前,建立新的字符數組m_ChunkChars在後,最後纔是複製字符到數組m_ChunkChars中(更新當前的m_ChunkChars)。歸根結底,StringBuilder是在內部以字符數組m_ChunkChars爲基礎維護一個鏈表m_ChunkPrevious,該鏈表以下圖所示:

  在最終的ToString方法中,當前的StringBuilder對象會根據這個鏈表以及記錄的長度和偏移變量去生成最終的一個String對象實例,StringBuilder的內部實現中使用了一些指針操做,其內部原理有興趣的園友能夠本身去經過反編譯工具查看源代碼。

1.2 String和Byte[]對象之間如何相互轉換?

  在實際開發中,常常會對數據進行處理,不可避免地會遇到字符串和字節數組相互轉換的需求。字符串和字節數組的轉換,事實上是表明了現實世界信息和數字世界信息之間的轉換,要了解其中的機制,須要先對比特、直接以及編碼這三個概念有所瞭解。

  (1)比特:bit是一個位,計算機內物理保存的最基本單元,一個bit就是一個二進制位;

  (2)字節:byte由8個bit構成,其值能夠由一個0~255的整數表示;

  (3)編碼:編碼是數字信息和現實信息的轉換機制,一種編碼一般就定義了一種字符集和轉換的原則,經常使用的編碼方式包括UTF八、GB23十二、Unicode等。

  下圖直觀地展現了比特、字節、編碼和字符串的關係:

  從上圖能夠看出,字節數組和字符串的轉換必然涉及到某種編碼方式,不一樣的編碼方式由不一樣的轉換結果。在C#中,可使用System.Text.Encoding來管理經常使用的編碼。

  下面的代碼展現瞭如何在字節數組和字符串之間進行轉換(分別使用UTF八、GB2312以及Unicode三種編碼方式):

    class Program
    {
        static void Main(string[] args)
        {
            string s = "我是字符串,I am a string!";
            // 字節數組 -> 字符串
            Byte[] utf8 = StringToByte(s, Encoding.UTF8);
            Byte[] gb2312 = StringToByte(s, Encoding.GetEncoding("GB2312"));
            Byte[] unicode = StringToByte(s, Encoding.Unicode);

            Console.WriteLine(utf8.Length);
            Console.WriteLine(gb2312.Length);
            Console.WriteLine(unicode.Length);
            // 字符串 -> 字符數組
            Console.WriteLine(ByteToString(utf8, Encoding.UTF8));
            Console.WriteLine(ByteToString(gb2312, Encoding.GetEncoding("GB2312")));
            Console.WriteLine(ByteToString(unicode, Encoding.Unicode));

            Console.ReadKey();
        }

        // 字符串 -> 字節數組
        static Byte[] StringToByte(string str, Encoding encoding)
        {
            if (string.IsNullOrEmpty(str))
            {
                return null;
            }
            return encoding.GetBytes(str);
        }

        // 字節數組 -> 字符串
        static string ByteToString(Byte[] bytes, Encoding encoding)
        {
            if (bytes == null || bytes.Length <= 0)
            {
                return string.Empty;
            }

            return encoding.GetString(bytes);
        }
    }
View Code

  上述代碼的運行結果以下圖所示:

  咱們也能夠從上圖中看出,不一樣的編碼方式產生的字節數組的長度各不相同

1.3 BASE64編碼的做用以及C#中對其的支持

  和傳統的編碼不一樣,BASE64編碼的設計致力於混淆那些8位字節的數據流(解決網絡傳輸中的明碼問題),在網絡傳輸、郵件等系統中被普遍應用。須要明確的是:BASE64不屬於加密機制,但它倒是把明碼變成了一種很難識別的形式

  BASE64的算法以下:

BASE64把全部的位分開,而且從新組合成字節,新的字節只包含6位,最後在每一個字節前添加兩個0,組成了新的字節數組。例如:一個字節數組只包含三個字節(每一個字節又有8位比特),對其進行BASE64編碼時會將其分配到4個新的字節中(爲何是4個呢?計算3*8/6=4),其中每一個字節只填充低6位,最後把高2位置爲零。

  下圖清晰地展現了上面所講到的BASE64的算法示例:

  在.NET中,BASE64編碼的應用也不少,例如在ASP.NET WebForm中,默認爲咱們生成了一個ViewState來保持狀態,以下圖所示:

viewstate

  這裏的ViewState其實就是服務器在返回給瀏覽器前進行了一次BASE64編碼,咱們能夠經過一些解碼工具進行反BASE64編碼查看其中的奧祕:

Decoder

  那麼,問題來了?在.NET中開發中,怎樣來進行BASE64的編碼和解碼呢,.NET基類庫中提供了一個Convert類,其中有兩個靜態方法提供了BASE64的編碼和解碼,但要注意的是:Convert類型在轉換失敗時會直接拋出異常,咱們須要在開發中注意對潛在異常的處理(好比使用is或as來進行高效的類型轉換)。下面的代碼展現了其用法:

    class Program
    {
        static void Main(string[] args)
        {
            string test = "abcde ";
            // 生成UTF8字節數組
            byte[] bytes = Encoding.UTF8.GetBytes(test);
            // 轉換成Base64字符串
            string base64 = BytesToBase64(bytes);
            Console.WriteLine(base64);
            // 轉換回UTF8字節數組
            bytes = Base64ToBytes(base64);
            Console.WriteLine(Encoding.UTF8.GetString(bytes));

            Console.ReadKey();
        }

        // Bytes to Base64
        static string BytesToBase64(byte[] bytes)
        {
            try
            {
                return Convert.ToBase64String(bytes);
            }
            catch
            {
                return null;
            }
        }

        // Base64 to Bytes
        static Byte[] Base64ToBytes(string base64)
        {
            try
            {
                return Convert.FromBase64String(base64);
            }
            catch
            {
                return null;
            }
        }
    }
View Code

  上面代碼的執行結果以下圖所示:

  

1.4 簡述SecureString安全字符串的特色和用法

  也許不少人都是第一次知道還有SecureString這樣一個類型,我也不例外。SecureString並非一個經常使用的類型,但在一些擁有特殊需求的額場合,它就會有很大的做用。顧名思義,SecureString意爲安全的字符串,它被設計用來保存一些機密的字符串,完成傳統字符串所不能作到的工做

  (1)傳統字符串以明碼的形式被分配在內存中,一個簡單的內存讀寫軟件就能夠輕易地捕獲這些字符串,而在這某些機密系統中是不被容許的。也許咱們會以爲對字符串加密就能夠解決相似問題,But,事實老是殘酷的,對字符串加密時字符串已經以明碼方式駐留在內存中好久了!對於該問題惟一的解決辦法就是在字符串的得到過程當中直接進行加密,SecureString的設計初衷就是解決該類問題

  (2)爲了保證安全性,SecureString是被分配在非託管內存上的(而普通String是被分配在託管內存中的),而且SecureString的對象從分配的一開始就以加密的形式存在,咱們全部對於SecureString的操做(不管是增刪查改)都是逐字符進行的

逐字符機制:在進行這些操做時,駐留在非託管內存中的字符串就會被解密,而後進行具體操做,最後再進行加密。不能否認的是,在具體操做的過程當中有小段時間字符串是處於明碼狀態的,但逐字符的機制讓這段時間維持在很是短的區間內,以保證破解程序很難有機會讀取明碼的字符串。

  (3)爲了保證資源釋放,SecureString實現了標準的Dispose模式(Finalize+Dispose左右開弓,由於上面提到它是被分配到非託管內存中的),保證每一個對象在做用域退出後均可以被釋放掉。

內存釋放方式:將其對象內存所有置爲0,而不是僅僅告訴CLR這一塊內存能夠分配,固然這樣作仍然是爲了確保安全。熟悉C/C++的朋友可能就會很熟悉,這不就是 memset 函數乾的事情嘛!下面這段C代碼便使用了memset函數將內存區域置爲0:

    // 下面申請的20個字節的內存有可能被別人用過
    char chs[20];
    // memset內存初始化:memset(void *,要填充的數據,要填充的字節個數)
    memset(chs,0,sizeof(chs));

  看完了SecureString的原理,如今咱們經過下面的代碼來熟悉一下在.NET中的基本用法:

using System;
using System.Runtime.InteropServices;
using System.Security;

namespace UseSecureString
{
    class Program
    {
        static void Main(string[] args)
        {
            // 使用using語句保證Dispose方法被及時調用
            using (SecureString ss = new SecureString())
            {
                // 只能逐字符地操做SecureString對象
                ss.AppendChar('e');
                ss.AppendChar('i');
                ss.AppendChar('s');
                ss.AppendChar('o');
                ss.AppendChar('n');
                ss.InsertAt(1, 'd');
                // 打印SecureStrign對象
                PrintSecureString(ss);
            }

            Console.ReadKey();
        }

        // 打印SecureString對象
        public unsafe static void PrintSecureString(SecureString ss)
        {
            char* buffer = null;

            try
            {
                // 只能逐字符地訪問SecureString對象
                buffer = (char*)Marshal.SecureStringToCoTaskMemUnicode(ss);
                for (int i = 0; *(buffer + i) != '\0'; i++)
                {
                    Console.Write(*(buffer + i));
                }
            }
            finally
            {
                // 釋放內存對象
                if (buffer != null)
                {
                    Marshal.ZeroFreeCoTaskMemUnicode((System.IntPtr)buffer);
                }
            }
        }
    }
}
View Code

  其運行顯示的結果很簡單:

  

  這裏須要注意的是:爲了顯示SecureString的內容,程序須要訪問非託管內存,所以會用到指針,而要在C#使用指針,則須要使用unsafe關鍵字(前提是你在項目屬性中勾選了容許不安全代碼,對你沒看錯,指針在C#可使用,可是被認爲是不安全的!)。此外,程序中使用了Marshal.SecureStringToCoTaskMemUnicode方法來把安全字符串解密到非託管內存中,最後就是就是咱們不要忘記在使用非託管資源時須要確保及時被釋放。

1.5 簡述字符串駐留池機制

  字符串具備不可變性,程序中對於同一個字符串的大量修改或者多個引用賦值同一字符串在理論上會產生大量的臨時字符串對象,這會極大地下降系統的性能。對於前者,可使用StringBuilder類型解決,然後者,.NET則提供了另外一種不透明的機制來優化,這就是傳說中的字符串駐留池機制。

  使用了字符串駐留池機制以後,當CLR啓動時,會在內部建立一個容器,該容器內部維持了一個相似於key-value對的數據結構,其中key是字符串的內容,而value則是字符串在託管堆上的引用(也能夠理解爲指針或地址)。當一個新的字符串對象須要分配時,CLR首先監測內部容器中是否已經存在該字符串對象,若是已經包含則直接返回已經存在的字符串對象引用;若是不存在,則新分配一個字符串對象,同時把其添加到內部容器中取。But,這裏有一個例外,就是當程序員用new關鍵字顯示地申明新分配一個字符串對象時,該機制將不會起做用

  從上面的描述中,咱們能夠看到字符串駐留池的本質是一個緩存,內部維持了一個鍵爲字符串內容,值爲該字符串在堆中的引用地址的鍵值對數據結構。咱們能夠經過下面一段代碼來加深對於字符串駐留池的理解:

    class Program
    {
        static void Main(string[] args)
        {
            // 01.兩個字符串對象,理論上引用應該不相等
            // 可是因爲字符串池機制,兩者指向了同一對象
            string a = "abcde";
            string b = "abcde";
            Console.WriteLine(object.ReferenceEquals(a, b));
            // 02.因爲編譯器的優化,因此下面這個c仍然指向了同一引用地址
            string c = "a" + "bc" + "de";
            Console.WriteLine(object.ReferenceEquals(a, c));
            // 03.顯示地使用new來分配內存,這時候字符串池不起做用
            char[] arr = { 'a', 'b', 'c', 'd', 'e' };
            string d = new string(arr);
            Console.WriteLine(object.ReferenceEquals(a, d));

            Console.ReadKey();
        }
    }
View Code

  在上述代碼中,因爲字符串駐留池機制的使用,變量a、b、c都指向了同一個字符串實例對象,而d則使用了new關鍵字顯示申明,所以字符串駐留池並無對其起做用,其運行結果以下圖所示:

  

  字符串駐留池的設計本意是爲了改善程序的性能,所以在C#中默認是打開了字符串駐留池機制,But,.NET也爲咱們提供了字符串駐留池的開關接口,若是程序集標記了一個System.Runtime.CompilerServices.CompilationRelaxationsAttribute特性,而且指定了一個System.Runtime.CompilerServices.CompilationRelaxations.NoStringInterning標誌,那麼CLR不會採用字符串駐留池機制,其代碼聲明以下所示,可是我添加後一直沒有嘗試成功:

[assembly: System.Runtime.CompilerServices.CompilationRelaxations(System.Runtime.CompilerServices.CompilationRelaxations.NoStringInterning)]  

2、經常使用集合和泛型

2.1 int[]是值類型仍是引用類型?

  在.NET中的數組類型和C++中區別很大,.NET中不管是存儲值類型對象的數組仍是存儲引用類型的數組,其自己都是引用類型,其內存也都是分配在堆上的。它們的共同特徵在於:全部的數組類型都繼承自System.Array,而System.Array又實現了多個接口,而且直接繼承自System.Object。不一樣之處則在於存儲值類型對象的數組全部的值都已經包含在數組內,而存儲引用類型對象的數組,其值則是一個引用,指向位於託管堆中的實例對象。

  下圖直觀地展現了兩者內存分配的差異(假設object[]中存儲都是DateTime類型的對象實例):

  在.NET中CLR會檢測全部對數組的訪問,任何視圖訪問數組邊界之外的代碼都會產生一個IndexOutOfRangeException異常。

2.2 數組之間如何進行轉換?

  數組類型的轉換須要遵循如下兩個原則:

  (1)包含值類型的數組不能被隱式轉換成其餘任何類型

  (2)兩個數組類型可以相互轉換的一個前提是二者維數相同

  咱們能夠經過如下代碼來看看數組類型轉換的機制:

    // 編譯成功
    string[] sz = { "a", "a", "a" };
    object[] oz = sz;
    // 編譯失敗,值類型的數組不能被轉換
    int[] sz2 = { 1, 2, 3 };
    object[] oz2 = sz;
    // 編譯失敗,二者維數不一樣
    string[,] sz3 = { { "a", "b" }, { "a", "c" } };
    object[] oz3 = sz3;

  除了類型上的轉換,咱們平時還可能會遇到內容轉換的需求。例如,在一系列的用戶界面操做以後,系統的後臺可能會獲得一個DateTime的數組,而如今的任務則是將它們存儲到數據庫中,而數據庫訪問層提供的接口只接受String[]參數,這時咱們要作的就是把DateTime[]從內容上轉換爲String[]對象。固然,慣常作法是遍歷整個源數組,逐一地轉換每一個對象而且將其放入一個目標數組類型容器中,最後再生成目標數組。But,這裏咱們推薦使用Array.ConvertAll方法,它提供了一個簡便的轉換數組間內容的接口,咱們只需指定源數組的類型、對象數組的類型和具體的轉換算法,該方法就能高效地完成轉換工做。

  下面的代碼清楚地展現了普通的數組內容轉換方式和使用Array.ConvertAll的數組內容轉換方式的區別:

    class Program
    {
        static void Main(string[] args)
        {
            String[] times ={"2008-1-1",
                            "2008-1-2",
                            "2008-1-3"};

            // 使用不一樣的方法轉換
            DateTime[] result1 = OneByOne(times);
            DateTime[] result2 = ConvertAll(times);

            // 結果是相同的
            Console.WriteLine("手動逐個轉換的方法:");
            foreach (DateTime item in result1)
            {
                Console.WriteLine(item.ToString("yyyy-MM-dd"));
            }
            Console.WriteLine("使用Array.Convert方法:");
            foreach (DateTime item2 in result2)
            {
                Console.WriteLine(item2.ToString("yyyy-MM-dd"));
            }

            Console.ReadKey();
        }

        // 逐個手動轉換
        private static DateTime[] OneByOne(String[] times)
        {
            List<DateTime> result = new List<DateTime>();
            foreach (String item in times)
            {
                result.Add(DateTime.Parse(item));
            }
            return result.ToArray();
        }

        // 使用Array.ConertAll方法
        private static DateTime[] ConvertAll(String[] times)
        {
            return Array.ConvertAll(times,
                new Converter<String, DateTime>
                (DateTimeToString));
        }

        private static DateTime DateTimeToString(String time)
        {
            return DateTime.Parse(time);
        }
    }
View Code

  從上述代碼能夠看出,兩者實現了相同的功能,可是Array.ConvertAll不須要咱們手動地遍歷數組,也不須要生成一個臨時的容器對象,更突出的優點是它能夠接受一個動態的算法做爲具體的轉換邏輯。固然,明眼人一看就知道,它是以一個委託的形式做爲參數傳入,這樣的機制保證了Array.ConvertAll具備較高的靈活性。

2.3 簡述泛型的基本原理

  泛型的語法和概念相似於C++中的template(模板),它是.NET 2.0中推出的衆多特性中最爲重要的一個,方便咱們設計更加通用的類型,也避免了容器操做中的裝箱和拆箱操做

  假如咱們要實現一個排序算法,要求可以針對各類類型進行排序。按照之前的作法,咱們須要對int、double、float等類型都實現一次,可是咱們發現除了數據類型,其餘的處理邏輯徹底一致。這時,咱們即可以考慮使用泛型來進行實現:

    public static class SortHelper<T> where T : IComparable
    {
        public static void BubbleSort(T[] array)
        {
            int length = array.Length;
            for (int i = 0; i <= length - 2; i++)
            {
                for (int j = length - 1; j >= 1; j--)
                {
                    // 對兩個元素進行交換            
                    if (array[j].CompareTo(array[j - 1]) < 0)
                    {
                        T temp = array[j];
                        array[j] = array[j - 1];
                        array[j - 1] = temp;
                    }
                }
            }
        }
    }

Tips:Microsoft在產品文檔中建議全部的泛型參數名稱都以T開頭,做爲一箇中編碼的通用規範,建議你們都能遵照這樣的規範,相似的規範還有全部的接口都以I開頭。

  泛型類型和普通類型有必定的區別,一般泛型類型被稱爲開放式類型,.NET中規定開放式類型不能實例化,這樣也就確保了開放式類型的泛型參數在被指定前,不會被實例化成任何對象(事實上,.NET也沒有辦法肯定到底要分配多少內存給開放式類型)。爲開放式的類型提供泛型的實例致使了一個新的封閉類型的生成,但這並不表明新的封閉類型和開放類型有任何繼承關係,它們在類結構圖上是處於同一層次,而且二者之間沒有任何關係。下圖展現了這一律念:

  此外,在.NET中的System.Collections.Generic命名空間下提供了諸如List<T>、Dictionary<T>、LinkedList<T>等泛型數據結構,而且在System.Array中定義了一些靜態的泛型方法,咱們應該在編碼實踐時充分使用這些泛型容器,以提升咱們的開發和系統的運行效率

2.4 泛型的主要約束和次要約束是什麼?

  當一個泛型參數沒有任何約束時,它能夠進行的操做和運算是很是有限的,由於不能對實參進行任何類型上的保證,這時候就須要用到泛型約束。泛型的約束分爲:主要約束和次要約束,它們都使實參必須知足必定的規範,C#編譯器在編譯的過程當中能夠根據約束來檢查全部泛型類型的實參並確保其知足約束條件。

  (1)主要約束

  一個泛型參數至多擁有一個主要約束,主要約束能夠是一個引用類型、class或者struct。若是指定一個引用類型(class),那麼實參必須是該類型或者該類型的派生類型。相反,struct則規定了實參必須是一個值類型。下面的代碼展現了泛型參數主要約束:

    public class ClassT1<T> where T : Exception
    {
        private T myException;
        public ClassT1(T t)
        {
            myException = t;
        }
        public override string ToString()
        {
            // 主要約束保證了myException擁有source成員
            return myException.Source;
        }
    }

    public class ClassT2<T> where T : class
    {
        private T myT;
        public void Clear()
        {
            // T是引用類型,能夠置null
            myT = null;
        }
    }

    public class ClassT3<T> where T : struct
    {
        private T myT;
        public override string ToString()
        {
            // T是值類型,不會發生NullReferenceException異常
            return myT.ToString();
        }
    }
View Code

  泛型參數有了主要約束後,也就可以在類型中對其進行必定的操做了。

  (2)次要約束

  次要約束主要是指實參實現的接口的限定。對於一個泛型,能夠有0到無限的次要約束,次要約束規定了實參必須實現全部的次要約束中規定的接口。次要約束與主要約束的語法基本一致,區別僅在於提供的不是一個引用類型而是一個或多個接口。例如咱們爲上面代碼中的ClassT3增長一個次要約束:

    public class ClassT3<T> where T : struct, IComparable
    {
        ......      
    }
View Code

3、流和序列化

3.1 流的概念以及.NET中有哪些常見的流?

  流是一種針對字節流的操做,它相似於內存與文件之間的一個管道。在對一個文件進行處理時,本質上須要通過藉助OS提供的API來進行打開文件,讀取文件中的字節流,再關閉文件等操做,其中讀取文件的過程就能夠看做是字節流的一個過程。

  常見的流類型包括:文件流、終端操做流以及網絡Socket等,在.NET中,System.IO.Stream類型被設計爲做爲全部流類型的虛基類,全部的常見流類型都繼承自System.IO.Stream類型,當咱們須要自定義一種流類型時,也應該直接或者間接地繼承自Stream類型。下圖展現了在.NET中常見的流類型以及它們的類型結構:

  從上圖中能夠發現,Stream類型繼承自MarshalByRefObject類型,這保證了流類型能夠跨越應用程序域進行交互。全部經常使用的流類型都繼承自System.IO.Stream類型,這保證了流類型的同一性,而且屏蔽了底層的一些複雜操做,使用起來很是方便。

  下面的代碼中展現瞭如何在.NET中使用FileStream文件流進行簡單的文件讀寫操做:

    class Program
    {
        private const int bufferlength = 1024;

        static void Main(string[] args)
        {
            //建立一個文件,並寫入內容
            string filename = @"C:\TestStream.txt";
            string filecontent = GetTestString();

            try
            {
                if (File.Exists(filename))
                {
                    File.Delete(filename);
                }

                // 建立文件並寫入內容
                using (FileStream fs = new FileStream(filename, FileMode.Create))
                {
                    Byte[] bytes = Encoding.UTF8.GetBytes(filecontent);
                    fs.Write(bytes, 0, bytes.Length);
                }

                // 讀取文件並打印出來
                using (FileStream fs = new FileStream(filename, FileMode.Open))
                {
                    Byte[] bytes = new Byte[bufferlength];
                    UTF8Encoding encoding = new UTF8Encoding(true);
                    while (fs.Read(bytes, 0, bytes.Length) > 0)
                    {
                        Console.WriteLine(encoding.GetString(bytes));
                    }
                }
                // 循環分批讀取打印
                //using (FileStream fs = new FileStream(filename, FileMode.Open, FileAccess.Read))
                //{
                //    Byte[] bytes = new Byte[bufferlength];
                //    int bytesRead;
                //    do
                //    {
                //        bytesRead = fs.Read(bytes, 0, bufferlength);
                //        Console.WriteLine(Encoding.UTF8.GetString(bytes, 0, bytesRead));
                //    } while (bytesRead > 0);
                //}
            }
            catch (IOException ex)
            {
                Console.WriteLine(ex.Message);
            }

            Console.ReadKey();
        }

        // 01.取得測試數據
        static string GetTestString()
        {
            StringBuilder builder = new StringBuilder();
            for (int i = 0; i < 10; i++)
            {
                builder.Append("我是測試數據\r\n");
                builder.Append("我是長江" + (i + 1) + "號\r\n");
            }
            return builder.ToString();
        }
    }
View Code

  上述代碼的執行結果以下圖所示:

      

  在實際開發中,咱們常常會遇到須要傳遞一個比較大的文件,或者事先沒法得知文件大小(Length屬性拋出異常),所以也就不能建立一個尺寸正好合適的Byte[]數組,此時只能分批讀取和寫入每次只讀取部分字節,直到文件尾。例如咱們須要複製G盤中一個大小爲4.4MB的mp3文件到C盤中去,假設咱們對大小超過2MB的文件都採用分批讀取寫入機制,能夠經過以下代碼實現:

    class Program
    {
        private const int BufferSize = 10240; // 10 KB
        public static void Main(string[] args)
        {
            string fileName = @"G:\My Musics\BlueMoves.mp3"; // Source 4.4 MB
            string copyName = @"C:\BlueMoves-Copy.mp3"; // Destination 4.4 MB
            using (Stream source = new FileStream(fileName, FileMode.Open, FileAccess.Read))
            {
                using (Stream target = new FileStream(copyName, FileMode.Create, FileAccess.Write))
                {
                    byte[] buffer = new byte[BufferSize];
                    int bytesRead;
                    do
                    {
                        // 從源文件中讀取指定的10K長度到緩存中
                        bytesRead = source.Read(buffer, 0, BufferSize);
                        // 從緩存中寫入已讀取到的長度到目標文件中
                        target.Write(buffer, 0, bytesRead);
                    } while (bytesRead > 0);
                }
            }
            Console.ReadKey();
        }
    }
View Code

  上述代碼中,設置了緩存buffer大小爲10K,即每次只讀取10K的內容長度到buffer中,經過循環的屢次讀寫和寫入完成整個複製操做。

3.2 如何使用壓縮流?

  因爲網絡帶寬的限制、硬盤內存空間的限制等緣由,文件和數據的壓縮是咱們常常會遇到的一個需求。所以,.NET中提供了對於壓縮和解壓的支持:GZipStream類型和DeflateStream類型,它們位於System.IO.Compression命名空間下,且都繼承於Stream類型(對文件壓縮的本質實際上是針對字節的操做,也屬於一種流的操做),實現了基本一致的功能。

  下面的代碼展現了GZipStream的使用方法,DeflateStream和GZipStream的使用方法幾乎徹底一致:

    class Program
    {
        // 緩存數組的長度
        private const int bufferSize = 1024;

        static void Main(string[] args)
        {
            string test = GetTestString();
            byte[] original = Encoding.UTF8.GetBytes(test);
            byte[] compressed = null;
            byte[] decompressed = null;
            Console.WriteLine("數據的原始長度是:{0}", original.LongLength);
            // 1.進行壓縮
            // 1.1 壓縮進入內存流
            using (MemoryStream target = new MemoryStream())
            {
                using (GZipStream gzs = new GZipStream(target, CompressionMode.Compress, true))
                {
                    // 1.2 將數據寫入壓縮流
                    WriteAllBytes(gzs, original, bufferSize);
                }
                compressed = target.ToArray();
                Console.WriteLine("壓縮後的數據長度:{0}", compressed.LongLength);
            }
            // 2.進行解壓縮
            // 2.1 將解壓後的數據寫入內存流
            using (MemoryStream source = new MemoryStream(compressed))
            {
                using (GZipStream gzs = new GZipStream(source, CompressionMode.Decompress, true))
                {
                    // 2.2 從壓縮流中讀取全部數據
                    decompressed = ReadAllBytes(gzs, bufferSize);
                }
                Console.WriteLine("解壓後的數據長度:{0}", decompressed.LongLength);
                Console.WriteLine("解壓先後是否相等:{0}", test.Equals(Encoding.UTF8.GetString(decompressed)));
            }
            Console.ReadKey();
        }

        // 01.取得測試數據
        static string GetTestString()
        {
            StringBuilder builder = new StringBuilder();
            for (int i = 0; i < 10; i++)
            {
                builder.Append("我是測試數據\r\n");
                builder.Append("我是長江" + (i + 1) + "號\r\n");
            }
            return builder.ToString();
        }

        // 02.從一個流總讀取全部字節
        static Byte[] ReadAllBytes(Stream stream, int bufferlength)
        {
            Byte[] buffer = new Byte[bufferlength];
            List<Byte> result = new List<Byte>();
            int read;
            while ((read = stream.Read(buffer, 0, bufferlength)) > 0)
            {
                if (read < bufferlength)
                {
                    Byte[] temp = new Byte[read];
                    Array.Copy(buffer, temp, read);
                    result.AddRange(temp);
                }
                else
                {
                    result.AddRange(buffer);
                }
            }
            return result.ToArray();
        }

        // 03.把字節寫入一個流中
        static void WriteAllBytes(Stream stream, Byte[] data, int bufferlength)
        {
            Byte[] buffer = new Byte[bufferlength];
            for (long i = 0; i < data.LongLength; i += bufferlength)
            {
                int length = bufferlength;
                if (i + bufferlength > data.LongLength)
                {
                    length = (int)(data.LongLength - i);
                }
                Array.Copy(data, i, buffer, 0, length);
                stream.Write(buffer, 0, length);
            }
        }
    }
View Code

  上述代碼的運行結果以下圖所示:

  

  須要注意的是:使用 GZipStream 類壓縮大於 4 GB 的文件時將會引起異常

  經過GZipStream的構造方法能夠看出,它是一個典型的Decorator裝飾者模式的應用,所謂裝飾者模式,就是動態地給一個對象添加一些額外的職責。對於增長新功能這個方面,裝飾者模式比新增一個之類更爲靈活。就拿上面代碼中的GZipStream來講,它擴展的是MemoryStream,爲Write方法增長了壓縮的功能,從而實現了壓縮的應用。

擴展:許多資料代表.NET提供的GZipStream和DeflateStream類型的壓縮算法並不出色,也不能調整壓縮率,有些第三方的組件例如SharpZipLib實現了更高效的壓縮和解壓算法,咱們能夠在nuget中爲項目添加該組件。

3.3 Serializable特性有什麼做用?

  經過上面的流類型能夠方便地操做各類字節流,可是如何把現有的實例對象轉換爲方便傳輸的字節流,就須要使用序列化技術。對象實例的序列化,是指將實例對象轉換爲可方便存儲、傳輸和交互的流。在.NET中,經過Serializable特性提供了序列化對象實例的機制,當一個類型被申明爲Serializable後,它就能被諸如BinaryFormatter等實現了IFormatter接口的類型進行序列化和反序列化。

    [Serializable]
    public class Person
    {
        ......
    }

  可是,在實際開發中咱們會遇到對於一些特殊的不但願被序列化的成員,這時咱們能夠爲某些成員添加NonSerialized特性。例如,有以下代碼所示的一個Person類,其中number表明學號,name表明姓名,咱們不但願name被序列化,因而能夠爲name添加NonSerialized特性:

    class Program
    {
        static void Main(string[] args)
        {
            Person obj = new Person(26, "Edison Chou");
            Console.WriteLine("初始狀態:");
            Console.WriteLine(obj);

            // 序列化對象
            byte[] data = Serialize(obj);
            // 反序列化對象
            Person newObj = DeSerialize(data);

            Console.WriteLine("通過序列化和反序列化後:");
            Console.WriteLine(newObj);

            Console.ReadKey();
        }

        // 序列化對象
        static byte[] Serialize(Person p)
        {
            // 使用二進制序列化
            IFormatter formatter = new BinaryFormatter();
            using (MemoryStream ms = new MemoryStream())
            {
                formatter.Serialize(ms, p);
                return ms.ToArray();
            }
        }

        // 反序列化對象
        static Person DeSerialize(byte[] data)
        {
            // 使用二進制反序列化
            IFormatter formatter = new BinaryFormatter();
            using (MemoryStream ms = new MemoryStream(data))
            {
                Person p = formatter.Deserialize(ms) as Person;
                return p;
            }
        }
    }
View Code

  上述代碼的運行結果以下圖所示:

  

注意:當一個基類使用了Serializable特性後,並不意味着其全部子類都能被序列化。事實上,咱們必須爲每一個子類都添加Serializable特性才能保證其能被正確地序列化。

3.4 .NET提供了哪幾種可進行序列化操做的類型?

  咱們已經理解了如何把一個類型聲明爲可序列化的類型,可是萬里長征只走了第一步,具體完成序列化和反序列化的操做還須要一個執行這些操做的類型。爲了序列化具體實例到某種專用的格式,.NET中提供了三種對象序列格式化類型:BinaryFormatterSoapFormatterXmlSerializer

  (1)BinaryFormatter

  顧名思義,BinaryFormatter可用於將可序列化的對象序列化成二進制的字節流,在前面Serializable特性的代碼示例中已經展現過,這裏再也不重複展現。

  (2)SoapFormatter

  SoapFormatter致力於將可序列化的類型序列化成符合SOAP規範的XML文檔以供使用。在.NET中,要使用SoapFormatter須要先添加對於SoapFormatter的引用:

using System.Runtime.Serialization.Formatters.Soap;

Tips:SOAP是一種位於應用層的網絡協議,它基於XML,而且是Web Service的基本協議。

  (3)XmlSerializer

  XmlSerializer並不只僅針對那些標記了Serializable特性的類型,更爲須要注意的是,Serializable和NonSerialized特性在XmlSerializer類型對象的操做中徹底不起做用,取而代之的是XmlIgnore屬性。XmlSerializer能夠對沒有標記Serializable特性的類型對象進行序列化,可是它仍然有必定的限制:

  ① 使用XmlSerializer序列化的對象必須顯示地擁有一個無參數的公共構造方法

  所以,咱們須要修改前面代碼示例中的Person類,添加一個無參數的公共構造方法:

    [Serializable]
    public class Person
    {
        ......
        public Person()
        {
        }
        ......
    }

  ② XmlSerializer只能序列化公共成員變量

  所以,Person類中的私有成員_number便不能被XmlSerializer進行序列化:

    [Serializable]
    public class Person
    {
        // 私有成員沒法被XmlSerializer序列化
        private int _number;
    }

  (4)綜合演示SoapFormatter和XmlSerializer的使用方法:

  ①從新改寫Person類

    [Serializable]
    public class Person
    {
        // 私有成員沒法被XmlSerializer序列化
        private int _number;
        // 使用NonSerialized特性標記此成員不可被BinaryFormatter和SoapFormatter序列化
        [NonSerialized]
        public string _name;
        // 使用XmlIgnore特性標記此成員不可悲XmlSerializer序列化
        [XmlIgnore]
        public string _univeristy;

        public Person()
        {
        }

        public Person(int i, string s, string u)
        {
            this._number = i;
            this._name = s;
            this._univeristy = u;
        }

        public override string ToString()
        {
            string result = string.Format("學號是:{0},姓名是:{1},大學是:{2}", _number, _name, _univeristy);
            return result;
        }
    }
View Code

  ②新增SoapFormatter和XmlSerializer的序列化和反序列化方法

    #region 01.SoapFormatter
    // 序列化對象-SoapFormatter
    static byte[] SoapFormatterSerialize(Person p)
    {
        // 使用Soap協議序列化
        IFormatter formatter = new SoapFormatter();
        using (MemoryStream ms = new MemoryStream())
        {
            formatter.Serialize(ms, p);
            return ms.ToArray();
        }
    }

    // 反序列化對象-SoapFormatter
    static Person SoapFormatterDeSerialize(byte[] data)
    {
        // 使用Soap協議反序列化
        IFormatter formatter = new SoapFormatter();
        using (MemoryStream ms = new MemoryStream(data))
        {
            Person p = formatter.Deserialize(ms) as Person;
            return p;
        }
    } 
    #endregion

    #region 02.XmlSerializer
    // 序列化對象-XmlSerializer
    static byte[] XmlSerializerSerialize(Person p)
    {
        // 使用XML規範序列化
        XmlSerializer serializer = new XmlSerializer(typeof(Person));
        using (MemoryStream ms = new MemoryStream())
        {
            serializer.Serialize(ms, p);
            return ms.ToArray();
        }
    }

    // 反序列化對象-XmlSerializer
    static Person XmlSerializerDeSerialize(byte[] data)
    {
        // 使用XML規範反序列化
        XmlSerializer serializer = new XmlSerializer(typeof(Person));
        using (MemoryStream ms = new MemoryStream(data))
        {
            Person p = serializer.Deserialize(ms) as Person;
            return p;
        }
    } 
    #endregion
View Code

  ③改寫Main方法進行測試

    static void Main(string[] args)
    {
        Person obj = new Person(26, "Edison Chou", "CUIT");
        Console.WriteLine("原始對象爲:");
        Console.WriteLine(obj.ToString());

        // 使用SoapFormatter序列化對象
        byte[] data1 = SoapFormatterSerialize(obj);
        Console.WriteLine("SoapFormatter序列化後:");
        Console.WriteLine(Encoding.UTF8.GetString(data1));
        Console.WriteLine();
        // 使用XmlSerializer序列化對象
        byte[] data2 = XmlSerializerSerialize(obj);
        Console.WriteLine("XmlSerializer序列化後:");
        Console.WriteLine(Encoding.UTF8.GetString(data2));

        Console.ReadKey();
    }
View Code

  示例運行結果以下圖所示:

3.5 如何自定義序列化和反序列化的過程?

  對於某些類型,序列化和反序列化每每有一些特殊的操做或邏輯檢查需求,這時就須要咱們可以主動地控制序列化和反序列化的過程。.NET中提供的Serializable特性幫助咱們很是快捷地申明瞭一個可序列化的類型(所以也就缺少了靈活性),但不少時候因爲業務邏輯的要求,咱們須要主動地控制序列化和反序列化的過程。所以,.NET提供了ISerializable接口來知足自定義序列化需求。

   下面的代碼展現了自定義序列化和反序列化的類型模板:

    [Serializable]
    public class MyObject : ISerializable
    {
        protected MyObject(SerializationInfo info, StreamingContext context)
        {
            // 在此構造方法中實現反序列化
        }

        public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            // 在此方法中實現序列化
        }
    }

  如上代碼所示,GetObjectData和特殊構造方法都接收兩個參數:SerializationInfo 類型參數的做用相似於一個哈希表,經過key/value對來存儲整個對象的內容,而StreamingContext 類型參數則包含了流的當前狀態,咱們能夠根據此參數來判斷是否須要序列化和反序列化類型獨享。

  若是基類實現了ISerializable接口,則派生類須要針對本身的成員實現反序列化構造方法,而且重寫基類中的GetObjectData方法。

  下面經過一個具體的代碼示例,來了解如何在.NET程序中自定義序列化和反序列化的過程:

  ①首先咱們須要一個須要被序列化和反序列化的類型,該類型有可能被其餘類型繼承

    [Serializable]
    public class MyObject : ISerializable
    {
        private int _number;
        [NonSerialized]
        private string _name;

        public MyObject(int num, string name)
        {
            this._number = num;
            this._name = name;
        }

        public override string ToString()
        {
            return string.Format("整數是:{0}\r\n字符串是:{1}", _number, _name);
        }

        // 實現自定義的序列化
        protected MyObject(SerializationInfo info, StreamingContext context)
        {
            // 從SerializationInfo對象(相似於一個HashTable)中讀取內容
            this._number = info.GetInt32("MyObjectInt");
            this._name = info.GetString("MyObjectString");
        }

        // 實現自定義的反序列化
        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            // 將成員對象寫入SerializationInfo對象中
            info.AddValue("MyObjectInt", this._number);
            info.AddValue("MyObjectString", this._name);
        }
    }
View Code

  ②隨後編寫一個繼承自MyObject的子類,並添加一個私有的成員變量。須要注意的是:子類必須負責序列化和反序列化本身添加的成員變量

    [Serializable]
    public class MyObjectSon : MyObject
    {
        // 本身添加的成員
        private string _sonName;

        public MyObjectSon(int num, string name)
            : base(num, name)
        {
            this._sonName = name;
        }

        public override string ToString()
        {
            return string.Format("{0}\r\n之類字符串是:{1}", base.ToString(), this._sonName);
        }

        // 實現自定義反序列化,只負責子類添加的成員
        protected MyObjectSon(SerializationInfo info, StreamingContext context)
            : base(info, context)
        {
            this._sonName = info.GetString("MyObjectSonString");
        }

        // 實現自定義序列化,只負責子類添加的成員
        public override void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            base.GetObjectData(info, context);
            info.AddValue("MyObjectSonString", this._sonName);
        }
    }
View Code

  ③最後編寫Main方法,測試自定義的序列化和反序列化

    class Program
    {
        static void Main(string[] args)
        {
            MyObjectSon obj = new MyObjectSon(10086, "Edison Chou");
            Console.WriteLine("初始對象爲:");
            Console.WriteLine(obj.ToString());
            // 序列化
            byte[] data = Serialize(obj);
            Console.WriteLine("通過序列化與反序列化以後:");
            Console.WriteLine(DeSerialize(data));

            Console.ReadKey();
        }

        // 序列化對象-BinaryFormatter
        static byte[] Serialize(MyObject p)
        {
            // 使用二進制序列化
            IFormatter formatter = new BinaryFormatter();
            using (MemoryStream ms = new MemoryStream())
            {
                formatter.Serialize(ms, p);
                return ms.ToArray();
            }
        }

        // 反序列化對象-BinaryFormatter
        static MyObject DeSerialize(byte[] data)
        {
            // 使用二進制反序列化
            IFormatter formatter = new BinaryFormatter();
            using (MemoryStream ms = new MemoryStream(data))
            {
                MyObject p = formatter.Deserialize(ms) as MyObject;
                return p;
            }
        }
    }
View Code

  上述代碼的運行結果以下圖所示:

      

  從結果圖中能夠看出,因爲實現了自定義的序列化和反序列化,從而原先使用Serializable特性的默認序列化和反序列化算法沒有起做用,MyObject類型的全部成員通過序列化和反序列化以後均被完整地還原了,包括申明瞭NonSerialized特性的成員。

參考資料

(1)朱毅,《進入IT企業必讀的200個.NET面試題》

(2)張子陽,《.NET之美:.NET關鍵技術深刻解析》

(3)王濤,《你必須知道的.NET》

(4)solan300,《C#基礎知識梳理之StringBuilder

(5)周旭龍,《ASP.NET WebForm溫故知新

(6)陸敏技,《C#中機密文本的保存方案

 

相關文章
相關標籤/搜索