2017年9月3日 星期日

.NET 如何在 C#,使用單例模式 (Singleton Pattern),設計這樣模式的類別

單例模式是設計模式(Design Patterns)的其中一種,在這裡,我們將透過底下程式碼,展示如何實作出單例模式的類別,並且檢測這樣的設計是否正確可以運行。


了解更多關於 [C# 程式設計手冊 




展示專案原始碼

    /// <summary>
    /// 單例模式 (Singleton Pattern) 的類別設計展示
    /// </summary>
    public class Singleton
    {
        /// <summary>
        /// 這個欄位為私有,用來持有唯一的執行實例(Instance)
        /// </summary>
        private static Singleton instance;

        public DateTime GenerateTime { get; set; }

        /// <summary>
        /// 建構式,可以在這裡做是到的實例物件的初始化動作
        /// 不過,這樣預設建構式的存取修飾詞為 private,也就是,讓何人都無法使用 new 運算子產生這個類別的物件,只有他自己本身可以
        /// </summary>
        private Singleton()
        {
            GenerateTime = DateTime.Now;
        }

        /// <summary>
        /// 取得這個類別的單例物件 (Singleton Instance)的屬性設計說明
        /// 這個屬性是唯讀,也就是只有讀取功能,無法做任何設定(因為沒有 set 屬性存取子)
        /// 當然,您也可以使用一個靜態方法,取得這個類別的單例物件
        /// </summary>
        public static Singleton Instance
        {
            get
            {
                // 若 instance 並沒有持有一個單例物件,則需要在這個時候,進行產生出來
                // ?? 若這個 單例物件 需要能夠在多執行緒環境下正確執行,又該如何設計呢?
                if (instance == null)
                {
                    instance = new Singleton();
                }
                return instance;
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("請稍後四秒鐘,正在產生四個單例物件中...");

            // 取得這個類別的單例物件
            Singleton singleton1 = Singleton.Instance;
            Thread.Sleep(1000);
            Singleton singleton2 = Singleton.Instance;
            Thread.Sleep(1000);
            Singleton singleton3 = Singleton.Instance;
            Thread.Sleep(1000);
            Singleton singleton4 = Singleton.Instance;
            Thread.Sleep(1000);

            Console.WriteLine($"單例物件1 HashCode: {singleton1.GetHashCode()} {singleton1.GenerateTime} 的產生時間識別");
            Console.WriteLine($"單例物件2 HashCode: {singleton2.GetHashCode()} {singleton2.GenerateTime} 的產生時間識別");
            Console.WriteLine($"單例物件3 HashCode: {singleton3.GetHashCode()} {singleton3.GenerateTime} 的產生時間識別");
            Console.WriteLine($"單例物件4 HashCode: {singleton4.GetHashCode()} {singleton4.GenerateTime} 的產生時間識別");

            if(singleton1 == singleton2 )
            {
                if (singleton2 == singleton3)
                {
                    if (singleton3 == singleton4)
                    {
                        Console.WriteLine($"這四個物件變數,指向同一個物件記憶體位置的單例物件");
                    }
                }
            }

            Console.WriteLine("Press any key for continuing...");
            Console.ReadKey();
        }
    }

單例模式設計說明

在這裡我們說明這個單例模式的類別該如何設計:
  • 私有 Private 建構式
    在單例模式中,使用這將無法使用 new 運算子來產生這個類別的物件,我們必須將這樣的需求給他禁止,因此,我們在預設建構函式的存取修飾詞(Access Modifier)設定成為 private
        private Singleton()
        {
            GenerateTime = DateTime.Now;
        }
  • 使用屬性來取得唯一的執行實體(Instance)
    接著,我們建立一個 Instance 屬性與 instance 私有欄位,使用者僅能夠透過這個 Instance 屬性來取得這個類別的執行實體,而且,這個屬性屬於唯讀,也就是說,使用者沒有任何權力來由外部設定這個 instance 私有欄位的內容。
    在這個 get屬性存取子中,我們會先判斷私有欄位 instance 這個物件欄位變數是否有任何物件存在,若該物件欄位變數為空值 (null),我們將會使用 instance = new Singleton(); 這樣的敘述,產生這個單例物件。在這裡,我們看到了我們使用了 new 運算子呼叫了預設建構函式,我們可以這樣做,這是因為,我們是在這個類別裡面的 get屬性存取來執行這樣的敘述,這是可以的,因為,預設建構函式設定其存取子修飾詞為 private,是限制不允許從類別以外的地方來呼叫這個建構函式,但是,卻必須可以在該類別裡面來呼叫這個私有的建構函式。
        public static Singleton Instance
        {
            get
            {
                // 若 instance 並沒有持有一個單例物件,則需要在這個時候,進行產生出來
                // ?? 若這個 單例物件 需要能夠在多執行緒環境下正確執行,又該如何設計呢?
                if (instance == null)
                {
                    instance = new Singleton();
                }
                return instance;
            }
        }

底下則是這個展示範例專案的執行結果,我們可以看的出來,透過 Singleton.Instance 所取得的物件,都是同一個。
請稍後四秒鐘,正在產生四個單例物件中...
單例物件1 HashCode: 46104728 2017/9/3 下午 10:17:24 的產生時間識別
單例物件2 HashCode: 46104728 2017/9/3 下午 10:17:24 的產生時間識別
單例物件3 HashCode: 46104728 2017/9/3 下午 10:17:24 的產生時間識別
單例物件4 HashCode: 46104728 2017/9/3 下午 10:17:24 的產生時間識別
這四個物件變數,指向同一個物件記憶體位置的單例物件
Press any key for continuing...

了解更多關於 [C# 程式設計手冊 





2017年9月2日 星期六

.NET C# 使用類別建構式與解構式,來觀察 GC 記憶體回收運作機制

當我們在 .NET 環境下進行程式開發,並且想要建立一個類別 Class 的物件 Object(或稱為執行個體 Instance)的時候,可以使用 new 運算子,並且緊接著該類別的建構式函式,就可以取得該物件;當然,這個時候我們會使用物件變數來取得並且儲存參考到該物件位置。
可是,當我們不再需要使用到該物件的時候,只要該物件沒有被任何屬性、物件變數參考到,這個時候,記憶體回收機制 Garbage Collection 就會自動地把這些沒有人認領的記憶體進行回收,開發人員是不需要做任何額外的處理與透過任何程式碼的撰寫,就可以讓您的應用程式,可以繼續使用這些已經回收的記憶體空間。
在這個筆記之中,我們將會透過一個類別 CountObject 類別,記錄下這個類別總共生成了多少個物件;在這裡,我們使用了底下的技巧:
  • 靜態的屬性
    當物件被生成的時候,會先執行該類別的建構式,因此,一旦建構式被呼叫的時候,我們在建構式內,會自動地將這個靜態屬性值加一,這樣,所們可以隨時查詢這個類別的靜態屬性,就可以知道透過該類別所產生的物件,總共有多少數量存在於記憶體中。
  • 解構式函式
    當該物件所占用的記憶體要被 .NET 系統回收,也就是該物件所占用的記憶體,將可以被其他 .NET 的物件來使用,此時,這個物件所屬的解構式函式將會被呼叫。
    我們將會透過此機運作機制特性,一旦解構式被呼叫的話,我們會在解構式中,將該靜態屬性值減一,代表該類別所生成且存在於記憶體中的物件,將會少一個。
    我們在這個物件上宣告一個欄位 ObjectName,用來記錄下這個新產生的物件名稱;另外,當物件被建立的時候,會在建構式中輸出一段訊息,說明是哪個物件被建立起來了;而當該物件要被記憶體回收的時候,會在解構式中輸出一段訊息,說明是哪個物件要被記憶體回收機制給回收了。
底下是這次所使用到的測試程式。
    public class CountObject
    {
        /// <summary>
        /// 記錄下現在記憶體中,總共存在這多少個物件
        /// </summary>
        private static int _TotalObjects;

        public static int TotalObjects
        {
            get { return _TotalObjects; }
            set { _TotalObjects = value; }
        }

        string ObjectName;

        public CountObject()
        {
            // 若有該類別的物件產生,則計數器會加一
            CountObject.TotalObjects += 1;
            Console.WriteLine($"現在總共有 {CountObject.TotalObjects} 物件");
        }

        public CountObject(string objectName) : this()
        {
            Console.WriteLine($"有新的物件要產生 {objectName}");
            ObjectName = objectName;
        }

        ~CountObject()
        {
            Console.WriteLine($"有物件要回收 {ObjectName}");
            // 若有物件被回收,則該計數器會減一
            CountObject.TotalObjects -= 1;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("現在要產生六個物件");
            new CountObject("物件1");
            new CountObject("物件2");
            new CountObject("物件3");
            new CountObject("物件4");
            var fooObject = new CountObject("物件5");
            var fooTempObject = new CountObject("物件6");
            Console.WriteLine($"Total Objects is {CountObject.TotalObjects}");
            Console.WriteLine("Press any key for continuing...");
            Console.ReadKey();

            // 進行記憶體回收工作
            Console.WriteLine("進行記憶體回收工作");
            GC.Collect(2, GCCollectionMode.Forced);

            // 將要休息三秒鐘,讓背景記憶體回收程序,可以有足夠的時間,進行記憶體回收
            Console.WriteLine("將要休息三秒鐘,讓背景記憶體回收程序,可以有足夠的時間,進行記憶體回收");
            Thread.Sleep(3000);
            Console.WriteLine($"Total Objects is {CountObject.TotalObjects}");
            Console.WriteLine("Press any key for continuing...");
            Console.ReadKey();

            Console.WriteLine("將其中一個物件變數 fooTempObject,設為空值 null,因此,該變數 fooTempObject 所指向的物件,將會被記憶體回收");
            fooTempObject = null;
            // 進行記憶體回收工作
            Console.WriteLine("進行記憶體回收工作");
            GC.Collect(2, GCCollectionMode.Forced);

            // 將要休息三秒鐘,讓背景記憶體回收程序,可以有足夠的時間,進行記憶體回收
            Console.WriteLine("將要休息三秒鐘,讓背景記憶體回收程序,可以有足夠的時間,進行記憶體回收");
            Thread.Sleep(3000);
            Console.WriteLine($"Total Objects is {CountObject.TotalObjects}");
            Console.WriteLine("Press any key for continuing...");
            Console.ReadKey();
        }
    }
底下是這段測試程式碼執行的輸出結果
現在要產生六個物件
現在總共有 1 物件
有新的物件要產生 物件1
現在總共有 2 物件
有新的物件要產生 物件2
現在總共有 3 物件
有新的物件要產生 物件3
現在總共有 4 物件
有新的物件要產生 物件4
現在總共有 5 物件
有新的物件要產生 物件5
現在總共有 6 物件
有新的物件要產生 物件6
Total Objects is 6
Press any key for continuing...
 進行記憶體回收工作
將要休息三秒鐘,讓背景記憶體回收程序,可以有足夠的時間,進行記憶體回收
有物件要回收 物件4
有物件要回收 物件3
有物件要回收 物件2
有物件要回收 物件1
Total Objects is 2
Press any key for continuing...
 將其中一個物件變數 fooTempObject,設為空值 null,因此,該變數 fooTempObject 所指向的物件,將會被記憶體回收
進行記憶體回收工作
將要休息三秒鐘,讓背景記憶體回收程序,可以有足夠的時間,進行記憶體回收
有物件要回收 物件6
Total Objects is 1
Press any key for continuing...

測試步驟說明

首先,我們使用底下程式碼產生了六個物件,我們看到前面四個敘述,是有產生四個物件,可是,並沒有任何物件變數會參考到這四個物件,因此,這四個物件也就是沒有人使用到的物件,一旦進行了記憶體回收的動作,這四個物件,將會被記憶體回收機制回收掉。
而後面兩個敘述,則是分別有兩個物件變數指向他們,因此,將不會被記憶體回收機制做回收。
不管如何,當底下程式碼執行完成之後,我們查詢到 CountObject 這個類別總共有六個物件存在於記憶體中(雖然有四個物件是沒有被任何程式碼參考到)。
            new CountObject("物件1");
            new CountObject("物件2");
            new CountObject("物件3");
            new CountObject("物件4");
            var fooObject = new CountObject("物件5");
            var fooTempObject = new CountObject("物件6");
接著,我們呼叫了 GC.Collect,強制記憶體回收機制進行記憶體回收(也就是把沒有在使用到的物件,從記憶體中釋放出來,該空間就可以提供給其他物件來使用。通常來說,我們在撰寫 .NET 程式碼的時候,非常強烈的,不建議自行呼叫記憶體回收機制來執行記憶體回收的動作,因為,.NET系統會自動決定何時用來執行記憶體回收的行為,並且,也會動態的依據當時系統運作形況,自行調整記憶體回收機制的運作行為。
GC.Collect(2, GCCollectionMode.Forced);
由於我們是要進行測試,所以,我們使用上述的方法,強制運行記憶體回收的機制,讓我們可以儘快地看到記憶體回收的結果。
不過,由於當 .NET 的記憶體回收機制發現到要回收的物件所占用的記憶體,會透過背景執行緒(Client / Server 的 .NET 程式採用不同的方式來進行回收動作),所以,我們就讓我們當前的執行緒暫停個3秒鐘,讓記憶體回收機制可以確實地把沒有被使用到的物件,進行回收;所以,我們使用了底下的程式碼:
Thread.Sleep(3000);
因此,你會看到了,當第一次記憶體回收機制執行的時候,總共有兩個物件記憶體存在 .NET 系統內。
不過,當我們想要把某個物件透過記憶體回收機制,讓他回收掉,這個時候,您將會有兩種選擇,第一個就是不去理會它,讓您的程式依照您的流程來運行,這樣當有不再繼續使用到的物件發生的時候,將會被記憶體回收掉。
第二個,就是您可以透過底下的敘述,就是把指向該物件記憶體位置的物件變數,設定成為 null,這樣,下一次記憶體回收機制開始運作的時候,該物件就會被記憶體回收機制回收掉。
fooTempObject = null;

.NET C# 類別繼承 Class Inheritance 中的建構函式 Constructor 執行順序

在這份筆記之中,我們要來檢測類別繼承的架構下,這些被繼承的建構式的執行順序。

了解更多關於 [C# 和 .NET 中的繼承的使用方式
了解更多關於 [C# 程式設計手冊 
了解更多關於 [建構函式



測設的類別宣告與測試程式

底下列出了我們這測試的三種不同建立類別的情境會用到的 C# 原始碼
namespace ConsoleApp2
{
    /// <summary>
    /// 祖父級的基礎類別
    /// </summary>
    public class BaseBaseClass
    {
        public BaseBaseClass()
        {
            Console.WriteLine("Call BaseBaseClass() Constructor");
        }
    }

    /// <summary>
    /// 父親級的基礎類別
    /// </summary>
    public class BaseClass : BaseBaseClass
    {
        public BaseClass()
        {
            Console.WriteLine("Call BaseClass() Constructor");
        }
        public BaseClass(string para)
        {
            Console.WriteLine($"Call BaseClass(\"{para}\") Constructor");
        }
    }

    /// <summary>
    /// 衍生類別
    /// </summary>
    public class DerivedClass : BaseClass
    {
        public DerivedClass()
        {
            Console.WriteLine("Call DerivedClass() Constructor");
        }
        public DerivedClass(string para)
        {
            Console.WriteLine($"Call DerivedClass(\"{para}\") Constructor");
        }
        public DerivedClass(string para, int paraInt):base(para)
        {
            Console.WriteLine($"Call DerivedClass(\"{para}\", \"{paraInt}\") Constructor");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("測試一 new DerivedClass()");
            var fooDerivedClassObject = new DerivedClass();
            Console.WriteLine("---------------");
            Console.WriteLine("測試二 new DerivedClass(some string)");
            var fooDerivedClassObjectWithArg = new DerivedClass("Create Derived Class with Argument");
            Console.WriteLine("---------------");
            Console.WriteLine("測試三 new DerivedClass(some string, some int)");
            var fooDerivedClassObjectWith2Arg = new DerivedClass("Create Derived Class with Argument", 2);
            Console.WriteLine("Press any key for continuing...");
            Console.ReadKey();
        }
    }
}

類別繼承架構與建構式設計說明

在這裡,我們定義了三個類別
  • BaseBaseClass
    這個類別是在繼承關係中,位於最上層的位置,也就是所有類別的祖先,我在這裡簡稱為 基礎基礎類別
    在這裡類別中,只有預設建構式,為了要能夠知道程式執行過程中,有呼叫到這個 基礎基礎類別 的建構式,所以,我還是把這個預設建構是寫了出來,不過,裡面只有輸出一段訊息到命令提示視窗中。
        public BaseBaseClass()
        {
            Console.WriteLine("Call BaseBaseClass() Constructor");
        }
  • BaseClass
    這個類別,繼承了 BaseBaseClass (基礎基礎類別) ,在這我稱它為 基礎類別
    在這個基礎類別中,我們定義了兩個建構式,一個是預設建構式,另外一個建構式會接收一個字串參數。會設計這兩個建構式,主要是要測試,當衍生類別沒有特別指定基礎類別要執行的建構式的時候,在基礎類別中,預設建構式會被主動的執行;若在衍生類別的建構式中,有特別指定呼叫的基礎類別的建構式,此時,該基礎類別上的指定符合函式簽章的建構式,就會被執行。
        public BaseClass()
        {
            Console.WriteLine("Call BaseClass() Constructor");
        }
        public BaseClass(string para)
        {
            Console.WriteLine($"Call BaseClass(\"{para}\") Constructor");
        }
  • DerivedClass
    這個類別,繼承了 BaseClass (基礎類別),在這裡我稱它為衍生類別
    在最後的衍生類別中,共宣告了三個種函式簽章的建構式。
        public DerivedClass()
        {
            Console.WriteLine("Call DerivedClass() Constructor");
        }
        public DerivedClass(string para)
        {
            Console.WriteLine($"Call DerivedClass(\"{para}\") Constructor");
        }
        public DerivedClass(string para, int paraInt):base(para)
        {
            Console.WriteLine($"Call DerivedClass(\"{para}\", \"{paraInt}\") Constructor");
        }

建立一個衍生類別的物件,但使用預設建構式

在這個測試中,我們使用了
            var fooDerivedClassObject = new DerivedClass();
這個敘述,建立了一個衍生類別 DerivedClass 的物件,此時,在命令提示視窗中會出現底下的訊息。
我底下的訊息中,我們可以得知,
  • 當衍生類別的物件被產生的時候,會先呼叫基礎基礎類別的建構式,因為,沒有特別指定要執行哪個基礎基礎類別的建構式,所以,基礎基礎類別的預設建構式 (BaseBaseClass())會先被執行。
  • 接著,將會呼叫基礎類別的建構式,因為,沒有特別指定要執行哪個基礎類別的建構式,所以,基礎類別的預設建構式 (BaseClass()) 會先被執行。
  • 最後,才是衍生類別的建構式 (DerivedClass()) 會被執行

執行結果

測試一 new DerivedClass()
Call BaseBaseClass() Constructor
Call BaseClass() Constructor
Call DerivedClass() Constructor

建立一個衍生類別的物件,但使用有字串參數的建構式

在這個測試中,我們使用了,也就是說,當我們要建立一個衍生類別物件的時候,順便將一個字串傳遞到建構式內,當然,我們在這個衍生類別中,就需要宣告多的建構式,並且使用不同的函式簽章,透過多載 (Overloading) 的技術,可以讓使用者選擇不同的建構式函式來建立該類別的物件。
var fooDerivedClassObjectWithArg = new DerivedClass("Create Derived Class with Argument");
在這個建構式 DerivedClass(string para) 中,我僅在呼叫衍生類別的建構式時候,輸出不同的文字內容,而基礎類別與基礎基礎類別,則會延續上個說明程序來執行。
        public DerivedClass(string para)
        {
            Console.WriteLine($"Call DerivedClass(\"{para}\") Constructor");
        }

執行結果

測試二 new DerivedClass(some string)
Call BaseBaseClass() Constructor
Call BaseClass() Constructor
Call DerivedClass("Create Derived Class with Argument") Constructor

建立一個衍生類別的物件,指定基礎類別要執行建構式函式

在這個測試中,我們將會在衍生類別的建構適中,指定要執行的基礎類別建構式,底下是我們要建立衍生類別物件的執行程式,在這裡,我們呼叫的衍生類別建構式,需要傳遞一個字串與整數進去。
            var fooDerivedClassObjectWith2Arg = new DerivedClass("Create Derived Class with Argument", 2);
在這個衍生類別建構式 DerivedClass(string para, int paraInt) 中,您可以看到,我們有使用了 :base(para) 敘述,指名要執行基礎類別的這個有接收字串參數的建構式。
因此,您可以從下方看到繼承關係的各類別之建構式的執行情況。
        public DerivedClass(string para, int paraInt):base(para)
        {
            Console.WriteLine($"Call DerivedClass(\"{para}\", \"{paraInt}\") Constructor");
        }


執行結果

測試三 new DerivedClass(some string, some int)
Call BaseBaseClass() Constructor
Call BaseClass("Create Derived Class with Argument") Constructor
Call DerivedClass("Create Derived Class with Argument", "2") Constructor


了解更多關於 [C# 和 .NET 中的繼承的使用方式
了解更多關於 [C# 程式設計手冊 
了解更多關於 [建構函式