2017年9月7日 星期四

.NET C# 類別 class 的繼承 Inheritance 與方法的覆寫 override 和覆蓋 new 的用法

在我們使用物件來設計不同實體的狀態與行為的時候,有個最為重要的 OOP 特性,那就是繼承 Inheritance;透過了繼承,我們可以將基底類別 Base Class的成員,全部都繼承到衍生類別內 Derived Class,也就是說,我們可以直接使用在基底類別中所宣告與定義的各類成員。


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



如下面的程式碼,是我們這次要練習的兩個主要類別
  • BaseClass
    這個類別是我們的基底類別
  • DerivedClass
    他繼承了 BaseClass 類別,因此,在 DerivedClass 內,是可以直接使用這個成員屬性 MyProperty。
        class BaseClass
        {
            public string MyProperty { get; set; }
        }
        class DerivedClass : BaseClass
        {
        }
    
不過,很多時候,當我們從基底類別繼承該類別到我們新建立的衍生類別時,有些行為我們需要在衍生類別內進行重新設計與定義,這樣才能夠滿足我們的需求;面對這樣的需求,您將會有兩個選擇,一個就是使用 OOP 的其中一個特色,多型 Polymorphism,這個時候,你需要在衍生類別中同樣函式簽章的前面,加入關鍵字 override ,就可以做到這樣的功能,不過,您也需要在基礎類別的同名同簽章的函式前面,加入 virtual 關鍵字。
另外一種選擇,就是直接在衍生類別類的函式前面,直接加入關鍵字 new,表示在衍生類別內,這個函式將會被覆蓋成為全新方法。
因此,為了要在同一個 C# 原始檔案中,使用同樣的基底與衍生類別名稱,並且也要能夠表達出 覆載 override與複寫 new 的差異,所以,在這裡我們使用了命名空間 namespace來做出這樣的區隔效果。

繼承類別的定義

在這裡,我們使用了三個命名空間,分別定義出同樣名稱的基底與衍生類別與繼承關係,但是,使用了覆載與副寫和沒有這兩用法的類別,分別做了同樣的程式碼呼叫,讓我們看看會出現甚麼結果。
  • 沒有虛擬的類別關係
    在這個命名空間內,這兩個類別類,並沒有同樣名稱與同樣函式簽章的定義。
  • 有虛擬的類別關係
    在這個命名空間內,基底類別的方法 MyMethod1() 有標示著虛擬 Virtual 關鍵字,並且在衍生類別中,也有同樣名稱與函式簽章的 MyMethod1(),不過,在該函式前面有使用了 override 關鍵字,這表示系統會在執行時期 runtime,動態的根據物件變數的實際型別,動態決定要呼叫哪個 MyMethod1() 函式。
  • 有覆蓋的類別關係
    在這個命名空間內,分別在基礎類別與衍生類別內,都有同樣名稱與函式簽章的函式,不過,在這裡,衍生類別裡的 MyMethod1() 這個函式,使用了 new 關鍵字,表示在衍生類別裡面的這個函式,是個全新的函式。
    namespace 沒有虛擬的類別關係
    {
        class BaseClass
        {
            public string MyProperty { get; set; }
            public void MyMethod1()
            {
                Console.WriteLine($"呼叫 BaseClass.MyMethod1() 方法");
            }
        }
        class DerivedClass : BaseClass
        {
            public void MyMethod2()
            {
                Console.WriteLine($"呼叫 DerivedClass.MyMethod2() 方法");
            }
        }
    }

    namespace 有虛擬的類別關係
    {
        class BaseClass
        {
            public string MyProperty { get; set; }
            public virtual void MyMethod1()
            {
                Console.WriteLine($"呼叫 BaseClass.MyMethod1() 方法");
            }
        }
        class DerivedClass : BaseClass
        {
            public override void MyMethod1()
            {
                Console.WriteLine($"呼叫 DerivedClass.MyMethod1() 方法");
            }
            public void MyMethod2()
            {
                Console.WriteLine($"呼叫 DerivedClass.MyMethod2() 方法");
            }
        }
    }

    namespace 有覆蓋的類別關係
    {
        class BaseClass
        {
            public string MyProperty { get; set; }
            public void MyMethod1()
            {
                Console.WriteLine($"呼叫 BaseClass.MyMethod1() 方法");
            }
        }
        class DerivedClass : BaseClass
        {
            public new void MyMethod1()
            {
                Console.WriteLine($"呼叫 DerivedClass.MyMethod1() 方法");
            }
            public void MyMethod2()
            {
                Console.WriteLine($"呼叫 DerivedClass.MyMethod2() 方法");
            }
        }
    }

開始進行測試

首先,我們要先進行沒有同樣函式名稱與函式簽章的繼承關係類別的使用,在這裡,我們會分別產生三個物件,為了要能夠區分這個測試程式中有同樣的名稱的類別,所以,我們將會在這些基底類別與衍生類別的前面,分別加入了特定的命名空間 namespace。
  • BaseClassBaseObject
    這個物件將是屬於基底類別的型別,不過,實際建立的物件將會是基底類別的物件
  • DerivedClassDerivedObject
    這個物件將是屬於衍生類別的型別,不過,實際建立的物件將會是衍生類別的物件
  • BaseClassDerivedObject
    這個物件將是屬於基底類別的型別,不過,實際建立的物件將會是衍生類別的物件
        private static void 測試_沒有虛擬的類別關係()
        {
            沒有虛擬的類別關係.BaseClass BaseClassBaseObject = new 沒有虛擬的類別關係.BaseClass();
            沒有虛擬的類別關係.DerivedClass DerivedClassDerivedObject = new 沒有虛擬的類別關係.DerivedClass();
            沒有虛擬的類別關係.BaseClass BaseClassDerivedObject = new 沒有虛擬的類別關係.DerivedClass();

            Console.WriteLine("物件變數型別:BaseClass 實體物件型別:BaseObject , 要來呼叫 MyMethod1();");
            BaseClassBaseObject.MyMethod1();
            Console.WriteLine("物件變數型別:DerivedClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod1();");
            DerivedClassDerivedObject.MyMethod1();
            Console.WriteLine("物件變數型別:DerivedClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod2();");
            DerivedClassDerivedObject.MyMethod2();
            Console.WriteLine("物件變數型別:BaseClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod1();");
            BaseClassDerivedObject.MyMethod1();
            // 為什麼底下這行無法執行呢?
            // BaseClassDerivedObject 這個物件看不到 MyMethod2() 方法
            //BaseClassDerivedObject.MyMethod2();
        }
接著,我們將會實際執行這三個實體物件的 MyMethod1、MyMethod2 兩個方法,我們可以從執行結果中看到我們所預期的執行結果。也就是所執行的方法,就是該實體物件內所擁有定義的方法,而在 DerivedClassDerivedObject.MyMethod1(); 與 BaseClassDerivedObject.MyMethod1(); 這些敘述中,其當時的物件實際上是衍生類別產生的執行個體 Instance (不管當時該物件變數所宣告的型別是基底類別還是衍生類別),不過,因為,在這個衍生類別中,並沒有宣告 MyMethod1,因此,此時將會使用從基底類別中繼承來的 MyMethod1,故,您看到所執行的 MyMethod1方法,將是定義在基底類別中的 MyMethod1。
這裡使用的是繼承的觀念
物件變數型別:BaseClass 實體物件型別:BaseObject , 要來呼叫 MyMethod1();
呼叫 BaseClass.MyMethod1() 方法
物件變數型別:DerivedClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod1();
呼叫 BaseClass.MyMethod1() 方法
物件變數型別:DerivedClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod2();
呼叫 DerivedClass.MyMethod2() 方法
物件變數型別:BaseClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod1();
呼叫 BaseClass.MyMethod1() 方法
Press any key for continuing...
第二個測試程式碼,將會使用命名空間 有虛擬的類別關係 裡面所定義的類別,測試程式碼如下:
        private static void 測試_有虛擬的類別關係()
        {
            有虛擬的類別關係.BaseClass BaseClassBaseObject = new 有虛擬的類別關係.BaseClass();
            有虛擬的類別關係.DerivedClass DerivedClassDerivedObject = new 有虛擬的類別關係.DerivedClass();
            有虛擬的類別關係.BaseClass BaseClassDerivedObject = new 有虛擬的類別關係.DerivedClass();

            Console.WriteLine("物件變數型別:BaseClass 實體物件型別:BaseObject , 要來呼叫 MyMethod1();");
            BaseClassBaseObject.MyMethod1();
            Console.WriteLine("物件變數型別:DerivedClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod1();");
            DerivedClassDerivedObject.MyMethod1();
            Console.WriteLine("物件變數型別:DerivedClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod2();");
            DerivedClassDerivedObject.MyMethod2();
            Console.WriteLine("物件變數型別:BaseClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod1();");
            BaseClassDerivedObject.MyMethod1();
        }
在這個測試中,基底類別中有定義了虛擬 virtual MyMethod1, 而衍生類別中,使用了 override MyMethod1 來覆寫了這個函式的定義。
所以,當這個敘述 DerivedClassDerivedObject.MyMethod1();,這個物件變數實際上是由衍生類別所產生的執行個體(此時,該物件變數的型別也是衍生類別),所以,在執行時期,會自動決定要使用衍生類別內複寫的 MyMethod1() 方法。
而這個敘述 BaseClassDerivedObject.MyMethod1();,這個物件變數實際上是由衍生類別所產生的執行個體(此時,該物件變數的型別卻是基底類別),所以,在執行時期,會自動決定要使用衍生類別內複寫的 MyMethod1() 方法(因為有使用了 virtual 與 override 機制,並不會因為當時物件變數宣告的型別為基底類別,就會去執行基底類別的方法)。
這裡使用了 OOP 的多型 Polymorphism 的觀念
物件變數型別:BaseClass 實體物件型別:BaseObject , 要來呼叫 MyMethod1();
呼叫 BaseClass.MyMethod1() 方法
物件變數型別:DerivedClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod1();
呼叫 DerivedClass.MyMethod1() 方法
物件變數型別:DerivedClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod2();
呼叫 DerivedClass.MyMethod2() 方法
物件變數型別:BaseClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod1();
呼叫 DerivedClass.MyMethod1() 方法
Press any key for continuing...
第三個測試程式碼,將會使用命名空間 有覆蓋的類別關係 裡面所定義的類別,測試程式碼如下:
        private static void 有覆蓋的類別關係()
        {
            有虛擬的類別關係.BaseClass BaseClassBaseObject = new 有虛擬的類別關係.BaseClass();
            有虛擬的類別關係.DerivedClass DerivedClassDerivedObject = new 有虛擬的類別關係.DerivedClass();
            有虛擬的類別關係.BaseClass BaseClassDerivedObject = new 有虛擬的類別關係.DerivedClass();

            Console.WriteLine("物件變數型別:BaseClass 實體物件型別:BaseObject , 要來呼叫 MyMethod1();");
            BaseClassBaseObject.MyMethod1();
            Console.WriteLine("物件變數型別:DerivedClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod1();");
            DerivedClassDerivedObject.MyMethod1();
            Console.WriteLine("物件變數型別:DerivedClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod2();");
            DerivedClassDerivedObject.MyMethod2();
            Console.WriteLine("物件變數型別:BaseClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod1();");
            BaseClassDerivedObject.MyMethod1();
        }
在這個測試中,基底類別中有定義了 MyMethod1, 而衍生類別中,使用了 new 關鍵字,重新宣告了一個全新屬於衍生類別的 MyMethod1。
所以,當這個敘述 DerivedClassDerivedObject.MyMethod1();,這個物件變數實際上是由衍生類別所產生的執行個體(此時,該物件變數的型別也是衍生類別),所以,理所當然的,將會執行衍生類別所定義的 MyMethod1 這個方法。

而這個敘述 BaseClassDerivedObject.MyMethod1();,這個物件變數實際上是由衍生類別所產生的執行個體(此時,該物件變數的型別卻是基底類別),因為在衍生類別中使用了 new 來重新宣告了一個全新的 MyMethod1 方法,所以,會根據當時的物件型別,直接使用了衍生類別中所定義的 MyMethod1 方法。
物件變數型別:BaseClass 實體物件型別:BaseObject , 要來呼叫 MyMethod1();
呼叫 BaseClass.MyMethod1() 方法
物件變數型別:DerivedClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod1();
呼叫 DerivedClass.MyMethod1() 方法
物件變數型別:DerivedClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod2();
呼叫 DerivedClass.MyMethod2() 方法
物件變數型別:BaseClass 實體物件型別:DerivedObject , 要來呼叫 MyMethod1();
呼叫 DerivedClass.MyMethod1() 方法
Press any key for continuing...

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



2017年9月6日 星期三

.NET C# 結構 struct 的宣告與封裝和使用練習

在 C# 中,結構是使用 structClass 關鍵字來進行定義,這個型別屬於實值型別,有別於類別的參考型別(您需要能夠區分這兩種 實值型別 Value Type 與 參考型別 Reference Type的差異)。

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

我們在這裡會進行結構的成員宣告與定義,其中最常用的就是 欄位屬性建構函式方法 的宣告。
在這個結構筆記中,我們定義了底下的結構
在這個結構中,我們定義了一個完整屬性 X 的宣告,你可以使用程式碼片段 propfull 來快速產生這個定義程式碼,這裡表示了屬性 X 的實際儲存內容,將會儲存在欄位 _X 內,當您要進行讀取/寫入動作的時候,是透過了 get存取子和set存取子 這兩個方法來間接存取欄位 _X。
另外,我們使用程式碼片段 prop 這個程式碼片段,宣告了一個自動建置的屬性 Y,在這樣的用法中,我們並不需要額外的定義一個欄位 _Y ,因為,編譯器會幫我們自動產生這些程式碼。使用了自動建置的屬性,大幅簡化了我們在屬性上的程式碼撰寫,也讓我們的程式碼更加的清爽,最重要的是,這兩種做法的最終效果,都是一樣的。
最後,我們定義了一個欄位 Z。
不過,與類別有點不同的地方,在要建立結構執行個體的時候,可以不需要使用 new 運算子與結構預設函式,就可以直接存取結構內的各個欄位,並且,在結構內,我們不能夠定義預設建構函式,但是,可以定義其他有參數的建構函式。
在這個結構範例程式碼中,您會看到我們並沒已宣告任何建構函式,此時,你可以直接使用預設建構函式來建立這個結構執行個體;然而,您會在底下的程式碼中看到有個有兩個參數的建構函式,若您將這個有參數的建構函式解除註解之後,您就可以在程式碼中,使用兩個引述與這個建構函式,來建立出這個結構的執行個體;不過,就在這個時候,您也無法繼續在使用預設建構函式來產生執行個體了。
最後,我們學習如何在結構中產生這個結構應該提供的行為,也就是方法。Length() 這個方法我們使用了 private 存取修飾詞,宣告這個函式僅能夠在結構內來呼叫,我們會在結構內的 Print()來呼叫,而 Print() 方法的存取修飾詞為 public,這表示,任何程式碼皆可以透過執行個體來執行這個方法。
最後,結構與類別有著很大的不同特性,您需要確實瞭解這兩個類型的相同與相異點,才能夠使用他們設計出符合您需要的型別與物件。
    public struct MyPoint
    {
        #region 座標點 (使用 region 來做到程式碼區隔)
        // 可以使用程式碼片段 propfull
        private double _X;

        public double X
        {
            get { return _X; }
            set { _X = value; }
        }

        // 可以使用程式碼片段 prop
        public double Y { get; set; }

        public double Z;
        #endregion

        // 底下的預設建構式,會產生甚麼錯誤訊息呢?
        //public MyPoint()
        //{
        //    _X = 0;
        //    Y = 0;
        //    Z = 0;
        //}

        // 底下的程式碼為何會出錯
        //public MyPoint(double x, double y)
        //{
        //    X = x;
        //    Y = y;
        //}

        /// <summary>
        /// 計算座標點長度
        /// (使用 XML 註解,提升程式碼閱讀性)
        /// 這個方法僅限類別內可以使用
        /// </summary>
        /// <returns></returns>
        private double Length()
        {
            return Math.Sqrt(X * X + Y * Y);
        }

        /// <summary>
        /// 顯示座標詳細資訊
        /// 任何程式碼都可以存取這個方法
        /// </summary>
        /// <returns></returns>
        public string Print()
        {
            return $"({X}, {Y}) 的長度是 {Length()}";
        }
    }

使用類別產生物件,進而存取類別內的資料與行為

這個程式碼 MyPoint fooObject4; 表示宣告一個物件變數 fooObject4,由於這個物件的型別是結構,因此,這個物件變數已經可以讓您直接存取該結構物件的內容。這樣的特性將會是與類別所不盡相同的。
另外,當執行底下程式碼
fooObject4 = fooObject1;
這表示了 fooObject4 會持有與 fooObject1 相同的物件值,不過,這個時候,這兩個物件變數,此時分別指向了不同的兩個物件。這樣的特性,也是與類別的用法不太一樣的。
在最後面,我們宣告了 MyPoint fooObject6; fooObject6 這個物件,接著,我們存取了其欄位 Z,您可以從 Visual Studio 中看到,這樣的行為是被允許的。
不過,當要存取屬性 X或者屬性 Y,卻會得到了
錯誤    CS0165    使用未指派的區域變數 'fooObject6'
這樣的錯誤訊息,這是因為,在這個時候,欄位的值,尚未被初始化,所以,會產生錯誤;這樣的用法也是和類別不太相同的。

最後,當我們宣告一個結構物件,有 new 運算子與建構函式或者 default,則這樣就可以得到一個已經完全初始化的結構物件,例如這樣的做法 MyPoint fooObject7 = new MyPoint() { X = 5, Y = 4 };。經過這樣獲得的結構物件,我們就可以存取該結構物件內的欄位與屬性了。
    class Program
    {
        static void Main(string[] args)
        {
            // 宣告一個 MyPoint 結構的物件變數,並且該變數持有這個所有成員的值
            MyPoint fooObject1 = new MyPoint() { X = 5, Y = 4 };

            // 宣告一個 MyPoint 類別的物件變數,並且指向新產生的 MyPoint 物件
            //MyPoint fooObject2 = new MyPoint(5, 4);
            // 宣告一個 MyPoint 類別的物件變數,並且指向新產生的 MyPoint 物件
            var fooObject3 = new MyPoint() { X = 5, Y = 4 };
            // 宣告一個 MyPoint 類別的物件變數,不需要使用任何 new 運算子與建構函式
            MyPoint fooObject4;
            // 將物件變數的參考值指向另外一個 MyPoint 物件
            fooObject4 = fooObject1;
            // 宣告一個 MyPoint 類別的物件變數,使用 Default
            MyPoint fooObject5 = default(MyPoint);

            // 在這裡,你可以呼叫 Length() 方法嗎?
            Console.WriteLine($"X:{fooObject1.X}, Y:{fooObject1.Y} / 更多詳細資訊 {fooObject1.Print()}");
            Console.WriteLine($"X:{fooObject3.X}, Y:{fooObject3.Y} / 更多詳細資訊 {fooObject3.Print()}");
            Console.WriteLine($"X:{fooObject4.X}, Y:{fooObject4.Y} / 更多詳細資訊 {fooObject4.Print()}");
            Console.WriteLine("Press any key for continuing...");
            Console.ReadKey();

            Console.WriteLine("修改物件的內容");
            fooObject1.X = 7.0;
            fooObject3.X = 8.0;
            fooObject4.X = 9.0;
            Console.WriteLine($"X:{fooObject1.X}, Y:{fooObject1.Y} / 更多詳細資訊 {fooObject1.Print()}");
            Console.WriteLine($"X:{fooObject3.X}, Y:{fooObject3.Y} / 更多詳細資訊 {fooObject3.Print()}");
            Console.WriteLine($"X:{fooObject4.X}, Y:{fooObject4.Y} / 更多詳細資訊 {fooObject4.Print()}");
            Console.WriteLine("Press any key for continuing...");
            Console.ReadKey();

            fooObject5.X = 10;
            fooObject5.Y = 20;
            Console.WriteLine($"X:{fooObject5.X}, Y:{fooObject5.Y} / 更多詳細資訊 {fooObject5.Print()}");
            Console.WriteLine("Press any key for continuing...");
            Console.ReadKey();

            MyPoint fooObject7 = new MyPoint() { X = 5, Y = 4 };
            fooObject7.Z = 16;
            fooObject7.X = 16;
            fooObject7.Y = 20;

            MyPoint fooObject6;
            // 這裡為什麼可以設定成員 欄位的值
            fooObject6.Z = 16;
            // 這裡為什麼無法設定成員 屬性的值
            fooObject6.X = 16;
            // 若把上一行註解,這裡為什麼無法設定成員 屬性的值
            fooObject6.Y = 20;
            Console.WriteLine("Press any key for continuing...");
            Console.ReadKey();
        }
    }


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






.NET C# 類別 class 的宣告與封裝和使用練習

在 C# 中,類別是使用 Class 關鍵字來進行定義。

了解更多關於 [類別
了解更多關於 [類別與物件
了解更多關於 [C# 程式設計手冊 

我們在這裡會進行類別的成員宣告與定義,其中最常用的就是 欄位屬性建構函式方法 的宣告。
在這個類別筆記中,我們定義了底下的類別
在這個類別中,我們定義了一個完整屬性 X 的宣告,你可以使用程式碼片段 propfull 來快速產生這個定義程式碼,這裡表示了屬性 X 的實際儲存內容,將會儲存在欄位 _X 內,當您要進行讀取/寫入動作的時候,是透過了 get存取子和set存取子 這兩個方法來間接存取欄位 _X。
另外,我們使用程式碼片段 prop 這個程式碼片段,宣告了一個自動建置的屬性 Y,在這樣的用法中,我們並不需要額外的定義一個欄位 _Y ,因為,編譯器會幫我們自動產生這些程式碼。使用了自動建置的屬性,大幅簡化了我們在屬性上的程式碼撰寫,也讓我們的程式碼更加的清爽,最重要的是,這兩種做法的最終效果,都是一樣的。
在這個類別中,我們定義了一個建構函式,這個建構函式需要提供兩個參數。若我們在類別中沒有定義任何建構函式,編譯器會自動幫我們產生一個預設建構函式(也就是沒有任何參數)。不過,若我們在類別中有建立了任何建構函式,則,當我們要使用預設建構函式的時候,我們是需要自行定義出這個預設建構函式,否則,我們是無法使用預設建構函式的。
建構函式是我們建立一個類別的執行個體,也就是物件,必須要呼叫的;我們透過了建構函式,可以在其裡面針對這個類別的各個資料進行初始化的動作,設定這些資料的預設值。
最後,我們學習如何在類別中產生這個類別應該提供的行為,也就是方法。Length() 這個方法我們使用了 private 存取修飾詞,宣告這個函式僅能夠在類別內來呼叫,我們會在類別內的 Print()來呼叫,而 Print() 方法的存取修飾詞為 public,這表示,任何程式碼皆可以透過執行個體來執行這個方法。
    public class MyPoint
    {
        #region 座標點 (使用 region 來做到程式碼區隔)
        // 可以使用程式碼片段 propfull
        private double _X;

        public double X
        {
            get { return _X; }
            set { _X = value; }
        }

        // 可以使用程式碼片段 prop
        public double Y { get; set; }
        #endregion

        // 可以使用程式碼片段 ctor
        public MyPoint(double x, double y)
        {
            X = x;
            Y = y;
        }

        /// <summary>
        /// 計算座標點長度
        /// (使用 XML 註解,提升程式碼閱讀性)
        /// 這個方法僅限類別內可以使用
        /// </summary>
        /// <returns></returns>
        private double Length()
        {
            return Math.Sqrt(X * X + Y * Y);
        }

        /// <summary>
        /// 顯示座標詳細資訊
        /// 任何程式碼都可以存取這個方法
        /// </summary>
        /// <returns></returns>
        public string Print()
        {
            return $"({X}, {Y}) 的長度是 {Length()}";
        }
    }

使用類別產生物件,進而存取類別內的資料與行為

首先,您無法使用這個程式碼 MyPoint fooObject1 = new MyPoint(); 來產生一個類別 MyPoint 的執行個體。
您只能夠透過建構函式 MyPoint(double x, double y) 與 new 運算子來建立一個物件。
這個程式碼 MyPoint fooObject4; 表示宣告一個物件變數 fooObject4;而這個物件變數並沒有指向任何參考物件,因此,這個變數物件的參考值為 null,也就是沒有任何內容。
在底下的測試程式碼中,最為重要的觀念就是,
fooObject4 = fooObject2;
這表示了 fooObject4 與 fooObject2; 這兩個物件同時指向了一個相同的物件,而不是產生一個新的物件執行個體,並且讓 fooObject4 物件變數指向這個物件。


不過,若 MyPoint 若是一個結構,則剛剛的設定行為,就會產生另外一個意義。
        static void Main(string[] args)
        {
            // 宣告一個 MyPoint 類別的物件變數,並且指向新產生的 MyPoint 物件
            //MyPoint fooObject1 = new MyPoint();  // 這樣為什麼會發生錯誤

            // 宣告一個 MyPoint 類別的物件變數,並且指向新產生的 MyPoint 物件
            MyPoint fooObject2 = new MyPoint(5, 4);
            // 宣告一個 MyPoint 類別的物件變數,並且指向新產生的 MyPoint 物件
            var fooObject3 = new MyPoint(5, 4);
            // 宣告一個 MyPoint 類別的物件變數
            MyPoint fooObject4;
            // 將物件變數的參考值指向另外一個 MyPoint 物件
            fooObject4 = fooObject2;

            // 在這裡,你可以呼叫 Length() 方法嗎?
            Console.WriteLine($"X:{fooObject2.X}, Y:{fooObject2.Y} / 更多詳細資訊 {fooObject2.Print()}");
            Console.WriteLine($"X:{fooObject3.X}, Y:{fooObject3.Y} / 更多詳細資訊 {fooObject3.Print()}");
            Console.WriteLine($"X:{fooObject4.X}, Y:{fooObject4.Y} / 更多詳細資訊 {fooObject4.Print()}");
            Console.WriteLine("Press any key for continuing...");
            Console.ReadKey();

            Console.WriteLine("修改物件的內容");
            fooObject2.X = 7.0;
            fooObject3.X = 8.0;
            fooObject4.X = 9.0;
            Console.WriteLine($"X:{fooObject2.X}, Y:{fooObject2.Y} / 更多詳細資訊 {fooObject2.Print()}");
            Console.WriteLine($"X:{fooObject3.X}, Y:{fooObject3.Y} / 更多詳細資訊 {fooObject3.Print()}");
            Console.WriteLine($"X:{fooObject4.X}, Y:{fooObject4.Y} / 更多詳細資訊 {fooObject4.Print()}");
            Console.WriteLine("Press any key for continuing...");
            Console.ReadKey();
        }


了解更多關於 [類別
了解更多關於 [類別與物件
了解更多關於 [C# 程式設計手冊 



2017年9月4日 星期一

.NET C# 物件參考與弱式參考的比較

在 C# 程式設計中,只要物件又被任何一個或以上的物件變數參考到的時候,並且記憶體回收機制被觸發執行的時候,這些物件是不被回收的。
若此時,我們採用了弱勢參考方式,則這些物件就會被記憶體機制回收。
底下為這次的測試專案原始碼:

範例原始碼

    public class MyClass
    {
        public string Name { get; set; }
        public MyClass(string name)
        {
            Name = name;
            Console.WriteLine($"----------> MyClass類別產生一個物件 : {Name}");
        }
        ~MyClass()
        {
            Console.WriteLine($"==========> MyClass類別的一個物件被記憶體回收了 : {Name}");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // 正常參考一個物件,只要該參考持續存在,該物件就不會被記憶體回收
            MyClass strongReference = new MyClass("Strong Reference Object") ;
            // 表示弱式參考,即在參考物件的同時,仍允許系統透過記憶體回收來回收該物件。
            WeakReference weakReference = new WeakReference(new MyClass("Weak Reference Object"));

            Console.WriteLine($"正常使用的參考物件為 {strongReference.Name}");
            Console.WriteLine($"弱式參考物件是否還存在 {weakReference.IsAlive}");
            Console.WriteLine($"弱式參考物件為 {(weakReference.Target as MyClass).Name}");
            Console.WriteLine("Press any key for continuing...");
            Console.ReadKey();

            Console.WriteLine("強制進行記憶體回收,等候三秒鐘...");
            GC.Collect(2);
            Thread.Sleep(3000);


            Console.WriteLine($"正常使用的參考物件為 {strongReference.Name}");
            Console.WriteLine($"弱式參考物件是否還存在 {weakReference.IsAlive}");
            Console.WriteLine("Press any key for continuing...");
            Console.ReadKey();

            ReAccessWeakReference(strongReference, weakReference);

            Console.WriteLine($"正常使用的參考物件為 {strongReference.Name}");
            Console.WriteLine($"弱式參考物件是否還存在 {weakReference.IsAlive}");
            Console.WriteLine($"弱式參考物件為 {(weakReference.Target as MyClass).Name}");
            Console.WriteLine("Press any key for continuing...");
            Console.ReadKey();
        }

        static void ReAccessWeakReference(MyClass strongReference, WeakReference weakReference)
        {
            if (weakReference.Target == null)
            {
                Console.WriteLine("因為弱式參考的物件不存在了,所以重新取得該弱式參考物件...");
                weakReference.Target = new MyClass("再次產生的 Weak Reference Object");
            }

        }
    }

程式執行結果

----------> MyClass類別產生一個物件 : Strong Reference Object
----------> MyClass類別產生一個物件 : Weak Reference Object
正常使用的參考物件為 Strong Reference Object
弱式參考物件是否還存在 True
弱式參考物件為 Weak Reference Object
Press any key for continuing...
強制進行記憶體回收,等候三秒鐘...
==========> MyClass類別的一個物件被記憶體回收了 : Weak Reference Object
正常使用的參考物件為 Strong Reference Object
弱式參考物件是否還存在 False
Press any key for continuing...
因為弱式參考的物件不存在了,所以重新取得該弱式參考物件...
----------> MyClass類別產生一個物件 : 再次產生的 Weak Reference Object
正常使用的參考物件為 Strong Reference Object
弱式參考物件是否還存在 True
弱式參考物件為 再次產生的 Weak Reference Object
Press any key for continuing...

程式運行說明

首先,我們定義一個非常簡單的類別,在這個類別,我們建立一個有參數的建構式,用來建立該物件之用,當該物件被建立的時候,會在控制台中顯示該物件已經產生的訊息;另外,我們也建立解構函式,當該物件被記憶體回收的時候,也會在控制台中顯示該物件已經被回收的訊息。
+

在程式一開始執行的時候,我們建立兩個物件,一個是一般參考的物件,一個為弱式參考物件。
然後,我們確認這兩個物件都存在於系統中,也就是我們可以透過程式來進行存取這兩個物件。
            // 正常參考一個物件,只要該參考持續存在,該物件就不會被記憶體回收
            MyClass strongReference = new MyClass("Strong Reference Object") ;
            // 表示弱式參考,即在參考物件的同時,仍允許系統透過記憶體回收來回收該物件。
            WeakReference weakReference = new WeakReference(new MyClass("Weak Reference Object"));
接著,我們強制執行記憶體回收機制,並且要等候三秒鐘(這是要讓背景記憶體回收工作可以順利完成,因為是在背景執行緒中運行,我們並無法可以確實掌握到這樣的執行工作何時可以完成。
之後,我們確認屬性 IsAlive 是否為 false,這表示該弱式參考的物件已經不存在於系統上(也就是被回收了)。
若我們這個時候想要繼續使用這個物件,我們可以使用敘述 weakReference.Target == null 確認物件是否還存在,若沒有,我們就可以再度產生該物件,這樣,我們還是可以繼續透過弱式參考的方式,存取該物件。
最後,我們可以從控制台畫面中,看到建立物件與回收物件行為發生的時候的訊息,這樣,我們便可以得到這些測試物件是有備確實建立與回收。