[.NET] 《Effective C#》快速筆記(二)- .NET 資源托管


《Effective C#》快速筆記(二)- .NET 資源托管

 

簡介

  續 《Effective C#》讀書筆記(一)- C# 語言習慣

  .NET 中,GC 會幫助我們管理內存,我們並不需要去擔心內存泄漏,資源分配和指針初始化等問題。不過,它也並非萬能,因為非托管資源需要我們自己進行清理,如文件句柄、數據庫連接、GDI+ 對象和COM 對象等。

 

目錄

  • 十二、推薦使用成員初始化器而不是賦值語句
  • 十三、正確地初始化靜態成員變量
  • 十四、盡量減少重復的初始化邏輯
  • 十五、使用 using 和 try/finally 清理資源
  • 十六、避免創建非必要的對象
  • 十七、實現標准的銷毀模式
  • 十八、區分值類型和引用類型
  • 十九、保證 0 為值類型的有效狀態
  • 二十、保證值類型的常量性和原子性

 

十二、推薦使用成員初始化器而不是賦值語句

  1.成員初始化器:在聲明變量時就進行初始化,而不是在每個構造函數中進行。

  2.以下 3 種情況,應避免使用成員初始化器:

  (1)當你想要初始化對象為 0 或 null 時。因為系統默認的初始化工作(在所有代碼執行前)會將一切設置為 0 或 null,我們做的是一步多余的操作。而且,如果是值類型,那么性能非常差。

            MyValueType myVal1; //初始化為 0
            MyValueType myVal2 = new MyValueType(); //也是 0

  這兩條語句都將變量初始化為 0,但第一條是通過設置包含 myVal1 的這一塊內存為 0 實現的,而第二條使用的是 initobj 這條 IL 指令,導致了對 myVal2 變量的一次裝拆箱操作,這將占用額外性能與時間。

  (2)需要對同一個變量執行不同的初始化方式:

    class Program
    {
        /// <summary>
        /// 聲明並初始化
        /// </summary>
        private List<string> _lables = new List<string>();

        public Program() { }

        public Program(int capacity)
        {
            _lables = new List<string>(capacity);
        }
    }

  初始化該類時,假如使用的是帶 capacity 的構造函數,那么 List<string> 對象表示初始化了 2 次,頭一個就成為了垃圾對象。

  (3)將初始化代碼放在構造函數的合適理由:可以方便異常管理 try-catch。

 

十三、正確地初始化靜態成員變量

  1.在使用類型的實例之前,就應該初始化該類型的所有靜態成員變量。靜態構造函數是一個特殊的函數,將在其他所有方法、變量或屬性被第一次訪問之前執行。你可以使用這個函數來初始化靜態變量和實現單例模式等操作。

  2.靜態初始化器和靜態構造函數是初始化類的靜態成員的最佳選擇。

  3.使用靜態構造函數而不是靜態初始化器最常見的理由是可以捕捉和處理異常。

 

十四、盡量減少重復的初始化邏輯

  1.如果多個構造函數包含類似的邏輯,我們應將其提取到一個公共的構造函數中,這樣可以避免代碼重復,也可以利用構造函數初始化器生成更高效的代碼。

    class MyClass
    {
        private List<string> _lables;
        private string _name;

        public MyClass() : this(0, string.Empty)
        {

        }

        public MyClass(int capacity = 0, string name = "")
        {
            _lables = new List<string>(capacity);
            _name = name;
        }
    }

  第二個構造函數使用了 "" 來給出 name 的默認值,而不是采用 string.Empty ,因為 string.Empty 並不是一個編譯期的常量,而是一個定義在 string 類中的靜態屬性,所以不能用作參數的默認值。

  2.創建某個類型的第一個實例時所進行的操作順序:

  (1)靜態變量設置為 0 ;

  (2)執行靜態變量初始化器;

  (3)執行基類的靜態構造函數;

  (4)執行靜態構造函數;

  (5)實例變量設置為 0;

  (6)執行實例變量初始化器;

  (7)執行基類中合適的實例構造函數;

  (8)執行實例構造函數。

  3.使用初始化器來初始化簡單的資源,使用構造函數來初始化需要復雜邏輯的成員,同事不要忘記將調用抽取到一個構造函數中,以便減少重復。

  4.構造函數定義中只能使用一個初始化器,要么使用 This() 委托給另一個構造函數,要么使用 base() 調用基類的構造函數。

 

十五、使用 using 和 try/finally 清理資源

  1.使用了非托管系統資源的類型必須顯示地使用 IDisposable 接口的 Dispose() 來釋放,Using() 語句將生成一個Try/finally 塊。

 

十六、避免創建非必要的對象

  1.GC 可以很好地管理內存,但不管多高效,分配和銷毀堆上的對象總會花費很長時間,如果過多的創建引用對象,那么會對程序的性能產生嚴重的影響。

        public void Paint()
        {
            using (var myFont = new Font("Arial", 10.0f))
            {
                Console.WriteLine($"使用 {myFont} 進行繪畫");
            }
        }

  假如該方法被非常頻繁地調用。每次調用時都會創建另一個 Font 對象,但它包含的內容和之前的是完全一樣。GC 每次都要為你清理這些垃圾,顯然是非常低效的。

  可以把 myFont 提升為靜態變量。

        private readonly static Font _myFont = new Font("Arial", 10.0f);

        public void Paint()
        {
            Console.WriteLine($"使用 {_myFont} 進行繪畫");
        }

  2.降低程序中創建對象數量的方法。

  (1)將常用的局部變量提升為成員變量;

  (2)提供一個類,存放某個類型常用實例的單例對象。

   3.用 StringBuilder 進行復雜的字符串操作

 

十七、實現標准的銷毀模式

  1.IDisposable.Dispose() 方法的實現中需要完成如下 4 個任務:

  (1)釋放所有非托管資源;

  (2)釋放所有托管資源,包括釋放事件監聽程序;

  (3)設定一個狀態標志,表示該對象已經被銷毀;

  (4)跳過終結操作,調用 GC.SuppressFinalize(this) 即可。

 

十八、區分值類型和引用類型

  1.一般來說,我們創建的大部分是引用類型。

  2.確定創建值類型的條件有 4 個 

  (1)該類型的主要職責在於數據存儲;

  (2)該類型的公有接口都是由訪問其數據成員屬性定義的嗎?

  (3)你確定該類型絕不會有派生類型嗎?

  (4)你確定該類型永遠都不需要多態支持嗎?

  3.用值類型表示底層存儲數據的類型,用引用類型來封裝程序的行為。

  4.如果你對類型未來的用途不確定,應選擇引用類型。

 

十九、保證 0 為值類型的有效狀態

  1..NET 系統的默認初始化過程會將所有的對象設置為 0,建議將 0 作為枚舉類型的默認值。

  2.枚舉(enum)必須將 0 設定為枚舉值的一個有效選擇。所有的枚舉值都派生自 System.ValueType。枚舉的默認值開始於 0。

  3.在創建自定義枚舉值時,請確保 0 是一個有效的選項。若你定義的是標識(flag),那么可將 0 定義為沒有選中任何的標志。

    enum Week
    {
        None = 0,
        Monday = 1,
        Tuesday = 2,
        Wednesday = 3,
        Thursday = 4,
        Friday = 5,
        Saturday = 6,
        Sunday = 7
    }

 

二十、保證值類型的常量性和原子性

  1.常量性:自創建后,其值保持不變。因為不能更改內部狀態,就可以省去許多不必要的錯誤檢查,它也是線程安全的,也可以安全地暴露給外界,因為調用者不能改變對象的內部狀態。

  2.設計常量類型時,要確保沒有任何漏洞會導致內部狀態被外界更改。因為值類型不能派生,所以不必擔心會受到派生類影響。

  不過,如果常量中是可變的引用類型字段的話,我們就應該對這些可變類型進行防御性的復制。

    class MyClass
    {
        private readonly object[] _objs;

        public MyClass(ICollection<object> objs)
        {
            _objs = new object[objs.Count];
            objs.CopyTo(_objs, 0);  //復制
        }

        public IEnumerable<object> Objs => _objs;
    }

 

        static void Main(string[] args)
        {
            var objs = new object[10];
            var myClass = new MyClass(objs);
            objs[1] = "hi";

            Console.WriteLine(myClass.Objs.ToArray()[1]);

            Console.Read();
        }

  因為數組是引用類型,如果不使用 CopyTo 復制一個副本的話,在外部的 objs 修改就會直接影響 MyClass 中的 _objs,因為他們指向的都是同一個引用。

  2.不要盲目地對每一個屬性都加上 { get; set; }。

 

本系列

  《Effective C#》快速筆記(一)- C# 語言習慣

  《Effective C#》快速筆記(二)- .NET 資源托管

  《Effective C#》快速筆記(三)- 使用 C# 表達設計

  《Effective C#》快速筆記(四) - 使用框架

  《Effective C#》快速筆記(五) - C# 中的動態編程

  《Effective C#》快速筆記(六) - C# 高效編程要點補充

 

 


【博主】反骨仔

【原文】http://www.cnblogs.com/liqingwen/p/6761409.html

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM