2018年7月6日 星期五

C# 繼承與多型 1 : 量測記憶體使用情況

在我們這篇文章將會以記憶體耗用的角度,來查看當進行類別繼承設計的時候,基底類別與延伸類別所產生的物件,到底會耗用多少的記憶體來進行觀察。
這篇文章將會建立一個名為 MeasureObjectSize 的 .NET Core 主控制台應用程式專案
首先,我們會宣告三個類別,一個是空的類別 MyEmptyClass,在這裡面甚麼都沒有宣告與定義,第二個是基底類別 MyBaseClase,他只有一個 Number 屬性並且為一個整數陣列,長度為 1024 個,第三個為衍生類別 MyDerivedClass,他繼承了 基底類別 MyBaseClase
C Sharp / C#
namespace ByInheritance
{
    public class MyBaseClase
    {
        protected int[] Number { get; set; } = new int[1024];
    }

    public class MyEmptyClass
    {

    }
    public class MyDerivedClass : MyBaseClase
    {

    }
}
我們開始進行建立與量測要建立不同類別物件的時候,究竟會耗用多少記憶體空間。由於在 .NET 環境之下,並沒有提供任何 API 可以讓您量測一個物件耗用了多少記憶體空間,因此,我們使用了 GC.GetTotalMemory 方法.aspx) 幫助我們取得 目前配置於 managed 記憶體中的位元組數目。
首先,我們先來量測,當建立了一個 1024 長度的整數陣列,會耗用多少的記憶體空間,所以,我們先取得現在執行環境中配置於 managed 記憶體中的位元組數目,接著,建立一個 1024 長度的整數陣列,然後再度取得配置於 managed 記憶體中的位元組數目,最後,兩者相減,就可以得到 1024 長度的整數陣列在 .NET 環境中,使用了多少記憶體空間。
我們得到了 4120 位元組這個數值
C Sharp / C#
long BeginMemoryUsage, ArrayMemoryUsage, 
    EmptyClassObjectMemoryUsage, BaseClassObjectMemoryUsage, DerivedClassObjectMemoryUsage;
int[] localNumber;

BeginMemoryUsage = GC.GetTotalMemory(true);
localNumber = new int[1024];
ArrayMemoryUsage = GC.GetTotalMemory(true);
Console.WriteLine("量測 int[1024] 的耗用記憶體");
Console.WriteLine($"int[1024] 耗用記憶體 : {ArrayMemoryUsage - BeginMemoryUsage} ");

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

BeginMemoryUsage = GC.GetTotalMemory(true);
ByInheritance.MyEmptyClass fooMyEmptyClass = new ByInheritance.MyEmptyClass();
EmptyClassObjectMemoryUsage = GC.GetTotalMemory(true);
ByInheritance.MyBaseClase fooMyBaseClassObject = new ByInheritance.MyBaseClase();
BaseClassObjectMemoryUsage = GC.GetTotalMemory(true);
ByInheritance.MyDerivedClass fooMyDerivedClassObject = new ByInheritance.MyDerivedClass();
DerivedClassObjectMemoryUsage = GC.GetTotalMemory(true);

Console.WriteLine("量測單純繼承之物件使用記憶體使用情況");
Console.WriteLine($"產生空類別  之執行個體耗用記憶體 : {EmptyClassObjectMemoryUsage - BeginMemoryUsage} ");
Console.WriteLine($"建立基底類別之執行個體耗用記憶體 : {BaseClassObjectMemoryUsage - EmptyClassObjectMemoryUsage} ");
Console.WriteLine($"建立衍生類別之執行個體耗用記憶體 : {DerivedClassObjectMemoryUsage - BaseClassObjectMemoryUsage} ");
Console.WriteLine($"衍生類別之執行個體 與 基底類別之執行個體 記憶體相差數量 : " +
    $"{(BaseClassObjectMemoryUsage - EmptyClassObjectMemoryUsage) - (DerivedClassObjectMemoryUsage - BaseClassObjectMemoryUsage)} ");

Console.WriteLine("Press any key for continuing...");
Console.ReadKey();
現在,我們使用相同的手法,分別來產生 空類別 Empty 、 基底類別 Base Class 、 衍生類別 Derived Class 的物件,並且得到他們分別耗用了多少記憶體空間,底下是我們的執行結果。
我們看到了,基底類別與衍生類別所產生的物件,他們具有相同的記憶體使用空間,你知道為什麼嗎?
Console
量測單純繼承之物件使用記憶體使用情況
產生空類別  之執行個體耗用記憶體 : 24
建立基底類別之執行個體耗用記憶體 : 4144
建立衍生類別之執行個體耗用記憶體 : 4144
衍生類別之執行個體 與 基底類別之執行個體 記憶體相差數量 : 0
Press any key for continuing...
剛剛我們看到了衍生類別繼承了基底類別,不過,衍生類別裡面沒有另外新增任何類別成員,他們耗用相同的記憶體空間。現在,我們在基底類別的陣列屬性成員前,加入 virtual 關鍵字,並且在衍生類別內,使用 override 來覆寫這個屬性,那麼,對於由基底類別與衍生類別所產生出來的物件,其佔用記憶體情況又會是如何呢?
C Sharp / C#
namespace ByInheritancePolymorphismOverride
{
    public class MyBaseClase
    {
        protected virtual int[] Number { get; set; } = new int[1024];
    }
    public class MyEmptyClass
    {

    }
    public class MyDerivedClass : MyBaseClase
    {
        protected override int[] Number { get; set; } = new int[1024];
    }
}
現在,就讓我們也使用相同的技巧,分別建立起基底類別與衍生類別的物件,計算出這兩個物件到底佔用了多少記憶體空間。
C Sharp / C#
long BeginMemoryUsage, ArrayMemoryUsage, 
    EmptyClassObjectMemoryUsage, BaseClassObjectMemoryUsage, DerivedClassObjectMemoryUsage;
int[] localNumber;

Console.WriteLine("量測單純繼承之物件使用記憶體使用情況");
Console.WriteLine($"產生空類別  之執行個體耗用記憶體 : {EmptyClassObjectMemoryUsage - BeginMemoryUsage} ");
Console.WriteLine($"建立基底類別之執行個體耗用記憶體 : {BaseClassObjectMemoryUsage - EmptyClassObjectMemoryUsage} ");
Console.WriteLine($"建立衍生類別之執行個體耗用記憶體 : {DerivedClassObjectMemoryUsage - BaseClassObjectMemoryUsage} ");
Console.WriteLine($"衍生類別之執行個體 與 基底類別之執行個體 記憶體相差數量 : " +
    $"{(BaseClassObjectMemoryUsage - EmptyClassObjectMemoryUsage) - (DerivedClassObjectMemoryUsage - BaseClassObjectMemoryUsage)} ");

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

BeginMemoryUsage = GC.GetTotalMemory(true);
ByInheritancePolymorphismOverride.MyEmptyClass fooInheritancePolymorphismMyEmptyClass = new ByInheritancePolymorphismOverride.MyEmptyClass();
EmptyClassObjectMemoryUsage = GC.GetTotalMemory(true);
ByInheritancePolymorphismOverride.MyBaseClase fooInheritancePolymorphismMyBaseClassObject = new ByInheritancePolymorphismOverride.MyBaseClase();
BaseClassObjectMemoryUsage = GC.GetTotalMemory(true);
ByInheritancePolymorphismOverride.MyDerivedClass fooInheritancePolymorphismMyDerivedClassObject = new ByInheritancePolymorphismOverride.MyDerivedClass();
DerivedClassObjectMemoryUsage = GC.GetTotalMemory(true);

Console.WriteLine("量測繼承與多型覆寫之物件使用記憶體使用情況");
Console.WriteLine($"產生空類別  之執行個體耗用記憶體 : {EmptyClassObjectMemoryUsage - BeginMemoryUsage} ");
Console.WriteLine($"建立基底類別之執行個體耗用記憶體 : {BaseClassObjectMemoryUsage - EmptyClassObjectMemoryUsage} ");
Console.WriteLine($"建立衍生類別之執行個體耗用記憶體 : {DerivedClassObjectMemoryUsage - BaseClassObjectMemoryUsage} ");
Console.WriteLine($"衍生類別之執行個體 與 基底類別之執行個體 記憶體相差數量 : " +
    $"{(DerivedClassObjectMemoryUsage - BaseClassObjectMemoryUsage)- (BaseClassObjectMemoryUsage - EmptyClassObjectMemoryUsage)} ");

Console.WriteLine("Press any key for continuing...");
Console.ReadKey();
這是執行結果內容,我們看到了,竟然衍生類別所建立起來的物件所耗用的記憶體大小,是基底類別的一倍,那這到底發生了甚麼問題?這是因為衍生類別繼承的基底類別,因此,每個基底類別的成員所耗用的記憶體空間,都會保留在衍生類別所建立的物件,也就是多了一份,而衍生類別因為使用了繼承多型的覆寫,所以,在衍生類別所產生的物件中,也會有一份 1024 長度的陣列整數記憶體空間,您能夠理解嗎?
Console
量測繼承與多型覆寫之物件使用記憶體使用情況
產生空類別  之執行個體耗用記憶體 : 24
建立基底類別之執行個體耗用記憶體 : 4144
建立衍生類別之執行個體耗用記憶體 : 8272
衍生類別之執行個體 與 基底類別之執行個體 記憶體相差數量 : 4128
最後,我們在基底類別的屬性成員前面將不會加入任何的 virtual 關鍵字,並且在衍生類別屬性成員中宣告了 new 這個隱藏基底成員
C Sharp / C#
namespace ByInheritancePolymorphismNew
{
    public class MyBaseClase
    {
        protected int[] Number { get; set; } = new int[1024];
    }
    public class MyEmptyClass
    {

    }
    public class MyDerivedClass : MyBaseClase
    {
        protected new int[] number { get; set; } = new int[1024];
    }
}
我們也使用了相同的記憶體使用量的量測技術,分別檢視基底類別與衍生類別他們所耗用的記憶體大小
C Sharp / C#
long BeginMemoryUsage, ArrayMemoryUsage, 
    EmptyClassObjectMemoryUsage, BaseClassObjectMemoryUsage, DerivedClassObjectMemoryUsage;
int[] localNumber;

BeginMemoryUsage = GC.GetTotalMemory(true);
localNumber = new int[1024];
ArrayMemoryUsage = GC.GetTotalMemory(true);
Console.WriteLine("量測 int[1024] 的耗用記憶體");
Console.WriteLine($"int[1024] 耗用記憶體 : {ArrayMemoryUsage - BeginMemoryUsage} ");

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

BeginMemoryUsage = GC.GetTotalMemory(true);
ByInheritancePolymorphismNew.MyEmptyClass fooInheritancePolymorphismNewMyEmptyClass = new ByInheritancePolymorphismNew.MyEmptyClass();
EmptyClassObjectMemoryUsage = GC.GetTotalMemory(true);
ByInheritancePolymorphismNew.MyBaseClase fooInheritancePolymorphismNewMyBaseClassObject = new ByInheritancePolymorphismNew.MyBaseClase();
BaseClassObjectMemoryUsage = GC.GetTotalMemory(true);
ByInheritancePolymorphismNew.MyDerivedClass fooInheritancePolymorphismNewMyDerivedClassObject = new ByInheritancePolymorphismNew.MyDerivedClass();
DerivedClassObjectMemoryUsage = GC.GetTotalMemory(true);

Console.WriteLine("量測繼承與多型隱藏之物件使用記憶體使用情況");
Console.WriteLine($"產生空類別  之執行個體耗用記憶體 : {EmptyClassObjectMemoryUsage - BeginMemoryUsage} ");
Console.WriteLine($"建立基底類別之執行個體耗用記憶體 : {BaseClassObjectMemoryUsage - EmptyClassObjectMemoryUsage} ");
Console.WriteLine($"建立衍生類別之執行個體耗用記憶體 : {DerivedClassObjectMemoryUsage - BaseClassObjectMemoryUsage} ");
Console.WriteLine($"衍生類別之執行個體 與 基底類別之執行個體 記憶體相差數量 : " +
    $"{(DerivedClassObjectMemoryUsage - BaseClassObjectMemoryUsage)- (BaseClassObjectMemoryUsage - EmptyClassObjectMemoryUsage)} ");

Console.WriteLine("Press any key for continuing...");
Console.ReadKey();
這是執行結果內容,我們看到了,竟然衍生類別所建立起來的物件所耗用的記憶體大小,是基底類別的一倍,那這到底發生了甚麼問題?這是因為衍生類別繼承的基底類別,因此,每個基底類別的成員所耗用的記憶體空間,都會保留在衍生類別所建立的物件,也就是多了一份,而衍生類別因為使用了繼承多型的隱藏,所以,在衍生類別所產生的物件中,也會有一份 1024 長度的陣列整數記憶體空間。
所以,只要在繼承關係的時候,有在衍生類別中,重新定義屬性成員,不論您使用了 override 覆寫或者是使用了 new 隱藏關鍵字,這個時候,當衍生類別物件產生了時候,除了衍生類別中宣告的成員會占用記憶體之外,衍生類別所產生的物件,也會包含了基底類別的屬性成員所需要用到的記憶體大小。
Console
量測繼承與多型隱藏之物件使用記憶體使用情況
產生空類別  之執行個體耗用記憶體 : 24
建立基底類別之執行個體耗用記憶體 : 4144
建立衍生類別之執行個體耗用記憶體 : 8272
衍生類別之執行個體 與 基底類別之執行個體 記憶體相差數量 : 4128

關於 Xamarin 在台灣的學習技術資源

Xamarin 實驗室 粉絲團
歡迎加入 Xamarin 實驗室 粉絲團,在這裡,將會經常性的貼出各種關於 Xamarin / Visual Studio / .NET 的相關消息、文章、技術開發等文件,讓您可以隨時掌握第一手的 Xamarin 方面消息。
Xamarin.Forms @ Taiwan
歡迎加入 Xamarin.Forms @ Taiwan,這是台灣的 Xamarin User Group,若您有任何關於 Xamarin / Visual Studio / .NET 上的問題,都可以在這裡來與各方高手來進行討論、交流。
Xamarin 實驗室 部落格
Xamarin 實驗室 部落格 是作者本身的部落格,這個部落格將會專注於 Xamarin 之跨平台 (Android / iOS / UWP) 方面的各類開技術探討、研究與分享的文章,最重要的是,它是全繁體中文。
Xamarin.Forms 系列課程
Xamarin.Forms 系列課程 想要快速進入到 Xamarin.Forms 的開發領域,學會各種 Xamarin.Forms 跨平台開發技術,例如:MVVM、Prism、Data Binding、各種 頁面 Page / 版面配置 Layout / 控制項 Control 的用法等等,千萬不要錯過這些 Xamarin.Forms 課程

沒有留言:

張貼留言