2017年9月14日 星期四

C# : 處置模式 Dispose 設計模式

在這份筆記中,我們來進行 Dispose 設計模式的設計與測試。

了解更多關於 [實作 Dispose 方法
了解更多關於 [C# 程式設計手冊 

在這裡,我們宣告一個類別,並且需要使用 IDisposable 這個介面,所以,我們對於這個類別的宣告為 public class MyClass : IDisposable
接著,將游標移動到 IDisposable 上,接著滑動到電燈泡上,從彈出選單中,選擇 使用 Dispose 模式實作介面,此時,Visual Studio 會幫您自動時做出類似下列類別宣告程式碼。我們僅需要在 TODO 標示處,根據您的需要,將相關的程式碼實作上去,這樣,這個類別就具有了 處置模式 的能力了。
在這裡,我們分別在建構函式與解構函式內,輸出訊息,說明這個物件現在要被回收還是要被建立,不過,在這裡,我們還將當時受管理執行緒的 ID 也顯示出來;透過這些受管理的執行緒 ID,我們可以得知,當時的解構式是在哪個執行緒上運行的。
若解構函式是由Finalize來執行,此時,將會是在一個特殊的執行緒上執行,而不是在主執行緒上執行,因此,對於要在另外一個執行緒上執行並且修改共享的資料的時候,請務必要遵循跨執行緒的程式設計規範,否則,這個程式會有問題。
    public class MyClass : IDisposable
    {
        public string MyProperty { get; set; } = "這是屬性";
        public MyClass()
        {
            Console.WriteLine($"MyClass 類別的物件已經被建立了,執行緒為 {Thread.CurrentThread.ManagedThreadId}");
        }

        #region IDisposable Support
        private bool disposedValue = false; // 偵測多餘的呼叫

        protected virtual void Dispose(bool disposing)
        {
            Console.WriteLine($"protected virtual void Dispose Ticks={DateTime.UtcNow.Ticks}");
            Console.WriteLine($"Dispose 方法被執行了,執行緒為 {Thread.CurrentThread.ManagedThreadId}");
            if (!disposedValue)
            {
                if (disposing)
                {
                    // TODO: 處置 Managed 狀態 (Managed 物件)。
                    // 例如,可以將綁定的事件解除
                }

                // TODO: 釋放 Unmanaged 資源 (Unmanaged 物件) 並覆寫下方的完成項。
                // TODO: 將大型欄位設為 null。

                disposedValue = true;
            }
        }

        // TODO: 僅當上方的 Dispose(bool disposing) 具有會釋放 Unmanaged 資源的程式碼時,才覆寫完成項。
        ~MyClass()
        {
            // 請勿變更這個程式碼。請將清除程式碼放入上方的 Dispose(bool disposing) 中。
            Dispose(false);
            Console.WriteLine($"Destructor Ticks={DateTime.UtcNow.Ticks}");
            Console.WriteLine($"MyClass 類別的物件已經被記憶體回收程序釋放了,執行緒為 {Thread.CurrentThread.ManagedThreadId}");
        }

        // 加入這個程式碼的目的在正確實作可處置的模式。
        public void Dispose()
        {
            Console.WriteLine($"public void Dispose Ticks={DateTime.UtcNow.Ticks}");
            // 請勿變更這個程式碼。請將清除程式碼放入上方的 Dispose(bool disposing) 中。
            Dispose(true);
            // TODO: 如果上方的完成項已被覆寫,即取消下行的註解狀態。
            //GC.SuppressFinalize(this);
        }
        #endregion

    }

進行測試

首先,我們先來測試 TestMethod1 方法的程式碼。
在這裡,我們建立一個物件,接著,將持有這個物件的物件變數設定成為空值,我們就緊接著進行強制的資源回收程序。
private static void TestMethod1()
{
    Console.WriteLine($"建立 myClassObject物件");
    MyClass myClassObject = new MyClass();
    Console.WriteLine("Press any key for continuing...");
    Console.ReadKey();

    Console.WriteLine($"準備強制進行記憶體回收程序");
    Console.WriteLine($"Ticks={DateTime.UtcNow.Ticks}");
    myClassObject = null;
    GC.Collect();
    Console.WriteLine("Press any key for continuing...");
    Console.ReadKey();
}
從底下的輸出結果,我們看到了,建立物件的執行緒為 1,這表示主執行緒為 ID = 1,可是,我們看到了,執行解構函式的執行緒 ID = 2。
而且 Dispose 方法被執行了,執行緒為 2 Dispose 的方法,也不是在主執行緒上運行。
執行的輸出結果
建立 myClassObject物件
MyClass 類別的物件已經被建立了,執行緒為 1
Press any key for continuing...
準備強制進行記憶體回收程序
Ticks=636409963642699536
Press any key for continuing...
protected virtual void Dispose Ticks=636409963642729535
Dispose 方法被執行了,執行緒為 2
Destructor Ticks=636409963642729535
MyClass 類別的物件已經被記憶體回收程序釋放了,執行緒為 2
-------------------------------
Press any key for continuing...
接下來,我們來看看第二個測試方法
因為這個類別有實作 IDisposable 這個介面,因此,我們在使用這個物件的時候,使用 using 將其包起來,因此,當結束 using 區段之後,就會自動呼叫這個類別 Dispose 方法,而且,呼叫這個方法所使用到的執行緒,與建立這個物件所用到的執行緒,皆為相同,都是主執行緒。
private static void TestMethod2()
{
    MyClass myClassObject;
    Console.WriteLine($"建立 myClassObject物件");
    using (myClassObject = new MyClass())
    {
        Console.WriteLine("Press any key for continuing...");
        Console.ReadKey();
    }

    Console.WriteLine($"準備強制進行記憶體回收程序");
    Console.WriteLine($"Ticks={DateTime.UtcNow.Ticks}");
    myClassObject = null;
    GC.Collect();
    Thread.Sleep(3000);
    Console.WriteLine("Press any key for continuing...");
    Console.ReadKey();
}
不過,當要測試這個方法的時候,在 Debug 與 Release 建置模式下,所跑出來的結果輸出文字會有些不同,不過,這是因為在 Release 模式下,編譯器會進行不同程度的優化處理,不過,結果都是一樣的。
從這裡的執行結果中,Dispose 方法被執行了,執行緒為 1 Dispose 的方法,在主執行緒上運行。
在 Debug 模式下執行的輸出結果
建立 myClassObject物件
MyClass 類別的物件已經被建立了,執行緒為 1
Press any key for continuing...
public void Dispose Ticks=636409966871336607
protected virtual void Dispose Ticks=636409966871341616
Dispose 方法被執行了,執行緒為 1
準備強制進行記憶體回收程序
Ticks=636409966871341616
Press any key for continuing...
-------------------------------
Press any key for continuing...
protected virtual void Dispose Ticks=636409966941140237
Dispose 方法被執行了,執行緒為 2
Destructor Ticks=636409966941140237
MyClass 類別的物件已經被記憶體回收程序釋放了,執行緒為 2
在 Release 模式下執行的輸出結果
建立 myClassObject物件
MyClass 類別的物件已經被建立了,執行緒為 1
Press any key for continuing...
public void Dispose Ticks=636409966350954649
protected virtual void Dispose Ticks=636409966350954649
Dispose 方法被執行了,執行緒為 1
準備強制進行記憶體回收程序
Ticks=636409966350959633
protected virtual void Dispose Ticks=636409966350979684
Dispose 方法被執行了,執行緒為 2
Destructor Ticks=636409966350979684
MyClass 類別的物件已經被記憶體回收程序釋放了,執行緒為 2
Press any key for continuing...
在第三個測試方法中,我們建立完成物件之後,就直接呼叫 Dispose,讓這個方法在主執行緒上運行。
        private static void TestMethod3()
        {
            Console.WriteLine($"建立 myClassObject物件");
            MyClass myClassObject = new MyClass();
            Console.WriteLine("Press any key for continuing...");
            Console.ReadKey();

            Console.WriteLine($"myClassObject.Dispose();");
            myClassObject.Dispose();
            Console.WriteLine($"準備強制進行記憶體回收程序");
            Console.WriteLine($"Ticks={DateTime.UtcNow.Ticks}");
            Console.WriteLine("Press any key for continuing...");
            Console.ReadKey();
        }

執行的輸出結果
建立 myClassObject物件
MyClass 類別的物件已經被建立了,執行緒為 1
Press any key for continuing...
myClassObject.Dispose();
public void Dispose Ticks=636409971688771288
protected virtual void Dispose Ticks=636409971688776280
Dispose 方法被執行了,執行緒為 1
準備強制進行記憶體回收程序
Ticks=636409971688776280
Press any key for continuing...
-------------------------------
Press any key for continuing...
protected virtual void Dispose Ticks=636409971722019242
Dispose 方法被執行了,執行緒為 2
Destructor Ticks=636409971722019242
MyClass 類別的物件已經被記憶體回收程序釋放了,執行緒為 2

了解更多關於 [實作 Dispose 方法
了解更多關於 [C# 程式設計手冊 





2017年9月13日 星期三

C# : 繼承類型 Inheritance Type 的成員實值相等

在這份筆記之中,我們將會進行有類別繼承情況下的衍生類別與基底類別,該如何實作出具有類別成員的實值比較功能。
在基底類別中,我們宣告了類別 TwoDPoint,這個類別的實作說明,可以參考 類型成員 Class Member 的實值相等 Value Equality 這份文章中的解釋,不過,我們在這個基底類別的 Equals 的強型別泛型介面方法實作中,有針對所傳入的物件是否與正在進行比較的物件,其所執行時期的物件型別,進行檢查: this.GetType() != other.GetType() 也就是說,這兩個比較的型別必須是相同的(也就是必須都為 TwoPoint 這個基底類別),才能夠繼續進行成員的實值比對,若不相同的話,則就會回傳 false,表示實值比對不成立。
class TwoDPoint : IEquatable<TwoDPoint>
{
    public int X { get; set; }
    public int Y { get; set; }
    public TwoDPoint(int x, int y)
    {
        X = x;
        Y = y;
    }

    // 請手動覆寫虛擬 Object.Equals(Object)
    // 輸入 over ,空白按鍵 , 選擇 Equals(object obj)
    public override bool Equals(object obj)
    {
        // 這理由該類別的 Equals 方法來檢查兩個物件是否擁有相當的值
        return this.Equals(obj as TwoDPoint);
    }

    // 實作出 IEquatable<TwoDPoint> 的 特定類型的 Equals 方法
    // 這裡的傳入參數,已經是強型別 TwoDPoint
    public bool Equals(TwoDPoint other)
    {
        // 若傳入參數為空值 null ,則這次比較不成立
        // 這個必須要先做檢查,免得發生例外異常
        if (Object.ReferenceEquals(other, null))
        {
            return false;
        }

        // 檢查這兩個物件變數是否指向同一個參考物件
        if (Object.ReferenceEquals(this, other))
        {
            return true;
        }

        // 若這兩個物件變數的執行時期型別不相同,則比較不成立
        if (this.GetType() != other.GetType())
        {
            return false;
        }

        // 進行該類別物件的欄位實際值的比較
        // 這裡不需要做基底類別的欄位值比較,因為,這個類別的基底類別為
        // System.Object ,該比較預設使用參考比較
        // 若基底類別為客製類別,則需要再呼叫基底類別的 Equals 
        return (X == other.X) && (Y == other.Y);
    }

    // 請手動覆寫虛擬 Object.GetHashCode
    // 輸入 over ,空白按鍵 , 選擇 GetHashCode()
    public override int GetHashCode()
    {
        // return X * 0x00010000 + Y;
        return X.GetHashCode() ^ Y.GetHashCode();
    }

    // 進行相等運算子的多載定義
    // 這裡傳入的參數為強型別
    public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs)
    {
        // 確認傳入參數不為空值
        if (Object.ReferenceEquals(lhs, null))
        {
            // 確認傳入參數不為空值
            if (Object.ReferenceEquals(rhs, null))
            {
                // 這裡實作出 null == null = true.
                return true;
            }

            // 若傳入參數其中一個為空值,比較不成立
            return false;
        }
        // 執行該物件的成員值比較
        return lhs.Equals(rhs);
    }

    // 進行不相等運算子的多載定義
    public static bool operator !=(TwoDPoint lhs, TwoDPoint rhs)
    {
        return !(lhs == rhs);
    }
}
接著,當然我們要繼續宣告衍生類別 ThreeDPoint,這個衍生類別繼承了基底類別 TwoDPoint,並且也需要實作出 IEquatable 介面方法。關於要實作出兩個類別的物件成員實值相等的過程,可以參考 類型成員 Class Member 的實值相等 Value Equality
在執行個體的 Equals 方法中,我們先比對了衍生類別才擁有的資料成員,接著,繼續比對基底類別內的資料成員是否相等,若全部都相等,則這兩個衍生成員的物件成員實值比對,就會成立了。
對於衍生類別的 GetHashCode 方法,我們則是把衍生類別內的成員 HashCode與基底類別物件的 HashCode 進行 XOR 邏輯運算,就會得到這個衍生類別物件的 HashCode。
// 這裡展示如何繼承一個類別,並且要實作出相關成員相等的程式碼
class ThreeDPoint : TwoDPoint, IEquatable<ThreeDPoint>
{
    public int Z { get; set; }

    // x,y 的參數,交由基底來別來做物件值的初始化
    public ThreeDPoint(int x, int y, int z)
        : base(x, y)
    {
        this.Z = z;
    }

    // 請手動覆寫虛擬 Object.Equals(Object)
    // 輸入 over ,空白按鍵 , 選擇 Equals(object obj)
    public override bool Equals(object obj)
    {
        // 這理由該類別的 Equals 方法來檢查兩個物件是否擁有相當的值
        return this.Equals(obj as ThreeDPoint);
    }

    // 實作出 IEquatable<TwoDPoint> 的 特定類型的 Equals 方法
    // 這裡的傳入參數,已經是強型別 ThreeDPoint
    public bool Equals(ThreeDPoint p)
    {
        // 若傳入參數為空值 null ,則這次比較不成立
        // 這個必須要先做檢查,免得發生例外異常
        if (Object.ReferenceEquals(p, null))
        {
            return false;
        }

        // 檢查這兩個物件變數是否指向同一個參考物件
        if (Object.ReferenceEquals(this, p))
        {
            return true;
        }

        // 這裡僅作衍生類別擁有的成員值檢查,
        // 至於,基底類別的成員,交由基底類別來檢查成員值
        if (Z == p.Z)
        {
            // 進行該類別物件的欄位實際值的比較
            // 這裡因為基底類別為客製類別,
            // 所以,需要再呼叫基底類別的 Equals 
            return base.Equals((TwoDPoint)p);
        }
        else
        {
            return false;
        }
    }

    // 請手動覆寫虛擬 Object.GetHashCode
    // 輸入 over ,空白按鍵 , 選擇 GetHashCode()
    public override int GetHashCode()
    {
        //return (X * 0x100000) + (Y * 0x1000) + Z;
        return Z.GetHashCode() ^ base.GetHashCode();

    }

    // 進行相等運算子的多載定義
    // 這裡傳入的參數為強型別
    public static bool operator ==(ThreeDPoint lhs, ThreeDPoint rhs)
    {
        // 確認傳入參數不為空值
        if (Object.ReferenceEquals(lhs, null))
        {
            // 確認傳入參數不為空值
            if (Object.ReferenceEquals(rhs, null))
            {
                // 這裡實作出 null == null = true.
                return true;
            }

            // 若傳入參數其中一個為空值,比較不成立
            return false;
        }
        // 執行該物件的成員值比較
        return lhs.Equals(rhs);
    }

    // 進行不相等運算子的多載定義
    public static bool operator !=(ThreeDPoint lhs, ThreeDPoint rhs)
    {
        return !(lhs == rhs);
    }
}

進行測試

首先,我們進行 衍生類別的物件成員值比較,在這裡,我們建立了兩個衍生類別物件,這兩個物件的資料成員,都具有相同的實值。而 classDerivedObject2 與 classDerivedObject3 這兩個物件變數,則是指向同一個參考物件。
在這裡,這個敘述 classDerivedObject1.Equals(i) 將會使用 object.Equals() 這個方法進行比對,這是因為,這兩個物件的型別不相同,一個是衍生類別,一個是 int;當然,比對的結果,當然也就會不成立。
由於,我們有做兩個物件需要具有相同的型別檢查,因此不論是使用 衍生類別物件.Equals(基底類別物件) 或者 基底類別物件.Equals(衍生類別物件),這樣的實值比對都是不會成立的。
但若您把基底類別的 if (this.GetType() != other.GetType()){return false;} 敘述註解起來,則就會得到不正確的結果了。
Console.WriteLine("衍生類別的物件成員值比較");
ThreeDPoint classDerivedObject1 = new ThreeDPoint(3, 4, 5);
ThreeDPoint classDerivedObject2 = new ThreeDPoint(3, 4, 5);
ThreeDPoint classDerivedObject3 = classDerivedObject2;
TwoDPoint classBaseObject3 = new TwoDPoint(3,4);
int i = 5;

Console.WriteLine("classDerivedObject1.Equals(classDerivedObject2) = {0}", classDerivedObject1.Equals(classDerivedObject2));
Console.WriteLine("classDerivedObject1 == classDerivedObject2 = {0}", classDerivedObject1 == classDerivedObject2);
Console.WriteLine("classDerivedObject1.Equals(classDerivedObject3) = {0}", classDerivedObject1.Equals(classDerivedObject3));
// classDerivedObject1.Equals 有兩個多載,
// 一個是這個型別本身(這裡使用強型別參數),
// 另外一個是 object 這個類別(這裡接受 object 型別參數)
Console.WriteLine("classDerivedObject1.Equals(classBaseObject3) = {0}", classDerivedObject1.Equals(classBaseObject3));
Console.WriteLine("classBaseObject3.Equals(classDerivedObject1) = {0}", classBaseObject3.Equals(classDerivedObject1));
Console.WriteLine("Compare to some other type = {0}", classDerivedObject1.Equals(i));
Console.WriteLine("Press any key for continuing...");
Console.ReadKey();
上面測試程式碼的執行結果如下:
衍生類別的物件成員值比較
classDerivedObject1.Equals(classDerivedObject2) = True
classDerivedObject1 == classDerivedObject2 = True
classDerivedObject1.Equals(classDerivedObject3) = True
classDerivedObject1.Equals(classBaseObject3) = False
classBaseObject3.Equals(classDerivedObject1) = False
Compare to some other type = False
Press any key for continuing...
最後,我們要來進行 基底類別的物件成員值比較
Console.WriteLine("基底類別的物件成員值比較");
TwoDPoint classBaseObject1 = null;
TwoDPoint classBaseObject2 = null;
Console.WriteLine("兩個物件變數都為空值的比較 : {0}", classBaseObject1 == classBaseObject2);
// 實例化基底類別的執行個體
classBaseObject1 = new TwoDPoint(3, 4);
classBaseObject2 = new TwoDPoint(3, 4);
Console.WriteLine(Environment.NewLine);
Console.WriteLine("classDerivedObject1 is TwoDPoint : {0}", classDerivedObject1 is TwoDPoint);
Console.WriteLine("classBaseObject1 is TwoDPoint : {0}", classBaseObject1 is TwoDPoint);
Console.WriteLine($"{Environment.NewLine}TwoDPoint(3, 4) == TwoDPoint(3, 4)");
Console.WriteLine("    (classBaseObject2 == classDerivedObject1) = {0}", classBaseObject2 == classBaseObject1);
Console.WriteLine($"{Environment.NewLine}底下是基底類別物件與衍生類別物件的比較");
Console.WriteLine("TwoDPoint(3, 4) == ThreeDPoint(3, 4, 5)");
Console.WriteLine("    (classBaseObject2 == classDerivedObject1) = {0}", classBaseObject2 == classDerivedObject1);
Console.WriteLine("ThreeDPoint(3, 4, 5) == TwoDPoint(3, 4)");
Console.WriteLine("   (classDerivedObject1 == classBaseObject2) = {0}", classDerivedObject1 == classBaseObject2);
Console.WriteLine("ThreeDPoint(3, 4, 5) != TwoDPoint(3, 4)");
Console.WriteLine("   (classDerivedObject1 != classBaseObject2) = {0}", classDerivedObject1 != classBaseObject2); 
System.Console.WriteLine("Press any key to exit.");
System.Console.ReadKey();
這是執行輸出結果
基底類別的物件成員值比較
兩個物件變數都為空值的比較 : True


classDerivedObject1 is TwoDPoint : True
classBaseObject1 is TwoDPoint : True

TwoDPoint(3, 4) == TwoDPoint(3, 4)
    (classBaseObject2 == classDerivedObject1) = True

底下是基底類別物件與衍生類別物件的比較
TwoDPoint(3, 4) == ThreeDPoint(3, 4, 5)
    (classBaseObject2 == classDerivedObject1) = False
ThreeDPoint(3, 4, 5) == TwoDPoint(3, 4)
   (classDerivedObject1 == classBaseObject2) = False
ThreeDPoint(3, 4, 5) != TwoDPoint(3, 4)
   (classDerivedObject1 != classBaseObject2) = True
Press any key to exit.

C# : 類型成員 Class Member 的實值相等 Value Equality

從前面的文章介紹中,我們知道,關於非基本類型(Primitive Type)的變數,他們的 == 運算子使用的參考相等來比較兩個物件,然而,對於結構型別所產生的物件,預設是無法使用 == 運算子,只能夠使用該執行個體的 Equals 方法來進行與其他結構物件的比較。
在這裡,我們要來了解如何讓您的結構與類別型別所產生的物件,根據您的需求定義以及執行效能的相關考量,實作屬於自己的兩個結構或類別物件的相等比較功能,這包含要實作出 == 與 Equals 這兩個功能。
為了要能夠做出這樣的機制,我們需要進行程式碼的多載 (Overloading)與覆載(Overriding);當然,在 .NET Framework 環境中,已經幫我們預先準備好了簡化這些開發工作,在這裡,我們僅需要實作出 IEquatable介面。
在我們的測試的類別宣告如下程式碼所示:
  • 設定客製化類別,要使用介面 IEquatable<TwoDPoint>
    在這裡,我們使用該泛型介面,要實作出強型別的介面功能。
    因此,我們將要定義這個方法 public bool Equals(TwoDPoint other) 的實際物件比較的行為。在這裡,我們將進行該類別內的資料成員的實際值得比較,已獲得該兩個物件是否具有實值相等。
    在這個方法裏,我們使用 (Object.ReferenceEquals(other, null) 來判斷出這個傳入參數是否為空值,若為空值,當然實值比較就不會成立。
    接著,我們使用這個方法 Object.ReferenceEquals(this, other) 來進行這兩個物件變數,是否指向同一個實體變數記憶體位址,若成立的話,則表示這兩個物件變數具有實值相等。
  • 覆寫 Object.Equals(Object)
    接著,我們需要覆寫這個方法,在類別中定義出 public override bool Equals(object obj) 這個覆寫方法,這個方法執行程式碼相當的簡單,就是執行第一個步驟所實作出來的強型別的泛型介面方法:return this.Equals(obj as TwoDPoint);
  • 覆寫強型別的 == 運算子
    由於,運算子為靜態方法,要執行哪個覆寫運算子方法,會在編譯時期就需要決定出來,因此,我們在這裡需要覆寫這個類別的 == 運算子方法。
    在這裡,我們覆寫了 == 運算子方法 public static bool operator ==,在這個覆寫靜態運算子方法內,我們使用執行個體的 Equals 方法,進行這兩個物件變數的比較 lhs.Equals(rhs)
    當然,我們也是需要在這個方法內檢查是否其中一個物件變數為空值,若成立的話,則這個實值比對就不會成立;不過,在這個需求定義中,若兩個比對的物件變數都是空值 null,則會回傳 true,表示,這次比對是相同的,當然,這個部分,可以依照您當時環境的需求,自行做出調整。
  • 覆寫強型別的 != 運算子
    在這裡,我們也需要覆寫靜態的不等於運算子 public static bool operator !=,在這個強型別靜態方法內,我們會呼叫相等 == 運算子所得到的結果,再進行 ! 布林運算,就會得到不相等的實值比較結果。
  • 覆寫 object.GetHashCode() 方法
    當您有覆寫了 object.Equals() 方法的時候,記得也需要 覆寫 object.GetHashCode() 方法,在這裡,我們簡單使用這兩個物件資料成員的 HashCode 值進行 XOR 的運算,就會得到這個類別執行個體的 HashCode。
class TwoDPoint : IEquatable<TwoDPoint>
{
    public int X { get; set; }
    public int Y { get; set; }
    public TwoDPoint(int x, int y)
    {
        X = x;
        Y = y;
    }

    // 請手動覆寫虛擬 Object.Equals(Object)
    // 輸入 over ,空白按鍵 , 選擇 Equals(object obj)
    public override bool Equals(object obj)
    {
        // 這理由該類別的 Equals 方法來檢查兩個物件是否擁有相當的值
        return this.Equals(obj as TwoDPoint);
    }

    // 實作出 IEquatable<TwoDPoint> 的 特定類型的 Equals 方法
    // 這裡的傳入參數,已經是強型別
    public bool Equals(TwoDPoint other)
    {
        // 若傳入參數為空值 null ,則這次比較不成立
        // 這個必須要先做檢查,免得發生例外異常
        if (Object.ReferenceEquals(other, null))
        {
            return false;
        }

        // 檢查這兩個物件變數是否指向同一個參考物件
        if (Object.ReferenceEquals(this, other))
        {
            return true;
        }

        // 若這兩個物件變數的執行時期型別不相同,則比較不成立
        //if (this.GetType() != other.GetType())
        //{
        //    return false;
        //}

        // 進行該類別物件的欄位實際值的比較
        // 這裡不需要做基底類別的欄位值比較,因為,這個類別的基底類別為
        // System.Object ,該比較預設使用參考比較
        // 若基底類別為客製類別,則需要再呼叫基底類別的 Equals 
        return (X == other.X) && (Y == other.Y);
    }

    // 請手動覆寫虛擬 Object.GetHashCode
    // 輸入 over ,空白按鍵 , 選擇 GetHashCode()
    public override int GetHashCode()
    {
        // return X * 0x00010000 + Y;
        return X.GetHashCode() ^ Y.GetHashCode();
    }

    // 進行相等運算子的多載定義
    // 這裡傳入的參數為強型別
    public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs)
    {
        // 確認傳入參數不為空值
        if (Object.ReferenceEquals(lhs, null))
        {
            // 確認傳入參數不為空值
            if (Object.ReferenceEquals(rhs, null))
            {
                // 這裡實作出 null == null = true.
                return true;
            }

            // 若傳入參數其中一個為空值,比較不成立
            return false;
        }
        // 執行該物件的成員值比較
        return lhs.Equals(rhs);
    }

    // 進行不相等運算子的多載定義
    public static bool operator !=(TwoDPoint lhs, TwoDPoint rhs)
    {
        return !(lhs == rhs);
    }
}

進行測試

首先,我們實例化兩個執行個體,分別設定到兩個物件變數內(classObject1, classObject2),接著,設定 classObject2 與 classObject3 同時參考到同一個執行個體(物件上)。
Console.WriteLine("參考型別的物件比較");
TwoDPoint classObject1 = new TwoDPoint(10, 20);
TwoDPoint classObject2 = new TwoDPoint(10, 20);
TwoDPoint classObject3;
classObject3 = classObject2;

Console.WriteLine($"classObject1==classObject2 is {classObject1 == classObject2}");
Console.WriteLine($"classObject1.Equals(classObject2) is {classObject1.Equals(classObject2)}");
Console.WriteLine($"object.Equals(classObject1, classObject2) is {object.Equals(classObject1, classObject2)}");
Console.WriteLine($"object.ReferenceEquals(classObject1, classObject2) is {object.ReferenceEquals(classObject1, classObject2)}");
Console.WriteLine("Press any key for continuing...");
Console.ReadKey();
參考型別的物件比較,執行結果如下所示,我們看到了,不論使用了 == 運算子、執行個體覆寫的 Equals 方法、object.Equals 靜態方法與object.ReferenceEquals,都可以確實的進行兩個物件的資料成員的實值相等比較。
參考型別的物件比較
classObject1==classObject2 is True
classObject1.Equals(classObject2) is True
object.Equals(classObject1, classObject2) is True
object.ReferenceEquals(classObject1, classObject2) is False
Press any key for continuing...
接下來,我們要進行參考型別的物件比較(兩個物件變數指向同一個參考物件)
Console.WriteLine("參考型別的物件比較(兩個物件變數指向同一個參考物件)");
Console.WriteLine($"classObject3==classObject2 is {classObject3 == classObject2}");
Console.WriteLine($"classObject1.Equals(classObject2) is {classObject3.Equals(classObject2)}");
Console.WriteLine($"object.Equals(classObject1, classObject2) is {object.Equals(classObject3, classObject2)}");
Console.WriteLine($"object.ReferenceEquals(classObject1, classObject2) is {object.ReferenceEquals(classObject3, classObject2)}");
Console.WriteLine("Press any key for continuing...");
Console.ReadKey();
參考型別的物件比較(兩個物件變數指向同一個參考物件)
classObject3==classObject2 is True
classObject3.Equals(classObject2) is True
object.Equals(classObject3, classObject2) is True
object.ReferenceEquals(classObject3, classObject2) is True
Press any key for continuing...

2017年9月11日 星期一

C# / CLR : 關於集合 Collection 內的成員 Member 的記憶體回收 Memory Garbage Collection 測試

在這個筆記中,我們將進一步的訓練大家關於記憶體回收的處理相關的觀念;在這裡,我們客製一個類別,MyTargetClass,這個類別內,僅有一個屬性,那就是我們客製類別 MyListClass,這個類別繼承了 List,所以,這個類別提供了一個集合的功能。
而在這個 MyListClass 是一個集合類別,他的每個成員為 MyClass 型別所產生的執行個體,這個類別僅有一個屬性 Name,用來標示每個物件的名稱。我們會在這個類別 MyClass 的建構函式與解構函式中,輸出一段訊息,說明現在是哪個物件被生成了,哪個物件被回收了。
    public class MyClass
    {
        public string Name { get; set; }
        public MyClass(string name)
        {
            Name = name;
            Console.WriteLine($"MyClass 型別建立一個物件 {Name}");
        }
        ~MyClass()
        {
            Console.WriteLine($"MyClass 型別的物件 {Name} 要被回收了");
        }
    }

    public class MyListClass : List<MyClass>
    {
        public MyListClass() : base()
        {
            Console.WriteLine($"MyListClass 型別建立一個物件");
        }
        ~MyListClass()
        {
            Console.WriteLine($"MyListClass 型別的物件 要被回收了");
        }
    }

    public class MyTargetClass
    {
        public MyListClass MyTargetClassObject { get; set; } = new MyListClass();
        public MyTargetClass()
        {
            Console.WriteLine($"MyTargetClass 型別建立一個物件");
        }
        ~MyTargetClass()
        {
            Console.WriteLine($"MyTargetClass 型別的物件 要被回收了");
        }
    }

進行測試

我們透過底下程式碼來進行建立出我們要測試的相關物件。
  • 首先,建立一個 MyTargetClass 類別物件 fooMyTargetClass
  • 接著,在這個物件變數的屬性 MyTargetClassObject,加入三個 MyClass 類別的物件
            Console.WriteLine("建立型別 MyTargetClass 的執行個體");
            MyTargetClass fooMyTargetClass = new MyTargetClass();
            Console.WriteLine("加入型別 MyClass 的執行個體到 MyTargetClassObject 集合內");
            fooMyTargetClass.MyTargetClassObject.Add(new MyClass("集合物件1"));
            Console.WriteLine("加入型別 MyClass 的執行個體到 MyTargetClassObject 集合內");
            fooMyTargetClass.MyTargetClassObject.Add(new MyClass("集合物件2"));
            Console.WriteLine("加入型別 MyClass 的執行個體到 MyTargetClassObject 集合內");
            fooMyTargetClass.MyTargetClassObject.Add(new MyClass("集合物件3"));
            Console.WriteLine("Press any key for continuing...");
            Console.ReadKey();
你可以從底下的訊息,得知到各個類別產生的物件順序與過程。
大部分的人,應該都可以知道這樣的執行結果。
建立型別 MyTargetClass 的執行個體
MyListClass 型別建立一個物件
MyTargetClass 型別建立一個物件
加入型別 MyClass 的執行個體到 MyTargetClassObject 集合內
MyClass 型別建立一個物件 集合物件1
加入型別 MyClass 的執行個體到 MyTargetClassObject 集合內
MyClass 型別建立一個物件 集合物件2
加入型別 MyClass 的執行個體到 MyTargetClassObject 集合內
MyClass 型別建立一個物件 集合物件3
接著,我們要將 fooMyTargetClass 這個物件變數的參考值,設定為空值,也就是剛剛生成的 MyTargetClass 類別的執行個體,在記憶體中,已經變成沒有任何變數有參考的到這個物件。
+

然後,我們就強制持行記憶體回收行程,我們執行 GC.Collect(); 這個敘述,完成這樣的需求。
這是我們使用的測試程式碼
            Console.WriteLine("將參考變數 fooMyTargetClass 設定為空值");
            fooMyTargetClass = null;
            Console.WriteLine("進行記憶體回收行程 Garbage Collection");
            GC.Collect();
            Console.WriteLine("休息3秒鐘");
            Thread.Sleep(3000);
            Console.WriteLine("Press any key for continuing...");
            Console.ReadKey();
此時,根據上述的測試程式碼,就會得到底下的執行結果。您可以解釋出來,為什麼會得到這樣的結果嗎?
將參考變數 fooMyTargetClass 設定為空值
進行記憶體回收行程 Garbage Collection
休息3秒鐘
MyClass 型別的物件 集合物件3 要被回收了
MyClass 型別的物件 集合物件2 要被回收了
MyClass 型別的物件 集合物件1 要被回收了
MyListClass 型別的物件 要被回收了
MyTargetClass 型別的物件 要被回收了

C# : 結構成員相等 Member Equal 的測試

我們知道,若由結構型別實例化出來的執行個體,若要使用該執行個體進行比較的話,預設會使用實值比較,也就是說,會針對該結構內的每個成員,進行比較。
若該結構內的資料成員全部都是屬於實值型別,與某些成員屬於參考型別的話,這樣會發生甚麼樣的情況呢?
在這裡,我們宣告了兩個結構與一個類別,進行這樣的測試。
  • MyStructString
    這個結構裡面的成員,全部都是實值型別
  • MyStructClass
    這個結構內,有一個成員的型別為 MyClass,這個成員的型別將會是參考型別
public struct MyStructString
{
    public int ID { get; set; }
    public string Name { get; set; }
    public MyStructString(int id, string name)
    {
        ID = id;
        Name = name;
    }
}
public class MyClass
{
    public int ID { get; set; }
    public MyClass(int id)
    {
        ID = id;
    }
}
public struct MyStructClass
{
    public int ID { get; set; }
    public MyClass MYClass { get; set; }
    public MyStructClass(int id, MyClass myClass)
    {
        ID = id;
        MYClass = myClass;
    }
}

進行測試

首先,實例化出兩個結構的物件,不過,這兩個結構的物件的資料成員都擁有一樣的值,這裡一個是 Int 型別 (ID),一個是字串型別(Name)。
經過使用物件的 Equals 方法來進行比對這兩個物件,得到的結果是相同的。
可是,在這裡,我們注意到了, Name 這個屬性的型別是字串,這是個參考型別,所以,底下的測試,我們將會建立另外一個類別,與 MyStructString 結構類似的成員,不過,我們把 Name 這個字串型別屬性,變更成為 MYClass 這個 MyClass 參考類別的屬性。
MyStructString myObject1 = new MyStructString(99, "C#");
MyStructString myObject2 = new MyStructString(99, "C#");
Console.WriteLine($"結構成員比較 {myObject1.Equals(myObject2)}");
Console.WriteLine("Press any key for continuing...");
Console.ReadKey();
執行結果為
結構成員比較 True
Press any key for continuing...
MyStructClass 這個結構類別中,有個參考型別的屬性,其型別為客製化的類別 MyClass,我們在建立這個兩個結構物件的時候,讓結構的成員都設定成為具有相同的值(不過,分別屬於不同的執行個體)。
當我們使用了結構物件的 Equals 方法進行比對的時候,卻得到不成立的答案。
可是,這個測試結構 MyStructClass 與前一個測試結構 MyStructString,裡面都會有一個參考型別,前者為 MyClass,後者為 string。同樣都是參考的類別型別,為什麼會得到不相同的結果呢?
這是因為, string 這個參考型別,由覆寫了 Equals 方法,當進行兩個 string 字串物件比較的時候,這些 string 所生成的物件,會根據物件內實際的字串內容進行比對,而不是使用參考來進行比對是否相等。
這樣就可以解釋,為什麼 MyStructClass 會得到 False 的結果,那是因為在 MyClass 沒有覆寫 Equals 這個方法,所以,當呼叫 Equals 方法的時候,會使用參考物件的參考值來進行比對,也就是當兩個參考都指向同一個物件的時候,才會得到 True 的結果。
MyStructClass mySCObject1 = new MyStructClass(66, new MyClass(168));
MyStructClass mySCObject2 = new MyStructClass(66, new MyClass(168));
Console.WriteLine($"結構成員比較 {mySCObject1.Equals(mySCObject2)}");
Console.WriteLine("Press any key for continuing...");
Console.ReadKey();
執行結果為
結構成員比較 False
Press any key for continuing...