C# 物件導向

2023-05-23 12:01:37

前言

C# 是一種物件導向、型別安全的語言。

❓什麼是物件導向

物件導向程式設計(OOP)是如今多種程式語言所實現的一種程式設計正規化,包括 Java、C++、C#。

物件導向程式設計將一個系統抽象為許多物件的集合,每一個物件代表了這個系統的特定方面。物件包括函數(方法)和資料。一個物件可以向其他部分的程式碼提供一個公共介面,而其他部分的程式碼可以通過公共介面執行該物件的特定操作,系統的其他部分不需要關心物件內部是如何完成任務的,這樣保持了物件自己內部狀態的私有性。

物件導向和程式導向的區別:

物件導向:用線性的思維。與程式導向相輔相成。在開發過程中,宏觀上,用物件導向來把握事物間複雜的關係,分析系統。微觀上,仍然使用程式導向。

程式導向:是一種是事件為中心的程式設計思想。就是分析出解決問題所需的步驟,然後用函數把這寫步驟實現,並按順序呼叫。

簡單來說:用程式導向的方法寫出來的程式是一份蛋炒飯,而用物件導向寫出來的程式是一份蓋澆飯。所謂蓋澆飯,就是在米飯上面澆上一份蓋菜,你喜歡什麼菜,你就澆上什麼菜。

這個比喻還是比較貼切的。

❓為什麼使用物件導向程式設計

物件導向程式設計,可以讓程式設計更加清晰,把程式中的功能進行模組化劃分,每個模組提供特定的功能,同時每個模組都是孤立的,這種模組化程式設計提供了非常大的多樣性,大大增加了重用程式碼的機會,而且各模組不用關心物件內部是如何完成的,可以保持內部的私有性。簡單來說物件導向程式設計就是結構化程式設計,對程式中的變數結構劃分,讓程式設計更清晰。

準確地說,本文所提及到的特性是一種特別的物件導向程式設計方式,即基於類的物件導向程式設計(class-based OOP)。當人們談論物件導向程式設計時,通常來說是指基於類的物件導向程式設計。

類 - 實際上是建立物件的模板。當你定義一個類時,你就定義了一個資料型別的藍圖。這實際上並沒有定義任何的資料,但它定義了類的名稱,這意味著什麼,這意味著類的物件由什麼組成及在這個物件上可執行什麼操作。物件是類的範例。構成類的方法和變數稱為類的成員。

類的定義和使用

類中的資料和函數稱為類的成員

  • 資料成員
    • 資料成員是包含類的資料 - 欄位,常數和事件的成員。
  • 函數成員
    • 函數成員提供了操作類中資料的某些功能 - 方法,屬性,構造器(構造方法)和終端子(解構方法),運運算元,和索引器

拿控制檯程式為例,當我們建立一個空的控制檯專案,在Main()函數里程式設計的時候就是在Program類裡面操作的:

而且,我們可以發現,Program類和儲存它的檔案的檔名其實是一樣的Program.cs,一般我們習慣一個檔案一個類,類名和檔名一致。當然了,這不是說一個檔案只能寫一個類,一個檔案是可以包含多個類的。

新建一個Customer類來表示商店中購物的顧客:

 class Customer
    {
        public string name;
        public string address;
        public int age;
        public string createTime;   // 加入會員的時間

        public void Show()
        {
            Console.WriteLine("名字:" + name);
            Console.WriteLine("地址:" + address);
            Console.WriteLine("年齡:" + age);
            Console.WriteLine("建立時間:" + createTime);
        }
    }

Customer類裡有四個公有欄位和一個共有方法Show()來輸出顧客資訊。

建立Customer類的物件:

static void Main(string[] args)
{
    Customer customer = new Customer();
    customer.name = "Test";
    customer.address = "Test01";
    customer.age = 24;
    customer.createTime = "2023-02-27";
    customer.Show();
    Console.ReadKey();
}

通過類建立的變數被稱之為物件,這個過程我們叫他範例化。所有物件在使用之前必須範例化,僅僅宣告一個物件變數或者賦值為null都是不行的。到現在看來,其實簡單的類在定義和使用起來跟結構體是差不多的,只不過結構體在建立的時候沒有範例化的過程,因為結構體是值型別的資料結構,而類是參照型別。

小小練習

推薦大家開發過程中,儘量一個檔案裡面一個類,當然一個檔案可以放多個類,但管理起來不方便,一個類一個檔案管理起來方便,如果程式很小,怎麼寫都無所謂,如果程式大或團隊合作,最好一個類一個檔案。

而且一個類定義也可以在多個檔案中哦 - partial className

定義一個車輛Vehicle類,具有RunStop等方法,具有 Speed ( 速度 ) 、MaxSpeed ( 最大速度 ) 、Weight ( 重量 )等(也叫做欄位)。

使用這個類宣告一個變數(物件)。

static void Main(string[] args)
{
    Vehicle vehicle = new Vehicle();
    vehicle.brand = "BMW X5";
    vehicle.speed = 90;
    vehicle.maxSpeed = 215;
    vehicle.weight = 32;
    vehicle.Run();
    vehicle.Stop();
    Console.ReadKey();
}
class Vehicle
{
    // 欄位
    public string brand;
    public int speed;
    public int maxSpeed;
    public float weight;
    // 方法
    public void Run()
    {
        Console.WriteLine("Run!");
    }
    public void Stop()
    {
        Console.WriteLine("Stop!");
    }
}

定義一個向量Vector類,裡面有x,y,z三個欄位,有取得長度的方法,有設定屬性Set的方法使用這個類宣告一個變數(物件)。

class Vector3
{
    // 欄位
    private double x;
    private double y;
    private double z;

    // 屬性【X】 - SetX為一個普通方法
    public void SetX(double temp)
    {
        x = temp;
    }
    public void SetY(double temp)
    {
        y = temp;
    }
    public void SetZ(double temp)
    {
        z = temp;
    }

    // 方法
    public double GetLength()
    {
        return Math.Sqrt(x * x + y * y + z * z);
    }
}

屬性 - 是類的一種成員,它提供靈活的機制來讀取、寫入或計算私有欄位的值。 屬性可用作公共資料成員,但它們是稱為「存取器」的特殊方法。 此功能使得可以輕鬆存取資料,還有助於提高方法的安全性和靈活性。

這裡先不詳細說,後續章節再展開。Vector3類裡面的Set*屬性是用來給x,y,z賦值的,可以看到與之前的簡單類不同的是,Vector3類裡的欄位是private也就是私有的,這意味著在類的外部是沒有辦法存取這寫欄位的,它只在類自己內部是大家都知道的,到外面就不行了。

這裡一開始寫錯了,類Vector3中的SetXSetYSetZ 方法是普通的方法,而不是屬性。它們僅僅是修改和存取範例中私有欄位的方法。它們需要一個引數才能設定相應的欄位值,而屬性是通過存取器方法來設定或獲取欄位的值,並且不需要額外的引數。

public 和 private 存取修飾符

  • 存取修飾符(C# 程式設計指南)
  • public修飾的資料成員和成員函數是公開的,所有的使用者都可以進行呼叫。
  • private修飾詞修飾的成員變數以及成員方法只供本類使用,也就是私有的,其他使用者是不可呼叫的。

publicprivate這兩個修飾符其實從字面意思就可以理解,沒什麼不好理解的,前者修飾的欄位大家可以隨意操作,千刀萬剮只要你樂意,而後者修飾的欄位就不能任你宰割了,你只能通過GetSet進行一系列的存取或者修改。

舉個例子,生活中每個人都有名字、性別,同時也有自己的銀行卡密碼,當別人跟你打交道的時候,他一般會先得知你的名字,性別,這些告訴他是無可厚非的,但是當他想知道你的銀行卡密碼的時候就不太合適了對吧。假設我們有一個類Person,我們就可以設定Name,Sex等欄位為公有的public,大家都可以知道,但是銀行卡密碼就不行,它得是私有的,只有你自己知道。但是加入你去銀行ATM機取錢,它就得知道你的銀行卡密碼才能讓你取錢對吧,前面我們已經了密碼是私有的,外部是沒辦法存取的,那該怎麼辦呢,這個時候就用到屬性了。我們用Get獲取密碼,用Set修改密碼。

放在程式碼裡面:

static void Main(string[] args)
{
    Vector3 vector = new Vector3();
    vector.w = 2;
    vector.SetX(1);
    Console.WriteLine(vector.GetX()); 
    Console.ReadKey();
}
class Vector3
{
    // 欄位
    private double x;
    public double w;
    // 屬性
    public void SetX(double temp)
    {
        x = temp;
    }
    // ......
    public double GetX()
    {
        return x;
    }
}

w欄位在類外部可以直接操作,x只能通過GetSet來操作。

日常開發推薦不要把欄位設定為共有的,至少要有點存取限制,當然了除了這兩個修飾符,還有其他的,比如internalprotect等等,以後的文章可能會專門來寫(❓)。

使用private修飾符除了多了一堆屬性(存取器)有什麼便利嗎?顯然得有,public的欄位你在設定的時候說啥就啥,即使它給到的內容可能不適合這個欄位,在後者,我們可以在屬性裡設定一些限制或者是操作。比如,Vector3類的x欄位顯然長度是不會出現負值的,這時候我們就可以在SetX裡面做些限制:

public void SetX(double temp)
{
    if (temp<0)
    {
        Console.WriteLine("資料不合法。");
    }
    x = temp;
}

對於不想讓外界存取的資訊我們可以不提供Get屬性以起到保護作用。

建構函式

建構函式(C# 程式設計指南)

建構函式 - 也被稱為「構造器」,是執行類或結構體的初始化程式碼。每當我們建立類或者結構體的範例的時候,就會呼叫它的建構函式。大家可能會疑惑,我們上面建立的類裡面也沒說這個建構函式這個東東啊,那是因為如果一個類沒有顯式範例建構函式,C#將提供可用於實現範例化該類範例的無參建構函式(隱式),比如:

public class Person
{
    public int age;
    public string name = "unknown";
}

class Example
{
    static void Main()
    {
        var person = new Person();
        Console.WriteLine($"Name: {person.name}, Age: {person.age}");
        // Output:  Name: unknown, Age: 0
    }
}

預設建構函式根據相應的初始值設定項初始化範例欄位和屬性。 如果欄位或屬性沒有初始值設定項,其值將設定為欄位或屬性型別的預設值。 如果在某個類中宣告至少一個範例建構函式,則 C# 不提供無引數建構函式。

回到開頭,建構函式有什麼作用呢?

我們構造物件的時候,物件的初始化過程是自動完成的,但是在初始化物件的過程中有的時候需要做一些額外的工作,比如初始化物件儲存的資料,建構函式就是用於初始化資料的函數。 使用建構函式,開發人員能夠設定預設值、限制範例化,並編寫靈活易讀的程式碼。

建構函式是一種方法。

建構函式的定義和方法的定義類似,區別僅在於建構函式的函數名只能和封裝它的型別相同。宣告基本的建構函式的語法就是宣告一個和所在類同名的方法,但是該方法沒有返回型別。

拿之前的Customer類為例,我們來給他寫一個簡單的建構函式:

static void Main(string[] args)
{
    Customer customer = new Customer();
    // Output :我一個建構函式。
    Console.ReadKey();
}

class Customer
{
    public string name;
    public string address;
    public int age;
    public string createTime;   // 加入會員的時間

    public Customer()
    {
        Console.WriteLine("我一個建構函式。");
    }

    public void Show()
    {
        Console.WriteLine("名字:" + name);
        Console.WriteLine("地址:" + address);
        Console.WriteLine("年齡:" + age);
        Console.WriteLine("建立時間:" + createTime);
    }
}

當我們建立Customer類的範例的時候就會呼叫我們寫無參的建構函式,雖然這個目前這個函數是沒什麼實際意義的,我們一般使用建構函式中實現資料初始化,比如我們來實現對顧客資訊的初始化:

static void Main(string[] args)
{
    Customer customer = new Customer();
    Customer customer2 = new Customer("光頭強", "狗熊嶺", 30, "2305507");
    customer2.Show();
    // Output:
    // 我一個建構函式。
    // 名字:光頭強
    // 地址:狗熊嶺
    // 年齡:30
    // 建立時間:2305507
    Console.ReadKey();
}

class Customer
{
    public string name;
    public string address;
    public int age;
    public string createTime;   // 加入會員的時間

    public Customer()
    {
        Console.WriteLine("我一個建構函式。");
    }

    public Customer(string arg1, string arg2, int arg3, string arg4)
    {
        name = arg1;
        address = arg2;
        age = arg3;
        createTime = arg4;
    }

    public void Show()
    {
        Console.WriteLine("名字:" + name);
        Console.WriteLine("地址:" + address);
        Console.WriteLine("年齡:" + age);
        Console.WriteLine("建立時間:" + createTime);
    }
}

有參的建構函式相當於無參建構函式的過載,在建立範例時,執行時會自動匹配對應的建構函式。這是時候輸出的內容裡面」我是」我一個建構函式「是在建立範例customer的時候呼叫的無參建構函式,customer2在建立的時候呼叫的時對應四個引數的有參建構函式。進行有參構造的範例時一定注意對應的參數列:型別、數量等必須一致,否則就不能成功建立範例。

當我們註釋掉Customer類裡的無參建構函式後,Customer customer = new Customer();就會報錯,這就是我們上面所說的,如果在某個類中宣告至少一個範例建構函式,則 C# 不提供預設的無引數建構函式。

我們例子中的四個引數的建構函式在使用起來是很不方便的,引數arg1在我們建立範例的時候可能會混淆,不清楚哪個引數代表哪個欄位,假入你現在使用的是Visual Studio 2022,你在建立類以後,IntelliSense程式碼感知工具可能會給你生成一個和類中欄位匹配的建構函式:

public Customer(string name,string address,int age,string createTime)
{
    this.name = name;
    this.address = address;
    this.age = age;
    this.createTime = createTime;
}

你會發現這個建構函式的引數和Customer的欄位是一樣的,型別、變數名都一樣,這個時候就需要用到this關鍵字了,如果這個時候我們還寫成name = name;就會出錯,雖然我們可能知道前面name是欄位,後面的是傳遞進去的引數,但是編譯器是不認識的,咱們這樣寫完它的CPU就冒煙了,這是幹啥呢,誰是誰啊。

簡單概述,後面會有章節展開說。this 關鍵字指代類的當前範例,我們可以通過this存取類中欄位來區分變數。

屬性

為了保護資料安全,類裡面的欄位我們一般都設定為私有的,之前的Vector3類中我們是通過編寫GetSet方法來存取或者修改欄位的資料,這樣在實際開發中是很麻煩的,會降低我們的效率而且使用起來我們必須通過呼叫這兩個方法來實現對私有欄位的操作:

static void Main(string[] args)
{
    Customer customer = new Customer();
    customer.SetAge(24);
    Console.WriteLine(customer.GetAge());
    // Output: 24
    Console.ReadKey();
}
class Customer
{
    public string name;
    public string address;
    public int age;
    public string createTime;
    public void SetAge(int age)
    {
        this.age = age;
    }
    public int GetAge()
    {
        return this.age;    // 這裡 this 可加可不加
    }
    public void Show()
    {
        Console.WriteLine("名字:" + name);
        Console.WriteLine("地址:" + address);
        Console.WriteLine("年齡:" + age);
        Console.WriteLine("建立時間:" + createTime);
    }
}

我們可以通過屬性來快捷實現對私有欄位的存取以及修改,通過getset存取器操作私有欄位的值。

❓什麼是屬性呢

  • 屬性是一種成員,它提供靈活的機制來讀取、寫入或計算私有欄位的值。 屬性可用作公共資料成員,但它們是稱為「存取器」的特殊方法。 此功能使得可以輕鬆存取資料,還有助於提高方法的安全性和靈活性。

  • 屬性允許類公開獲取和設定值的公共方法,而隱藏實現或驗證程式碼。

  • 屬性可以是讀-寫屬性(既有 get 存取器又有 set 存取器)、唯讀屬性(有 get 存取器,但沒有 set 存取器)或只寫存取器(有 set 存取器,但沒有 get 存取器)。 只寫屬性很少出現,常用於限制對敏感資料的存取。

  • 不需要自定義存取器程式碼的簡單屬性可以作為表示式主體定義或自動實現的屬性來實現。

上面的SetAgeGetAge方法我們用屬性替換掉就是:

static void Main(string[] args)
{
    Customer customer = new Customer();
    customer.Age = 10;
    Console.WriteLine(customer.Age);
    // Output: 10
    Console.ReadKey();
}
class Customer
{
    private string name;
    private string address;
    private int age;
    private string createTime;
    // 屬性
    public int Age
    {
        get
        {
            return this.age;
        }
        set // value 引數
        {
            this.age = value;
        }
    }
    public void Show()
    {
        Console.WriteLine("名字:" + name);
        Console.WriteLine("地址:" + address);
        Console.WriteLine("年齡:" + age);
        Console.WriteLine("建立時間:" + createTime);
    }
}

屬性的時候就像存取一個公有的欄位一樣方便,我們在可以像是一個普通的公有的資料成員一樣使用屬性。只不過我們通過屬性Age進行賦值的時候,在類的內部會呼叫set存取器,這是我們給屬性Age賦的值就會被當作value引數傳遞進去,實現賦值;同理,我們在使用屬性Age的時候也是通過get存取器來實現的。

上面屬性Age裡的關鍵字可以不寫也沒問題的。

除了進行簡單資料存取和賦值,我們有一個實現屬性的基本模式: get 存取器返回私有欄位的值,set 存取器在向私有欄位賦值之前可能會執行一些資料驗證。 這兩個存取器還可以在儲存或返回資料之前對其執行某些轉換或計算。

比如我們可以驗證顧客的年齡不為負值:

static void Main(string[] args)
{
    Customer customer = new Customer();
    customer.Age = -10;
    // 引發 ArgumentOutOfRangeException 異常
    Console.ReadKey();
}
class Customer
{
    private string name;
    private string address;
    private int age;
    private string createTime;
    // 屬性
    public int Age
    {
        get
        {
            return this.age;
        }
        set // value 引數
        {
            if (value < 0)
            {
               throw new ArgumentOutOfRangeException(nameof(value), "The age must be greater than 0.");
            }
            this.age = value;
        }
    }
}

同時呢,我們一個定義存取器的存取許可權,如果在Age屬性的set存取器前面加上private修飾符,那我們就沒辦法使用 customer.Age = -10;來進行賦值了,編譯器會告知錯誤set存取器無法存取。

此外,我們可以通過get 存取器和 set 存取器的有無來控制屬性是讀 - 寫、唯讀、還是隻寫,只寫屬性很少出現,常用於限制對敏感資料的存取。

class Customer
{
    private string name;
    private string address;
    private int age;
    private string createTime;
    // 屬性
    public int Age  // 讀 - 寫
    {
        get
        {
            return this.age;
        }
        set // value 引數
        {
            if (value < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(value), "The age must be greater than 0.");
            }
            this.age = value;
        }
    }
    public string Name  // 唯讀
    {
        get { return this.name; }
    }
    public string Address   // 只寫
    {
        set { this.address = value; }
    }
}

表示式屬性

C# 6開始,唯讀屬性(就像之前的例子中那樣的屬性)可簡寫為表示式屬性。它使用雙箭頭替換了花括號、get存取器和return關鍵字。

class Customer
{
    private string name;
    private string address;
    private int age;
    private string createTime;
    
    public int Age => age;	// 表示式屬性 唯讀屬性
}

C# 7進一步允許在set存取器上使用表示式體:

class Customer
{
    private string name;
    private string address;
    private int age;
    private string createTime;
    
    public int Age => age;
    public string Name { get => name; set => name = value; }
    public string Address{ set => address = value; }
}

自動實現的屬性

當屬性存取器中不需要任何其他邏輯時,自動實現的屬性會使屬性宣告更加簡潔。

自動實現的屬性是C# 3.0引入的新特性,它可以讓我們在不顯式定義欄位和存取器方法的情況下快速定義一個屬性。具體來說,一個屬性包含一個欄位和兩個存取器方法,其中getset存取器方法都是自動實現的。

static void Main(string[] args)
{
    Customer customer = new Customer();
    customer.name = "光頭強";
    customer.address = "狗熊嶺";
    customer.age = 30;
    customer.createTime = "2305507";
    customer.Show();
    // output:
    // 名字:光頭強
    // 地址:狗熊嶺
    // 年齡:30
    // 建立時間:2305507
    Console.ReadKey();
}
class Customer
{
  	// 自動實現的屬性
    public string name { get; set; }
    public string address { get; set; }
    public int age { get; set; }
    public string createTime { get; set; }

    public void Show()
    {
        Console.WriteLine("名字:" + name);
        Console.WriteLine("地址:" + address);
        Console.WriteLine("年齡:" + age);
        Console.WriteLine("建立時間:" + createTime);
    }
}

屬性初始化器

C# 6開始支援自動屬性的初始化器。其寫法就像初始化欄位一樣:

public int age { get; set; }=24;

上述寫法將``age`的值初始化為24。擁有初始化器的屬性可以為唯讀屬性:

public string sex { get; } = "male";

就像唯讀欄位那樣,唯讀自動屬性只可以在型別的構造器中賦值。這個功能適於建立不可變(唯讀)的物件。

匿名型別

匿名型別提供了一種方便的方法,可用來將一組唯讀屬性封裝到單個物件中,而無需首先顯式定義一個型別。 型別名由編譯器生成,並且不能在原始碼級使用。 每個屬性的型別由編譯器推斷,是一個由編譯器臨時建立來儲存一組值的簡單類。如果需要建立一個匿名型別,則可以使用new關鍵字,後面加上物件初始化器,指定該型別包含的屬性和值。例如:

var dude = new { Name = "Bob", Age = 23 };

編譯器將會把上述語句(大致)轉變為:

internal class AnonymousGeneratedTypeName
{
    private string name;  // Actual field name is irrelevant
    private int     age;    // Actual field name is irrelevant

    public AnonymousGeneratedTypeName (string name, int age)
    {
        this.name = name; this.age = age;
    }

    public string  Name { get { return name; } }
    public int      Age  { get { return age;  } }

    // The Equals and GetHashCode methods are overridden (see Chapter 6).
    // The ToString method is also overridden.
}
...

    var dude = new AnonymousGeneratedTypeName ("Bob", 23);

匿名型別只能通過var關鍵字來參照,因為它並沒有一個名字。

堆、棧

程式在執行時,記憶體一般從邏輯上分為兩大塊 - 堆、棧。

  • 堆疊(Stack - 因為和堆一起叫著彆扭,所以簡稱為棧):棧是一種先進後出(Last-In-First-Out,LIFO)的資料結構。當你宣告一個變數時,它會自動地被分配到棧記憶體中,並且它的作用域僅限於當前程式碼塊。在方法中宣告的區域性變數就是放在棧中的。棧的好處是,由於它的操作特性,棧的存取非常快,它也沒有垃圾回收的問題。棧空間比較小,但是讀取速度快。
  • 堆(Heap):堆是一種動態分配記憶體的資料結構。堆記憶體的大小不受限制,而且程式設計師可以控制它的生命週期,也就是說,在堆上分配的記憶體需要手動釋放。堆空間比較大,但是讀取速度慢。

堆和棧就相當於倉庫和商店,倉庫放的東西多,但是當我們需要裡面的東西時需要去裡面自行查詢然後取出來,後者雖然存放的東西沒有前者多,但是好在隨拿隨取,方便快捷。

棧是一種先進後出(Last-In-First-Out,LIFO)的資料結構。本質上講堆疊也是一種線性結構,符合線性結構的基本特點:即每個節點有且只有一個前驅節點和一個後續節點。

  • 資料只能從棧的頂端插入和刪除
  • 把資料放入棧頂稱為入棧(push)
  • 從棧頂刪除資料稱為出棧(pop)

堆是一塊記憶體區域,與棧不同,堆裡的記憶體可以以任意順序存入和移除。

GC

GC(Garbage Collector)垃圾回收器,是一種自動記憶體管理技術,用於自動釋放記憶體。在.NET Framework中,GC.NET的執行時環境CLR自動執行。在公共語言執行時 (CLR) 中,垃圾回收器 (GC) 用作自動記憶體管理器。 垃圾回收器管理應用程式的記憶體分配和釋放。 因此,使用受控程式碼的開發人員無需編寫執行記憶體管理任務的程式碼。 自動記憶體管理可解決常見問題,例如,忘記釋放物件並導致記憶體漏失,或嘗試存取已釋放物件的已釋放記憶體。

通過GC進行自動記憶體管理得益於C#是一種託管語言C#會將程式碼編譯為受控程式碼。受控程式碼以中間語言(Intermediate Language, IL)的形式表示。CLR通常會在執行前,將IL轉換為機器(例如x86或x64)原生程式碼,稱為即時(Just-In-Time, JIT)編譯。除此之外,還可以使用提前編譯(ahead-of-time compilation)技術來改善擁有大程式集,或在資源有限的裝置上執行的程式的啟動速度。

託管語言是一種在託管執行環境中執行的程式語言,該環境提供了自動記憶體管理、垃圾回收、型別檢查等服務。

託管執行環境是指由作業系統提供的一種高階執行時環境,例如Java虛擬機器器、.NET Framework、.NET Core 等。這種執行環境為程式提供了許多優勢,例如:

  1. 自動記憶體管理:託管執行環境為程式管理記憶體分配和釋放,程式設計師無需手動管理記憶體,避免了記憶體漏失和越界等問題。
  2. 垃圾回收:託管執行環境提供了垃圾回收服務,自動回收不再使用的記憶體,提高了程式的效能和可靠性。
  3. 型別檢查:託管執行環境提供了強型別檢查,防止了型別錯誤等問題。
  4. 平臺無關性:託管語言編寫的程式可以在不同作業系統和硬體平臺上執行,提高了程式的可移植性。

CLR中:

  • 每個程序都有其自己單獨的虛擬地址空間。 同一臺計算機上的所有程序共用相同的實體記憶體和頁檔案(如果有)。
  • 預設情況下,32 位計算機上的每個程序都具有 2 GB 的使用者模式虛擬地址空間。
  • 作為一名應用程式開發人員,你只能使用虛擬地址空間,請勿直接操控實體記憶體。 垃圾回收器為你分配和釋放託管堆上的虛擬記憶體。
  • 初始化新程序時,執行時會為程序保留一個連續的地址空間區域。 這個保留的地址空間被稱為託管堆。 託管堆維護著一個指標,用它指向將在堆中分配的下一個物件的地址。

既然垃圾回收是自動進行的,那麼一般什麼時候GC會開始回收垃圾呢?

  • 系統具有低的實體記憶體。記憶體大小是通過作業系統的記憶體不足通知或主機指示的記憶體不足檢測出來的。
  • 由託管堆上已分配的物件使用的記憶體超出了可接受的閾值。 隨著程序的執行,此閾值會不斷地進行調整。
  • 呼叫 GC.Collect 方法。幾乎在所有情況下,你都不必呼叫此方法,因為垃圾回收器會持續執行。 此方法主要用於特殊情況和測試。

我們開發人員可以使用new關鍵字在託管堆上動態分配記憶體,不需要手動釋放,GC會定期檢查託管堆上的物件,並回收掉沒有被參照的物件,從而釋放它們所佔用的記憶體。

❗❗❗需要注意的是,棧記憶體無需我們管理,同時它也不受GC管理。當棧頂元素使用完畢以後,所佔用的記憶體會被立刻釋放。而堆則需要依賴於GC清理。

值型別、參照型別

文章之前部分已經提到過C#是託管語言,在託管執行環境中執行的程式語言,該環境提供了強型別檢查,所以與其他語言相比,C#對其可用的型別及其定義有更嚴格的描述 ———— C#是一種強型別語言,每個變數和常數都有一個型別,每個求值的表示式也是如此。 每個方法宣告都為每個輸入引數和返回值指定名稱、型別和種類(值、參照或輸出)。

所有的C#型別可以分為以下幾類:

  • 值型別

  • 參照型別

  • 泛型型別

    C#泛型可以是值型別也可以是參照型別,具體取決於泛型引數的型別。

    如果泛型引數是值型別,那麼範例化出來的泛型型別也是值型別。例如,List<int>就是一個值型別,因為int是值型別。

    如果泛型引數是參照型別,那麼範例化出來的泛型型別也是參照型別。例如,List<string>就是一個參照型別,因為string是參照型別。

    需要注意的是,雖然泛型型別可以是值型別或參照型別,但是泛型型別的範例總是參照型別。這是因為在記憶體中,泛型型別的範例始終是在堆上分配的,無論它的泛型引數是值型別還是參照型別。因此,使用泛型型別時需要注意它的範例是參照型別。

  • 指標型別

    指標型別是C#中的一種高階語言特性,允許程式設計師直接操作記憶體地址。指標型別主要用於與非受控程式碼互動、實現底層資料結構等。指標型別在普通的C#程式碼中並不常見。

撇去指標型別,我們可以把C#中的資料型別分為兩種:

  • 值型別 - 分兩類:structenum,包括內建的數值型別(所有的數值型別、char型別和bool型別)以及自定義的struct型別和enum型別。
  • 參照型別 - 參照型別包含所有的類型別、介面型別、陣列型別或委託型別。和值型別一樣,C#支援兩種預定義的參照型別:objectstring

❗❗❗ object型別是所有型別的基本類型,其他型別都是從它派生而來的(包括值型別)。

各自在記憶體中的儲存方式

在此之前,我們需要明白Windows使用的是一個虛擬定址系統,該系統把程式可用的記憶體地址對映到硬體記憶體中的實際地址上,這些任務完全由Windows在後臺管理。其實際結果是32位元處理器上的每個程序都可以使用4GB的記憶體————不管計算機上實際有多少實體記憶體。這4個GB的記憶體實際上包含了程式的所有部分,包括可執行的程式碼、程式碼載入的所有DLL,以及程式執行時使用的所有變數的內容。這4個GB的記憶體稱為虛擬地址空間、虛擬記憶體,我們這裡簡稱它為記憶體。

我們可以藉助VS在直觀地體會這一特性,任意給個斷點,把變數移到記憶體視窗就可以檢視當前變數在記憶體中的地址以及儲存的內容:

例舉一些常用的變數:

// 值型別
int a = 123;
float b = 34.5f;
bool c = true;

// 參照型別
string name = "SiKi";
int[] array1 = new int[] { 23, 23, 11, 32, 4, 2435 };
string[] array2 = new string[] { "熊大", "熊二", "翠花" };
Customer customer = new Customer("光頭強", "狗熊嶺", 30, "2305507");

它們在記憶體中是怎麼儲存的呢?

  • 值型別就直觀的儲存在堆中。
  • array1在棧中儲存著一個指向堆中存放array1陣列首地址的參照,array2customer同理
  • name字串,儘管它看上去像是一個值型別的賦值,但是它是一個參照型別,name物件被分配在堆上。

關於字串在記憶體中的儲存,雖然它是參照型別,但是它與參照型別的常見行為是有一些區別的,例如:字串是不可變的。修改其中一個字串,就會建立一個全新的string物件,而對已存在的字串不會產生任何影響。例如:

static void Main(string[] args)
{
    string s1 = "a string";
    string s2 = s1;
    s1 = "another string";
    Console.ReadKey();
}

藉助VS的記憶體視窗:

s1也就是儲存著a string字串的地址是0x038023DC,再執行你就會發現s2的記憶體地址也是0x038023DC,但是當s1中儲存的字串發生變化時,s1的記憶體地址也會隨之變化,但是s2的記憶體地址還是之前a string所在的位置。

也就是說,字串的值在發生變化時並不會替換原來的值,而是在堆上為新的字串值分配一個新的物件(記憶體空間),之前的字串值物件是不受影響的【這實際上是運運算元過載的結果】。

To sum up,值型別直接儲存其值,而參照型別儲存對值的參照。這兩種型別儲存在記憶體的不同地方:值型別儲存在棧(stack)中,而參照型別儲存在託管堆(managed heap)上。

  • 值型別只需要一段記憶體,總是分配在它宣告的地方,做為區域性變數時,儲存在棧上;假如是類物件的欄位時,則跟隨此類儲存在堆中。
  • 參照型別需要兩段記憶體,第一段儲存實際的資料【堆】,第二段是一個參照【棧】,用於指向資料在堆中的儲存位置。參照型別範例化的時候,會在託管堆上分配記憶體給類的範例,類物件變數只保留對物件位置的參照,參照存放在棧中。

物件參照的改變

因為參照型別在儲存的時候是兩段記憶體,所以對於參照型別的物件的改變和值型別是不同的,以Customer類的兩個物件為例:

static void Main(string[] args)
{
    Customer c1 = new Customer("光頭強", "狗熊嶺", 30, "2305507");
    Customer c2 = c1;
    c1.Show();
    c2.Show();
    Console.WriteLine();
    c2.address = "團結屯";
    c1.Show();
    c2.Show();
    Console.ReadKey();
}

執行結果為:

名字:光頭強
地址:狗熊嶺
年齡:30
建立時間:2305507
名字:光頭強
地址:狗熊嶺
年齡:30
建立時間:2305507

名字:光頭強
地址:團結屯
年齡:30
建立時間:2305507
名字:光頭強
地址:團結屯
年齡:30
建立時間:2305507

可以發現當我們修改了物件s2中的address欄位以後s1也跟著發生了變化,之所以這樣和參照型別在記憶體中的儲存方式是密不可分的:

在建立s2時並沒有和建立s1一樣通過new來建立一個全新的物件,而是通過=賦值來的,因為參照型別儲存是二段儲存,所以賦值以後s2在棧中儲存的其實是s1物件在堆中的儲存空間的地址,所以修改s2的時候s1也會隨之變化,因為二者指向的是同一塊記憶體空間。如果你通過new關鍵字來範例化s2,那s2就是儲存的一個全新的Customer物件了。感興趣可以看看不同方式建立的s2物件在記憶體中的地址一不一樣。

static void Main(string[] args)
{
    Customer c1 = new Customer("光頭強", "狗熊嶺", 30, "2305507");
    Customer c2 = new Customer("大熊", "東京", 14, "2309856");
    Console.ReadKey();
}

這裡面的s1s2就儲存在兩段不同的記憶體中。

繼承

本篇文章的標題是「C# 物件導向」,但是,C#並不是一種純粹的物件導向程式語言,C#中還包含一些非物件導向的特性,比如靜態成員、靜態方法和值型別等,還支援一些其他的程式設計正規化,比如泛型程式設計、非同步程式設計和函數語言程式設計。雖然但是,物件導向仍然是C#中的一個重要概念,也是.NET提供的所有庫的核心原則。

物件導向程式設計有四項基本原則:

  • 抽象:將實體的相關特性和互動建模為類,以定義系統的抽象表示。
  • 封裝:隱藏物件的內部狀態和功能,並僅允許通過一組公共函數進行存取。
  • 繼承:根據現有抽象建立新抽象的能力。
  • 多形性:跨多個抽象以不同方式實現繼承屬性或方法的能力。【多型性】

在我們學習和使用類的過程中都或多或少在應用抽象、封裝這些概念,或者說這些思想,我們之前都是在使用單個的某一個類,但在開發過程中,我們往往會遇到這樣一種情況:很多我們宣告的類中都有相似的資料,比如一個遊戲,裡面有Boss類、Enermy類,這些類有很多相同的屬性,但是也有不同的,比方說BossEnermy都會飛龍在天,但是Boss還會烏鴉坐飛機這種高階技能等等,這個時候我們可以如果按照我們之前的思路,分別編寫了兩個類,假如飛龍在天的技能被「聰明的」策劃廢棄了或者調整了引數,我們在維護起來是很不方便的,這個時候就可以使用繼承來解決這個問題,它有父類別和子類,相同的部分放在父類別裡就可以了。

繼承的型別:

  • 由類實現繼承:

    表示一個型別派生於一個基本類型,它擁有該基本類型的所有成員欄位和函數。在實現繼承中,派生型別採用基本類型的每個函數的實現程式碼,除非在派生型別的定義中指定重寫某個函數的實現程式碼。在需要給現有的型別新增功能,或許多相關的型別共用一組重要的公共功能時,這種型別的繼承非常有用。

  • 由介面實現繼承:

    表示一個型別只繼承了函數的簽名,沒有繼承任何實現程式碼。在需要指定該型別具有某些可用的特性時,最好使用這種型別的繼承。

細說的話,繼承有單重繼承和多重繼承,單重繼承就是一個類派生自一個基礎類別(C#就是採用這種繼承),多重繼承就是一個類派生自多個類。

派生類也稱為子類(subclass);父類別、基礎類別也稱為超類(superclass)。

一些語言(例如C++)是支援所謂的「多重繼承」的,但是關於多重繼承是有爭議的:一方面,多重繼承可以編寫更為複雜且較為緊湊的程式碼;另一方面,使用多重繼承編寫的程式碼一般很難理解和偵錯,也會產生一定的開銷。C#的重要設計目標就是簡化健壯程式碼,所以C#的設計人員決定不支援多重繼承。一般情況下,不使用多重繼承也是可以解決我們的問題的,所以很多程式語言,尤其是高階程式語言就不支援多重繼承了。

雖然C#不支援多重繼承,但是C#是允許一個類派生自多個介面的,這個後面章節再展開論述。

只需要知道,C#中的類可以通過繼承另一個類來對自身進行拓展或客製化,子類可以繼承父類別的所有函數成員和欄位(繼承父類別的所有功能而無需重新構建),一個類只能有一個基礎類別(父類別),而且它只能繼承自唯一一個父類別❗但是,一個類可以被多個類繼承,這會使得類之間產生一定的層次,也被稱為多層繼承C#支援,並且很常用)。到這,你可能會想到,我們之前寫的宣告Customer類啊或者Vehicle啊它們有父類別嘛❓答案當然是有的。就像在值型別、參照型別所說的,所有型別都有一個基本類型就是Object類,當然了Object可沒有基礎類別,不能套娃嘛不是