天天看點

《C#進階程式設計》讀書筆記

本文已同步發表在部落格園:http://www.cnblogs.com/davidsheh/p/5236686.html
  1. C#類型的取值範圍
    《C#進階程式設計》讀書筆記
  2. 通路限制符
    修飾符 應用于 說明
    public 所有類型或成員 任何代碼均可以通路該項
    protected 類型和内嵌類型的所有成員 隻有派生的類型能夠通路該項
    internal 所有類型或成員 隻能在包含它的程式集中通路該項
    private 類型和内嵌類型的所有成員 隻能在它所屬的類型中通路該項
    protected internal 類型和内嵌類型的所有成員 隻能在包含它的程式集和派生類型的任何代碼中通路該項
  3. C#常見的修飾符
    修飾符 應用于 說明
    new 函數成員 成員用相同的簽名隐藏繼承的成員
    static 所有成員 成員不作用于類的具體執行個體
    virtual 僅函數成員 成員可以由派生類重寫
    abstract 僅函數成員 虛拟成員定義了成員的簽名,但沒有提供實作代碼
    override 僅函數成員 成員重寫了繼承的虛拟或抽象成員
    sealed 類、方法和屬性 對于類,不能繼承自密封類。對于屬性和方法,成員重寫已繼承的虛拟成員,但任何派生類中的任何成員都不能重寫該成員。該修飾符必須與override一起使用
    extern 僅靜态[DllImport]方法 成員在外部用另一種語言實作
  4. 結構體
    • 結構是值類型,不是引用類型。
    • 存儲在棧中或存儲為内聯(inline)(如果它們是存儲在堆中的另一個對象的一部分),其生存期的限制與簡單的資料類型一樣。
    • 結構體不支援繼承。
    • 對于結構構造函數的工作方式有一些差別。尤其是編譯器總是提供一個無參數的預設構造函數,它是不允許替換的。
    • 使用結構,可以指定字段如何在記憶體中的布局。
    • 注意,因為結構是值類型,是以

      new

      運算符與類和其他引用類型的工作方式不同。

      new

      運算符并不配置設定堆中的記憶體,而是隻調用相應的構造函數,根據傳送給它的參數初始化所有的字段。
    • 結構遵循其他資料類型都遵循的規則:在使用前所有的元素都必須進行初始化。在結構上調用

      new

      運算符,或者給所有的字段分别指派,結構就完全初始化了。當然,如果結構定義為類的成員字段,在初始化包含的對象時,該結構會自動初始化為0。
    • 結構是會影響性能的值類型,但根據使用結構的方式,這種影響可能是正面的,也可能是負面的。正面的影響是為結構配置設定記憶體時,速度非常快,因為它們将内聯或者儲存在棧中。在結構超出了作用域被删除時,速度也很快。負面影響是,隻要把結構作為參數來傳遞或者把一個結構賦予另一個結構(如A=B,其 中A和B是結構),結構的所有内容就被複制,而對于類,則隻複制引用。這樣就會有性能損失,根據結構的大小,性能損失也不同。注意,結構主要用于小的資料結構。但當把結構作為參數傳遞給方法時,應把它作為ref參數傳遞,以避免性能損失————此時隻傳遞了結構在記憶體中的位址,這樣傳遞速度就與在類中的傳遞速度一樣快了。但如果這樣做,就必須注意被調用的方法可以改變結構的值。
    • 結構不是為繼承設計的。這意味着:它不能從一個結構中繼承。唯一的例外是對應的結構(和C#中的其他類型一樣)最終派生于類

      System.Object

      。是以,結構也可以通路

      System.Object

      的方法。在結構中,甚至可以重寫

      System.Object

      中的方法————如重寫

      ToString()

      方法。結構的繼承鍊是:每個結構派生自

      System.ValueType

      類,

      System.ValueType

      類又派生自

      System.Object

      ValueType

      并沒有給

      Object

      添加任何新成員,但提供了一些更适合結構的實作方式。注意,不能為結構提供其他基類,每個結構都派生自

      ValueType

    • 為結構定義構造函數的方式與為類定義構造函數的方式相同,但 不允許定義無參數的構造函數。預設構造函數把數值字段都初始化為0,把引用類型字段初始化為

      null

      ,且總是隐式地給出,即使提供了其他帶參數的構造函數,也是如此。提供字段的初始值也不能繞過預設構造函數。
  5. 擴充方法
    • 擴充方法允許改變一個類,但不需要該類的源代碼。是以使用擴充方法的情景之一是,當不知道類的源碼或者不想修改該類的源碼卻想擴充該類,就可以用擴充方法。
    • 擴充方法是靜态方法,它是類的一部分,但實際上沒有放在類的源代碼中。
    • 擴充方法需放在靜态類中。
    • 對于擴充方法,第一個參數是要擴充的類型,它放在

      this

      關鍵字的後面。
    • 在擴充方法中,可以通路所擴充類型的所有共有方法和屬性。
    • 如果擴充方法與類中的某個方法同名,就從來不會調用擴充方法。類中已有的任何執行個體方法優先。
  6. var

    關鍵字。編譯器可以根據變量的初始化值“推斷 ” 變量的類型。使用var關鍵字需要遵循的一些規則:
    • 變量必須初始化。否則,編譯器就沒有推斷變量類型的依據。
    • 初始化器不能為空。
    • 初始化器必須放在表達式中。
    • 不能把初始化器設定為一個對象,除非在初始化器中建立了一個新對象。
  7. 密封類和密封方法
    • C#允許把類和方法聲明為

      sealed

      。對于類,這表示不能繼承該類;對于方法,這表示不能重寫該方法。
    • 在把類或方法标記為

      sealed

      時,最可能的情形是:如果要對庫、類或自己編寫的其他類作用域之外的類或方法進行操作,則重寫某些功能會導緻代碼混亂。也可以因商業原因把類或方法标記為

      sealed

      ,以防第三方以違反授權協定的方式擴充該類。但一般情況下,在把類或成員标記為

      sealed

      時要小心,因為這麼做會嚴重限制它的使用方式。即使認為它不能對繼承自一個類或重寫類的某個成員發揮作用,仍有可能在将來的某個時刻,有人會遇到我們沒有預料到的情形,此時這麼做就很有用。.Net基類庫大量使用了密封類 ,使希望從這些類中派生出自己的類的第三方開發人員無法通路這些類。例如,

      string

      就是一個密封類。
  8. 限制
    • 泛型支援的幾種限制類型:
    限制 說明
    where T : struct 對于結構限制,類型T必須是值類型
    where T : class 類限制指定類型T必須是引用類型
    where T : IFoo 指定類型T必須實作接口IFoo
    where T : Foo 指定類型T必須派生自基類Foo
    where T : new() 這是一個構造函數限制,指定類型T必須有一個預設構造函數
    where T1 : T2 這個限制也可以指定類型T1派生自泛型類型T2。該限制也稱為裸類型限制
    • 隻能為預設構造函數定義構造函數限制,不能為其他構造函數定義構造函數限制。
    • 在C#中,

      where

      子句的一個重要限制是,不能定義必須由泛型類型實作的運算符。運算符不能再借口中定義。在

      where

      子句中,隻能定義基類、接口和預設構造函數。
  9. 複制數組
    • 如果數組的元素是值類型,調用Clone()方法就會複制所有值。如,int[] intArray1 = {1, 2}; int[] intArray2 = (int[])intArray1.Clone();其中intArray2數組的元素也變成了{1, 2}
    • 如果數組包含引用類型,則不複制元素,而隻複制引用。
    • 除了使用Clone()方法之外,還可以使用Array.Copy()方法建立淺表副本。
    • Clone()方法和Copy()方法有一個重要差別:Clone()方法會建立一個新數組,而Copy()方法必須傳遞階數相同且有足夠元素的已有數組。
    • 如果需要包含引用類型的數組的深層副本,就必須疊代數組并建立新對象。
  10. Array

    類使用

    QuickSort

    算法對數組中的元素進行排序。

    Array

    類中的Sort()方法需要數組中的元素實作

    IComparable

    接口。簡單類型(如System.String和System.Int32)已經實作了IComparable接口。
  11. 元組
    • 數組合并了相同類型的對象,而元組合并了不同類型的對象。
    • .NET 4

      定義了8個泛型

      Tuple

      類和一個靜态

      Tuple

      類,不同泛型

      Tuple

      類支援不同數量的元素。例如,

      Tuple<T1>

      包含一個元素,

      Tuple<T1, T2>

      包含兩個元素,以此類推。
    • 代碼示例:
      public class TupleExample
      {
          static void Main()
          {
              TupleExample example = new TupleExample();
              var result = example.Divide(, );
              Console.WriteLine("result of division: {0}, reminder: {1}", result.Item1, result.Item2);
          }
      
          public static Tuple< int, int > Divide(int dividend, int divisor)
          {
              int result = dividend / divisor;
              int reminder = dividend % divisor;
      
              return TupleExample.Create<int, int>(result, reminder);
          }
      }
                 
    • 如果元組包含項超過8個,就可以使用帶8個參數的Tuple類定義。最後一個模闆參數是TRest,表示必須給它傳遞一個元組,這樣就可以建立帶任意個參數的元組了。示例:
      var tuple = Tuple.Create<string, string, string, int, int, int, double, Tuple<int, int>>("Stephanie", "Alina", "Nagel", , , , , Tuple.Create<int, int>(, ));
                 
  12. 運算符
    • is

      運算符:可以檢查對象是否與特定的類型相容。“相容”表示對象是該類型或者派生自該類型。
    • as

      運算符:用于執行引用類型的顯示類型轉換。如果要轉換的類型與制定的類型相容,轉換就會成功進行;如果類型不相容,

      as

      運算符就會傳回

      null

      值。
    • sizeof

      運算符:使用該運算符可以确定棧中值類型需要的長度(機關是位元組);如果對于複雜類型(和非基元類型)使用該運算符,就需要把代碼寫在

      unsafe

      塊中,如:

      unsafe{Console.WriteLine(sizeof(Customer));}

    • 可空類型和運算符:通常可空類型與一進制或二進制運算符一起使用時,如果其中一個操作數或兩個操作數都是

      null

      ,其結果就是

      null

      。如:

      int? a = null; int? b = a + 4; // b = null int? c = a * 5; // c = null

    • 空合并運算符(??):該運算符提供了一種快捷方式,可以在處理可空類型和引用類型時表示

      null

      可能的值。這個運算符放在兩個操作數之間,第一個操作數必須是一個可空類型或者引用類型;第二個操作數必須與第一個操作數的類型相同,或者可以隐含地轉換為第一個操作數的類型。
  13. 比較引用類型的相等性
    • ReferenceEquals()方法:該方法是一個靜态方法,測試兩個引用是否引用類的同一個執行個體,特别是兩個引用是否包含記憶體中的相同位址。如果提供的兩個引用引用同一個對象執行個體,則傳回

      true

      ,否則傳回

      false

      。但是它認為

      null

      等于

      null

      。另外,該方法在應用于值類型時,它總是傳回false,因為為了調用這個方法,值類型需要裝箱到對象中。
    • 虛拟的Equals()方法:Equals()虛拟版本的

      System.Object

      實作代碼也可以比較引用。但因為這個方法是虛拟的,是以可以在自己的類中重寫它,進而按值來比較對象。特别是如果希望類的執行個體用作字典中的鍵,就需要重寫這個方法,以比較相關值。否則,根據重寫

      Object.GetHashCode()

      的方式,包含對象的字典類要麼不工作,要麼工作的效率非常低。在重寫Equals()方法時要注意,重寫的代碼不會抛出異常。同理,這是因為如果抛出異常,字典類就會出問題,一些在内部調用這個方法的

      .NET

      基類也可能出問題。
    • 靜态的Equals()方法:Equals()的靜态版本與其虛拟執行個體版本的作用相同,其差別是靜态版本帶有兩個參數,并對它們進行相等比較。這個方法可以處理兩個對象中有一個是

      null

      的情況,是以,如果一個對象可能是

      null

      ,這個方法就可以抛出異常,提供額外保護。靜态重載版本首先要檢查它傳遞的引用是否為

      null

      。如果他們都是

      null

      ,就傳回

      true

      (因為

      null

      null

      相等)。如果隻有一個引用是

      null

      ,就傳回

      false

      。如果兩個引用實際上引用了某個對象,它就調用Equals()的虛拟執行個體版本。這表示在重寫Equals()的執行個體版本時,其效果相當于也重寫了靜态版本。
    • 比較運算符(==):最好将比較運算符看作嚴格的值比較和嚴格的引用比較之間的中間選項。在大多數情況下,下面的代碼表示正在比較引用:

      bool b = (x == y);// x, y object references

  14. 運算符重載
    • 運算符重載的聲明方式與方法相同,但

      operator

      關鍵字告訴編譯器,它實際上是一個自定義的運算符重載,後面是相關運算符的實際符号,傳回類型是在使用這個運算符時獲得的類型。
    • 對于二進制運算符(它帶兩個參數),如

      +

      -

      運算符,第一個參數是運算符左邊的值,第二個參數是運算符右邊的值。
    • 一般把運算符左邊的參數命名為

      lhs

      ,運算符右邊的參數命名為

      rhs

    • C#要求所有的運算符重載都聲明為

      public

      static

      ,這表示它們與它們的類或結構相關聯,而不是與某個特定執行個體相關聯,是以運算符重載的代碼體不能通路非靜态類成員,也不能通路

      this

      辨別符。
    • C#語言要求成對重載比較運算符。即,如果重載了

      ==

      ,也就必須重載

      !=

      ;否則會産生編譯錯誤。另外,比較運算符必須傳回布爾類型的值。這是它們與算術運算符的根本差別。
    • 在重載

      ==

      !=

      時,還必須重載從

      System.Object

      中繼承的Equals()和GetHashCode()方法,否則會産生一個編譯警告。原因是Equals()方法應實作與

      ==

      運算符相同類型的相等邏輯。
  15. 委托
    • 了解委托的一個要點是它們的類型安全性非常高。
    • 了解委托的一種好方式是把委托當作這樣一件事,它給方法的簽名和傳回類型指定名稱。
    • Action

      • Action

        是無傳回值的泛型委托。
      • Action

        表示無參,無傳回值的委托
      • Action<int,string>

        表示有傳入參數

        int

        ,

        string

        無傳回值的委托
      • Action<int,string,bool>

        表示有傳入參數

        int

        ,

        string

        ,

        bool

        無傳回值的委托
      • Action<int,int,int,int>

        表示有傳入4個

        int

        型參數,無傳回值的委托
      • Action

        至少0個參數,至多16個參數,無傳回值。
    • Func

      • Func

        是有傳回值的泛型委托
      • Func<int>

        表示無參,傳回值為int的委托
      • Func<object,string,int>

        表示傳入參數為

        object

        ,

        string

        傳回值為int的委托
      • Func<object,string,int>

        表示傳入參數為

        object

        ,

        string

        傳回值為int的委托
      • Func<T1,T2,,T3,int>

        表示傳入參數為

        T1

        ,

        T2

        ,

        T3

        (泛型)傳回值為

        int

        的委托
      • Func

        至少0個參數,至多16個參數,根據傳回值泛型傳回。必須有傳回值,不可

        void

  16. Lambda

    表達式
    • 隻要有委托參數類型的地方,就可以使用

      Lambda

      表達式。或者說

      Lambda

      表達式可以用于類型是一個委托的任意地方。
    • 如果隻有一個參數,隻寫出參數名就足夠了。如果委托使用多個參數,就把參數名放在小括号中。為了友善可以在小括号中給變量添加參數類型。
    • 如果

      Lambda

      表達式隻有一條語句,在方法塊内就不需要花括号和

      return

      語句,因為編譯器會添加一條隐式

      return

      語句。
  17. 正規表達式
    • 常用的特定字元和轉義序列如下表:
    符 号 含 義 示 例 比對的示例
    ^ 輸入文本的開頭 ^B B,但隻能是文本中的第一個字元
    $ 輸入文本的結尾 X$ X,但隻能是文本中的最後一個字元
    . 除了換行符(\n)以外的所有單個字元 i.ation isation、ization
    * 可以重複0次或多次的前導字元 ra*t rt、rat、raat和raaat等
    + 可以重複1次或多次的前導字元 ra+t rat、raat和raaat等(但不能是rt)
    ? 可以重複0次或1次的前導字元 ra?t 隻有rt和rat比對
    \s 任何空白字元 \sa [space]a、\ta、\na(其中[space]表示空格,\t和\n都是轉移字元)
    \S 任何不是空白的字元 \SF aF、rF、cF,但不能是\tF
    \b 字邊界 ion\b 以ion結尾的任何字
    \B 不是字邊界的任意位置 \BX\B 字中間的任何X
    • 可以把替換的字元放在方括号中,請求比對包含這些字元。例如,

      [1|c]

      表示字元可以是

      1

      c

      。在方括号中,也可以指定一個範圍,例如

      [a-z]

      表示所有的小寫字母,

      [A-E]

      表示

      A

      ~

      E

      之間的所有大寫字母(包括字母

      A

      E

      ),

      [0-9]

      表示一個數字。如果要搜尋一個整數,就可以編寫

      [0-9]+

  18. 集合
    • 連結清單。

      LinkedList<T>

      是一個雙向連結清單,其元素指向它前面和後面的元素。其特點是:插入快,查找慢。
    • 有序清單。如果需要基于鍵對所需集合排序,就可以使用

      SortedList<TKey,TValue>

      類,這個類按照鍵給元素排序。
    • 字典。
      • 字典的主要特征是能根據鍵快速查找值。也可以自由添加和删除元素,這點有點像

        List<T>

        類,但沒有在記憶體中移動後續元素的性能開銷。
      • 用作字典中鍵的類型必須重寫Object類的GetHashCode()方法。隻要字典類需要确定元素的位置,它就要調用GetHashCode()方法。GetHashCode()方法傳回的

        int

        由字典用于計算在對應位置放置元素的索引。
      • 字典的性能取決于GetHashCode()方法的實作代碼。
      • 除了實作GetHashCode()方法之外,鍵類型還必須實作

        IEquatable<T>.Equals()

        方法,或重寫

        Object

        類的Equals()方法。因為不同的鍵對象可能傳回相同的散列代碼,是以字典使用Equals()方法來比較鍵。
  19. GetHashCode()方法的實作代碼必須滿足如下要求:
    • 相同的對象應總是傳回相同的值。
    • 不同的對象可以傳回相同的值。
    • 它應執行得比較快,計算的開銷不大。
    • 它不能抛出異常。
    • 它應至少使用一個執行個體字段。
    • 散列代碼值應平均分布在

      int

      可以存儲的整個數字範圍上。
    • 散列代碼最好在對象的生存期中不發生變化。
  20. 如果為Equals()方法提供了重寫版本,但沒有提供GetHashCode()方法的重寫版本,C#編譯器就會顯示一個編譯警告。
  21. LINQ

    • 查詢表達式必須以

      from

      子句開頭,以

      select

      group

      子句結束。在這兩個子句之間,可以使用

      where

      orderby

      join

      let

      和其他

      from

      子句。
    • LINQ

      IEnumerable<T>

      接口提供了各種擴充方法,以便使用者在實作了該接口的任意集合上使用

      LINQ

      查詢。
  22. 釋放非托管的資源
    • 在定義一個類時,可以使用兩種機制來自動釋放非托管的資源。這些機制常常放在一起實作,因為每種機制都為問題提供了略微不同的解決辦法。
    • 釋放非托管資源的兩種機制:聲明一個析構函數(或終結器);在類中實作

      System.IDisposable

      接口。
  23. 析構函數
    • 在銷毀C++對象時,其析構函數會立即運作。但由于使用C#時垃圾回收器的工作方式,無法确定C#對象的析構函數合适執行。是以,不能在析構函數中放置需要在某一時刻運作的代碼,也不應使用能以任意順序對不同類的執行個體調用的析構函數。如果對象占用了寶貴而重要的資源,應盡快釋放這些資源,此時就不能等待垃圾回收器來釋放了。
    • C#析構函數的實作會延遲對象最終從記憶體中删除的時間。沒有析構函數的對象會在垃圾回收器的一次進行中從記憶體中删除,但有析構函數的對象需要兩次處理才能銷毀:第一次調用析構函數時,沒有删除對象,第二次調用才真正删除對象。另外,運作庫使用一個線程來執行所有對象的Finalize()方法。如果頻繁使用析構函數,而且使用它們執行長時間的清理任務,對性能的影響就會非常顯著。
  24. IDisposable接口
    • 在C#中,推薦使用

      System.IDisposable

      接口替代析構函數。

      IDisposable

      接口定義了一種模式(具有語言級的支援),該模式為釋放非托管的資源提供了确定的機制,并避免産生析構函數固有的與垃圾回收器相關的問題。

      IDisposable

      接口聲明了一個Dispos()方法,它不帶參數,傳回

      void

    • Dispose()方法的實作代碼顯式地釋放由對象直接使用的所有非托管資源,并在所有也實作

      IDisposable

      接口的封裝對象上調用Dispose()方法。這樣,Dispose()方法為何時釋放非托管資源提供了精确的控制。
以上是《C#進階程式設計》前二十章的讀書筆記。筆記摘錄了筆者認為易忘的知識點,友善以後查閱和複習。摘抄整理過程中難免疏忽和遺漏,如有錯誤不當之處,請不吝指出,在此感激不盡!
聲明:本文歡迎轉載和分享,但是請尊重作者的勞動成果,轉載分享時請注明出處:http://blog.csdn.net/wenxin2011/article/details/50790837。

繼續閱讀