<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12
        My Github

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

      Index:

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

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

      (3)字符串、集合與流

      (4)委托、事件、反射與特性

      (5)多線程開發基礎

      (6)ADO.NET與數據庫開發基礎

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

      一、字符串處理

      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[]對象之間如何相互轉換?

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

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

       ?。?)字節:byte由8個bit構成,其值可以由一個0~255的整數表示;

        (3)編碼:編碼是數字信息和現實信息的轉換機制,一種編碼通常就定義了一種字符集和轉換的原則,常用的編碼方式包括UTF8、GB2312、Unicode等。

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

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

        下面的代碼展示了如何在字節數組和字符串之間進行轉換(分別使用UTF8、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的設計初衷就是解決該類問題。

       ?。?)為了保證安全性,SecureString是被分配在非托管內存上的(而普通String是被分配在托管內存中的),并且SecureString的對象從分配的一開始就以加密的形式存在,我們所有對于SecureString的操作(無論是增刪查改)都是逐字符進行的。

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

       ?。?)為了保證資源釋放,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#可以使用,但是被認為是不安全的?。4送猓绦蛑惺褂昧薓arshal.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.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#編譯器在編譯的過程中可以根據約束來檢查所有泛型類型的實參并確保其滿足約束條件。

       ?。?)主要約束

        一個泛型參數至多擁有一個主要約束,主要約束可以是一個引用類型、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.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。

       ?。?)BinaryFormatter

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

        (2)SoapFormatter

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

      using System.Runtime.Serialization.Formatters.Soap;

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

       ?。?)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;
          }

       ?。?)綜合演示SoapFormatter和XmlSerializer的使用方法:

       ?、僦匦赂膶慞erson類

          [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

       ?、谛略鯯oapFormatter和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

       ?、鄹膶慚ain方法進行測試

          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

       ?、圩詈缶帉慚ain方法,測試自定義的序列化和反序列化

          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#中機密文本的保存方案

       

      posted @ 2015-09-18 00:04  EdisonZhou  閱讀(6310)  評論(3)    收藏  舉報
      主站蜘蛛池模板: 国产精品中文字幕观看| 免费人成在线观看网站| 国产国产午夜福利视频| 亚洲av日韩在线资源| 风流老熟女一区二区三区 | 性姿势真人免费视频放| 亚洲成av人片不卡无码手机版| 亚洲国产午夜精品理论片妓女| 苏尼特右旗| 国产午夜福利片在线观看| 九九热爱视频精品| 蜜臀av色欲a片无人一区| 国产白袜脚足j棉袜在线观看| 午夜福利片1000无码免费| 日韩精品一区二区高清视频| 熟女亚洲综合精品伊人久久 | 在线观看成人av天堂不卡| 一区二区三区四区五区黄色 | 婷婷综合久久中文字幕| 综合人妻久久一区二区精品| 日韩在线观看精品亚洲| 在线人人车操人人看视频| 国产卡一卡二卡三免费入口| 亚洲の无码国产の无码步美| xxxx丰满少妇高潮| 欧美日韩一区二区三区视频播放| 亚洲精品国产自在现线最新| 不卡免费一区二区日韩av| 德保县| 国产SM重味一区二区三区| 92久久精品一区二区| 色爱综合另类图片av| 亚洲尤码不卡av麻豆| 一级国产在线观看高清| 熟女系列丰满熟妇AV| 精品视频在线观自拍自拍| 日本一级午夜福利免费区| 少妇人妻偷人精品免费| 桓台县| 日本乱码在线看亚洲乱码| 日韩精品三区二区三区|