前言
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 訪問修飾符
- 訪問修飾符(C# 編程指南)
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# 編程指南)
建構式 - 也被稱為“構造器”,是執行類或結構體的初始化代碼,每當我們創建類或者結構體的實體的時候,就會呼叫它的建構式,大家可能會疑惑,我們上面創建的類里面也沒說這個建構式這個東東啊,那是因為如果一個類沒有顯式實體建構式,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
關鍵字來參考,因為它并沒有一個名字,
堆、堆疊
程式在運行時,記憶體一般從邏輯上分為兩大塊 - 堆、堆疊,
- 堆疊(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 等,這種執行環境為程式提供了許多優勢,例如:
- 自動記憶體管理:托管執行環境為程式管理記憶體分配和釋放,程式員無需手動管理記憶體,避免了記憶體泄漏和越界等問題,
- 垃圾回收:托管執行環境提供了垃圾回收服務,自動回收不再使用的記憶體,提高了程式的性能和可靠性,
- 型別檢查:托管執行環境提供了強型別檢查,防止了型別錯誤等問題,
- 平臺無關性:托管語言撰寫的程式可以在不同作業系統和硬體平臺上運行,提高了程式的可移植性,
在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#
中的資料型別分為兩種:
- 值型別 - 分兩類:
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
可沒有基類,不能套娃嘛不是??
實作繼承
接著上面的游戲案例:
- 基類:敵人類 -
hp
speed
AI()
Move()
- 派生類:
boss
、type1enemy
、type2enemy
基類(父類):
class Enemy
{
protected int hp;
protected int speed;
public void AI() { Console.WriteLine("AI"); }
public void Move() { Console.WriteLine("Move"); }
}
protected
:僅允許在包含類或者子類中訪問
派生類Boss
(子類):
class Boss : Enemy
{
private int attack; // Boss的攻擊力比普通小兵攻擊力高
public Boss(int attack)
{
this.attack = attack;
}
public void Skill() { Console.WriteLine("Boss Skill"); }
public void Print()
{
Console.WriteLine("HP:"+hp);
Console.WriteLine("Speed:" + speed);
Console.WriteLine("Attack:" + attack);
}
}
創建一個Boss
物件,看一下:
static void Main(string[] args)
{
Boss boss = new Boss(100);
boss.Print();
//Output:
//HP:0
//Speed:0
//Attack:100
Console.ReadKey();
}
雖然可以訪問基類的欄位,但是在創建物件的時候是沒有賦值的,使用的是默認值,那怎樣才能在創建物件的時候也給基類的欄位賦值呢?
this
關鍵字指代類的當前實體嘛:
class Boss : Enemy
{
private int attack;
public Boss(int attack,int hp,int speed)
{
this.attack = attack;
this.hp = hp;
this.speed = speed;
}
public void Skill() { Console.WriteLine("Boss Skill"); }
public void Print()
{
Console.WriteLine("HP:"+hp);
Console.WriteLine("Speed:" + speed);
Console.WriteLine("Attack:" + attack);
}
}
用this
當然是而可行的,但是除了this
之外,還有一個專門的關鍵字來幫助我們從派生類中訪問基類成員 - base
,
this 和 base 關鍵字
this
:代指當前實體本身,可避免欄位、區域變數或屬性之間發生混淆,this
參考僅在類或結構體的非靜態成員中有效,
base
:用于從派生類中訪問基類成員,它有兩個重要作用:
? - 呼叫基類上已被其他方法重寫的方法,
? - 指定創建派生類實體時應呼叫的基類建構式,
使用base
關鍵字的建構式:
private int attack;
public Boss(int attack,int hp,int speed)
{
this.attack = attack;
base.hp = hp;
base.speed = speed;
}
有什么好處呢,加入派生類中的欄位和基類中的欄位一樣時,就可以通過這種方式來避免混淆,假設我們的派生類Boss
里面也有個hp
的欄位:
class Boss : Enemy
{
private int attack;
private int hp;
public Boss(int attack,int hp,int speed)
{
this.attack = attack;
//this.hp = hp;
//this.speed = speed;
base.hp = hp;
base.speed = speed;
}
public void Skill() { Console.WriteLine("Boss Skill"); }
public void Print()
{
Console.WriteLine("HP:"+hp);
Console.WriteLine("Base.HP:"+base.hp);
Console.WriteLine("Speed:" + speed);
Console.WriteLine("Attack:" + attack);
}
}
我們再創建一個實體:
傳進去的hp
的值是根據 base.hp = hp;
給到了基類中的hp
欄位,但是一般情況下,不推薦派生類中和基類重名的欄位,一般在子類重寫父類方法時通過base
關鍵字區分,就是上面說的第一種應用,
通過base
關鍵字是沒辦法訪問attck
欄位的,base.attack
會報錯,
base
所訪問的基類是類宣告中指定的基類,不能是多級訪問,
多載和重寫
多載(Overloading):指的是在同一個類中定義多個具有相同名稱但引數串列不同的方法,通過多載,可以在同一個類中創建多個方法,它們執行相似的操作但接受不同型別或數量的引數,編譯器會根據呼叫時提供的引數型別和數量來確定呼叫哪個多載方法,多載可以提高代碼的可讀性和靈活性,允許在不同的情況下使用相同的方法名進行不同的操作,
例如,在一個名為Calculator
的類中可以定義多個名為add
的方法,如下所示:
class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
public double Add(double a, double b)
{
return a + b;
}
}
上述代碼中,Calculator
類定義了兩個多載的add
方法,一個用于整數相加,另一個用于浮點數相加,根據提供的引數型別,編譯器會選擇適合的多載方法,
??????注意
方法簽名由方法名、引數數量和引數型別共同決定,方法的回傳型別不計入簽名,兩個同名方法如果獲取相同的引數串列,就說它們有相同的簽名,即使它們的回傳型別不同,
重寫(Overriding):指的是子類重新實作(覆寫)了從父類繼承的方法,以改變方法的行為,當子類需要修改父類中的方法實作時,可以使用方法重寫,在重寫程序中,子類需要使用相同的方法名稱、相同的引數串列和相同或更寬松的訪問修飾符來重新定義父類的方法,重寫方法可以提供特定于子類的實作,
以下是一個簡單的例子,展示了父類Animal
和子類Cat
之間的方法重寫:
class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("Animal makes sound");
}
}
class Cat : Animal
{
public override void MakeSound()
{
Console.WriteLine("Cat meows");
}
}
在上述代碼中,Animal
類定義了一個名為MakeSound
的方法,而Cat
類繼承自Animal
類并重寫了MakeSound
方法,當呼叫MakeSound
方法時,如果物件是Cat
類的實體,將執行Cat
類中的重寫方法,輸出"Cat meows";否則,將執行父類Animal
的原始方法,輸出"Animal makes sound",
總結來說,多載用于在同一個類中定義多個具有相同名稱但引數串列不同的方法,以便在不同情況下執行不同的操作,而重寫用于子類重新實作(覆寫)繼承自父類的方法,以改變方法的行為,
我感覺這倆兄弟八竿子打不著,但是我一開始理解錯了,所以Mark
一下??????
隱藏成員
編程最困難的地方之一是為識別符號想一個獨特的、有意義的名稱,如果基類和派生類同時宣告了兩個具有相同簽名的方法,編譯時會顯示一個警告,比如下面的例子中,當我們在Boss
類中重新定義一個Move
方法,編譯器會顯示警告訊息指出Boss.Move()
隱藏了繼承的成員Enemy.Move()
,也就是說Boss
繼承的基類Enemy
的Move
方法就會被隱藏掉:
這個時候創建一個Boss
物件并呼叫Move
方法會是在Boss
類中重新定義的方法:
雖然代碼能編譯并運行,但是應該嚴肅對待該警告,如果有另一個派生自Boss
類并呼叫了Move
方法,它想要呼叫的可能是Enemy
類實作的Move
方法,但是這被Boss
類中的Move
隱藏了,所以實際呼叫的是后者,大多數時候,像這樣的巧合會成為混亂之源,應重命名方法以免沖突,但如果確實希望兩個方法具有相同簽名,從而隱藏Enemy
的Move
方法,可明確使用new
關鍵字消除警告,告訴編譯器我知道自己在干什么:
class Boss : Enemy
{
public int attack; // Boss的攻擊力比普通小兵攻擊力高
public Boss(int attack, int hp, int speed)
{
this.attack = attack;
base.hp = hp;
base.speed = speed;
}
public new void Move()
{
Console.WriteLine("Boss Move");
}
...
虛方法
什么是虛方法?
虛方法(Virtual method)是一種允許子類重寫(覆寫)父類以實作方法的方法,通過宣告方法為虛方法,可以在父類中定義一個方法,并允許子類通過重寫該方法來提供自己的實作,在父類中,可以使用 virtual
關鍵字來宣告一個方法為虛方法,虛方法允許子類通過使用 override
關鍵字來重寫該方法,以便子類可以根據自身的需要改變方法的行為,
為什么需要虛方法?
為了隱藏方法在基類中的實作??這么說可能比較不好理解,我們舉個例子,在開始學習C#
的時候大家可能會注意到C#
中萬物都可ToString()
,任何型別的實體都可以通過呼叫ToString
方法將自身轉換為一個字串,這得益于C#
擁有統一的型別系統,其所有型別都共享一個公共的基類Object
,因為它很有用所以設計者把它作為Object
的成員自動提供給所有類,我們看看boss
實體在字串之后會輸出什么:
??輸出內容并不盡如人意,那ToString
是如何將實體轉換成字串的呢?派生類(指的是所有類都派生于基類System.Object
)中可能包含任意數量的欄位,這些欄位包含的值應該是字串的一部分,但是System.Object
中實作的ToString
太過于簡單,它唯一能做的就是將物件轉化成其型別名稱的字串,就像圖片中輸出的那樣CSharpTutorial_01.Program+Boss
,這種轉換是毫無意義的,那為什么要提供一個沒有用的方法呢?
顯然,ToString
是一個很好的概念,所有類都應當提供一個方法將物件轉換成字串,以便于查看和除錯,事實上,System.Object
中實作的ToString
只是一個“占位符”,我們應該在每一個自定義類中提供自己的ToString
方法 ———— 重寫基類System.Object
中的ToString
方法,
我們選中ToString
進入到定義可以發現,在基類System.Object
中ToString
故意設計成要被重寫的帶有virtual
的虛方法,

接下來我們在Boss
中實作我們自己的ToString
方法(當你在VS中拼完override`關鍵字后智能提示會顯示當前可重寫的方法串列,并且選中后會有一個默認實作):
我們通過該方法顯示當前物件的hp
、attack
、speed
:
class Boss : Enemy
{
private int attack;
private int hp;
public Boss(int attack, int hp, int speed)
{
this.attack = attack;
base.hp = hp;
base.speed = speed;
}
// . . .
public override string ToString()
{
return "HP: " + base.hp + " " + "Attack: " + this.attack + " " + "Speed: " + base.speed;
}
}
故意設計成要被重寫的方法稱為虛(virtual)方法,“重寫方法”和“隱藏方法(隱藏成員)”的區別現在應該很明顯了,重寫是提供同一個方法的不同實作,這些方法有關系,因為都旨在完成相同的任務,只是不同的類用不同的方式,但隱藏是指方法被替換成另一個方法,方法通常沒關系,而且可能執行完全不同的任務,對方法進行重寫是有用的編程概念:而如果方法被隱藏,則意味著可能發生了一處編程錯誤(除非你加上new
強調自己沒錯),
Object.ToString 方法
虛方法和多型性
多型性(Polymorphism)是面向物件編程中的一個重要概念,指的是在運行時能夠根據物件的實際型別來執行不同的操作,
在多型性中,重要的概念是虛方法和方法重寫,通過在父類中宣告虛方法,并在子類中使用 override
關鍵字重寫該方法,可以實作多型性,當使用基型別別的參考變數參考派生類物件并呼叫虛方法時,實際上會根據物件的實際型別來選擇執行哪個重寫方法,
多型性允許使用基型別別的參考變數來參考派生類物件,并根據物件的實際型別來呼叫相應的方法,這使得我們可以在不同的物件上執行相同的操作,而不需要針對每個具體的物件型別撰寫單獨的代碼,這樣做的好處是,在撰寫代碼時,我們不需要針對每個具體的物件型別撰寫單獨的代碼,我們可以使用一個通用的代碼塊來處理所有派生類物件,只需要使用基型別別的參考變數來參考它們,并呼叫相同的方法,
接著上面的游戲案例,除了boss
不是還有type1enemy
、type2enemy
兩個派生類嗎,假如說boss
、type1enemy
、type2enemy
都有各自不同于基類Enemy
的行動方式,也就是Move
方法,這時候就可以用到多型性():
class Enemy
{
protected int hp;
protected int speed;
public void AI() { Console.WriteLine("AI"); }
public virtual void Move() { Console.WriteLine("Move"); }
}
class Boss : Enemy
{
public int attack; // Boss的攻擊力比普通小兵攻擊力高
public override void Move()
{
Console.WriteLine("Boss Move");
}
}
class Type1enemy : Enemy
{
public override void Move()
{
Console.WriteLine("type1enemy Move");
}
}
class Type2enemy : Enemy
{
public override void Move()
{
Console.WriteLine("type2enemy Move");
}
}
通過多型性,我們可以使用基型別別的參考變數來參考不同派生類的物件,然后呼叫它們的Move()
方法:
internal class Program
{
static void Main(string[] args)
{
Enemy boss = new Boss();
boss.Move();
Enemy enemy1 = new Type1enemy();
enemy1.Move();
enemy1 = new Type2enemy();
enemy1.Move();
Console.ReadKey();
}
}
執行結果為:
Boss Move
type1enemy Move
type2enemy Move
這種方式使得我們的代碼更加靈活和可擴展,當我們新增一個派生類時,只需要讓它繼承自基類并重寫基類中的方法,然后我們就可以在通用的代碼中使用基型別別的參考變數來參考新的派生類物件,并呼叫相同的方法,無需修改通用的代碼塊,
總結來說,多型性通過使用基型別別的參考變數來參考派生類物件,并根據物件的實際型別來選擇執行相應的方法,實作了在不同的物件上執行相同操作的便利性,減少了代碼的重復和冗余,
抽象方法和抽象類
抽象類(Abstract class)是一種特殊的類,它不能被實體化,只能被用作其他類的基類,抽象類用于定義一組相關的類的公共結構和行為,并可以包含抽象成員(抽象方法、抽象屬性等)和非抽象成員,
抽象類通過在類定義前面加上 abstract
關鍵字來宣告,抽象類可以包含普通方法的實作和抽象方法的定義,抽象方法是沒有具體實作的方法,只有方法的簽名(回傳型別、方法名和引數串列),并且在派生類中必須進行重寫,顯然,抽象方法本身也是虛擬Virtual
的(雖然不需要提供virtual
關鍵字,事實上,如果寫了該關鍵字,程式會產生一個語法錯誤??),如果類包含抽象方法,那么這個類就是抽象的,并且必須宣告為抽象類,
那么什么時候用到抽象類呢?繼續之前的游戲案例,敵人有很多種,都會攻擊,但是每個人的攻擊方式都不一樣,在父類Enemy
中宣告的Attack
來表示攻擊,這個時候我們在父類里闡述清楚關于Attack
的詳細定義是沒有用處的,因為我們都知道每個敵人的攻擊方式是不一樣的,即使我們宣告定義好也需要在子類中根據子類的種類來定義不同的攻擊方式,這個時候,我們只需要在父類中有一個“攻擊方式”的占位符就行了,不必要具體實作,就和上面說的ToString
方法類似(當然也是有些細微區別的哈),
如果一個方法在抽象類中提供默認實作沒有意義,但有需要派生類提供該方法夫人實現,那這個方法就適合定義成抽象方法:
public abstract void Attack();
這樣寫了之后我們會發現編譯器會報錯:
這就是上面說的,如果類包含抽象方法,那么這個類就是抽象類,并且必須宣告為抽象類:
abstract class Enemy
{
public abstract void Attack();
}
可以認為,抽象方法是不完整的,因為它包含的抽象成員是不完整的,需要在每個派生類中完成定義,當創建抽象類的實體時是錯誤的,因為類中的成員是不完整的,當然了宣告一個Enemy
物件是可以的,但是實體化不可以,可以通過它的派生類來完成構造,
我們完善一下Enemy
類內容:
abstract class Enemy
{
private int hp;
private int speed;
public void Move() { Console.WriteLine("Move"); }
public abstract void Attack();
}
接著來實作一個Boss
派生類,按照以往的繼承方法Boss
在繼承基類Enemy
后會報錯:
這個時候我們可以通過那個小燈泡??快速實作抽象類,然后實作各自的方法就可以了:
class Boss : Enemy
{
public override void Attack()
{
Console.WriteLine("Boss Attack");
}
}
以面向物件思想,重復的代碼是警告的信號,應該重構以免重復并減少維護開銷,
密封類和密封方法
實際開發中使用較少,但是語法簡單,/
如果不想一個類作為基類,可以使用sealed(密封)
關鍵字防止類被用作基類,如果你不想讓子類重寫某個方法,可以添加sealed(密封)
來防止子類重寫該方法,
- 防止代碼混亂
- 商業原因
派生類的建構式
前面章節中我們介紹過單個類的建構式如果定義以及如何作業的,還有在繼承的時候如果通過base
關鍵字訪問基類欄位完成初始化,我們知道,繼承除了得到方法派生類還會自動包含來自基類的所有欄位,這寫欄位通常需要初始化,此外,所有類都至少有一個構造器(沒有顯示宣告的話,編譯器會自動生成一個無參的默認構造器),當我們在派生類定義建構式的時候可以通過base
關鍵字呼叫基類的建構式:
class BaseClass
{
public BaseClass()
{
Console.WriteLine("基類 建構式");
}
}
class DrivedClass : BaseClass
{
public DrivedClass() :base()
{
Console.WriteLine("派生類 建構式");
}
}
初始化一個DrivedClass
實體后先后呼叫,注意是先呼叫基類的建構式:
如果我們不寫base()
也會呼叫基類的建構式的,因為你繼承了,,,
上面是無參的情況,那有參的情況呢?
class BaseClass
{
private string name;
private string description;
public BaseClass()
{
Console.WriteLine("基類 建構式");
}
public BaseClass(string name, string description)
{
this.name = name;
this.description = description;
}
}
class DrivedClass : BaseClass
{
private int age;
public DrivedClass(int age,string name,string description):base(name,description)
{
this.age = age;
}
public DrivedClass() :base()
{
Console.WriteLine("派生類 建構式");
}
}
修飾符
- 修飾符
前面遇到過很多修飾符,或修飾類、類成員,或指定方法得到可見性,又或是指定其本質 - virtual
、abstract
,C#
有許多修飾符,
訪問修飾符
- public:同一程式集中的任何其他代碼或參考該程式集的其他程式集都可以訪問該型別或成員, 某一型別的公共成員的可訪問性水平由該型別本身的可訪問性級別控制,
- private:只有同一
class
或struct
中的代碼可以訪問該型別或成員, - protected:只有同一
class
或者從該class
派生的class
中的代碼可以訪問該型別或成員, - internal:同一程式集中的任何代碼都可以訪問該型別或成員,但其他程式集中的代碼不可以, 換句話說,
internal
型別或成員可以從屬于同一編譯的代碼中訪問, - protected internal:該型別或成員可由對其進行宣告的程式集或另一程式集中的派生
class
中的任何代碼訪問, - private protected:該型別或成員可以通過從
class
派生的型別訪問,這些型別在其包含程式集中進行宣告,
public
和private
修飾欄位和方法的時候,表示該欄位或者方法能不能通過物件去訪問,只有public
的才可以通過物件訪問,private(私有的)
只能在類內部訪問,protected保護的
,當沒有繼承的時候,它的作用和private
是一樣的,當有繼承的時候,protected
:表示可以被子類訪問的欄位或者方法
其他修飾符
-
abstract:使用
abstract
修飾的類為抽象類,抽象類只能是其他類的基類,不能與sealed
、static
一起使用,abstract
可以修飾抽象類中的方法或屬性,此時,方法或屬性不能包含實作,且訪問級別不能為私有,抽象類不能被實體化,
-
sealed:使用
sealed
修飾的類為密封類,密封類無法被繼承,不能和abstract
、static
一起使用,當
sealed
用于方法或屬性時,必須始終與override
一起使用, -
static:使用
static
修飾的類為靜態類,靜態類所有成員都必須是靜態的,不能與abstract
、sealed
一起使用,static
可以修飾方法、欄位、屬性或事件,始終通過類名而不是實體名稱訪問靜態成員,靜態欄位只有一個副本,靜態類不能被實體化,
-
const:使用
const
關鍵字來宣告某個常量欄位或常量區域變數,必須在宣告常量時賦初值,不能與
static
一起使用,常量默認是static
的,常量欄位只有一個副本, -
readonly:使用
readonly
關鍵字來宣告只讀欄位,只讀欄位可以在宣告或建構式中初始化,每個類或結構的實體都有一個獨立的副本,
可以與
static
一起使用,宣告靜態只讀欄位,靜態只讀欄位可以在宣告或靜態建構式中初始化,靜態常量欄位只有一個副本,
-
virtual:
virtual
關鍵字用于修飾方法、屬性、索引器或事件宣告,并使它們可以在派生類中被重寫,默認情況下,方法是非虛擬的, 不能重寫非虛方法,
virtual
修飾符不能與static
、abstract
、private
或override
修飾符一起使用, -
override:要擴展或修改繼承的方法、屬性、索引器或事件的抽象實作或虛實作,必須使用
override
修飾符,重寫的成員必須是
virtual
、abstract
或override
的,
C#常用修飾符
關于static
在C#
中,static
關鍵字用于宣告靜態成員,這意味著它們與類相關而不是與類的實體(物件)相關,以下是static
關鍵字的一些常見用法:
-
靜態欄位(Static Fields): 靜態欄位是與類相關聯的欄位,而不與類的實體相關聯,它們在類的所有實體之間共享相同的值,靜態欄位可以通過類名直接訪問,而無需創建類的實體,下面是一個靜態欄位的示例:
class Counter { public static int Count; // 靜態欄位 public Counter() { Count++; } } Console.WriteLine(Counter.Count); // 輸出:0 Counter counter1 = new Counter(); Console.WriteLine(Counter.Count); // 輸出:1 Counter counter2 = new Counter(); Console.WriteLine(Counter.Count); // 輸出:2
-
靜態方法(Static Methods): 靜態方法是屬于類而不是類的實體的方法,它們可以直接通過類名呼叫,無需創建類的實體,靜態方法通常用于執行與類相關的任務,而不需要訪問實體的狀態,下面是一個靜態方法的示例:
class MathUtils { public static int Add(int a, int b) // 靜態方法 { return a + b; } } int result = MathUtils.Add(5, 3); // 呼叫靜態方法 Console.WriteLine(result); // 輸出:8
??在靜態方法中只能訪問和使用靜態成員
-
靜態類(Static Classes): 靜態類是一種特殊型別的類,它只包含靜態成員,并且不能被實體化,靜態類通常用于提供一組相關的靜態方法和工具函式,不如
Math
類,下面是一個靜態類的示例:static class StringUtils { public static bool IsNullOrEmpty(string str) { return string.IsNullOrEmpty(str); } } bool isEmpty = StringUtils.IsNullOrEmpty(""); // 呼叫靜態方法 Console.WriteLine(isEmpty); // 輸出:True
請注意,靜態成員只能訪問其他靜態成員,不能直接訪問實體成員,而實體成員可以訪問靜態成員,
靜態欄位在記憶體中存盤在特定的位置,這取決于它們的訪問修飾符和作用域,
對于靜態欄位,它們的存盤位置有兩種情況:
- 靜態欄位存盤在靜態資料區(Static Data Area): 當靜態欄位是類的靜態成員時,它們存盤在靜態資料區,靜態資料區在程式啟動時分配,并在整個程式執行期間保持不變,靜態欄位的記憶體分配在程式開始運行時進行,當程式結束時,靜態資料區的記憶體會被釋放,
- 靜態欄位存盤在元資料區(Metadata Area): 當靜態欄位是類的常量成員(使用
const
修飾符)時,它們存盤在元資料區,元資料區是用于存盤型別資訊和常量的地方,它在程式編譯時就被確定,并隨著程式的執行一直存在,
無論靜態欄位存盤在靜態資料區還是元資料區,它們都具有全域可見性,可以在程式的任何地方訪問,
需要注意的是,靜態欄位是與類相關聯的,而不是與類的實體相關聯,這意味著所有類的實體共享相同的靜態欄位,它們在記憶體中只有一份副本,
此外,靜態欄位的生命周期和應用程式的生命周期一致,它們在應用程式啟動時初始化,并在應用程式關閉時銷毀,
介面
介面(Interface)是一種在C#
中定義協定(Contract)的方式,它描述了類或結構體應該具有的成員(方法、屬性、事件等),介面定義了一組公共行為,但不提供實作細節,
介面在很多方面和抽象類類似,宣告介面在語法上和宣告抽象類完全相同,但是在介面中不允許提供任何成員的實作方式,它是純抽象的,此外,介面既不能有建構式也不能有欄位,介面的定義也不允許有運算子的多載,介面成員總是隱式Public
的,也不能宣告成員的修飾符,比如Virtual
,如果需要的話,應該由實作的類來完成,
??介面和抽象類在建構式上是不同的,雖然二者都不能創建其對應的實體,但是抽象類可以定義建構式,只不過只能在創建子類實體時才可以呼叫,
在C#
中,介面使用 interface
關鍵字定義,可以包含方法、屬性、事件和索引器等成員的宣告,介面中的成員默認是公共的,并且不能包含欄位或實作代碼,類可以實作一個或多個介面,表示類承諾實作介面中定義的所有成員,
介面名稱前面一般添加 "I" 來表示介面的特殊性,這是一種常見的命名約定,例如,IShape
表示一個形狀介面,
以下是一個簡單的介面示例,另外很多操作我們都可以借助VS的Intelligence
快速完成:

namespace CSharpTutorial_01
{
internal class Program
{
static void Main(string[] args)
{
Eagle eagle = new Eagle();
eagle.Fly();
eagle.FlyAttack();
IFly fly = new Eagle();
fly.Fly();
fly.FlyAttack();
fly = new Bird();
fly .Fly();
fly.FlyAttack();
Console.ReadKey();
}
}
interface IFly
{
void Fly(); // 飛翔
void FlyAttack(); // 虛空打擊
}
class Eagle : IFly
{
public void Fly()
{
Console.WriteLine("飛鷹展翅");
}
public void FlyAttack()
{
Console.WriteLine("龍卷風摧毀停車場");
}
}
class Bird : IFly
{
public void Fly()
{
Console.WriteLine("怒鴉飛行");
}
public void FlyAttack()
{
Console.WriteLine("烏鴉坐飛機");
}
}
}
IFly
相當簡單的介面,只定義了兩個方法,大多數介面都包含許多成員,執行結果:

某個模塊需要包含若干個功能,這個時候就可以將這些功能放在一個介面中,如果某個類想要擁有這個功能的話就自行去實作這個介面就可以了,
介面的繼承
介面可以彼此繼承,其方式與類的繼承方式相同,哦,對了,記得C#
里派生類只能繼承自一個基類,但是可以繼承多個介面也就是多介面實作,只要老老實實實作每個介面就行啦,介面繼承語法很簡單,繼承哪個介面就:Ixxxxx
就可以了,只要在實作這個介面的那個類里面需要把繼承的介面也實作:
internal class Program
{
static void Main(string[] args)
{
Pterosaur pterosaur = new Pterosaur();
pterosaur.Run();
pterosaur.Fly();
Console.ReadKey();
}
}
interface IFly
{
void Fly(); // 飛翔
void FlyAttack(); // 虛空打擊
}
interface IRun:IFly
{
void Run();
}
class Pterosaur : IRun
{
public void Fly()
{
Console.WriteLine("Fly");
}
public void FlyAttack()
{
Console.WriteLine("FlyAttack");
}
public void Run()
{
Console.WriteLine("Run");
}
}
介面繼承不太常用,
索引器
不太常用
索引器(Indexer)允許通過類實體的類似陣列的語法來訪問物件的元素,索引器允許在類內部定義一個特殊的訪問器(Getter
和Setter
),通過索引引數來獲取或設定物件中的元素,
使用索引器,可以像使用陣列一樣通過索引訪問物件中的元素,這使得物件可以按照一定的順序組織和訪問資料,索引器提供了一種方便的方式來訪問和操作物件的元素,增加了代碼的可讀性和易用性,
以下是一個使用索引器的簡單示例:
class MyList
{
private int[] data;
public MyList()
{
data = https://www.cnblogs.com/BoiledYakult/archive/2023/05/23/new int[10];
}
// 索引器的定義
public int this[int index]
{
get
{
return data[index];
}
set
{
data[index] = value;
}
}
}
// 使用索引器訪問物件的元素
MyList myList = new MyList();
myList[0] = 1; // 設定索引為0的元素的值
int value = myList[0]; // 獲取索引為0的元素的值
在上面的示例中,MyList
類定義了一個索引器,它允許通過整數索引訪問內部的 data
陣列,索引器的訪問器使用 get
和 set
關鍵字來定義獲取和設定元素的邏輯,
通過使用索引器,可以通過類似 myList[0]
的語法來訪問 MyList
物件中的元素,就像訪問陣列元素一樣,在索引器的背后,實際上是呼叫了索引器的訪問器方法,
需要注意的是,索引器可以具有多個引數,以便實作多維索引或具有其他復雜的訪問邏輯,此外,一個類可以定義多個索引器,只要它們的引數型別和個數不同即可,
運算子多載
不太常用
運算子多載(Operator Overloading)允許我們為自定義的類或結構體定義運算子的行為,通過運算子多載,我們可以對自定義型別的物件執行類似于內置型別的操作,使代碼更具表達力和易讀性,
在C#
中,可以對很多運算子進行多載,例如算術運算子(+、-、*、/等)、關系運算子(==、!=、>、<等)、邏輯運算子(&&、||等)等,
以下是一個簡單的示例,展示了如何多載等號運算子:
開始之前,先看一下傳統的==
運算子,定義一個Student
類:
class Student
{
private int age;
private string name;
private long id;
public Student(int age, string name, long id)
{
this.age = age;
this.name = name;
this.id = id;
}
}
創建n
個實體進行比較:
s1
和s2
一樣但是為什么不相等呢?這是因為==
運算子比較的兩個變數s1
和s2
分別是存盤的在堆中的s1
和s2
的記憶體地址,雖然內容一樣,但是它們指向的記憶體地址是不一樣,所以就出現了上面的輸出結果,
那如果想在判斷是否相等的時候判斷的時候比較的是存盤的欄位而不是變數地址怎么辦呢?這個時候就可以多載==
運算子:
internal class Student
{
private int age;
private string name;
private long id;
public Student(int age, string name, long id)
{
this.age = age;
this.name = name;
this.id = id;
}
public static bool operator==(Student a, Student b)
{
if (a.age == b.age && a.name == b.name && a.id == b.id) return true;
return false;
}
public static bool operator !=(Student a, Student b)
{
bool result = a == b; return result;
}
}
這個時候再執行上面的比較:

對于其他的運算子多載大家可自行嘗試,
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/553151.html
標籤:.NET技术
上一篇:C# 面向物件
下一篇:返回列表