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
類,具有Run
、Stop
等方法,具有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
中的SetX
、SetY
和 SetZ
方法是普通的方法,而不是屬性。它們僅僅是修改和存取範例中私有欄位的方法。它們需要一個引數才能設定相應的欄位值,而屬性是通過存取器方法來設定或獲取欄位的值,並且不需要額外的引數。
public
修飾的資料成員和成員函數是公開的,所有的使用者都可以進行呼叫。private
修飾詞修飾的成員變數以及成員方法只供本類使用,也就是私有的,其他使用者是不可呼叫的。public
和private
這兩個修飾符其實從字面意思就可以理解,沒什麼不好理解的,前者修飾的欄位大家可以隨意操作,千刀萬剮只要你樂意,而後者修飾的欄位就不能任你宰割了,你只能通過Get
、Set
進行一系列的存取或者修改。
舉個例子,生活中每個人都有名字、性別,同時也有自己的銀行卡密碼,當別人跟你打交道的時候,他一般會先得知你的名字,性別,這些告訴他是無可厚非的,但是當他想知道你的銀行卡密碼的時候就不太合適了對吧。假設我們有一個類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
只能通過Get
、Set
來操作。
日常開發推薦不要把欄位設定為共有的,至少要有點存取限制,當然了除了這兩個修飾符,還有其他的,比如internal
、protect
等等,以後的文章可能會專門來寫(❓)。
使用private
修飾符除了多了一堆屬性(存取器)有什麼便利嗎?顯然得有,public
的欄位你在設定的時候說啥就啥,即使它給到的內容可能不適合這個欄位,在後者,我們可以在屬性裡設定一些限制或者是操作。比如,Vector3
類的x
欄位顯然長度是不會出現負值的,這時候我們就可以在SetX
裡面做些限制:
public void SetX(double temp)
{
if (temp<0)
{
Console.WriteLine("資料不合法。");
}
x = temp;
}
對於不想讓外界存取的資訊我們可以不提供Get
屬性以起到保護作用。
建構函式 - 也被稱為「構造器」,是執行類或結構體的初始化程式碼。每當我們建立類或者結構體的範例的時候,就會呼叫它的建構函式。大家可能會疑惑,我們上面建立的類裡面也沒說這個建構函式這個東東啊,那是因為如果一個類沒有顯式範例建構函式,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
類中我們是通過編寫Get
、Set
方法來存取或者修改欄位的資料,這樣在實際開發中是很麻煩的,會降低我們的效率而且使用起來我們必須通過呼叫這兩個方法來實現對私有欄位的操作:
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);
}
}
我們可以通過屬性來快捷實現對私有欄位的存取以及修改,通過get
、set
存取器操作私有欄位的值。
❓什麼是屬性呢
屬性是一種成員,它提供靈活的機制來讀取、寫入或計算私有欄位的值。 屬性可用作公共資料成員,但它們是稱為「存取器」的特殊方法。 此功能使得可以輕鬆存取資料,還有助於提高方法的安全性和靈活性。
屬性允許類公開獲取和設定值的公共方法,而隱藏實現或驗證程式碼。
屬性可以是讀-寫屬性(既有 get
存取器又有 set
存取器)、唯讀屬性(有 get
存取器,但沒有 set
存取器)或只寫存取器(有 set
存取器,但沒有 get
存取器)。 只寫屬性很少出現,常用於限制對敏感資料的存取。
不需要自定義存取器程式碼的簡單屬性可以作為表示式主體定義或自動實現的屬性來實現。
上面的SetAge
和GetAge
方法我們用屬性替換掉就是:
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
引入的新特性,它可以讓我們在不顯式定義欄位和存取器方法的情況下快速定義一個屬性。具體來說,一個屬性包含一個欄位和兩個存取器方法,其中get
和set
存取器方法都是自動實現的。
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
關鍵字來參照,因為它並沒有一個名字。
程式在執行時,記憶體一般從邏輯上分為兩大塊 - 堆、棧。
堆和棧就相當於倉庫和商店,倉庫放的東西多,但是當我們需要裡面的東西時需要去裡面自行查詢然後取出來,後者雖然存放的東西沒有前者多,但是好在隨拿隨取,方便快捷。
棧是一種先進後出(Last-In-First-Out,LIFO)的資料結構。本質上講堆疊也是一種線性結構,符合線性結構的基本特點:即每個節點有且只有一個前驅節點和一個後續節點。
堆是一塊記憶體區域,與棧不同,堆裡的記憶體可以以任意順序存入和移除。
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 等。這種執行環境為程式提供了許多優勢,例如:
- 自動記憶體管理:託管執行環境為程式管理記憶體分配和釋放,程式設計師無需手動管理記憶體,避免了記憶體漏失和越界等問題。
- 垃圾回收:託管執行環境提供了垃圾回收服務,自動回收不再使用的記憶體,提高了程式的效能和可靠性。
- 型別檢查:託管執行環境提供了強型別檢查,防止了型別錯誤等問題。
- 平臺無關性:託管語言編寫的程式可以在不同作業系統和硬體平臺上執行,提高了程式的可移植性。
在CLR
中:
既然垃圾回收是自動進行的,那麼一般什麼時候GC
會開始回收垃圾呢?
我們開發人員可以使用new
關鍵字在託管堆上動態分配記憶體,不需要手動釋放,GC
會定期檢查託管堆上的物件,並回收掉沒有被參照的物件,從而釋放它們所佔用的記憶體。
❗❗❗需要注意的是,棧記憶體無需我們管理,同時它也不受
GC
管理。當棧頂元素使用完畢以後,所佔用的記憶體會被立刻釋放。而堆則需要依賴於GC
清理。
文章之前部分已經提到過C#
是託管語言,在託管執行環境中執行的程式語言,該環境提供了強型別檢查,所以與其他語言相比,C#
對其可用的型別及其定義有更嚴格的描述 ———— C#
是一種強型別語言,每個變數和常數都有一個型別,每個求值的表示式也是如此。 每個方法宣告都為每個輸入引數和返回值指定名稱、型別和種類(值、參照或輸出)。
所有的C#
型別可以分為以下幾類:
值型別
參照型別
泛型型別
C#泛型可以是值型別也可以是參照型別,具體取決於泛型引數的型別。
如果泛型引數是值型別,那麼範例化出來的泛型型別也是值型別。例如,
List<int>
就是一個值型別,因為int
是值型別。如果泛型引數是參照型別,那麼範例化出來的泛型型別也是參照型別。例如,
List<string>
就是一個參照型別,因為string
是參照型別。需要注意的是,雖然泛型型別可以是值型別或參照型別,但是泛型型別的範例總是參照型別。這是因為在記憶體中,泛型型別的範例始終是在堆上分配的,無論它的泛型引數是值型別還是參照型別。因此,使用泛型型別時需要注意它的範例是參照型別。
指標型別
指標型別是C#中的一種高階語言特性,允許程式設計師直接操作記憶體地址。指標型別主要用於與非受控程式碼互動、實現底層資料結構等。指標型別在普通的C#程式碼中並不常見。
撇去指標型別,我們可以把C#
中的資料型別分為兩種:
struct
和enum
,包括內建的數值型別(所有的數值型別、char
型別和bool
型別)以及自定義的struct
型別和enum
型別。C#
支援兩種預定義的參照型別:object
和string
。❗❗❗
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
陣列首地址的參照,array2
和customer
同理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();
}
這裡面的s1
和s2
就儲存在兩段不同的記憶體中。
本篇文章的標題是「C# 物件導向」,但是,C#
並不是一種純粹的物件導向程式語言,C#
中還包含一些非物件導向的特性,比如靜態成員、靜態方法和值型別等,還支援一些其他的程式設計正規化,比如泛型程式設計、非同步程式設計和函數語言程式設計。雖然但是,物件導向仍然是C#
中的一個重要概念,也是.NET
提供的所有庫的核心原則。
物件導向程式設計有四項基本原則:
在我們學習和使用類的過程中都或多或少在應用抽象、封裝這些概念,或者說這些思想,我們之前都是在使用單個的某一個類,但在開發過程中,我們往往會遇到這樣一種情況:很多我們宣告的類中都有相似的資料,比如一個遊戲,裡面有Boss
類、Enermy
類,這些類有很多相同的屬性,但是也有不同的,比方說Boss
和Enermy
都會飛龍在天,但是Boss
還會烏鴉坐飛機這種高階技能等等,這個時候我們可以如果按照我們之前的思路,分別編寫了兩個類,假如飛龍在天的技能被「聰明的」策劃廢棄了或者調整了引數,我們在維護起來是很不方便的,這個時候就可以使用繼承來解決這個問題,它有父類別和子類,相同的部分放在父類別裡就可以了。
繼承的型別:
由類實現繼承:
表示一個型別派生於一個基本類型,它擁有該基本類型的所有成員欄位和函數。在實現繼承中,派生型別採用基本類型的每個函數的實現程式碼,除非在派生型別的定義中指定重寫某個函數的實現程式碼。在需要給現有的型別新增功能,或許多相關的型別共用一組重要的公共功能時,這種型別的繼承非常有用。
由介面實現繼承:
表示一個型別只繼承了函數的簽名,沒有繼承任何實現程式碼。在需要指定該型別具有某些可用的特性時,最好使用這種型別的繼承。
細說的話,繼承有單重繼承和多重繼承,單重繼承就是一個類派生自一個基礎類別(C#
就是採用這種繼承),多重繼承就是一個類派生自多個類。
派生類也稱為子類(subclass);父類別、基礎類別也稱為超類(superclass)。
一些語言(例如C++
)是支援所謂的「多重繼承」的,但是關於多重繼承是有爭議的:一方面,多重繼承可以編寫更為複雜且較為緊湊的程式碼;另一方面,使用多重繼承編寫的程式碼一般很難理解和偵錯,也會產生一定的開銷。C#
的重要設計目標就是簡化健壯程式碼,所以C#
的設計人員決定不支援多重繼承。一般情況下,不使用多重繼承也是可以解決我們的問題的,所以很多程式語言,尤其是高階程式語言就不支援多重繼承了。
雖然C#
不支援多重繼承,但是C#
是允許一個類派生自多個介面的,這個後面章節再展開論述。
只需要知道,C#
中的類可以通過繼承另一個類來對自身進行拓展或客製化,子類可以繼承父類別的所有函數成員和欄位(繼承父類別的所有功能而無需重新構建),一個類只能有一個基礎類別(父類別),而且它只能繼承自唯一一個父類別❗但是,一個類可以被多個類繼承,這會使得類之間產生一定的層次,也被稱為多層繼承(C#
支援,並且很常用)。到這,你可能會想到,我們之前寫的宣告Customer
類啊或者Vehicle
啊它們有父類別嘛❓答案當然是有的。就像在值型別、參照型別所說的,所有型別都有一個基本類型就是Object
類,當然了Object
可沒有基礎類別,不能套娃嘛不是